Merge branch 'livekit' into remove-walkie-talkie
This commit is contained in:
@@ -29,8 +29,9 @@
|
||||
"@opentelemetry/instrumentation-document-load": "^0.33.0",
|
||||
"@opentelemetry/instrumentation-user-interaction": "^0.33.0",
|
||||
"@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/dialog": "^3.1.4",
|
||||
"@react-aria/focus": "^3.5.0",
|
||||
"@react-aria/menu": "^3.3.0",
|
||||
"@react-aria/overlays": "^3.7.3",
|
||||
@@ -40,7 +41,6 @@
|
||||
"@react-aria/utils": "^3.10.0",
|
||||
"@react-spring/web": "^9.4.4",
|
||||
"@react-stately/collections": "^3.3.4",
|
||||
"@react-stately/overlays": "^3.1.3",
|
||||
"@react-stately/select": "^3.1.3",
|
||||
"@react-stately/tooltip": "^3.0.5",
|
||||
"@react-stately/tree": "^3.2.0",
|
||||
@@ -52,7 +52,6 @@
|
||||
"@vitejs/plugin-basic-ssl": "^1.0.1",
|
||||
"@vitejs/plugin-react": "^4.0.1",
|
||||
"classnames": "^2.3.1",
|
||||
"color-hash": "^2.0.1",
|
||||
"events": "^3.3.0",
|
||||
"i18next": "^21.10.0",
|
||||
"i18next-browser-languagedetector": "^6.1.8",
|
||||
@@ -77,11 +76,13 @@
|
||||
"sdp-transform": "^2.14.1",
|
||||
"tinyqueue": "^2.0.3",
|
||||
"unique-names-generator": "^4.6.0",
|
||||
"uuid": "9"
|
||||
"uuid": "9",
|
||||
"vaul": "^0.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.16.5",
|
||||
"@react-spring/rafz": "^9.7.3",
|
||||
"@react-types/dialog": "^3.5.5",
|
||||
"@sentry/vite-plugin": "^0.3.0",
|
||||
"@storybook/react": "^6.5.0-alpha.5",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
|
||||
31
src/Glass.module.css
Normal file
31
src/Glass.module.css
Normal 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
52
src/Glass.tsx
Normal 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>
|
||||
)
|
||||
);
|
||||
@@ -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");
|
||||
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.
|
||||
*/
|
||||
|
||||
.modalOverlay {
|
||||
.overlay {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background: rgba(23, 25, 28, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
inset: 0;
|
||||
background: rgba(3, 12, 27, 0.528);
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
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 {
|
||||
background: var(--cpd-color-bg-subtle-secondary);
|
||||
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 8px;
|
||||
max-width: 90vw;
|
||||
width: 600px;
|
||||
position: fixed;
|
||||
z-index: 101;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modalHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 34px 32px 0 32px;
|
||||
.dialog {
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
box-sizing: border-box;
|
||||
inline-size: 520px;
|
||||
max-inline-size: 90%;
|
||||
max-block-size: 600px;
|
||||
}
|
||||
|
||||
.modalHeader h3 {
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-title);
|
||||
margin: 0;
|
||||
@keyframes zoom-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(80%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
@keyframes zoom-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(100%);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(80%);
|
||||
}
|
||||
}
|
||||
|
||||
.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 {
|
||||
padding: 24px 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content p {
|
||||
margin-top: 0;
|
||||
.dialog .content {
|
||||
flex-grow: 1;
|
||||
background: var(--cpd-color-bg-canvas-default);
|
||||
}
|
||||
|
||||
@media (max-width: 799px) {
|
||||
.modalHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 32px 20px 0 20px;
|
||||
}
|
||||
.drawer .content {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.modal.mobileFullScreen {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
border-radius: 0;
|
||||
.drawer {
|
||||
background: var(--cpd-color-bg-canvas-default);
|
||||
inset-block-end: 0;
|
||||
inset-inline: max(0px, calc((100% - 520px) / 2));
|
||||
max-block-size: 90%;
|
||||
border-start-start-radius: 20px;
|
||||
border-start-end-radius: 20px;
|
||||
/* Drawer handle comes in the Android style by default */
|
||||
--handle-block-size: 4px;
|
||||
--handle-inline-size: 32px;
|
||||
--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);
|
||||
}
|
||||
|
||||
203
src/Modal.tsx
203
src/Modal.tsx
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 New Vector Ltd
|
||||
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.
|
||||
@@ -14,123 +14,128 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable jsx-a11y/no-autofocus */
|
||||
|
||||
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 { ReactNode, useCallback } from "react";
|
||||
import { AriaDialogProps } from "@react-types/dialog";
|
||||
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 "./Modal.module.css";
|
||||
import styles from "./NewModal.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;
|
||||
children: ReactNode;
|
||||
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({
|
||||
title,
|
||||
children,
|
||||
className,
|
||||
mobileFullScreen,
|
||||
onClose,
|
||||
open,
|
||||
onDismiss,
|
||||
...rest
|
||||
}: ModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const modalRef = useRef(null);
|
||||
const { overlayProps, underlayProps } = useOverlay(
|
||||
{ ...rest, onClose },
|
||||
modalRef
|
||||
);
|
||||
usePreventScroll();
|
||||
const { modalProps } = useModal();
|
||||
const { dialogProps, titleProps } = useDialog(rest, modalRef);
|
||||
const closeButtonRef = useRef(null);
|
||||
const { buttonProps: closeButtonProps } = useButton(
|
||||
{
|
||||
onPress: () => onClose(),
|
||||
const touchscreen = useMediaQuery("(hover: none)");
|
||||
const onOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
if (!open) onDismiss?.();
|
||||
},
|
||||
closeButtonRef
|
||||
[onDismiss]
|
||||
);
|
||||
|
||||
return (
|
||||
<OverlayContainer>
|
||||
<div className={styles.modalOverlay} {...underlayProps}>
|
||||
<FocusScope contain restoreFocus autoFocus>
|
||||
<div
|
||||
{...overlayProps}
|
||||
{...dialogProps}
|
||||
{...modalProps}
|
||||
ref={modalRef}
|
||||
className={classNames(
|
||||
styles.modal,
|
||||
{ [styles.mobileFullScreen]: mobileFullScreen },
|
||||
className
|
||||
)}
|
||||
if (touchscreen) {
|
||||
return (
|
||||
<Drawer.Root
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
dismissible={onDismiss !== undefined}
|
||||
>
|
||||
<Drawer.Portal>
|
||||
<Drawer.Overlay className={styles.overlay} />
|
||||
<Drawer.Content
|
||||
className={classNames(className, styles.modal, styles.drawer)}
|
||||
{...rest}
|
||||
>
|
||||
<div className={styles.modalHeader}>
|
||||
<h3 {...titleProps}>{title}</h3>
|
||||
<button
|
||||
{...closeButtonProps}
|
||||
ref={closeButtonRef}
|
||||
className={styles.closeButton}
|
||||
data-testid="modal_close"
|
||||
title={t("Close")}
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.handle} />
|
||||
<VisuallyHidden asChild>
|
||||
<Drawer.Title>{title}</Drawer.Title>
|
||||
</VisuallyHidden>
|
||||
</div>
|
||||
<div className={styles.body}>{children}</div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</FocusScope>
|
||||
</div>
|
||||
</OverlayContainer>
|
||||
);
|
||||
}
|
||||
|
||||
interface ModalContentProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ModalContent({
|
||||
children,
|
||||
className,
|
||||
...rest
|
||||
}: ModalContentProps) {
|
||||
return (
|
||||
<div className={classNames(styles.content, className)} {...rest}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function useModalTriggerState(): {
|
||||
modalState: OverlayTriggerState;
|
||||
modalProps: { isOpen: boolean; onClose: () => void };
|
||||
} {
|
||||
const modalState = useOverlayTriggerState({});
|
||||
const modalProps = useMemo(
|
||||
() => ({ isOpen: modalState.isOpen, onClose: modalState.close }),
|
||||
[modalState]
|
||||
);
|
||||
return { modalState, modalProps };
|
||||
</Drawer.Content>
|
||||
</Drawer.Portal>
|
||||
</Drawer.Root>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<DialogRoot open={open} onOpenChange={onOpenChange}>
|
||||
<DialogPortal>
|
||||
<DialogOverlay
|
||||
className={classNames(styles.overlay, styles.dialogOverlay)}
|
||||
/>
|
||||
<DialogContent asChild {...rest}>
|
||||
<Glass
|
||||
frosted
|
||||
className={classNames(className, styles.modal, styles.dialog)}
|
||||
>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.header}>
|
||||
<DialogTitle asChild>
|
||||
<Heading as="h2" weight="semibold" size="md">
|
||||
{title}
|
||||
</Heading>
|
||||
</DialogTitle>
|
||||
{onDismiss !== undefined && (
|
||||
<DialogClose
|
||||
className={styles.close}
|
||||
data-testid="modal_close"
|
||||
aria-label={t("Close")}
|
||||
>
|
||||
<CloseIcon width={20} height={20} />
|
||||
</DialogClose>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.body}>{children}</div>
|
||||
</div>
|
||||
</Glass>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</DialogRoot>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
38
src/Platform.ts
Normal file
38
src/Platform.ts
Normal 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";
|
||||
}
|
||||
@@ -19,7 +19,6 @@ import { useHistory, useLocation } from "react-router-dom";
|
||||
|
||||
import { useClientLegacy } from "./ClientContext";
|
||||
import { useProfile } from "./profile/useProfile";
|
||||
import { useModalTriggerState } from "./Modal";
|
||||
import { SettingsModal } from "./settings/SettingsModal";
|
||||
import { UserMenu } from "./UserMenu";
|
||||
|
||||
@@ -32,7 +31,11 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
|
||||
const history = useHistory();
|
||||
const { client, logout, authenticated, passwordlessUser } = useClientLegacy();
|
||||
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>();
|
||||
|
||||
@@ -41,11 +44,11 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
|
||||
switch (value) {
|
||||
case "user":
|
||||
setDefaultSettingsTab("profile");
|
||||
modalState.open();
|
||||
setSettingsModalOpen(true);
|
||||
break;
|
||||
case "settings":
|
||||
setDefaultSettingsTab("audio");
|
||||
modalState.open();
|
||||
setSettingsModalOpen(true);
|
||||
break;
|
||||
case "logout":
|
||||
logout?.();
|
||||
@@ -55,7 +58,7 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
|
||||
break;
|
||||
}
|
||||
},
|
||||
[history, location, logout, modalState]
|
||||
[history, location, logout, setSettingsModalOpen]
|
||||
);
|
||||
|
||||
const userName = client?.getUserIdLocalpart() ?? "";
|
||||
@@ -70,11 +73,12 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
|
||||
userId={client?.getUserId() ?? ""}
|
||||
displayName={displayName || (userName ? userName.replace("@", "") : "")}
|
||||
/>
|
||||
{modalState.isOpen && client && (
|
||||
{client && (
|
||||
<SettingsModal
|
||||
client={client}
|
||||
defaultTab={defaultSettingsTab}
|
||||
{...modalProps}
|
||||
open={settingsModalOpen}
|
||||
onDismiss={onDismissSettingsModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -17,36 +17,29 @@ limitations under the License.
|
||||
import { PressEvent } from "@react-types/shared";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Modal, ModalContent } from "../Modal";
|
||||
import { Modal } from "../Modal";
|
||||
import { Button } from "../button";
|
||||
import { FieldRow } from "../input/Input";
|
||||
import styles from "./JoinExistingCallModal.module.css";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onDismiss: () => 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();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t("Join existing call?")}
|
||||
isDismissable
|
||||
{...rest}
|
||||
onClose={onClose}
|
||||
>
|
||||
<ModalContent>
|
||||
<p>{t("This call already exists, would you like to join?")}</p>
|
||||
<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 title={t("Join existing call?")} open={open} onDismiss={onDismiss}>
|
||||
<p>{t("This call already exists, would you like to join?")}</p>
|
||||
<FieldRow rightAlign className={styles.buttons}>
|
||||
<Button onPress={onDismiss}>{t("No")}</Button>
|
||||
<Button onPress={onJoin} data-testid="home_joinExistingRoom">
|
||||
{t("Yes, join call")}
|
||||
</Button>
|
||||
</FieldRow>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ import { FieldRow, InputField, ErrorMessage } from "../input/Input";
|
||||
import { Button } from "../button";
|
||||
import { CallList } from "./CallList";
|
||||
import { UserMenuContainer } from "../UserMenuContainer";
|
||||
import { useModalTriggerState } from "../Modal";
|
||||
import { JoinExistingCallModal } from "./JoinExistingCallModal";
|
||||
import { Caption } from "../typography/Typography";
|
||||
import { Form } from "../form/Form";
|
||||
@@ -54,7 +53,12 @@ export function RegisteredView({ client }: Props) {
|
||||
const [optInAnalytics] = useOptInAnalytics();
|
||||
const history = useHistory();
|
||||
const { t } = useTranslation();
|
||||
const { modalState, modalProps } = useModalTriggerState();
|
||||
const [joinExistingCallModalOpen, setJoinExistingCallModalOpen] =
|
||||
useState(false);
|
||||
const onDismissJoinExistingCallModal = useCallback(
|
||||
() => setJoinExistingCallModalOpen(false),
|
||||
[setJoinExistingCallModalOpen]
|
||||
);
|
||||
const [e2eeEnabled] = useEnableE2EE();
|
||||
|
||||
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
|
||||
@@ -90,7 +94,7 @@ export function RegisteredView({ client }: Props) {
|
||||
setExistingAlias(roomAliasLocalpartFromRoomName(roomName));
|
||||
setLoading(false);
|
||||
setError(undefined);
|
||||
modalState.open();
|
||||
setJoinExistingCallModalOpen(true);
|
||||
} else {
|
||||
console.error(error);
|
||||
setLoading(false);
|
||||
@@ -98,7 +102,7 @@ export function RegisteredView({ client }: Props) {
|
||||
}
|
||||
});
|
||||
},
|
||||
[client, history, modalState, e2eeEnabled]
|
||||
[client, history, setJoinExistingCallModalOpen, e2eeEnabled]
|
||||
);
|
||||
|
||||
const recentRooms = useGroupCallRooms(client);
|
||||
@@ -164,9 +168,11 @@ export function RegisteredView({ client }: Props) {
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
{modalState.isOpen && (
|
||||
<JoinExistingCallModal onJoin={onJoinExistingRoom} {...modalProps} />
|
||||
)}
|
||||
<JoinExistingCallModal
|
||||
onJoin={onJoinExistingRoom}
|
||||
open={joinExistingCallModalOpen}
|
||||
onDismiss={onDismissJoinExistingCallModal}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
sanitiseRoomNameInput,
|
||||
} from "../matrix-utils";
|
||||
import { useInteractiveRegistration } from "../auth/useInteractiveRegistration";
|
||||
import { useModalTriggerState } from "../Modal";
|
||||
import { JoinExistingCallModal } from "./JoinExistingCallModal";
|
||||
import { useRecaptcha } from "../auth/useRecaptcha";
|
||||
import { Body, Caption, Link } from "../typography/Typography";
|
||||
@@ -54,7 +53,12 @@ export const UnauthenticatedView: FC = () => {
|
||||
const { recaptchaKey, register } = useInteractiveRegistration();
|
||||
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 history = useHistory();
|
||||
const { t } = useTranslation();
|
||||
@@ -108,7 +112,7 @@ export const UnauthenticatedView: FC = () => {
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
modalState.open();
|
||||
setJoinExistingCallModalOpen(true);
|
||||
return;
|
||||
} else {
|
||||
throw error;
|
||||
@@ -131,7 +135,15 @@ export const UnauthenticatedView: FC = () => {
|
||||
reset();
|
||||
});
|
||||
},
|
||||
[register, reset, execute, history, modalState, setClient, e2eeEnabled]
|
||||
[
|
||||
register,
|
||||
reset,
|
||||
execute,
|
||||
history,
|
||||
setJoinExistingCallModalOpen,
|
||||
setClient,
|
||||
e2eeEnabled,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -221,8 +233,12 @@ export const UnauthenticatedView: FC = () => {
|
||||
</Body>
|
||||
</footer>
|
||||
</div>
|
||||
{modalState.isOpen && onFinished && (
|
||||
<JoinExistingCallModal onJoin={onFinished} {...modalProps} />
|
||||
{onFinished && (
|
||||
<JoinExistingCallModal
|
||||
onJoin={onFinished}
|
||||
open={joinExistingCallModalOpen}
|
||||
onDismiss={onDismissJoinExistingCallModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -25,12 +25,6 @@ limitations under the License.
|
||||
@import "@vector-im/compound-web/dist/style.css";
|
||||
|
||||
: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-size-micro: calc(10px * var(--font-scale));
|
||||
--font-size-caption: calc(12px * var(--font-scale));
|
||||
@@ -149,7 +143,6 @@ body {
|
||||
color: var(--cpd-color-text-primary);
|
||||
color-scheme: dark;
|
||||
margin: 0;
|
||||
font-family: var(--font-family);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
@@ -157,7 +150,9 @@ body {
|
||||
html,
|
||||
body,
|
||||
#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 {
|
||||
@@ -165,6 +160,21 @@ body,
|
||||
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,
|
||||
h2,
|
||||
h3,
|
||||
|
||||
@@ -24,6 +24,7 @@ import * as Sentry from "@sentry/react";
|
||||
import { getUrlParams } from "./UrlParams";
|
||||
import { Config } from "./config/Config";
|
||||
import { ElementCallOpenTelemetry } from "./otel/otel";
|
||||
import { platform } from "./Platform";
|
||||
|
||||
enum LoadState {
|
||||
None,
|
||||
@@ -107,6 +108,9 @@ export class Initializer {
|
||||
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 {
|
||||
|
||||
@@ -200,8 +200,10 @@ export const useMediaDevices = () => useContext(MediaDevicesContext);
|
||||
* default because it may involve requesting additional permissions from the
|
||||
* user.
|
||||
*/
|
||||
export const useMediaDeviceNames = (context: MediaDevices) =>
|
||||
export const useMediaDeviceNames = (context: MediaDevices, enabled = true) =>
|
||||
useEffect(() => {
|
||||
context.startUsingDeviceNames();
|
||||
return context.stopUsingDeviceNames;
|
||||
}, [context]);
|
||||
if (enabled) {
|
||||
context.startUsingDeviceNames();
|
||||
return context.stopUsingDeviceNames;
|
||||
}
|
||||
}, [context, enabled]);
|
||||
|
||||
@@ -45,7 +45,6 @@ import {
|
||||
import { useEnableE2EE } from "../settings/useSetting";
|
||||
import { useRoomAvatar } from "./useRoomAvatar";
|
||||
import { useRoomName } from "./useRoomName";
|
||||
import { useModalTriggerState } from "../Modal";
|
||||
import { useJoinRule } from "./useJoinRule";
|
||||
import { ShareModal } from "./ShareModal";
|
||||
|
||||
@@ -286,12 +285,15 @@ export function GroupCallView({
|
||||
|
||||
const joinRule = useJoinRule(rtcSession.room);
|
||||
|
||||
const { modalState: shareModalState, modalProps: shareModalProps } =
|
||||
useModalTriggerState();
|
||||
const [shareModalOpen, setShareModalOpen] = useState(false);
|
||||
const onDismissShareModal = useCallback(
|
||||
() => setShareModalOpen(false),
|
||||
[setShareModalOpen]
|
||||
);
|
||||
|
||||
const onShareClickFn = useCallback(
|
||||
() => shareModalState.open(),
|
||||
[shareModalState]
|
||||
() => setShareModalOpen(true),
|
||||
[setShareModalOpen]
|
||||
);
|
||||
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.")} />;
|
||||
}
|
||||
|
||||
const shareModal = shareModalState.isOpen && (
|
||||
<ShareModal roomId={rtcSession.room.roomId} {...shareModalProps} />
|
||||
const shareModal = (
|
||||
<ShareModal
|
||||
roomId={rtcSession.room.roomId}
|
||||
open={shareModalOpen}
|
||||
onDismiss={onDismissShareModal}
|
||||
/>
|
||||
);
|
||||
|
||||
if (isJoined) {
|
||||
|
||||
@@ -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 { useTranslation } from "react-i18next";
|
||||
import useMeasure from "react-use-measure";
|
||||
import { OverlayTriggerState } from "@react-stately/overlays";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||
|
||||
@@ -51,7 +50,6 @@ import {
|
||||
VideoGrid,
|
||||
} from "../video-grid/VideoGrid";
|
||||
import { useShowConnectionStats } from "../settings/useSetting";
|
||||
import { useModalTriggerState } from "../Modal";
|
||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||
import { useUrlParams } from "../UrlParams";
|
||||
import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
|
||||
@@ -313,25 +311,20 @@ export function InCallView({
|
||||
);
|
||||
};
|
||||
|
||||
const {
|
||||
modalState: rageshakeRequestModalState,
|
||||
modalProps: rageshakeRequestModalProps,
|
||||
} = useRageshakeRequestModal(rtcSession.room.roomId);
|
||||
const rageshakeRequestModalProps = useRageshakeRequestModal(
|
||||
rtcSession.room.roomId
|
||||
);
|
||||
|
||||
const {
|
||||
modalState: settingsModalState,
|
||||
modalProps: settingsModalProps,
|
||||
}: {
|
||||
modalState: OverlayTriggerState;
|
||||
modalProps: {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
} = useModalTriggerState();
|
||||
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
|
||||
|
||||
const openSettings = useCallback(() => {
|
||||
settingsModalState.open();
|
||||
}, [settingsModalState]);
|
||||
const openSettings = useCallback(
|
||||
() => setSettingsModalOpen(true),
|
||||
[setSettingsModalOpen]
|
||||
);
|
||||
const closeSettings = useCallback(
|
||||
() => setSettingsModalOpen(false),
|
||||
[setSettingsModalOpen]
|
||||
);
|
||||
|
||||
const toggleScreensharing = useCallback(async () => {
|
||||
exitFullscreen();
|
||||
@@ -442,19 +435,13 @@ export function InCallView({
|
||||
show={showInspector}
|
||||
/>
|
||||
)*/}
|
||||
{rageshakeRequestModalState.isOpen && !noControls && (
|
||||
<RageshakeRequestModal
|
||||
{...rageshakeRequestModalProps}
|
||||
roomId={rtcSession.room.roomId}
|
||||
/>
|
||||
)}
|
||||
{settingsModalState.isOpen && (
|
||||
<SettingsModal
|
||||
client={client}
|
||||
roomId={rtcSession.room.roomId}
|
||||
{...settingsModalProps}
|
||||
/>
|
||||
)}
|
||||
{!noControls && <RageshakeRequestModal {...rageshakeRequestModalProps} />}
|
||||
<SettingsModal
|
||||
client={client}
|
||||
roomId={rtcSession.room.roomId}
|
||||
open={settingsModalOpen}
|
||||
onDismiss={closeSettings}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ limitations under the License.
|
||||
import { FC, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Modal, ModalContent, ModalProps } from "../Modal";
|
||||
import { Modal, ModalProps } from "../Modal";
|
||||
import { Button } from "../button";
|
||||
import { FieldRow, ErrorMessage } from "../input/Input";
|
||||
import { useSubmitRageshake } from "../settings/submit-rageshake";
|
||||
@@ -26,51 +26,49 @@ import { Body } from "../typography/Typography";
|
||||
interface Props extends Omit<ModalProps, "title" | "children"> {
|
||||
rageshakeRequestId: string;
|
||||
roomId: string;
|
||||
onClose: () => void;
|
||||
open: boolean;
|
||||
onDismiss: () => void;
|
||||
}
|
||||
|
||||
export const RageshakeRequestModal: FC<Props> = ({
|
||||
rageshakeRequestId,
|
||||
roomId,
|
||||
...rest
|
||||
open,
|
||||
onDismiss,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
|
||||
|
||||
useEffect(() => {
|
||||
if (sent) {
|
||||
rest.onClose();
|
||||
}
|
||||
}, [sent, rest]);
|
||||
if (sent) onDismiss();
|
||||
}, [sent, onDismiss]);
|
||||
|
||||
return (
|
||||
<Modal title={t("Debug log request")} isDismissable {...rest}>
|
||||
<ModalContent>
|
||||
<Body>
|
||||
{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."
|
||||
)}
|
||||
</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 title={t("Debug log request")} open={open} onDismiss={onDismiss}>
|
||||
<Body>
|
||||
{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."
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,10 +14,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.inviteModal {
|
||||
max-width: 413px;
|
||||
}
|
||||
|
||||
.copyButton {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -17,35 +17,30 @@ limitations under the License.
|
||||
import { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Modal, ModalContent, ModalProps } from "../Modal";
|
||||
import { Modal } from "../Modal";
|
||||
import { CopyButton } from "../button";
|
||||
import { getRoomUrl } from "../matrix-utils";
|
||||
import styles from "./ShareModal.module.css";
|
||||
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
|
||||
|
||||
interface Props extends Omit<ModalProps, "title" | "children"> {
|
||||
interface Props {
|
||||
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 roomSharedKey = useRoomSharedKey(roomId);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t("Share this call")}
|
||||
isDismissable
|
||||
className={styles.inviteModal}
|
||||
{...rest}
|
||||
>
|
||||
<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 title={t("Share this call")} open={open} onDismiss={onDismiss}>
|
||||
<p>{t("Copy and share this call link")}</p>
|
||||
<CopyButton
|
||||
className={styles.copyButton}
|
||||
value={getRoomUrl(roomId, roomSharedKey ?? undefined)}
|
||||
data-testid="modal_inviteLink"
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,10 +14,9 @@ See the License for the specific language governing permissions and
|
||||
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 { ResizeObserver } from "@juggle/resize-observer";
|
||||
import { OverlayTriggerState } from "@react-stately/overlays";
|
||||
import { usePreviewTracks } from "@livekit/components-react";
|
||||
import {
|
||||
CreateLocalTracksOptions,
|
||||
@@ -28,7 +27,6 @@ import {
|
||||
import { MicButton, SettingsButton, VideoButton } from "../button";
|
||||
import { Avatar } from "../Avatar";
|
||||
import styles from "./VideoPreview.module.css";
|
||||
import { useModalTriggerState } from "../Modal";
|
||||
import { SettingsModal } from "../settings/SettingsModal";
|
||||
import { useClient } from "../ClientContext";
|
||||
import { useMediaDevices } from "../livekit/MediaDevicesContext";
|
||||
@@ -54,20 +52,16 @@ export const VideoPreview: FC<Props> = ({ matrixInfo, muteStates }) => {
|
||||
const { client } = useClient();
|
||||
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
|
||||
|
||||
const {
|
||||
modalState: settingsModalState,
|
||||
modalProps: settingsModalProps,
|
||||
}: {
|
||||
modalState: OverlayTriggerState;
|
||||
modalProps: {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
} = useModalTriggerState();
|
||||
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
|
||||
|
||||
const openSettings = useCallback(() => {
|
||||
settingsModalState.open();
|
||||
}, [settingsModalState]);
|
||||
const openSettings = useCallback(
|
||||
() => setSettingsModalOpen(true),
|
||||
[setSettingsModalOpen]
|
||||
);
|
||||
const closeSettings = useCallback(
|
||||
() => setSettingsModalOpen(false),
|
||||
[setSettingsModalOpen]
|
||||
);
|
||||
|
||||
const devices = useMediaDevices();
|
||||
|
||||
@@ -153,8 +147,12 @@ export const VideoPreview: FC<Props> = ({ matrixInfo, muteStates }) => {
|
||||
<SettingsButton onPress={openSettings} />
|
||||
</div>
|
||||
</>
|
||||
{settingsModalState.isOpen && client && (
|
||||
<SettingsModal client={client} {...settingsModalProps} />
|
||||
{client && (
|
||||
<SettingsModal
|
||||
client={client}
|
||||
open={settingsModalOpen}
|
||||
onDismiss={closeSettings}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -16,21 +16,7 @@ limitations under the License.
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
// https://stackoverflow.com/a/9039885
|
||||
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)
|
||||
);
|
||||
}
|
||||
import { platform } from "../Platform";
|
||||
|
||||
export function usePageUnload(callback: () => void) {
|
||||
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.
|
||||
if (isIOS()) {
|
||||
if (platform === "ios") {
|
||||
window.addEventListener("pagehide", onBeforeUnload);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
|
||||
@@ -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");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -15,18 +15,13 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
.settingsModal {
|
||||
width: 774px;
|
||||
height: 480px;
|
||||
block-size: 550px;
|
||||
}
|
||||
|
||||
.settingsModal p {
|
||||
color: var(--cpd-color-text-secondary);
|
||||
}
|
||||
|
||||
.tabContainer {
|
||||
padding: 27px 20px;
|
||||
}
|
||||
|
||||
.fieldRowText {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -51,11 +51,11 @@ import {
|
||||
} from "../livekit/MediaDevicesContext";
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
open: boolean;
|
||||
onDismiss: () => void;
|
||||
client: MatrixClient;
|
||||
roomId?: string;
|
||||
defaultTab?: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const SettingsModal = (props: Props) => {
|
||||
@@ -119,7 +119,7 @@ export const SettingsModal = (props: Props) => {
|
||||
);
|
||||
|
||||
const devices = useMediaDevices();
|
||||
useMediaDeviceNames(devices);
|
||||
useMediaDeviceNames(devices, props.open);
|
||||
|
||||
const audioTab = (
|
||||
<TabItem
|
||||
@@ -289,10 +289,9 @@ export const SettingsModal = (props: Props) => {
|
||||
return (
|
||||
<Modal
|
||||
title={t("Settings")}
|
||||
isDismissable
|
||||
mobileFullScreen
|
||||
className={styles.settingsModal}
|
||||
{...props}
|
||||
open={props.open}
|
||||
onDismiss={props.onDismiss}
|
||||
>
|
||||
<TabContainer
|
||||
onSelectionChange={onSelectedTabChanged}
|
||||
|
||||
@@ -14,20 +14,25 @@ See the License for the specific language governing permissions and
|
||||
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
|
||||
// @ts-ignore
|
||||
import pako from "pako";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { OverlayTriggerState } from "@react-stately/overlays";
|
||||
import { ClientEvent } from "matrix-js-sdk/src/client";
|
||||
|
||||
import { getLogsForReport } from "./rageshake";
|
||||
import { useClient } from "../ClientContext";
|
||||
import { InspectorContext } from "../room/GroupCallInspector";
|
||||
import { useModalTriggerState } from "../Modal";
|
||||
import { Config } from "../config/Config";
|
||||
import { ElementCallOpenTelemetry } from "../otel/otel";
|
||||
import { RageshakeRequestModal } from "../room/RageshakeRequestModal";
|
||||
|
||||
const gzip = (text: string): Blob => {
|
||||
// encode as UTF-8
|
||||
@@ -343,22 +348,12 @@ export function useRageshakeRequest(): (
|
||||
|
||||
return sendRageshakeRequest;
|
||||
}
|
||||
interface ModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
interface ModalPropsWithId extends ModalProps {
|
||||
rageshakeRequestId: string;
|
||||
}
|
||||
|
||||
export function useRageshakeRequestModal(roomId: string): {
|
||||
modalState: OverlayTriggerState;
|
||||
modalProps: ModalPropsWithId;
|
||||
} {
|
||||
const { modalState, modalProps } = useModalTriggerState() as {
|
||||
modalState: OverlayTriggerState;
|
||||
modalProps: ModalProps;
|
||||
};
|
||||
export function useRageshakeRequestModal(
|
||||
roomId: string
|
||||
): ComponentProps<typeof RageshakeRequestModal> {
|
||||
const [open, setOpen] = useState(false);
|
||||
const onDismiss = useCallback(() => setOpen(false), [setOpen]);
|
||||
const { client } = useClient();
|
||||
const [rageshakeRequestId, setRageshakeRequestId] = useState<string>();
|
||||
|
||||
@@ -374,7 +369,7 @@ export function useRageshakeRequestModal(roomId: string): {
|
||||
client.getUserId() !== event.getSender()
|
||||
) {
|
||||
setRageshakeRequestId(event.getContent().request_id);
|
||||
modalState.open();
|
||||
setOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -383,10 +378,12 @@ export function useRageshakeRequestModal(roomId: string): {
|
||||
return () => {
|
||||
client.removeListener(ClientEvent.Event, onEvent);
|
||||
};
|
||||
}, [modalState.open, roomId, client, modalState]);
|
||||
}, [setOpen, roomId, client]);
|
||||
|
||||
return {
|
||||
modalState,
|
||||
modalProps: { ...modalProps, rageshakeRequestId: rageshakeRequestId ?? "" },
|
||||
rageshakeRequestId: rageshakeRequestId ?? "",
|
||||
roomId,
|
||||
open,
|
||||
onDismiss,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -78,32 +78,3 @@ limitations under the License.
|
||||
padding: 0;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,13 @@ See the License for the specific language governing permissions and
|
||||
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 classNames from "classnames";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -36,7 +42,6 @@ import { Avatar } from "../Avatar";
|
||||
import styles from "./VideoTile.module.css";
|
||||
import { useReactiveState } from "../useReactiveState";
|
||||
import { AudioButton, FullscreenButton } from "../button/Button";
|
||||
import { useModalTriggerState } from "../Modal";
|
||||
import { VideoTileSettingsModal } from "./VideoTileSettingsModal";
|
||||
|
||||
export interface ItemData {
|
||||
@@ -117,11 +122,16 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
||||
onToggleFullscreen(data.id);
|
||||
}, [data, onToggleFullscreen]);
|
||||
|
||||
const {
|
||||
modalState: videoTileSettingsModalState,
|
||||
modalProps: videoTileSettingsModalProps,
|
||||
} = useModalTriggerState();
|
||||
const onOptionsPress = videoTileSettingsModalState.open;
|
||||
const [videoTileSettingsModalOpen, setVideoTileSettingsModalOpen] =
|
||||
useState(false);
|
||||
const openVideoTileSettingsModal = useCallback(
|
||||
() => setVideoTileSettingsModalOpen(true),
|
||||
[setVideoTileSettingsModalOpen]
|
||||
);
|
||||
const closeVideoTileSettingsModal = useCallback(
|
||||
() => setVideoTileSettingsModalOpen(false),
|
||||
[setVideoTileSettingsModalOpen]
|
||||
);
|
||||
|
||||
const toolbarButtons: JSX.Element[] = [];
|
||||
if (!sfuParticipant.isLocal) {
|
||||
@@ -130,7 +140,7 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
||||
key="localVolume"
|
||||
className={styles.button}
|
||||
volume={(sfuParticipant as RemoteParticipant).getVolume() ?? 0}
|
||||
onPress={onOptionsPress}
|
||||
onPress={openVideoTileSettingsModal}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -208,10 +218,11 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
||||
: Track.Source.ScreenShare
|
||||
}
|
||||
/>
|
||||
{videoTileSettingsModalState.isOpen && !maximised && (
|
||||
{!maximised && (
|
||||
<VideoTileSettingsModal
|
||||
{...videoTileSettingsModalProps}
|
||||
data={data}
|
||||
open={videoTileSettingsModalOpen}
|
||||
onDismiss={closeVideoTileSettingsModal}
|
||||
/>
|
||||
)}
|
||||
</animated.div>
|
||||
|
||||
@@ -66,23 +66,21 @@ const LocalVolume: React.FC<LocalVolumeProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
// TODO: Extend ModalProps
|
||||
interface Props {
|
||||
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();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={styles.videoTileSettingsModal}
|
||||
title={t("Local volume")}
|
||||
isDismissable
|
||||
mobileFullScreen
|
||||
onClose={onClose}
|
||||
{...rest}
|
||||
open={open}
|
||||
onDismiss={onDismiss}
|
||||
>
|
||||
<div className={styles.content}>
|
||||
<LocalVolume
|
||||
|
||||
Reference in New Issue
Block a user