Merge branch 'main' into matroska
This commit is contained in:
		
				commit
				
					
						7fab4ca1ba
					
				
			
		
					 34 changed files with 4059 additions and 3874 deletions
				
			
		
							
								
								
									
										67
									
								
								.github/ISSUE_TEMPLATE/bug.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								.github/ISSUE_TEMPLATE/bug.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,67 @@
 | 
				
			||||||
 | 
					name: Bug report
 | 
				
			||||||
 | 
					description: Create a report to help us improve
 | 
				
			||||||
 | 
					labels: [T-Defect]
 | 
				
			||||||
 | 
					body:
 | 
				
			||||||
 | 
					  - type: markdown
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      value: |
 | 
				
			||||||
 | 
					        Thanks for taking the time to fill out this bug report!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Please report security issues by email to security@matrix.org
 | 
				
			||||||
 | 
					  - type: textarea
 | 
				
			||||||
 | 
					    id: reproduction-steps
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: Steps to reproduce
 | 
				
			||||||
 | 
					      description: Please attach screenshots, videos or logs if you can.
 | 
				
			||||||
 | 
					      placeholder: Tell us what you see!
 | 
				
			||||||
 | 
					      value: |
 | 
				
			||||||
 | 
					        1. Where are you starting? What can you see?
 | 
				
			||||||
 | 
					        2. What do you click?
 | 
				
			||||||
 | 
					        3. More steps…
 | 
				
			||||||
 | 
					    validations:
 | 
				
			||||||
 | 
					      required: true
 | 
				
			||||||
 | 
					  - type: textarea
 | 
				
			||||||
 | 
					    id: result
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: Outcome
 | 
				
			||||||
 | 
					      placeholder: Tell us what went wrong
 | 
				
			||||||
 | 
					      value: |
 | 
				
			||||||
 | 
					        #### What did you expect?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        #### What happened instead?
 | 
				
			||||||
 | 
					    validations:
 | 
				
			||||||
 | 
					      required: true
 | 
				
			||||||
 | 
					  - type: input
 | 
				
			||||||
 | 
					    id: os
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: Operating system
 | 
				
			||||||
 | 
					      placeholder: Windows, macOS, Ubuntu, Android…
 | 
				
			||||||
 | 
					    validations:
 | 
				
			||||||
 | 
					      required: false
 | 
				
			||||||
 | 
					  - type: input
 | 
				
			||||||
 | 
					    id: browser
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: Browser information
 | 
				
			||||||
 | 
					      description: Which browser are you using? Which version? 
 | 
				
			||||||
 | 
					      placeholder: e.g. Chromium Version 92.0.4515.131
 | 
				
			||||||
 | 
					    validations:
 | 
				
			||||||
 | 
					      required: false
 | 
				
			||||||
 | 
					  - type: input
 | 
				
			||||||
 | 
					    id: webapp-url
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: URL for webapp
 | 
				
			||||||
 | 
					      description: Which URL are you using to access the webapp? If a private server, tell us what version of Element Call you are using.
 | 
				
			||||||
 | 
					      placeholder: e.g. call.element.io
 | 
				
			||||||
 | 
					    validations:
 | 
				
			||||||
 | 
					      required: false
 | 
				
			||||||
 | 
					  - type: dropdown
 | 
				
			||||||
 | 
					    id: rageshake
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: Will you send logs?
 | 
				
			||||||
 | 
					      description:  |
 | 
				
			||||||
 | 
					        To send them, press the 'Submit Feedback' button and check 'Include Debug Logs'. Please link to this issue in the description field.
 | 
				
			||||||
 | 
					      options:
 | 
				
			||||||
 | 
					        - 'Yes'
 | 
				
			||||||
 | 
					        - 'No'
 | 
				
			||||||
 | 
					    validations:
 | 
				
			||||||
 | 
					      required: true
 | 
				
			||||||
							
								
								
									
										5
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,5 @@
 | 
				
			||||||
 | 
					blank_issues_enabled: false
 | 
				
			||||||
 | 
					contact_links:
 | 
				
			||||||
 | 
					  - name: Questions & support
 | 
				
			||||||
 | 
					    url: https://matrix.to/#/#webrtc:matrix.org
 | 
				
			||||||
 | 
					    about: Please ask and answer questions here.
 | 
				
			||||||
							
								
								
									
										36
									
								
								.github/ISSUE_TEMPLATE/enhancement.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								.github/ISSUE_TEMPLATE/enhancement.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,36 @@
 | 
				
			||||||
 | 
					name: Enhancement request
 | 
				
			||||||
 | 
					description: Do you have a suggestion or feature request?
 | 
				
			||||||
 | 
					labels: [T-Enhancement]
 | 
				
			||||||
 | 
					body:
 | 
				
			||||||
 | 
					  - type: markdown
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      value: |
 | 
				
			||||||
 | 
					        Thank you for taking the time to propose a new feature or make a suggestion.
 | 
				
			||||||
 | 
					  - type: textarea
 | 
				
			||||||
 | 
					    id: usecase
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: Your use case
 | 
				
			||||||
 | 
					      description: What would you like to be able to do? Please feel welcome to include screenshots or mock ups.
 | 
				
			||||||
 | 
					      placeholder: Tell us what you would like to do!
 | 
				
			||||||
 | 
					      value: |
 | 
				
			||||||
 | 
					        #### What would you like to do?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        #### Why would you like to do it?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        #### How would you like to achieve it?
 | 
				
			||||||
 | 
					    validations:
 | 
				
			||||||
 | 
					      required: true
 | 
				
			||||||
 | 
					  - type: textarea
 | 
				
			||||||
 | 
					    id: alternative
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: Have you considered any alternatives?
 | 
				
			||||||
 | 
					      placeholder: A clear and concise description of any alternative solutions or features you've considered.
 | 
				
			||||||
 | 
					    validations:
 | 
				
			||||||
 | 
					      required: false
 | 
				
			||||||
 | 
					  - type: textarea
 | 
				
			||||||
 | 
					    id: additional-context
 | 
				
			||||||
 | 
					    attributes:
 | 
				
			||||||
 | 
					      label: Additional context
 | 
				
			||||||
 | 
					      placeholder: Is there anything else you'd like to add?
 | 
				
			||||||
 | 
					    validations:
 | 
				
			||||||
 | 
					      required: false
 | 
				
			||||||
| 
						 | 
					@ -38,12 +38,12 @@
 | 
				
			||||||
    "classnames": "^2.3.1",
 | 
					    "classnames": "^2.3.1",
 | 
				
			||||||
    "color-hash": "^2.0.1",
 | 
					    "color-hash": "^2.0.1",
 | 
				
			||||||
    "events": "^3.3.0",
 | 
					    "events": "^3.3.0",
 | 
				
			||||||
    "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#ebcb26f1b3b9e2d709615fde03f9ce6ac77871f1",
 | 
					    "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#984dd26a138411ef73903ff4e635f2752e0829f2",
 | 
				
			||||||
    "matrix-widget-api": "^0.1.0-beta.18",
 | 
					    "matrix-widget-api": "^0.1.0-beta.18",
 | 
				
			||||||
    "mermaid": "^8.13.8",
 | 
					    "mermaid": "^8.13.8",
 | 
				
			||||||
    "normalize.css": "^8.0.1",
 | 
					    "normalize.css": "^8.0.1",
 | 
				
			||||||
    "pako": "^2.0.4",
 | 
					    "pako": "^2.0.4",
 | 
				
			||||||
    "postcss-preset-env": "^6.7.0",
 | 
					    "postcss-preset-env": "^7",
 | 
				
			||||||
    "re-resizable": "^6.9.0",
 | 
					    "re-resizable": "^6.9.0",
 | 
				
			||||||
    "react": "^17.0.0",
 | 
					    "react": "^17.0.0",
 | 
				
			||||||
    "react-dom": "^17.0.0",
 | 
					    "react-dom": "^17.0.0",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -72,7 +72,11 @@ type ClientProviderState = Omit<
 | 
				
			||||||
  "changePassword" | "logout" | "setClient"
 | 
					  "changePassword" | "logout" | "setClient"
 | 
				
			||||||
> & { error?: Error };
 | 
					> & { error?: Error };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ClientProvider: FC = ({ children }) => {
 | 
					interface Props {
 | 
				
			||||||
 | 
					  children: JSX.Element;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ClientProvider: FC<Props> = ({ children }) => {
 | 
				
			||||||
  const history = useHistory();
 | 
					  const history = useHistory();
 | 
				
			||||||
  const [
 | 
					  const [
 | 
				
			||||||
    { loading, isAuthenticated, isPasswordlessUser, client, userName, error },
 | 
					    { loading, isAuthenticated, isPasswordlessUser, client, userName, error },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,7 +24,11 @@ export function Facepile({
 | 
				
			||||||
    <div
 | 
					    <div
 | 
				
			||||||
      className={classNames(styles.facepile, styles[size], className)}
 | 
					      className={classNames(styles.facepile, styles[size], className)}
 | 
				
			||||||
      title={participants.map((member) => member.name).join(", ")}
 | 
					      title={participants.map((member) => member.name).join(", ")}
 | 
				
			||||||
      style={{ width: participants.length * (_size - _overlap) + _overlap }}
 | 
					      style={{
 | 
				
			||||||
 | 
					        width:
 | 
				
			||||||
 | 
					          Math.min(participants.length, max + 1) * (_size - _overlap) +
 | 
				
			||||||
 | 
					          _overlap,
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
      {...rest}
 | 
					      {...rest}
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      {participants.slice(0, max).map((member, i) => {
 | 
					      {participants.slice(0, max).map((member, i) => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -77,9 +77,23 @@ export function RoomHeaderInfo({ roomName, avatarUrl }) {
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function RoomSetupHeaderInfo({ roomName, avatarUrl, ...rest }) {
 | 
					export function RoomSetupHeaderInfo({
 | 
				
			||||||
 | 
					  roomName,
 | 
				
			||||||
 | 
					  avatarUrl,
 | 
				
			||||||
 | 
					  isEmbedded,
 | 
				
			||||||
 | 
					  ...rest
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
  const ref = useRef();
 | 
					  const ref = useRef();
 | 
				
			||||||
  const { buttonProps } = useButton(rest, ref);
 | 
					  const { buttonProps } = useButton(rest, ref);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (isEmbedded) {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div ref={ref}>
 | 
				
			||||||
 | 
					        <RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <button className={styles.backButton} ref={ref} {...buttonProps}>
 | 
					    <button className={styles.backButton} ref={ref} {...buttonProps}>
 | 
				
			||||||
      <ArrowLeftIcon width={16} height={16} />
 | 
					      <ArrowLeftIcon width={16} height={16} />
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,9 +13,12 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
See the License for the specific language governing permissions and
 | 
					See the License for the specific language governing permissions and
 | 
				
			||||||
limitations under the License.
 | 
					limitations under the License.
 | 
				
			||||||
*/
 | 
					*/
 | 
				
			||||||
 | 
					 | 
				
			||||||
import React, { forwardRef } from "react";
 | 
					import React, { forwardRef } from "react";
 | 
				
			||||||
 | 
					import { PressEvent } from "@react-types/shared";
 | 
				
			||||||
import classNames from "classnames";
 | 
					import classNames from "classnames";
 | 
				
			||||||
 | 
					import { useButton } from "@react-aria/button";
 | 
				
			||||||
 | 
					import { mergeProps, useObjectRef } from "@react-aria/utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import styles from "./Button.module.css";
 | 
					import styles from "./Button.module.css";
 | 
				
			||||||
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
 | 
					import { ReactComponent as MicIcon } from "../icons/Mic.svg";
 | 
				
			||||||
import { ReactComponent as MuteMicIcon } from "../icons/MuteMic.svg";
 | 
					import { ReactComponent as MuteMicIcon } from "../icons/MuteMic.svg";
 | 
				
			||||||
| 
						 | 
					@ -26,10 +29,21 @@ import { ReactComponent as ScreenshareIcon } from "../icons/Screenshare.svg";
 | 
				
			||||||
import { ReactComponent as SettingsIcon } from "../icons/Settings.svg";
 | 
					import { ReactComponent as SettingsIcon } from "../icons/Settings.svg";
 | 
				
			||||||
import { ReactComponent as AddUserIcon } from "../icons/AddUser.svg";
 | 
					import { ReactComponent as AddUserIcon } from "../icons/AddUser.svg";
 | 
				
			||||||
import { ReactComponent as ArrowDownIcon } from "../icons/ArrowDown.svg";
 | 
					import { ReactComponent as ArrowDownIcon } from "../icons/ArrowDown.svg";
 | 
				
			||||||
import { useButton } from "@react-aria/button";
 | 
					 | 
				
			||||||
import { mergeProps, useObjectRef } from "@react-aria/utils";
 | 
					 | 
				
			||||||
import { TooltipTrigger } from "../Tooltip";
 | 
					import { TooltipTrigger } from "../Tooltip";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ButtonVariant =
 | 
				
			||||||
 | 
					  | "default"
 | 
				
			||||||
 | 
					  | "toolbar"
 | 
				
			||||||
 | 
					  | "toolbarSecondary"
 | 
				
			||||||
 | 
					  | "icon"
 | 
				
			||||||
 | 
					  | "secondary"
 | 
				
			||||||
 | 
					  | "copy"
 | 
				
			||||||
 | 
					  | "secondaryCopy"
 | 
				
			||||||
 | 
					  | "iconCopy"
 | 
				
			||||||
 | 
					  | "secondaryHangup"
 | 
				
			||||||
 | 
					  | "dropdown"
 | 
				
			||||||
 | 
					  | "link";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const variantToClassName = {
 | 
					export const variantToClassName = {
 | 
				
			||||||
  default: [styles.button],
 | 
					  default: [styles.button],
 | 
				
			||||||
  toolbar: [styles.toolbarButton],
 | 
					  toolbar: [styles.toolbarButton],
 | 
				
			||||||
| 
						 | 
					@ -44,11 +58,24 @@ export const variantToClassName = {
 | 
				
			||||||
  link: [styles.linkButton],
 | 
					  link: [styles.linkButton],
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const sizeToClassName = {
 | 
					export type ButtonSize = "lg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const sizeToClassName: { lg: string[] } = {
 | 
				
			||||||
  lg: [styles.lg],
 | 
					  lg: [styles.lg],
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					interface Props {
 | 
				
			||||||
export const Button = forwardRef(
 | 
					  variant: ButtonVariant;
 | 
				
			||||||
 | 
					  size: ButtonSize;
 | 
				
			||||||
 | 
					  on: () => void;
 | 
				
			||||||
 | 
					  off: () => void;
 | 
				
			||||||
 | 
					  iconStyle: string;
 | 
				
			||||||
 | 
					  className: string;
 | 
				
			||||||
 | 
					  children: Element[];
 | 
				
			||||||
 | 
					  onPress: (e: PressEvent) => void;
 | 
				
			||||||
 | 
					  onPressStart: (e: PressEvent) => void;
 | 
				
			||||||
 | 
					  [index: string]: unknown;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export const Button = forwardRef<HTMLButtonElement, Props>(
 | 
				
			||||||
  (
 | 
					  (
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      variant = "default",
 | 
					      variant = "default",
 | 
				
			||||||
| 
						 | 
					@ -64,7 +91,7 @@ export const Button = forwardRef(
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    ref
 | 
					    ref
 | 
				
			||||||
  ) => {
 | 
					  ) => {
 | 
				
			||||||
    const buttonRef = useObjectRef(ref);
 | 
					    const buttonRef = useObjectRef<HTMLButtonElement>(ref);
 | 
				
			||||||
    const { buttonProps } = useButton(
 | 
					    const { buttonProps } = useButton(
 | 
				
			||||||
      { onPress, onPressStart, ...rest },
 | 
					      { onPress, onPressStart, ...rest },
 | 
				
			||||||
      buttonRef
 | 
					      buttonRef
 | 
				
			||||||
| 
						 | 
					@ -75,7 +102,7 @@ export const Button = forwardRef(
 | 
				
			||||||
    let filteredButtonProps = buttonProps;
 | 
					    let filteredButtonProps = buttonProps;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (rest.type === "submit" && !rest.onPress) {
 | 
					    if (rest.type === "submit" && !rest.onPress) {
 | 
				
			||||||
      const { onKeyDown, onKeyUp, ...filtered } = buttonProps;
 | 
					      const { ...filtered } = buttonProps;
 | 
				
			||||||
      filteredButtonProps = filtered;
 | 
					      filteredButtonProps = filtered;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -94,14 +121,22 @@ export const Button = forwardRef(
 | 
				
			||||||
        {...mergeProps(rest, filteredButtonProps)}
 | 
					        {...mergeProps(rest, filteredButtonProps)}
 | 
				
			||||||
        ref={buttonRef}
 | 
					        ref={buttonRef}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
 | 
					        <>
 | 
				
			||||||
          {children}
 | 
					          {children}
 | 
				
			||||||
          {variant === "dropdown" && <ArrowDownIcon />}
 | 
					          {variant === "dropdown" && <ArrowDownIcon />}
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
      </button>
 | 
					      </button>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function MicButton({ muted, ...rest }) {
 | 
					export function MicButton({
 | 
				
			||||||
 | 
					  muted,
 | 
				
			||||||
 | 
					  ...rest
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					  muted: boolean;
 | 
				
			||||||
 | 
					  [index: string]: unknown;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <TooltipTrigger>
 | 
					    <TooltipTrigger>
 | 
				
			||||||
      <Button variant="toolbar" {...rest} off={muted}>
 | 
					      <Button variant="toolbar" {...rest} off={muted}>
 | 
				
			||||||
| 
						 | 
					@ -112,7 +147,13 @@ export function MicButton({ muted, ...rest }) {
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function VideoButton({ muted, ...rest }) {
 | 
					export function VideoButton({
 | 
				
			||||||
 | 
					  muted,
 | 
				
			||||||
 | 
					  ...rest
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					  muted: boolean;
 | 
				
			||||||
 | 
					  [index: string]: unknown;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <TooltipTrigger>
 | 
					    <TooltipTrigger>
 | 
				
			||||||
      <Button variant="toolbar" {...rest} off={muted}>
 | 
					      <Button variant="toolbar" {...rest} off={muted}>
 | 
				
			||||||
| 
						 | 
					@ -123,7 +164,15 @@ export function VideoButton({ muted, ...rest }) {
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function ScreenshareButton({ enabled, className, ...rest }) {
 | 
					export function ScreenshareButton({
 | 
				
			||||||
 | 
					  enabled,
 | 
				
			||||||
 | 
					  className,
 | 
				
			||||||
 | 
					  ...rest
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					  enabled: boolean;
 | 
				
			||||||
 | 
					  className?: string;
 | 
				
			||||||
 | 
					  [index: string]: unknown;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <TooltipTrigger>
 | 
					    <TooltipTrigger>
 | 
				
			||||||
      <Button variant="toolbarSecondary" {...rest} on={enabled}>
 | 
					      <Button variant="toolbarSecondary" {...rest} on={enabled}>
 | 
				
			||||||
| 
						 | 
					@ -134,7 +183,13 @@ export function ScreenshareButton({ enabled, className, ...rest }) {
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function HangupButton({ className, ...rest }) {
 | 
					export function HangupButton({
 | 
				
			||||||
 | 
					  className,
 | 
				
			||||||
 | 
					  ...rest
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					  className?: string;
 | 
				
			||||||
 | 
					  [index: string]: unknown;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <TooltipTrigger>
 | 
					    <TooltipTrigger>
 | 
				
			||||||
      <Button
 | 
					      <Button
 | 
				
			||||||
| 
						 | 
					@ -149,7 +204,13 @@ export function HangupButton({ className, ...rest }) {
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function SettingsButton({ className, ...rest }) {
 | 
					export function SettingsButton({
 | 
				
			||||||
 | 
					  className,
 | 
				
			||||||
 | 
					  ...rest
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					  className?: string;
 | 
				
			||||||
 | 
					  [index: string]: unknown;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <TooltipTrigger>
 | 
					    <TooltipTrigger>
 | 
				
			||||||
      <Button variant="toolbar" {...rest}>
 | 
					      <Button variant="toolbar" {...rest}>
 | 
				
			||||||
| 
						 | 
					@ -160,7 +221,13 @@ export function SettingsButton({ className, ...rest }) {
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function InviteButton({ className, ...rest }) {
 | 
					export function InviteButton({
 | 
				
			||||||
 | 
					  className,
 | 
				
			||||||
 | 
					  ...rest
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					  className?: string;
 | 
				
			||||||
 | 
					  [index: string]: unknown;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <TooltipTrigger>
 | 
					    <TooltipTrigger>
 | 
				
			||||||
      <Button variant="toolbar" {...rest}>
 | 
					      <Button variant="toolbar" {...rest}>
 | 
				
			||||||
| 
						 | 
					@ -16,10 +16,18 @@ limitations under the License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import React from "react";
 | 
					import React from "react";
 | 
				
			||||||
import useClipboard from "react-use-clipboard";
 | 
					import useClipboard from "react-use-clipboard";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { ReactComponent as CheckIcon } from "../icons/Check.svg";
 | 
					import { ReactComponent as CheckIcon } from "../icons/Check.svg";
 | 
				
			||||||
import { ReactComponent as CopyIcon } from "../icons/Copy.svg";
 | 
					import { ReactComponent as CopyIcon } from "../icons/Copy.svg";
 | 
				
			||||||
import { Button } from "./Button";
 | 
					import { Button, ButtonVariant } from "./Button";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Props {
 | 
				
			||||||
 | 
					  value: string;
 | 
				
			||||||
 | 
					  children: JSX.Element;
 | 
				
			||||||
 | 
					  className: string;
 | 
				
			||||||
 | 
					  variant: ButtonVariant;
 | 
				
			||||||
 | 
					  copiedMessage: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
export function CopyButton({
 | 
					export function CopyButton({
 | 
				
			||||||
  value,
 | 
					  value,
 | 
				
			||||||
  children,
 | 
					  children,
 | 
				
			||||||
| 
						 | 
					@ -27,7 +35,7 @@ export function CopyButton({
 | 
				
			||||||
  variant,
 | 
					  variant,
 | 
				
			||||||
  copiedMessage,
 | 
					  copiedMessage,
 | 
				
			||||||
  ...rest
 | 
					  ...rest
 | 
				
			||||||
}) {
 | 
					}: Props) {
 | 
				
			||||||
  const [isCopied, setCopied] = useClipboard(value, { successDuration: 3000 });
 | 
					  const [isCopied, setCopied] = useClipboard(value, { successDuration: 3000 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
| 
						 | 
					@ -17,9 +17,28 @@ limitations under the License.
 | 
				
			||||||
import React from "react";
 | 
					import React from "react";
 | 
				
			||||||
import { Link } from "react-router-dom";
 | 
					import { Link } from "react-router-dom";
 | 
				
			||||||
import classNames from "classnames";
 | 
					import classNames from "classnames";
 | 
				
			||||||
import { variantToClassName, sizeToClassName } from "./Button";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function LinkButton({ className, variant, size, children, ...rest }) {
 | 
					import {
 | 
				
			||||||
 | 
					  variantToClassName,
 | 
				
			||||||
 | 
					  sizeToClassName,
 | 
				
			||||||
 | 
					  ButtonVariant,
 | 
				
			||||||
 | 
					  ButtonSize,
 | 
				
			||||||
 | 
					} from "./Button";
 | 
				
			||||||
 | 
					interface Props {
 | 
				
			||||||
 | 
					  className: string;
 | 
				
			||||||
 | 
					  variant: ButtonVariant;
 | 
				
			||||||
 | 
					  size: ButtonSize;
 | 
				
			||||||
 | 
					  children: JSX.Element;
 | 
				
			||||||
 | 
					  [index: string]: unknown;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function LinkButton({
 | 
				
			||||||
 | 
					  className,
 | 
				
			||||||
 | 
					  variant,
 | 
				
			||||||
 | 
					  size,
 | 
				
			||||||
 | 
					  children,
 | 
				
			||||||
 | 
					  ...rest
 | 
				
			||||||
 | 
					}: Props) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Link
 | 
					    <Link
 | 
				
			||||||
      className={classNames(
 | 
					      className={classNames(
 | 
				
			||||||
| 
						 | 
					@ -36,6 +36,14 @@ initRageshake();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
console.info(`matrix-video-chat ${import.meta.env.VITE_APP_VERSION || "dev"}`);
 | 
					console.info(`matrix-video-chat ${import.meta.env.VITE_APP_VERSION || "dev"}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if (!window.isSecureContext) {
 | 
				
			||||||
 | 
					  throw new Error(
 | 
				
			||||||
 | 
					    "This app cannot run in an insecure context. To fix this, access the app " +
 | 
				
			||||||
 | 
					      "via a local loopback address, or serve it over HTTPS.\n" +
 | 
				
			||||||
 | 
					      "https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts"
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if (import.meta.env.VITE_CUSTOM_THEME) {
 | 
					if (import.meta.env.VITE_CUSTOM_THEME) {
 | 
				
			||||||
  const style = document.documentElement.style;
 | 
					  const style = document.documentElement.style;
 | 
				
			||||||
  style.setProperty("--accent", import.meta.env.VITE_THEME_ACCENT as string);
 | 
					  style.setProperty("--accent", import.meta.env.VITE_THEME_ACCENT as string);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,6 +16,7 @@ import {
 | 
				
			||||||
} from "matrix-js-sdk/src/webrtc/groupCall";
 | 
					} from "matrix-js-sdk/src/webrtc/groupCall";
 | 
				
			||||||
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
 | 
					import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
 | 
				
			||||||
import { WidgetApi } from "matrix-widget-api";
 | 
					import { WidgetApi } from "matrix-widget-api";
 | 
				
			||||||
 | 
					import { logger } from "matrix-js-sdk/src/logger";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import IndexedDBWorker from "./IndexedDBWorker?worker";
 | 
					import IndexedDBWorker from "./IndexedDBWorker?worker";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -138,6 +139,19 @@ export async function initClient(
 | 
				
			||||||
    storeOpts.cryptoStore = new MemoryCryptoStore();
 | 
					    storeOpts.cryptoStore = new MemoryCryptoStore();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // XXX: we read from the URL search params in RoomPage too:
 | 
				
			||||||
 | 
					  // it would be much better to read them in one place and pass
 | 
				
			||||||
 | 
					  // the values around, but we initialise the matrix client in
 | 
				
			||||||
 | 
					  // many different places so we'd have to pass it into all of
 | 
				
			||||||
 | 
					  // them.
 | 
				
			||||||
 | 
					  const params = new URLSearchParams(window.location.search);
 | 
				
			||||||
 | 
					  // disable e2e only if enableE2e=false is given
 | 
				
			||||||
 | 
					  const enableE2e = params.get("enableE2e") !== "false";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!enableE2e) {
 | 
				
			||||||
 | 
					    logger.info("Disabling E2E: group call signalling will NOT be encrypted.");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const client = createClient({
 | 
					  const client = createClient({
 | 
				
			||||||
    ...storeOpts,
 | 
					    ...storeOpts,
 | 
				
			||||||
    ...clientOptions,
 | 
					    ...clientOptions,
 | 
				
			||||||
| 
						 | 
					@ -145,6 +159,7 @@ export async function initClient(
 | 
				
			||||||
    // Use a relatively low timeout for API calls: this is a realtime app
 | 
					    // Use a relatively low timeout for API calls: this is a realtime app
 | 
				
			||||||
    // so we don't want API calls taking ages, we'd rather they just fail.
 | 
					    // so we don't want API calls taking ages, we'd rather they just fail.
 | 
				
			||||||
    localTimeoutMs: 5000,
 | 
					    localTimeoutMs: 5000,
 | 
				
			||||||
 | 
					    useE2eForGroupCall: enableE2e,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,6 +15,8 @@ limitations under the License.
 | 
				
			||||||
*/
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import React, { useCallback, useEffect, useState } from "react";
 | 
					import React, { useCallback, useEffect, useState } from "react";
 | 
				
			||||||
 | 
					import { MatrixClient } from "matrix-js-sdk";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { Button } from "../button";
 | 
					import { Button } from "../button";
 | 
				
			||||||
import { useProfile } from "./useProfile";
 | 
					import { useProfile } from "./useProfile";
 | 
				
			||||||
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
 | 
					import { FieldRow, InputField, ErrorMessage } from "../input/Input";
 | 
				
			||||||
| 
						 | 
					@ -22,7 +24,12 @@ import { Modal, ModalContent } from "../Modal";
 | 
				
			||||||
import { AvatarInputField } from "../input/AvatarInputField";
 | 
					import { AvatarInputField } from "../input/AvatarInputField";
 | 
				
			||||||
import styles from "./ProfileModal.module.css";
 | 
					import styles from "./ProfileModal.module.css";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function ProfileModal({ client, ...rest }) {
 | 
					interface Props {
 | 
				
			||||||
 | 
					  client: MatrixClient;
 | 
				
			||||||
 | 
					  onClose: () => {};
 | 
				
			||||||
 | 
					  [rest: string]: unknown;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export function ProfileModal({ client, ...rest }: Props) {
 | 
				
			||||||
  const { onClose } = rest;
 | 
					  const { onClose } = rest;
 | 
				
			||||||
  const {
 | 
					  const {
 | 
				
			||||||
    success,
 | 
					    success,
 | 
				
			||||||
| 
						 | 
					@ -50,13 +57,20 @@ export function ProfileModal({ client, ...rest }) {
 | 
				
			||||||
    (e) => {
 | 
					    (e) => {
 | 
				
			||||||
      e.preventDefault();
 | 
					      e.preventDefault();
 | 
				
			||||||
      const data = new FormData(e.target);
 | 
					      const data = new FormData(e.target);
 | 
				
			||||||
      const displayName = data.get("displayName");
 | 
					      const displayNameDataEntry = data.get("displayName");
 | 
				
			||||||
      const avatar = data.get("avatar");
 | 
					      const avatar: File | string = data.get("avatar");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const avatarSize =
 | 
				
			||||||
 | 
					        typeof avatar == "string" ? avatar.length : avatar.size;
 | 
				
			||||||
 | 
					      const displayName =
 | 
				
			||||||
 | 
					        typeof displayNameDataEntry == "string"
 | 
				
			||||||
 | 
					          ? displayNameDataEntry
 | 
				
			||||||
 | 
					          : displayNameDataEntry.name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      saveProfile({
 | 
					      saveProfile({
 | 
				
			||||||
        displayName,
 | 
					        displayName,
 | 
				
			||||||
        avatar: avatar && avatar.size > 0 ? avatar : undefined,
 | 
					        avatar: avatar && avatarSize > 0 ? avatar : undefined,
 | 
				
			||||||
        removeAvatar: removeAvatar && (!avatar || avatar.size === 0),
 | 
					        removeAvatar: removeAvatar && (!avatar || avatarSize === 0),
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    [saveProfile, removeAvatar]
 | 
					    [saveProfile, removeAvatar]
 | 
				
			||||||
| 
						 | 
					@ -14,11 +14,33 @@ See the License for the specific language governing permissions and
 | 
				
			||||||
limitations under the License.
 | 
					limitations under the License.
 | 
				
			||||||
*/
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { MatrixClient } from "matrix-js-sdk/src/client";
 | 
				
			||||||
 | 
					import { MatrixEvent } from "matrix-js-sdk/src/models/event";
 | 
				
			||||||
 | 
					import { User, UserEvent } from "matrix-js-sdk/src/models/user";
 | 
				
			||||||
 | 
					import { FileType } from "matrix-js-sdk/src/http-api";
 | 
				
			||||||
import { useState, useCallback, useEffect } from "react";
 | 
					import { useState, useCallback, useEffect } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function useProfile(client) {
 | 
					interface ProfileLoadState {
 | 
				
			||||||
 | 
					  success?: boolean;
 | 
				
			||||||
 | 
					  loading?: boolean;
 | 
				
			||||||
 | 
					  displayName: string;
 | 
				
			||||||
 | 
					  avatarUrl: string;
 | 
				
			||||||
 | 
					  error?: Error;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ProfileSaveCallback = ({
 | 
				
			||||||
 | 
					  displayName,
 | 
				
			||||||
 | 
					  avatar,
 | 
				
			||||||
 | 
					  removeAvatar,
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					  displayName: string;
 | 
				
			||||||
 | 
					  avatar: FileType;
 | 
				
			||||||
 | 
					  removeAvatar: boolean;
 | 
				
			||||||
 | 
					}) => Promise<void>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function useProfile(client: MatrixClient) {
 | 
				
			||||||
  const [{ loading, displayName, avatarUrl, error, success }, setState] =
 | 
					  const [{ loading, displayName, avatarUrl, error, success }, setState] =
 | 
				
			||||||
    useState(() => {
 | 
					    useState<ProfileLoadState>(() => {
 | 
				
			||||||
      const user = client?.getUser(client.getUserId());
 | 
					      const user = client?.getUser(client.getUserId());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return {
 | 
					      return {
 | 
				
			||||||
| 
						 | 
					@ -31,7 +53,10 @@ export function useProfile(client) {
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    const onChangeUser = (_event, { displayName, avatarUrl }) => {
 | 
					    const onChangeUser = (
 | 
				
			||||||
 | 
					      _event: MatrixEvent,
 | 
				
			||||||
 | 
					      { displayName, avatarUrl }: User
 | 
				
			||||||
 | 
					    ) => {
 | 
				
			||||||
      setState({
 | 
					      setState({
 | 
				
			||||||
        success: false,
 | 
					        success: false,
 | 
				
			||||||
        loading: false,
 | 
					        loading: false,
 | 
				
			||||||
| 
						 | 
					@ -41,24 +66,24 @@ export function useProfile(client) {
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let user;
 | 
					    let user: User;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (client) {
 | 
					    if (client) {
 | 
				
			||||||
      const userId = client.getUserId();
 | 
					      const userId = client.getUserId();
 | 
				
			||||||
      user = client.getUser(userId);
 | 
					      user = client.getUser(userId);
 | 
				
			||||||
      user.on("User.displayName", onChangeUser);
 | 
					      user.on(UserEvent.DisplayName, onChangeUser);
 | 
				
			||||||
      user.on("User.avatarUrl", onChangeUser);
 | 
					      user.on(UserEvent.AvatarUrl, onChangeUser);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return () => {
 | 
					    return () => {
 | 
				
			||||||
      if (user) {
 | 
					      if (user) {
 | 
				
			||||||
        user.removeListener("User.displayName", onChangeUser);
 | 
					        user.removeListener(UserEvent.DisplayName, onChangeUser);
 | 
				
			||||||
        user.removeListener("User.avatarUrl", onChangeUser);
 | 
					        user.removeListener(UserEvent.AvatarUrl, onChangeUser);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }, [client]);
 | 
					  }, [client]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const saveProfile = useCallback(
 | 
					  const saveProfile = useCallback<ProfileSaveCallback>(
 | 
				
			||||||
    async ({ displayName, avatar, removeAvatar }) => {
 | 
					    async ({ displayName, avatar, removeAvatar }) => {
 | 
				
			||||||
      if (client) {
 | 
					      if (client) {
 | 
				
			||||||
        setState((prev) => ({
 | 
					        setState((prev) => ({
 | 
				
			||||||
| 
						 | 
					@ -71,7 +96,7 @@ export function useProfile(client) {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
          await client.setDisplayName(displayName);
 | 
					          await client.setDisplayName(displayName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          let mxcAvatarUrl;
 | 
					          let mxcAvatarUrl: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          if (removeAvatar) {
 | 
					          if (removeAvatar) {
 | 
				
			||||||
            await client.setAvatarUrl("");
 | 
					            await client.setAvatarUrl("");
 | 
				
			||||||
| 
						 | 
					@ -87,11 +112,11 @@ export function useProfile(client) {
 | 
				
			||||||
            loading: false,
 | 
					            loading: false,
 | 
				
			||||||
            success: true,
 | 
					            success: true,
 | 
				
			||||||
          }));
 | 
					          }));
 | 
				
			||||||
        } catch (error) {
 | 
					        } catch (error: unknown) {
 | 
				
			||||||
          setState((prev) => ({
 | 
					          setState((prev) => ({
 | 
				
			||||||
            ...prev,
 | 
					            ...prev,
 | 
				
			||||||
            loading: false,
 | 
					            loading: false,
 | 
				
			||||||
            error,
 | 
					            error: error instanceof Error ? error : Error(error as string),
 | 
				
			||||||
            success: false,
 | 
					            success: false,
 | 
				
			||||||
          }));
 | 
					          }));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					@ -102,5 +127,12 @@ export function useProfile(client) {
 | 
				
			||||||
    [client]
 | 
					    [client]
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return { loading, error, displayName, avatarUrl, saveProfile, success };
 | 
					  return {
 | 
				
			||||||
 | 
					    loading,
 | 
				
			||||||
 | 
					    error,
 | 
				
			||||||
 | 
					    displayName,
 | 
				
			||||||
 | 
					    avatarUrl,
 | 
				
			||||||
 | 
					    saveProfile,
 | 
				
			||||||
 | 
					    success,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -53,8 +53,12 @@ export function AudioPreview({
 | 
				
			||||||
              onSelectionChange={setAudioInput}
 | 
					              onSelectionChange={setAudioInput}
 | 
				
			||||||
              className={styles.inputField}
 | 
					              className={styles.inputField}
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              {audioInputs.map(({ deviceId, label }) => (
 | 
					              {audioInputs.map(({ deviceId, label }, index) => (
 | 
				
			||||||
                <Item key={deviceId}>{label}</Item>
 | 
					                <Item key={deviceId}>
 | 
				
			||||||
 | 
					                  {!!label && label.trim().length > 0
 | 
				
			||||||
 | 
					                    ? label
 | 
				
			||||||
 | 
					                    : `Microphone ${index + 1}`}
 | 
				
			||||||
 | 
					                </Item>
 | 
				
			||||||
              ))}
 | 
					              ))}
 | 
				
			||||||
            </SelectInput>
 | 
					            </SelectInput>
 | 
				
			||||||
            {audioOutputs.length > 0 && (
 | 
					            {audioOutputs.length > 0 && (
 | 
				
			||||||
| 
						 | 
					@ -64,8 +68,12 @@ export function AudioPreview({
 | 
				
			||||||
                onSelectionChange={setAudioOutput}
 | 
					                onSelectionChange={setAudioOutput}
 | 
				
			||||||
                className={styles.inputField}
 | 
					                className={styles.inputField}
 | 
				
			||||||
              >
 | 
					              >
 | 
				
			||||||
                {audioOutputs.map(({ deviceId, label }) => (
 | 
					                {audioOutputs.map(({ deviceId, label }, index) => (
 | 
				
			||||||
                  <Item key={deviceId}>{label}</Item>
 | 
					                  <Item key={deviceId}>
 | 
				
			||||||
 | 
					                    {!!label && label.trim().length > 0
 | 
				
			||||||
 | 
					                      ? label
 | 
				
			||||||
 | 
					                      : `Speaker ${index + 1}`}
 | 
				
			||||||
 | 
					                  </Item>
 | 
				
			||||||
                ))}
 | 
					                ))}
 | 
				
			||||||
              </SelectInput>
 | 
					              </SelectInput>
 | 
				
			||||||
            )}
 | 
					            )}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,12 +19,19 @@ import { useLoadGroupCall } from "./useLoadGroupCall";
 | 
				
			||||||
import { ErrorView, FullScreenView } from "../FullScreenView";
 | 
					import { ErrorView, FullScreenView } from "../FullScreenView";
 | 
				
			||||||
import { usePageTitle } from "../usePageTitle";
 | 
					import { usePageTitle } from "../usePageTitle";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function GroupCallLoader({ client, roomId, viaServers, children }) {
 | 
					export function GroupCallLoader({
 | 
				
			||||||
 | 
					  client,
 | 
				
			||||||
 | 
					  roomId,
 | 
				
			||||||
 | 
					  viaServers,
 | 
				
			||||||
 | 
					  createPtt,
 | 
				
			||||||
 | 
					  children,
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
  const { loading, error, groupCall } = useLoadGroupCall(
 | 
					  const { loading, error, groupCall } = useLoadGroupCall(
 | 
				
			||||||
    client,
 | 
					    client,
 | 
				
			||||||
    roomId,
 | 
					    roomId,
 | 
				
			||||||
    viaServers,
 | 
					    viaServers,
 | 
				
			||||||
    true
 | 
					    true,
 | 
				
			||||||
 | 
					    createPtt
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  usePageTitle(groupCall ? groupCall.room.name : "Loading...");
 | 
					  usePageTitle(groupCall ? groupCall.room.name : "Loading...");
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -30,6 +30,7 @@ import { useLocationNavigation } from "../useLocationNavigation";
 | 
				
			||||||
export function GroupCallView({
 | 
					export function GroupCallView({
 | 
				
			||||||
  client,
 | 
					  client,
 | 
				
			||||||
  isPasswordlessUser,
 | 
					  isPasswordlessUser,
 | 
				
			||||||
 | 
					  isEmbedded,
 | 
				
			||||||
  roomId,
 | 
					  roomId,
 | 
				
			||||||
  groupCall,
 | 
					  groupCall,
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
| 
						 | 
					@ -60,7 +61,10 @@ export function GroupCallView({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    window.groupCall = groupCall;
 | 
					    window.groupCall = groupCall;
 | 
				
			||||||
  }, [groupCall]);
 | 
					
 | 
				
			||||||
 | 
					    // In embedded mode, bypass the lobby and just enter the call straight away
 | 
				
			||||||
 | 
					    if (isEmbedded) groupCall.enter();
 | 
				
			||||||
 | 
					  }, [groupCall, isEmbedded]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useSentryGroupCallHandler(groupCall);
 | 
					  useSentryGroupCallHandler(groupCall);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -92,6 +96,7 @@ export function GroupCallView({
 | 
				
			||||||
          participants={participants}
 | 
					          participants={participants}
 | 
				
			||||||
          userMediaFeeds={userMediaFeeds}
 | 
					          userMediaFeeds={userMediaFeeds}
 | 
				
			||||||
          onLeave={onLeave}
 | 
					          onLeave={onLeave}
 | 
				
			||||||
 | 
					          isEmbedded={isEmbedded}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
| 
						 | 
					@ -125,6 +130,13 @@ export function GroupCallView({
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  } else if (left) {
 | 
					  } else if (left) {
 | 
				
			||||||
    return <CallEndedView client={client} />;
 | 
					    return <CallEndedView client={client} />;
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    if (isEmbedded) {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        <FullScreenView>
 | 
				
			||||||
 | 
					          <h1>Loading room...</h1>
 | 
				
			||||||
 | 
					        </FullScreenView>
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      return (
 | 
					      return (
 | 
				
			||||||
        <LobbyView
 | 
					        <LobbyView
 | 
				
			||||||
| 
						 | 
					@ -142,7 +154,9 @@ export function GroupCallView({
 | 
				
			||||||
          toggleLocalVideoMuted={toggleLocalVideoMuted}
 | 
					          toggleLocalVideoMuted={toggleLocalVideoMuted}
 | 
				
			||||||
          toggleMicrophoneMuted={toggleMicrophoneMuted}
 | 
					          toggleMicrophoneMuted={toggleMicrophoneMuted}
 | 
				
			||||||
          roomId={roomId}
 | 
					          roomId={roomId}
 | 
				
			||||||
 | 
					          isEmbedded={isEmbedded}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -44,7 +44,7 @@ import { useShowInspector } from "../settings/useSetting";
 | 
				
			||||||
import { useModalTriggerState } from "../Modal";
 | 
					import { useModalTriggerState } from "../Modal";
 | 
				
			||||||
import { useAudioContext } from "../video-grid/useMediaStream";
 | 
					import { useAudioContext } from "../video-grid/useMediaStream";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const canScreenshare = "getDisplayMedia" in navigator.mediaDevices;
 | 
					const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
 | 
				
			||||||
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
 | 
					// There is currently a bug in Safari our our code with cloning and sending MediaStreams
 | 
				
			||||||
// or with getUsermedia and getDisplaymedia being used within the same session.
 | 
					// or with getUsermedia and getDisplaymedia being used within the same session.
 | 
				
			||||||
// For now we can disable screensharing in Safari.
 | 
					// For now we can disable screensharing in Safari.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -42,6 +42,7 @@ export function LobbyView({
 | 
				
			||||||
  toggleLocalVideoMuted,
 | 
					  toggleLocalVideoMuted,
 | 
				
			||||||
  toggleMicrophoneMuted,
 | 
					  toggleMicrophoneMuted,
 | 
				
			||||||
  roomId,
 | 
					  roomId,
 | 
				
			||||||
 | 
					  isEmbedded,
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
  const { stream } = useCallFeed(localCallFeed);
 | 
					  const { stream } = useCallFeed(localCallFeed);
 | 
				
			||||||
  const {
 | 
					  const {
 | 
				
			||||||
| 
						 | 
					@ -122,11 +123,13 @@ export function LobbyView({
 | 
				
			||||||
            Copy call link and join later
 | 
					            Copy call link and join later
 | 
				
			||||||
          </CopyButton>
 | 
					          </CopyButton>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					        {!isEmbedded && (
 | 
				
			||||||
          <Body className={styles.joinRoomFooter}>
 | 
					          <Body className={styles.joinRoomFooter}>
 | 
				
			||||||
            <Link color="primary" to="/">
 | 
					            <Link color="primary" to="/">
 | 
				
			||||||
              Take me Home
 | 
					              Take me Home
 | 
				
			||||||
            </Link>
 | 
					            </Link>
 | 
				
			||||||
          </Body>
 | 
					          </Body>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,17 +1,28 @@
 | 
				
			||||||
.pttButton {
 | 
					.pttButton {
 | 
				
			||||||
  width: 100vw;
 | 
					  width: 100vw;
 | 
				
			||||||
  height: 100vh;
 | 
					  aspect-ratio: 1;
 | 
				
			||||||
  max-height: 232px;
 | 
					  max-height: min(232px, calc(100vh - 16px));
 | 
				
			||||||
  max-width: 232px;
 | 
					  max-width: min(232px, calc(100vw - 16px));
 | 
				
			||||||
  border-radius: 116px;
 | 
					  border-radius: 116px;
 | 
				
			||||||
  color: var(--primary-content);
 | 
					  color: var(--primary-content);
 | 
				
			||||||
  border: 6px solid var(--accent);
 | 
					  border: 6px solid var(--accent);
 | 
				
			||||||
  background-color: #21262c;
 | 
					  background-color: #21262c;
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
  padding: 0;
 | 
					  padding: 0;
 | 
				
			||||||
 | 
					  margin: 4px;
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.micIcon {
 | 
				
			||||||
 | 
					  max-height: 50%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.avatar {
 | 
				
			||||||
 | 
					  /* Remove explicit size to allow avatar to scale with the button */
 | 
				
			||||||
 | 
					  width: 100% !important;
 | 
				
			||||||
 | 
					  height: 100% !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.talking {
 | 
					.talking {
 | 
				
			||||||
  background-color: var(--accent);
 | 
					  background-color: var(--accent);
 | 
				
			||||||
  cursor: unset;
 | 
					  cursor: unset;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 | 
				
			||||||
limitations under the License.
 | 
					limitations under the License.
 | 
				
			||||||
*/
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import React, { useCallback, useState, createRef } from "react";
 | 
					import React, { useCallback, useState, useRef } from "react";
 | 
				
			||||||
import classNames from "classnames";
 | 
					import classNames from "classnames";
 | 
				
			||||||
import { useSpring, animated } from "@react-spring/web";
 | 
					import { useSpring, animated } from "@react-spring/web";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -54,16 +54,20 @@ export const PTTButton: React.FC<Props> = ({
 | 
				
			||||||
  enqueueNetworkWaiting,
 | 
					  enqueueNetworkWaiting,
 | 
				
			||||||
  setNetworkWaiting,
 | 
					  setNetworkWaiting,
 | 
				
			||||||
}) => {
 | 
					}) => {
 | 
				
			||||||
  const buttonRef = createRef<HTMLButtonElement>();
 | 
					  const buttonRef = useRef<HTMLButtonElement>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [activeTouchId, setActiveTouchId] = useState<number | null>(null);
 | 
					  const [activeTouchId, setActiveTouchId] = useState<number | null>(null);
 | 
				
			||||||
 | 
					  const [buttonHeld, setButtonHeld] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const hold = useCallback(() => {
 | 
					  const hold = useCallback(() => {
 | 
				
			||||||
    // This update is delayed so the user only sees it if latency is significant
 | 
					    // This update is delayed so the user only sees it if latency is significant
 | 
				
			||||||
 | 
					    if (buttonHeld) return;
 | 
				
			||||||
 | 
					    setButtonHeld(true);
 | 
				
			||||||
    enqueueNetworkWaiting(true, 100);
 | 
					    enqueueNetworkWaiting(true, 100);
 | 
				
			||||||
    startTalking();
 | 
					    startTalking();
 | 
				
			||||||
  }, [enqueueNetworkWaiting, startTalking]);
 | 
					  }, [enqueueNetworkWaiting, startTalking, buttonHeld]);
 | 
				
			||||||
  const unhold = useCallback(() => {
 | 
					  const unhold = useCallback(() => {
 | 
				
			||||||
 | 
					    setButtonHeld(false);
 | 
				
			||||||
    setNetworkWaiting(false);
 | 
					    setNetworkWaiting(false);
 | 
				
			||||||
    stopTalking();
 | 
					    stopTalking();
 | 
				
			||||||
  }, [setNetworkWaiting, stopTalking]);
 | 
					  }, [setNetworkWaiting, stopTalking]);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,6 +28,7 @@
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  flex-direction: column;
 | 
					  flex-direction: column;
 | 
				
			||||||
  margin: 20px;
 | 
					  margin: 20px;
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.participants > p {
 | 
					.participants > p {
 | 
				
			||||||
| 
						 | 
					@ -41,6 +42,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.talkingInfo {
 | 
					.talkingInfo {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-shrink: 0;
 | 
				
			||||||
  flex-direction: column;
 | 
					  flex-direction: column;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  margin-bottom: 20px;
 | 
					  margin-bottom: 20px;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -93,6 +93,7 @@ interface Props {
 | 
				
			||||||
  participants: RoomMember[];
 | 
					  participants: RoomMember[];
 | 
				
			||||||
  userMediaFeeds: CallFeed[];
 | 
					  userMediaFeeds: CallFeed[];
 | 
				
			||||||
  onLeave: () => void;
 | 
					  onLeave: () => void;
 | 
				
			||||||
 | 
					  isEmbedded: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const PTTCallView: React.FC<Props> = ({
 | 
					export const PTTCallView: React.FC<Props> = ({
 | 
				
			||||||
| 
						 | 
					@ -104,6 +105,7 @@ export const PTTCallView: React.FC<Props> = ({
 | 
				
			||||||
  participants,
 | 
					  participants,
 | 
				
			||||||
  userMediaFeeds,
 | 
					  userMediaFeeds,
 | 
				
			||||||
  onLeave,
 | 
					  onLeave,
 | 
				
			||||||
 | 
					  isEmbedded,
 | 
				
			||||||
}) => {
 | 
					}) => {
 | 
				
			||||||
  const { modalState: inviteModalState, modalProps: inviteModalProps } =
 | 
					  const { modalState: inviteModalState, modalProps: inviteModalProps } =
 | 
				
			||||||
    useModalTriggerState();
 | 
					    useModalTriggerState();
 | 
				
			||||||
| 
						 | 
					@ -111,6 +113,7 @@ export const PTTCallView: React.FC<Props> = ({
 | 
				
			||||||
    useModalTriggerState();
 | 
					    useModalTriggerState();
 | 
				
			||||||
  const [containerRef, bounds] = useMeasure({ polyfill: ResizeObserver });
 | 
					  const [containerRef, bounds] = useMeasure({ polyfill: ResizeObserver });
 | 
				
			||||||
  const facepileSize = bounds.width < 800 ? "sm" : "md";
 | 
					  const facepileSize = bounds.width < 800 ? "sm" : "md";
 | 
				
			||||||
 | 
					  const showControls = bounds.height > 500;
 | 
				
			||||||
  const pttButtonSize = 232;
 | 
					  const pttButtonSize = 232;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const { audioOutput } = useMediaHandler();
 | 
					  const { audioOutput } = useMediaHandler();
 | 
				
			||||||
| 
						 | 
					@ -170,17 +173,22 @@ export const PTTCallView: React.FC<Props> = ({
 | 
				
			||||||
        // https://github.com/vector-im/element-call/issues/328
 | 
					        // https://github.com/vector-im/element-call/issues/328
 | 
				
			||||||
        show={false}
 | 
					        show={false}
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
 | 
					      {showControls && (
 | 
				
			||||||
        <Header className={styles.header}>
 | 
					        <Header className={styles.header}>
 | 
				
			||||||
          <LeftNav>
 | 
					          <LeftNav>
 | 
				
			||||||
            <RoomSetupHeaderInfo
 | 
					            <RoomSetupHeaderInfo
 | 
				
			||||||
              roomName={roomName}
 | 
					              roomName={roomName}
 | 
				
			||||||
              avatarUrl={avatarUrl}
 | 
					              avatarUrl={avatarUrl}
 | 
				
			||||||
              onPress={onLeave}
 | 
					              onPress={onLeave}
 | 
				
			||||||
 | 
					              isEmbedded={isEmbedded}
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
          </LeftNav>
 | 
					          </LeftNav>
 | 
				
			||||||
          <RightNav />
 | 
					          <RightNav />
 | 
				
			||||||
        </Header>
 | 
					        </Header>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
      <div className={styles.center}>
 | 
					      <div className={styles.center}>
 | 
				
			||||||
 | 
					        {showControls && (
 | 
				
			||||||
 | 
					          <>
 | 
				
			||||||
            <div className={styles.participants}>
 | 
					            <div className={styles.participants}>
 | 
				
			||||||
              <p>{`${participants.length} ${
 | 
					              <p>{`${participants.length} ${
 | 
				
			||||||
                participants.length > 1 ? "people" : "person"
 | 
					                participants.length > 1 ? "people" : "person"
 | 
				
			||||||
| 
						 | 
					@ -203,12 +211,15 @@ export const PTTCallView: React.FC<Props> = ({
 | 
				
			||||||
                feedbackModalState={feedbackModalState}
 | 
					                feedbackModalState={feedbackModalState}
 | 
				
			||||||
                feedbackModalProps={feedbackModalProps}
 | 
					                feedbackModalProps={feedbackModalProps}
 | 
				
			||||||
              />
 | 
					              />
 | 
				
			||||||
          <HangupButton onPress={onLeave} />
 | 
					              {!isEmbedded && <HangupButton onPress={onLeave} />}
 | 
				
			||||||
              <InviteButton onPress={() => inviteModalState.open()} />
 | 
					              <InviteButton onPress={() => inviteModalState.open()} />
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
 | 
					          </>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div className={styles.pttButtonContainer}>
 | 
					        <div className={styles.pttButtonContainer}>
 | 
				
			||||||
          {activeSpeakerUserId ? (
 | 
					          {showControls &&
 | 
				
			||||||
 | 
					            (activeSpeakerUserId ? (
 | 
				
			||||||
              <div className={styles.talkingInfo}>
 | 
					              <div className={styles.talkingInfo}>
 | 
				
			||||||
                <h2>
 | 
					                <h2>
 | 
				
			||||||
                  {!activeSpeakerIsLocalUser && (
 | 
					                  {!activeSpeakerIsLocalUser && (
 | 
				
			||||||
| 
						 | 
					@ -222,7 +233,7 @@ export const PTTCallView: React.FC<Props> = ({
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
            ) : (
 | 
					            ) : (
 | 
				
			||||||
              <div className={styles.talkingInfo} />
 | 
					              <div className={styles.talkingInfo} />
 | 
				
			||||||
          )}
 | 
					            ))}
 | 
				
			||||||
          <PTTButton
 | 
					          <PTTButton
 | 
				
			||||||
            enabled={!feedbackModalState.isOpen}
 | 
					            enabled={!feedbackModalState.isOpen}
 | 
				
			||||||
            showTalkOverError={showTalkOverError}
 | 
					            showTalkOverError={showTalkOverError}
 | 
				
			||||||
| 
						 | 
					@ -238,6 +249,7 @@ export const PTTCallView: React.FC<Props> = ({
 | 
				
			||||||
            enqueueNetworkWaiting={enqueueTalkingExpected}
 | 
					            enqueueNetworkWaiting={enqueueTalkingExpected}
 | 
				
			||||||
            setNetworkWaiting={setTalkingExpected}
 | 
					            setNetworkWaiting={setTalkingExpected}
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
 | 
					          {showControls && (
 | 
				
			||||||
            <p className={styles.actionTip}>
 | 
					            <p className={styles.actionTip}>
 | 
				
			||||||
              {getPromptText(
 | 
					              {getPromptText(
 | 
				
			||||||
                networkWaiting,
 | 
					                networkWaiting,
 | 
				
			||||||
| 
						 | 
					@ -250,6 +262,7 @@ export const PTTCallView: React.FC<Props> = ({
 | 
				
			||||||
                connected
 | 
					                connected
 | 
				
			||||||
              )}
 | 
					              )}
 | 
				
			||||||
            </p>
 | 
					            </p>
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
          {userMediaFeeds.map((callFeed) => (
 | 
					          {userMediaFeeds.map((callFeed) => (
 | 
				
			||||||
            <PTTFeed
 | 
					            <PTTFeed
 | 
				
			||||||
              key={callFeed.userId}
 | 
					              key={callFeed.userId}
 | 
				
			||||||
| 
						 | 
					@ -257,7 +270,7 @@ export const PTTCallView: React.FC<Props> = ({
 | 
				
			||||||
              audioOutputDevice={audioOutput}
 | 
					              audioOutputDevice={audioOutput}
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
          ))}
 | 
					          ))}
 | 
				
			||||||
          {isAdmin && (
 | 
					          {isAdmin && showControls && (
 | 
				
			||||||
            <Toggle
 | 
					            <Toggle
 | 
				
			||||||
              isSelected={talkOverEnabled}
 | 
					              isSelected={talkOverEnabled}
 | 
				
			||||||
              onChange={setTalkOverEnabled}
 | 
					              onChange={setTalkOverEnabled}
 | 
				
			||||||
| 
						 | 
					@ -268,7 +281,7 @@ export const PTTCallView: React.FC<Props> = ({
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      {inviteModalState.isOpen && (
 | 
					      {inviteModalState.isOpen && showControls && (
 | 
				
			||||||
        <InviteModal roomId={roomId} {...inviteModalProps} />
 | 
					        <InviteModal roomId={roomId} {...inviteModalProps} />
 | 
				
			||||||
      )}
 | 
					      )}
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,9 +29,13 @@ export function RoomPage() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const { roomId: maybeRoomId } = useParams();
 | 
					  const { roomId: maybeRoomId } = useParams();
 | 
				
			||||||
  const { hash, search } = useLocation();
 | 
					  const { hash, search } = useLocation();
 | 
				
			||||||
  const [viaServers] = useMemo(() => {
 | 
					  const [viaServers, isEmbedded, isPtt] = useMemo(() => {
 | 
				
			||||||
    const params = new URLSearchParams(search);
 | 
					    const params = new URLSearchParams(search);
 | 
				
			||||||
    return [params.getAll("via")];
 | 
					    return [
 | 
				
			||||||
 | 
					      params.getAll("via"),
 | 
				
			||||||
 | 
					      params.has("embed"),
 | 
				
			||||||
 | 
					      params.get("ptt") === "true",
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
  }, [search]);
 | 
					  }, [search]);
 | 
				
			||||||
  const roomId = (maybeRoomId || hash || "").toLowerCase();
 | 
					  const roomId = (maybeRoomId || hash || "").toLowerCase();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -49,13 +53,19 @@ export function RoomPage() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <MediaHandlerProvider client={client}>
 | 
					    <MediaHandlerProvider client={client}>
 | 
				
			||||||
      <GroupCallLoader client={client} roomId={roomId} viaServers={viaServers}>
 | 
					      <GroupCallLoader
 | 
				
			||||||
 | 
					        client={client}
 | 
				
			||||||
 | 
					        roomId={roomId}
 | 
				
			||||||
 | 
					        viaServers={viaServers}
 | 
				
			||||||
 | 
					        createPtt={isPtt}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
        {(groupCall) => (
 | 
					        {(groupCall) => (
 | 
				
			||||||
          <GroupCallView
 | 
					          <GroupCallView
 | 
				
			||||||
            client={client}
 | 
					            client={client}
 | 
				
			||||||
            roomId={roomId}
 | 
					            roomId={roomId}
 | 
				
			||||||
            groupCall={groupCall}
 | 
					            groupCall={groupCall}
 | 
				
			||||||
            isPasswordlessUser={isPasswordlessUser}
 | 
					            isPasswordlessUser={isPasswordlessUser}
 | 
				
			||||||
 | 
					            isEmbedded={isEmbedded}
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
        )}
 | 
					        )}
 | 
				
			||||||
      </GroupCallLoader>
 | 
					      </GroupCallLoader>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -54,7 +54,13 @@ async function fetchGroupCall(
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function useLoadGroupCall(client, roomId, viaServers, createIfNotFound) {
 | 
					export function useLoadGroupCall(
 | 
				
			||||||
 | 
					  client,
 | 
				
			||||||
 | 
					  roomId,
 | 
				
			||||||
 | 
					  viaServers,
 | 
				
			||||||
 | 
					  createIfNotFound,
 | 
				
			||||||
 | 
					  createPtt
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
  const [state, setState] = useState({
 | 
					  const [state, setState] = useState({
 | 
				
			||||||
    loading: true,
 | 
					    loading: true,
 | 
				
			||||||
    error: undefined,
 | 
					    error: undefined,
 | 
				
			||||||
| 
						 | 
					@ -80,7 +86,7 @@ export function useLoadGroupCall(client, roomId, viaServers, createIfNotFound) {
 | 
				
			||||||
          isLocalRoomId(roomId)
 | 
					          isLocalRoomId(roomId)
 | 
				
			||||||
        ) {
 | 
					        ) {
 | 
				
			||||||
          const roomName = roomNameFromRoomId(roomId);
 | 
					          const roomName = roomNameFromRoomId(roomId);
 | 
				
			||||||
          await createRoom(client, roomName);
 | 
					          await createRoom(client, roomName, createPtt);
 | 
				
			||||||
          const groupCall = await fetchGroupCall(
 | 
					          const groupCall = await fetchGroupCall(
 | 
				
			||||||
            client,
 | 
					            client,
 | 
				
			||||||
            roomId,
 | 
					            roomId,
 | 
				
			||||||
| 
						 | 
					@ -103,7 +109,7 @@ export function useLoadGroupCall(client, roomId, viaServers, createIfNotFound) {
 | 
				
			||||||
      .catch((error) =>
 | 
					      .catch((error) =>
 | 
				
			||||||
        setState((prevState) => ({ ...prevState, loading: false, error }))
 | 
					        setState((prevState) => ({ ...prevState, loading: false, error }))
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
  }, [client, roomId, state.reloadId, createIfNotFound, viaServers]);
 | 
					  }, [client, roomId, state.reloadId, createIfNotFound, viaServers, createPtt]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return state;
 | 
					  return state;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -130,7 +130,7 @@ export const usePTT = (
 | 
				
			||||||
  const onMuteStateChanged = useCallback(() => {
 | 
					  const onMuteStateChanged = useCallback(() => {
 | 
				
			||||||
    const activeSpeakerFeed = getActiveSpeakerFeed(userMediaFeeds, groupCall);
 | 
					    const activeSpeakerFeed = getActiveSpeakerFeed(userMediaFeeds, groupCall);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let blocked = false;
 | 
					    let blocked = transmitBlocked;
 | 
				
			||||||
    if (activeSpeakerUserId === null && activeSpeakerFeed !== null) {
 | 
					    if (activeSpeakerUserId === null && activeSpeakerFeed !== null) {
 | 
				
			||||||
      if (activeSpeakerFeed.userId === client.getUserId()) {
 | 
					      if (activeSpeakerFeed.userId === client.getUserId()) {
 | 
				
			||||||
        playClip(PTTClipID.START_TALKING_LOCAL);
 | 
					        playClip(PTTClipID.START_TALKING_LOCAL);
 | 
				
			||||||
| 
						 | 
					@ -141,8 +141,8 @@ export const usePTT = (
 | 
				
			||||||
      playClip(PTTClipID.END_TALKING);
 | 
					      playClip(PTTClipID.END_TALKING);
 | 
				
			||||||
    } else if (
 | 
					    } else if (
 | 
				
			||||||
      pttButtonHeld &&
 | 
					      pttButtonHeld &&
 | 
				
			||||||
      activeSpeakerUserId === client.getUserId() &&
 | 
					      activeSpeakerFeed?.userId !== client.getUserId() &&
 | 
				
			||||||
      activeSpeakerFeed?.userId !== client.getUserId()
 | 
					      !transmitBlocked
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
      // We were talking but we've been cut off: mute our own mic
 | 
					      // We were talking but we've been cut off: mute our own mic
 | 
				
			||||||
      // (this is the easier way of cutting other speakers off if an
 | 
					      // (this is the easier way of cutting other speakers off if an
 | 
				
			||||||
| 
						 | 
					@ -167,6 +167,7 @@ export const usePTT = (
 | 
				
			||||||
    client,
 | 
					    client,
 | 
				
			||||||
    userMediaFeeds,
 | 
					    userMediaFeeds,
 | 
				
			||||||
    setMicMuteWrapper,
 | 
					    setMicMuteWrapper,
 | 
				
			||||||
 | 
					    transmitBlocked,
 | 
				
			||||||
  ]);
 | 
					  ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,6 +15,8 @@ limitations under the License.
 | 
				
			||||||
*/
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import React from "react";
 | 
					import React from "react";
 | 
				
			||||||
 | 
					import { Item } from "@react-stately/collections";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { Modal } from "../Modal";
 | 
					import { Modal } from "../Modal";
 | 
				
			||||||
import styles from "./SettingsModal.module.css";
 | 
					import styles from "./SettingsModal.module.css";
 | 
				
			||||||
import { TabContainer, TabItem } from "../tabs/Tabs";
 | 
					import { TabContainer, TabItem } from "../tabs/Tabs";
 | 
				
			||||||
| 
						 | 
					@ -22,7 +24,6 @@ import { ReactComponent as AudioIcon } from "../icons/Audio.svg";
 | 
				
			||||||
import { ReactComponent as VideoIcon } from "../icons/Video.svg";
 | 
					import { ReactComponent as VideoIcon } from "../icons/Video.svg";
 | 
				
			||||||
import { ReactComponent as DeveloperIcon } from "../icons/Developer.svg";
 | 
					import { ReactComponent as DeveloperIcon } from "../icons/Developer.svg";
 | 
				
			||||||
import { SelectInput } from "../input/SelectInput";
 | 
					import { SelectInput } from "../input/SelectInput";
 | 
				
			||||||
import { Item } from "@react-stately/collections";
 | 
					 | 
				
			||||||
import { useMediaHandler } from "./useMediaHandler";
 | 
					import { useMediaHandler } from "./useMediaHandler";
 | 
				
			||||||
import { useSpatialAudio, useShowInspector } from "./useSetting";
 | 
					import { useSpatialAudio, useShowInspector } from "./useSetting";
 | 
				
			||||||
import { FieldRow, InputField } from "../input/Input";
 | 
					import { FieldRow, InputField } from "../input/Input";
 | 
				
			||||||
| 
						 | 
					@ -30,7 +31,13 @@ import { Button } from "../button";
 | 
				
			||||||
import { useDownloadDebugLog } from "./submit-rageshake";
 | 
					import { useDownloadDebugLog } from "./submit-rageshake";
 | 
				
			||||||
import { Body } from "../typography/Typography";
 | 
					import { Body } from "../typography/Typography";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const SettingsModal = (props) => {
 | 
					interface Props {
 | 
				
			||||||
 | 
					  setShowInspector: boolean;
 | 
				
			||||||
 | 
					  showInspector: boolean;
 | 
				
			||||||
 | 
					  [rest: string]: unknown;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const SettingsModal = (props: Props) => {
 | 
				
			||||||
  const {
 | 
					  const {
 | 
				
			||||||
    audioInput,
 | 
					    audioInput,
 | 
				
			||||||
    audioInputs,
 | 
					    audioInputs,
 | 
				
			||||||
| 
						 | 
					@ -42,6 +49,7 @@ export const SettingsModal = (props) => {
 | 
				
			||||||
    audioOutputs,
 | 
					    audioOutputs,
 | 
				
			||||||
    setAudioOutput,
 | 
					    setAudioOutput,
 | 
				
			||||||
  } = useMediaHandler();
 | 
					  } = useMediaHandler();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [spatialAudio, setSpatialAudio] = useSpatialAudio();
 | 
					  const [spatialAudio, setSpatialAudio] = useSpatialAudio();
 | 
				
			||||||
  const [showInspector, setShowInspector] = useShowInspector();
 | 
					  const [showInspector, setShowInspector] = useShowInspector();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -69,8 +77,12 @@ export const SettingsModal = (props) => {
 | 
				
			||||||
            selectedKey={audioInput}
 | 
					            selectedKey={audioInput}
 | 
				
			||||||
            onSelectionChange={setAudioInput}
 | 
					            onSelectionChange={setAudioInput}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            {audioInputs.map(({ deviceId, label }) => (
 | 
					            {audioInputs.map(({ deviceId, label }, index) => (
 | 
				
			||||||
              <Item key={deviceId}>{label}</Item>
 | 
					              <Item key={deviceId}>
 | 
				
			||||||
 | 
					                {!!label && label.trim().length > 0
 | 
				
			||||||
 | 
					                  ? label
 | 
				
			||||||
 | 
					                  : `Microphone ${index + 1}`}
 | 
				
			||||||
 | 
					              </Item>
 | 
				
			||||||
            ))}
 | 
					            ))}
 | 
				
			||||||
          </SelectInput>
 | 
					          </SelectInput>
 | 
				
			||||||
          {audioOutputs.length > 0 && (
 | 
					          {audioOutputs.length > 0 && (
 | 
				
			||||||
| 
						 | 
					@ -79,8 +91,12 @@ export const SettingsModal = (props) => {
 | 
				
			||||||
              selectedKey={audioOutput}
 | 
					              selectedKey={audioOutput}
 | 
				
			||||||
              onSelectionChange={setAudioOutput}
 | 
					              onSelectionChange={setAudioOutput}
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              {audioOutputs.map(({ deviceId, label }) => (
 | 
					              {audioOutputs.map(({ deviceId, label }, index) => (
 | 
				
			||||||
                <Item key={deviceId}>{label}</Item>
 | 
					                <Item key={deviceId}>
 | 
				
			||||||
 | 
					                  {!!label && label.trim().length > 0
 | 
				
			||||||
 | 
					                    ? label
 | 
				
			||||||
 | 
					                    : `Speaker ${index + 1}`}
 | 
				
			||||||
 | 
					                </Item>
 | 
				
			||||||
              ))}
 | 
					              ))}
 | 
				
			||||||
            </SelectInput>
 | 
					            </SelectInput>
 | 
				
			||||||
          )}
 | 
					          )}
 | 
				
			||||||
| 
						 | 
					@ -91,7 +107,9 @@ export const SettingsModal = (props) => {
 | 
				
			||||||
              type="checkbox"
 | 
					              type="checkbox"
 | 
				
			||||||
              checked={spatialAudio}
 | 
					              checked={spatialAudio}
 | 
				
			||||||
              description="This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)"
 | 
					              description="This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)"
 | 
				
			||||||
              onChange={(e) => setSpatialAudio(e.target.checked)}
 | 
					              onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
 | 
				
			||||||
 | 
					                setSpatialAudio(event.target.checked)
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
          </FieldRow>
 | 
					          </FieldRow>
 | 
				
			||||||
        </TabItem>
 | 
					        </TabItem>
 | 
				
			||||||
| 
						 | 
					@ -108,8 +126,12 @@ export const SettingsModal = (props) => {
 | 
				
			||||||
            selectedKey={videoInput}
 | 
					            selectedKey={videoInput}
 | 
				
			||||||
            onSelectionChange={setVideoInput}
 | 
					            onSelectionChange={setVideoInput}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            {videoInputs.map(({ deviceId, label }) => (
 | 
					            {videoInputs.map(({ deviceId, label }, index) => (
 | 
				
			||||||
              <Item key={deviceId}>{label}</Item>
 | 
					              <Item key={deviceId}>
 | 
				
			||||||
 | 
					                {!!label && label.trim().length > 0
 | 
				
			||||||
 | 
					                  ? label
 | 
				
			||||||
 | 
					                  : `Camera ${index + 1}`}
 | 
				
			||||||
 | 
					              </Item>
 | 
				
			||||||
            ))}
 | 
					            ))}
 | 
				
			||||||
          </SelectInput>
 | 
					          </SelectInput>
 | 
				
			||||||
        </TabItem>
 | 
					        </TabItem>
 | 
				
			||||||
| 
						 | 
					@ -133,7 +155,9 @@ export const SettingsModal = (props) => {
 | 
				
			||||||
              label="Show Call Inspector"
 | 
					              label="Show Call Inspector"
 | 
				
			||||||
              type="checkbox"
 | 
					              type="checkbox"
 | 
				
			||||||
              checked={showInspector}
 | 
					              checked={showInspector}
 | 
				
			||||||
              onChange={(e) => setShowInspector(e.target.checked)}
 | 
					              onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
 | 
				
			||||||
 | 
					                setShowInspector(e.target.checked)
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
          </FieldRow>
 | 
					          </FieldRow>
 | 
				
			||||||
          <FieldRow>
 | 
					          <FieldRow>
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,4 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/ban-ts-comment */
 | 
				
			||||||
/*
 | 
					/*
 | 
				
			||||||
Copyright 2017 OpenMarket Ltd
 | 
					Copyright 2017 OpenMarket Ltd
 | 
				
			||||||
Copyright 2018 New Vector Ltd
 | 
					Copyright 2018 New Vector Ltd
 | 
				
			||||||
| 
						 | 
					@ -37,19 +38,33 @@ limitations under the License.
 | 
				
			||||||
//    actually timestamps. We then purge the remaining logs. We also do this
 | 
					//    actually timestamps. We then purge the remaining logs. We also do this
 | 
				
			||||||
//    purge on startup to prevent logs from accumulating.
 | 
					//    purge on startup to prevent logs from accumulating.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// the frequency with which we flush to indexeddb
 | 
					 | 
				
			||||||
import { logger } from "matrix-js-sdk/src/logger";
 | 
					import { logger } from "matrix-js-sdk/src/logger";
 | 
				
			||||||
 | 
					import { randomString } from "matrix-js-sdk/src/randomstring";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// the frequency with which we flush to indexeddb
 | 
				
			||||||
const FLUSH_RATE_MS = 30 * 1000;
 | 
					const FLUSH_RATE_MS = 30 * 1000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// the length of log data we keep in indexeddb (and include in the reports)
 | 
					// the length of log data we keep in indexeddb (and include in the reports)
 | 
				
			||||||
const MAX_LOG_SIZE = 1024 * 1024 * 5; // 5 MB
 | 
					const MAX_LOG_SIZE = 1024 * 1024 * 5; // 5 MB
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// A class which monkey-patches the global console and stores log lines.
 | 
					type LogFunction = (
 | 
				
			||||||
export class ConsoleLogger {
 | 
					  ...args: (Error | DOMException | object | string)[]
 | 
				
			||||||
  logs = "";
 | 
					) => void;
 | 
				
			||||||
 | 
					type LogFunctionName = "log" | "info" | "warn" | "error";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  monkeyPatch(consoleObj) {
 | 
					// A class which monkey-patches the global console and stores log lines.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface LogEntry {
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  lines: string;
 | 
				
			||||||
 | 
					  index?: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class ConsoleLogger {
 | 
				
			||||||
 | 
					  private logs = "";
 | 
				
			||||||
 | 
					  private originalFunctions: { [key in LogFunctionName]?: LogFunction } = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public monkeyPatch(consoleObj: Console): void {
 | 
				
			||||||
    // Monkey-patch console logging
 | 
					    // Monkey-patch console logging
 | 
				
			||||||
    const consoleFunctionsToLevels = {
 | 
					    const consoleFunctionsToLevels = {
 | 
				
			||||||
      log: "I",
 | 
					      log: "I",
 | 
				
			||||||
| 
						 | 
					@ -60,6 +75,7 @@ export class ConsoleLogger {
 | 
				
			||||||
    Object.keys(consoleFunctionsToLevels).forEach((fnName) => {
 | 
					    Object.keys(consoleFunctionsToLevels).forEach((fnName) => {
 | 
				
			||||||
      const level = consoleFunctionsToLevels[fnName];
 | 
					      const level = consoleFunctionsToLevels[fnName];
 | 
				
			||||||
      const originalFn = consoleObj[fnName].bind(consoleObj);
 | 
					      const originalFn = consoleObj[fnName].bind(consoleObj);
 | 
				
			||||||
 | 
					      this.originalFunctions[fnName] = originalFn;
 | 
				
			||||||
      consoleObj[fnName] = (...args) => {
 | 
					      consoleObj[fnName] = (...args) => {
 | 
				
			||||||
        this.log(level, ...args);
 | 
					        this.log(level, ...args);
 | 
				
			||||||
        originalFn(...args);
 | 
					        originalFn(...args);
 | 
				
			||||||
| 
						 | 
					@ -67,7 +83,17 @@ export class ConsoleLogger {
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  log(level, ...args) {
 | 
					  public bypassRageshake(
 | 
				
			||||||
 | 
					    fnName: LogFunctionName,
 | 
				
			||||||
 | 
					    ...args: (Error | DOMException | object | string)[]
 | 
				
			||||||
 | 
					  ): void {
 | 
				
			||||||
 | 
					    this.originalFunctions[fnName](...args);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public log(
 | 
				
			||||||
 | 
					    level: string,
 | 
				
			||||||
 | 
					    ...args: (Error | DOMException | object | string)[]
 | 
				
			||||||
 | 
					  ): void {
 | 
				
			||||||
    // We don't know what locale the user may be running so use ISO strings
 | 
					    // We don't know what locale the user may be running so use ISO strings
 | 
				
			||||||
    const ts = new Date().toISOString();
 | 
					    const ts = new Date().toISOString();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -78,21 +104,7 @@ export class ConsoleLogger {
 | 
				
			||||||
      } else if (arg instanceof Error) {
 | 
					      } else if (arg instanceof Error) {
 | 
				
			||||||
        return arg.message + (arg.stack ? `\n${arg.stack}` : "");
 | 
					        return arg.message + (arg.stack ? `\n${arg.stack}` : "");
 | 
				
			||||||
      } else if (typeof arg === "object") {
 | 
					      } else if (typeof arg === "object") {
 | 
				
			||||||
        try {
 | 
					        return JSON.stringify(arg, getCircularReplacer());
 | 
				
			||||||
          return JSON.stringify(arg);
 | 
					 | 
				
			||||||
        } catch (e) {
 | 
					 | 
				
			||||||
          // In development, it can be useful to log complex cyclic
 | 
					 | 
				
			||||||
          // objects to the console for inspection. This is fine for
 | 
					 | 
				
			||||||
          // the console, but default `stringify` can't handle that.
 | 
					 | 
				
			||||||
          // We workaround this by using a special replacer function
 | 
					 | 
				
			||||||
          // to only log values of the root object and avoid cycles.
 | 
					 | 
				
			||||||
          return JSON.stringify(arg, (key, value) => {
 | 
					 | 
				
			||||||
            if (key && typeof value === "object") {
 | 
					 | 
				
			||||||
              return "<object>";
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            return value;
 | 
					 | 
				
			||||||
          });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        return arg;
 | 
					        return arg;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
| 
						 | 
					@ -116,7 +128,7 @@ export class ConsoleLogger {
 | 
				
			||||||
   * @param {boolean} keepLogs True to not delete logs after flushing.
 | 
					   * @param {boolean} keepLogs True to not delete logs after flushing.
 | 
				
			||||||
   * @return {string} \n delimited log lines to flush.
 | 
					   * @return {string} \n delimited log lines to flush.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  flush(keepLogs) {
 | 
					  public flush(keepLogs?: boolean): string {
 | 
				
			||||||
    // The ConsoleLogger doesn't care how these end up on disk, it just
 | 
					    // The ConsoleLogger doesn't care how these end up on disk, it just
 | 
				
			||||||
    // flushes them to the caller.
 | 
					    // flushes them to the caller.
 | 
				
			||||||
    if (keepLogs) {
 | 
					    if (keepLogs) {
 | 
				
			||||||
| 
						 | 
					@ -130,24 +142,23 @@ export class ConsoleLogger {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// A class which stores log lines in an IndexedDB instance.
 | 
					// A class which stores log lines in an IndexedDB instance.
 | 
				
			||||||
export class IndexedDBLogStore {
 | 
					export class IndexedDBLogStore {
 | 
				
			||||||
  index = 0;
 | 
					  private index = 0;
 | 
				
			||||||
  db = null;
 | 
					  private db: IDBDatabase = null;
 | 
				
			||||||
  flushPromise = null;
 | 
					  private flushPromise: Promise<void> = null;
 | 
				
			||||||
  flushAgainPromise = null;
 | 
					  private flushAgainPromise: Promise<void> = null;
 | 
				
			||||||
 | 
					  private id: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(indexedDB, logger) {
 | 
					  constructor(private indexedDB: IDBFactory, private logger: ConsoleLogger) {
 | 
				
			||||||
    this.indexedDB = indexedDB;
 | 
					    this.id = "instance-" + randomString(16);
 | 
				
			||||||
    this.logger = logger;
 | 
					 | 
				
			||||||
    this.id = "instance-" + Math.random() + Date.now();
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * @return {Promise} Resolves when the store is ready.
 | 
					   * @return {Promise} Resolves when the store is ready.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  connect() {
 | 
					  public connect(): Promise<void> {
 | 
				
			||||||
    const req = this.indexedDB.open("logs");
 | 
					    const req = this.indexedDB.open("logs");
 | 
				
			||||||
    return new Promise((resolve, reject) => {
 | 
					    return new Promise((resolve, reject) => {
 | 
				
			||||||
      req.onsuccess = (event) => {
 | 
					      req.onsuccess = (event: Event) => {
 | 
				
			||||||
        // @ts-ignore
 | 
					        // @ts-ignore
 | 
				
			||||||
        this.db = event.target.result;
 | 
					        this.db = event.target.result;
 | 
				
			||||||
        // Periodically flush logs to local storage / indexeddb
 | 
					        // Periodically flush logs to local storage / indexeddb
 | 
				
			||||||
| 
						 | 
					@ -206,7 +217,7 @@ export class IndexedDBLogStore {
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @return {Promise} Resolved when the logs have been flushed.
 | 
					   * @return {Promise} Resolved when the logs have been flushed.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  flush() {
 | 
					  public flush(): Promise<void> {
 | 
				
			||||||
    // check if a flush() operation is ongoing
 | 
					    // check if a flush() operation is ongoing
 | 
				
			||||||
    if (this.flushPromise) {
 | 
					    if (this.flushPromise) {
 | 
				
			||||||
      if (this.flushAgainPromise) {
 | 
					      if (this.flushAgainPromise) {
 | 
				
			||||||
| 
						 | 
					@ -225,7 +236,7 @@ export class IndexedDBLogStore {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    // there is no flush promise or there was but it has finished, so do
 | 
					    // there is no flush promise or there was but it has finished, so do
 | 
				
			||||||
    // a brand new one, destroying the chain which may have been built up.
 | 
					    // a brand new one, destroying the chain which may have been built up.
 | 
				
			||||||
    this.flushPromise = new Promise((resolve, reject) => {
 | 
					    this.flushPromise = new Promise<void>((resolve, reject) => {
 | 
				
			||||||
      if (!this.db) {
 | 
					      if (!this.db) {
 | 
				
			||||||
        // not connected yet or user rejected access for us to r/w to the db.
 | 
					        // not connected yet or user rejected access for us to r/w to the db.
 | 
				
			||||||
        reject(new Error("No connected database"));
 | 
					        reject(new Error("No connected database"));
 | 
				
			||||||
| 
						 | 
					@ -243,6 +254,7 @@ export class IndexedDBLogStore {
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
      txn.onerror = (event) => {
 | 
					      txn.onerror = (event) => {
 | 
				
			||||||
        logger.error("Failed to flush logs : ", event);
 | 
					        logger.error("Failed to flush logs : ", event);
 | 
				
			||||||
 | 
					        // @ts-ignore
 | 
				
			||||||
        reject(new Error("Failed to write logs: " + event.target.errorCode));
 | 
					        reject(new Error("Failed to write logs: " + event.target.errorCode));
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
      objStore.add(this.generateLogEntry(lines));
 | 
					      objStore.add(this.generateLogEntry(lines));
 | 
				
			||||||
| 
						 | 
					@ -264,12 +276,12 @@ export class IndexedDBLogStore {
 | 
				
			||||||
   * log ID). The objects have said log ID in an "id" field and "lines" which
 | 
					   * log ID). The objects have said log ID in an "id" field and "lines" which
 | 
				
			||||||
   * is a big string with all the new-line delimited logs.
 | 
					   * is a big string with all the new-line delimited logs.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async consume() {
 | 
					  public async consume(): Promise<LogEntry[]> {
 | 
				
			||||||
    const db = this.db;
 | 
					    const db = this.db;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Returns: a string representing the concatenated logs for this ID.
 | 
					    // Returns: a string representing the concatenated logs for this ID.
 | 
				
			||||||
    // Stops adding log fragments when the size exceeds maxSize
 | 
					    // Stops adding log fragments when the size exceeds maxSize
 | 
				
			||||||
    function fetchLogs(id, maxSize) {
 | 
					    function fetchLogs(id: string, maxSize: number): Promise<string> {
 | 
				
			||||||
      const objectStore = db
 | 
					      const objectStore = db
 | 
				
			||||||
        .transaction("logs", "readonly")
 | 
					        .transaction("logs", "readonly")
 | 
				
			||||||
        .objectStore("logs");
 | 
					        .objectStore("logs");
 | 
				
			||||||
| 
						 | 
					@ -280,9 +292,11 @@ export class IndexedDBLogStore {
 | 
				
			||||||
          .openCursor(IDBKeyRange.only(id), "prev");
 | 
					          .openCursor(IDBKeyRange.only(id), "prev");
 | 
				
			||||||
        let lines = "";
 | 
					        let lines = "";
 | 
				
			||||||
        query.onerror = (event) => {
 | 
					        query.onerror = (event) => {
 | 
				
			||||||
 | 
					          // @ts-ignore
 | 
				
			||||||
          reject(new Error("Query failed: " + event.target.errorCode));
 | 
					          reject(new Error("Query failed: " + event.target.errorCode));
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
        query.onsuccess = (event) => {
 | 
					        query.onsuccess = (event) => {
 | 
				
			||||||
 | 
					          // @ts-ignore
 | 
				
			||||||
          const cursor = event.target.result;
 | 
					          const cursor = event.target.result;
 | 
				
			||||||
          if (!cursor) {
 | 
					          if (!cursor) {
 | 
				
			||||||
            resolve(lines);
 | 
					            resolve(lines);
 | 
				
			||||||
| 
						 | 
					@ -299,12 +313,12 @@ export class IndexedDBLogStore {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Returns: A sorted array of log IDs. (newest first)
 | 
					    // Returns: A sorted array of log IDs. (newest first)
 | 
				
			||||||
    function fetchLogIds() {
 | 
					    function fetchLogIds(): Promise<string[]> {
 | 
				
			||||||
      // To gather all the log IDs, query for all records in logslastmod.
 | 
					      // To gather all the log IDs, query for all records in logslastmod.
 | 
				
			||||||
      const o = db
 | 
					      const o = db
 | 
				
			||||||
        .transaction("logslastmod", "readonly")
 | 
					        .transaction("logslastmod", "readonly")
 | 
				
			||||||
        .objectStore("logslastmod");
 | 
					        .objectStore("logslastmod");
 | 
				
			||||||
      return selectQuery(o, undefined, (cursor) => {
 | 
					      return selectQuery<{ ts: number; id: string }>(o, undefined, (cursor) => {
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
          id: cursor.value.id,
 | 
					          id: cursor.value.id,
 | 
				
			||||||
          ts: cursor.value.ts,
 | 
					          ts: cursor.value.ts,
 | 
				
			||||||
| 
						 | 
					@ -319,13 +333,14 @@ export class IndexedDBLogStore {
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function deleteLogs(id) {
 | 
					    function deleteLogs(id: number): Promise<void> {
 | 
				
			||||||
      return new Promise((resolve, reject) => {
 | 
					      return new Promise<void>((resolve, reject) => {
 | 
				
			||||||
        const txn = db.transaction(["logs", "logslastmod"], "readwrite");
 | 
					        const txn = db.transaction(["logs", "logslastmod"], "readwrite");
 | 
				
			||||||
        const o = txn.objectStore("logs");
 | 
					        const o = txn.objectStore("logs");
 | 
				
			||||||
        // only load the key path, not the data which may be huge
 | 
					        // only load the key path, not the data which may be huge
 | 
				
			||||||
        const query = o.index("id").openKeyCursor(IDBKeyRange.only(id));
 | 
					        const query = o.index("id").openKeyCursor(IDBKeyRange.only(id));
 | 
				
			||||||
        query.onsuccess = (event) => {
 | 
					        query.onsuccess = (event) => {
 | 
				
			||||||
 | 
					          // @ts-ignore
 | 
				
			||||||
          const cursor = event.target.result;
 | 
					          const cursor = event.target.result;
 | 
				
			||||||
          if (!cursor) {
 | 
					          if (!cursor) {
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
| 
						 | 
					@ -340,6 +355,7 @@ export class IndexedDBLogStore {
 | 
				
			||||||
          reject(
 | 
					          reject(
 | 
				
			||||||
            new Error(
 | 
					            new Error(
 | 
				
			||||||
              "Failed to delete logs for " +
 | 
					              "Failed to delete logs for " +
 | 
				
			||||||
 | 
					                // @ts-ignore
 | 
				
			||||||
                `'${id}' : ${event.target.errorCode}`
 | 
					                `'${id}' : ${event.target.errorCode}`
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
| 
						 | 
					@ -352,7 +368,7 @@ export class IndexedDBLogStore {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const allLogIds = await fetchLogIds();
 | 
					    const allLogIds = await fetchLogIds();
 | 
				
			||||||
    let removeLogIds = [];
 | 
					    let removeLogIds = [];
 | 
				
			||||||
    const logs = [];
 | 
					    const logs: LogEntry[] = [];
 | 
				
			||||||
    let size = 0;
 | 
					    let size = 0;
 | 
				
			||||||
    for (let i = 0; i < allLogIds.length; i++) {
 | 
					    for (let i = 0; i < allLogIds.length; i++) {
 | 
				
			||||||
      const lines = await fetchLogs(allLogIds[i], MAX_LOG_SIZE - size);
 | 
					      const lines = await fetchLogs(allLogIds[i], MAX_LOG_SIZE - size);
 | 
				
			||||||
| 
						 | 
					@ -390,7 +406,7 @@ export class IndexedDBLogStore {
 | 
				
			||||||
    return logs;
 | 
					    return logs;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  generateLogEntry(lines) {
 | 
					  private generateLogEntry(lines: string): LogEntry {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      id: this.id,
 | 
					      id: this.id,
 | 
				
			||||||
      lines: lines,
 | 
					      lines: lines,
 | 
				
			||||||
| 
						 | 
					@ -398,7 +414,7 @@ export class IndexedDBLogStore {
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  generateLastModifiedTime() {
 | 
					  private generateLastModifiedTime(): { id: string; ts: number } {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      id: this.id,
 | 
					      id: this.id,
 | 
				
			||||||
      ts: Date.now(),
 | 
					      ts: Date.now(),
 | 
				
			||||||
| 
						 | 
					@ -416,7 +432,11 @@ export class IndexedDBLogStore {
 | 
				
			||||||
 * @return {Promise<T[]>} Resolves to an array of whatever you returned from
 | 
					 * @return {Promise<T[]>} Resolves to an array of whatever you returned from
 | 
				
			||||||
 * resultMapper.
 | 
					 * resultMapper.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function selectQuery(store, keyRange, resultMapper) {
 | 
					function selectQuery<T>(
 | 
				
			||||||
 | 
					  store: IDBObjectStore,
 | 
				
			||||||
 | 
					  keyRange: IDBKeyRange,
 | 
				
			||||||
 | 
					  resultMapper: (cursor: IDBCursorWithValue) => T
 | 
				
			||||||
 | 
					): Promise<T[]> {
 | 
				
			||||||
  const query = store.openCursor(keyRange);
 | 
					  const query = store.openCursor(keyRange);
 | 
				
			||||||
  return new Promise((resolve, reject) => {
 | 
					  return new Promise((resolve, reject) => {
 | 
				
			||||||
    const results = [];
 | 
					    const results = [];
 | 
				
			||||||
| 
						 | 
					@ -437,6 +457,16 @@ function selectQuery(store, keyRange, resultMapper) {
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					declare global {
 | 
				
			||||||
 | 
					  // eslint-disable-next-line no-var, camelcase
 | 
				
			||||||
 | 
					  var mx_rage_store: IndexedDBLogStore;
 | 
				
			||||||
 | 
					  // eslint-disable-next-line no-var, camelcase
 | 
				
			||||||
 | 
					  var mx_rage_logger: ConsoleLogger;
 | 
				
			||||||
 | 
					  // eslint-disable-next-line no-var, camelcase
 | 
				
			||||||
 | 
					  var mx_rage_initPromise: Promise<void>;
 | 
				
			||||||
 | 
					  // eslint-disable-next-line no-var, camelcase
 | 
				
			||||||
 | 
					  var mx_rage_initStoragePromise: Promise<void>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Configure rage shaking support for sending bug reports.
 | 
					 * Configure rage shaking support for sending bug reports.
 | 
				
			||||||
| 
						 | 
					@ -445,7 +475,7 @@ function selectQuery(store, keyRange, resultMapper) {
 | 
				
			||||||
 * be set up immediately for the logs.
 | 
					 * be set up immediately for the logs.
 | 
				
			||||||
 * @return {Promise} Resolves when set up.
 | 
					 * @return {Promise} Resolves when set up.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export function init(setUpPersistence = true) {
 | 
					export function init(setUpPersistence = true): Promise<void> {
 | 
				
			||||||
  if (global.mx_rage_initPromise) {
 | 
					  if (global.mx_rage_initPromise) {
 | 
				
			||||||
    return global.mx_rage_initPromise;
 | 
					    return global.mx_rage_initPromise;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -465,7 +495,7 @@ export function init(setUpPersistence = true) {
 | 
				
			||||||
 * then this no-ops.
 | 
					 * then this no-ops.
 | 
				
			||||||
 * @return {Promise} Resolves when complete.
 | 
					 * @return {Promise} Resolves when complete.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export function tryInitStorage() {
 | 
					export function tryInitStorage(): Promise<void> {
 | 
				
			||||||
  if (global.mx_rage_initStoragePromise) {
 | 
					  if (global.mx_rage_initStoragePromise) {
 | 
				
			||||||
    return global.mx_rage_initStoragePromise;
 | 
					    return global.mx_rage_initStoragePromise;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -491,7 +521,7 @@ export function tryInitStorage() {
 | 
				
			||||||
  return global.mx_rage_initStoragePromise;
 | 
					  return global.mx_rage_initStoragePromise;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function flush() {
 | 
					export function flush(): Promise<void> {
 | 
				
			||||||
  if (!global.mx_rage_store) {
 | 
					  if (!global.mx_rage_store) {
 | 
				
			||||||
    return;
 | 
					    return;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -502,7 +532,7 @@ export function flush() {
 | 
				
			||||||
 * Clean up old logs.
 | 
					 * Clean up old logs.
 | 
				
			||||||
 * @return {Promise} Resolves if cleaned logs.
 | 
					 * @return {Promise} Resolves if cleaned logs.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export async function cleanup() {
 | 
					export async function cleanup(): Promise<void> {
 | 
				
			||||||
  if (!global.mx_rage_store) {
 | 
					  if (!global.mx_rage_store) {
 | 
				
			||||||
    return;
 | 
					    return;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -512,9 +542,9 @@ export async function cleanup() {
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Get a recent snapshot of the logs, ready for attaching to a bug report
 | 
					 * Get a recent snapshot of the logs, ready for attaching to a bug report
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * @return {Array<{lines: string, id, string}>}  list of log data
 | 
					 * @return {LogEntry[]}  list of log data
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export async function getLogsForReport() {
 | 
					export async function getLogsForReport(): Promise<LogEntry[]> {
 | 
				
			||||||
  if (!global.mx_rage_logger) {
 | 
					  if (!global.mx_rage_logger) {
 | 
				
			||||||
    throw new Error("No console logger, did you forget to call init()?");
 | 
					    throw new Error("No console logger, did you forget to call init()?");
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -523,7 +553,7 @@ export async function getLogsForReport() {
 | 
				
			||||||
  if (global.mx_rage_store) {
 | 
					  if (global.mx_rage_store) {
 | 
				
			||||||
    // flush most recent logs
 | 
					    // flush most recent logs
 | 
				
			||||||
    await global.mx_rage_store.flush();
 | 
					    await global.mx_rage_store.flush();
 | 
				
			||||||
    return await global.mx_rage_store.consume();
 | 
					    return global.mx_rage_store.consume();
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    return [
 | 
					    return [
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
| 
						 | 
					@ -533,3 +563,24 @@ export async function getLogsForReport() {
 | 
				
			||||||
    ];
 | 
					    ];
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type StringifyReplacer = (
 | 
				
			||||||
 | 
					  this: unknown,
 | 
				
			||||||
 | 
					  key: string,
 | 
				
			||||||
 | 
					  value: unknown
 | 
				
			||||||
 | 
					) => unknown;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value#circular_references
 | 
				
			||||||
 | 
					// Injects `<$ cycle-trimmed $>` wherever it cuts a cyclical object relationship
 | 
				
			||||||
 | 
					const getCircularReplacer = (): StringifyReplacer => {
 | 
				
			||||||
 | 
					  const seen = new WeakSet();
 | 
				
			||||||
 | 
					  return (key: string, value: unknown): unknown => {
 | 
				
			||||||
 | 
					    if (typeof value === "object" && value !== null) {
 | 
				
			||||||
 | 
					      if (seen.has(value)) {
 | 
				
			||||||
 | 
					        return "<$ cycle-trimmed $>";
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      seen.add(value);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return value;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -15,14 +15,31 @@ limitations under the License.
 | 
				
			||||||
*/
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { useCallback, useContext, useEffect, useState } from "react";
 | 
					import { useCallback, useContext, useEffect, useState } from "react";
 | 
				
			||||||
import { getLogsForReport } from "./rageshake";
 | 
					 | 
				
			||||||
import pako from "pako";
 | 
					import pako from "pako";
 | 
				
			||||||
 | 
					import { MatrixEvent } from "matrix-js-sdk";
 | 
				
			||||||
 | 
					import { OverlayTriggerState } from "@react-stately/overlays";
 | 
				
			||||||
 | 
					import { MatrixClient, ClientEvent } from "matrix-js-sdk/src/client";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { getLogsForReport } from "./rageshake";
 | 
				
			||||||
import { useClient } from "../ClientContext";
 | 
					import { useClient } from "../ClientContext";
 | 
				
			||||||
import { InspectorContext } from "../room/GroupCallInspector";
 | 
					import { InspectorContext } from "../room/GroupCallInspector";
 | 
				
			||||||
import { useModalTriggerState } from "../Modal";
 | 
					import { useModalTriggerState } from "../Modal";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function useSubmitRageshake() {
 | 
					interface RageShakeSubmitOptions {
 | 
				
			||||||
  const { client } = useClient();
 | 
					  description: string;
 | 
				
			||||||
 | 
					  roomId: string;
 | 
				
			||||||
 | 
					  label: string;
 | 
				
			||||||
 | 
					  sendLogs: boolean;
 | 
				
			||||||
 | 
					  rageshakeRequestId: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function useSubmitRageshake(): {
 | 
				
			||||||
 | 
					  submitRageshake: (opts: RageShakeSubmitOptions) => Promise<void>;
 | 
				
			||||||
 | 
					  sending: boolean;
 | 
				
			||||||
 | 
					  sent: boolean;
 | 
				
			||||||
 | 
					  error: Error;
 | 
				
			||||||
 | 
					} {
 | 
				
			||||||
 | 
					  const client: MatrixClient = useClient().client;
 | 
				
			||||||
  const [{ json }] = useContext(InspectorContext);
 | 
					  const [{ json }] = useContext(InspectorContext);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [{ sending, sent, error }, setState] = useState({
 | 
					  const [{ sending, sent, error }, setState] = useState({
 | 
				
			||||||
| 
						 | 
					@ -57,9 +74,12 @@ export function useSubmitRageshake() {
 | 
				
			||||||
          opts.description || "User did not supply any additional text."
 | 
					          opts.description || "User did not supply any additional text."
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        body.append("app", "matrix-video-chat");
 | 
					        body.append("app", "matrix-video-chat");
 | 
				
			||||||
        body.append("version", import.meta.env.VITE_APP_VERSION || "dev");
 | 
					        body.append(
 | 
				
			||||||
 | 
					          "version",
 | 
				
			||||||
 | 
					          (import.meta.env.VITE_APP_VERSION as string) || "dev"
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
        body.append("user_agent", userAgent);
 | 
					        body.append("user_agent", userAgent);
 | 
				
			||||||
        body.append("installed_pwa", false);
 | 
					        body.append("installed_pwa", "false");
 | 
				
			||||||
        body.append("touch_input", touchInput);
 | 
					        body.append("touch_input", touchInput);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (client) {
 | 
					        if (client) {
 | 
				
			||||||
| 
						 | 
					@ -181,7 +201,11 @@ export function useSubmitRageshake() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (navigator.storage && navigator.storage.estimate) {
 | 
					        if (navigator.storage && navigator.storage.estimate) {
 | 
				
			||||||
          try {
 | 
					          try {
 | 
				
			||||||
            const estimate = await navigator.storage.estimate();
 | 
					            const estimate: {
 | 
				
			||||||
 | 
					              quota?: number;
 | 
				
			||||||
 | 
					              usage?: number;
 | 
				
			||||||
 | 
					              usageDetails?: { [x: string]: unknown };
 | 
				
			||||||
 | 
					            } = await navigator.storage.estimate();
 | 
				
			||||||
            body.append("storageManager_quota", String(estimate.quota));
 | 
					            body.append("storageManager_quota", String(estimate.quota));
 | 
				
			||||||
            body.append("storageManager_usage", String(estimate.usage));
 | 
					            body.append("storageManager_usage", String(estimate.usage));
 | 
				
			||||||
            if (estimate.usageDetails) {
 | 
					            if (estimate.usageDetails) {
 | 
				
			||||||
| 
						 | 
					@ -201,7 +225,6 @@ export function useSubmitRageshake() {
 | 
				
			||||||
          for (const entry of logs) {
 | 
					          for (const entry of logs) {
 | 
				
			||||||
            // encode as UTF-8
 | 
					            // encode as UTF-8
 | 
				
			||||||
            let buf = new TextEncoder().encode(entry.lines);
 | 
					            let buf = new TextEncoder().encode(entry.lines);
 | 
				
			||||||
 | 
					 | 
				
			||||||
            // compress
 | 
					            // compress
 | 
				
			||||||
            buf = pako.gzip(buf);
 | 
					            buf = pako.gzip(buf);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -225,7 +248,7 @@ export function useSubmitRageshake() {
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await fetch(
 | 
					        await fetch(
 | 
				
			||||||
          import.meta.env.VITE_RAGESHAKE_SUBMIT_URL ||
 | 
					          (import.meta.env.VITE_RAGESHAKE_SUBMIT_URL as string) ||
 | 
				
			||||||
            "https://element.io/bugreports/submit",
 | 
					            "https://element.io/bugreports/submit",
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            method: "POST",
 | 
					            method: "POST",
 | 
				
			||||||
| 
						 | 
					@ -250,7 +273,7 @@ export function useSubmitRageshake() {
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function useDownloadDebugLog() {
 | 
					export function useDownloadDebugLog(): () => void {
 | 
				
			||||||
  const [{ json }] = useContext(InspectorContext);
 | 
					  const [{ json }] = useContext(InspectorContext);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const downloadDebugLog = useCallback(() => {
 | 
					  const downloadDebugLog = useCallback(() => {
 | 
				
			||||||
| 
						 | 
					@ -271,7 +294,10 @@ export function useDownloadDebugLog() {
 | 
				
			||||||
  return downloadDebugLog;
 | 
					  return downloadDebugLog;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function useRageshakeRequest() {
 | 
					export function useRageshakeRequest(): (
 | 
				
			||||||
 | 
					  roomId: string,
 | 
				
			||||||
 | 
					  rageshakeRequestId: string
 | 
				
			||||||
 | 
					) => void {
 | 
				
			||||||
  const { client } = useClient();
 | 
					  const { client } = useClient();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const sendRageshakeRequest = useCallback(
 | 
					  const sendRageshakeRequest = useCallback(
 | 
				
			||||||
| 
						 | 
					@ -285,14 +311,27 @@ export function useRageshakeRequest() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return sendRageshakeRequest;
 | 
					  return sendRageshakeRequest;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					interface ModalProps {
 | 
				
			||||||
 | 
					  isOpen: boolean;
 | 
				
			||||||
 | 
					  onClose: () => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					interface ModalPropsWithId extends ModalProps {
 | 
				
			||||||
 | 
					  rageshakeRequestId: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function useRageshakeRequestModal(roomId) {
 | 
					export function useRageshakeRequestModal(roomId: string): {
 | 
				
			||||||
  const { modalState, modalProps } = useModalTriggerState();
 | 
					  modalState: OverlayTriggerState;
 | 
				
			||||||
  const { client } = useClient();
 | 
					  modalProps: ModalPropsWithId;
 | 
				
			||||||
  const [rageshakeRequestId, setRageshakeRequestId] = useState();
 | 
					} {
 | 
				
			||||||
 | 
					  const { modalState, modalProps } = useModalTriggerState() as {
 | 
				
			||||||
 | 
					    modalState: OverlayTriggerState;
 | 
				
			||||||
 | 
					    modalProps: ModalProps;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  const client: MatrixClient = useClient().client;
 | 
				
			||||||
 | 
					  const [rageshakeRequestId, setRageshakeRequestId] = useState<string>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    const onEvent = (event) => {
 | 
					    const onEvent = (event: MatrixEvent) => {
 | 
				
			||||||
      const type = event.getType();
 | 
					      const type = event.getType();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (
 | 
					      if (
 | 
				
			||||||
| 
						 | 
					@ -305,10 +344,10 @@ export function useRageshakeRequestModal(roomId) {
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    client.on("event", onEvent);
 | 
					    client.on(ClientEvent.Event, onEvent);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return () => {
 | 
					    return () => {
 | 
				
			||||||
      client.removeListener("event", onEvent);
 | 
					      client.removeListener(ClientEvent.Event, onEvent);
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }, [modalState.open, roomId, client, modalState]);
 | 
					  }, [modalState.open, roomId, client, modalState]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,4 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/ban-ts-comment */
 | 
				
			||||||
/*
 | 
					/*
 | 
				
			||||||
Copyright 2022 Matrix.org Foundation C.I.C.
 | 
					Copyright 2022 Matrix.org Foundation C.I.C.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,6 +15,8 @@ See the License for the specific language governing permissions and
 | 
				
			||||||
limitations under the License.
 | 
					limitations under the License.
 | 
				
			||||||
*/
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { MatrixClient } from "matrix-js-sdk";
 | 
				
			||||||
 | 
					import { MediaHandlerEvent } from "matrix-js-sdk/src/webrtc/mediaHandler";
 | 
				
			||||||
import React, {
 | 
					import React, {
 | 
				
			||||||
  useState,
 | 
					  useState,
 | 
				
			||||||
  useEffect,
 | 
					  useEffect,
 | 
				
			||||||
| 
						 | 
					@ -23,9 +26,27 @@ import React, {
 | 
				
			||||||
  createContext,
 | 
					  createContext,
 | 
				
			||||||
} from "react";
 | 
					} from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const MediaHandlerContext = createContext();
 | 
					export interface MediaHandlerContextInterface {
 | 
				
			||||||
 | 
					  audioInput: string;
 | 
				
			||||||
 | 
					  audioInputs: MediaDeviceInfo[];
 | 
				
			||||||
 | 
					  setAudioInput: (deviceId: string) => void;
 | 
				
			||||||
 | 
					  videoInput: string;
 | 
				
			||||||
 | 
					  videoInputs: MediaDeviceInfo[];
 | 
				
			||||||
 | 
					  setVideoInput: (deviceId: string) => void;
 | 
				
			||||||
 | 
					  audioOutput: string;
 | 
				
			||||||
 | 
					  audioOutputs: MediaDeviceInfo[];
 | 
				
			||||||
 | 
					  setAudioOutput: (deviceId: string) => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getMediaPreferences() {
 | 
					const MediaHandlerContext =
 | 
				
			||||||
 | 
					  createContext<MediaHandlerContextInterface>(undefined);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface MediaPreferences {
 | 
				
			||||||
 | 
					  audioInput?: string;
 | 
				
			||||||
 | 
					  videoInput?: string;
 | 
				
			||||||
 | 
					  audioOutput?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					function getMediaPreferences(): MediaPreferences {
 | 
				
			||||||
  const mediaPreferences = localStorage.getItem("matrix-media-preferences");
 | 
					  const mediaPreferences = localStorage.getItem("matrix-media-preferences");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (mediaPreferences) {
 | 
					  if (mediaPreferences) {
 | 
				
			||||||
| 
						 | 
					@ -39,8 +60,8 @@ function getMediaPreferences() {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function updateMediaPreferences(newPreferences) {
 | 
					function updateMediaPreferences(newPreferences: MediaPreferences): void {
 | 
				
			||||||
  const oldPreferences = getMediaPreferences(newPreferences);
 | 
					  const oldPreferences = getMediaPreferences();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  localStorage.setItem(
 | 
					  localStorage.setItem(
 | 
				
			||||||
    "matrix-media-preferences",
 | 
					    "matrix-media-preferences",
 | 
				
			||||||
| 
						 | 
					@ -50,8 +71,11 @@ function updateMediaPreferences(newPreferences) {
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					interface Props {
 | 
				
			||||||
export function MediaHandlerProvider({ client, children }) {
 | 
					  client: MatrixClient;
 | 
				
			||||||
 | 
					  children: JSX.Element[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export function MediaHandlerProvider({ client, children }: Props): JSX.Element {
 | 
				
			||||||
  const [
 | 
					  const [
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      audioInput,
 | 
					      audioInput,
 | 
				
			||||||
| 
						 | 
					@ -72,7 +96,9 @@ export function MediaHandlerProvider({ client, children }) {
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
 | 
					      // @ts-ignore, ignore that audioInput is a private members of mediaHandler
 | 
				
			||||||
      audioInput: mediaHandler.audioInput,
 | 
					      audioInput: mediaHandler.audioInput,
 | 
				
			||||||
 | 
					      // @ts-ignore, ignore that videoInput is a private members of mediaHandler
 | 
				
			||||||
      videoInput: mediaHandler.videoInput,
 | 
					      videoInput: mediaHandler.videoInput,
 | 
				
			||||||
      audioOutput: undefined,
 | 
					      audioOutput: undefined,
 | 
				
			||||||
      audioInputs: [],
 | 
					      audioInputs: [],
 | 
				
			||||||
| 
						 | 
					@ -84,7 +110,7 @@ export function MediaHandlerProvider({ client, children }) {
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    const mediaHandler = client.getMediaHandler();
 | 
					    const mediaHandler = client.getMediaHandler();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function updateDevices() {
 | 
					    function updateDevices(): void {
 | 
				
			||||||
      navigator.mediaDevices.enumerateDevices().then((devices) => {
 | 
					      navigator.mediaDevices.enumerateDevices().then((devices) => {
 | 
				
			||||||
        const mediaPreferences = getMediaPreferences();
 | 
					        const mediaPreferences = getMediaPreferences();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -92,9 +118,10 @@ export function MediaHandlerProvider({ client, children }) {
 | 
				
			||||||
          (device) => device.kind === "audioinput"
 | 
					          (device) => device.kind === "audioinput"
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        const audioConnected = audioInputs.some(
 | 
					        const audioConnected = audioInputs.some(
 | 
				
			||||||
 | 
					          // @ts-ignore
 | 
				
			||||||
          (device) => device.deviceId === mediaHandler.audioInput
 | 
					          (device) => device.deviceId === mediaHandler.audioInput
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					        // @ts-ignore
 | 
				
			||||||
        let audioInput = mediaHandler.audioInput;
 | 
					        let audioInput = mediaHandler.audioInput;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!audioConnected && audioInputs.length > 0) {
 | 
					        if (!audioConnected && audioInputs.length > 0) {
 | 
				
			||||||
| 
						 | 
					@ -105,9 +132,11 @@ export function MediaHandlerProvider({ client, children }) {
 | 
				
			||||||
          (device) => device.kind === "videoinput"
 | 
					          (device) => device.kind === "videoinput"
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        const videoConnected = videoInputs.some(
 | 
					        const videoConnected = videoInputs.some(
 | 
				
			||||||
 | 
					          // @ts-ignore
 | 
				
			||||||
          (device) => device.deviceId === mediaHandler.videoInput
 | 
					          (device) => device.deviceId === mediaHandler.videoInput
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // @ts-ignore
 | 
				
			||||||
        let videoInput = mediaHandler.videoInput;
 | 
					        let videoInput = mediaHandler.videoInput;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!videoConnected && videoInputs.length > 0) {
 | 
					        if (!videoConnected && videoInputs.length > 0) {
 | 
				
			||||||
| 
						 | 
					@ -129,7 +158,9 @@ export function MediaHandlerProvider({ client, children }) {
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (
 | 
					        if (
 | 
				
			||||||
 | 
					          // @ts-ignore
 | 
				
			||||||
          mediaHandler.videoInput !== videoInput ||
 | 
					          mediaHandler.videoInput !== videoInput ||
 | 
				
			||||||
 | 
					          // @ts-ignore
 | 
				
			||||||
          mediaHandler.audioInput !== audioInput
 | 
					          mediaHandler.audioInput !== audioInput
 | 
				
			||||||
        ) {
 | 
					        ) {
 | 
				
			||||||
          mediaHandler.setMediaInputs(audioInput, videoInput);
 | 
					          mediaHandler.setMediaInputs(audioInput, videoInput);
 | 
				
			||||||
| 
						 | 
					@ -149,18 +180,21 @@ export function MediaHandlerProvider({ client, children }) {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    updateDevices();
 | 
					    updateDevices();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    mediaHandler.on("local_streams_changed", updateDevices);
 | 
					    mediaHandler.on(MediaHandlerEvent.LocalStreamsChanged, updateDevices);
 | 
				
			||||||
    navigator.mediaDevices.addEventListener("devicechange", updateDevices);
 | 
					    navigator.mediaDevices.addEventListener("devicechange", updateDevices);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return () => {
 | 
					    return () => {
 | 
				
			||||||
      mediaHandler.removeListener("local_streams_changed", updateDevices);
 | 
					      mediaHandler.removeListener(
 | 
				
			||||||
 | 
					        MediaHandlerEvent.LocalStreamsChanged,
 | 
				
			||||||
 | 
					        updateDevices
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
      navigator.mediaDevices.removeEventListener("devicechange", updateDevices);
 | 
					      navigator.mediaDevices.removeEventListener("devicechange", updateDevices);
 | 
				
			||||||
      mediaHandler.stopAllStreams();
 | 
					      mediaHandler.stopAllStreams();
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }, [client]);
 | 
					  }, [client]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const setAudioInput = useCallback(
 | 
					  const setAudioInput: (deviceId: string) => void = useCallback(
 | 
				
			||||||
    (deviceId) => {
 | 
					    (deviceId: string) => {
 | 
				
			||||||
      updateMediaPreferences({ audioInput: deviceId });
 | 
					      updateMediaPreferences({ audioInput: deviceId });
 | 
				
			||||||
      setState((prevState) => ({ ...prevState, audioInput: deviceId }));
 | 
					      setState((prevState) => ({ ...prevState, audioInput: deviceId }));
 | 
				
			||||||
      client.getMediaHandler().setAudioInput(deviceId);
 | 
					      client.getMediaHandler().setAudioInput(deviceId);
 | 
				
			||||||
| 
						 | 
					@ -168,7 +202,7 @@ export function MediaHandlerProvider({ client, children }) {
 | 
				
			||||||
    [client]
 | 
					    [client]
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const setVideoInput = useCallback(
 | 
					  const setVideoInput: (deviceId: string) => void = useCallback(
 | 
				
			||||||
    (deviceId) => {
 | 
					    (deviceId) => {
 | 
				
			||||||
      updateMediaPreferences({ videoInput: deviceId });
 | 
					      updateMediaPreferences({ videoInput: deviceId });
 | 
				
			||||||
      setState((prevState) => ({ ...prevState, videoInput: deviceId }));
 | 
					      setState((prevState) => ({ ...prevState, videoInput: deviceId }));
 | 
				
			||||||
| 
						 | 
					@ -177,12 +211,13 @@ export function MediaHandlerProvider({ client, children }) {
 | 
				
			||||||
    [client]
 | 
					    [client]
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const setAudioOutput = useCallback((deviceId) => {
 | 
					  const setAudioOutput: (deviceId: string) => void = useCallback((deviceId) => {
 | 
				
			||||||
    updateMediaPreferences({ audioOutput: deviceId });
 | 
					    updateMediaPreferences({ audioOutput: deviceId });
 | 
				
			||||||
    setState((prevState) => ({ ...prevState, audioOutput: deviceId }));
 | 
					    setState((prevState) => ({ ...prevState, audioOutput: deviceId }));
 | 
				
			||||||
  }, []);
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const context = useMemo(
 | 
					  const context: MediaHandlerContextInterface =
 | 
				
			||||||
 | 
					    useMemo<MediaHandlerContextInterface>(
 | 
				
			||||||
      () => ({
 | 
					      () => ({
 | 
				
			||||||
        audioInput,
 | 
					        audioInput,
 | 
				
			||||||
        audioInputs,
 | 
					        audioInputs,
 | 
				
			||||||
| 
						 | 
					@ -58,8 +58,12 @@ export const usePTTSounds = (): PTTSounds => {
 | 
				
			||||||
          break;
 | 
					          break;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      if (ref.current) {
 | 
					      if (ref.current) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
          ref.current.currentTime = 0;
 | 
					          ref.current.currentTime = 0;
 | 
				
			||||||
          await ref.current.play();
 | 
					          await ref.current.play();
 | 
				
			||||||
 | 
					        } catch (e) {
 | 
				
			||||||
 | 
					          console.log("Couldn't play sound effect", e);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        console.log("No media element found");
 | 
					        console.log("No media element found");
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,6 +16,10 @@ limitations under the License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { useRef, useEffect, RefObject } from "react";
 | 
					import { useRef, useEffect, RefObject } from "react";
 | 
				
			||||||
import { parse as parseSdp, write as writeSdp } from "sdp-transform";
 | 
					import { parse as parseSdp, write as writeSdp } from "sdp-transform";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  acquireContext,
 | 
				
			||||||
 | 
					  releaseContext,
 | 
				
			||||||
 | 
					} from "matrix-js-sdk/src/webrtc/audioContext";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { useSpatialAudio } from "../settings/useSetting";
 | 
					import { useSpatialAudio } from "../settings/useSetting";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -68,9 +72,15 @@ export const useMediaStream = (
 | 
				
			||||||
      audioOutputDevice &&
 | 
					      audioOutputDevice &&
 | 
				
			||||||
      mediaRef.current !== undefined
 | 
					      mediaRef.current !== undefined
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
      console.log(`useMediaStream setSinkId ${audioOutputDevice}`);
 | 
					      if (mediaRef.current.setSinkId) {
 | 
				
			||||||
 | 
					        console.log(
 | 
				
			||||||
 | 
					          `useMediaStream setting output setSinkId ${audioOutputDevice}`
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
        // Chrome for Android doesn't support this
 | 
					        // Chrome for Android doesn't support this
 | 
				
			||||||
      mediaRef.current.setSinkId?.(audioOutputDevice);
 | 
					        mediaRef.current.setSinkId(audioOutputDevice);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        console.log("Can't set output - no setsinkid");
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }, [audioOutputDevice]);
 | 
					  }, [audioOutputDevice]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -146,7 +156,7 @@ export const useAudioContext = (): [
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    if (audioRef.current && !context.current) {
 | 
					    if (audioRef.current && !context.current) {
 | 
				
			||||||
      context.current = new AudioContext();
 | 
					      context.current = acquireContext();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (window.chrome) {
 | 
					      if (window.chrome) {
 | 
				
			||||||
        // We're in Chrome, which needs a loopback hack applied to enable AEC
 | 
					        // We're in Chrome, which needs a loopback hack applied to enable AEC
 | 
				
			||||||
| 
						 | 
					@ -160,9 +170,11 @@ export const useAudioContext = (): [
 | 
				
			||||||
        })();
 | 
					        })();
 | 
				
			||||||
        return () => {
 | 
					        return () => {
 | 
				
			||||||
          audioEl.srcObject = null;
 | 
					          audioEl.srcObject = null;
 | 
				
			||||||
 | 
					          releaseContext();
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        destination.current = context.current.destination;
 | 
					        destination.current = context.current.destination;
 | 
				
			||||||
 | 
					        return releaseContext;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }, []);
 | 
					  }, []);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue