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.
 | 
			
		||||
# 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).
 | 
			
		||||
							
								
								
									
										31
									
								
								index.html
									
										
									
									
									
								
							
							
						
						
									
										31
									
								
								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>
 | 
			
		||||
 | 
			
		||||
<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…
	
	Add table
		Add a link
		
	
		Reference in a new issue