Merge branch 'livekit' into app-selection

This commit is contained in:
Robin
2023-09-18 11:50:09 -04:00
34 changed files with 1418 additions and 1924 deletions

View File

@@ -26,7 +26,7 @@ jobs:
uses: actions/checkout@v2
- name: Log in to container registry
uses: docker/login-action@cf8514a65188af1d4f94f8c28a7a4153af1088ce
uses: docker/login-action@b4bedf8053341df3b5a9f9e0f2cf4e79e27360c6
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -54,7 +54,7 @@ jobs:
tar --numeric-owner --transform "s/dist/element-call-${TARBALL_VERSION}/" -cvzf element-call-${TARBALL_VERSION}.tar.gz dist
- name: Upload
uses: actions/upload-artifact@65d862660abb392b8c4a3d1195a2108db131dd05
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32
env:
GITHUB_TOKEN: ${{ github.token }}
with:
@@ -62,7 +62,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@0f8c876bafbf5dbce05c36682ec68e9a0274a48a
uses: docker/metadata-action@879dcbb708d40f8b8679d4f7941b938a086e23a7
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
@@ -72,10 +72,10 @@ jobs:
type=raw,value=latest-ci_${{steps.current-time.outputs.unix_time}},enable={{is_default_branch}}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55
uses: docker/setup-buildx-action@dedd61cf5d839122591f5027c89bf3ad27691d18
- name: Build and push Docker image
uses: docker/build-push-action@9311bf5263ae5b36f3ec67aff768790c6e2344ad
uses: docker/build-push-action@4c1b68d83ad20cc1a09620ca477d5bbbb5fa14d0
with:
context: .
platforms: linux/amd64,linux/arm64

View File

@@ -32,7 +32,6 @@
"@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",
@@ -42,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",
@@ -84,6 +82,7 @@
"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",

View File

@@ -35,7 +35,7 @@
"Microphone": "Mikrofon",
"More": "Mehr",
"No": "Nein",
"Not now, return to home screen": "Nicht jetzt, zurück zum Startbildschirm",
"Not now, return to home screen": "Nicht jetzt, zurück zur Startseite",
"Not registered yet? <2>Create an account</2>": "Noch nicht registriert? <2>Konto erstellen</2>",
"Password": "Passwort",
"Passwords must match": "Passwörter müssen übereinstimmen",
@@ -45,7 +45,7 @@
"Register": "Registrieren",
"Registering…": "Registrierung …",
"Remove": "Entfernen",
"Return to home screen": "Zurück zum Startbildschirm",
"Return to home screen": "Zurück zur Startseite",
"Select an option": "Wähle eine Option",
"Send debug logs": "Debug-Logs senden",
"Sending…": "Senden …",
@@ -71,7 +71,7 @@
"Your recent calls": "Deine letzten Anrufe",
"Walkie-talkie call name": "Name des Walkie-Talkie-Anrufs",
"Sending debug logs…": "Sende Debug-Protokolle …",
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Anruf beitreten</0><1>Oder</1><2>Anruflink kopieren und später beitreten</2>",
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Anruf beitreten</0><1>Oder</1><2>Link kopieren und später beitreten</2>",
"Copy": "Kopieren",
"Element Call Home": "Element Call-Startseite",
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Übermittelte Problemberichte helfen uns, Fehler zu beheben.</0>",
@@ -99,10 +99,25 @@
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>": "Diese Seite wird durch reCAPTCHA geschützt und es gelten Googles <2>Datenschutzerklärung</2> und <6>Nutzungsbedingungen</6>. <9></9>Mit einem Klick auf „Registrieren“ akzeptierst du unseren <2>Endbenutzer-Lizenzvertrag (EULA)</2>",
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call ist temporär nicht Ende-zu-Ende-verschlüsselt, während wir die Skalierbarkeit testen.",
"Connectivity to the server has been lost.": "Die Verbindung zum Server wurde getrennt.",
"Enable end-to-end encryption (password protected calls)": "Ende-zu-Ende-Verschlüsselung aktivieren (Passwort geschützte Anrufe)",
"Enable end-to-end encryption (password protected calls)": "Ende-zu-Ende-Verschlüsselung aktivieren (Passwortgeschützte Anrufe)",
"End-to-end encryption isn't supported on your browser.": "Ende-zu-Ende-Verschlüsselung wird in deinem Browser nicht unterstützt.",
"Thanks!": "Danke!",
"You were disconnected from the call": "Deine Verbindung wurde getrennt",
"Reconnect": "Erneut verbinden",
"Retry sending logs": "Protokolle erneut senden"
"Retry sending logs": "Protokolle erneut senden",
"Encrypted": "Verschlüsselt",
"End call": "Anruf beenden",
"Grid": "Raster",
"Not encrypted": "Nicht verschlüsselt",
"Microphone off": "Mikrofon aus",
"Microphone on": "Mikrofon an",
"{{count, number}}|one": "{{count, number}}",
"{{count, number}}|other": "{{count, number}}",
"{{names, list(style: short;)}}": "{{names, list(style: short;)}}",
"Share": "Teilen",
"Share this call": "Diesen Anruf teilen",
"Video off": "Video aus",
"Video on": "Video an",
"Sharing screen": "Bildschirm wird geteilt",
"You": "Du"
}

View File

@@ -118,5 +118,6 @@
"Video off": "Vidéo éteinte",
"Video on": "Vidéo allumée",
"{{count, number}}|one": "{{count, number}}",
"Not encrypted": "Non chiffré"
"Not encrypted": "Non chiffré",
"You": "Vous"
}

View File

@@ -1,7 +1,7 @@
{
"{{count, number}}|one": "{{count, number}}",
"{{count, number}}|other": "{{count, number}}",
"{{count}} stars|one": "{{count}} stella",
"{{count}} stars|one": "{{count}} stelle",
"{{count}} stars|other": "{{count}} stelle",
"{{displayName}} is presenting": "{{displayName}} sta presentando",
"{{names, list(style: short;)}}": "{{names, list(style: short;)}}",
@@ -118,5 +118,6 @@
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Un altro utente in questa chiamata sta avendo problemi. Per diagnosticare meglio questi problemi, vorremmo raccogliere un registro di debug.",
"End-to-end encryption isn't supported on your browser.": "La crittografia end-to-end non è supportata nel tuo browser.",
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Cliccando \"Entra in chiamata ora\", accetti il nostro <2>accordo di licenza con l'utente finale (EULA)</2>",
"By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.": "Partecipando a questa beta, acconsenti alla raccolta di dati anonimi che usiamo per migliorare il prodotto. Puoi trovare più informazioni su quali dati monitoriamo nella nostra <2>informativa sulla privacy</2> e nell'<5>informativa sui cookie</5>."
"By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.": "Partecipando a questa beta, acconsenti alla raccolta di dati anonimi che usiamo per migliorare il prodotto. Puoi trovare più informazioni su quali dati monitoriamo nella nostra <2>informativa sulla privacy</2> e nell'<5>informativa sui cookie</5>.",
"You": "Tu"
}

View File

@@ -87,7 +87,7 @@
"Submit": "Wyślij",
"Your feedback": "Twoje opinie",
"{{count}} stars|other": "{{count}} gwiazdki",
"{{count}} stars|one": "{{count}} gwiazdka",
"{{count}} stars|one": "{{count}} gwiazdki",
"{{displayName}}, your call has ended.": "{{displayName}}, Twoje połączenie zostało zakończone.",
"<0>Thanks for your feedback!</0>": "<0>Dziękujemy za Twoją opinię!</0>",
"<0>We'd love to hear your feedback so we can improve your experience.</0>": "<0>Z przyjemnością wysłuchamy Twojej opinii, aby poprawić Twoje doświadczenia.</0>",

View File

@@ -118,5 +118,6 @@
"Video on": "Video zapnuté",
"{{count, number}}|one": "{{count, number}}",
"Share this call": "Zdieľať tento hovor",
"{{names, list(style: short;)}}": "{{names, list(style: short;)}}"
"{{names, list(style: short;)}}": "{{names, list(style: short;)}}",
"You": "Vy"
}

View File

@@ -87,7 +87,7 @@
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Якщо у вас виникли проблеми або ви просто хочете залишити відгук, надішліть нам короткий опис нижче.",
"Feedback": "Відгук",
"<0>Thanks for your feedback!</0>": "<0>Дякуємо за ваш відгук!</0>",
"{{count}} stars|one": "{{count}} зірка",
"{{count}} stars|one": "{{count}} зірок",
"{{count}} stars|other": "{{count}} зірок",
"{{displayName}}, your call has ended.": "{{displayName}}, ваш виклик завершено.",
"<0>We'd love to hear your feedback so we can improve your experience.</0>": "<0>Ми будемо раді почути ваші відгуки, щоб поліпшити роботу застосунку.</0>",
@@ -118,5 +118,6 @@
"End call": "Завершити виклик",
"Grid": "Сітка",
"Microphone off": "Мікрофон вимкнено",
"Share this call": "Поділитися цим викликом"
"Share this call": "Поділитися цим викликом",
"You": "Ви"
}

View File

@@ -118,5 +118,6 @@
"Share this call": "分享此通話",
"Sharing screen": "分享畫面",
"Video off": "視訊關閉",
"Video on": "視訊開啟"
"Video on": "視訊開啟",
"You": "您"
}

View File

@@ -1,5 +1,5 @@
/*
Copyright 2022 New Vector Ltd
Copyright 2022 - 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
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);
}

View File

@@ -1,5 +1,5 @@
/*
Copyright 2022 New Vector Ltd
Copyright 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
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>
);
}
}

View File

@@ -1,217 +0,0 @@
/*
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.
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.
*/
.overlay {
position: fixed;
z-index: 100;
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 {
position: fixed;
z-index: 101;
display: flex;
flex-direction: column;
}
.dialog {
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
box-sizing: border-box;
inline-size: 520px;
max-inline-size: 90%;
max-block-size: 600px;
}
@keyframes zoom-in {
from {
opacity: 0;
transform: translate(-50%, -50%) scale(80%);
}
to {
opacity: 1;
transform: translate(-50%, -50%) scale(100%);
}
}
@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 {
display: flex;
flex-direction: column;
overflow: hidden;
}
.dialog .content {
flex-grow: 1;
background: var(--cpd-color-bg-canvas-default);
}
.drawer .content {
overflow: auto;
}
.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);
}

View File

@@ -1,141 +0,0 @@
/*
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 { 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 styles from "./NewModal.module.css";
import { useMediaQuery } from "./useMediaQuery";
import { Glass } from "./Glass";
// TODO: Support tabs
export interface ModalProps extends AriaDialogProps {
title: string;
children: ReactNode;
className?: string;
/**
* 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,
open,
onDismiss,
...rest
}: ModalProps) {
const { t } = useTranslation();
const touchscreen = useMediaQuery("(hover: none)");
const onOpenChange = useCallback(
(open: boolean) => {
if (!open) onDismiss?.();
},
[onDismiss]
);
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.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>
</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>
);
}
}

View File

@@ -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}
/>
)}
</>

View File

@@ -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>
);
}

View File

@@ -33,7 +33,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, Title } from "../typography/Typography";
import { Form } from "../form/Form";
@@ -56,7 +55,12 @@ export function RegisteredView({ client, isPasswordlessUser }: 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(
@@ -93,7 +97,7 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
setExistingAlias(roomAliasLocalpartFromRoomName(roomName));
setLoading(false);
setError(undefined);
modalState.open();
setJoinExistingCallModalOpen(true);
} else {
console.error(error);
setLoading(false);
@@ -101,7 +105,7 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
}
});
},
[client, history, modalState, callType, e2eeEnabled]
[client, history, setJoinExistingCallModalOpen, callType, e2eeEnabled]
);
const recentRooms = useGroupCallRooms(client);
@@ -175,9 +179,11 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
)}
</main>
</div>
{modalState.isOpen && (
<JoinExistingCallModal onJoin={onJoinExistingRoom} {...modalProps} />
)}
<JoinExistingCallModal
onJoin={onJoinExistingRoom}
open={joinExistingCallModalOpen}
onDismiss={onDismissJoinExistingCallModal}
/>
</>
);
}

View File

@@ -30,7 +30,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";
@@ -55,7 +54,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();
@@ -110,7 +114,7 @@ export const UnauthenticatedView: FC = () => {
});
setLoading(false);
modalState.open();
setJoinExistingCallModalOpen(true);
return;
} else {
throw error;
@@ -139,7 +143,7 @@ export const UnauthenticatedView: FC = () => {
execute,
history,
callType,
modalState,
setJoinExistingCallModalOpen,
setClient,
e2eeEnabled,
]
@@ -235,8 +239,12 @@ export const UnauthenticatedView: FC = () => {
</Body>
</footer>
</div>
{modalState.isOpen && onFinished && (
<JoinExistingCallModal onJoin={onFinished} {...modalProps} />
{onFinished && (
<JoinExistingCallModal
onJoin={onFinished}
open={joinExistingCallModalOpen}
onDismiss={onDismissJoinExistingCallModal}
/>
)}
</>
);

View File

@@ -160,7 +160,9 @@ body,
flex-direction: column;
}
/* On Android and iOS, prefer native system fonts */
/* 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;
}

View File

@@ -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]);

View File

@@ -17,8 +17,10 @@ limitations under the License.
import {
AudioCaptureOptions,
ConnectionState,
LocalTrackPublication,
Room,
RoomEvent,
Track,
} from "livekit-client";
import { useCallback, useEffect, useRef, useState } from "react";
import { logger } from "matrix-js-sdk/src/logger";
@@ -54,16 +56,24 @@ async function doConnect(
audioOptions: AudioCaptureOptions
): Promise<void> {
await livekitRoom!.connect(sfuConfig!.url, sfuConfig!.jwt);
const audioTracks = await livekitRoom!.localParticipant.createTracks({
audio: audioOptions,
const hasMicrophoneTrack = Array.from(
livekitRoom?.localParticipant.audioTracks.values()
).some((track: LocalTrackPublication) => {
return track.source == Track.Source.Microphone;
});
if (audioTracks.length < 1) {
logger.info("Tried to pre-create local audio track but got no tracks");
return;
}
if (!audioEnabled) await audioTracks[0].mute();
// We create a track in case there isn't any.
if (!hasMicrophoneTrack) {
const audioTracks = await livekitRoom!.localParticipant.createTracks({
audio: audioOptions,
});
if (audioTracks.length < 1) {
logger.info("Tried to pre-create local audio track but got no tracks");
return;
}
if (!audioEnabled) await audioTracks[0].mute();
await livekitRoom?.localParticipant.publishTrack(audioTracks[0]);
await livekitRoom?.localParticipant.publishTrack(audioTracks[0]);
}
}
export function useECConnectionState(

View File

@@ -19,7 +19,7 @@ import { useTranslation } from "react-i18next";
import { Button, Text } from "@vector-im/compound-web";
import { ReactComponent as PopOutIcon } from "@vector-im/compound-design-tokens/icons/pop-out.svg";
import { Modal } from "../NewModal";
import { Modal } from "../Modal";
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
import { getRoomUrl } from "../matrix-utils";
import styles from "./AppSelectionModal.module.css";

View File

@@ -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) {

View File

@@ -30,7 +30,6 @@ import { Room as MatrixRoom } from "matrix-js-sdk/src/models/room";
import { Ref, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { 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>
);
}

View File

@@ -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>
);
};

View File

@@ -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%;
}

View File

@@ -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>
);
};

View File

@@ -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>
);

View File

@@ -1,5 +1,5 @@
/*
Copyright 2022 New Vector Ltd
Copyright 2022 - 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
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;
}

View File

@@ -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}

View File

@@ -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,
};
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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

2023
yarn.lock

File diff suppressed because it is too large Load Diff