Merge branch 'livekit' into remove-storybook
This commit is contained in:
@@ -18,5 +18,6 @@ export default {
|
|||||||
output: "public/locales/$LOCALE/$NAMESPACE.json",
|
output: "public/locales/$LOCALE/$NAMESPACE.json",
|
||||||
input: ["src/**/*.{ts,tsx}"],
|
input: ["src/**/*.{ts,tsx}"],
|
||||||
sort: true,
|
sort: true,
|
||||||
useKeysAsDefaultValue: true,
|
// The key becomes the English version of the string
|
||||||
|
defaultValue: (_l, _ns, key) => key,
|
||||||
};
|
};
|
||||||
|
|||||||
12
package.json
12
package.json
@@ -42,8 +42,8 @@
|
|||||||
"@react-stately/select": "^3.1.3",
|
"@react-stately/select": "^3.1.3",
|
||||||
"@react-stately/tooltip": "^3.0.5",
|
"@react-stately/tooltip": "^3.0.5",
|
||||||
"@react-stately/tree": "^3.2.0",
|
"@react-stately/tree": "^3.2.0",
|
||||||
"@sentry/react": "^6.13.3",
|
"@sentry/react": "^7.0.0",
|
||||||
"@sentry/tracing": "^6.13.3",
|
"@sentry/tracing": "^7.0.0",
|
||||||
"@types/lodash": "^4.14.199",
|
"@types/lodash": "^4.14.199",
|
||||||
"@use-gesture/react": "^10.2.11",
|
"@use-gesture/react": "^10.2.11",
|
||||||
"@vector-im/compound-design-tokens": "^0.0.6",
|
"@vector-im/compound-design-tokens": "^0.0.6",
|
||||||
@@ -60,16 +60,13 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#6385c9c0dab8fe67bd3a8992a4777f243fdd1b68",
|
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#6385c9c0dab8fe67bd3a8992a4777f243fdd1b68",
|
||||||
"matrix-widget-api": "^1.3.1",
|
"matrix-widget-api": "^1.3.1",
|
||||||
"mermaid": "^9.0.0",
|
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"pako": "^2.0.4",
|
"pako": "^2.0.4",
|
||||||
"postcss-preset-env": "^9.0.0",
|
"postcss-preset-env": "^9.0.0",
|
||||||
"posthog-js": "^1.29.0",
|
"posthog-js": "^1.29.0",
|
||||||
"re-resizable": "^6.9.0",
|
|
||||||
"react": "18",
|
"react": "18",
|
||||||
"react-dom": "18",
|
"react-dom": "18",
|
||||||
"react-i18next": "^13.0.0",
|
"react-i18next": "^13.0.0",
|
||||||
"react-json-view": "^1.21.3",
|
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-use-clipboard": "^1.0.7",
|
"react-use-clipboard": "^1.0.7",
|
||||||
"react-use-measure": "^2.1.1",
|
"react-use-measure": "^2.1.1",
|
||||||
@@ -89,6 +86,7 @@
|
|||||||
"@sentry/vite-plugin": "^2.0.0",
|
"@sentry/vite-plugin": "^2.0.0",
|
||||||
"@testing-library/jest-dom": "^6.0.0",
|
"@testing-library/jest-dom": "^6.0.0",
|
||||||
"@testing-library/react": "^14.0.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
|
"@testing-library/user-event": "^14.5.1",
|
||||||
"@types/content-type": "^1.1.5",
|
"@types/content-type": "^1.1.5",
|
||||||
"@types/d3": "^7.4.0",
|
"@types/d3": "^7.4.0",
|
||||||
"@types/dom-screen-wake-lock": "^1.0.1",
|
"@types/dom-screen-wake-lock": "^1.0.1",
|
||||||
@@ -122,7 +120,7 @@
|
|||||||
"typescript": "^5.1.6",
|
"typescript": "^5.1.6",
|
||||||
"vite": "^4.2.0",
|
"vite": "^4.2.0",
|
||||||
"vite-plugin-html-template": "^1.1.0",
|
"vite-plugin-html-template": "^1.1.0",
|
||||||
"vite-plugin-svgr": "^3.2.0"
|
"vite-plugin-svgr": "^4.0.0"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"testEnvironment": "./test/environment.ts",
|
"testEnvironment": "./test/environment.ts",
|
||||||
@@ -135,7 +133,7 @@
|
|||||||
],
|
],
|
||||||
"moduleNameMapper": {
|
"moduleNameMapper": {
|
||||||
"\\.css$": "identity-obj-proxy",
|
"\\.css$": "identity-obj-proxy",
|
||||||
"\\.svg$": "<rootDir>/test/mocks/svgr.ts",
|
"\\.svg\\?react$": "<rootDir>/test/mocks/svgr.ts",
|
||||||
"^\\./IndexedDBWorker\\?worker$": "<rootDir>/test/mocks/workerMock.ts",
|
"^\\./IndexedDBWorker\\?worker$": "<rootDir>/test/mocks/workerMock.ts",
|
||||||
"^\\./olm$": "<rootDir>/test/mocks/olmMock.ts"
|
"^\\./olm$": "<rootDir>/test/mocks/olmMock.ts"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
"{{count}} stars|other": "{{count}} stars",
|
"{{count}} stars|other": "{{count}} stars",
|
||||||
"{{displayName}} is presenting": "{{displayName}} is presenting",
|
"{{displayName}} is presenting": "{{displayName}} is presenting",
|
||||||
"{{displayName}}, your call has ended.": "{{displayName}}, your call has ended.",
|
"{{displayName}}, your call has ended.": "{{displayName}}, your call has ended.",
|
||||||
"{{names, list(style: short;)}}": "{{names, list(style: short;)}}",
|
|
||||||
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.",
|
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.",
|
||||||
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>",
|
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>",
|
||||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Create an account</0> Or <2>Access as a guest</2>",
|
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Create an account</0> Or <2>Access as a guest</2>",
|
||||||
@@ -30,14 +29,12 @@
|
|||||||
"Continue in browser": "Continue in browser",
|
"Continue in browser": "Continue in browser",
|
||||||
"Copied!": "Copied!",
|
"Copied!": "Copied!",
|
||||||
"Copy": "Copy",
|
"Copy": "Copy",
|
||||||
"Copy and share this call link": "Copy and share this call link",
|
"Copy link": "Copy link",
|
||||||
"Create account": "Create account",
|
"Create account": "Create account",
|
||||||
"Debug log": "Debug log",
|
|
||||||
"Debug log request": "Debug log request",
|
"Debug log request": "Debug log request",
|
||||||
"Developer": "Developer",
|
"Developer": "Developer",
|
||||||
"Developer Settings": "Developer Settings",
|
"Developer Settings": "Developer Settings",
|
||||||
"Display name": "Display name",
|
"Display name": "Display name",
|
||||||
"Download debug logs": "Download debug logs",
|
|
||||||
"Element Call Home": "Element Call Home",
|
"Element Call Home": "Element Call Home",
|
||||||
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call is temporarily not end-to-end encrypted while we test scalability.",
|
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call is temporarily not end-to-end encrypted while we test scalability.",
|
||||||
"Enable end-to-end encryption (password protected calls)": "Enable end-to-end encryption (password protected calls)",
|
"Enable end-to-end encryption (password protected calls)": "Enable end-to-end encryption (password protected calls)",
|
||||||
@@ -54,10 +51,12 @@
|
|||||||
"How did it go?": "How did it go?",
|
"How did it go?": "How did it go?",
|
||||||
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.",
|
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.",
|
||||||
"Include debug logs": "Include debug logs",
|
"Include debug logs": "Include debug logs",
|
||||||
"Inspector": "Inspector",
|
"Invite": "Invite",
|
||||||
|
"Invite to this call": "Invite to this call",
|
||||||
"Join call": "Join call",
|
"Join call": "Join call",
|
||||||
"Join call now": "Join call now",
|
"Join call now": "Join call now",
|
||||||
"Join existing call?": "Join existing call?",
|
"Join existing call?": "Join existing call?",
|
||||||
|
"Link copied to clipboard": "Link copied to clipboard",
|
||||||
"Loading…": "Loading…",
|
"Loading…": "Loading…",
|
||||||
"Local volume": "Local volume",
|
"Local volume": "Local volume",
|
||||||
"Logging in…": "Logging in…",
|
"Logging in…": "Logging in…",
|
||||||
@@ -74,6 +73,7 @@
|
|||||||
"Not now, return to home screen": "Not now, return to home screen",
|
"Not now, return to home screen": "Not now, return to home screen",
|
||||||
"Not registered yet? <2>Create an account</2>": "Not registered yet? <2>Create an account</2>",
|
"Not registered yet? <2>Create an account</2>": "Not registered yet? <2>Create an account</2>",
|
||||||
"Open in the app": "Open in the app",
|
"Open in the app": "Open in the app",
|
||||||
|
"Participants": "Participants",
|
||||||
"Password": "Password",
|
"Password": "Password",
|
||||||
"Passwords must match": "Passwords must match",
|
"Passwords must match": "Passwords must match",
|
||||||
"Profile": "Profile",
|
"Profile": "Profile",
|
||||||
@@ -92,11 +92,8 @@
|
|||||||
"Sending debug logs…": "Sending debug logs…",
|
"Sending debug logs…": "Sending debug logs…",
|
||||||
"Sending…": "Sending…",
|
"Sending…": "Sending…",
|
||||||
"Settings": "Settings",
|
"Settings": "Settings",
|
||||||
"Share": "Share",
|
|
||||||
"Share screen": "Share screen",
|
"Share screen": "Share screen",
|
||||||
"Share this call": "Share this call",
|
|
||||||
"Sharing screen": "Sharing screen",
|
"Sharing screen": "Sharing screen",
|
||||||
"Show call inspector": "Show call inspector",
|
|
||||||
"Show connection stats": "Show connection stats",
|
"Show connection stats": "Show connection stats",
|
||||||
"Sign in": "Sign in",
|
"Sign in": "Sign in",
|
||||||
"Sign out": "Sign out",
|
"Sign out": "Sign out",
|
||||||
|
|||||||
@@ -119,7 +119,8 @@
|
|||||||
"Stop video": "Ferma video",
|
"Stop video": "Ferma video",
|
||||||
"Unmute microphone": "Riaccendi il microfono",
|
"Unmute microphone": "Riaccendi il microfono",
|
||||||
"Back to recents": "Torna ai recenti",
|
"Back to recents": "Torna ai recenti",
|
||||||
"Start new call": "Inizia nuova chiamata",
|
"Start new call": "Inizia una nuova chiamata",
|
||||||
"Call not found": "Chiamata non trovata",
|
"Call not found": "Chiamata non trovata",
|
||||||
"Calls are now end-to-end encrypted and need to be created from the home page. This helps make sure everyone's using the same encryption key.": "Le chiamate ora sono cifrate end-to-end e devono essere create dalla pagina principale. Ciò assicura che chiunque usi la stessa chiave di crittografia."
|
"Calls are now end-to-end encrypted and need to be created from the home page. This helps make sure everyone's using the same encryption key.": "Le chiamate ora sono cifrate end-to-end e devono essere create dalla pagina principale. Ciò assicura che chiunque usi la stessa chiave di crittografia.",
|
||||||
|
"Your web browser does not support media end-to-end encryption. Supported Browsers are Chrome, Safari, Firefox >=117": "Il tuo browser non supporta la crittografia end-to-end dei media. I browser supportati sono Chrome, Safari, Firefox >=117"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,5 +107,20 @@
|
|||||||
"Share this call": "Udostępnij to połączenie",
|
"Share this call": "Udostępnij to połączenie",
|
||||||
"Sharing screen": "Udostępnianie ekranu",
|
"Sharing screen": "Udostępnianie ekranu",
|
||||||
"{{count, number}}|one": "{{count, number}}",
|
"{{count, number}}|one": "{{count, number}}",
|
||||||
"{{names, list(style: short;)}}": "{{names, list(style: short;)}}"
|
"{{names, list(style: short;)}}": "{{names, list(style: short;)}}",
|
||||||
|
"Continue in browser": "Kontynuuj w przeglądarce",
|
||||||
|
"Mute microphone": "Wycisz mikrofon",
|
||||||
|
"Name of call": "Nazwa połączenia",
|
||||||
|
"Open in the app": "Otwórz w aplikacji",
|
||||||
|
"Ready to join?": "Gotowy, by dołączyć?",
|
||||||
|
"Select app": "Wybierz aplikację",
|
||||||
|
"Start new call": "Rozpocznij nowe połączenie",
|
||||||
|
"Start video": "Rozpocznij wideo",
|
||||||
|
"Back to recents": "Wróć do ostatnie",
|
||||||
|
"Stop video": "Zakończ wideo",
|
||||||
|
"Unmute microphone": "Odcisz mikrofon",
|
||||||
|
"Call not found": "Nie znaleziono połączenia",
|
||||||
|
"Calls are now end-to-end encrypted and need to be created from the home page. This helps make sure everyone's using the same encryption key.": "Połączenia są teraz szyfrowane end-to-end i muszą zostać utworzone ze strony głównej. Pomaga to upewnić się, że każdy korzysta z tego samego klucza szyfrującego.",
|
||||||
|
"You": "Ty",
|
||||||
|
"Your web browser does not support media end-to-end encryption. Supported Browsers are Chrome, Safari, Firefox >=117": "Twoja przeglądarka nie wspiera szyfrowania end-to-end. Wspierane przeglądarki to Chrome, Safari, Firefox >=117"
|
||||||
}
|
}
|
||||||
|
|||||||
45
src/App.tsx
45
src/App.tsx
@@ -30,8 +30,6 @@ import { LoginPage } from "./auth/LoginPage";
|
|||||||
import { RegisterPage } from "./auth/RegisterPage";
|
import { RegisterPage } from "./auth/RegisterPage";
|
||||||
import { RoomPage } from "./room/RoomPage";
|
import { RoomPage } from "./room/RoomPage";
|
||||||
import { ClientProvider } from "./ClientContext";
|
import { ClientProvider } from "./ClientContext";
|
||||||
import { SequenceDiagramViewerPage } from "./SequenceDiagramViewerPage";
|
|
||||||
import { InspectorContextProvider } from "./room/GroupCallInspector";
|
|
||||||
import { CrashView, LoadingView } from "./FullScreenView";
|
import { CrashView, LoadingView } from "./FullScreenView";
|
||||||
import { DisconnectedBanner } from "./DisconnectedBanner";
|
import { DisconnectedBanner } from "./DisconnectedBanner";
|
||||||
import { Initializer } from "./initializer";
|
import { Initializer } from "./initializer";
|
||||||
@@ -83,30 +81,25 @@ export default function App({ history }: AppProps) {
|
|||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<ClientProvider>
|
<ClientProvider>
|
||||||
<MediaDevicesProvider>
|
<MediaDevicesProvider>
|
||||||
<InspectorContextProvider>
|
<Sentry.ErrorBoundary fallback={errorPage}>
|
||||||
<Sentry.ErrorBoundary fallback={errorPage}>
|
<OverlayProvider>
|
||||||
<OverlayProvider>
|
<DisconnectedBanner />
|
||||||
<DisconnectedBanner />
|
<Switch>
|
||||||
<Switch>
|
<SentryRoute exact path="/">
|
||||||
<SentryRoute exact path="/">
|
<HomePage />
|
||||||
<HomePage />
|
</SentryRoute>
|
||||||
</SentryRoute>
|
<SentryRoute exact path="/login">
|
||||||
<SentryRoute exact path="/login">
|
<LoginPage />
|
||||||
<LoginPage />
|
</SentryRoute>
|
||||||
</SentryRoute>
|
<SentryRoute exact path="/register">
|
||||||
<SentryRoute exact path="/register">
|
<RegisterPage />
|
||||||
<RegisterPage />
|
</SentryRoute>
|
||||||
</SentryRoute>
|
<SentryRoute path="*">
|
||||||
<SentryRoute path="/inspector">
|
<RoomPage />
|
||||||
<SequenceDiagramViewerPage />
|
</SentryRoute>
|
||||||
</SentryRoute>
|
</Switch>
|
||||||
<SentryRoute path="*">
|
</OverlayProvider>
|
||||||
<RoomPage />
|
</Sentry.ErrorBoundary>
|
||||||
</SentryRoute>
|
|
||||||
</Switch>
|
|
||||||
</OverlayProvider>
|
|
||||||
</Sentry.ErrorBoundary>
|
|
||||||
</InspectorContextProvider>
|
|
||||||
</MediaDevicesProvider>
|
</MediaDevicesProvider>
|
||||||
</ClientProvider>
|
</ClientProvider>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { Trans } from "react-i18next";
|
|||||||
|
|
||||||
import { Banner } from "./Banner";
|
import { Banner } from "./Banner";
|
||||||
import styles from "./E2EEBanner.module.css";
|
import styles from "./E2EEBanner.module.css";
|
||||||
import { ReactComponent as LockOffIcon } from "./icons/LockOff.svg";
|
import LockOffIcon from "./icons/LockOff.svg?react";
|
||||||
import { useEnableE2EE } from "./settings/useSetting";
|
import { useEnableE2EE } from "./settings/useSetting";
|
||||||
|
|
||||||
export const E2EEBanner = () => {
|
export const E2EEBanner = () => {
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2022 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 { HTMLAttributes } from "react";
|
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { AvatarStack } from "@vector-im/compound-web";
|
|
||||||
|
|
||||||
import { Avatar, Size } from "./Avatar";
|
|
||||||
|
|
||||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
||||||
className?: string;
|
|
||||||
client: MatrixClient;
|
|
||||||
members: RoomMember[];
|
|
||||||
max?: number;
|
|
||||||
size?: Size | number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Facepile({
|
|
||||||
className,
|
|
||||||
client,
|
|
||||||
members,
|
|
||||||
max = 3,
|
|
||||||
size = Size.XS,
|
|
||||||
...rest
|
|
||||||
}: Props) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const displayedMembers = members.slice(0, max);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AvatarStack
|
|
||||||
title={t("{{names, list(style: short;)}}", {
|
|
||||||
list: displayedMembers.map((m) => m.name),
|
|
||||||
})}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{displayedMembers.map((member, i) => {
|
|
||||||
const avatarUrl = member.getMxcAvatarUrl();
|
|
||||||
return (
|
|
||||||
<Avatar
|
|
||||||
key={i}
|
|
||||||
id={member.userId}
|
|
||||||
name={member.name}
|
|
||||||
size={size}
|
|
||||||
src={avatarUrl ?? undefined}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</AvatarStack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -111,7 +111,6 @@ limitations under the License.
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--cpd-space-1-5x);
|
gap: var(--cpd-space-1-5x);
|
||||||
font: var(--cpd-font-body-sm-medium);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 800px) {
|
@media (min-width: 800px) {
|
||||||
|
|||||||
@@ -18,13 +18,12 @@ import classNames from "classnames";
|
|||||||
import { FC, HTMLAttributes, ReactNode } from "react";
|
import { FC, HTMLAttributes, ReactNode } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { MatrixClient, RoomMember } from "matrix-js-sdk/src/matrix";
|
import { Heading, Text } from "@vector-im/compound-web";
|
||||||
import { Heading } from "@vector-im/compound-web";
|
import UserProfileIcon from "@vector-im/compound-design-tokens/icons/user-profile.svg?react";
|
||||||
|
|
||||||
import styles from "./Header.module.css";
|
import styles from "./Header.module.css";
|
||||||
import { ReactComponent as Logo } from "./icons/Logo.svg";
|
import Logo from "./icons/Logo.svg?react";
|
||||||
import { Avatar, Size } from "./Avatar";
|
import { Avatar, Size } from "./Avatar";
|
||||||
import { Facepile } from "./Facepile";
|
|
||||||
import { EncryptionLock } from "./room/EncryptionLock";
|
import { EncryptionLock } from "./room/EncryptionLock";
|
||||||
import { useMediaQuery } from "./useMediaQuery";
|
import { useMediaQuery } from "./useMediaQuery";
|
||||||
|
|
||||||
@@ -118,8 +117,7 @@ interface RoomHeaderInfoProps {
|
|||||||
name: string;
|
name: string;
|
||||||
avatarUrl: string | null;
|
avatarUrl: string | null;
|
||||||
encrypted: boolean;
|
encrypted: boolean;
|
||||||
participants: RoomMember[];
|
participantCount: number;
|
||||||
client: MatrixClient;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RoomHeaderInfo: FC<RoomHeaderInfoProps> = ({
|
export const RoomHeaderInfo: FC<RoomHeaderInfoProps> = ({
|
||||||
@@ -127,8 +125,7 @@ export const RoomHeaderInfo: FC<RoomHeaderInfoProps> = ({
|
|||||||
name,
|
name,
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
encrypted,
|
encrypted,
|
||||||
participants,
|
participantCount,
|
||||||
client,
|
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const size = useMediaQuery("(max-width: 550px)") ? "sm" : "lg";
|
const size = useMediaQuery("(max-width: 550px)") ? "sm" : "lg";
|
||||||
@@ -153,10 +150,16 @@ export const RoomHeaderInfo: FC<RoomHeaderInfoProps> = ({
|
|||||||
</Heading>
|
</Heading>
|
||||||
<EncryptionLock encrypted={encrypted} />
|
<EncryptionLock encrypted={encrypted} />
|
||||||
</div>
|
</div>
|
||||||
{participants.length > 0 && (
|
{participantCount > 0 && (
|
||||||
<div className={styles.participantsLine}>
|
<div className={styles.participantsLine}>
|
||||||
<Facepile client={client} members={participants} size={20} />
|
<UserProfileIcon
|
||||||
{t("{{count, number}}", { count: participants.length })}
|
width={20}
|
||||||
|
height={20}
|
||||||
|
aria-label={t("Participants")}
|
||||||
|
/>
|
||||||
|
<Text as="span" size="sm" weight="medium">
|
||||||
|
{t("{{count, number}}", { count: participantCount })}
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,96 +14,18 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
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 {
|
.modal {
|
||||||
position: fixed;
|
|
||||||
z-index: 101;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog {
|
.dialog {
|
||||||
left: 50%;
|
|
||||||
top: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
inline-size: 520px;
|
inline-size: 520px;
|
||||||
max-inline-size: 90%;
|
max-inline-size: 90%;
|
||||||
max-block-size: 600px;
|
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 {
|
.content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -27,11 +27,12 @@ import {
|
|||||||
} from "@radix-ui/react-dialog";
|
} from "@radix-ui/react-dialog";
|
||||||
import { Drawer } from "vaul";
|
import { Drawer } from "vaul";
|
||||||
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
||||||
import { ReactComponent as CloseIcon } from "@vector-im/compound-design-tokens/icons/close.svg";
|
import CloseIcon from "@vector-im/compound-design-tokens/icons/close.svg?react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Heading } from "@vector-im/compound-web";
|
import { Heading } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import styles from "./Modal.module.css";
|
import styles from "./Modal.module.css";
|
||||||
|
import overlayStyles from "./Overlay.module.css";
|
||||||
import { useMediaQuery } from "./useMediaQuery";
|
import { useMediaQuery } from "./useMediaQuery";
|
||||||
import { Glass } from "./Glass";
|
import { Glass } from "./Glass";
|
||||||
|
|
||||||
@@ -85,9 +86,14 @@ export function Modal({
|
|||||||
dismissible={onDismiss !== undefined}
|
dismissible={onDismiss !== undefined}
|
||||||
>
|
>
|
||||||
<Drawer.Portal>
|
<Drawer.Portal>
|
||||||
<Drawer.Overlay className={styles.overlay} />
|
<Drawer.Overlay className={classNames(overlayStyles.bg)} />
|
||||||
<Drawer.Content
|
<Drawer.Content
|
||||||
className={classNames(className, styles.modal, styles.drawer)}
|
className={classNames(
|
||||||
|
className,
|
||||||
|
overlayStyles.overlay,
|
||||||
|
styles.modal,
|
||||||
|
styles.drawer
|
||||||
|
)}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
@@ -108,12 +114,18 @@ export function Modal({
|
|||||||
<DialogRoot open={open} onOpenChange={onOpenChange}>
|
<DialogRoot open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogPortal>
|
<DialogPortal>
|
||||||
<DialogOverlay
|
<DialogOverlay
|
||||||
className={classNames(styles.overlay, styles.dialogOverlay)}
|
className={classNames(overlayStyles.bg, overlayStyles.animate)}
|
||||||
/>
|
/>
|
||||||
<DialogContent asChild {...rest}>
|
<DialogContent asChild {...rest}>
|
||||||
<Glass
|
<Glass
|
||||||
frosted
|
frosted
|
||||||
className={classNames(className, styles.modal, styles.dialog)}
|
className={classNames(
|
||||||
|
className,
|
||||||
|
overlayStyles.overlay,
|
||||||
|
overlayStyles.animate,
|
||||||
|
styles.modal,
|
||||||
|
styles.dialog
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
|
|||||||
99
src/Overlay.module.css
Normal file
99
src/Overlay.module.css
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.bg {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 100;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(3, 12, 27, 0.528);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg.animate[data-state="open"] {
|
||||||
|
animation: fade-in 200ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-out {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg.animate[data-state="closed"] {
|
||||||
|
animation: fade-out 130ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 101;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay.animate {
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay.animate[data-state="open"] {
|
||||||
|
animation: zoom-in 200ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay.animate[data-state="closed"] {
|
||||||
|
animation: zoom-out 130ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion) {
|
||||||
|
.overlay.animate[data-state="open"] {
|
||||||
|
animation-name: fade-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay.animate[data-state="closed"] {
|
||||||
|
animation-name: fade-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2022 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 { useCallback, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
import {
|
|
||||||
SequenceDiagramViewer,
|
|
||||||
SequenceDiagramMatrixEvent,
|
|
||||||
} from "./room/GroupCallInspector";
|
|
||||||
import { FieldRow, InputField } from "./input/Input";
|
|
||||||
import { usePageTitle } from "./usePageTitle";
|
|
||||||
|
|
||||||
interface DebugLog {
|
|
||||||
localUserId: string;
|
|
||||||
eventsByUserId: { [userId: string]: SequenceDiagramMatrixEvent[] };
|
|
||||||
remoteUserIds: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SequenceDiagramViewerPage() {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
usePageTitle(t("Inspector"));
|
|
||||||
|
|
||||||
const [debugLog, setDebugLog] = useState<DebugLog>();
|
|
||||||
const [selectedUserId, setSelectedUserId] = useState<string>();
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
const onChangeDebugLog = useCallback((e) => {
|
|
||||||
if (e.target.files && e.target.files.length > 0) {
|
|
||||||
e.target.files[0].text().then((text: string) => {
|
|
||||||
setDebugLog(JSON.parse(text));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ marginTop: 20 }}>
|
|
||||||
<FieldRow>
|
|
||||||
<InputField
|
|
||||||
type="file"
|
|
||||||
id="debugLog"
|
|
||||||
name="debugLog"
|
|
||||||
label={t("Debug log")}
|
|
||||||
onChange={onChangeDebugLog}
|
|
||||||
/>
|
|
||||||
</FieldRow>
|
|
||||||
{debugLog && selectedUserId && (
|
|
||||||
<SequenceDiagramViewer
|
|
||||||
localUserId={debugLog.localUserId}
|
|
||||||
selectedUserId={selectedUserId}
|
|
||||||
onSelectUserId={setSelectedUserId}
|
|
||||||
remoteUserIds={debugLog.remoteUserIds}
|
|
||||||
events={debugLog.eventsByUserId[selectedUserId]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
38
src/Toast.module.css
Normal file
38
src/Toast.module.css
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
color: var(--cpd-color-text-on-solid-primary);
|
||||||
|
background: var(--cpd-color-alpha-gray-1200);
|
||||||
|
padding-inline: var(--cpd-space-3x);
|
||||||
|
padding-block: var(--cpd-space-1x);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--cpd-radius-pill-effect);
|
||||||
|
box-shadow: var(--small-drop-shadow);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--cpd-space-1x);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast > h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast > svg {
|
||||||
|
color: var(--cpd-color-icon-on-solid-primary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-inline-end: calc(-1 * var(--cpd-space-1x));
|
||||||
|
}
|
||||||
108
src/Toast.tsx
Normal file
108
src/Toast.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
/*
|
||||||
|
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 {
|
||||||
|
ComponentType,
|
||||||
|
FC,
|
||||||
|
SVGAttributes,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
} from "react";
|
||||||
|
import {
|
||||||
|
Root as DialogRoot,
|
||||||
|
Portal as DialogPortal,
|
||||||
|
Overlay as DialogOverlay,
|
||||||
|
Content as DialogContent,
|
||||||
|
Close as DialogClose,
|
||||||
|
Title as DialogTitle,
|
||||||
|
} from "@radix-ui/react-dialog";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { Text } from "@vector-im/compound-web";
|
||||||
|
|
||||||
|
import styles from "./Toast.module.css";
|
||||||
|
import overlayStyles from "./Overlay.module.css";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/**
|
||||||
|
* The controlled open state of the toast.
|
||||||
|
*/
|
||||||
|
open: boolean;
|
||||||
|
/**
|
||||||
|
* Callback for when the user dismisses the toast.
|
||||||
|
*/
|
||||||
|
onDismiss: () => void;
|
||||||
|
/**
|
||||||
|
* A number of milliseconds after which the toast should be automatically
|
||||||
|
* dismissed.
|
||||||
|
*/
|
||||||
|
autoDismiss?: number;
|
||||||
|
children: string;
|
||||||
|
/**
|
||||||
|
* A supporting icon to display within the toast.
|
||||||
|
*/
|
||||||
|
Icon?: ComponentType<SVGAttributes<SVGElement>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A temporary message shown in an overlay in the center of the screen.
|
||||||
|
*/
|
||||||
|
export const Toast: FC<Props> = ({
|
||||||
|
open,
|
||||||
|
onDismiss,
|
||||||
|
autoDismiss,
|
||||||
|
children,
|
||||||
|
Icon,
|
||||||
|
}) => {
|
||||||
|
const onOpenChange = useCallback(
|
||||||
|
(open: boolean) => {
|
||||||
|
if (!open) onDismiss();
|
||||||
|
},
|
||||||
|
[onDismiss]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && autoDismiss !== undefined) {
|
||||||
|
const timeout = setTimeout(onDismiss, autoDismiss);
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
}, [open, autoDismiss, onDismiss]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogRoot open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogPortal>
|
||||||
|
<DialogOverlay
|
||||||
|
className={classNames(overlayStyles.bg, overlayStyles.animate)}
|
||||||
|
/>
|
||||||
|
<DialogContent asChild>
|
||||||
|
<DialogClose
|
||||||
|
className={classNames(
|
||||||
|
overlayStyles.overlay,
|
||||||
|
overlayStyles.animate,
|
||||||
|
styles.toast
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<DialogTitle asChild>
|
||||||
|
<Text as="h3" size="sm" weight="semibold">
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
</DialogTitle>
|
||||||
|
{Icon && <Icon width={20} height={20} aria-hidden />}
|
||||||
|
</DialogClose>
|
||||||
|
</DialogContent>
|
||||||
|
</DialogPortal>
|
||||||
|
</DialogRoot>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -24,10 +24,10 @@ import { PopoverMenuTrigger } from "./popover/PopoverMenu";
|
|||||||
import { Menu } from "./Menu";
|
import { Menu } from "./Menu";
|
||||||
import { TooltipTrigger } from "./Tooltip";
|
import { TooltipTrigger } from "./Tooltip";
|
||||||
import { Avatar, Size } from "./Avatar";
|
import { Avatar, Size } from "./Avatar";
|
||||||
import { ReactComponent as UserIcon } from "./icons/User.svg";
|
import UserIcon from "./icons/User.svg?react";
|
||||||
import { ReactComponent as SettingsIcon } from "./icons/Settings.svg";
|
import SettingsIcon from "./icons/Settings.svg?react";
|
||||||
import { ReactComponent as LoginIcon } from "./icons/Login.svg";
|
import LoginIcon from "./icons/Login.svg?react";
|
||||||
import { ReactComponent as LogoutIcon } from "./icons/Logout.svg";
|
import LogoutIcon from "./icons/Logout.svg?react";
|
||||||
import { Body } from "./typography/Typography";
|
import { Body } from "./typography/Typography";
|
||||||
import styles from "./UserMenu.module.css";
|
import styles from "./UserMenu.module.css";
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { FC, FormEvent, useCallback, useRef, useState } from "react";
|
|||||||
import { useHistory, useLocation, Link } from "react-router-dom";
|
import { useHistory, useLocation, Link } from "react-router-dom";
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { ReactComponent as Logo } from "../icons/LogoLarge.svg";
|
import Logo from "../icons/LogoLarge.svg?react";
|
||||||
import { useClient } from "../ClientContext";
|
import { useClient } from "../ClientContext";
|
||||||
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
|
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import { Button } from "../button";
|
|||||||
import { useClientLegacy } from "../ClientContext";
|
import { useClientLegacy } from "../ClientContext";
|
||||||
import { useInteractiveRegistration } from "./useInteractiveRegistration";
|
import { useInteractiveRegistration } from "./useInteractiveRegistration";
|
||||||
import styles from "./LoginPage.module.css";
|
import styles from "./LoginPage.module.css";
|
||||||
import { ReactComponent as Logo } from "../icons/LogoLarge.svg";
|
import Logo from "../icons/LogoLarge.svg?react";
|
||||||
import { LoadingView } from "../FullScreenView";
|
import { LoadingView } from "../FullScreenView";
|
||||||
import { useRecaptcha } from "./useRecaptcha";
|
import { useRecaptcha } from "./useRecaptcha";
|
||||||
import { Caption, Link } from "../typography/Typography";
|
import { Caption, Link } from "../typography/Typography";
|
||||||
|
|||||||
@@ -20,18 +20,18 @@ import { useButton } from "@react-aria/button";
|
|||||||
import { mergeProps, useObjectRef } from "@react-aria/utils";
|
import { mergeProps, useObjectRef } from "@react-aria/utils";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Tooltip } from "@vector-im/compound-web";
|
import { Tooltip } from "@vector-im/compound-web";
|
||||||
import { ReactComponent as MicOnSolidIcon } from "@vector-im/compound-design-tokens/icons/mic-on-solid.svg";
|
import MicOnSolidIcon from "@vector-im/compound-design-tokens/icons/mic-on-solid.svg?react";
|
||||||
import { ReactComponent as MicOffSolidIcon } from "@vector-im/compound-design-tokens/icons/mic-off-solid.svg";
|
import MicOffSolidIcon from "@vector-im/compound-design-tokens/icons/mic-off-solid.svg?react";
|
||||||
import { ReactComponent as VideoCallIcon } from "@vector-im/compound-design-tokens/icons/video-call.svg";
|
import VideoCallIcon from "@vector-im/compound-design-tokens/icons/video-call.svg?react";
|
||||||
import { ReactComponent as VideoCallOffIcon } from "@vector-im/compound-design-tokens/icons/video-call-off.svg";
|
import VideoCallOffIcon from "@vector-im/compound-design-tokens/icons/video-call-off.svg?react";
|
||||||
import { ReactComponent as EndCallIcon } from "@vector-im/compound-design-tokens/icons/end-call.svg";
|
import EndCallIcon from "@vector-im/compound-design-tokens/icons/end-call.svg?react";
|
||||||
import { ReactComponent as ShareScreenSolidIcon } from "@vector-im/compound-design-tokens/icons/share-screen-solid.svg";
|
import ShareScreenSolidIcon from "@vector-im/compound-design-tokens/icons/share-screen-solid.svg?react";
|
||||||
import { ReactComponent as SettingsSolidIcon } from "@vector-im/compound-design-tokens/icons/settings-solid.svg";
|
import SettingsSolidIcon from "@vector-im/compound-design-tokens/icons/settings-solid.svg?react";
|
||||||
import { ReactComponent as ChevronDownIcon } from "@vector-im/compound-design-tokens/icons/chevron-down.svg";
|
import ChevronDownIcon from "@vector-im/compound-design-tokens/icons/chevron-down.svg?react";
|
||||||
|
|
||||||
import styles from "./Button.module.css";
|
import styles from "./Button.module.css";
|
||||||
import { ReactComponent as Fullscreen } from "../icons/Fullscreen.svg";
|
import Fullscreen from "../icons/Fullscreen.svg?react";
|
||||||
import { ReactComponent as FullscreenExit } from "../icons/FullscreenExit.svg";
|
import FullscreenExit from "../icons/FullscreenExit.svg?react";
|
||||||
import { VolumeIcon } from "./VolumeIcon";
|
import { VolumeIcon } from "./VolumeIcon";
|
||||||
|
|
||||||
export type ButtonVariant =
|
export type ButtonVariant =
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ limitations under the License.
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import useClipboard from "react-use-clipboard";
|
import useClipboard from "react-use-clipboard";
|
||||||
|
|
||||||
import { ReactComponent as CheckIcon } from "../icons/Check.svg";
|
import CheckIcon from "../icons/Check.svg?react";
|
||||||
import { ReactComponent as CopyIcon } from "../icons/Copy.svg";
|
import CopyIcon from "../icons/Copy.svg?react";
|
||||||
import { Button, ButtonVariant } from "./Button";
|
import { Button, ButtonVariant } from "./Button";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -17,15 +17,15 @@ limitations under the License.
|
|||||||
import { ComponentPropsWithoutRef, FC } from "react";
|
import { ComponentPropsWithoutRef, FC } from "react";
|
||||||
import { Button } from "@vector-im/compound-web";
|
import { Button } from "@vector-im/compound-web";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ReactComponent as UserAddSolidIcon } from "@vector-im/compound-design-tokens/icons/user-add-solid.svg";
|
import UserAddSolidIcon from "@vector-im/compound-design-tokens/icons/user-add-solid.svg?react";
|
||||||
|
|
||||||
export const ShareButton: FC<
|
export const InviteButton: FC<
|
||||||
Omit<ComponentPropsWithoutRef<"button">, "children">
|
Omit<ComponentPropsWithoutRef<"button">, "children">
|
||||||
> = (props) => {
|
> = (props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Button kind="secondary" size="sm" Icon={UserAddSolidIcon} {...props}>
|
<Button kind="secondary" size="sm" Icon={UserAddSolidIcon} {...props}>
|
||||||
{t("Share")}
|
{t("Invite")}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -17,9 +17,9 @@ limitations under the License.
|
|||||||
|
|
||||||
import { ComponentPropsWithoutRef, FC } from "react";
|
import { ComponentPropsWithoutRef, FC } from "react";
|
||||||
|
|
||||||
import { ReactComponent as AudioMuted } from "../icons/AudioMuted.svg";
|
import AudioMuted from "../icons/AudioMuted.svg?react";
|
||||||
import { ReactComponent as AudioLow } from "../icons/AudioLow.svg";
|
import AudioLow from "../icons/AudioLow.svg?react";
|
||||||
import { ReactComponent as Audio } from "../icons/Audio.svg";
|
import Audio from "../icons/Audio.svg?react";
|
||||||
|
|
||||||
interface Props extends ComponentPropsWithoutRef<"svg"> {
|
interface Props extends ComponentPropsWithoutRef<"svg"> {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
import { Avatar, Size } from "../Avatar";
|
import { Avatar, Size } from "../Avatar";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { ReactComponent as EditIcon } from "../icons/Edit.svg";
|
import EditIcon from "../icons/Edit.svg?react";
|
||||||
import styles from "./AvatarInputField.module.css";
|
import styles from "./AvatarInputField.module.css";
|
||||||
|
|
||||||
interface Props extends AllHTMLAttributes<HTMLInputElement> {
|
interface Props extends AllHTMLAttributes<HTMLInputElement> {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
import styles from "./Input.module.css";
|
import styles from "./Input.module.css";
|
||||||
import { ReactComponent as CheckIcon } from "../icons/Check.svg";
|
import CheckIcon from "../icons/Check.svg?react";
|
||||||
import { TranslatedError } from "../TranslatedError";
|
import { TranslatedError } from "../TranslatedError";
|
||||||
|
|
||||||
interface FieldRowProps {
|
interface FieldRowProps {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Popover } from "../popover/Popover";
|
import { Popover } from "../popover/Popover";
|
||||||
import { ListBox } from "../ListBox";
|
import { ListBox } from "../ListBox";
|
||||||
import styles from "./SelectInput.module.css";
|
import styles from "./SelectInput.module.css";
|
||||||
import { ReactComponent as ArrowDownIcon } from "../icons/ArrowDown.svg";
|
import ArrowDownIcon from "../icons/ArrowDown.svg?react";
|
||||||
|
|
||||||
interface Props extends AriaSelectOptions<object> {
|
interface Props extends AriaSelectOptions<object> {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import { useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import styles from "./StarRatingInput.module.css";
|
import styles from "./StarRatingInput.module.css";
|
||||||
import { ReactComponent as StarSelected } from "../icons/StarSelected.svg";
|
import StarSelected from "../icons/StarSelected.svg?react";
|
||||||
import { ReactComponent as StarUnselected } from "../icons/StarUnselected.svg";
|
import StarUnselected from "../icons/StarUnselected.svg?react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
starCount: number;
|
starCount: number;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ limitations under the License.
|
|||||||
import { FC, MouseEvent, useCallback, useMemo, useState } from "react";
|
import { FC, MouseEvent, useCallback, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Button, Text } from "@vector-im/compound-web";
|
import { Button, Text } from "@vector-im/compound-web";
|
||||||
import { ReactComponent as PopOutIcon } from "@vector-im/compound-design-tokens/icons/pop-out.svg";
|
import PopOutIcon from "@vector-im/compound-design-tokens/icons/pop-out.svg?react";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
import { Modal } from "../Modal";
|
import { Modal } from "../Modal";
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ limitations under the License.
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { Tooltip } from "@vector-im/compound-web";
|
import { Tooltip } from "@vector-im/compound-web";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ReactComponent as LockIcon } from "@vector-im/compound-design-tokens/icons/lock.svg";
|
import LockIcon from "@vector-im/compound-design-tokens/icons/lock.svg?react";
|
||||||
import { ReactComponent as LockOffIcon } from "@vector-im/compound-design-tokens/icons/lock-off.svg";
|
import LockOffIcon from "@vector-im/compound-design-tokens/icons/lock-off.svg?react";
|
||||||
|
|
||||||
import styles from "./EncryptionLock.module.css";
|
import styles from "./EncryptionLock.module.css";
|
||||||
|
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2022 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.inspector {
|
|
||||||
background-color: var(--cpd-color-bg-subtle-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollContainer {
|
|
||||||
height: 100%;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sequenceDiagramViewer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selectInput {
|
|
||||||
align-self: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sequenceDiagramViewer :global(.messageText) {
|
|
||||||
font-size: var(--font-size-caption);
|
|
||||||
fill: var(--cpd-color-text-primary) !important;
|
|
||||||
stroke: var(--cpd-color-text-primary) !important;
|
|
||||||
}
|
|
||||||
@@ -1,542 +0,0 @@
|
|||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-nocheck
|
|
||||||
|
|
||||||
/*
|
|
||||||
Copyright 2022 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 * as Sentry from "@sentry/react";
|
|
||||||
import { Resizable } from "re-resizable";
|
|
||||||
import {
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
useReducer,
|
|
||||||
useRef,
|
|
||||||
createContext,
|
|
||||||
useContext,
|
|
||||||
Dispatch,
|
|
||||||
SetStateAction,
|
|
||||||
ReactNode,
|
|
||||||
} from "react";
|
|
||||||
import ReactJson, { CollapsedFieldProps } from "react-json-view";
|
|
||||||
import mermaid from "mermaid";
|
|
||||||
import { Item } from "@react-stately/collections";
|
|
||||||
import { MatrixEvent, IContent } from "matrix-js-sdk/src/models/event";
|
|
||||||
import {
|
|
||||||
GroupCall,
|
|
||||||
GroupCallError,
|
|
||||||
GroupCallEvent,
|
|
||||||
} from "matrix-js-sdk/src/webrtc/groupCall";
|
|
||||||
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
|
|
||||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
|
||||||
import {
|
|
||||||
CallEvent,
|
|
||||||
CallState,
|
|
||||||
CallError,
|
|
||||||
MatrixCall,
|
|
||||||
VoipEvent,
|
|
||||||
} from "matrix-js-sdk/src/webrtc/call";
|
|
||||||
|
|
||||||
import styles from "./GroupCallInspector.module.css";
|
|
||||||
import { SelectInput } from "../input/SelectInput";
|
|
||||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
|
||||||
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
|
|
||||||
|
|
||||||
interface InspectorContextState {
|
|
||||||
eventsByUserId?: { [userId: string]: SequenceDiagramMatrixEvent[] };
|
|
||||||
remoteUserIds?: string[];
|
|
||||||
localUserId?: string;
|
|
||||||
localSessionId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultCollapsedFields = [
|
|
||||||
"org.matrix.msc3401.call",
|
|
||||||
"org.matrix.msc3401.call.member",
|
|
||||||
"calls",
|
|
||||||
"callStats",
|
|
||||||
"hangupCalls",
|
|
||||||
"toDeviceEvents",
|
|
||||||
"sentVoipEvents",
|
|
||||||
"content",
|
|
||||||
];
|
|
||||||
|
|
||||||
function shouldCollapse({ name }: CollapsedFieldProps) {
|
|
||||||
return name ? defaultCollapsedFields.includes(name) : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getUserName(userId: string) {
|
|
||||||
const match = userId.match(/@([^:]+):/);
|
|
||||||
|
|
||||||
return match && match.length > 0
|
|
||||||
? match[1].replace("-", " ").replace(/\W/g, "")
|
|
||||||
: userId.replace(/\W/g, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatContent(type: string, content: CallEventContent) {
|
|
||||||
if (type === "m.call.hangup") {
|
|
||||||
return `callId: ${content.call_id.slice(-4)} reason: ${
|
|
||||||
content.reason
|
|
||||||
} senderSID: ${content.sender_session_id} destSID: ${
|
|
||||||
content.dest_session_id
|
|
||||||
}`;
|
|
||||||
}
|
|
||||||
if (type.startsWith("m.call.")) {
|
|
||||||
return `callId: ${content.call_id?.slice(-4)} senderSID: ${
|
|
||||||
content.sender_session_id
|
|
||||||
} destSID: ${content.dest_session_id}`;
|
|
||||||
} else if (type === "org.matrix.msc3401.call.member") {
|
|
||||||
const call =
|
|
||||||
content["m.calls"] &&
|
|
||||||
content["m.calls"].length > 0 &&
|
|
||||||
content["m.calls"][0];
|
|
||||||
const device =
|
|
||||||
call &&
|
|
||||||
call["m.devices"] &&
|
|
||||||
call["m.devices"].length > 0 &&
|
|
||||||
call["m.devices"][0];
|
|
||||||
return `conf_id: ${call && call["m.call_id"].slice(-4)} sessionId: ${
|
|
||||||
device && device.session_id
|
|
||||||
}`;
|
|
||||||
} else {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const dateFormatter = new Intl.DateTimeFormat([], {
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
second: "2-digit",
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore the linter does not know about this property of the DataTimeFormatOptions
|
|
||||||
fractionalSecondDigits: 3,
|
|
||||||
});
|
|
||||||
|
|
||||||
function formatTimestamp(timestamp: number | Date) {
|
|
||||||
return dateFormatter.format(timestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatType(event: SequenceDiagramMatrixEvent): string {
|
|
||||||
if (event.content.msgtype === "m.bad.encrypted") return "Undecryptable";
|
|
||||||
return event.type;
|
|
||||||
}
|
|
||||||
|
|
||||||
function lineForEvent(event: SequenceDiagramMatrixEvent): string {
|
|
||||||
return `${getUserName(event.from)} ${
|
|
||||||
event.ignored ? "-x" : "->>"
|
|
||||||
} ${getUserName(event.to)}: ${formatTimestamp(event.timestamp)} ${formatType(
|
|
||||||
event
|
|
||||||
)} ${formatContent(event.type, event.content)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const InspectorContext =
|
|
||||||
createContext<
|
|
||||||
[InspectorContextState, Dispatch<SetStateAction<InspectorContextState>>]
|
|
||||||
>(undefined);
|
|
||||||
|
|
||||||
export function InspectorContextProvider({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: ReactNode;
|
|
||||||
}) {
|
|
||||||
// We take the tuple of [currentState, setter] and stick
|
|
||||||
// it straight into the context for other things to call
|
|
||||||
// the setState method... this feels like a fairly severe
|
|
||||||
// contortion of the hooks API - is this really the best way
|
|
||||||
// to do this?
|
|
||||||
const context = useState<InspectorContextState>({});
|
|
||||||
return (
|
|
||||||
<InspectorContext.Provider value={context}>
|
|
||||||
{children}
|
|
||||||
</InspectorContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type CallEventContent = {
|
|
||||||
["m.calls"]: {
|
|
||||||
["m.devices"]: { session_id: string; [x: string]: unknown }[];
|
|
||||||
["m.call_id"]: string;
|
|
||||||
}[];
|
|
||||||
} & {
|
|
||||||
call_id: string;
|
|
||||||
reason: string;
|
|
||||||
sender_session_id: string;
|
|
||||||
dest_session_id: string;
|
|
||||||
} & IContent;
|
|
||||||
|
|
||||||
export type SequenceDiagramMatrixEvent = {
|
|
||||||
to: string;
|
|
||||||
from: string;
|
|
||||||
timestamp: number;
|
|
||||||
type: string;
|
|
||||||
content: CallEventContent;
|
|
||||||
ignored: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface SequenceDiagramViewerProps {
|
|
||||||
localUserId: string;
|
|
||||||
remoteUserIds: string[];
|
|
||||||
selectedUserId: string;
|
|
||||||
onSelectUserId: (userId: string) => void;
|
|
||||||
events: SequenceDiagramMatrixEvent[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SequenceDiagramViewer({
|
|
||||||
localUserId,
|
|
||||||
remoteUserIds,
|
|
||||||
selectedUserId,
|
|
||||||
onSelectUserId,
|
|
||||||
events,
|
|
||||||
}: SequenceDiagramViewerProps) {
|
|
||||||
const mermaidElRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
mermaid.initialize({
|
|
||||||
startOnLoad: true,
|
|
||||||
theme: "dark",
|
|
||||||
sequence: {
|
|
||||||
showSequenceNumbers: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const graphDefinition = `sequenceDiagram
|
|
||||||
participant ${getUserName(localUserId)}
|
|
||||||
participant Room
|
|
||||||
participant ${selectedUserId ? getUserName(selectedUserId) : "unknown"}
|
|
||||||
${events ? events.map(lineForEvent).join("\n ") : ""}
|
|
||||||
`;
|
|
||||||
|
|
||||||
mermaid.mermaidAPI.render("mermaid", graphDefinition, (svgCode: string) => {
|
|
||||||
if (!mermaidElRef.current) return;
|
|
||||||
mermaidElRef.current.innerHTML = svgCode;
|
|
||||||
});
|
|
||||||
}, [events, localUserId, selectedUserId]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.scrollContainer}>
|
|
||||||
<div className={styles.sequenceDiagramViewer}>
|
|
||||||
<SelectInput
|
|
||||||
className={styles.selectInput}
|
|
||||||
label="Remote User"
|
|
||||||
selectedKey={selectedUserId}
|
|
||||||
onSelectionChange={(key) => onSelectUserId(key.toString())}
|
|
||||||
>
|
|
||||||
{remoteUserIds.map((userId) => (
|
|
||||||
<Item key={userId}>{userId}</Item>
|
|
||||||
))}
|
|
||||||
</SelectInput>
|
|
||||||
<div id="mermaid" />
|
|
||||||
<div ref={mermaidElRef} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function reducer(
|
|
||||||
state: InspectorContextState,
|
|
||||||
action: {
|
|
||||||
type?: CallEvent | ClientEvent | RoomStateEvent;
|
|
||||||
event?: MatrixEvent;
|
|
||||||
rawEvent?: VoipEvent;
|
|
||||||
callStateEvent?: MatrixEvent;
|
|
||||||
memberStateEvents?: MatrixEvent[];
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
switch (action.type) {
|
|
||||||
case RoomStateEvent.Events: {
|
|
||||||
const { event, callStateEvent, memberStateEvents } = action;
|
|
||||||
|
|
||||||
let eventsByUserId = state.eventsByUserId;
|
|
||||||
let remoteUserIds = state.remoteUserIds;
|
|
||||||
|
|
||||||
if (event) {
|
|
||||||
const fromId = event.getStateKey();
|
|
||||||
|
|
||||||
remoteUserIds =
|
|
||||||
fromId === state.localUserId || eventsByUserId[fromId]
|
|
||||||
? state.remoteUserIds
|
|
||||||
: [...state.remoteUserIds, fromId];
|
|
||||||
|
|
||||||
eventsByUserId = { ...state.eventsByUserId };
|
|
||||||
|
|
||||||
if (event.getStateKey() === state.localUserId) {
|
|
||||||
for (const userId in eventsByUserId) {
|
|
||||||
eventsByUserId[userId] = [
|
|
||||||
...(eventsByUserId[userId] || []),
|
|
||||||
{
|
|
||||||
from: fromId,
|
|
||||||
to: "Room",
|
|
||||||
type: event.getType(),
|
|
||||||
content: event.getContent(),
|
|
||||||
timestamp: event.getTs() || Date.now(),
|
|
||||||
ignored: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
eventsByUserId[fromId] = [
|
|
||||||
...(eventsByUserId[fromId] || []),
|
|
||||||
{
|
|
||||||
from: fromId,
|
|
||||||
to: "Room",
|
|
||||||
type: event.getType(),
|
|
||||||
content: event.getContent(),
|
|
||||||
timestamp: event.getTs() || Date.now(),
|
|
||||||
ignored: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
eventsByUserId,
|
|
||||||
remoteUserIds,
|
|
||||||
callStateEvent: callStateEvent.getContent(),
|
|
||||||
memberStateEvents: Object.fromEntries(
|
|
||||||
memberStateEvents.map((e) => [e.getStateKey(), e.getContent()])
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case ClientEvent.ReceivedVoipEvent: {
|
|
||||||
const event = action.event;
|
|
||||||
const eventsByUserId = { ...state.eventsByUserId };
|
|
||||||
const fromId = event.getSender();
|
|
||||||
const toId = state.localUserId;
|
|
||||||
const content = event.getContent<CallEventContent>();
|
|
||||||
|
|
||||||
const remoteUserIds = eventsByUserId[fromId]
|
|
||||||
? state.remoteUserIds
|
|
||||||
: [...state.remoteUserIds, fromId];
|
|
||||||
|
|
||||||
eventsByUserId[fromId] = [
|
|
||||||
...(eventsByUserId[fromId] || []),
|
|
||||||
{
|
|
||||||
from: fromId,
|
|
||||||
to: toId,
|
|
||||||
type: event.getType(),
|
|
||||||
content,
|
|
||||||
timestamp: event.getTs() || Date.now(),
|
|
||||||
ignored: state.localSessionId !== content.dest_session_id,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return { ...state, eventsByUserId, remoteUserIds };
|
|
||||||
}
|
|
||||||
case CallEvent.SendVoipEvent: {
|
|
||||||
const event = action.rawEvent;
|
|
||||||
const eventsByUserId = { ...state.eventsByUserId };
|
|
||||||
const fromId = state.localUserId;
|
|
||||||
const toId = event.userId as string;
|
|
||||||
|
|
||||||
const remoteUserIds = eventsByUserId[toId]
|
|
||||||
? state.remoteUserIds
|
|
||||||
: [...state.remoteUserIds, toId];
|
|
||||||
|
|
||||||
eventsByUserId[toId] = [
|
|
||||||
...(eventsByUserId[toId] || []),
|
|
||||||
{
|
|
||||||
from: fromId,
|
|
||||||
to: toId,
|
|
||||||
type: event.eventType as string,
|
|
||||||
content: event.content as CallEventContent,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
ignored: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return { ...state, eventsByUserId, remoteUserIds };
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function useGroupCallState(
|
|
||||||
client: MatrixClient,
|
|
||||||
groupCall: GroupCall,
|
|
||||||
otelGroupCallMembership: OTelGroupCallMembership
|
|
||||||
): InspectorContextState {
|
|
||||||
const [state, dispatch] = useReducer(reducer, {
|
|
||||||
localUserId: client.getUserId(),
|
|
||||||
localSessionId: client.getSessionId(),
|
|
||||||
eventsByUserId: {},
|
|
||||||
remoteUserIds: [],
|
|
||||||
callStateEvent: null,
|
|
||||||
memberStateEvents: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function onUpdateRoomState(event?: MatrixEvent) {
|
|
||||||
const callStateEvent = groupCall.room.currentState.getStateEvents(
|
|
||||||
"org.matrix.msc3401.call",
|
|
||||||
groupCall.groupCallId
|
|
||||||
);
|
|
||||||
|
|
||||||
const memberStateEvents = groupCall.room.currentState.getStateEvents(
|
|
||||||
"org.matrix.msc3401.call.member"
|
|
||||||
);
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: RoomStateEvent.Events,
|
|
||||||
event,
|
|
||||||
callStateEvent,
|
|
||||||
memberStateEvents,
|
|
||||||
});
|
|
||||||
|
|
||||||
otelGroupCallMembership?.onUpdateRoomState(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onReceivedVoipEvent(event: MatrixEvent) {
|
|
||||||
dispatch({ type: ClientEvent.ReceivedVoipEvent, event });
|
|
||||||
|
|
||||||
otelGroupCallMembership?.onReceivedVoipEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSendVoipEvent(event: VoipEvent, call: MatrixCall) {
|
|
||||||
dispatch({ type: CallEvent.SendVoipEvent, rawEvent: event });
|
|
||||||
|
|
||||||
otelGroupCallMembership?.onSendEvent(call, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onCallStateChange(
|
|
||||||
newState: CallState,
|
|
||||||
_: CallState,
|
|
||||||
call: MatrixCall
|
|
||||||
) {
|
|
||||||
otelGroupCallMembership?.onCallStateChange(call, newState);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onCallError(error: CallError, call: MatrixCall) {
|
|
||||||
otelGroupCallMembership.onCallError(error, call);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onGroupCallError(error: GroupCallError) {
|
|
||||||
otelGroupCallMembership.onGroupCallError(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onUndecryptableToDevice(event: MatrixEvent) {
|
|
||||||
dispatch({ type: ClientEvent.ReceivedVoipEvent, event });
|
|
||||||
|
|
||||||
Sentry.captureMessage("Undecryptable to-device Event");
|
|
||||||
// probably unnecessary if it's now captured via otel?
|
|
||||||
PosthogAnalytics.instance.eventUndecryptableToDevice.track(
|
|
||||||
groupCall.groupCallId
|
|
||||||
);
|
|
||||||
|
|
||||||
otelGroupCallMembership.onUndecryptableToDevice(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
client.on(RoomStateEvent.Events, onUpdateRoomState);
|
|
||||||
groupCall.on(CallEvent.SendVoipEvent, onSendVoipEvent);
|
|
||||||
groupCall.on(CallEvent.State, onCallStateChange);
|
|
||||||
groupCall.on(CallEvent.Error, onCallError);
|
|
||||||
groupCall.on(GroupCallEvent.Error, onGroupCallError);
|
|
||||||
//client.on("state", onCallsChanged);
|
|
||||||
//client.on("hangup", onCallHangup);
|
|
||||||
client.on(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent);
|
|
||||||
client.on(ClientEvent.UndecryptableToDeviceEvent, onUndecryptableToDevice);
|
|
||||||
|
|
||||||
onUpdateRoomState();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
client.removeListener(RoomStateEvent.Events, onUpdateRoomState);
|
|
||||||
groupCall.removeListener(CallEvent.SendVoipEvent, onSendVoipEvent);
|
|
||||||
groupCall.removeListener(CallEvent.State, onCallStateChange);
|
|
||||||
groupCall.removeListener(CallEvent.Error, onCallError);
|
|
||||||
groupCall.removeListener(GroupCallEvent.Error, onGroupCallError);
|
|
||||||
//client.removeListener("state", onCallsChanged);
|
|
||||||
//client.removeListener("hangup", onCallHangup);
|
|
||||||
client.removeListener(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent);
|
|
||||||
client.removeListener(
|
|
||||||
ClientEvent.UndecryptableToDeviceEvent,
|
|
||||||
onUndecryptableToDevice
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}, [client, groupCall, otelGroupCallMembership]);
|
|
||||||
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GroupCallInspectorProps {
|
|
||||||
client: MatrixClient;
|
|
||||||
groupCall: GroupCall;
|
|
||||||
otelGroupCallMembership: OTelGroupCallMembership;
|
|
||||||
show: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GroupCallInspector({
|
|
||||||
client,
|
|
||||||
groupCall,
|
|
||||||
otelGroupCallMembership,
|
|
||||||
show,
|
|
||||||
}: GroupCallInspectorProps) {
|
|
||||||
const [currentTab, setCurrentTab] = useState("sequence-diagrams");
|
|
||||||
const [selectedUserId, setSelectedUserId] = useState<string>();
|
|
||||||
const state = useGroupCallState(client, groupCall, otelGroupCallMembership);
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const [_, setState] = useContext(InspectorContext);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setState(state);
|
|
||||||
}, [setState, state]);
|
|
||||||
|
|
||||||
if (!show) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Resizable
|
|
||||||
enable={{ top: true }}
|
|
||||||
defaultSize={{ height: 200, width: 0 }}
|
|
||||||
className={styles.inspector}
|
|
||||||
>
|
|
||||||
<div className={styles.toolbar}>
|
|
||||||
<button onClick={() => setCurrentTab("sequence-diagrams")}>
|
|
||||||
Sequence Diagrams
|
|
||||||
</button>
|
|
||||||
<button onClick={() => setCurrentTab("inspector")}>Inspector</button>
|
|
||||||
</div>
|
|
||||||
{currentTab === "sequence-diagrams" &&
|
|
||||||
state.localUserId &&
|
|
||||||
selectedUserId &&
|
|
||||||
state.eventsByUserId &&
|
|
||||||
state.remoteUserIds && (
|
|
||||||
<SequenceDiagramViewer
|
|
||||||
localUserId={state.localUserId}
|
|
||||||
selectedUserId={selectedUserId}
|
|
||||||
onSelectUserId={setSelectedUserId}
|
|
||||||
remoteUserIds={state.remoteUserIds}
|
|
||||||
events={state.eventsByUserId[selectedUserId]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{currentTab === "inspector" && (
|
|
||||||
<ReactJson
|
|
||||||
theme="monokai"
|
|
||||||
src={state}
|
|
||||||
name={null}
|
|
||||||
indentWidth={2}
|
|
||||||
shouldCollapse={shouldCollapse}
|
|
||||||
displayDataTypes={false}
|
|
||||||
displayObjectSize={false}
|
|
||||||
enableClipboard
|
|
||||||
style={{ height: "100%", overflowY: "scroll" }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Resizable>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -20,7 +20,7 @@ import { MatrixClient } from "matrix-js-sdk/src/client";
|
|||||||
import { Room, isE2EESupported } from "livekit-client";
|
import { Room, isE2EESupported } from "livekit-client";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||||
import { JoinRule, RoomMember } from "matrix-js-sdk/src/matrix";
|
import { JoinRule } from "matrix-js-sdk/src/matrix";
|
||||||
import { Heading, Link, Text } from "@vector-im/compound-web";
|
import { Heading, Link, Text } from "@vector-im/compound-web";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ import { useEnableE2EE } from "../settings/useSetting";
|
|||||||
import { useRoomAvatar } from "./useRoomAvatar";
|
import { useRoomAvatar } from "./useRoomAvatar";
|
||||||
import { useRoomName } from "./useRoomName";
|
import { useRoomName } from "./useRoomName";
|
||||||
import { useJoinRule } from "./useJoinRule";
|
import { useJoinRule } from "./useJoinRule";
|
||||||
import { ShareModal } from "./ShareModal";
|
import { InviteModal } from "./InviteModal";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -111,18 +111,11 @@ export function GroupCallView({
|
|||||||
client,
|
client,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const participatingMembers = useMemo(() => {
|
// Count each member only once, regardless of how many devices they use
|
||||||
const members: RoomMember[] = [];
|
const participantCount = useMemo(
|
||||||
// Count each member only once, regardless of how many devices they use
|
() => new Set<string>(memberships.map((m) => m.member.userId)).size,
|
||||||
const addedUserIds = new Set<string>();
|
[memberships]
|
||||||
for (const membership of memberships) {
|
);
|
||||||
if (!addedUserIds.has(membership.member.userId)) {
|
|
||||||
addedUserIds.add(membership.member.userId);
|
|
||||||
members.push(membership.member);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return members;
|
|
||||||
}, [memberships]);
|
|
||||||
|
|
||||||
const deviceContext = useMediaDevices();
|
const deviceContext = useMediaDevices();
|
||||||
const latestDevices = useRef<MediaDevices>();
|
const latestDevices = useRef<MediaDevices>();
|
||||||
@@ -274,15 +267,15 @@ export function GroupCallView({
|
|||||||
|
|
||||||
const joinRule = useJoinRule(rtcSession.room);
|
const joinRule = useJoinRule(rtcSession.room);
|
||||||
|
|
||||||
const [shareModalOpen, setShareModalOpen] = useState(false);
|
const [shareModalOpen, setInviteModalOpen] = useState(false);
|
||||||
const onDismissShareModal = useCallback(
|
const onDismissInviteModal = useCallback(
|
||||||
() => setShareModalOpen(false),
|
() => setInviteModalOpen(false),
|
||||||
[setShareModalOpen]
|
[setInviteModalOpen]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onShareClickFn = useCallback(
|
const onShareClickFn = useCallback(
|
||||||
() => setShareModalOpen(true),
|
() => setInviteModalOpen(true),
|
||||||
[setShareModalOpen]
|
[setInviteModalOpen]
|
||||||
);
|
);
|
||||||
const onShareClick = joinRule === JoinRule.Public ? onShareClickFn : null;
|
const onShareClick = joinRule === JoinRule.Public ? onShareClickFn : null;
|
||||||
|
|
||||||
@@ -325,10 +318,10 @@ export function GroupCallView({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const shareModal = (
|
const shareModal = (
|
||||||
<ShareModal
|
<InviteModal
|
||||||
room={rtcSession.room}
|
room={rtcSession.room}
|
||||||
open={shareModalOpen}
|
open={shareModalOpen}
|
||||||
onDismiss={onDismissShareModal}
|
onDismiss={onDismissInviteModal}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -340,7 +333,7 @@ export function GroupCallView({
|
|||||||
client={client}
|
client={client}
|
||||||
matrixInfo={matrixInfo}
|
matrixInfo={matrixInfo}
|
||||||
rtcSession={rtcSession}
|
rtcSession={rtcSession}
|
||||||
participatingMembers={participatingMembers}
|
participantCount={participantCount}
|
||||||
onLeave={onLeave}
|
onLeave={onLeave}
|
||||||
hideHeader={hideHeader}
|
hideHeader={hideHeader}
|
||||||
muteStates={muteStates}
|
muteStates={muteStates}
|
||||||
@@ -391,7 +384,7 @@ export function GroupCallView({
|
|||||||
onEnter={() => enterRTCSession(rtcSession)}
|
onEnter={() => enterRTCSession(rtcSession)}
|
||||||
confineToRoom={confineToRoom}
|
confineToRoom={confineToRoom}
|
||||||
hideHeader={hideHeader}
|
hideHeader={hideHeader}
|
||||||
participatingMembers={participatingMembers}
|
participantCount={participantCount}
|
||||||
onShareClick={onShareClick}
|
onShareClick={onShareClick}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ import useMeasure from "react-use-measure";
|
|||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||||
|
|
||||||
import { ReactComponent as LogoMark } from "../icons/LogoMark.svg";
|
import LogoMark from "../icons/LogoMark.svg?react";
|
||||||
import { ReactComponent as LogoType } from "../icons/LogoType.svg";
|
import LogoType from "../icons/LogoType.svg?react";
|
||||||
import type { IWidgetApiRequest } from "matrix-widget-api";
|
import type { IWidgetApiRequest } from "matrix-widget-api";
|
||||||
import {
|
import {
|
||||||
HangupButton,
|
HangupButton,
|
||||||
@@ -69,7 +69,7 @@ import { useWakeLock } from "../useWakeLock";
|
|||||||
import { useMergedRefs } from "../useMergedRefs";
|
import { useMergedRefs } from "../useMergedRefs";
|
||||||
import { MuteStates } from "./MuteStates";
|
import { MuteStates } from "./MuteStates";
|
||||||
import { MatrixInfo } from "./VideoPreview";
|
import { MatrixInfo } from "./VideoPreview";
|
||||||
import { ShareButton } from "../button/ShareButton";
|
import { InviteButton } from "../button/InviteButton";
|
||||||
import { LayoutToggle } from "./LayoutToggle";
|
import { LayoutToggle } from "./LayoutToggle";
|
||||||
import {
|
import {
|
||||||
ECAddonConnectionState,
|
ECAddonConnectionState,
|
||||||
@@ -120,7 +120,7 @@ export interface InCallViewProps {
|
|||||||
rtcSession: MatrixRTCSession;
|
rtcSession: MatrixRTCSession;
|
||||||
livekitRoom: Room;
|
livekitRoom: Room;
|
||||||
muteStates: MuteStates;
|
muteStates: MuteStates;
|
||||||
participatingMembers: RoomMember[];
|
participantCount: number;
|
||||||
onLeave: (error?: Error) => void;
|
onLeave: (error?: Error) => void;
|
||||||
hideHeader: boolean;
|
hideHeader: boolean;
|
||||||
otelGroupCallMembership?: OTelGroupCallMembership;
|
otelGroupCallMembership?: OTelGroupCallMembership;
|
||||||
@@ -134,7 +134,7 @@ export function InCallView({
|
|||||||
rtcSession,
|
rtcSession,
|
||||||
livekitRoom,
|
livekitRoom,
|
||||||
muteStates,
|
muteStates,
|
||||||
participatingMembers,
|
participantCount,
|
||||||
onLeave,
|
onLeave,
|
||||||
hideHeader,
|
hideHeader,
|
||||||
otelGroupCallMembership,
|
otelGroupCallMembership,
|
||||||
@@ -169,7 +169,6 @@ export function InCallView({
|
|||||||
screenSharingTracks.length > 0
|
screenSharingTracks.length > 0
|
||||||
);
|
);
|
||||||
|
|
||||||
//const [showInspector] = useShowInspector();
|
|
||||||
const [showConnectionStats] = useShowConnectionStats();
|
const [showConnectionStats] = useShowConnectionStats();
|
||||||
|
|
||||||
const { hideScreensharing } = useUrlParams();
|
const { hideScreensharing } = useUrlParams();
|
||||||
@@ -411,13 +410,12 @@ export function InCallView({
|
|||||||
name={matrixInfo.roomName}
|
name={matrixInfo.roomName}
|
||||||
avatarUrl={matrixInfo.roomAvatar}
|
avatarUrl={matrixInfo.roomAvatar}
|
||||||
encrypted={matrixInfo.roomEncrypted}
|
encrypted={matrixInfo.roomEncrypted}
|
||||||
participants={participatingMembers}
|
participantCount={participantCount}
|
||||||
client={client}
|
|
||||||
/>
|
/>
|
||||||
</LeftNav>
|
</LeftNav>
|
||||||
<RightNav>
|
<RightNav>
|
||||||
{!reducedControls && onShareClick !== null && (
|
{!reducedControls && onShareClick !== null && (
|
||||||
<ShareButton onClick={onShareClick} />
|
<InviteButton onClick={onShareClick} />
|
||||||
)}
|
)}
|
||||||
</RightNav>
|
</RightNav>
|
||||||
</Header>
|
</Header>
|
||||||
@@ -427,14 +425,6 @@ export function InCallView({
|
|||||||
{renderContent()}
|
{renderContent()}
|
||||||
{footer}
|
{footer}
|
||||||
</div>
|
</div>
|
||||||
{/*otelGroupCallMembership && (
|
|
||||||
<GroupCallInspector
|
|
||||||
client={client}
|
|
||||||
groupCall={groupCall}
|
|
||||||
otelGroupCallMembership={otelGroupCallMembership}
|
|
||||||
show={showInspector}
|
|
||||||
/>
|
|
||||||
)*/}
|
|
||||||
{!noControls && <RageshakeRequestModal {...rageshakeRequestModalProps} />}
|
{!noControls && <RageshakeRequestModal {...rageshakeRequestModalProps} />}
|
||||||
<SettingsModal
|
<SettingsModal
|
||||||
client={client}
|
client={client}
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.copyButton {
|
.url {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--cpd-color-text-secondary);
|
||||||
|
margin-block-end: var(--cpd-space-8x);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
84
src/room/InviteModal.tsx
Normal file
84
src/room/InviteModal.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { FC, MouseEvent, useCallback, useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Room } from "matrix-js-sdk";
|
||||||
|
import { Button, Text } from "@vector-im/compound-web";
|
||||||
|
import LinkIcon from "@vector-im/compound-design-tokens/icons/link.svg?react";
|
||||||
|
import CheckIcon from "@vector-im/compound-design-tokens/icons/check.svg?react";
|
||||||
|
import useClipboard from "react-use-clipboard";
|
||||||
|
|
||||||
|
import { Modal } from "../Modal";
|
||||||
|
import { getAbsoluteRoomUrl } from "../matrix-utils";
|
||||||
|
import styles from "./InviteModal.module.css";
|
||||||
|
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
|
||||||
|
import { Toast } from "../Toast";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
room: Room;
|
||||||
|
open: boolean;
|
||||||
|
onDismiss: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InviteModal: FC<Props> = ({ room, open, onDismiss }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const roomSharedKey = useRoomSharedKey(room.roomId);
|
||||||
|
const url = useMemo(
|
||||||
|
() =>
|
||||||
|
getAbsoluteRoomUrl(room.roomId, room.name, roomSharedKey ?? undefined),
|
||||||
|
[room, roomSharedKey]
|
||||||
|
);
|
||||||
|
const [, setCopied] = useClipboard(url);
|
||||||
|
const [toastOpen, setToastOpen] = useState(false);
|
||||||
|
const onToastDismiss = useCallback(() => setToastOpen(false), [setToastOpen]);
|
||||||
|
|
||||||
|
const onButtonClick = useCallback(
|
||||||
|
(e: MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setCopied();
|
||||||
|
onDismiss();
|
||||||
|
setToastOpen(true);
|
||||||
|
},
|
||||||
|
[setCopied, onDismiss]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal title={t("Invite to this call")} open={open} onDismiss={onDismiss}>
|
||||||
|
<Text className={styles.url} size="sm" weight="semibold">
|
||||||
|
{url}
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
className={styles.button}
|
||||||
|
Icon={LinkIcon}
|
||||||
|
onClick={onButtonClick}
|
||||||
|
data-testid="modal_inviteLink"
|
||||||
|
>
|
||||||
|
{t("Copy link")}
|
||||||
|
</Button>
|
||||||
|
</Modal>
|
||||||
|
<Toast
|
||||||
|
open={toastOpen}
|
||||||
|
onDismiss={onToastDismiss}
|
||||||
|
autoDismiss={2000}
|
||||||
|
Icon={CheckIcon}
|
||||||
|
>
|
||||||
|
{t("Link copied to clipboard")}
|
||||||
|
</Toast>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -17,8 +17,8 @@ limitations under the License.
|
|||||||
import { ChangeEvent, FC, useCallback, useId } from "react";
|
import { ChangeEvent, FC, useCallback, useId } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Tooltip } from "@vector-im/compound-web";
|
import { Tooltip } from "@vector-im/compound-web";
|
||||||
import { ReactComponent as SpotlightViewIcon } from "@vector-im/compound-design-tokens/icons/spotlight-view.svg";
|
import SpotlightViewIcon from "@vector-im/compound-design-tokens/icons/spotlight-view.svg?react";
|
||||||
import { ReactComponent as GridViewIcon } from "@vector-im/compound-design-tokens/icons/grid-view.svg";
|
import GridViewIcon from "@vector-im/compound-design-tokens/icons/grid-view.svg?react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
import styles from "./LayoutToggle.module.css";
|
import styles from "./LayoutToggle.module.css";
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ limitations under the License.
|
|||||||
|
|
||||||
import { FC, useCallback, useState } from "react";
|
import { FC, useCallback, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { MatrixClient, RoomMember } from "matrix-js-sdk/src/matrix";
|
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
import { Button, Link } from "@vector-im/compound-web";
|
import { Button, Link } from "@vector-im/compound-web";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
@@ -27,7 +27,7 @@ import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
|
|||||||
import { useLocationNavigation } from "../useLocationNavigation";
|
import { useLocationNavigation } from "../useLocationNavigation";
|
||||||
import { MatrixInfo, VideoPreview } from "./VideoPreview";
|
import { MatrixInfo, VideoPreview } from "./VideoPreview";
|
||||||
import { MuteStates } from "./MuteStates";
|
import { MuteStates } from "./MuteStates";
|
||||||
import { ShareButton } from "../button/ShareButton";
|
import { InviteButton } from "../button/InviteButton";
|
||||||
import {
|
import {
|
||||||
HangupButton,
|
HangupButton,
|
||||||
MicButton,
|
MicButton,
|
||||||
@@ -44,7 +44,7 @@ interface Props {
|
|||||||
onEnter: () => void;
|
onEnter: () => void;
|
||||||
confineToRoom: boolean;
|
confineToRoom: boolean;
|
||||||
hideHeader: boolean;
|
hideHeader: boolean;
|
||||||
participatingMembers: RoomMember[];
|
participantCount: number;
|
||||||
onShareClick: (() => void) | null;
|
onShareClick: (() => void) | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ export const LobbyView: FC<Props> = ({
|
|||||||
onEnter,
|
onEnter,
|
||||||
confineToRoom,
|
confineToRoom,
|
||||||
hideHeader,
|
hideHeader,
|
||||||
participatingMembers,
|
participantCount,
|
||||||
onShareClick,
|
onShareClick,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -104,12 +104,11 @@ export const LobbyView: FC<Props> = ({
|
|||||||
name={matrixInfo.roomName}
|
name={matrixInfo.roomName}
|
||||||
avatarUrl={matrixInfo.roomAvatar}
|
avatarUrl={matrixInfo.roomAvatar}
|
||||||
encrypted={matrixInfo.roomEncrypted}
|
encrypted={matrixInfo.roomEncrypted}
|
||||||
participants={participatingMembers}
|
participantCount={participantCount}
|
||||||
client={client}
|
|
||||||
/>
|
/>
|
||||||
</LeftNav>
|
</LeftNav>
|
||||||
<RightNav>
|
<RightNav>
|
||||||
{onShareClick !== null && <ShareButton onClick={onShareClick} />}
|
{onShareClick !== null && <InviteButton onClick={onShareClick} />}
|
||||||
</RightNav>
|
</RightNav>
|
||||||
</Header>
|
</Header>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,51 +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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { FC } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { Room } from "matrix-js-sdk";
|
|
||||||
|
|
||||||
import { Modal } from "../Modal";
|
|
||||||
import { CopyButton } from "../button";
|
|
||||||
import { getAbsoluteRoomUrl } from "../matrix-utils";
|
|
||||||
import styles from "./ShareModal.module.css";
|
|
||||||
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
room: Room;
|
|
||||||
open: boolean;
|
|
||||||
onDismiss: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ShareModal: FC<Props> = ({ room, open, onDismiss }) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const roomSharedKey = useRoomSharedKey(room.roomId);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal title={t("Share this call")} open={open} onDismiss={onDismiss}>
|
|
||||||
<p>{t("Copy and share this call link")}</p>
|
|
||||||
<CopyButton
|
|
||||||
className={styles.copyButton}
|
|
||||||
value={getAbsoluteRoomUrl(
|
|
||||||
room.roomId,
|
|
||||||
room.name,
|
|
||||||
roomSharedKey ?? undefined
|
|
||||||
)}
|
|
||||||
data-testid="modal_inviteLink"
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -22,15 +22,14 @@ import { MatrixClient } from "matrix-js-sdk";
|
|||||||
import { Modal } from "../Modal";
|
import { Modal } from "../Modal";
|
||||||
import styles from "./SettingsModal.module.css";
|
import styles from "./SettingsModal.module.css";
|
||||||
import { TabContainer, TabItem } from "../tabs/Tabs";
|
import { TabContainer, TabItem } from "../tabs/Tabs";
|
||||||
import { ReactComponent as AudioIcon } from "../icons/Audio.svg";
|
import AudioIcon from "../icons/Audio.svg?react";
|
||||||
import { ReactComponent as VideoIcon } from "../icons/Video.svg";
|
import VideoIcon from "../icons/Video.svg?react";
|
||||||
import { ReactComponent as DeveloperIcon } from "../icons/Developer.svg";
|
import DeveloperIcon from "../icons/Developer.svg?react";
|
||||||
import { ReactComponent as OverflowIcon } from "../icons/Overflow.svg";
|
import OverflowIcon from "../icons/Overflow.svg?react";
|
||||||
import { ReactComponent as UserIcon } from "../icons/User.svg";
|
import UserIcon from "../icons/User.svg?react";
|
||||||
import { ReactComponent as FeedbackIcon } from "../icons/Feedback.svg";
|
import FeedbackIcon from "../icons/Feedback.svg?react";
|
||||||
import { SelectInput } from "../input/SelectInput";
|
import { SelectInput } from "../input/SelectInput";
|
||||||
import {
|
import {
|
||||||
useShowInspector,
|
|
||||||
useOptInAnalytics,
|
useOptInAnalytics,
|
||||||
useDeveloperSettingsTab,
|
useDeveloperSettingsTab,
|
||||||
useShowConnectionStats,
|
useShowConnectionStats,
|
||||||
@@ -38,8 +37,6 @@ import {
|
|||||||
isFirefox,
|
isFirefox,
|
||||||
} from "./useSetting";
|
} from "./useSetting";
|
||||||
import { FieldRow, InputField } from "../input/Input";
|
import { FieldRow, InputField } from "../input/Input";
|
||||||
import { Button } from "../button";
|
|
||||||
import { useDownloadDebugLog } from "./submit-rageshake";
|
|
||||||
import { Body, Caption } from "../typography/Typography";
|
import { Body, Caption } from "../typography/Typography";
|
||||||
import { AnalyticsNotice } from "../analytics/AnalyticsNotice";
|
import { AnalyticsNotice } from "../analytics/AnalyticsNotice";
|
||||||
import { ProfileSettingsTab } from "./ProfileSettingsTab";
|
import { ProfileSettingsTab } from "./ProfileSettingsTab";
|
||||||
@@ -62,7 +59,6 @@ interface Props {
|
|||||||
export const SettingsModal = (props: Props) => {
|
export const SettingsModal = (props: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [showInspector, setShowInspector] = useShowInspector();
|
|
||||||
const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics();
|
const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics();
|
||||||
const [developerSettingsTab, setDeveloperSettingsTab] =
|
const [developerSettingsTab, setDeveloperSettingsTab] =
|
||||||
useDeveloperSettingsTab();
|
useDeveloperSettingsTab();
|
||||||
@@ -70,8 +66,6 @@ export const SettingsModal = (props: Props) => {
|
|||||||
useShowConnectionStats();
|
useShowConnectionStats();
|
||||||
const [enableE2EE, setEnableE2EE] = useEnableE2EE();
|
const [enableE2EE, setEnableE2EE] = useEnableE2EE();
|
||||||
|
|
||||||
const downloadDebugLog = useDownloadDebugLog();
|
|
||||||
|
|
||||||
// Generate a `SelectInput` with a list of devices for a given device kind.
|
// Generate a `SelectInput` with a list of devices for a given device kind.
|
||||||
const generateDeviceSelection = (devices: MediaDevice, caption: string) => {
|
const generateDeviceSelection = (devices: MediaDevice, caption: string) => {
|
||||||
if (devices.available.length == 0) return null;
|
if (devices.available.length == 0) return null;
|
||||||
@@ -234,18 +228,6 @@ export const SettingsModal = (props: Props) => {
|
|||||||
})}
|
})}
|
||||||
</Body>
|
</Body>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
<FieldRow>
|
|
||||||
<InputField
|
|
||||||
id="showInspector"
|
|
||||||
name="inspector"
|
|
||||||
label={t("Show call inspector")}
|
|
||||||
type="checkbox"
|
|
||||||
checked={showInspector}
|
|
||||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
||||||
setShowInspector(e.target.checked)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</FieldRow>
|
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<InputField
|
<InputField
|
||||||
id="showConnectionStats"
|
id="showConnectionStats"
|
||||||
@@ -275,9 +257,6 @@ export const SettingsModal = (props: Props) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
<FieldRow>
|
|
||||||
<Button onPress={downloadDebugLog}>{t("Download debug logs")}</Button>
|
|
||||||
</FieldRow>
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -14,13 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { ComponentProps, useCallback, useEffect, useState } from "react";
|
||||||
ComponentProps,
|
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import pako from "pako";
|
import pako from "pako";
|
||||||
@@ -30,7 +24,6 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||||||
|
|
||||||
import { getLogsForReport } from "./rageshake";
|
import { getLogsForReport } from "./rageshake";
|
||||||
import { useClient } from "../ClientContext";
|
import { useClient } from "../ClientContext";
|
||||||
import { InspectorContext } from "../room/GroupCallInspector";
|
|
||||||
import { Config } from "../config/Config";
|
import { Config } from "../config/Config";
|
||||||
import { ElementCallOpenTelemetry } from "../otel/otel";
|
import { ElementCallOpenTelemetry } from "../otel/otel";
|
||||||
import { RageshakeRequestModal } from "../room/RageshakeRequestModal";
|
import { RageshakeRequestModal } from "../room/RageshakeRequestModal";
|
||||||
@@ -58,10 +51,6 @@ export function useSubmitRageshake(): {
|
|||||||
} {
|
} {
|
||||||
const { client } = useClient();
|
const { client } = useClient();
|
||||||
|
|
||||||
// The value of the context is the whole tuple returned from setState,
|
|
||||||
// so we just want the current state.
|
|
||||||
const [inspectorState] = useContext(InspectorContext) ?? [];
|
|
||||||
|
|
||||||
const [{ sending, sent, error }, setState] = useState<{
|
const [{ sending, sent, error }, setState] = useState<{
|
||||||
sending: boolean;
|
sending: boolean;
|
||||||
sent: boolean;
|
sent: boolean;
|
||||||
@@ -270,16 +259,6 @@ export function useSubmitRageshake(): {
|
|||||||
gzip(ElementCallOpenTelemetry.instance.rageshakeProcessor!.dump()),
|
gzip(ElementCallOpenTelemetry.instance.rageshakeProcessor!.dump()),
|
||||||
"traces.json.gz"
|
"traces.json.gz"
|
||||||
);
|
);
|
||||||
|
|
||||||
if (inspectorState) {
|
|
||||||
body.append(
|
|
||||||
"file",
|
|
||||||
new Blob([JSON.stringify(inspectorState)], {
|
|
||||||
type: "text/plain",
|
|
||||||
}),
|
|
||||||
"groupcall.txt"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.rageshakeRequestId) {
|
if (opts.rageshakeRequestId) {
|
||||||
@@ -300,7 +279,7 @@ export function useSubmitRageshake(): {
|
|||||||
logger.error(error);
|
logger.error(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[client, inspectorState, sending]
|
[client, sending]
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -311,27 +290,6 @@ export function useSubmitRageshake(): {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDownloadDebugLog(): () => void {
|
|
||||||
const json = useContext(InspectorContext);
|
|
||||||
|
|
||||||
const downloadDebugLog = useCallback(() => {
|
|
||||||
const blob = new Blob([JSON.stringify(json)], { type: "application/json" });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const el = document.createElement("a");
|
|
||||||
el.href = url;
|
|
||||||
el.download = "groupcall.json";
|
|
||||||
el.style.display = "none";
|
|
||||||
document.body.appendChild(el);
|
|
||||||
el.click();
|
|
||||||
setTimeout(() => {
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
el.parentNode!.removeChild(el);
|
|
||||||
}, 0);
|
|
||||||
}, [json]);
|
|
||||||
|
|
||||||
return downloadDebugLog;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useRageshakeRequest(): (
|
export function useRageshakeRequest(): (
|
||||||
roomId: string,
|
roomId: string,
|
||||||
rageshakeRequestId: string
|
rageshakeRequestId: string
|
||||||
|
|||||||
@@ -83,8 +83,6 @@ export const useSpatialAudio = (): DisableableSetting<boolean> => {
|
|||||||
return [false, null];
|
return [false, null];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useShowInspector = () => useSetting("show-inspector", false);
|
|
||||||
|
|
||||||
// null = undecided
|
// null = undecided
|
||||||
export const useOptInAnalytics = (): DisableableSetting<boolean | null> => {
|
export const useOptInAnalytics = (): DisableableSetting<boolean | null> => {
|
||||||
const settingVal = useSetting<boolean | null>("opt-in-analytics", null);
|
const settingVal = useSetting<boolean | null>("opt-in-analytics", null);
|
||||||
|
|||||||
@@ -71,8 +71,7 @@ limitations under the License.
|
|||||||
padding: var(--cpd-space-1x);
|
padding: var(--cpd-space-1x);
|
||||||
padding-block: var(--cpd-space-1x);
|
padding-block: var(--cpd-space-1x);
|
||||||
color: var(--cpd-color-text-primary);
|
color: var(--cpd-color-text-primary);
|
||||||
/* TODO: un-hardcode this color. It comes from the dark theme. */
|
background-color: var(--cpd-color-bg-canvas-default);
|
||||||
background-color: rgba(237, 244, 252, 0.79);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: var(--cpd-radius-pill-effect);
|
border-radius: var(--cpd-radius-pill-effect);
|
||||||
@@ -83,11 +82,6 @@ limitations under the License.
|
|||||||
box-shadow: var(--small-drop-shadow);
|
box-shadow: var(--small-drop-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.cpd-theme-dark) .nameTag {
|
|
||||||
/* TODO: un-hardcode this color. It comes from the light theme. */
|
|
||||||
background-color: rgba(2, 7, 13, 0.77);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nameTag > svg {
|
.nameTag > svg {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ import {
|
|||||||
RoomMember,
|
RoomMember,
|
||||||
RoomMemberEvent,
|
RoomMemberEvent,
|
||||||
} from "matrix-js-sdk/src/models/room-member";
|
} from "matrix-js-sdk/src/models/room-member";
|
||||||
import { ReactComponent as MicOnSolidIcon } from "@vector-im/compound-design-tokens/icons/mic-on-solid.svg";
|
import MicOnSolidIcon from "@vector-im/compound-design-tokens/icons/mic-on-solid.svg?react";
|
||||||
import { ReactComponent as MicOffSolidIcon } from "@vector-im/compound-design-tokens/icons/mic-off-solid.svg";
|
import MicOffSolidIcon from "@vector-im/compound-design-tokens/icons/mic-off-solid.svg?react";
|
||||||
import { Text } from "@vector-im/compound-web";
|
import { Text } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import { Avatar } from "../Avatar";
|
import { Avatar } from "../Avatar";
|
||||||
|
|||||||
59
test/Toast-test.tsx
Normal file
59
test/Toast-test.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
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 { screen, render } from "@testing-library/react";
|
||||||
|
import { Toast } from "../src/Toast";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { withFakeTimers } from "./utils";
|
||||||
|
|
||||||
|
test("Toast renders", () => {
|
||||||
|
render(
|
||||||
|
<Toast open={false} onDismiss={() => {}}>
|
||||||
|
Hello world!
|
||||||
|
</Toast>
|
||||||
|
);
|
||||||
|
expect(screen.queryByRole("dialog")).toBe(null);
|
||||||
|
render(
|
||||||
|
<Toast open={true} onDismiss={() => {}}>
|
||||||
|
Hello world!
|
||||||
|
</Toast>
|
||||||
|
);
|
||||||
|
expect(screen.getByRole("dialog")).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Toast dismisses when clicked", async () => {
|
||||||
|
const onDismiss = jest.fn();
|
||||||
|
render(
|
||||||
|
<Toast open={true} onDismiss={onDismiss}>
|
||||||
|
Hello world!
|
||||||
|
</Toast>
|
||||||
|
);
|
||||||
|
await userEvent.click(screen.getByRole("dialog"));
|
||||||
|
expect(onDismiss).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Toast dismisses itself after the specified timeout", async () => {
|
||||||
|
withFakeTimers(() => {
|
||||||
|
const onDismiss = jest.fn();
|
||||||
|
render(
|
||||||
|
<Toast open={true} onDismiss={onDismiss} autoDismiss={2000}>
|
||||||
|
Hello world!
|
||||||
|
</Toast>
|
||||||
|
);
|
||||||
|
jest.advanceTimersByTime(2000);
|
||||||
|
expect(onDismiss).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
22
test/__snapshots__/Toast-test.tsx.snap
Normal file
22
test/__snapshots__/Toast-test.tsx.snap
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Toast renders 1`] = `
|
||||||
|
<button
|
||||||
|
aria-describedby="radix-:r5:"
|
||||||
|
aria-labelledby="radix-:r4:"
|
||||||
|
class="overlay animate toast"
|
||||||
|
data-state="open"
|
||||||
|
id="radix-:r3:"
|
||||||
|
role="dialog"
|
||||||
|
style="pointer-events: auto;"
|
||||||
|
tabindex="-1"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
class="_font-body-sm-semibold_1jx6b_45"
|
||||||
|
id="radix-:r4:"
|
||||||
|
>
|
||||||
|
Hello world!
|
||||||
|
</h3>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
// Mock file for SVG imports
|
// Mock file for SVG imports
|
||||||
export default "SvgrURL";
|
const ReactComponent = "svg";
|
||||||
export const ReactComponent = "div";
|
export default ReactComponent;
|
||||||
|
|||||||
@@ -18,15 +18,7 @@ import { Mocked, mocked } from "jest-mock";
|
|||||||
import { RoomState } from "matrix-js-sdk/src/models/room-state";
|
import { RoomState } from "matrix-js-sdk/src/models/room-state";
|
||||||
import { PosthogAnalytics } from "../../src/analytics/PosthogAnalytics";
|
import { PosthogAnalytics } from "../../src/analytics/PosthogAnalytics";
|
||||||
import { checkForParallelCalls } from "../../src/room/checkForParallelCalls";
|
import { checkForParallelCalls } from "../../src/room/checkForParallelCalls";
|
||||||
|
import { withFakeTimers } from "../utils";
|
||||||
const withFakeTimers = (continuation: () => void) => {
|
|
||||||
jest.useFakeTimers();
|
|
||||||
try {
|
|
||||||
continuation();
|
|
||||||
} finally {
|
|
||||||
jest.useRealTimers();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const withMockedPosthog = (
|
const withMockedPosthog = (
|
||||||
continuation: (posthog: Mocked<PosthogAnalytics>) => void
|
continuation: (posthog: Mocked<PosthogAnalytics>) => void
|
||||||
|
|||||||
24
test/utils.ts
Normal file
24
test/utils.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function withFakeTimers(continuation: () => void): void {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
try {
|
||||||
|
continuation();
|
||||||
|
} finally {
|
||||||
|
jest.useRealTimers();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user