Merge branch 'livekit' into app-selection
This commit is contained in:
10
.github/workflows/publish.yaml
vendored
10
.github/workflows/publish.yaml
vendored
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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>",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "Ви"
|
||||
}
|
||||
|
||||
@@ -118,5 +118,6 @@
|
||||
"Share this call": "分享此通話",
|
||||
"Sharing screen": "分享畫面",
|
||||
"Video off": "視訊關閉",
|
||||
"Video on": "視訊開啟"
|
||||
"Video on": "視訊開啟",
|
||||
"You": "您"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 New Vector Ltd
|
||||
Copyright 2022 - 2023 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -14,77 +14,204 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.modalOverlay {
|
||||
.overlay {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background: rgba(23, 25, 28, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
inset: 0;
|
||||
background: rgba(3, 12, 27, 0.528);
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.dialogOverlay[data-state="open"] {
|
||||
animation: fade-in 200ms;
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dialogOverlay[data-state="closed"] {
|
||||
animation: fade-out 130ms;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: var(--cpd-color-bg-subtle-secondary);
|
||||
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 8px;
|
||||
max-width: 90vw;
|
||||
width: 600px;
|
||||
position: fixed;
|
||||
z-index: 101;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modalHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 34px 32px 0 32px;
|
||||
.dialog {
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
box-sizing: border-box;
|
||||
inline-size: 520px;
|
||||
max-inline-size: 90%;
|
||||
max-block-size: 600px;
|
||||
}
|
||||
|
||||
.modalHeader h3 {
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-title);
|
||||
margin: 0;
|
||||
@keyframes zoom-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(80%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
@keyframes zoom-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(100%);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(80%);
|
||||
}
|
||||
}
|
||||
|
||||
.dialog[data-state="open"] {
|
||||
animation: zoom-in 200ms;
|
||||
}
|
||||
|
||||
.dialog[data-state="closed"] {
|
||||
animation: zoom-out 130ms;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
.dialog[data-state="open"] {
|
||||
animation-name: fade-in;
|
||||
}
|
||||
|
||||
.dialog[data-state="closed"] {
|
||||
animation-name: fade-out;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24px 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content p {
|
||||
margin-top: 0;
|
||||
.dialog .content {
|
||||
flex-grow: 1;
|
||||
background: var(--cpd-color-bg-canvas-default);
|
||||
}
|
||||
|
||||
@media (max-width: 799px) {
|
||||
.modalHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 32px 20px 0 20px;
|
||||
}
|
||||
.drawer .content {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.modal.mobileFullScreen {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
border-radius: 0;
|
||||
.drawer {
|
||||
background: var(--cpd-color-bg-canvas-default);
|
||||
inset-block-end: 0;
|
||||
inset-inline: max(0px, calc((100% - 520px) / 2));
|
||||
max-block-size: 90%;
|
||||
border-start-start-radius: 20px;
|
||||
border-start-end-radius: 20px;
|
||||
/* Drawer handle comes in the Android style by default */
|
||||
--handle-block-size: 4px;
|
||||
--handle-inline-size: 32px;
|
||||
--handle-inset-block-start: var(--cpd-space-4x);
|
||||
--handle-inset-block-end: var(--cpd-space-4x);
|
||||
}
|
||||
|
||||
body[data-platform="ios"] .drawer {
|
||||
--handle-block-size: 5px;
|
||||
--handle-inline-size: 36px;
|
||||
--handle-inset-block-start: var(--cpd-space-1-5x);
|
||||
--handle-inset-block-end: calc(var(--cpd-space-1x) / 4);
|
||||
}
|
||||
|
||||
.close {
|
||||
cursor: pointer;
|
||||
color: var(--cpd-color-icon-secondary);
|
||||
border-radius: var(--cpd-radius-pill-effect);
|
||||
padding: var(--cpd-space-1x);
|
||||
background: var(--cpd-color-bg-subtle-secondary);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.close svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.close:hover {
|
||||
background: var(--cpd-color-bg-subtle-primary);
|
||||
color: var(--cpd-color-icon-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.close:active {
|
||||
background: var(--cpd-color-bg-subtle-primary);
|
||||
color: var(--cpd-color-icon-primary);
|
||||
}
|
||||
|
||||
.header {
|
||||
background: var(--cpd-color-bg-subtle-secondary);
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.dialog .header {
|
||||
padding-block-start: var(--cpd-space-4x);
|
||||
grid-template-columns:
|
||||
var(--cpd-space-10x) 1fr minmax(var(--cpd-space-6x), auto)
|
||||
var(--cpd-space-4x);
|
||||
grid-template-rows: auto minmax(var(--cpd-space-4x), auto);
|
||||
/* TODO: Support tabs */
|
||||
grid-template-areas: ". title close ." "tabs tabs tabs tabs";
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dialog .header h2 {
|
||||
grid-area: title;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.drawer .header {
|
||||
grid-template-areas: "tabs";
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.close {
|
||||
grid-area: close;
|
||||
}
|
||||
|
||||
.dialog .body {
|
||||
padding-inline: var(--cpd-space-10x);
|
||||
padding-block: var(--cpd-space-10x) var(--cpd-space-12x);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.drawer .body {
|
||||
padding-inline: var(--cpd-space-4x);
|
||||
padding-block: var(--cpd-space-9x) var(--cpd-space-10x);
|
||||
}
|
||||
|
||||
.handle {
|
||||
content: "";
|
||||
position: absolute;
|
||||
block-size: var(--handle-block-size);
|
||||
inset-inline: calc((100% - var(--handle-inline-size)) / 2);
|
||||
inset-block-start: var(--handle-inset-block-start);
|
||||
background: var(--cpd-color-icon-secondary);
|
||||
border-radius: var(--cpd-radius-pill-effect);
|
||||
}
|
||||
|
||||
203
src/Modal.tsx
203
src/Modal.tsx
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 New Vector Ltd
|
||||
Copyright 2023 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -14,123 +14,128 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable jsx-a11y/no-autofocus */
|
||||
|
||||
import { useRef, useMemo, ReactNode } from "react";
|
||||
import {
|
||||
useOverlay,
|
||||
usePreventScroll,
|
||||
useModal,
|
||||
OverlayContainer,
|
||||
OverlayProps,
|
||||
} from "@react-aria/overlays";
|
||||
import {
|
||||
OverlayTriggerState,
|
||||
useOverlayTriggerState,
|
||||
} from "@react-stately/overlays";
|
||||
import { useDialog } from "@react-aria/dialog";
|
||||
import { FocusScope } from "@react-aria/focus";
|
||||
import { useButton } from "@react-aria/button";
|
||||
import classNames from "classnames";
|
||||
import { ReactNode, useCallback } from "react";
|
||||
import { AriaDialogProps } from "@react-types/dialog";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Root as DialogRoot,
|
||||
Portal as DialogPortal,
|
||||
Overlay as DialogOverlay,
|
||||
Content as DialogContent,
|
||||
Title as DialogTitle,
|
||||
Close as DialogClose,
|
||||
} from "@radix-ui/react-dialog";
|
||||
import { Drawer } from "vaul";
|
||||
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
||||
import { ReactComponent as CloseIcon } from "@vector-im/compound-design-tokens/icons/close.svg";
|
||||
import classNames from "classnames";
|
||||
import { Heading } from "@vector-im/compound-web";
|
||||
|
||||
import { ReactComponent as CloseIcon } from "./icons/Close.svg";
|
||||
import styles from "./Modal.module.css";
|
||||
import styles from "./NewModal.module.css";
|
||||
import { useMediaQuery } from "./useMediaQuery";
|
||||
import { Glass } from "./Glass";
|
||||
|
||||
export interface ModalProps extends OverlayProps, AriaDialogProps {
|
||||
// TODO: Support tabs
|
||||
export interface ModalProps extends AriaDialogProps {
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
mobileFullScreen?: boolean;
|
||||
onClose: () => void;
|
||||
/**
|
||||
* The controlled open state of the modal.
|
||||
*/
|
||||
// An option to leave the open state uncontrolled is intentionally not
|
||||
// provided, since modals are always opened due to external triggers, and it
|
||||
// is the author's belief that controlled components lead to more obvious code.
|
||||
open: boolean;
|
||||
/**
|
||||
* Callback for when the user dismisses the modal. If undefined, the modal
|
||||
* will be non-dismissable.
|
||||
*/
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A modal, taking the form of a drawer / bottom sheet on touchscreen devices,
|
||||
* and a dialog box on desktop.
|
||||
*/
|
||||
export function Modal({
|
||||
title,
|
||||
children,
|
||||
className,
|
||||
mobileFullScreen,
|
||||
onClose,
|
||||
open,
|
||||
onDismiss,
|
||||
...rest
|
||||
}: ModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const modalRef = useRef(null);
|
||||
const { overlayProps, underlayProps } = useOverlay(
|
||||
{ ...rest, onClose },
|
||||
modalRef
|
||||
);
|
||||
usePreventScroll();
|
||||
const { modalProps } = useModal();
|
||||
const { dialogProps, titleProps } = useDialog(rest, modalRef);
|
||||
const closeButtonRef = useRef(null);
|
||||
const { buttonProps: closeButtonProps } = useButton(
|
||||
{
|
||||
onPress: () => onClose(),
|
||||
const touchscreen = useMediaQuery("(hover: none)");
|
||||
const onOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
if (!open) onDismiss?.();
|
||||
},
|
||||
closeButtonRef
|
||||
[onDismiss]
|
||||
);
|
||||
|
||||
return (
|
||||
<OverlayContainer>
|
||||
<div className={styles.modalOverlay} {...underlayProps}>
|
||||
<FocusScope contain restoreFocus autoFocus>
|
||||
<div
|
||||
{...overlayProps}
|
||||
{...dialogProps}
|
||||
{...modalProps}
|
||||
ref={modalRef}
|
||||
className={classNames(
|
||||
styles.modal,
|
||||
{ [styles.mobileFullScreen]: mobileFullScreen },
|
||||
className
|
||||
)}
|
||||
if (touchscreen) {
|
||||
return (
|
||||
<Drawer.Root
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
dismissible={onDismiss !== undefined}
|
||||
>
|
||||
<Drawer.Portal>
|
||||
<Drawer.Overlay className={styles.overlay} />
|
||||
<Drawer.Content
|
||||
className={classNames(className, styles.modal, styles.drawer)}
|
||||
{...rest}
|
||||
>
|
||||
<div className={styles.modalHeader}>
|
||||
<h3 {...titleProps}>{title}</h3>
|
||||
<button
|
||||
{...closeButtonProps}
|
||||
ref={closeButtonRef}
|
||||
className={styles.closeButton}
|
||||
data-testid="modal_close"
|
||||
title={t("Close")}
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.handle} />
|
||||
<VisuallyHidden asChild>
|
||||
<Drawer.Title>{title}</Drawer.Title>
|
||||
</VisuallyHidden>
|
||||
</div>
|
||||
<div className={styles.body}>{children}</div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</FocusScope>
|
||||
</div>
|
||||
</OverlayContainer>
|
||||
);
|
||||
}
|
||||
|
||||
interface ModalContentProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ModalContent({
|
||||
children,
|
||||
className,
|
||||
...rest
|
||||
}: ModalContentProps) {
|
||||
return (
|
||||
<div className={classNames(styles.content, className)} {...rest}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function useModalTriggerState(): {
|
||||
modalState: OverlayTriggerState;
|
||||
modalProps: { isOpen: boolean; onClose: () => void };
|
||||
} {
|
||||
const modalState = useOverlayTriggerState({});
|
||||
const modalProps = useMemo(
|
||||
() => ({ isOpen: modalState.isOpen, onClose: modalState.close }),
|
||||
[modalState]
|
||||
);
|
||||
return { modalState, modalProps };
|
||||
</Drawer.Content>
|
||||
</Drawer.Portal>
|
||||
</Drawer.Root>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<DialogRoot open={open} onOpenChange={onOpenChange}>
|
||||
<DialogPortal>
|
||||
<DialogOverlay
|
||||
className={classNames(styles.overlay, styles.dialogOverlay)}
|
||||
/>
|
||||
<DialogContent asChild {...rest}>
|
||||
<Glass
|
||||
frosted
|
||||
className={classNames(className, styles.modal, styles.dialog)}
|
||||
>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.header}>
|
||||
<DialogTitle asChild>
|
||||
<Heading as="h2" weight="semibold" size="md">
|
||||
{title}
|
||||
</Heading>
|
||||
</DialogTitle>
|
||||
{onDismiss !== undefined && (
|
||||
<DialogClose
|
||||
className={styles.close}
|
||||
data-testid="modal_close"
|
||||
aria-label={t("Close")}
|
||||
>
|
||||
<CloseIcon width={20} height={20} />
|
||||
</DialogClose>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.body}>{children}</div>
|
||||
</div>
|
||||
</Glass>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</DialogRoot>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
141
src/NewModal.tsx
141
src/NewModal.tsx
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@ import { useHistory, useLocation } from "react-router-dom";
|
||||
|
||||
import { useClientLegacy } from "./ClientContext";
|
||||
import { useProfile } from "./profile/useProfile";
|
||||
import { useModalTriggerState } from "./Modal";
|
||||
import { SettingsModal } from "./settings/SettingsModal";
|
||||
import { UserMenu } from "./UserMenu";
|
||||
|
||||
@@ -32,7 +31,11 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
|
||||
const history = useHistory();
|
||||
const { client, logout, authenticated, passwordlessUser } = useClientLegacy();
|
||||
const { displayName, avatarUrl } = useProfile(client);
|
||||
const { modalState, modalProps } = useModalTriggerState();
|
||||
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
|
||||
const onDismissSettingsModal = useCallback(
|
||||
() => setSettingsModalOpen(false),
|
||||
[setSettingsModalOpen]
|
||||
);
|
||||
|
||||
const [defaultSettingsTab, setDefaultSettingsTab] = useState<string>();
|
||||
|
||||
@@ -41,11 +44,11 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
|
||||
switch (value) {
|
||||
case "user":
|
||||
setDefaultSettingsTab("profile");
|
||||
modalState.open();
|
||||
setSettingsModalOpen(true);
|
||||
break;
|
||||
case "settings":
|
||||
setDefaultSettingsTab("audio");
|
||||
modalState.open();
|
||||
setSettingsModalOpen(true);
|
||||
break;
|
||||
case "logout":
|
||||
logout?.();
|
||||
@@ -55,7 +58,7 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
|
||||
break;
|
||||
}
|
||||
},
|
||||
[history, location, logout, modalState]
|
||||
[history, location, logout, setSettingsModalOpen]
|
||||
);
|
||||
|
||||
const userName = client?.getUserIdLocalpart() ?? "";
|
||||
@@ -70,11 +73,12 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
|
||||
userId={client?.getUserId() ?? ""}
|
||||
displayName={displayName || (userName ? userName.replace("@", "") : "")}
|
||||
/>
|
||||
{modalState.isOpen && client && (
|
||||
{client && (
|
||||
<SettingsModal
|
||||
client={client}
|
||||
defaultTab={defaultSettingsTab}
|
||||
{...modalProps}
|
||||
open={settingsModalOpen}
|
||||
onDismiss={onDismissSettingsModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -17,36 +17,29 @@ limitations under the License.
|
||||
import { PressEvent } from "@react-types/shared";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Modal, ModalContent } from "../Modal";
|
||||
import { Modal } from "../Modal";
|
||||
import { Button } from "../button";
|
||||
import { FieldRow } from "../input/Input";
|
||||
import styles from "./JoinExistingCallModal.module.css";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onDismiss: () => void;
|
||||
onJoin: (e: PressEvent) => void;
|
||||
onClose: () => void;
|
||||
// TODO: add used parameters for <Modal>
|
||||
[index: string]: unknown;
|
||||
}
|
||||
export function JoinExistingCallModal({ onJoin, onClose, ...rest }: Props) {
|
||||
|
||||
export function JoinExistingCallModal({ onJoin, open, onDismiss }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t("Join existing call?")}
|
||||
isDismissable
|
||||
{...rest}
|
||||
onClose={onClose}
|
||||
>
|
||||
<ModalContent>
|
||||
<p>{t("This call already exists, would you like to join?")}</p>
|
||||
<FieldRow rightAlign className={styles.buttons}>
|
||||
<Button onPress={onClose}>{t("No")}</Button>
|
||||
<Button onPress={onJoin} data-testid="home_joinExistingRoom">
|
||||
{t("Yes, join call")}
|
||||
</Button>
|
||||
</FieldRow>
|
||||
</ModalContent>
|
||||
<Modal title={t("Join existing call?")} open={open} onDismiss={onDismiss}>
|
||||
<p>{t("This call already exists, would you like to join?")}</p>
|
||||
<FieldRow rightAlign className={styles.buttons}>
|
||||
<Button onPress={onDismiss}>{t("No")}</Button>
|
||||
<Button onPress={onJoin} data-testid="home_joinExistingRoom">
|
||||
{t("Yes, join call")}
|
||||
</Button>
|
||||
</FieldRow>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -45,7 +45,6 @@ import {
|
||||
import { useEnableE2EE } from "../settings/useSetting";
|
||||
import { useRoomAvatar } from "./useRoomAvatar";
|
||||
import { useRoomName } from "./useRoomName";
|
||||
import { useModalTriggerState } from "../Modal";
|
||||
import { useJoinRule } from "./useJoinRule";
|
||||
import { ShareModal } from "./ShareModal";
|
||||
|
||||
@@ -286,12 +285,15 @@ export function GroupCallView({
|
||||
|
||||
const joinRule = useJoinRule(rtcSession.room);
|
||||
|
||||
const { modalState: shareModalState, modalProps: shareModalProps } =
|
||||
useModalTriggerState();
|
||||
const [shareModalOpen, setShareModalOpen] = useState(false);
|
||||
const onDismissShareModal = useCallback(
|
||||
() => setShareModalOpen(false),
|
||||
[setShareModalOpen]
|
||||
);
|
||||
|
||||
const onShareClickFn = useCallback(
|
||||
() => shareModalState.open(),
|
||||
[shareModalState]
|
||||
() => setShareModalOpen(true),
|
||||
[setShareModalOpen]
|
||||
);
|
||||
const onShareClick = joinRule === JoinRule.Public ? onShareClickFn : null;
|
||||
|
||||
@@ -311,8 +313,12 @@ export function GroupCallView({
|
||||
return <ErrorView error={new Error("You need to enable E2EE to join.")} />;
|
||||
}
|
||||
|
||||
const shareModal = shareModalState.isOpen && (
|
||||
<ShareModal roomId={rtcSession.room.roomId} {...shareModalProps} />
|
||||
const shareModal = (
|
||||
<ShareModal
|
||||
roomId={rtcSession.room.roomId}
|
||||
open={shareModalOpen}
|
||||
onDismiss={onDismissShareModal}
|
||||
/>
|
||||
);
|
||||
|
||||
if (isJoined) {
|
||||
|
||||
@@ -30,7 +30,6 @@ import { Room as MatrixRoom } from "matrix-js-sdk/src/models/room";
|
||||
import { Ref, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useMeasure from "react-use-measure";
|
||||
import { OverlayTriggerState } from "@react-stately/overlays";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||
|
||||
@@ -51,7 +50,6 @@ import {
|
||||
VideoGrid,
|
||||
} from "../video-grid/VideoGrid";
|
||||
import { useShowConnectionStats } from "../settings/useSetting";
|
||||
import { useModalTriggerState } from "../Modal";
|
||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||
import { useUrlParams } from "../UrlParams";
|
||||
import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
|
||||
@@ -313,25 +311,20 @@ export function InCallView({
|
||||
);
|
||||
};
|
||||
|
||||
const {
|
||||
modalState: rageshakeRequestModalState,
|
||||
modalProps: rageshakeRequestModalProps,
|
||||
} = useRageshakeRequestModal(rtcSession.room.roomId);
|
||||
const rageshakeRequestModalProps = useRageshakeRequestModal(
|
||||
rtcSession.room.roomId
|
||||
);
|
||||
|
||||
const {
|
||||
modalState: settingsModalState,
|
||||
modalProps: settingsModalProps,
|
||||
}: {
|
||||
modalState: OverlayTriggerState;
|
||||
modalProps: {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
} = useModalTriggerState();
|
||||
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
|
||||
|
||||
const openSettings = useCallback(() => {
|
||||
settingsModalState.open();
|
||||
}, [settingsModalState]);
|
||||
const openSettings = useCallback(
|
||||
() => setSettingsModalOpen(true),
|
||||
[setSettingsModalOpen]
|
||||
);
|
||||
const closeSettings = useCallback(
|
||||
() => setSettingsModalOpen(false),
|
||||
[setSettingsModalOpen]
|
||||
);
|
||||
|
||||
const toggleScreensharing = useCallback(async () => {
|
||||
exitFullscreen();
|
||||
@@ -442,19 +435,13 @@ export function InCallView({
|
||||
show={showInspector}
|
||||
/>
|
||||
)*/}
|
||||
{rageshakeRequestModalState.isOpen && !noControls && (
|
||||
<RageshakeRequestModal
|
||||
{...rageshakeRequestModalProps}
|
||||
roomId={rtcSession.room.roomId}
|
||||
/>
|
||||
)}
|
||||
{settingsModalState.isOpen && (
|
||||
<SettingsModal
|
||||
client={client}
|
||||
roomId={rtcSession.room.roomId}
|
||||
{...settingsModalProps}
|
||||
/>
|
||||
)}
|
||||
{!noControls && <RageshakeRequestModal {...rageshakeRequestModalProps} />}
|
||||
<SettingsModal
|
||||
client={client}
|
||||
roomId={rtcSession.room.roomId}
|
||||
open={settingsModalOpen}
|
||||
onDismiss={closeSettings}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ limitations under the License.
|
||||
import { FC, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Modal, ModalContent, ModalProps } from "../Modal";
|
||||
import { Modal, ModalProps } from "../Modal";
|
||||
import { Button } from "../button";
|
||||
import { FieldRow, ErrorMessage } from "../input/Input";
|
||||
import { useSubmitRageshake } from "../settings/submit-rageshake";
|
||||
@@ -26,51 +26,49 @@ import { Body } from "../typography/Typography";
|
||||
interface Props extends Omit<ModalProps, "title" | "children"> {
|
||||
rageshakeRequestId: string;
|
||||
roomId: string;
|
||||
onClose: () => void;
|
||||
open: boolean;
|
||||
onDismiss: () => void;
|
||||
}
|
||||
|
||||
export const RageshakeRequestModal: FC<Props> = ({
|
||||
rageshakeRequestId,
|
||||
roomId,
|
||||
...rest
|
||||
open,
|
||||
onDismiss,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
|
||||
|
||||
useEffect(() => {
|
||||
if (sent) {
|
||||
rest.onClose();
|
||||
}
|
||||
}, [sent, rest]);
|
||||
if (sent) onDismiss();
|
||||
}, [sent, onDismiss]);
|
||||
|
||||
return (
|
||||
<Modal title={t("Debug log request")} isDismissable {...rest}>
|
||||
<ModalContent>
|
||||
<Body>
|
||||
{t(
|
||||
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log."
|
||||
)}
|
||||
</Body>
|
||||
<FieldRow>
|
||||
<Button
|
||||
onPress={() =>
|
||||
submitRageshake({
|
||||
sendLogs: true,
|
||||
rageshakeRequestId,
|
||||
roomId,
|
||||
})
|
||||
}
|
||||
disabled={sending}
|
||||
>
|
||||
{sending ? t("Sending debug logs…") : t("Send debug logs")}
|
||||
</Button>
|
||||
</FieldRow>
|
||||
{error && (
|
||||
<FieldRow>
|
||||
<ErrorMessage error={error} />
|
||||
</FieldRow>
|
||||
<Modal title={t("Debug log request")} open={open} onDismiss={onDismiss}>
|
||||
<Body>
|
||||
{t(
|
||||
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log."
|
||||
)}
|
||||
</ModalContent>
|
||||
</Body>
|
||||
<FieldRow>
|
||||
<Button
|
||||
onPress={() =>
|
||||
submitRageshake({
|
||||
sendLogs: true,
|
||||
rageshakeRequestId,
|
||||
roomId,
|
||||
})
|
||||
}
|
||||
disabled={sending}
|
||||
>
|
||||
{sending ? t("Sending debug logs…") : t("Send debug logs")}
|
||||
</Button>
|
||||
</FieldRow>
|
||||
{error && (
|
||||
<FieldRow>
|
||||
<ErrorMessage error={error} />
|
||||
</FieldRow>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,10 +14,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.inviteModal {
|
||||
max-width: 413px;
|
||||
}
|
||||
|
||||
.copyButton {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -17,35 +17,30 @@ limitations under the License.
|
||||
import { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Modal, ModalContent, ModalProps } from "../Modal";
|
||||
import { Modal } from "../Modal";
|
||||
import { CopyButton } from "../button";
|
||||
import { getRoomUrl } from "../matrix-utils";
|
||||
import styles from "./ShareModal.module.css";
|
||||
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
|
||||
|
||||
interface Props extends Omit<ModalProps, "title" | "children"> {
|
||||
interface Props {
|
||||
roomId: string;
|
||||
open: boolean;
|
||||
onDismiss: () => void;
|
||||
}
|
||||
|
||||
export const ShareModal: FC<Props> = ({ roomId, ...rest }) => {
|
||||
export const ShareModal: FC<Props> = ({ roomId, open, onDismiss }) => {
|
||||
const { t } = useTranslation();
|
||||
const roomSharedKey = useRoomSharedKey(roomId);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t("Share this call")}
|
||||
isDismissable
|
||||
className={styles.inviteModal}
|
||||
{...rest}
|
||||
>
|
||||
<ModalContent>
|
||||
<p>{t("Copy and share this call link")}</p>
|
||||
<CopyButton
|
||||
className={styles.copyButton}
|
||||
value={getRoomUrl(roomId, roomSharedKey ?? undefined)}
|
||||
data-testid="modal_inviteLink"
|
||||
/>
|
||||
</ModalContent>
|
||||
<Modal title={t("Share this call")} open={open} onDismiss={onDismiss}>
|
||||
<p>{t("Copy and share this call link")}</p>
|
||||
<CopyButton
|
||||
className={styles.copyButton}
|
||||
value={getRoomUrl(roomId, roomSharedKey ?? undefined)}
|
||||
data-testid="modal_inviteLink"
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,10 +14,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useCallback, useMemo, useRef, FC } from "react";
|
||||
import { useEffect, useCallback, useMemo, useRef, FC, useState } from "react";
|
||||
import useMeasure from "react-use-measure";
|
||||
import { ResizeObserver } from "@juggle/resize-observer";
|
||||
import { OverlayTriggerState } from "@react-stately/overlays";
|
||||
import { usePreviewTracks } from "@livekit/components-react";
|
||||
import {
|
||||
CreateLocalTracksOptions,
|
||||
@@ -28,7 +27,6 @@ import {
|
||||
import { MicButton, SettingsButton, VideoButton } from "../button";
|
||||
import { Avatar } from "../Avatar";
|
||||
import styles from "./VideoPreview.module.css";
|
||||
import { useModalTriggerState } from "../Modal";
|
||||
import { SettingsModal } from "../settings/SettingsModal";
|
||||
import { useClient } from "../ClientContext";
|
||||
import { useMediaDevices } from "../livekit/MediaDevicesContext";
|
||||
@@ -54,20 +52,16 @@ export const VideoPreview: FC<Props> = ({ matrixInfo, muteStates }) => {
|
||||
const { client } = useClient();
|
||||
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
|
||||
|
||||
const {
|
||||
modalState: settingsModalState,
|
||||
modalProps: settingsModalProps,
|
||||
}: {
|
||||
modalState: OverlayTriggerState;
|
||||
modalProps: {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
} = useModalTriggerState();
|
||||
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
|
||||
|
||||
const openSettings = useCallback(() => {
|
||||
settingsModalState.open();
|
||||
}, [settingsModalState]);
|
||||
const openSettings = useCallback(
|
||||
() => setSettingsModalOpen(true),
|
||||
[setSettingsModalOpen]
|
||||
);
|
||||
const closeSettings = useCallback(
|
||||
() => setSettingsModalOpen(false),
|
||||
[setSettingsModalOpen]
|
||||
);
|
||||
|
||||
const devices = useMediaDevices();
|
||||
|
||||
@@ -153,8 +147,12 @@ export const VideoPreview: FC<Props> = ({ matrixInfo, muteStates }) => {
|
||||
<SettingsButton onPress={openSettings} />
|
||||
</div>
|
||||
</>
|
||||
{settingsModalState.isOpen && client && (
|
||||
<SettingsModal client={client} {...settingsModalProps} />
|
||||
{client && (
|
||||
<SettingsModal
|
||||
client={client}
|
||||
open={settingsModalOpen}
|
||||
onDismiss={closeSettings}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 New Vector Ltd
|
||||
Copyright 2022 - 2023 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -15,18 +15,13 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
.settingsModal {
|
||||
width: 774px;
|
||||
height: 480px;
|
||||
block-size: 550px;
|
||||
}
|
||||
|
||||
.settingsModal p {
|
||||
color: var(--cpd-color-text-secondary);
|
||||
}
|
||||
|
||||
.tabContainer {
|
||||
padding: 27px 20px;
|
||||
}
|
||||
|
||||
.fieldRowText {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -51,11 +51,11 @@ import {
|
||||
} from "../livekit/MediaDevicesContext";
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
open: boolean;
|
||||
onDismiss: () => void;
|
||||
client: MatrixClient;
|
||||
roomId?: string;
|
||||
defaultTab?: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const SettingsModal = (props: Props) => {
|
||||
@@ -119,7 +119,7 @@ export const SettingsModal = (props: Props) => {
|
||||
);
|
||||
|
||||
const devices = useMediaDevices();
|
||||
useMediaDeviceNames(devices);
|
||||
useMediaDeviceNames(devices, props.open);
|
||||
|
||||
const audioTab = (
|
||||
<TabItem
|
||||
@@ -289,10 +289,9 @@ export const SettingsModal = (props: Props) => {
|
||||
return (
|
||||
<Modal
|
||||
title={t("Settings")}
|
||||
isDismissable
|
||||
mobileFullScreen
|
||||
className={styles.settingsModal}
|
||||
{...props}
|
||||
open={props.open}
|
||||
onDismiss={props.onDismiss}
|
||||
>
|
||||
<TabContainer
|
||||
onSelectionChange={onSelectedTabChanged}
|
||||
|
||||
@@ -14,20 +14,25 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { useCallback, useContext, useEffect, useState } from "react";
|
||||
import {
|
||||
ComponentProps,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import pako from "pako";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { OverlayTriggerState } from "@react-stately/overlays";
|
||||
import { ClientEvent } from "matrix-js-sdk/src/client";
|
||||
|
||||
import { getLogsForReport } from "./rageshake";
|
||||
import { useClient } from "../ClientContext";
|
||||
import { InspectorContext } from "../room/GroupCallInspector";
|
||||
import { useModalTriggerState } from "../Modal";
|
||||
import { Config } from "../config/Config";
|
||||
import { ElementCallOpenTelemetry } from "../otel/otel";
|
||||
import { RageshakeRequestModal } from "../room/RageshakeRequestModal";
|
||||
|
||||
const gzip = (text: string): Blob => {
|
||||
// encode as UTF-8
|
||||
@@ -343,22 +348,12 @@ export function useRageshakeRequest(): (
|
||||
|
||||
return sendRageshakeRequest;
|
||||
}
|
||||
interface ModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
interface ModalPropsWithId extends ModalProps {
|
||||
rageshakeRequestId: string;
|
||||
}
|
||||
|
||||
export function useRageshakeRequestModal(roomId: string): {
|
||||
modalState: OverlayTriggerState;
|
||||
modalProps: ModalPropsWithId;
|
||||
} {
|
||||
const { modalState, modalProps } = useModalTriggerState() as {
|
||||
modalState: OverlayTriggerState;
|
||||
modalProps: ModalProps;
|
||||
};
|
||||
export function useRageshakeRequestModal(
|
||||
roomId: string
|
||||
): ComponentProps<typeof RageshakeRequestModal> {
|
||||
const [open, setOpen] = useState(false);
|
||||
const onDismiss = useCallback(() => setOpen(false), [setOpen]);
|
||||
const { client } = useClient();
|
||||
const [rageshakeRequestId, setRageshakeRequestId] = useState<string>();
|
||||
|
||||
@@ -374,7 +369,7 @@ export function useRageshakeRequestModal(roomId: string): {
|
||||
client.getUserId() !== event.getSender()
|
||||
) {
|
||||
setRageshakeRequestId(event.getContent().request_id);
|
||||
modalState.open();
|
||||
setOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -383,10 +378,12 @@ export function useRageshakeRequestModal(roomId: string): {
|
||||
return () => {
|
||||
client.removeListener(ClientEvent.Event, onEvent);
|
||||
};
|
||||
}, [modalState.open, roomId, client, modalState]);
|
||||
}, [setOpen, roomId, client]);
|
||||
|
||||
return {
|
||||
modalState,
|
||||
modalProps: { ...modalProps, rageshakeRequestId: rageshakeRequestId ?? "" },
|
||||
rageshakeRequestId: rageshakeRequestId ?? "",
|
||||
roomId,
|
||||
open,
|
||||
onDismiss,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -78,32 +78,3 @@ limitations under the License.
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
.tab {
|
||||
width: 200px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.tab > * {
|
||||
margin: 0 12px 0 0;
|
||||
}
|
||||
|
||||
.tabContainer {
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
padding: 20px 18px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tabList {
|
||||
flex-direction: column;
|
||||
margin-bottom: 0;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.tabPanel {
|
||||
padding: 0 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,13 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ComponentProps, forwardRef, useCallback, useEffect } from "react";
|
||||
import {
|
||||
ComponentProps,
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { animated } from "@react-spring/web";
|
||||
import classNames from "classnames";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -36,7 +42,6 @@ import { Avatar } from "../Avatar";
|
||||
import styles from "./VideoTile.module.css";
|
||||
import { useReactiveState } from "../useReactiveState";
|
||||
import { AudioButton, FullscreenButton } from "../button/Button";
|
||||
import { useModalTriggerState } from "../Modal";
|
||||
import { VideoTileSettingsModal } from "./VideoTileSettingsModal";
|
||||
|
||||
export interface ItemData {
|
||||
@@ -117,11 +122,16 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
||||
onToggleFullscreen(data.id);
|
||||
}, [data, onToggleFullscreen]);
|
||||
|
||||
const {
|
||||
modalState: videoTileSettingsModalState,
|
||||
modalProps: videoTileSettingsModalProps,
|
||||
} = useModalTriggerState();
|
||||
const onOptionsPress = videoTileSettingsModalState.open;
|
||||
const [videoTileSettingsModalOpen, setVideoTileSettingsModalOpen] =
|
||||
useState(false);
|
||||
const openVideoTileSettingsModal = useCallback(
|
||||
() => setVideoTileSettingsModalOpen(true),
|
||||
[setVideoTileSettingsModalOpen]
|
||||
);
|
||||
const closeVideoTileSettingsModal = useCallback(
|
||||
() => setVideoTileSettingsModalOpen(false),
|
||||
[setVideoTileSettingsModalOpen]
|
||||
);
|
||||
|
||||
const toolbarButtons: JSX.Element[] = [];
|
||||
if (!sfuParticipant.isLocal) {
|
||||
@@ -130,7 +140,7 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
||||
key="localVolume"
|
||||
className={styles.button}
|
||||
volume={(sfuParticipant as RemoteParticipant).getVolume() ?? 0}
|
||||
onPress={onOptionsPress}
|
||||
onPress={openVideoTileSettingsModal}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -208,10 +218,11 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
||||
: Track.Source.ScreenShare
|
||||
}
|
||||
/>
|
||||
{videoTileSettingsModalState.isOpen && !maximised && (
|
||||
{!maximised && (
|
||||
<VideoTileSettingsModal
|
||||
{...videoTileSettingsModalProps}
|
||||
data={data}
|
||||
open={videoTileSettingsModalOpen}
|
||||
onDismiss={closeVideoTileSettingsModal}
|
||||
/>
|
||||
)}
|
||||
</animated.div>
|
||||
|
||||
@@ -66,23 +66,21 @@ const LocalVolume: React.FC<LocalVolumeProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
// TODO: Extend ModalProps
|
||||
interface Props {
|
||||
data: ItemData;
|
||||
onClose: () => void;
|
||||
open: boolean;
|
||||
onDismiss: () => void;
|
||||
}
|
||||
|
||||
export const VideoTileSettingsModal = ({ data, onClose, ...rest }: Props) => {
|
||||
export const VideoTileSettingsModal = ({ data, open, onDismiss }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={styles.videoTileSettingsModal}
|
||||
title={t("Local volume")}
|
||||
isDismissable
|
||||
mobileFullScreen
|
||||
onClose={onClose}
|
||||
{...rest}
|
||||
open={open}
|
||||
onDismiss={onDismiss}
|
||||
>
|
||||
<div className={styles.content}>
|
||||
<LocalVolume
|
||||
|
||||
Reference in New Issue
Block a user