mirror of
https://github.com/vector-im/element-call.git
synced 2024-11-15 00:04:59 +08:00
Merge branch 'main' into robertlong/spotlight-layout
This commit is contained in:
commit
dec47d21c0
3
.env
3
.env
@ -7,6 +7,9 @@
|
||||
# Used for determining the homeserver to use for short urls etc.
|
||||
# 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.
|
||||
# VITE_SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0
|
||||
|
||||
|
@ -2,13 +2,20 @@ const svgrPlugin = require("vite-plugin-svgr");
|
||||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
|
||||
addons: ["@storybook/addon-links", "@storybook/addon-essentials"],
|
||||
stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"],
|
||||
framework: "@storybook/react",
|
||||
core: {
|
||||
builder: "storybook-builder-vite",
|
||||
},
|
||||
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.resolve = config.resolve || {};
|
||||
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>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/src/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
||||
<title>Matrix Video Chat</title>
|
||||
<script>
|
||||
window.global = window;
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/src/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
||||
<title>
|
||||
<%- title %>
|
||||
</title>
|
||||
<script>
|
||||
window.global = window;
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@juggle/resize-observer": "^3.3.1",
|
||||
"@react-aria/button": "^3.3.4",
|
||||
"@react-aria/dialog": "^3.1.4",
|
||||
"@react-aria/focus": "^3.5.0",
|
||||
@ -29,7 +30,9 @@
|
||||
"events": "^3.3.0",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-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",
|
||||
"pako": "^2.0.4",
|
||||
"postcss-preset-env": "^6.7.0",
|
||||
"re-resizable": "^6.9.0",
|
||||
"react": "^17.0.0",
|
||||
@ -37,18 +40,17 @@
|
||||
"react-json-view": "^1.21.3",
|
||||
"react-router": "6",
|
||||
"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": {
|
||||
"@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",
|
||||
"babel-loader": "^8.2.3",
|
||||
"sass": "^1.42.1",
|
||||
"storybook-builder-vite": "^0.1.12",
|
||||
"vite": "^2.4.2",
|
||||
"vite-plugin-html": "^3.0.3",
|
||||
"vite-plugin-svgr": "^0.4.0"
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import { RoomPage } from "./room/RoomPage";
|
||||
import { RoomRedirect } from "./room/RoomRedirect";
|
||||
import { ClientProvider } from "./ClientContext";
|
||||
import { usePageFocusStyle } from "./usePageFocusStyle";
|
||||
import { SequenceDiagramViewerPage } from "./SequenceDiagramViewerPage";
|
||||
|
||||
const SentryRoute = Sentry.withSentryRouting(Route);
|
||||
|
||||
@ -48,6 +49,9 @@ export default function App({ history }) {
|
||||
<SentryRoute path="/room/:roomId?">
|
||||
<RoomPage />
|
||||
</SentryRoute>
|
||||
<SentryRoute path="/inspector">
|
||||
<SequenceDiagramViewerPage />
|
||||
</SentryRoute>
|
||||
<SentryRoute path="*">
|
||||
<RoomRedirect />
|
||||
</SentryRoute>
|
||||
|
@ -49,7 +49,7 @@
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 42px;
|
||||
font-size: 36px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.xl {
|
||||
|
@ -7,6 +7,7 @@ import { ReactComponent as VideoIcon } from "./icons/Video.svg";
|
||||
import { ReactComponent as ArrowLeftIcon } from "./icons/ArrowLeft.svg";
|
||||
import { useButton } from "@react-aria/button";
|
||||
import { Subtitle } from "./typography/Typography";
|
||||
import { Avatar } from "./Avatar";
|
||||
|
||||
export function Header({ children, className, ...rest }) {
|
||||
return (
|
||||
@ -60,6 +61,11 @@ export function RoomHeaderInfo({ roomName }) {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.roomAvatar}>
|
||||
<Avatar
|
||||
size="md"
|
||||
bgKey={roomName}
|
||||
fallback={roomName.slice(0, 1).toUpperCase()}
|
||||
/>
|
||||
<VideoIcon width={16} height={16} />
|
||||
</div>
|
||||
<Subtitle fontWeight="semiBold">{roomName}</Subtitle>
|
||||
|
@ -23,10 +23,6 @@
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.option.selected {
|
||||
color: #0dbd8b;
|
||||
}
|
||||
|
||||
.option.focused {
|
||||
background-color: rgba(111, 120, 130, 0.2);
|
||||
}
|
||||
|
@ -16,7 +16,7 @@
|
||||
}
|
||||
|
||||
.menuItem > * {
|
||||
margin-right: 10px;
|
||||
margin: 0 10px 0 0;
|
||||
}
|
||||
|
||||
.menuItem > :last-child {
|
||||
|
@ -52,6 +52,12 @@
|
||||
}
|
||||
|
||||
@media (max-width: 799px) {
|
||||
.modalHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 24px 24px 0 24px;
|
||||
}
|
||||
|
||||
.modal.mobileFullScreen {
|
||||
position: fixed;
|
||||
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 { FocusableProvider } from "@react-aria/focus";
|
||||
import { useTooltipTrigger, useTooltip } from "@react-aria/tooltip";
|
||||
import { mergeProps, useObjectRef } from "@react-aria/utils";
|
||||
import styles from "./Tooltip.module.css";
|
||||
import classNames from "classnames";
|
||||
import { OverlayContainer, useOverlayPosition } from "@react-aria/overlays";
|
||||
|
||||
export function Tooltip({ position, state, ...props }) {
|
||||
let { tooltipProps } = useTooltip(props, state);
|
||||
export const Tooltip = forwardRef(
|
||||
({ position, state, className, ...props }, ref) => {
|
||||
let { tooltipProps } = useTooltip(props, state);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.tooltip, styles[position || "bottom"])}
|
||||
{...mergeProps(props, tooltipProps)}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.tooltip, className)}
|
||||
{...mergeProps(props, tooltipProps)}
|
||||
ref={ref}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const TooltipTrigger = forwardRef(({ children, ...rest }, ref) => {
|
||||
const tooltipState = useTooltipTriggerState(rest);
|
||||
const triggerRef = useObjectRef(ref);
|
||||
const overlayRef = useRef();
|
||||
const { triggerProps, tooltipProps } = useTooltipTrigger(
|
||||
rest,
|
||||
tooltipState,
|
||||
triggerRef
|
||||
);
|
||||
|
||||
const { overlayProps } = useOverlayPosition({
|
||||
placement: rest.placement || "top",
|
||||
targetRef: triggerRef,
|
||||
overlayRef,
|
||||
isOpen: tooltipState.isOpen,
|
||||
offset: 5,
|
||||
});
|
||||
|
||||
if (
|
||||
!Array.isArray(children) ||
|
||||
children.length > 2 ||
|
||||
@ -40,13 +54,20 @@ export const TooltipTrigger = forwardRef(({ children, ...rest }, ref) => {
|
||||
const [tooltipTrigger, tooltip] = children;
|
||||
|
||||
return (
|
||||
<div className={styles.tooltipContainer}>
|
||||
<tooltipTrigger.type
|
||||
{...mergeProps(triggerProps, tooltipTrigger.props, rest)}
|
||||
ref={triggerRef}
|
||||
/>
|
||||
{tooltipState.isOpen && tooltip({ state: tooltipState, ...tooltipProps })}
|
||||
</div>
|
||||
<FocusableProvider ref={triggerRef} {...triggerProps}>
|
||||
{<tooltipTrigger.type {...mergeProps(tooltipTrigger.props, rest)} />}
|
||||
{tooltipState.isOpen && (
|
||||
<OverlayContainer>
|
||||
<Tooltip
|
||||
state={tooltipState}
|
||||
{...mergeProps(tooltipProps, overlayProps)}
|
||||
ref={overlayRef}
|
||||
>
|
||||
{tooltip()}
|
||||
</Tooltip>
|
||||
</OverlayContainer>
|
||||
)}
|
||||
</FocusableProvider>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
.tooltip {
|
||||
background-color: var(--bgColor2);
|
||||
position: absolute;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@ -9,25 +8,5 @@
|
||||
border-radius: 8px;
|
||||
max-width: 135px;
|
||||
width: max-content;
|
||||
z-index: 1;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
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 styles from "./UserMenu.module.css";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { Body } from "./typography/Typography";
|
||||
|
||||
export function UserMenu({
|
||||
disableLogout,
|
||||
preventNavigation,
|
||||
isAuthenticated,
|
||||
isPasswordlessUser,
|
||||
displayName,
|
||||
@ -31,7 +32,7 @@ export function UserMenu({
|
||||
label: displayName,
|
||||
});
|
||||
|
||||
if (isPasswordlessUser) {
|
||||
if (isPasswordlessUser && !preventNavigation) {
|
||||
arr.push({
|
||||
key: "login",
|
||||
label: "Sign In",
|
||||
@ -39,7 +40,7 @@ export function UserMenu({
|
||||
});
|
||||
}
|
||||
|
||||
if (!isPasswordlessUser && !disableLogout) {
|
||||
if (!isPasswordlessUser && !preventNavigation) {
|
||||
arr.push({
|
||||
key: "logout",
|
||||
label: "Sign Out",
|
||||
@ -49,7 +50,7 @@ export function UserMenu({
|
||||
}
|
||||
|
||||
return arr;
|
||||
}, [isAuthenticated, isPasswordlessUser, displayName, disableLogout]);
|
||||
}, [isAuthenticated, isPasswordlessUser, displayName, preventNavigation]);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
@ -61,9 +62,9 @@ export function UserMenu({
|
||||
|
||||
return (
|
||||
<PopoverMenuTrigger placement="bottom right">
|
||||
<TooltipTrigger>
|
||||
<TooltipTrigger placement="bottom left">
|
||||
<Button variant="icon" className={styles.userButton}>
|
||||
{isAuthenticated && !isPasswordlessUser ? (
|
||||
{isAuthenticated && (!isPasswordlessUser || avatarUrl) ? (
|
||||
<Avatar
|
||||
size="sm"
|
||||
className={styles.avatar}
|
||||
@ -74,18 +75,14 @@ export function UserMenu({
|
||||
<UserIcon />
|
||||
)}
|
||||
</Button>
|
||||
{(props) => (
|
||||
<Tooltip position="bottomLeft" {...props}>
|
||||
Profile
|
||||
</Tooltip>
|
||||
)}
|
||||
{() => "Profile"}
|
||||
</TooltipTrigger>
|
||||
{(props) => (
|
||||
<Menu {...props} label="User menu" onAction={onAction}>
|
||||
{items.map(({ key, icon: Icon, label }) => (
|
||||
<Item key={key} textValue={label}>
|
||||
<Icon />
|
||||
<span>{label}</span>
|
||||
<Item key={key} textValue={label} className={styles.menuItem}>
|
||||
<Icon width={24} height={24} className={styles.menuIcon} />
|
||||
<Body overflowEllipsis>{label}</Body>
|
||||
</Item>
|
||||
))}
|
||||
</Menu>
|
||||
|
@ -1,3 +1,8 @@
|
||||
.menuIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.userButton svg * {
|
||||
fill: var(--textColor1);
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import { useModalTriggerState } from "./Modal";
|
||||
import { ProfileModal } from "./profile/ProfileModal";
|
||||
import { UserMenu } from "./UserMenu";
|
||||
|
||||
export function UserMenuContainer({ disableLogout }) {
|
||||
export function UserMenuContainer({ preventNavigation }) {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const { isAuthenticated, isPasswordlessUser, logout, userName, client } =
|
||||
@ -34,7 +34,7 @@ export function UserMenuContainer({ disableLogout }) {
|
||||
return (
|
||||
<>
|
||||
<UserMenu
|
||||
disableLogout={disableLogout}
|
||||
preventNavigation={preventNavigation}
|
||||
isAuthenticated={isAuthenticated}
|
||||
isPasswordlessUser={isPasswordlessUser}
|
||||
avatarUrl={avatarUrl}
|
||||
|
@ -22,8 +22,11 @@ import { Button } from "../button";
|
||||
import { defaultHomeserver, defaultHomeserverHost } from "../matrix-utils";
|
||||
import styles from "./LoginPage.module.css";
|
||||
import { useInteractiveLogin } from "./useInteractiveLogin";
|
||||
import { usePageTitle } from "../usePageTitle";
|
||||
|
||||
export function LoginPage() {
|
||||
usePageTitle("Login");
|
||||
|
||||
const [_, login] = useInteractiveLogin();
|
||||
const [homeserver, setHomeServer] = useState(defaultHomeserver);
|
||||
const usernameRef = useRef();
|
||||
|
@ -1,6 +1,7 @@
|
||||
.logo {
|
||||
max-width: 300px;
|
||||
margin: 80px 0;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.container {
|
||||
|
@ -26,8 +26,11 @@ import { ReactComponent as Logo } from "../icons/LogoLarge.svg";
|
||||
import { LoadingView } from "../FullScreenView";
|
||||
import { useRecaptcha } from "./useRecaptcha";
|
||||
import { Caption, Link } from "../typography/Typography";
|
||||
import { usePageTitle } from "../usePageTitle";
|
||||
|
||||
export function RegisterPage() {
|
||||
usePageTitle("Register");
|
||||
|
||||
const {
|
||||
loading,
|
||||
client,
|
||||
@ -177,7 +180,7 @@ export function RegisterPage() {
|
||||
</Link>{" "}
|
||||
apply.
|
||||
<br />
|
||||
By clicking "Go", you agree to our{" "}
|
||||
By clicking "Log in", you agree to our{" "}
|
||||
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
|
||||
</Caption>
|
||||
)}
|
||||
|
@ -66,6 +66,8 @@ export function useInteractiveRegistration() {
|
||||
deviceId: device_id,
|
||||
});
|
||||
|
||||
await client.setDisplayName(username);
|
||||
|
||||
const session = { user_id, device_id, access_token, passwordlessUser };
|
||||
|
||||
if (passwordlessUser) {
|
||||
|
@ -57,6 +57,7 @@ export function useRecaptcha(sitekey) {
|
||||
}
|
||||
|
||||
if (!window.grecaptcha) {
|
||||
console.log("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(() => {
|
||||
if (window.grecaptcha) {
|
||||
|
@ -34,12 +34,17 @@ export const Button = forwardRef(
|
||||
iconStyle,
|
||||
className,
|
||||
children,
|
||||
onPress,
|
||||
onPressStart,
|
||||
...rest
|
||||
},
|
||||
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
|
||||
// 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 }) {
|
||||
return (
|
||||
<TooltipTrigger>
|
||||
<Button variant="toolbar" {...rest} off={muted}>
|
||||
{muted ? <MuteMicIcon /> : <MicIcon />}
|
||||
</Button>
|
||||
{(props) => (
|
||||
<Tooltip position="top" {...props}>
|
||||
{muted ? "Unmute microphone" : "Mute microphone"}
|
||||
</Tooltip>
|
||||
)}
|
||||
{() => (muted ? "Unmute microphone" : "Mute microphone")}
|
||||
</TooltipTrigger>
|
||||
);
|
||||
}
|
||||
@ -100,11 +93,7 @@ export function VideoButton({ muted, ...rest }) {
|
||||
<Button variant="toolbar" {...rest} off={muted}>
|
||||
{muted ? <DisableVideoIcon /> : <VideoIcon />}
|
||||
</Button>
|
||||
{(props) => (
|
||||
<Tooltip position="top" {...props}>
|
||||
{muted ? "Turn on camera" : "Turn off camera"}
|
||||
</Tooltip>
|
||||
)}
|
||||
{() => (muted ? "Turn on camera" : "Turn off camera")}
|
||||
</TooltipTrigger>
|
||||
);
|
||||
}
|
||||
@ -115,11 +104,7 @@ export function ScreenshareButton({ enabled, className, ...rest }) {
|
||||
<Button variant="toolbar" {...rest} on={enabled}>
|
||||
<ScreenshareIcon />
|
||||
</Button>
|
||||
{(props) => (
|
||||
<Tooltip position="top" {...props}>
|
||||
{enabled ? "Stop sharing screen" : "Share screen"}
|
||||
</Tooltip>
|
||||
)}
|
||||
{() => (enabled ? "Stop sharing screen" : "Share screen")}
|
||||
</TooltipTrigger>
|
||||
);
|
||||
}
|
||||
@ -134,11 +119,7 @@ export function HangupButton({ className, ...rest }) {
|
||||
>
|
||||
<HangupIcon />
|
||||
</Button>
|
||||
{(props) => (
|
||||
<Tooltip position="top" {...props}>
|
||||
Leave
|
||||
</Tooltip>
|
||||
)}
|
||||
{() => "Leave"}
|
||||
</TooltipTrigger>
|
||||
);
|
||||
}
|
||||
|
@ -46,6 +46,15 @@ limitations under the License.
|
||||
background-color: var(--primaryColor);
|
||||
}
|
||||
|
||||
.button:focus,
|
||||
.toolbarButton:focus,
|
||||
.iconButton:focus,
|
||||
.iconCopyButton:focus,
|
||||
.secondary:focus,
|
||||
.copyButton:focus {
|
||||
outline: auto;
|
||||
}
|
||||
|
||||
.toolbarButton {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
@ -91,35 +100,6 @@ limitations under the License.
|
||||
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,
|
||||
.copyButton {
|
||||
color: #0dbd8b;
|
||||
|
@ -8,7 +8,7 @@ import styles from "./CallList.module.css";
|
||||
import { getRoomUrl } from "../matrix-utils";
|
||||
import { Body, Caption } from "../typography/Typography";
|
||||
|
||||
export function CallList({ rooms, client }) {
|
||||
export function CallList({ rooms, client, disableFacepile }) {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.callList}>
|
||||
@ -20,6 +20,7 @@ export function CallList({ rooms, client }) {
|
||||
avatarUrl={avatarUrl}
|
||||
roomId={roomId}
|
||||
participants={participants}
|
||||
disableFacepile={disableFacepile}
|
||||
/>
|
||||
))}
|
||||
{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 (
|
||||
<div className={styles.callTile}>
|
||||
<Link to={`/room/${roomId}`} className={styles.callTileLink}>
|
||||
@ -41,7 +49,7 @@ function CallTile({ name, avatarUrl, roomId, participants, client }) {
|
||||
size="lg"
|
||||
bgKey={name}
|
||||
src={avatarUrl}
|
||||
fallback={<VideoIcon width={16} height={16} />}
|
||||
fallback={name.slice(0, 1).toUpperCase()}
|
||||
className={styles.avatar}
|
||||
/>
|
||||
<div className={styles.callInfo}>
|
||||
@ -49,7 +57,7 @@ function CallTile({ name, avatarUrl, roomId, participants, client }) {
|
||||
{name}
|
||||
</Body>
|
||||
<Caption overflowEllipsis>{getRoomUrl(roomId)}</Caption>
|
||||
{participants && (
|
||||
{participants && !disableFacepile && (
|
||||
<Facepile
|
||||
className={styles.facePile}
|
||||
client={client}
|
||||
|
@ -19,8 +19,11 @@ import { useClient } from "../ClientContext";
|
||||
import { ErrorView, LoadingView } from "../FullScreenView";
|
||||
import { UnauthenticatedView } from "./UnauthenticatedView";
|
||||
import { RegisteredView } from "./RegisteredView";
|
||||
import { usePageTitle } from "../usePageTitle";
|
||||
|
||||
export function HomePage() {
|
||||
usePageTitle("Home");
|
||||
|
||||
const { isAuthenticated, isPasswordlessUser, loading, error, client } =
|
||||
useClient();
|
||||
|
||||
|
@ -107,7 +107,7 @@ export function RegisteredView({ client }) {
|
||||
<Title className={styles.recentCallsTitle}>
|
||||
Your recent Calls
|
||||
</Title>
|
||||
<CallList rooms={recentRooms} client={client} />
|
||||
<CallList rooms={recentRooms} client={client} disableFacepile />
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
|
@ -102,8 +102,8 @@ export function UnauthenticatedView() {
|
||||
<InputField
|
||||
id="userName"
|
||||
name="userName"
|
||||
label="Your name"
|
||||
placeholder="Your name"
|
||||
label="Username"
|
||||
placeholder="Username"
|
||||
type="text"
|
||||
required
|
||||
autoComplete="off"
|
||||
|
@ -27,6 +27,10 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selectTrigger:focus {
|
||||
outline: auto;
|
||||
}
|
||||
|
||||
.selectedItem {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
@ -22,6 +22,10 @@ import App from "./App";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { Integrations } from "@sentry/tracing";
|
||||
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) {
|
||||
const style = document.documentElement.style;
|
||||
@ -59,7 +63,9 @@ Sentry.init({
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<Sentry.ErrorBoundary fallback={ErrorView}>
|
||||
<App history={history} />
|
||||
<InspectorContextProvider>
|
||||
<App history={history} />
|
||||
</InspectorContextProvider>
|
||||
</Sentry.ErrorBoundary>
|
||||
</React.StrictMode>,
|
||||
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 { useMenuTriggerState } from "@react-stately/menu";
|
||||
import { useMenuTrigger } from "@react-aria/menu";
|
||||
import { OverlayContainer, useOverlayPosition } from "@react-aria/overlays";
|
||||
import { mergeProps, useObjectRef } from "@react-aria/utils";
|
||||
import classNames from "classnames";
|
||||
import { Popover } from "./Popover";
|
||||
|
||||
export function PopoverMenuTrigger({
|
||||
children,
|
||||
placement,
|
||||
className,
|
||||
disableOnState,
|
||||
...rest
|
||||
}) {
|
||||
const popoverMenuState = useMenuTriggerState(rest);
|
||||
const buttonRef = useRef();
|
||||
const { menuTriggerProps, menuProps } = useMenuTrigger(
|
||||
{},
|
||||
popoverMenuState,
|
||||
buttonRef
|
||||
);
|
||||
export const PopoverMenuTrigger = forwardRef(
|
||||
({ children, placement, className, disableOnState, ...rest }, ref) => {
|
||||
const popoverMenuState = useMenuTriggerState(rest);
|
||||
const buttonRef = useObjectRef(ref);
|
||||
const { menuTriggerProps, menuProps } = useMenuTrigger(
|
||||
{},
|
||||
popoverMenuState,
|
||||
buttonRef
|
||||
);
|
||||
|
||||
const popoverRef = useRef();
|
||||
const popoverRef = useRef();
|
||||
|
||||
const { overlayProps } = useOverlayPosition({
|
||||
targetRef: buttonRef,
|
||||
overlayRef: popoverRef,
|
||||
placement: placement || "top",
|
||||
offset: 5,
|
||||
isOpen: popoverMenuState.isOpen,
|
||||
});
|
||||
const { overlayProps } = useOverlayPosition({
|
||||
targetRef: buttonRef,
|
||||
overlayRef: popoverRef,
|
||||
placement: placement || "top",
|
||||
offset: 5,
|
||||
isOpen: popoverMenuState.isOpen,
|
||||
});
|
||||
|
||||
if (
|
||||
!Array.isArray(children) ||
|
||||
children.length > 2 ||
|
||||
typeof children[1] !== "function"
|
||||
) {
|
||||
throw new Error(
|
||||
"PopoverMenu must have two props. The first being a button and the second being a render prop."
|
||||
if (
|
||||
!Array.isArray(children) ||
|
||||
children.length > 2 ||
|
||||
typeof children[1] !== "function"
|
||||
) {
|
||||
throw new Error(
|
||||
"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({
|
||||
displayName,
|
||||
avatar,
|
||||
avatar: avatar && avatar.size > 0 ? avatar : undefined,
|
||||
});
|
||||
},
|
||||
[saveProfile]
|
||||
@ -52,6 +52,16 @@ export function ProfileModal({
|
||||
<Modal title="Profile" isDismissable {...rest}>
|
||||
<ModalContent>
|
||||
<form onSubmit={onSubmit}>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
id="userId"
|
||||
name="userId"
|
||||
label="User Id"
|
||||
type="text"
|
||||
disabled
|
||||
value={client.getUserId()}
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
id="displayName"
|
||||
@ -65,7 +75,7 @@ export function ProfileModal({
|
||||
onChange={onChangeDisplayName}
|
||||
/>
|
||||
</FieldRow>
|
||||
{isAuthenticated && !isPasswordlessUser && (
|
||||
{isAuthenticated && (
|
||||
<FieldRow>
|
||||
<InputField
|
||||
type="file"
|
||||
|
@ -46,7 +46,6 @@ limitations under the License.
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
@ -70,4 +69,8 @@ limitations under the License.
|
||||
.container {
|
||||
min-height: calc(100% - 76px);
|
||||
}
|
||||
|
||||
.main {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
@ -16,11 +16,7 @@ export function GridLayoutMenu({ layout, setLayout }) {
|
||||
<Button variant="icon">
|
||||
{layout === "spotlight" ? <SpotlightIcon /> : <FreedomIcon />}
|
||||
</Button>
|
||||
{(props) => (
|
||||
<Tooltip position="bottom" {...props}>
|
||||
Layout Type
|
||||
</Tooltip>
|
||||
)}
|
||||
{() => "Layout Type"}
|
||||
</TooltipTrigger>
|
||||
{(props) => (
|
||||
<Menu {...props} label="Grid layout menu" onAction={setLayout}>
|
||||
|
@ -1,7 +1,17 @@
|
||||
import { Resizable } from "re-resizable";
|
||||
import React, { useEffect, useState, useMemo } from "react";
|
||||
import { useCallback } from "react";
|
||||
import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
useReducer,
|
||||
useRef,
|
||||
createContext,
|
||||
useContext,
|
||||
} from "react";
|
||||
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) {
|
||||
return call.getOpponentMember()?.userId || call.invitee || null;
|
||||
@ -23,203 +33,434 @@ function getHangupCallState(call) {
|
||||
};
|
||||
}
|
||||
|
||||
export function GroupCallInspector({ client, groupCall, show }) {
|
||||
const [roomStateEvents, setRoomStateEvents] = useState([]);
|
||||
const [toDeviceEvents, setToDeviceEvents] = useState([]);
|
||||
const [sentVoipEvents, setSentVoipEvents] = useState([]);
|
||||
const [state, setState] = useState({
|
||||
userId: client.getUserId(),
|
||||
});
|
||||
const dateFormatter = new Intl.DateTimeFormat([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
fractionalSecondDigits: 3,
|
||||
});
|
||||
|
||||
const updateState = useCallback(
|
||||
(next) => setState((prev) => ({ ...prev, ...next })),
|
||||
[]
|
||||
const defaultCollapsedFields = [
|
||||
"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(() => {
|
||||
function onUpdateRoomState(event) {
|
||||
if (event) {
|
||||
setRoomStateEvents((prev) => [
|
||||
...prev,
|
||||
{
|
||||
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()])
|
||||
const callStateEvent = groupCall.room.currentState.getStateEvents(
|
||||
"org.matrix.msc3401.call",
|
||||
groupCall.groupCallId
|
||||
);
|
||||
|
||||
updateState({
|
||||
["org.matrix.msc3401.call"]: roomEvent,
|
||||
["org.matrix.msc3401.call.member"]: memberEvents,
|
||||
const memberStateEvents = groupCall.room.currentState.getStateEvents(
|
||||
"org.matrix.msc3401.call.member"
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: "receive_room_state_event",
|
||||
event,
|
||||
callStateEvent,
|
||||
memberStateEvents,
|
||||
});
|
||||
}
|
||||
|
||||
function onCallsChanged() {
|
||||
const calls = groupCall.calls.reduce((obj, call) => {
|
||||
obj[
|
||||
`${call.callId} (${call.getOpponentMember()?.userId || call.sender})`
|
||||
] = getCallState(call);
|
||||
return obj;
|
||||
}, {});
|
||||
// function onCallsChanged() {
|
||||
// const calls = groupCall.calls.reduce((obj, call) => {
|
||||
// obj[
|
||||
// `${call.callId} (${call.getOpponentMember()?.userId || call.sender})`
|
||||
// ] = getCallState(call);
|
||||
// return obj;
|
||||
// }, {});
|
||||
|
||||
updateState({ calls });
|
||||
}
|
||||
// updateState({ calls });
|
||||
// }
|
||||
|
||||
function onCallHangup(call) {
|
||||
setState(({ hangupCalls, ...rest }) => ({
|
||||
...rest,
|
||||
hangupCalls: {
|
||||
...hangupCalls,
|
||||
[`${call.callId} (${
|
||||
call.getOpponentMember()?.userId || call.sender
|
||||
})`]: getHangupCallState(call),
|
||||
},
|
||||
}));
|
||||
}
|
||||
// function onCallHangup(call) {
|
||||
// setState(({ hangupCalls, ...rest }) => ({
|
||||
// ...rest,
|
||||
// hangupCalls: {
|
||||
// ...hangupCalls,
|
||||
// [`${call.callId} (${
|
||||
// call.getOpponentMember()?.userId || call.sender
|
||||
// })`]: getHangupCallState(call),
|
||||
// },
|
||||
// }));
|
||||
// dispatch({ type: "call_hangup", call });
|
||||
// }
|
||||
|
||||
function onToDeviceEvent(event) {
|
||||
const eventType = event.getType();
|
||||
|
||||
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() },
|
||||
]);
|
||||
dispatch({ type: "receive_to_device_event", event });
|
||||
}
|
||||
|
||||
function onSendVoipEvent(event) {
|
||||
setSentVoipEvents((prev) => [...prev, event]);
|
||||
dispatch({ type: "send_voip_event", event });
|
||||
}
|
||||
|
||||
client.on("RoomState.events", onUpdateRoomState);
|
||||
groupCall.on("calls_changed", onCallsChanged);
|
||||
//groupCall.on("calls_changed", onCallsChanged);
|
||||
groupCall.on("send_voip_event", onSendVoipEvent);
|
||||
client.on("state", onCallsChanged);
|
||||
client.on("hangup", onCallHangup);
|
||||
//client.on("state", onCallsChanged);
|
||||
//client.on("hangup", onCallHangup);
|
||||
client.on("toDeviceEvent", onToDeviceEvent);
|
||||
|
||||
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 () => {
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Resizable enable={{ top: true }} defaultSize={{ height: 200 }}>
|
||||
<ReactJson
|
||||
theme="monokai"
|
||||
src={{
|
||||
...state,
|
||||
roomStateEvents,
|
||||
toDeviceEvents,
|
||||
toDeviceEventsByCall,
|
||||
sentVoipEvents,
|
||||
sentVoipEventsByCall,
|
||||
}}
|
||||
name={null}
|
||||
indentWidth={2}
|
||||
collapsed={1}
|
||||
displayDataTypes={false}
|
||||
displayObjectSize={false}
|
||||
enableClipboard={false}
|
||||
style={{ height: "100%", overflowY: "scroll" }}
|
||||
/>
|
||||
<Resizable
|
||||
enable={{ top: true }}
|
||||
defaultSize={{ height: 200 }}
|
||||
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" && (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
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 { useLoadGroupCall } from "./useLoadGroupCall";
|
||||
import { ErrorView, FullScreenView } from "../FullScreenView";
|
||||
import { usePageTitle } from "../usePageTitle";
|
||||
|
||||
export function GroupCallLoader({ client, roomId, viaServers, children }) {
|
||||
const { loading, error, groupCall } = useLoadGroupCall(
|
||||
@ -9,6 +10,8 @@ export function GroupCallLoader({ client, roomId, viaServers, children }) {
|
||||
viaServers
|
||||
);
|
||||
|
||||
usePageTitle(groupCall ? groupCall.room.name : "Loading...");
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<FullScreenView>
|
||||
|
@ -15,7 +15,19 @@ export function GroupCallView({
|
||||
groupCall,
|
||||
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 {
|
||||
state,
|
||||
error,
|
||||
@ -46,12 +58,11 @@ export function GroupCallView({
|
||||
const history = useHistory();
|
||||
|
||||
const onLeave = useCallback(() => {
|
||||
setLeft(true);
|
||||
leave();
|
||||
|
||||
if (!isPasswordlessUser) {
|
||||
history.push("/");
|
||||
} else {
|
||||
setLeft(true);
|
||||
}
|
||||
}, [leave, history]);
|
||||
|
||||
@ -75,7 +86,7 @@ export function GroupCallView({
|
||||
localScreenshareFeed={localScreenshareFeed}
|
||||
screenshareFeeds={screenshareFeeds}
|
||||
simpleGrid={simpleGrid}
|
||||
setShowInspector={setShowInspector}
|
||||
setShowInspector={onChangeShowInspector}
|
||||
showInspector={showInspector}
|
||||
roomId={roomId}
|
||||
/>
|
||||
@ -102,7 +113,7 @@ export function GroupCallView({
|
||||
localVideoMuted={localVideoMuted}
|
||||
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
||||
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
||||
setShowInspector={setShowInspector}
|
||||
setShowInspector={onChangeShowInspector}
|
||||
showInspector={showInspector}
|
||||
roomId={roomId}
|
||||
/>
|
||||
|
@ -128,7 +128,7 @@ export function InCallView({
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
<GridLayoutMenu layout={layout} setLayout={setLayout} />
|
||||
<UserMenuContainer disableLogout />
|
||||
<UserMenuContainer preventNavigation />
|
||||
</RightNav>
|
||||
</Header>
|
||||
{items.length === 0 ? (
|
||||
|
@ -9,6 +9,11 @@ import { getRoomUrl } from "../matrix-utils";
|
||||
import { OverflowMenu } from "./OverflowMenu";
|
||||
import { UserMenuContainer } from "../UserMenuContainer";
|
||||
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({
|
||||
client,
|
||||
@ -27,9 +32,11 @@ export function LobbyView({
|
||||
}) {
|
||||
const { stream } = useCallFeed(localCallFeed);
|
||||
const videoRef = useMediaStream(stream, true);
|
||||
const { displayName, avatarUrl } = useProfile(client);
|
||||
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
|
||||
const avatarSize = (previewBounds.height - 66) / 2;
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: Only init once
|
||||
onInitLocalCallFeed();
|
||||
}, [onInitLocalCallFeed]);
|
||||
|
||||
@ -45,7 +52,7 @@ export function LobbyView({
|
||||
</Header>
|
||||
<div className={styles.joinRoom}>
|
||||
<div className={styles.joinRoomContent}>
|
||||
<div className={styles.preview}>
|
||||
<div className={styles.preview} ref={previewRef}>
|
||||
<video ref={videoRef} muted playsInline disablePictureInPicture />
|
||||
{state === GroupCallState.LocalCallFeedUninitialized && (
|
||||
<Body fontWeight="semiBold" className={styles.webcamPermissions}>
|
||||
@ -59,6 +66,20 @@ export function LobbyView({
|
||||
)}
|
||||
{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
|
||||
className={styles.joinCallButton}
|
||||
disabled={state !== GroupCallState.LocalCallFeedInitialized}
|
||||
|
@ -53,15 +53,28 @@ limitations under the License.
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
background-color: var(--bgColor3);
|
||||
margin: 40px 20px 20px 20px;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.preview video {
|
||||
width: 100%;
|
||||
width: calc(100% + 1px);
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
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 {
|
||||
@ -108,3 +121,9 @@ limitations under the License.
|
||||
.previewButtons > :last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
.preview {
|
||||
margin-top: 40px;
|
||||
}
|
||||
}
|
||||
|
@ -38,15 +38,11 @@ export function OverflowMenu({
|
||||
return (
|
||||
<>
|
||||
<PopoverMenuTrigger disableOnState>
|
||||
<TooltipTrigger>
|
||||
<TooltipTrigger placement="top">
|
||||
<Button variant="toolbar">
|
||||
<OverflowIcon />
|
||||
</Button>
|
||||
{(props) => (
|
||||
<Tooltip position="top" {...props}>
|
||||
More
|
||||
</Tooltip>
|
||||
)}
|
||||
{() => "More"}
|
||||
</TooltipTrigger>
|
||||
{(props) => (
|
||||
<Menu {...props} label="More menu" onAction={onAction}>
|
||||
|
@ -49,7 +49,7 @@ export function RoomAuthView() {
|
||||
<HeaderLogo />
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
<UserMenuContainer disableLogout />
|
||||
<UserMenuContainer preventNavigation />
|
||||
</RightNav>
|
||||
</Header>
|
||||
<div className={styles.container}>
|
||||
@ -60,15 +60,15 @@ export function RoomAuthView() {
|
||||
<InputField
|
||||
id="userName"
|
||||
name="userName"
|
||||
label="Your name"
|
||||
placeholder="Your name"
|
||||
label="Username"
|
||||
placeholder="Username"
|
||||
type="text"
|
||||
required
|
||||
autoComplete="off"
|
||||
/>
|
||||
</FieldRow>
|
||||
<Caption>
|
||||
By clicking "Go", you agree to our{" "}
|
||||
By clicking "Join call now", you agree to our{" "}
|
||||
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
|
||||
</Caption>
|
||||
{error && (
|
||||
|
@ -1,14 +1,17 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { Modal } from "../Modal";
|
||||
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 VideoIcon } from "../icons/Video.svg";
|
||||
import { ReactComponent as DeveloperIcon } from "../icons/Developer.svg";
|
||||
import { SelectInput } from "../input/SelectInput";
|
||||
import { Item } from "@react-stately/collections";
|
||||
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({
|
||||
client,
|
||||
@ -25,6 +28,11 @@ export function SettingsModal({
|
||||
setVideoInput,
|
||||
} = useMediaHandler(client);
|
||||
|
||||
const [description, setDescription] = useState("");
|
||||
|
||||
const { submitRageshake, sending, sent, error, downloadDebugLog } =
|
||||
useSubmitRageshake();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Settings"
|
||||
@ -88,6 +96,34 @@ export function SettingsModal({
|
||||
onChange={(e) => setShowInspector(e.target.checked)}
|
||||
/>
|
||||
</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>
|
||||
</TabContainer>
|
||||
</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 {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tabList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin: 0 auto 24px auto;
|
||||
}
|
||||
|
||||
.tab {
|
||||
@ -20,14 +19,14 @@
|
||||
background-color: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
padding: 0 8px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tab > * {
|
||||
color: var(--textColor4);
|
||||
margin-right: 16px;
|
||||
margin: 0 8px 0 0;
|
||||
}
|
||||
|
||||
.tab svg * {
|
||||
@ -57,6 +56,31 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding: 0 40px;
|
||||
padding: 0;
|
||||
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 * {
|
||||
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 svgrPlugin from "vite-plugin-svgr";
|
||||
import { createHtmlPlugin } from "vite-plugin-html";
|
||||
import path from "path";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
@ -23,17 +24,37 @@ export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd());
|
||||
|
||||
return {
|
||||
plugins: [svgrPlugin()],
|
||||
plugins: [
|
||||
svgrPlugin(),
|
||||
createHtmlPlugin({
|
||||
inject: {
|
||||
data: {
|
||||
title: env.VITE_PRODUCT_NAME || "Matrix Video Chat",
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
server: {
|
||||
proxy: {
|
||||
"/_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: {
|
||||
alias: {
|
||||
"$(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…
Reference in New Issue
Block a user