Merge branch 'main' into robertlong/spotlight-layout
This commit is contained in:
commit
dec47d21c0
53 changed files with 3531 additions and 2987 deletions
3
.env
3
.env
|
@ -7,6 +7,9 @@
|
||||||
# Used for determining the homeserver to use for short urls etc.
|
# Used for determining the homeserver to use for short urls etc.
|
||||||
# VITE_DEFAULT_HOMESERVER=http://localhost:8008
|
# VITE_DEFAULT_HOMESERVER=http://localhost:8008
|
||||||
|
|
||||||
|
# Used for submitting debug logs to an external rageshake server
|
||||||
|
# VITE_RAGESHAKE_SUBMIT_URL=http://localhost:9110/api/submit
|
||||||
|
|
||||||
# The Sentry DSN to use for error reporting. Leave undefined to disable.
|
# The Sentry DSN to use for error reporting. Leave undefined to disable.
|
||||||
# VITE_SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0
|
# VITE_SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,20 @@ const svgrPlugin = require("vite-plugin-svgr");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
|
stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"],
|
||||||
addons: ["@storybook/addon-links", "@storybook/addon-essentials"],
|
|
||||||
framework: "@storybook/react",
|
framework: "@storybook/react",
|
||||||
core: {
|
core: {
|
||||||
builder: "storybook-builder-vite",
|
builder: "storybook-builder-vite",
|
||||||
},
|
},
|
||||||
async viteFinal(config) {
|
async viteFinal(config) {
|
||||||
|
config.plugins = config.plugins.filter(
|
||||||
|
(item) =>
|
||||||
|
!(
|
||||||
|
Array.isArray(item) &&
|
||||||
|
item.length > 0 &&
|
||||||
|
item[0].name === "vite-plugin-mdx"
|
||||||
|
)
|
||||||
|
);
|
||||||
config.plugins.push(svgrPlugin());
|
config.plugins.push(svgrPlugin());
|
||||||
config.resolve = config.resolve || {};
|
config.resolve = config.resolve || {};
|
||||||
config.resolve.alias = config.resolve.alias || {};
|
config.resolve.alias = config.resolve.alias || {};
|
||||||
|
|
4
CONTRIBUTING.md
Normal file
4
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
Contributing code to Element
|
||||||
|
============================
|
||||||
|
|
||||||
|
Element follows the same pattern as the [matrix-js-sdk](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md).
|
33
index.html
33
index.html
|
@ -1,16 +1,21 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
<head>
|
||||||
<link rel="icon" type="image/svg+xml" href="/src/favicon.png" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
<link rel="icon" type="image/svg+xml" href="/src/favicon.png" />
|
||||||
<title>Matrix Video Chat</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
||||||
<script>
|
<title>
|
||||||
window.global = window;
|
<%- title %>
|
||||||
</script>
|
</title>
|
||||||
</head>
|
<script>
|
||||||
<body>
|
window.global = window;
|
||||||
<div id="root"></div>
|
</script>
|
||||||
<script type="module" src="/src/main.jsx"></script>
|
</head>
|
||||||
</body>
|
|
||||||
</html>
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
10
package.json
10
package.json
|
@ -8,6 +8,7 @@
|
||||||
"build-storybook": "build-storybook"
|
"build-storybook": "build-storybook"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@juggle/resize-observer": "^3.3.1",
|
||||||
"@react-aria/button": "^3.3.4",
|
"@react-aria/button": "^3.3.4",
|
||||||
"@react-aria/dialog": "^3.1.4",
|
"@react-aria/dialog": "^3.1.4",
|
||||||
"@react-aria/focus": "^3.5.0",
|
"@react-aria/focus": "^3.5.0",
|
||||||
|
@ -29,7 +30,9 @@
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#robertlong/group-call",
|
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#robertlong/group-call",
|
||||||
"matrix-react-sdk": "github:matrix-org/matrix-react-sdk#robertlong/group-call",
|
"matrix-react-sdk": "github:matrix-org/matrix-react-sdk#robertlong/group-call",
|
||||||
|
"mermaid": "^8.13.8",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
|
"pako": "^2.0.4",
|
||||||
"postcss-preset-env": "^6.7.0",
|
"postcss-preset-env": "^6.7.0",
|
||||||
"re-resizable": "^6.9.0",
|
"re-resizable": "^6.9.0",
|
||||||
"react": "^17.0.0",
|
"react": "^17.0.0",
|
||||||
|
@ -37,18 +40,17 @@
|
||||||
"react-json-view": "^1.21.3",
|
"react-json-view": "^1.21.3",
|
||||||
"react-router": "6",
|
"react-router": "6",
|
||||||
"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"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.16.5",
|
"@babel/core": "^7.16.5",
|
||||||
"@storybook/addon-actions": "^6.5.0-alpha.5",
|
|
||||||
"@storybook/addon-essentials": "^6.5.0-alpha.5",
|
|
||||||
"@storybook/addon-links": "^6.5.0-alpha.5",
|
|
||||||
"@storybook/react": "^6.5.0-alpha.5",
|
"@storybook/react": "^6.5.0-alpha.5",
|
||||||
"babel-loader": "^8.2.3",
|
"babel-loader": "^8.2.3",
|
||||||
"sass": "^1.42.1",
|
"sass": "^1.42.1",
|
||||||
"storybook-builder-vite": "^0.1.12",
|
"storybook-builder-vite": "^0.1.12",
|
||||||
"vite": "^2.4.2",
|
"vite": "^2.4.2",
|
||||||
|
"vite-plugin-html": "^3.0.3",
|
||||||
"vite-plugin-svgr": "^0.4.0"
|
"vite-plugin-svgr": "^0.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { RoomPage } from "./room/RoomPage";
|
||||||
import { RoomRedirect } from "./room/RoomRedirect";
|
import { RoomRedirect } from "./room/RoomRedirect";
|
||||||
import { ClientProvider } from "./ClientContext";
|
import { ClientProvider } from "./ClientContext";
|
||||||
import { usePageFocusStyle } from "./usePageFocusStyle";
|
import { usePageFocusStyle } from "./usePageFocusStyle";
|
||||||
|
import { SequenceDiagramViewerPage } from "./SequenceDiagramViewerPage";
|
||||||
|
|
||||||
const SentryRoute = Sentry.withSentryRouting(Route);
|
const SentryRoute = Sentry.withSentryRouting(Route);
|
||||||
|
|
||||||
|
@ -48,6 +49,9 @@ export default function App({ history }) {
|
||||||
<SentryRoute path="/room/:roomId?">
|
<SentryRoute path="/room/:roomId?">
|
||||||
<RoomPage />
|
<RoomPage />
|
||||||
</SentryRoute>
|
</SentryRoute>
|
||||||
|
<SentryRoute path="/inspector">
|
||||||
|
<SequenceDiagramViewerPage />
|
||||||
|
</SentryRoute>
|
||||||
<SentryRoute path="*">
|
<SentryRoute path="*">
|
||||||
<RoomRedirect />
|
<RoomRedirect />
|
||||||
</SentryRoute>
|
</SentryRoute>
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
width: 42px;
|
width: 42px;
|
||||||
height: 42px;
|
height: 42px;
|
||||||
border-radius: 42px;
|
border-radius: 42px;
|
||||||
font-size: 36px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xl {
|
.xl {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { ReactComponent as VideoIcon } from "./icons/Video.svg";
|
||||||
import { ReactComponent as ArrowLeftIcon } from "./icons/ArrowLeft.svg";
|
import { ReactComponent as ArrowLeftIcon } from "./icons/ArrowLeft.svg";
|
||||||
import { useButton } from "@react-aria/button";
|
import { useButton } from "@react-aria/button";
|
||||||
import { Subtitle } from "./typography/Typography";
|
import { Subtitle } from "./typography/Typography";
|
||||||
|
import { Avatar } from "./Avatar";
|
||||||
|
|
||||||
export function Header({ children, className, ...rest }) {
|
export function Header({ children, className, ...rest }) {
|
||||||
return (
|
return (
|
||||||
|
@ -60,6 +61,11 @@ export function RoomHeaderInfo({ roomName }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.roomAvatar}>
|
<div className={styles.roomAvatar}>
|
||||||
|
<Avatar
|
||||||
|
size="md"
|
||||||
|
bgKey={roomName}
|
||||||
|
fallback={roomName.slice(0, 1).toUpperCase()}
|
||||||
|
/>
|
||||||
<VideoIcon width={16} height={16} />
|
<VideoIcon width={16} height={16} />
|
||||||
</div>
|
</div>
|
||||||
<Subtitle fontWeight="semiBold">{roomName}</Subtitle>
|
<Subtitle fontWeight="semiBold">{roomName}</Subtitle>
|
||||||
|
|
|
@ -23,10 +23,6 @@
|
||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.option.selected {
|
|
||||||
color: #0dbd8b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option.focused {
|
.option.focused {
|
||||||
background-color: rgba(111, 120, 130, 0.2);
|
background-color: rgba(111, 120, 130, 0.2);
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.menuItem > * {
|
.menuItem > * {
|
||||||
margin-right: 10px;
|
margin: 0 10px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menuItem > :last-child {
|
.menuItem > :last-child {
|
||||||
|
|
|
@ -52,6 +52,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 799px) {
|
@media (max-width: 799px) {
|
||||||
|
.modalHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 24px 24px 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
.modal.mobileFullScreen {
|
.modal.mobileFullScreen {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
41
src/SequenceDiagramViewerPage.jsx
Normal file
41
src/SequenceDiagramViewerPage.jsx
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import React, { useCallback, useState } from "react";
|
||||||
|
import { SequenceDiagramViewer } from "./room/GroupCallInspector";
|
||||||
|
import { FieldRow, InputField } from "./input/Input";
|
||||||
|
import { usePageTitle } from "./usePageTitle";
|
||||||
|
|
||||||
|
export function SequenceDiagramViewerPage() {
|
||||||
|
usePageTitle("Inspector");
|
||||||
|
|
||||||
|
const [debugLog, setDebugLog] = useState();
|
||||||
|
const [selectedUserId, setSelectedUserId] = useState();
|
||||||
|
const onChangeDebugLog = useCallback((e) => {
|
||||||
|
if (e.target.files && e.target.files.length > 0) {
|
||||||
|
e.target.files[0].text().then((text) => {
|
||||||
|
setDebugLog(JSON.parse(text));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ marginTop: 20 }}>
|
||||||
|
<FieldRow>
|
||||||
|
<InputField
|
||||||
|
type="file"
|
||||||
|
id="debugLog"
|
||||||
|
name="debugLog"
|
||||||
|
label="Debug Log"
|
||||||
|
onChange={onChangeDebugLog}
|
||||||
|
/>
|
||||||
|
</FieldRow>
|
||||||
|
{debugLog && (
|
||||||
|
<SequenceDiagramViewer
|
||||||
|
localUserId={debugLog.localUserId}
|
||||||
|
selectedUserId={selectedUserId}
|
||||||
|
onSelectUserId={setSelectedUserId}
|
||||||
|
remoteUserIds={debugLog.remoteUserIds}
|
||||||
|
events={debugLog.eventsByUserId[selectedUserId]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,32 +1,46 @@
|
||||||
import React, { forwardRef } from "react";
|
import React, { forwardRef, useRef } from "react";
|
||||||
import { useTooltipTriggerState } from "@react-stately/tooltip";
|
import { useTooltipTriggerState } from "@react-stately/tooltip";
|
||||||
|
import { FocusableProvider } from "@react-aria/focus";
|
||||||
import { useTooltipTrigger, useTooltip } from "@react-aria/tooltip";
|
import { useTooltipTrigger, useTooltip } from "@react-aria/tooltip";
|
||||||
import { mergeProps, useObjectRef } from "@react-aria/utils";
|
import { mergeProps, useObjectRef } from "@react-aria/utils";
|
||||||
import styles from "./Tooltip.module.css";
|
import styles from "./Tooltip.module.css";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { OverlayContainer, useOverlayPosition } from "@react-aria/overlays";
|
||||||
|
|
||||||
export function Tooltip({ position, state, ...props }) {
|
export const Tooltip = forwardRef(
|
||||||
let { tooltipProps } = useTooltip(props, state);
|
({ position, state, className, ...props }, ref) => {
|
||||||
|
let { tooltipProps } = useTooltip(props, state);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(styles.tooltip, styles[position || "bottom"])}
|
className={classNames(styles.tooltip, className)}
|
||||||
{...mergeProps(props, tooltipProps)}
|
{...mergeProps(props, tooltipProps)}
|
||||||
>
|
ref={ref}
|
||||||
{props.children}
|
>
|
||||||
</div>
|
{props.children}
|
||||||
);
|
</div>
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const TooltipTrigger = forwardRef(({ children, ...rest }, ref) => {
|
export const TooltipTrigger = forwardRef(({ children, ...rest }, ref) => {
|
||||||
const tooltipState = useTooltipTriggerState(rest);
|
const tooltipState = useTooltipTriggerState(rest);
|
||||||
const triggerRef = useObjectRef(ref);
|
const triggerRef = useObjectRef(ref);
|
||||||
|
const overlayRef = useRef();
|
||||||
const { triggerProps, tooltipProps } = useTooltipTrigger(
|
const { triggerProps, tooltipProps } = useTooltipTrigger(
|
||||||
rest,
|
rest,
|
||||||
tooltipState,
|
tooltipState,
|
||||||
triggerRef
|
triggerRef
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { overlayProps } = useOverlayPosition({
|
||||||
|
placement: rest.placement || "top",
|
||||||
|
targetRef: triggerRef,
|
||||||
|
overlayRef,
|
||||||
|
isOpen: tooltipState.isOpen,
|
||||||
|
offset: 5,
|
||||||
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!Array.isArray(children) ||
|
!Array.isArray(children) ||
|
||||||
children.length > 2 ||
|
children.length > 2 ||
|
||||||
|
@ -40,13 +54,20 @@ export const TooltipTrigger = forwardRef(({ children, ...rest }, ref) => {
|
||||||
const [tooltipTrigger, tooltip] = children;
|
const [tooltipTrigger, tooltip] = children;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.tooltipContainer}>
|
<FocusableProvider ref={triggerRef} {...triggerProps}>
|
||||||
<tooltipTrigger.type
|
{<tooltipTrigger.type {...mergeProps(tooltipTrigger.props, rest)} />}
|
||||||
{...mergeProps(triggerProps, tooltipTrigger.props, rest)}
|
{tooltipState.isOpen && (
|
||||||
ref={triggerRef}
|
<OverlayContainer>
|
||||||
/>
|
<Tooltip
|
||||||
{tooltipState.isOpen && tooltip({ state: tooltipState, ...tooltipProps })}
|
state={tooltipState}
|
||||||
</div>
|
{...mergeProps(tooltipProps, overlayProps)}
|
||||||
|
ref={overlayRef}
|
||||||
|
>
|
||||||
|
{tooltip()}
|
||||||
|
</Tooltip>
|
||||||
|
</OverlayContainer>
|
||||||
|
)}
|
||||||
|
</FocusableProvider>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
.tooltip {
|
.tooltip {
|
||||||
background-color: var(--bgColor2);
|
background-color: var(--bgColor2);
|
||||||
position: absolute;
|
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -9,25 +8,5 @@
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
max-width: 135px;
|
max-width: 135px;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
z-index: 1;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip.top {
|
|
||||||
bottom: calc(100% + 6px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip.bottom {
|
|
||||||
top: calc(100% + 6px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip.bottomLeft {
|
|
||||||
top: calc(100% + 6px);
|
|
||||||
left: -25%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltipContainer {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
|
@ -10,9 +10,10 @@ import { ReactComponent as LoginIcon } from "./icons/Login.svg";
|
||||||
import { ReactComponent as LogoutIcon } from "./icons/Logout.svg";
|
import { ReactComponent as LogoutIcon } from "./icons/Logout.svg";
|
||||||
import styles from "./UserMenu.module.css";
|
import styles from "./UserMenu.module.css";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
|
import { Body } from "./typography/Typography";
|
||||||
|
|
||||||
export function UserMenu({
|
export function UserMenu({
|
||||||
disableLogout,
|
preventNavigation,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
isPasswordlessUser,
|
isPasswordlessUser,
|
||||||
displayName,
|
displayName,
|
||||||
|
@ -31,7 +32,7 @@ export function UserMenu({
|
||||||
label: displayName,
|
label: displayName,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isPasswordlessUser) {
|
if (isPasswordlessUser && !preventNavigation) {
|
||||||
arr.push({
|
arr.push({
|
||||||
key: "login",
|
key: "login",
|
||||||
label: "Sign In",
|
label: "Sign In",
|
||||||
|
@ -39,7 +40,7 @@ export function UserMenu({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isPasswordlessUser && !disableLogout) {
|
if (!isPasswordlessUser && !preventNavigation) {
|
||||||
arr.push({
|
arr.push({
|
||||||
key: "logout",
|
key: "logout",
|
||||||
label: "Sign Out",
|
label: "Sign Out",
|
||||||
|
@ -49,7 +50,7 @@ export function UserMenu({
|
||||||
}
|
}
|
||||||
|
|
||||||
return arr;
|
return arr;
|
||||||
}, [isAuthenticated, isPasswordlessUser, displayName, disableLogout]);
|
}, [isAuthenticated, isPasswordlessUser, displayName, preventNavigation]);
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return (
|
return (
|
||||||
|
@ -61,9 +62,9 @@ export function UserMenu({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PopoverMenuTrigger placement="bottom right">
|
<PopoverMenuTrigger placement="bottom right">
|
||||||
<TooltipTrigger>
|
<TooltipTrigger placement="bottom left">
|
||||||
<Button variant="icon" className={styles.userButton}>
|
<Button variant="icon" className={styles.userButton}>
|
||||||
{isAuthenticated && !isPasswordlessUser ? (
|
{isAuthenticated && (!isPasswordlessUser || avatarUrl) ? (
|
||||||
<Avatar
|
<Avatar
|
||||||
size="sm"
|
size="sm"
|
||||||
className={styles.avatar}
|
className={styles.avatar}
|
||||||
|
@ -74,18 +75,14 @@ export function UserMenu({
|
||||||
<UserIcon />
|
<UserIcon />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
{(props) => (
|
{() => "Profile"}
|
||||||
<Tooltip position="bottomLeft" {...props}>
|
|
||||||
Profile
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
{(props) => (
|
{(props) => (
|
||||||
<Menu {...props} label="User menu" onAction={onAction}>
|
<Menu {...props} label="User menu" onAction={onAction}>
|
||||||
{items.map(({ key, icon: Icon, label }) => (
|
{items.map(({ key, icon: Icon, label }) => (
|
||||||
<Item key={key} textValue={label}>
|
<Item key={key} textValue={label} className={styles.menuItem}>
|
||||||
<Icon />
|
<Icon width={24} height={24} className={styles.menuIcon} />
|
||||||
<span>{label}</span>
|
<Body overflowEllipsis>{label}</Body>
|
||||||
</Item>
|
</Item>
|
||||||
))}
|
))}
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
.menuIcon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
.userButton svg * {
|
.userButton svg * {
|
||||||
fill: var(--textColor1);
|
fill: var(--textColor1);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { useModalTriggerState } from "./Modal";
|
||||||
import { ProfileModal } from "./profile/ProfileModal";
|
import { ProfileModal } from "./profile/ProfileModal";
|
||||||
import { UserMenu } from "./UserMenu";
|
import { UserMenu } from "./UserMenu";
|
||||||
|
|
||||||
export function UserMenuContainer({ disableLogout }) {
|
export function UserMenuContainer({ preventNavigation }) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { isAuthenticated, isPasswordlessUser, logout, userName, client } =
|
const { isAuthenticated, isPasswordlessUser, logout, userName, client } =
|
||||||
|
@ -34,7 +34,7 @@ export function UserMenuContainer({ disableLogout }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<UserMenu
|
<UserMenu
|
||||||
disableLogout={disableLogout}
|
preventNavigation={preventNavigation}
|
||||||
isAuthenticated={isAuthenticated}
|
isAuthenticated={isAuthenticated}
|
||||||
isPasswordlessUser={isPasswordlessUser}
|
isPasswordlessUser={isPasswordlessUser}
|
||||||
avatarUrl={avatarUrl}
|
avatarUrl={avatarUrl}
|
||||||
|
|
|
@ -22,8 +22,11 @@ import { Button } from "../button";
|
||||||
import { defaultHomeserver, defaultHomeserverHost } from "../matrix-utils";
|
import { defaultHomeserver, defaultHomeserverHost } from "../matrix-utils";
|
||||||
import styles from "./LoginPage.module.css";
|
import styles from "./LoginPage.module.css";
|
||||||
import { useInteractiveLogin } from "./useInteractiveLogin";
|
import { useInteractiveLogin } from "./useInteractiveLogin";
|
||||||
|
import { usePageTitle } from "../usePageTitle";
|
||||||
|
|
||||||
export function LoginPage() {
|
export function LoginPage() {
|
||||||
|
usePageTitle("Login");
|
||||||
|
|
||||||
const [_, login] = useInteractiveLogin();
|
const [_, login] = useInteractiveLogin();
|
||||||
const [homeserver, setHomeServer] = useState(defaultHomeserver);
|
const [homeserver, setHomeServer] = useState(defaultHomeserver);
|
||||||
const usernameRef = useRef();
|
const usernameRef = useRef();
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
.logo {
|
.logo {
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
margin: 80px 0;
|
margin: 80px 0;
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
|
|
|
@ -26,8 +26,11 @@ import { ReactComponent as Logo } from "../icons/LogoLarge.svg";
|
||||||
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";
|
||||||
|
import { usePageTitle } from "../usePageTitle";
|
||||||
|
|
||||||
export function RegisterPage() {
|
export function RegisterPage() {
|
||||||
|
usePageTitle("Register");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
loading,
|
loading,
|
||||||
client,
|
client,
|
||||||
|
@ -177,7 +180,7 @@ export function RegisterPage() {
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
apply.
|
apply.
|
||||||
<br />
|
<br />
|
||||||
By clicking "Go", you agree to our{" "}
|
By clicking "Log in", you agree to our{" "}
|
||||||
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
|
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
|
||||||
</Caption>
|
</Caption>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -66,6 +66,8 @@ export function useInteractiveRegistration() {
|
||||||
deviceId: device_id,
|
deviceId: device_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await client.setDisplayName(username);
|
||||||
|
|
||||||
const session = { user_id, device_id, access_token, passwordlessUser };
|
const session = { user_id, device_id, access_token, passwordlessUser };
|
||||||
|
|
||||||
if (passwordlessUser) {
|
if (passwordlessUser) {
|
||||||
|
|
|
@ -57,6 +57,7 @@ export function useRecaptcha(sitekey) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!window.grecaptcha) {
|
if (!window.grecaptcha) {
|
||||||
|
console.log("Recaptcha not loaded");
|
||||||
return Promise.reject(new Error("Recaptcha not loaded"));
|
return Promise.reject(new Error("Recaptcha not loaded"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,7 +95,7 @@ export function useRecaptcha(sitekey) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [recaptchaId]);
|
}, [recaptchaId, sitekey]);
|
||||||
|
|
||||||
const reset = useCallback(() => {
|
const reset = useCallback(() => {
|
||||||
if (window.grecaptcha) {
|
if (window.grecaptcha) {
|
||||||
|
|
|
@ -34,12 +34,17 @@ export const Button = forwardRef(
|
||||||
iconStyle,
|
iconStyle,
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
|
onPress,
|
||||||
|
onPressStart,
|
||||||
...rest
|
...rest
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const buttonRef = useObjectRef(ref);
|
const buttonRef = useObjectRef(ref);
|
||||||
const { buttonProps } = useButton(rest, buttonRef);
|
const { buttonProps } = useButton(
|
||||||
|
{ onPress, onPressStart, ...rest },
|
||||||
|
buttonRef
|
||||||
|
);
|
||||||
|
|
||||||
// TODO: react-aria's useButton hook prevents form submission via keyboard
|
// TODO: react-aria's useButton hook prevents form submission via keyboard
|
||||||
// Remove the hack below after this is merged https://github.com/adobe/react-spectrum/pull/904
|
// Remove the hack below after this is merged https://github.com/adobe/react-spectrum/pull/904
|
||||||
|
@ -71,25 +76,13 @@ export const Button = forwardRef(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export function ButtonTooltip({ className, children }) {
|
|
||||||
return (
|
|
||||||
<div className={classNames(styles.buttonTooltip, className)}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MicButton({ muted, ...rest }) {
|
export function MicButton({ muted, ...rest }) {
|
||||||
return (
|
return (
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<Button variant="toolbar" {...rest} off={muted}>
|
<Button variant="toolbar" {...rest} off={muted}>
|
||||||
{muted ? <MuteMicIcon /> : <MicIcon />}
|
{muted ? <MuteMicIcon /> : <MicIcon />}
|
||||||
</Button>
|
</Button>
|
||||||
{(props) => (
|
{() => (muted ? "Unmute microphone" : "Mute microphone")}
|
||||||
<Tooltip position="top" {...props}>
|
|
||||||
{muted ? "Unmute microphone" : "Mute microphone"}
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -100,11 +93,7 @@ export function VideoButton({ muted, ...rest }) {
|
||||||
<Button variant="toolbar" {...rest} off={muted}>
|
<Button variant="toolbar" {...rest} off={muted}>
|
||||||
{muted ? <DisableVideoIcon /> : <VideoIcon />}
|
{muted ? <DisableVideoIcon /> : <VideoIcon />}
|
||||||
</Button>
|
</Button>
|
||||||
{(props) => (
|
{() => (muted ? "Turn on camera" : "Turn off camera")}
|
||||||
<Tooltip position="top" {...props}>
|
|
||||||
{muted ? "Turn on camera" : "Turn off camera"}
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -115,11 +104,7 @@ export function ScreenshareButton({ enabled, className, ...rest }) {
|
||||||
<Button variant="toolbar" {...rest} on={enabled}>
|
<Button variant="toolbar" {...rest} on={enabled}>
|
||||||
<ScreenshareIcon />
|
<ScreenshareIcon />
|
||||||
</Button>
|
</Button>
|
||||||
{(props) => (
|
{() => (enabled ? "Stop sharing screen" : "Share screen")}
|
||||||
<Tooltip position="top" {...props}>
|
|
||||||
{enabled ? "Stop sharing screen" : "Share screen"}
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -134,11 +119,7 @@ export function HangupButton({ className, ...rest }) {
|
||||||
>
|
>
|
||||||
<HangupIcon />
|
<HangupIcon />
|
||||||
</Button>
|
</Button>
|
||||||
{(props) => (
|
{() => "Leave"}
|
||||||
<Tooltip position="top" {...props}>
|
|
||||||
Leave
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,15 @@ limitations under the License.
|
||||||
background-color: var(--primaryColor);
|
background-color: var(--primaryColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button:focus,
|
||||||
|
.toolbarButton:focus,
|
||||||
|
.iconButton:focus,
|
||||||
|
.iconCopyButton:focus,
|
||||||
|
.secondary:focus,
|
||||||
|
.copyButton:focus {
|
||||||
|
outline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.toolbarButton {
|
.toolbarButton {
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
|
@ -91,35 +100,6 @@ limitations under the License.
|
||||||
fill: #21262c;
|
fill: #21262c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonTooltip {
|
|
||||||
display: none;
|
|
||||||
background-color: var(--bgColor2);
|
|
||||||
position: absolute;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 8px 10px;
|
|
||||||
color: var(--textColor1);
|
|
||||||
border-radius: 8px;
|
|
||||||
max-width: 135px;
|
|
||||||
width: max-content;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonTooltip.bottomRight {
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbarButton:hover .buttonTooltip {
|
|
||||||
display: flex;
|
|
||||||
bottom: calc(100% + 6px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconButton:hover .buttonTooltip {
|
|
||||||
display: flex;
|
|
||||||
top: calc(100% + 6px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary,
|
.secondary,
|
||||||
.copyButton {
|
.copyButton {
|
||||||
color: #0dbd8b;
|
color: #0dbd8b;
|
||||||
|
|
|
@ -8,7 +8,7 @@ import styles from "./CallList.module.css";
|
||||||
import { getRoomUrl } from "../matrix-utils";
|
import { getRoomUrl } from "../matrix-utils";
|
||||||
import { Body, Caption } from "../typography/Typography";
|
import { Body, Caption } from "../typography/Typography";
|
||||||
|
|
||||||
export function CallList({ rooms, client }) {
|
export function CallList({ rooms, client, disableFacepile }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.callList}>
|
<div className={styles.callList}>
|
||||||
|
@ -20,6 +20,7 @@ export function CallList({ rooms, client }) {
|
||||||
avatarUrl={avatarUrl}
|
avatarUrl={avatarUrl}
|
||||||
roomId={roomId}
|
roomId={roomId}
|
||||||
participants={participants}
|
participants={participants}
|
||||||
|
disableFacepile={disableFacepile}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{rooms.length > 3 && (
|
{rooms.length > 3 && (
|
||||||
|
@ -33,7 +34,14 @@ export function CallList({ rooms, client }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CallTile({ name, avatarUrl, roomId, participants, client }) {
|
function CallTile({
|
||||||
|
name,
|
||||||
|
avatarUrl,
|
||||||
|
roomId,
|
||||||
|
participants,
|
||||||
|
client,
|
||||||
|
disableFacepile,
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.callTile}>
|
<div className={styles.callTile}>
|
||||||
<Link to={`/room/${roomId}`} className={styles.callTileLink}>
|
<Link to={`/room/${roomId}`} className={styles.callTileLink}>
|
||||||
|
@ -41,7 +49,7 @@ function CallTile({ name, avatarUrl, roomId, participants, client }) {
|
||||||
size="lg"
|
size="lg"
|
||||||
bgKey={name}
|
bgKey={name}
|
||||||
src={avatarUrl}
|
src={avatarUrl}
|
||||||
fallback={<VideoIcon width={16} height={16} />}
|
fallback={name.slice(0, 1).toUpperCase()}
|
||||||
className={styles.avatar}
|
className={styles.avatar}
|
||||||
/>
|
/>
|
||||||
<div className={styles.callInfo}>
|
<div className={styles.callInfo}>
|
||||||
|
@ -49,7 +57,7 @@ function CallTile({ name, avatarUrl, roomId, participants, client }) {
|
||||||
{name}
|
{name}
|
||||||
</Body>
|
</Body>
|
||||||
<Caption overflowEllipsis>{getRoomUrl(roomId)}</Caption>
|
<Caption overflowEllipsis>{getRoomUrl(roomId)}</Caption>
|
||||||
{participants && (
|
{participants && !disableFacepile && (
|
||||||
<Facepile
|
<Facepile
|
||||||
className={styles.facePile}
|
className={styles.facePile}
|
||||||
client={client}
|
client={client}
|
||||||
|
|
|
@ -19,8 +19,11 @@ import { useClient } from "../ClientContext";
|
||||||
import { ErrorView, LoadingView } from "../FullScreenView";
|
import { ErrorView, LoadingView } from "../FullScreenView";
|
||||||
import { UnauthenticatedView } from "./UnauthenticatedView";
|
import { UnauthenticatedView } from "./UnauthenticatedView";
|
||||||
import { RegisteredView } from "./RegisteredView";
|
import { RegisteredView } from "./RegisteredView";
|
||||||
|
import { usePageTitle } from "../usePageTitle";
|
||||||
|
|
||||||
export function HomePage() {
|
export function HomePage() {
|
||||||
|
usePageTitle("Home");
|
||||||
|
|
||||||
const { isAuthenticated, isPasswordlessUser, loading, error, client } =
|
const { isAuthenticated, isPasswordlessUser, loading, error, client } =
|
||||||
useClient();
|
useClient();
|
||||||
|
|
||||||
|
|
|
@ -107,7 +107,7 @@ export function RegisteredView({ client }) {
|
||||||
<Title className={styles.recentCallsTitle}>
|
<Title className={styles.recentCallsTitle}>
|
||||||
Your recent Calls
|
Your recent Calls
|
||||||
</Title>
|
</Title>
|
||||||
<CallList rooms={recentRooms} client={client} />
|
<CallList rooms={recentRooms} client={client} disableFacepile />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|
|
@ -102,8 +102,8 @@ export function UnauthenticatedView() {
|
||||||
<InputField
|
<InputField
|
||||||
id="userName"
|
id="userName"
|
||||||
name="userName"
|
name="userName"
|
||||||
label="Your name"
|
label="Username"
|
||||||
placeholder="Your name"
|
placeholder="Username"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
|
|
|
@ -27,6 +27,10 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selectTrigger:focus {
|
||||||
|
outline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.selectedItem {
|
.selectedItem {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
|
@ -22,6 +22,10 @@ import App from "./App";
|
||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
import { Integrations } from "@sentry/tracing";
|
import { Integrations } from "@sentry/tracing";
|
||||||
import { ErrorView } from "./FullScreenView";
|
import { ErrorView } from "./FullScreenView";
|
||||||
|
import * as rageshake from "matrix-react-sdk/src/rageshake/rageshake";
|
||||||
|
import { InspectorContextProvider } from "./room/GroupCallInspector";
|
||||||
|
|
||||||
|
rageshake.init();
|
||||||
|
|
||||||
if (import.meta.env.VITE_CUSTOM_THEME) {
|
if (import.meta.env.VITE_CUSTOM_THEME) {
|
||||||
const style = document.documentElement.style;
|
const style = document.documentElement.style;
|
||||||
|
@ -59,7 +63,9 @@ Sentry.init({
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<Sentry.ErrorBoundary fallback={ErrorView}>
|
<Sentry.ErrorBoundary fallback={ErrorView}>
|
||||||
<App history={history} />
|
<InspectorContextProvider>
|
||||||
|
<App history={history} />
|
||||||
|
</InspectorContextProvider>
|
||||||
</Sentry.ErrorBoundary>
|
</Sentry.ErrorBoundary>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
document.getElementById("root")
|
document.getElementById("root")
|
||||||
|
|
|
@ -1,72 +1,68 @@
|
||||||
import React, { useRef } from "react";
|
import React, { forwardRef, useRef } from "react";
|
||||||
import styles from "./PopoverMenu.module.css";
|
import styles from "./PopoverMenu.module.css";
|
||||||
import { useMenuTriggerState } from "@react-stately/menu";
|
import { useMenuTriggerState } from "@react-stately/menu";
|
||||||
import { useMenuTrigger } from "@react-aria/menu";
|
import { useMenuTrigger } from "@react-aria/menu";
|
||||||
import { OverlayContainer, useOverlayPosition } from "@react-aria/overlays";
|
import { OverlayContainer, useOverlayPosition } from "@react-aria/overlays";
|
||||||
|
import { mergeProps, useObjectRef } from "@react-aria/utils";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Popover } from "./Popover";
|
import { Popover } from "./Popover";
|
||||||
|
|
||||||
export function PopoverMenuTrigger({
|
export const PopoverMenuTrigger = forwardRef(
|
||||||
children,
|
({ children, placement, className, disableOnState, ...rest }, ref) => {
|
||||||
placement,
|
const popoverMenuState = useMenuTriggerState(rest);
|
||||||
className,
|
const buttonRef = useObjectRef(ref);
|
||||||
disableOnState,
|
const { menuTriggerProps, menuProps } = useMenuTrigger(
|
||||||
...rest
|
{},
|
||||||
}) {
|
popoverMenuState,
|
||||||
const popoverMenuState = useMenuTriggerState(rest);
|
buttonRef
|
||||||
const buttonRef = useRef();
|
);
|
||||||
const { menuTriggerProps, menuProps } = useMenuTrigger(
|
|
||||||
{},
|
|
||||||
popoverMenuState,
|
|
||||||
buttonRef
|
|
||||||
);
|
|
||||||
|
|
||||||
const popoverRef = useRef();
|
const popoverRef = useRef();
|
||||||
|
|
||||||
const { overlayProps } = useOverlayPosition({
|
const { overlayProps } = useOverlayPosition({
|
||||||
targetRef: buttonRef,
|
targetRef: buttonRef,
|
||||||
overlayRef: popoverRef,
|
overlayRef: popoverRef,
|
||||||
placement: placement || "top",
|
placement: placement || "top",
|
||||||
offset: 5,
|
offset: 5,
|
||||||
isOpen: popoverMenuState.isOpen,
|
isOpen: popoverMenuState.isOpen,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!Array.isArray(children) ||
|
!Array.isArray(children) ||
|
||||||
children.length > 2 ||
|
children.length > 2 ||
|
||||||
typeof children[1] !== "function"
|
typeof children[1] !== "function"
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"PopoverMenu must have two props. The first being a button and the second being a render prop."
|
"PopoverMenu must have two props. The first being a button and the second being a render prop."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [popoverTrigger, popoverMenu] = children;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames(styles.popoverMenuTrigger, className)}>
|
||||||
|
<popoverTrigger.type
|
||||||
|
{...mergeProps(popoverTrigger.props, menuTriggerProps)}
|
||||||
|
on={!disableOnState && popoverMenuState.isOpen}
|
||||||
|
ref={buttonRef}
|
||||||
|
/>
|
||||||
|
{popoverMenuState.isOpen && (
|
||||||
|
<OverlayContainer>
|
||||||
|
<Popover
|
||||||
|
{...overlayProps}
|
||||||
|
isOpen={popoverMenuState.isOpen}
|
||||||
|
onClose={popoverMenuState.close}
|
||||||
|
ref={popoverRef}
|
||||||
|
>
|
||||||
|
{popoverMenu({
|
||||||
|
...menuProps,
|
||||||
|
autoFocus: popoverMenuState.focusStrategy,
|
||||||
|
onClose: popoverMenuState.close,
|
||||||
|
})}
|
||||||
|
</Popover>
|
||||||
|
</OverlayContainer>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
const [popoverTrigger, popoverMenu] = children;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames(styles.popoverMenuTrigger, className)}>
|
|
||||||
<popoverTrigger.type
|
|
||||||
{...popoverTrigger.props}
|
|
||||||
{...menuTriggerProps}
|
|
||||||
on={!disableOnState && popoverMenuState.isOpen}
|
|
||||||
ref={buttonRef}
|
|
||||||
/>
|
|
||||||
{popoverMenuState.isOpen && (
|
|
||||||
<OverlayContainer>
|
|
||||||
<Popover
|
|
||||||
{...overlayProps}
|
|
||||||
isOpen={popoverMenuState.isOpen}
|
|
||||||
onClose={popoverMenuState.close}
|
|
||||||
ref={popoverRef}
|
|
||||||
>
|
|
||||||
{popoverMenu({
|
|
||||||
...menuProps,
|
|
||||||
autoFocus: popoverMenuState.focusStrategy,
|
|
||||||
onClose: popoverMenuState.close,
|
|
||||||
})}
|
|
||||||
</Popover>
|
|
||||||
</OverlayContainer>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ export function ProfileModal({
|
||||||
|
|
||||||
saveProfile({
|
saveProfile({
|
||||||
displayName,
|
displayName,
|
||||||
avatar,
|
avatar: avatar && avatar.size > 0 ? avatar : undefined,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[saveProfile]
|
[saveProfile]
|
||||||
|
@ -52,6 +52,16 @@ export function ProfileModal({
|
||||||
<Modal title="Profile" isDismissable {...rest}>
|
<Modal title="Profile" isDismissable {...rest}>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
|
<FieldRow>
|
||||||
|
<InputField
|
||||||
|
id="userId"
|
||||||
|
name="userId"
|
||||||
|
label="User Id"
|
||||||
|
type="text"
|
||||||
|
disabled
|
||||||
|
value={client.getUserId()}
|
||||||
|
/>
|
||||||
|
</FieldRow>
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<InputField
|
<InputField
|
||||||
id="displayName"
|
id="displayName"
|
||||||
|
@ -65,7 +75,7 @@ export function ProfileModal({
|
||||||
onChange={onChangeDisplayName}
|
onChange={onChangeDisplayName}
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
{isAuthenticated && !isPasswordlessUser && (
|
{isAuthenticated && (
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<InputField
|
<InputField
|
||||||
type="file"
|
type="file"
|
||||||
|
|
|
@ -46,7 +46,6 @@ limitations under the License.
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
|
@ -70,4 +69,8 @@ limitations under the License.
|
||||||
.container {
|
.container {
|
||||||
min-height: calc(100% - 76px);
|
min-height: calc(100% - 76px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,11 +16,7 @@ export function GridLayoutMenu({ layout, setLayout }) {
|
||||||
<Button variant="icon">
|
<Button variant="icon">
|
||||||
{layout === "spotlight" ? <SpotlightIcon /> : <FreedomIcon />}
|
{layout === "spotlight" ? <SpotlightIcon /> : <FreedomIcon />}
|
||||||
</Button>
|
</Button>
|
||||||
{(props) => (
|
{() => "Layout Type"}
|
||||||
<Tooltip position="bottom" {...props}>
|
|
||||||
Layout Type
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
{(props) => (
|
{(props) => (
|
||||||
<Menu {...props} label="Grid layout menu" onAction={setLayout}>
|
<Menu {...props} label="Grid layout menu" onAction={setLayout}>
|
||||||
|
|
|
@ -1,7 +1,17 @@
|
||||||
import { Resizable } from "re-resizable";
|
import { Resizable } from "re-resizable";
|
||||||
import React, { useEffect, useState, useMemo } from "react";
|
import React, {
|
||||||
import { useCallback } from "react";
|
useEffect,
|
||||||
|
useState,
|
||||||
|
useReducer,
|
||||||
|
useRef,
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
} from "react";
|
||||||
import ReactJson from "react-json-view";
|
import ReactJson from "react-json-view";
|
||||||
|
import mermaid from "mermaid";
|
||||||
|
import styles from "./GroupCallInspector.module.css";
|
||||||
|
import { SelectInput } from "../input/SelectInput";
|
||||||
|
import { Item } from "@react-stately/collections";
|
||||||
|
|
||||||
function getCallUserId(call) {
|
function getCallUserId(call) {
|
||||||
return call.getOpponentMember()?.userId || call.invitee || null;
|
return call.getOpponentMember()?.userId || call.invitee || null;
|
||||||
|
@ -23,203 +33,434 @@ function getHangupCallState(call) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GroupCallInspector({ client, groupCall, show }) {
|
const dateFormatter = new Intl.DateTimeFormat([], {
|
||||||
const [roomStateEvents, setRoomStateEvents] = useState([]);
|
hour: "2-digit",
|
||||||
const [toDeviceEvents, setToDeviceEvents] = useState([]);
|
minute: "2-digit",
|
||||||
const [sentVoipEvents, setSentVoipEvents] = useState([]);
|
second: "2-digit",
|
||||||
const [state, setState] = useState({
|
fractionalSecondDigits: 3,
|
||||||
userId: client.getUserId(),
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const updateState = useCallback(
|
const defaultCollapsedFields = [
|
||||||
(next) => setState((prev) => ({ ...prev, ...next })),
|
"org.matrix.msc3401.call",
|
||||||
[]
|
"org.matrix.msc3401.call.member",
|
||||||
|
"calls",
|
||||||
|
"callStats",
|
||||||
|
"hangupCalls",
|
||||||
|
"toDeviceEvents",
|
||||||
|
"sentVoipEvents",
|
||||||
|
"content",
|
||||||
|
];
|
||||||
|
|
||||||
|
function shouldCollapse({ name, src, type, namespace }) {
|
||||||
|
return defaultCollapsedFields.includes(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUserName(userId) {
|
||||||
|
const match = userId.match(/@([^\:]+):/);
|
||||||
|
|
||||||
|
return match && match.length > 0
|
||||||
|
? match[1].replace("-", " ").replace("W", "")
|
||||||
|
: userId.replace("W", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatContent(type, content) {
|
||||||
|
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 "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimestamp(timestamp) {
|
||||||
|
return dateFormatter.format(timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InspectorContext = createContext();
|
||||||
|
|
||||||
|
export function InspectorContextProvider({ children }) {
|
||||||
|
const context = useState({});
|
||||||
|
return (
|
||||||
|
<InspectorContext.Provider value={context}>
|
||||||
|
{children}
|
||||||
|
</InspectorContext.Provider>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SequenceDiagramViewer({
|
||||||
|
localUserId,
|
||||||
|
remoteUserIds,
|
||||||
|
selectedUserId,
|
||||||
|
onSelectUserId,
|
||||||
|
events,
|
||||||
|
}) {
|
||||||
|
const mermaidElRef = useRef();
|
||||||
|
|
||||||
|
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(
|
||||||
|
({ to, from, timestamp, type, content, ignored }) =>
|
||||||
|
`${getUserName(from)} ${ignored ? "-x" : "->>"} ${getUserName(
|
||||||
|
to
|
||||||
|
)}: ${formatTimestamp(timestamp)} ${type} ${formatContent(
|
||||||
|
type,
|
||||||
|
content
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
.join("\n ")
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
mermaid.mermaidAPI.render("mermaid", graphDefinition, (svgCode) => {
|
||||||
|
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={onSelectUserId}
|
||||||
|
>
|
||||||
|
{remoteUserIds.map((userId) => (
|
||||||
|
<Item key={userId}>{userId}</Item>
|
||||||
|
))}
|
||||||
|
</SelectInput>
|
||||||
|
<div id="mermaid" />
|
||||||
|
<div ref={mermaidElRef} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reducer(state, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case "receive_room_state_event": {
|
||||||
|
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 "receive_to_device_event": {
|
||||||
|
const event = action.event;
|
||||||
|
const eventsByUserId = { ...state.eventsByUserId };
|
||||||
|
const fromId = event.getSender();
|
||||||
|
const toId = state.localUserId;
|
||||||
|
const content = event.getContent();
|
||||||
|
|
||||||
|
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 "send_voip_event": {
|
||||||
|
const event = action.event;
|
||||||
|
const eventsByUserId = { ...state.eventsByUserId };
|
||||||
|
const fromId = state.localUserId;
|
||||||
|
const toId = event.userId;
|
||||||
|
|
||||||
|
const remoteUserIds = eventsByUserId[toId]
|
||||||
|
? state.remoteUserIds
|
||||||
|
: [...state.remoteUserIds, toId];
|
||||||
|
|
||||||
|
eventsByUserId[toId] = [
|
||||||
|
...(eventsByUserId[toId] || []),
|
||||||
|
{
|
||||||
|
from: fromId,
|
||||||
|
to: toId,
|
||||||
|
type: event.eventType,
|
||||||
|
content: event.content,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
ignored: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return { ...state, eventsByUserId, remoteUserIds };
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function useGroupCallState(client, groupCall, pollCallStats) {
|
||||||
|
const [state, dispatch] = useReducer(reducer, {
|
||||||
|
localUserId: client.getUserId(),
|
||||||
|
localSessionId: client.getSessionId(),
|
||||||
|
eventsByUserId: {},
|
||||||
|
remoteUserIds: [],
|
||||||
|
callStateEvent: null,
|
||||||
|
memberStateEvents: {},
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function onUpdateRoomState(event) {
|
function onUpdateRoomState(event) {
|
||||||
if (event) {
|
const callStateEvent = groupCall.room.currentState.getStateEvents(
|
||||||
setRoomStateEvents((prev) => [
|
"org.matrix.msc3401.call",
|
||||||
...prev,
|
groupCall.groupCallId
|
||||||
{
|
|
||||||
eventType: event.getType(),
|
|
||||||
stateKey: event.getStateKey(),
|
|
||||||
content: event.getContent(),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const roomEvent = groupCall.room.currentState
|
|
||||||
.getStateEvents("org.matrix.msc3401.call", groupCall.groupCallId)
|
|
||||||
.getContent();
|
|
||||||
|
|
||||||
const memberEvents = Object.fromEntries(
|
|
||||||
groupCall.room.currentState
|
|
||||||
.getStateEvents("org.matrix.msc3401.call.member")
|
|
||||||
.map((event) => [event.getStateKey(), event.getContent()])
|
|
||||||
);
|
);
|
||||||
|
|
||||||
updateState({
|
const memberStateEvents = groupCall.room.currentState.getStateEvents(
|
||||||
["org.matrix.msc3401.call"]: roomEvent,
|
"org.matrix.msc3401.call.member"
|
||||||
["org.matrix.msc3401.call.member"]: memberEvents,
|
);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "receive_room_state_event",
|
||||||
|
event,
|
||||||
|
callStateEvent,
|
||||||
|
memberStateEvents,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCallsChanged() {
|
// function onCallsChanged() {
|
||||||
const calls = groupCall.calls.reduce((obj, call) => {
|
// const calls = groupCall.calls.reduce((obj, call) => {
|
||||||
obj[
|
// obj[
|
||||||
`${call.callId} (${call.getOpponentMember()?.userId || call.sender})`
|
// `${call.callId} (${call.getOpponentMember()?.userId || call.sender})`
|
||||||
] = getCallState(call);
|
// ] = getCallState(call);
|
||||||
return obj;
|
// return obj;
|
||||||
}, {});
|
// }, {});
|
||||||
|
|
||||||
updateState({ calls });
|
// updateState({ calls });
|
||||||
}
|
// }
|
||||||
|
|
||||||
function onCallHangup(call) {
|
// function onCallHangup(call) {
|
||||||
setState(({ hangupCalls, ...rest }) => ({
|
// setState(({ hangupCalls, ...rest }) => ({
|
||||||
...rest,
|
// ...rest,
|
||||||
hangupCalls: {
|
// hangupCalls: {
|
||||||
...hangupCalls,
|
// ...hangupCalls,
|
||||||
[`${call.callId} (${
|
// [`${call.callId} (${
|
||||||
call.getOpponentMember()?.userId || call.sender
|
// call.getOpponentMember()?.userId || call.sender
|
||||||
})`]: getHangupCallState(call),
|
// })`]: getHangupCallState(call),
|
||||||
},
|
// },
|
||||||
}));
|
// }));
|
||||||
}
|
// dispatch({ type: "call_hangup", call });
|
||||||
|
// }
|
||||||
|
|
||||||
function onToDeviceEvent(event) {
|
function onToDeviceEvent(event) {
|
||||||
const eventType = event.getType();
|
dispatch({ type: "receive_to_device_event", event });
|
||||||
|
|
||||||
if (
|
|
||||||
!(
|
|
||||||
eventType.startsWith("m.call.") ||
|
|
||||||
eventType.startsWith("org.matrix.call.")
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = event.getContent();
|
|
||||||
|
|
||||||
if (content.conf_id && content.conf_id !== groupCall.groupCallId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setToDeviceEvents((prev) => [
|
|
||||||
...prev,
|
|
||||||
{ eventType, content, sender: event.getSender() },
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSendVoipEvent(event) {
|
function onSendVoipEvent(event) {
|
||||||
setSentVoipEvents((prev) => [...prev, event]);
|
dispatch({ type: "send_voip_event", event });
|
||||||
}
|
}
|
||||||
|
|
||||||
client.on("RoomState.events", onUpdateRoomState);
|
client.on("RoomState.events", onUpdateRoomState);
|
||||||
groupCall.on("calls_changed", onCallsChanged);
|
//groupCall.on("calls_changed", onCallsChanged);
|
||||||
groupCall.on("send_voip_event", onSendVoipEvent);
|
groupCall.on("send_voip_event", onSendVoipEvent);
|
||||||
client.on("state", onCallsChanged);
|
//client.on("state", onCallsChanged);
|
||||||
client.on("hangup", onCallHangup);
|
//client.on("hangup", onCallHangup);
|
||||||
client.on("toDeviceEvent", onToDeviceEvent);
|
client.on("toDeviceEvent", onToDeviceEvent);
|
||||||
|
|
||||||
onUpdateRoomState();
|
onUpdateRoomState();
|
||||||
}, [client, groupCall]);
|
|
||||||
|
|
||||||
const toDeviceEventsByCall = useMemo(() => {
|
|
||||||
const result = {};
|
|
||||||
|
|
||||||
for (const event of toDeviceEvents) {
|
|
||||||
const callId = event.content.call_id;
|
|
||||||
const key = `${callId} (${event.sender})`;
|
|
||||||
result[key] = result[key] || [];
|
|
||||||
result[key].push(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}, [toDeviceEvents]);
|
|
||||||
|
|
||||||
const sentVoipEventsByCall = useMemo(() => {
|
|
||||||
const result = {};
|
|
||||||
|
|
||||||
for (const event of sentVoipEvents) {
|
|
||||||
const callId = event.content.call_id;
|
|
||||||
const key = `${callId} (${event.userId})`;
|
|
||||||
result[key] = result[key] || [];
|
|
||||||
result[key].push(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}, [sentVoipEvents]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let timeout;
|
|
||||||
|
|
||||||
async function updateCallStats() {
|
|
||||||
const callIds = groupCall.calls.map(
|
|
||||||
(call) =>
|
|
||||||
`${call.callId} (${call.getOpponentMember()?.userId || call.sender})`
|
|
||||||
);
|
|
||||||
const stats = await Promise.all(
|
|
||||||
groupCall.calls.map((call) =>
|
|
||||||
call.peerConn
|
|
||||||
? call.peerConn
|
|
||||||
.getStats(null)
|
|
||||||
.then((stats) =>
|
|
||||||
Object.fromEntries(
|
|
||||||
Array.from(stats).map(([_id, report], i) => [
|
|
||||||
report.type + i,
|
|
||||||
report,
|
|
||||||
])
|
|
||||||
)
|
|
||||||
)
|
|
||||||
: Promise.resolve(null)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const callStats = {};
|
|
||||||
|
|
||||||
for (let i = 0; i < groupCall.calls.length; i++) {
|
|
||||||
callStats[callIds[i]] = stats[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
updateState({ callStats });
|
|
||||||
timeout = setTimeout(updateCallStats, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (show) {
|
|
||||||
updateCallStats();
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearTimeout(timeout);
|
client.removeListener("RoomState.events", onUpdateRoomState);
|
||||||
|
//groupCall.removeListener("calls_changed", onCallsChanged);
|
||||||
|
groupCall.removeListener("send_voip_event", onSendVoipEvent);
|
||||||
|
//client.removeListener("state", onCallsChanged);
|
||||||
|
//client.removeListener("hangup", onCallHangup);
|
||||||
|
client.removeListener("toDeviceEvent", onToDeviceEvent);
|
||||||
};
|
};
|
||||||
}, [show]);
|
}, [client, groupCall]);
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// let timeout;
|
||||||
|
|
||||||
|
// async function updateCallStats() {
|
||||||
|
// const callIds = groupCall.calls.map(
|
||||||
|
// (call) =>
|
||||||
|
// `${call.callId} (${call.getOpponentMember()?.userId || call.sender})`
|
||||||
|
// );
|
||||||
|
// const stats = await Promise.all(
|
||||||
|
// groupCall.calls.map((call) =>
|
||||||
|
// call.peerConn
|
||||||
|
// ? call.peerConn
|
||||||
|
// .getStats(null)
|
||||||
|
// .then((stats) =>
|
||||||
|
// Object.fromEntries(
|
||||||
|
// Array.from(stats).map(([_id, report], i) => [
|
||||||
|
// report.type + i,
|
||||||
|
// report,
|
||||||
|
// ])
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// : Promise.resolve(null)
|
||||||
|
// )
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const callStats = {};
|
||||||
|
|
||||||
|
// for (let i = 0; i < groupCall.calls.length; i++) {
|
||||||
|
// callStats[callIds[i]] = stats[i];
|
||||||
|
// }
|
||||||
|
|
||||||
|
// dispatch({ type: "callStats", callStats });
|
||||||
|
// timeout = setTimeout(updateCallStats, 1000);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (pollCallStats) {
|
||||||
|
// updateCallStats();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return () => {
|
||||||
|
// clearTimeout(timeout);
|
||||||
|
// };
|
||||||
|
// }, [pollCallStats]);
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GroupCallInspector({ client, groupCall, show }) {
|
||||||
|
const [currentTab, setCurrentTab] = useState("sequence-diagrams");
|
||||||
|
const [selectedUserId, setSelectedUserId] = useState();
|
||||||
|
const state = useGroupCallState(client, groupCall, show);
|
||||||
|
|
||||||
|
const [_, setState] = useContext(InspectorContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setState({ json: state });
|
||||||
|
}, [setState, state]);
|
||||||
|
|
||||||
if (!show) {
|
if (!show) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Resizable enable={{ top: true }} defaultSize={{ height: 200 }}>
|
<Resizable
|
||||||
<ReactJson
|
enable={{ top: true }}
|
||||||
theme="monokai"
|
defaultSize={{ height: 200 }}
|
||||||
src={{
|
className={styles.inspector}
|
||||||
...state,
|
>
|
||||||
roomStateEvents,
|
<div className={styles.toolbar}>
|
||||||
toDeviceEvents,
|
<button onClick={() => setCurrentTab("sequence-diagrams")}>
|
||||||
toDeviceEventsByCall,
|
Sequence Diagrams
|
||||||
sentVoipEvents,
|
</button>
|
||||||
sentVoipEventsByCall,
|
<button onClick={() => setCurrentTab("inspector")}>Inspector</button>
|
||||||
}}
|
</div>
|
||||||
name={null}
|
{currentTab === "sequence-diagrams" && (
|
||||||
indentWidth={2}
|
<SequenceDiagramViewer
|
||||||
collapsed={1}
|
localUserId={state.localUserId}
|
||||||
displayDataTypes={false}
|
selectedUserId={selectedUserId}
|
||||||
displayObjectSize={false}
|
onSelectUserId={setSelectedUserId}
|
||||||
enableClipboard={false}
|
remoteUserIds={state.remoteUserIds}
|
||||||
style={{ height: "100%", overflowY: "scroll" }}
|
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>
|
</Resizable>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
25
src/room/GroupCallInspector.module.css
Normal file
25
src/room/GroupCallInspector.module.css
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
.inspector {
|
||||||
|
background-color: var(--bgColor2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: 12px;
|
||||||
|
fill: var(--textColor1) !important;
|
||||||
|
stroke: var(--textColor1) !important;
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useLoadGroupCall } from "./useLoadGroupCall";
|
import { useLoadGroupCall } from "./useLoadGroupCall";
|
||||||
import { ErrorView, FullScreenView } from "../FullScreenView";
|
import { ErrorView, FullScreenView } from "../FullScreenView";
|
||||||
|
import { usePageTitle } from "../usePageTitle";
|
||||||
|
|
||||||
export function GroupCallLoader({ client, roomId, viaServers, children }) {
|
export function GroupCallLoader({ client, roomId, viaServers, children }) {
|
||||||
const { loading, error, groupCall } = useLoadGroupCall(
|
const { loading, error, groupCall } = useLoadGroupCall(
|
||||||
|
@ -9,6 +10,8 @@ export function GroupCallLoader({ client, roomId, viaServers, children }) {
|
||||||
viaServers
|
viaServers
|
||||||
);
|
);
|
||||||
|
|
||||||
|
usePageTitle(groupCall ? groupCall.room.name : "Loading...");
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<FullScreenView>
|
<FullScreenView>
|
||||||
|
|
|
@ -15,7 +15,19 @@ export function GroupCallView({
|
||||||
groupCall,
|
groupCall,
|
||||||
simpleGrid,
|
simpleGrid,
|
||||||
}) {
|
}) {
|
||||||
const [showInspector, setShowInspector] = useState(false);
|
const [showInspector, setShowInspector] = useState(
|
||||||
|
() => !!localStorage.getItem("matrix-group-call-inspector")
|
||||||
|
);
|
||||||
|
const onChangeShowInspector = useCallback((show) => {
|
||||||
|
setShowInspector(show);
|
||||||
|
|
||||||
|
if (show) {
|
||||||
|
localStorage.setItem("matrix-group-call-inspector", "true");
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem("matrix-group-call-inspector");
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state,
|
state,
|
||||||
error,
|
error,
|
||||||
|
@ -46,12 +58,11 @@ export function GroupCallView({
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const onLeave = useCallback(() => {
|
const onLeave = useCallback(() => {
|
||||||
|
setLeft(true);
|
||||||
leave();
|
leave();
|
||||||
|
|
||||||
if (!isPasswordlessUser) {
|
if (!isPasswordlessUser) {
|
||||||
history.push("/");
|
history.push("/");
|
||||||
} else {
|
|
||||||
setLeft(true);
|
|
||||||
}
|
}
|
||||||
}, [leave, history]);
|
}, [leave, history]);
|
||||||
|
|
||||||
|
@ -75,7 +86,7 @@ export function GroupCallView({
|
||||||
localScreenshareFeed={localScreenshareFeed}
|
localScreenshareFeed={localScreenshareFeed}
|
||||||
screenshareFeeds={screenshareFeeds}
|
screenshareFeeds={screenshareFeeds}
|
||||||
simpleGrid={simpleGrid}
|
simpleGrid={simpleGrid}
|
||||||
setShowInspector={setShowInspector}
|
setShowInspector={onChangeShowInspector}
|
||||||
showInspector={showInspector}
|
showInspector={showInspector}
|
||||||
roomId={roomId}
|
roomId={roomId}
|
||||||
/>
|
/>
|
||||||
|
@ -102,7 +113,7 @@ export function GroupCallView({
|
||||||
localVideoMuted={localVideoMuted}
|
localVideoMuted={localVideoMuted}
|
||||||
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
||||||
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
||||||
setShowInspector={setShowInspector}
|
setShowInspector={onChangeShowInspector}
|
||||||
showInspector={showInspector}
|
showInspector={showInspector}
|
||||||
roomId={roomId}
|
roomId={roomId}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -128,7 +128,7 @@ export function InCallView({
|
||||||
</LeftNav>
|
</LeftNav>
|
||||||
<RightNav>
|
<RightNav>
|
||||||
<GridLayoutMenu layout={layout} setLayout={setLayout} />
|
<GridLayoutMenu layout={layout} setLayout={setLayout} />
|
||||||
<UserMenuContainer disableLogout />
|
<UserMenuContainer preventNavigation />
|
||||||
</RightNav>
|
</RightNav>
|
||||||
</Header>
|
</Header>
|
||||||
{items.length === 0 ? (
|
{items.length === 0 ? (
|
||||||
|
|
|
@ -9,6 +9,11 @@ import { getRoomUrl } from "../matrix-utils";
|
||||||
import { OverflowMenu } from "./OverflowMenu";
|
import { OverflowMenu } from "./OverflowMenu";
|
||||||
import { UserMenuContainer } from "../UserMenuContainer";
|
import { UserMenuContainer } from "../UserMenuContainer";
|
||||||
import { Body, Link } from "../typography/Typography";
|
import { Body, Link } from "../typography/Typography";
|
||||||
|
import { Avatar } from "../Avatar";
|
||||||
|
import { getAvatarUrl } from "../matrix-utils";
|
||||||
|
import { useProfile } from "../profile/useProfile";
|
||||||
|
import useMeasure from "react-use-measure";
|
||||||
|
import { ResizeObserver } from "@juggle/resize-observer";
|
||||||
|
|
||||||
export function LobbyView({
|
export function LobbyView({
|
||||||
client,
|
client,
|
||||||
|
@ -27,9 +32,11 @@ export function LobbyView({
|
||||||
}) {
|
}) {
|
||||||
const { stream } = useCallFeed(localCallFeed);
|
const { stream } = useCallFeed(localCallFeed);
|
||||||
const videoRef = useMediaStream(stream, true);
|
const videoRef = useMediaStream(stream, true);
|
||||||
|
const { displayName, avatarUrl } = useProfile(client);
|
||||||
|
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
|
||||||
|
const avatarSize = (previewBounds.height - 66) / 2;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// TODO: Only init once
|
|
||||||
onInitLocalCallFeed();
|
onInitLocalCallFeed();
|
||||||
}, [onInitLocalCallFeed]);
|
}, [onInitLocalCallFeed]);
|
||||||
|
|
||||||
|
@ -45,7 +52,7 @@ export function LobbyView({
|
||||||
</Header>
|
</Header>
|
||||||
<div className={styles.joinRoom}>
|
<div className={styles.joinRoom}>
|
||||||
<div className={styles.joinRoomContent}>
|
<div className={styles.joinRoomContent}>
|
||||||
<div className={styles.preview}>
|
<div className={styles.preview} ref={previewRef}>
|
||||||
<video ref={videoRef} muted playsInline disablePictureInPicture />
|
<video ref={videoRef} muted playsInline disablePictureInPicture />
|
||||||
{state === GroupCallState.LocalCallFeedUninitialized && (
|
{state === GroupCallState.LocalCallFeedUninitialized && (
|
||||||
<Body fontWeight="semiBold" className={styles.webcamPermissions}>
|
<Body fontWeight="semiBold" className={styles.webcamPermissions}>
|
||||||
|
@ -59,6 +66,20 @@ export function LobbyView({
|
||||||
)}
|
)}
|
||||||
{state === GroupCallState.LocalCallFeedInitialized && (
|
{state === GroupCallState.LocalCallFeedInitialized && (
|
||||||
<>
|
<>
|
||||||
|
{localVideoMuted && (
|
||||||
|
<div className={styles.avatarContainer}>
|
||||||
|
<Avatar
|
||||||
|
style={{
|
||||||
|
width: avatarSize,
|
||||||
|
height: avatarSize,
|
||||||
|
borderRadius: avatarSize,
|
||||||
|
fontSize: Math.round(avatarSize / 2),
|
||||||
|
}}
|
||||||
|
src={avatarUrl && getAvatarUrl(client, avatarUrl, 96)}
|
||||||
|
fallback={displayName.slice(0, 1).toUpperCase()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
className={styles.joinCallButton}
|
className={styles.joinCallButton}
|
||||||
disabled={state !== GroupCallState.LocalCallFeedInitialized}
|
disabled={state !== GroupCallState.LocalCallFeedInitialized}
|
||||||
|
|
|
@ -53,15 +53,28 @@ limitations under the License.
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: var(--bgColor3);
|
background-color: var(--bgColor3);
|
||||||
margin: 40px 20px 20px 20px;
|
margin: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview video {
|
.preview video {
|
||||||
width: 100%;
|
width: calc(100% + 1px);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
transform: scaleX(-1);
|
/* transform scale doesn't perfectly match width, so make -1.01 border issues */
|
||||||
|
transform: scaleX(-1.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatarContainer {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 66px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--bgColor3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.webcamPermissions {
|
.webcamPermissions {
|
||||||
|
@ -108,3 +121,9 @@ limitations under the License.
|
||||||
.previewButtons > :last-child {
|
.previewButtons > :last-child {
|
||||||
margin-right: 0px;
|
margin-right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 800px) {
|
||||||
|
.preview {
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -38,15 +38,11 @@ export function OverflowMenu({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PopoverMenuTrigger disableOnState>
|
<PopoverMenuTrigger disableOnState>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger placement="top">
|
||||||
<Button variant="toolbar">
|
<Button variant="toolbar">
|
||||||
<OverflowIcon />
|
<OverflowIcon />
|
||||||
</Button>
|
</Button>
|
||||||
{(props) => (
|
{() => "More"}
|
||||||
<Tooltip position="top" {...props}>
|
|
||||||
More
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
{(props) => (
|
{(props) => (
|
||||||
<Menu {...props} label="More menu" onAction={onAction}>
|
<Menu {...props} label="More menu" onAction={onAction}>
|
||||||
|
|
|
@ -49,7 +49,7 @@ export function RoomAuthView() {
|
||||||
<HeaderLogo />
|
<HeaderLogo />
|
||||||
</LeftNav>
|
</LeftNav>
|
||||||
<RightNav>
|
<RightNav>
|
||||||
<UserMenuContainer disableLogout />
|
<UserMenuContainer preventNavigation />
|
||||||
</RightNav>
|
</RightNav>
|
||||||
</Header>
|
</Header>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
|
@ -60,15 +60,15 @@ export function RoomAuthView() {
|
||||||
<InputField
|
<InputField
|
||||||
id="userName"
|
id="userName"
|
||||||
name="userName"
|
name="userName"
|
||||||
label="Your name"
|
label="Username"
|
||||||
placeholder="Your name"
|
placeholder="Username"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
<Caption>
|
<Caption>
|
||||||
By clicking "Go", you agree to our{" "}
|
By clicking "Join call now", you agree to our{" "}
|
||||||
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
|
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
|
||||||
</Caption>
|
</Caption>
|
||||||
{error && (
|
{error && (
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
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";
|
import { TabContainer, TabItem } from "../tabs/Tabs";
|
||||||
import { ReactComponent as AudioIcon } from "../icons/Audio.svg";
|
import { ReactComponent as AudioIcon } from "../icons/Audio.svg";
|
||||||
import { ReactComponent as VideoIcon } from "../icons/Video.svg";
|
import { ReactComponent as VideoIcon } from "../icons/Video.svg";
|
||||||
import { ReactComponent as DeveloperIcon } from "../icons/Developer.svg";
|
import { ReactComponent as DeveloperIcon } from "../icons/Developer.svg";
|
||||||
import { SelectInput } from "../input/SelectInput";
|
import { SelectInput } from "../input/SelectInput";
|
||||||
import { Item } from "@react-stately/collections";
|
import { Item } from "@react-stately/collections";
|
||||||
import { useMediaHandler } from "./useMediaHandler";
|
import { useMediaHandler } from "./useMediaHandler";
|
||||||
import { FieldRow, InputField } from "../input/Input";
|
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
|
||||||
|
import { Button } from "../button";
|
||||||
|
import { useSubmitRageshake } from "./useSubmitRageshake";
|
||||||
|
import { Subtitle } from "../typography/Typography";
|
||||||
|
|
||||||
export function SettingsModal({
|
export function SettingsModal({
|
||||||
client,
|
client,
|
||||||
|
@ -25,6 +28,11 @@ export function SettingsModal({
|
||||||
setVideoInput,
|
setVideoInput,
|
||||||
} = useMediaHandler(client);
|
} = useMediaHandler(client);
|
||||||
|
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
|
||||||
|
const { submitRageshake, sending, sent, error, downloadDebugLog } =
|
||||||
|
useSubmitRageshake();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title="Settings"
|
title="Settings"
|
||||||
|
@ -88,6 +96,34 @@ export function SettingsModal({
|
||||||
onChange={(e) => setShowInspector(e.target.checked)}
|
onChange={(e) => setShowInspector(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
|
<Subtitle>Feedback</Subtitle>
|
||||||
|
<FieldRow>
|
||||||
|
<InputField
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
label="Description"
|
||||||
|
type="text"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FieldRow>
|
||||||
|
<FieldRow>
|
||||||
|
<Button onPress={() => submitRageshake({ description })}>
|
||||||
|
{sent
|
||||||
|
? "Debug Logs Sent"
|
||||||
|
: sending
|
||||||
|
? "Sending Debug Logs..."
|
||||||
|
: "Send Debug Logs"}
|
||||||
|
</Button>
|
||||||
|
</FieldRow>
|
||||||
|
{error && (
|
||||||
|
<FieldRow>
|
||||||
|
<ErrorMessage>{error.message}</ErrorMessage>
|
||||||
|
</FieldRow>
|
||||||
|
)}
|
||||||
|
<FieldRow>
|
||||||
|
<Button onPress={downloadDebugLog}>Download Debug Logs</Button>
|
||||||
|
</FieldRow>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</TabContainer>
|
</TabContainer>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
228
src/settings/useSubmitRageshake.js
Normal file
228
src/settings/useSubmitRageshake.js
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
import { useCallback, useContext, useState } from "react";
|
||||||
|
import * as rageshake from "matrix-react-sdk/src/rageshake/rageshake";
|
||||||
|
import pako from "pako";
|
||||||
|
import { useClient } from "../ClientContext";
|
||||||
|
import { InspectorContext } from "../room/GroupCallInspector";
|
||||||
|
|
||||||
|
export function useSubmitRageshake() {
|
||||||
|
const { client } = useClient();
|
||||||
|
const [{ json }] = useContext(InspectorContext);
|
||||||
|
|
||||||
|
const [{ sending, sent, error }, setState] = useState({
|
||||||
|
sending: false,
|
||||||
|
sent: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const submitRageshake = useCallback(
|
||||||
|
async (opts) => {
|
||||||
|
if (sending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setState({ sending: true, sent: false, error: null });
|
||||||
|
|
||||||
|
let userAgent = "UNKNOWN";
|
||||||
|
if (window.navigator && window.navigator.userAgent) {
|
||||||
|
userAgent = window.navigator.userAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
let touchInput = "UNKNOWN";
|
||||||
|
try {
|
||||||
|
// MDN claims broad support across browsers
|
||||||
|
touchInput = String(window.matchMedia("(pointer: coarse)").matches);
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
const body = new FormData();
|
||||||
|
body.append(
|
||||||
|
"text",
|
||||||
|
opts.description || "User did not supply any additional text."
|
||||||
|
);
|
||||||
|
body.append("app", "matrix-video-chat");
|
||||||
|
body.append("version", "dev");
|
||||||
|
body.append("user_agent", userAgent);
|
||||||
|
body.append("installed_pwa", false);
|
||||||
|
body.append("touch_input", touchInput);
|
||||||
|
|
||||||
|
if (client) {
|
||||||
|
body.append("user_id", client.credentials.userId);
|
||||||
|
body.append("device_id", client.deviceId);
|
||||||
|
|
||||||
|
if (client.isCryptoEnabled()) {
|
||||||
|
const keys = [`ed25519:${client.getDeviceEd25519Key()}`];
|
||||||
|
if (client.getDeviceCurve25519Key) {
|
||||||
|
keys.push(`curve25519:${client.getDeviceCurve25519Key()}`);
|
||||||
|
}
|
||||||
|
body.append("device_keys", keys.join(", "));
|
||||||
|
body.append("cross_signing_key", client.getCrossSigningId());
|
||||||
|
|
||||||
|
// add cross-signing status information
|
||||||
|
const crossSigning = client.crypto.crossSigningInfo;
|
||||||
|
const secretStorage = client.crypto.secretStorage;
|
||||||
|
|
||||||
|
body.append(
|
||||||
|
"cross_signing_ready",
|
||||||
|
String(await client.isCrossSigningReady())
|
||||||
|
);
|
||||||
|
body.append(
|
||||||
|
"cross_signing_supported_by_hs",
|
||||||
|
String(
|
||||||
|
await client.doesServerSupportUnstableFeature(
|
||||||
|
"org.matrix.e2e_cross_signing"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
body.append("cross_signing_key", crossSigning.getId());
|
||||||
|
body.append(
|
||||||
|
"cross_signing_privkey_in_secret_storage",
|
||||||
|
String(
|
||||||
|
!!(await crossSigning.isStoredInSecretStorage(secretStorage))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const pkCache = client.getCrossSigningCacheCallbacks();
|
||||||
|
body.append(
|
||||||
|
"cross_signing_master_privkey_cached",
|
||||||
|
String(
|
||||||
|
!!(pkCache && (await pkCache.getCrossSigningKeyCache("master")))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
body.append(
|
||||||
|
"cross_signing_self_signing_privkey_cached",
|
||||||
|
String(
|
||||||
|
!!(
|
||||||
|
pkCache &&
|
||||||
|
(await pkCache.getCrossSigningKeyCache("self_signing"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
body.append(
|
||||||
|
"cross_signing_user_signing_privkey_cached",
|
||||||
|
String(
|
||||||
|
!!(
|
||||||
|
pkCache &&
|
||||||
|
(await pkCache.getCrossSigningKeyCache("user_signing"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
body.append(
|
||||||
|
"secret_storage_ready",
|
||||||
|
String(await client.isSecretStorageReady())
|
||||||
|
);
|
||||||
|
body.append(
|
||||||
|
"secret_storage_key_in_account",
|
||||||
|
String(!!(await secretStorage.hasKey()))
|
||||||
|
);
|
||||||
|
|
||||||
|
body.append(
|
||||||
|
"session_backup_key_in_secret_storage",
|
||||||
|
String(!!(await client.isKeyBackupKeyStored()))
|
||||||
|
);
|
||||||
|
const sessionBackupKeyFromCache =
|
||||||
|
await client.crypto.getSessionBackupPrivateKey();
|
||||||
|
body.append(
|
||||||
|
"session_backup_key_cached",
|
||||||
|
String(!!sessionBackupKeyFromCache)
|
||||||
|
);
|
||||||
|
body.append(
|
||||||
|
"session_backup_key_well_formed",
|
||||||
|
String(sessionBackupKeyFromCache instanceof Uint8Array)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.label) {
|
||||||
|
body.append("label", opts.label);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add storage persistence/quota information
|
||||||
|
if (navigator.storage && navigator.storage.persisted) {
|
||||||
|
try {
|
||||||
|
body.append(
|
||||||
|
"storageManager_persisted",
|
||||||
|
String(await navigator.storage.persisted())
|
||||||
|
);
|
||||||
|
} catch (e) {}
|
||||||
|
} else if (document.hasStorageAccess) {
|
||||||
|
// Safari
|
||||||
|
try {
|
||||||
|
body.append(
|
||||||
|
"storageManager_persisted",
|
||||||
|
String(await document.hasStorageAccess())
|
||||||
|
);
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navigator.storage && navigator.storage.estimate) {
|
||||||
|
try {
|
||||||
|
const estimate = await navigator.storage.estimate();
|
||||||
|
body.append("storageManager_quota", String(estimate.quota));
|
||||||
|
body.append("storageManager_usage", String(estimate.usage));
|
||||||
|
if (estimate.usageDetails) {
|
||||||
|
Object.keys(estimate.usageDetails).forEach((k) => {
|
||||||
|
body.append(
|
||||||
|
`storageManager_usage_${k}`,
|
||||||
|
String(estimate.usageDetails[k])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const logs = await rageshake.getLogsForReport();
|
||||||
|
|
||||||
|
for (const entry of logs) {
|
||||||
|
// encode as UTF-8
|
||||||
|
let buf = new TextEncoder().encode(entry.lines);
|
||||||
|
|
||||||
|
// compress
|
||||||
|
buf = pako.gzip(buf);
|
||||||
|
|
||||||
|
body.append("compressed-log", new Blob([buf]), entry.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json) {
|
||||||
|
body.append(
|
||||||
|
"file",
|
||||||
|
new Blob([JSON.stringify(json)], { type: "text/plain" }),
|
||||||
|
"groupcall.txt"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await fetch(
|
||||||
|
import.meta.env.VITE_RAGESHAKE_SUBMIT_URL ||
|
||||||
|
"https://element.io/bugreports/submit",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
setState({ sending: false, sent: true, error: null });
|
||||||
|
} catch (error) {
|
||||||
|
setState({ sending: false, sent: false, error });
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[client]
|
||||||
|
);
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { submitRageshake, sending, sent, error, downloadDebugLog };
|
||||||
|
}
|
|
@ -1,15 +1,14 @@
|
||||||
.tabContainer {
|
.tabContainer {
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabList {
|
.tabList {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0 auto 24px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
|
@ -20,14 +19,14 @@
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 16px;
|
padding: 0 8px;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab > * {
|
.tab > * {
|
||||||
color: var(--textColor4);
|
color: var(--textColor4);
|
||||||
margin-right: 16px;
|
margin: 0 8px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab svg * {
|
.tab svg * {
|
||||||
|
@ -57,6 +56,31 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 0 40px;
|
padding: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 800px) {
|
||||||
|
.tab {
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab > * {
|
||||||
|
margin: 0 16px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabContainer {
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: row;
|
||||||
|
margin: 27px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabList {
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabPanel {
|
||||||
|
padding: 0 40px;
|
||||||
|
}
|
||||||
|
}
|
49
src/tabs/Tabs.stories.jsx
Normal file
49
src/tabs/Tabs.stories.jsx
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import React from "react";
|
||||||
|
import { TabContainer, TabItem } from "./Tabs";
|
||||||
|
import { ReactComponent as AudioIcon } from "../icons/Audio.svg";
|
||||||
|
import { ReactComponent as VideoIcon } from "../icons/Video.svg";
|
||||||
|
import { ReactComponent as DeveloperIcon } from "../icons/Developer.svg";
|
||||||
|
import { Body } from "../typography/Typography";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Tabs",
|
||||||
|
component: TabContainer,
|
||||||
|
parameters: {
|
||||||
|
layout: "fullscreen",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Tabs = () => (
|
||||||
|
<TabContainer>
|
||||||
|
<TabItem
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
<AudioIcon width={16} height={16} />
|
||||||
|
<Body>Audio</Body>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Audio Tab Content
|
||||||
|
</TabItem>
|
||||||
|
<TabItem
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
<VideoIcon width={16} height={16} />
|
||||||
|
<Body>Video</Body>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Video Tab Content
|
||||||
|
</TabItem>
|
||||||
|
<TabItem
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
<DeveloperIcon width={16} height={16} />
|
||||||
|
<Body>Developer</Body>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Developer Tab Content
|
||||||
|
</TabItem>
|
||||||
|
</TabContainer>
|
||||||
|
);
|
|
@ -1,3 +1,3 @@
|
||||||
.hideFocus * {
|
.hideFocus * {
|
||||||
outline: none;
|
outline: none !important;
|
||||||
}
|
}
|
||||||
|
|
9
src/usePageTitle.js
Normal file
9
src/usePageTitle.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export function usePageTitle(title) {
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = title
|
||||||
|
? `${import.meta.env.VITE_PRODUCT_NAME} | ${title}`
|
||||||
|
: import.meta.env.VITE_PRODUCT_NAME;
|
||||||
|
}, [title]);
|
||||||
|
}
|
|
@ -1,17 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"experimentalDecorators": true,
|
|
||||||
"emitDecoratorMetadata": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"module": "commonjs",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"target": "es2016",
|
|
||||||
"noImplicitAny": false,
|
|
||||||
"sourceMap": false,
|
|
||||||
"declaration": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"jsx": "react",
|
|
||||||
"lib": ["es2019", "dom", "dom.iterable"]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import { defineConfig, loadEnv } from "vite";
|
import { defineConfig, loadEnv } from "vite";
|
||||||
import svgrPlugin from "vite-plugin-svgr";
|
import svgrPlugin from "vite-plugin-svgr";
|
||||||
|
import { createHtmlPlugin } from "vite-plugin-html";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
|
@ -23,17 +24,37 @@ export default defineConfig(({ mode }) => {
|
||||||
const env = loadEnv(mode, process.cwd());
|
const env = loadEnv(mode, process.cwd());
|
||||||
|
|
||||||
return {
|
return {
|
||||||
plugins: [svgrPlugin()],
|
plugins: [
|
||||||
|
svgrPlugin(),
|
||||||
|
createHtmlPlugin({
|
||||||
|
inject: {
|
||||||
|
data: {
|
||||||
|
title: env.VITE_PRODUCT_NAME || "Matrix Video Chat",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
"/_matrix": env.VITE_DEFAULT_HOMESERVER || "http://localhost:8008",
|
"/_matrix": env.VITE_DEFAULT_HOMESERVER || "http://localhost:8008",
|
||||||
},
|
},
|
||||||
|
fs: {
|
||||||
|
// Current we're bundling files linked in from matrix-react-sdk
|
||||||
|
// We should re-enable this if we plan to run Vite outside the dev server mode
|
||||||
|
strict: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"$(res)": path.resolve(__dirname, "node_modules/matrix-react-sdk/res"),
|
"$(res)": path.resolve(__dirname, "node_modules/matrix-react-sdk/res"),
|
||||||
},
|
},
|
||||||
dedupe: ["react", "react-dom", "matrix-js-sdk"],
|
dedupe: [
|
||||||
|
"react",
|
||||||
|
"react-dom",
|
||||||
|
"matrix-js-sdk",
|
||||||
|
"react-use-measure",
|
||||||
|
"@juggle/resize-observer",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue