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-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
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");
|
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);
|
||||||
|
}
|
||||||
|
|||||||
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");
|
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
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 { 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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user