Enable users to join calls from multiple devices
This commit is contained in:
		
					parent
					
						
							
								46e429c37b
							
						
					
				
			
			
				commit
				
					
						13def24f7e
					
				
			
		
					 11 changed files with 199 additions and 216 deletions
				
			
		| 
						 | 
				
			
			@ -45,7 +45,7 @@
 | 
			
		|||
    "i18next": "^21.10.0",
 | 
			
		||||
    "i18next-browser-languagedetector": "^6.1.8",
 | 
			
		||||
    "i18next-http-backend": "^1.4.4",
 | 
			
		||||
    "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#3f1c3392d45b0fc054c3788cc6c043cd5b4fb730",
 | 
			
		||||
    "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#f46ecf970c658ae34b8d5fc3e73369c31ac79e90",
 | 
			
		||||
    "matrix-widget-api": "^1.0.0",
 | 
			
		||||
    "mermaid": "^8.13.8",
 | 
			
		||||
    "normalize.css": "^8.0.1",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,8 +25,7 @@ import React, {
 | 
			
		|||
  useRef,
 | 
			
		||||
} from "react";
 | 
			
		||||
import { useHistory } from "react-router-dom";
 | 
			
		||||
import { MatrixClient, ClientEvent } from "matrix-js-sdk/src/client";
 | 
			
		||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
 | 
			
		||||
import { MatrixClient } from "matrix-js-sdk/src/client";
 | 
			
		||||
import { logger } from "matrix-js-sdk/src/logger";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -40,6 +39,7 @@ import {
 | 
			
		|||
import { widget } from "./widget";
 | 
			
		||||
import { PosthogAnalytics, RegistrationType } from "./PosthogAnalytics";
 | 
			
		||||
import { translatedError } from "./TranslatedError";
 | 
			
		||||
import { useEventTarget } from "./useEvents";
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
  interface Window {
 | 
			
		||||
| 
						 | 
				
			
			@ -55,6 +55,8 @@ export interface Session {
 | 
			
		|||
  tempPassword?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const loadChannel = new BroadcastChannel("load");
 | 
			
		||||
 | 
			
		||||
const loadSession = (): Session => {
 | 
			
		||||
  const data = localStorage.getItem("matrix-auth-store");
 | 
			
		||||
  if (data) return JSON.parse(data);
 | 
			
		||||
| 
						 | 
				
			
			@ -292,47 +294,29 @@ export const ClientProvider: FC<Props> = ({ children }) => {
 | 
			
		|||
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
 | 
			
		||||
  // To protect against multiple sessions writing to the same storage
 | 
			
		||||
  // simultaneously, we send a broadcast message that shuts down all other
 | 
			
		||||
  // running instances of the app. This isn't necessary if the app is running in
 | 
			
		||||
  // a widget though, since then it'll be mostly stateless.
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    // To protect against multiple sessions writing to the same storage
 | 
			
		||||
    // simultaneously, we send a to-device message that shuts down all other
 | 
			
		||||
    // running instances of the app. This isn't necessary if the app is running
 | 
			
		||||
    // in a widget though, since then it'll be mostly stateless.
 | 
			
		||||
    if (!widget && client) {
 | 
			
		||||
      const loadTime = Date.now();
 | 
			
		||||
    if (!widget) loadChannel.postMessage({});
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
      const onToDeviceEvent = (event: MatrixEvent) => {
 | 
			
		||||
        if (event.getType() !== "org.matrix.call_duplicate_session") return;
 | 
			
		||||
  useEventTarget(
 | 
			
		||||
    loadChannel,
 | 
			
		||||
    "message",
 | 
			
		||||
    useCallback(() => {
 | 
			
		||||
      client?.stopClient();
 | 
			
		||||
 | 
			
		||||
        const content = event.getContent();
 | 
			
		||||
 | 
			
		||||
        if (content.session_id === client.getSessionId()) return;
 | 
			
		||||
 | 
			
		||||
        if (content.timestamp > loadTime) {
 | 
			
		||||
          client?.stopClient();
 | 
			
		||||
 | 
			
		||||
          setState((prev) => ({
 | 
			
		||||
            ...prev,
 | 
			
		||||
            error: translatedError(
 | 
			
		||||
              "This application has been opened in another tab.",
 | 
			
		||||
              t
 | 
			
		||||
            ),
 | 
			
		||||
          }));
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      client.on(ClientEvent.ToDeviceEvent, onToDeviceEvent);
 | 
			
		||||
 | 
			
		||||
      client.sendToDevice("org.matrix.call_duplicate_session", {
 | 
			
		||||
        [client.getUserId()]: {
 | 
			
		||||
          "*": { session_id: client.getSessionId(), timestamp: loadTime },
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return () => {
 | 
			
		||||
        client?.removeListener(ClientEvent.ToDeviceEvent, onToDeviceEvent);
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
  }, [client, t]);
 | 
			
		||||
      setState((prev) => ({
 | 
			
		||||
        ...prev,
 | 
			
		||||
        error: translatedError(
 | 
			
		||||
          "This application has been opened in another tab.",
 | 
			
		||||
          t
 | 
			
		||||
        ),
 | 
			
		||||
      }));
 | 
			
		||||
    }, [client, setState, t])
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const context = useMemo<ClientState>(
 | 
			
		||||
    () => ({
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -32,7 +32,7 @@ const overlapMap: Partial<Record<Size, number>> = {
 | 
			
		|||
interface Props extends HTMLAttributes<HTMLDivElement> {
 | 
			
		||||
  className: string;
 | 
			
		||||
  client: MatrixClient;
 | 
			
		||||
  participants: RoomMember[];
 | 
			
		||||
  members: RoomMember[];
 | 
			
		||||
  max?: number;
 | 
			
		||||
  size?: Size;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -40,7 +40,7 @@ interface Props extends HTMLAttributes<HTMLDivElement> {
 | 
			
		|||
export function Facepile({
 | 
			
		||||
  className,
 | 
			
		||||
  client,
 | 
			
		||||
  participants,
 | 
			
		||||
  members,
 | 
			
		||||
  max = 3,
 | 
			
		||||
  size = Size.XS,
 | 
			
		||||
  ...rest
 | 
			
		||||
| 
						 | 
				
			
			@ -51,14 +51,14 @@ export function Facepile({
 | 
			
		|||
  const _overlap = overlapMap[size];
 | 
			
		||||
 | 
			
		||||
  const title = useMemo(() => {
 | 
			
		||||
    return participants.reduce<string | null>(
 | 
			
		||||
    return members.reduce<string | null>(
 | 
			
		||||
      (prev, curr) =>
 | 
			
		||||
        prev === null
 | 
			
		||||
          ? curr.name
 | 
			
		||||
          : t("{{names}}, {{name}}", { names: prev, name: curr.name }),
 | 
			
		||||
      null
 | 
			
		||||
    ) as string;
 | 
			
		||||
  }, [participants, t]);
 | 
			
		||||
  }, [members, t]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
| 
						 | 
				
			
			@ -66,12 +66,11 @@ export function Facepile({
 | 
			
		|||
      title={title}
 | 
			
		||||
      style={{
 | 
			
		||||
        width:
 | 
			
		||||
          Math.min(participants.length, max + 1) * (_size - _overlap) +
 | 
			
		||||
          _overlap,
 | 
			
		||||
          Math.min(members.length, max + 1) * (_size - _overlap) + _overlap,
 | 
			
		||||
      }}
 | 
			
		||||
      {...rest}
 | 
			
		||||
    >
 | 
			
		||||
      {participants.slice(0, max).map((member, i) => {
 | 
			
		||||
      {members.slice(0, max).map((member, i) => {
 | 
			
		||||
        const avatarUrl = member.getMxcAvatarUrl();
 | 
			
		||||
        return (
 | 
			
		||||
          <Avatar
 | 
			
		||||
| 
						 | 
				
			
			@ -84,11 +83,11 @@ export function Facepile({
 | 
			
		|||
          />
 | 
			
		||||
        );
 | 
			
		||||
      })}
 | 
			
		||||
      {participants.length > max && (
 | 
			
		||||
      {members.length > max && (
 | 
			
		||||
        <Avatar
 | 
			
		||||
          key="additional"
 | 
			
		||||
          size={size}
 | 
			
		||||
          fallback={`+${participants.length - max}`}
 | 
			
		||||
          fallback={`+${members.length - max}`}
 | 
			
		||||
          className={styles.avatar}
 | 
			
		||||
          style={{ left: max * (_size - _overlap) }}
 | 
			
		||||
        />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -92,7 +92,7 @@ function CallTile({
 | 
			
		|||
            <Facepile
 | 
			
		||||
              className={styles.facePile}
 | 
			
		||||
              client={client}
 | 
			
		||||
              participants={participants}
 | 
			
		||||
              members={participants}
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -79,7 +79,6 @@ export function GroupCallView({
 | 
			
		|||
    isScreensharing,
 | 
			
		||||
    screenshareFeeds,
 | 
			
		||||
    participants,
 | 
			
		||||
    calls,
 | 
			
		||||
    unencryptedEventsFromUsers,
 | 
			
		||||
  } = useGroupCall(groupCall);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -173,9 +172,14 @@ export function GroupCallView({
 | 
			
		|||
  const onLeave = useCallback(() => {
 | 
			
		||||
    setLeft(true);
 | 
			
		||||
 | 
			
		||||
    let participantCount = 0;
 | 
			
		||||
    for (const deviceMap of groupCall.participants.values()) {
 | 
			
		||||
      participantCount += deviceMap.size;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    PosthogAnalytics.instance.eventCallEnded.track(
 | 
			
		||||
      groupCall.room.name,
 | 
			
		||||
      groupCall.participants.length
 | 
			
		||||
      participantCount
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    leave();
 | 
			
		||||
| 
						 | 
				
			
			@ -187,14 +191,7 @@ export function GroupCallView({
 | 
			
		|||
    if (!isPasswordlessUser && !isEmbedded) {
 | 
			
		||||
      history.push("/");
 | 
			
		||||
    }
 | 
			
		||||
  }, [
 | 
			
		||||
    groupCall.room.name,
 | 
			
		||||
    groupCall.participants.length,
 | 
			
		||||
    leave,
 | 
			
		||||
    isPasswordlessUser,
 | 
			
		||||
    isEmbedded,
 | 
			
		||||
    history,
 | 
			
		||||
  ]);
 | 
			
		||||
  }, [groupCall, leave, isPasswordlessUser, isEmbedded, history]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (widget && state === GroupCallState.Entered) {
 | 
			
		||||
| 
						 | 
				
			
			@ -236,7 +233,6 @@ export function GroupCallView({
 | 
			
		|||
          roomName={groupCall.room.name}
 | 
			
		||||
          avatarUrl={avatarUrl}
 | 
			
		||||
          participants={participants}
 | 
			
		||||
          calls={calls}
 | 
			
		||||
          microphoneMuted={microphoneMuted}
 | 
			
		||||
          localVideoMuted={localVideoMuted}
 | 
			
		||||
          toggleLocalVideoMuted={toggleLocalVideoMuted}
 | 
			
		||||
| 
						 | 
				
			
			@ -253,12 +249,6 @@ export function GroupCallView({
 | 
			
		|||
        />
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  } else if (state === GroupCallState.Entering) {
 | 
			
		||||
    return (
 | 
			
		||||
      <FullScreenView>
 | 
			
		||||
        <h1>{t("Entering room…")}</h1>
 | 
			
		||||
      </FullScreenView>
 | 
			
		||||
    );
 | 
			
		||||
  } else if (left) {
 | 
			
		||||
    if (isPasswordlessUser) {
 | 
			
		||||
      return <CallEndedView client={client} />;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,13 +14,7 @@ See the License for the specific language governing permissions and
 | 
			
		|||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
import React, {
 | 
			
		||||
  useEffect,
 | 
			
		||||
  useCallback,
 | 
			
		||||
  useMemo,
 | 
			
		||||
  useRef,
 | 
			
		||||
  useState,
 | 
			
		||||
} from "react";
 | 
			
		||||
import React, { useEffect, useCallback, useMemo, useRef } from "react";
 | 
			
		||||
import { usePreventScroll } from "@react-aria/overlays";
 | 
			
		||||
import useMeasure from "react-use-measure";
 | 
			
		||||
import { ResizeObserver } from "@juggle/resize-observer";
 | 
			
		||||
| 
						 | 
				
			
			@ -31,11 +25,6 @@ import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
 | 
			
		|||
import classNames from "classnames";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
 | 
			
		||||
import {
 | 
			
		||||
  CallEvent,
 | 
			
		||||
  CallState,
 | 
			
		||||
  MatrixCall,
 | 
			
		||||
} from "matrix-js-sdk/src/webrtc/call";
 | 
			
		||||
 | 
			
		||||
import type { IWidgetApiRequest } from "matrix-widget-api";
 | 
			
		||||
import styles from "./InCallView.module.css";
 | 
			
		||||
| 
						 | 
				
			
			@ -73,6 +62,7 @@ import { widget, ElementWidgetActions } from "../widget";
 | 
			
		|||
import { useJoinRule } from "./useJoinRule";
 | 
			
		||||
import { useUrlParams } from "../UrlParams";
 | 
			
		||||
import { usePrefersReducedMotion } from "../usePrefersReducedMotion";
 | 
			
		||||
import { ConnectionState, ParticipantInfo } from "./useGroupCall";
 | 
			
		||||
 | 
			
		||||
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
 | 
			
		||||
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
 | 
			
		||||
| 
						 | 
				
			
			@ -83,8 +73,7 @@ const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
 | 
			
		|||
interface Props {
 | 
			
		||||
  client: MatrixClient;
 | 
			
		||||
  groupCall: GroupCall;
 | 
			
		||||
  participants: RoomMember[];
 | 
			
		||||
  calls: MatrixCall[];
 | 
			
		||||
  participants: Map<RoomMember, Map<string, ParticipantInfo>>;
 | 
			
		||||
  roomName: string;
 | 
			
		||||
  avatarUrl: string;
 | 
			
		||||
  microphoneMuted: boolean;
 | 
			
		||||
| 
						 | 
				
			
			@ -93,7 +82,7 @@ interface Props {
 | 
			
		|||
  toggleMicrophoneMuted: () => void;
 | 
			
		||||
  toggleScreensharing: () => void;
 | 
			
		||||
  userMediaFeeds: CallFeed[];
 | 
			
		||||
  activeSpeaker: string;
 | 
			
		||||
  activeSpeaker: CallFeed | null;
 | 
			
		||||
  onLeave: () => void;
 | 
			
		||||
  isScreensharing: boolean;
 | 
			
		||||
  screenshareFeeds: CallFeed[];
 | 
			
		||||
| 
						 | 
				
			
			@ -102,12 +91,6 @@ interface Props {
 | 
			
		|||
  hideHeader: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum ConnectionState {
 | 
			
		||||
  EstablishingCall = "establishing call", // call hasn't been established yet
 | 
			
		||||
  WaitMedia = "wait_media", // call is set up, waiting for ICE to connect
 | 
			
		||||
  Connected = "connected", // media is flowing
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Represents something that should get a tile on the layout,
 | 
			
		||||
// ie. a user's video feed or a screen share feed.
 | 
			
		||||
export interface TileDescriptor {
 | 
			
		||||
| 
						 | 
				
			
			@ -124,7 +107,6 @@ export function InCallView({
 | 
			
		|||
  client,
 | 
			
		||||
  groupCall,
 | 
			
		||||
  participants,
 | 
			
		||||
  calls,
 | 
			
		||||
  roomName,
 | 
			
		||||
  avatarUrl,
 | 
			
		||||
  microphoneMuted,
 | 
			
		||||
| 
						 | 
				
			
			@ -174,50 +156,6 @@ export function InCallView({
 | 
			
		|||
 | 
			
		||||
  const { hideScreensharing } = useUrlParams();
 | 
			
		||||
 | 
			
		||||
  const makeConnectionStatesMap = useCallback(() => {
 | 
			
		||||
    const newConnStates = new Map<string, ConnectionState>();
 | 
			
		||||
    for (const participant of participants) {
 | 
			
		||||
      const userCall = groupCall.getCallByUserId(participant.userId);
 | 
			
		||||
      const feed = userMediaFeeds.find((f) => f.userId === participant.userId);
 | 
			
		||||
      let connectionState = ConnectionState.EstablishingCall;
 | 
			
		||||
      if (feed && feed.isLocal()) {
 | 
			
		||||
        connectionState = ConnectionState.Connected;
 | 
			
		||||
      } else if (userCall) {
 | 
			
		||||
        if (userCall.state === CallState.Connected) {
 | 
			
		||||
          connectionState = ConnectionState.Connected;
 | 
			
		||||
        } else if (userCall.state === CallState.Connecting) {
 | 
			
		||||
          connectionState = ConnectionState.WaitMedia;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      newConnStates.set(participant.userId, connectionState);
 | 
			
		||||
    }
 | 
			
		||||
    return newConnStates;
 | 
			
		||||
  }, [groupCall, participants, userMediaFeeds]);
 | 
			
		||||
 | 
			
		||||
  const [connStates, setConnStates] = useState(
 | 
			
		||||
    new Map<string, ConnectionState>()
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const updateConnectionStates = useCallback(() => {
 | 
			
		||||
    setConnStates(makeConnectionStatesMap());
 | 
			
		||||
  }, [setConnStates, makeConnectionStatesMap]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    for (const call of calls) {
 | 
			
		||||
      call.on(CallEvent.State, updateConnectionStates);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      for (const call of calls) {
 | 
			
		||||
        call.off(CallEvent.State, updateConnectionStates);
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
  }, [calls, updateConnectionStates]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    updateConnectionStates();
 | 
			
		||||
  }, [participants, updateConnectionStates]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    widget?.api.transport.send(
 | 
			
		||||
      layout === "freedom"
 | 
			
		||||
| 
						 | 
				
			
			@ -256,59 +194,57 @@ export function InCallView({
 | 
			
		|||
 | 
			
		||||
  const items = useMemo(() => {
 | 
			
		||||
    const tileDescriptors: TileDescriptor[] = [];
 | 
			
		||||
    const localUserId = client.getUserId()!;
 | 
			
		||||
    const localDeviceId = client.getDeviceId()!;
 | 
			
		||||
 | 
			
		||||
    // one tile for each participants, to start with (we want a tile for everyone we
 | 
			
		||||
    // think should be in the call, even if we don't have a media feed for them yet)
 | 
			
		||||
    for (const p of participants) {
 | 
			
		||||
      const userMediaFeed = userMediaFeeds.find((f) => f.userId === p.userId);
 | 
			
		||||
    // One tile for each participant, to start with (we want a tile for everyone we
 | 
			
		||||
    // think should be in the call, even if we don't have a call feed for them yet)
 | 
			
		||||
    for (const [member, participantMap] of participants) {
 | 
			
		||||
      for (const [deviceId, { connectionState, presenter }] of participantMap) {
 | 
			
		||||
        const callFeed = userMediaFeeds.find(
 | 
			
		||||
          (f) => f.userId === member.userId && f.deviceId === deviceId
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
      // NB. this assumes that the same user can't join more than once from multiple
 | 
			
		||||
      // devices, but the participants are just RoomMembers, so this assumption is baked
 | 
			
		||||
      // into GroupCall itself.
 | 
			
		||||
      tileDescriptors.push({
 | 
			
		||||
        id: p.userId,
 | 
			
		||||
        member: p,
 | 
			
		||||
        callFeed: userMediaFeed,
 | 
			
		||||
        focused: screenshareFeeds.length === 0 && p.userId === activeSpeaker,
 | 
			
		||||
        isLocal: p.userId === client.getUserId(),
 | 
			
		||||
        presenter: false,
 | 
			
		||||
        connectionState: connStates.get(p.userId),
 | 
			
		||||
      });
 | 
			
		||||
        tileDescriptors.push({
 | 
			
		||||
          id: `${member.userId} ${deviceId}`,
 | 
			
		||||
          member,
 | 
			
		||||
          callFeed,
 | 
			
		||||
          focused: screenshareFeeds.length === 0 && callFeed === activeSpeaker,
 | 
			
		||||
          isLocal: member.userId === localUserId && deviceId === localDeviceId,
 | 
			
		||||
          presenter,
 | 
			
		||||
          connectionState,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    PosthogAnalytics.instance.eventCallEnded.cacheParticipantCountChanged(
 | 
			
		||||
      participants.length
 | 
			
		||||
      tileDescriptors.length
 | 
			
		||||
    );
 | 
			
		||||
    // add the screenshares too
 | 
			
		||||
 | 
			
		||||
    // Add the screenshares too
 | 
			
		||||
    for (const screenshareFeed of screenshareFeeds) {
 | 
			
		||||
      const userMediaItem = tileDescriptors.find(
 | 
			
		||||
        (item) => item.member.userId === screenshareFeed.userId
 | 
			
		||||
      );
 | 
			
		||||
      const member = screenshareFeed.getMember()!;
 | 
			
		||||
      const connectionState = participants
 | 
			
		||||
        .get(member)
 | 
			
		||||
        ?.get(screenshareFeed.deviceId!)?.connectionState;
 | 
			
		||||
 | 
			
		||||
      if (userMediaItem) {
 | 
			
		||||
        userMediaItem.presenter = true;
 | 
			
		||||
      // If the participant has left, their screenshare feed is stale and we
 | 
			
		||||
      // shouldn't bother showing it
 | 
			
		||||
      if (connectionState !== undefined) {
 | 
			
		||||
        tileDescriptors.push({
 | 
			
		||||
          id: screenshareFeed.stream.id,
 | 
			
		||||
          member,
 | 
			
		||||
          callFeed: screenshareFeed,
 | 
			
		||||
          focused: true,
 | 
			
		||||
          isLocal: screenshareFeed.isLocal(),
 | 
			
		||||
          presenter: false,
 | 
			
		||||
          connectionState,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      tileDescriptors.push({
 | 
			
		||||
        id: screenshareFeed.stream.id,
 | 
			
		||||
        member: screenshareFeed.getMember()!,
 | 
			
		||||
        callFeed: screenshareFeed,
 | 
			
		||||
        focused: true,
 | 
			
		||||
        isLocal: screenshareFeed.isLocal(),
 | 
			
		||||
        presenter: false,
 | 
			
		||||
        connectionState: connStates.get(screenshareFeed.userId),
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return tileDescriptors;
 | 
			
		||||
  }, [
 | 
			
		||||
    client,
 | 
			
		||||
    participants,
 | 
			
		||||
    userMediaFeeds,
 | 
			
		||||
    activeSpeaker,
 | 
			
		||||
    screenshareFeeds,
 | 
			
		||||
    connStates,
 | 
			
		||||
  ]);
 | 
			
		||||
  }, [client, participants, userMediaFeeds, activeSpeaker, screenshareFeeds]);
 | 
			
		||||
 | 
			
		||||
  // The maximised participant: either the participant that the user has
 | 
			
		||||
  // manually put in fullscreen, or the focused (active) participant if the
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 | 
			
		|||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
import React, { useEffect } from "react";
 | 
			
		||||
import React, { useEffect, useMemo } from "react";
 | 
			
		||||
import useMeasure from "react-use-measure";
 | 
			
		||||
import { ResizeObserver } from "@juggle/resize-observer";
 | 
			
		||||
import i18n from "i18next";
 | 
			
		||||
| 
						 | 
				
			
			@ -43,6 +43,7 @@ import { PTTClips } from "../sound/PTTClips";
 | 
			
		|||
import { GroupCallInspector } from "./GroupCallInspector";
 | 
			
		||||
import { OverflowMenu } from "./OverflowMenu";
 | 
			
		||||
import { Size } from "../Avatar";
 | 
			
		||||
import { ParticipantInfo } from "./useGroupCall";
 | 
			
		||||
 | 
			
		||||
function getPromptText(
 | 
			
		||||
  networkWaiting: boolean,
 | 
			
		||||
| 
						 | 
				
			
			@ -100,7 +101,7 @@ interface Props {
 | 
			
		|||
  roomName: string;
 | 
			
		||||
  avatarUrl: string;
 | 
			
		||||
  groupCall: GroupCall;
 | 
			
		||||
  participants: RoomMember[];
 | 
			
		||||
  participants: Map<RoomMember, Map<string, ParticipantInfo>>;
 | 
			
		||||
  userMediaFeeds: CallFeed[];
 | 
			
		||||
  onLeave: () => void;
 | 
			
		||||
  isEmbedded: boolean;
 | 
			
		||||
| 
						 | 
				
			
			@ -152,6 +153,15 @@ export const PTTCallView: React.FC<Props> = ({
 | 
			
		|||
    connected,
 | 
			
		||||
  } = usePTT(client, groupCall, userMediaFeeds, playClip);
 | 
			
		||||
 | 
			
		||||
  const participatingMembers = useMemo(() => {
 | 
			
		||||
    const members: RoomMember[] = [];
 | 
			
		||||
    for (const [member, deviceMap] of participants) {
 | 
			
		||||
      // Repeat the member for as many devices as they're using
 | 
			
		||||
      for (let i = 0; i < deviceMap.size; i++) members.push(member);
 | 
			
		||||
    }
 | 
			
		||||
    return members;
 | 
			
		||||
  }, [participants]);
 | 
			
		||||
 | 
			
		||||
  const [talkingExpected, enqueueTalkingExpected, setTalkingExpected] =
 | 
			
		||||
    useDelayedState(false);
 | 
			
		||||
  const showTalkOverError = pttButtonHeld && transmitBlocked;
 | 
			
		||||
| 
						 | 
				
			
			@ -205,7 +215,7 @@ export const PTTCallView: React.FC<Props> = ({
 | 
			
		|||
            <div className={styles.participants}>
 | 
			
		||||
              <p>
 | 
			
		||||
                {t("{{count}} people connected", {
 | 
			
		||||
                  count: participants.length,
 | 
			
		||||
                  count: participatingMembers.length,
 | 
			
		||||
                })}
 | 
			
		||||
              </p>
 | 
			
		||||
              <Facepile
 | 
			
		||||
| 
						 | 
				
			
			@ -213,7 +223,7 @@ export const PTTCallView: React.FC<Props> = ({
 | 
			
		|||
                max={8}
 | 
			
		||||
                className={styles.facepile}
 | 
			
		||||
                client={client}
 | 
			
		||||
                participants={participants}
 | 
			
		||||
                members={participatingMembers}
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className={styles.footer}>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,7 +23,11 @@ import {
 | 
			
		|||
  GroupCallUnknownDeviceError,
 | 
			
		||||
  GroupCallError,
 | 
			
		||||
} from "matrix-js-sdk/src/webrtc/groupCall";
 | 
			
		||||
import { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
 | 
			
		||||
import {
 | 
			
		||||
  CallState,
 | 
			
		||||
  MatrixCall,
 | 
			
		||||
  CallEvent,
 | 
			
		||||
} from "matrix-js-sdk/src/webrtc/call";
 | 
			
		||||
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
 | 
			
		||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
| 
						 | 
				
			
			@ -36,11 +40,21 @@ import { ElementWidgetActions, ScreenshareStartData, widget } from "../widget";
 | 
			
		|||
import { getSetting } from "../settings/useSetting";
 | 
			
		||||
import { useEventTarget } from "../useEvents";
 | 
			
		||||
 | 
			
		||||
export enum ConnectionState {
 | 
			
		||||
  EstablishingCall = "establishing call", // call hasn't been established yet
 | 
			
		||||
  WaitMedia = "wait_media", // call is set up, waiting for ICE to connect
 | 
			
		||||
  Connected = "connected", // media is flowing
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ParticipantInfo {
 | 
			
		||||
  connectionState: ConnectionState;
 | 
			
		||||
  presenter: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface UseGroupCallReturnType {
 | 
			
		||||
  state: GroupCallState;
 | 
			
		||||
  calls: MatrixCall[];
 | 
			
		||||
  localCallFeed: CallFeed;
 | 
			
		||||
  activeSpeaker: string;
 | 
			
		||||
  activeSpeaker: CallFeed | null;
 | 
			
		||||
  userMediaFeeds: CallFeed[];
 | 
			
		||||
  microphoneMuted: boolean;
 | 
			
		||||
  localVideoMuted: boolean;
 | 
			
		||||
| 
						 | 
				
			
			@ -55,16 +69,15 @@ export interface UseGroupCallReturnType {
 | 
			
		|||
  isScreensharing: boolean;
 | 
			
		||||
  screenshareFeeds: CallFeed[];
 | 
			
		||||
  localDesktopCapturerSourceId: string; // XXX: This looks unused?
 | 
			
		||||
  participants: RoomMember[];
 | 
			
		||||
  participants: Map<RoomMember, Map<string, ParticipantInfo>>;
 | 
			
		||||
  hasLocalParticipant: boolean;
 | 
			
		||||
  unencryptedEventsFromUsers: Set<string>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface State {
 | 
			
		||||
  state: GroupCallState;
 | 
			
		||||
  calls: MatrixCall[];
 | 
			
		||||
  localCallFeed: CallFeed;
 | 
			
		||||
  activeSpeaker: string;
 | 
			
		||||
  activeSpeaker: CallFeed | null;
 | 
			
		||||
  userMediaFeeds: CallFeed[];
 | 
			
		||||
  error: TranslatedError | null;
 | 
			
		||||
  microphoneMuted: boolean;
 | 
			
		||||
| 
						 | 
				
			
			@ -73,15 +86,51 @@ interface State {
 | 
			
		|||
  localDesktopCapturerSourceId: string;
 | 
			
		||||
  isScreensharing: boolean;
 | 
			
		||||
  requestingScreenshare: boolean;
 | 
			
		||||
  participants: RoomMember[];
 | 
			
		||||
  participants: Map<RoomMember, Map<string, ParticipantInfo>>;
 | 
			
		||||
  hasLocalParticipant: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getParticipants(
 | 
			
		||||
  groupCall: GroupCall
 | 
			
		||||
): Map<RoomMember, Map<string, ParticipantInfo>> {
 | 
			
		||||
  const participants = new Map<RoomMember, Map<string, ParticipantInfo>>();
 | 
			
		||||
 | 
			
		||||
  for (const [member, participantsStateMap] of groupCall.participants) {
 | 
			
		||||
    const callMap = groupCall.calls.get(member);
 | 
			
		||||
    const participantInfoMap = new Map<string, ParticipantInfo>();
 | 
			
		||||
    participants.set(member, participantInfoMap);
 | 
			
		||||
 | 
			
		||||
    for (const [deviceId, participant] of participantsStateMap) {
 | 
			
		||||
      const call = callMap?.get(deviceId);
 | 
			
		||||
      const feed = groupCall.userMediaFeeds.find(
 | 
			
		||||
        (f) => f.userId === member.userId && f.deviceId === deviceId
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      let connectionState = ConnectionState.EstablishingCall;
 | 
			
		||||
      if (feed?.isLocal()) {
 | 
			
		||||
        connectionState = ConnectionState.Connected;
 | 
			
		||||
      } else if (call !== undefined) {
 | 
			
		||||
        if (call.state === CallState.Connected) {
 | 
			
		||||
          connectionState = ConnectionState.Connected;
 | 
			
		||||
        } else if (call.state === CallState.Connecting) {
 | 
			
		||||
          connectionState = ConnectionState.WaitMedia;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      participantInfoMap.set(deviceId, {
 | 
			
		||||
        connectionState,
 | 
			
		||||
        presenter: participant.screensharing,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return participants;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
 | 
			
		||||
  const [
 | 
			
		||||
    {
 | 
			
		||||
      state,
 | 
			
		||||
      calls,
 | 
			
		||||
      localCallFeed,
 | 
			
		||||
      activeSpeaker,
 | 
			
		||||
      userMediaFeeds,
 | 
			
		||||
| 
						 | 
				
			
			@ -98,7 +147,6 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
 | 
			
		|||
    setState,
 | 
			
		||||
  ] = useState<State>({
 | 
			
		||||
    state: GroupCallState.LocalCallFeedUninitialized,
 | 
			
		||||
    calls: [],
 | 
			
		||||
    localCallFeed: null,
 | 
			
		||||
    activeSpeaker: null,
 | 
			
		||||
    userMediaFeeds: [],
 | 
			
		||||
| 
						 | 
				
			
			@ -109,7 +157,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
 | 
			
		|||
    screenshareFeeds: [],
 | 
			
		||||
    localDesktopCapturerSourceId: null,
 | 
			
		||||
    requestingScreenshare: false,
 | 
			
		||||
    participants: [],
 | 
			
		||||
    participants: new Map(),
 | 
			
		||||
    hasLocalParticipant: false,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -120,29 +168,30 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
 | 
			
		|||
    new Set<string>()
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const updateState = (state: Partial<State>) =>
 | 
			
		||||
    setState((prevState) => ({ ...prevState, ...state }));
 | 
			
		||||
  const updateState = useCallback(
 | 
			
		||||
    (state: Partial<State>) => setState((prev) => ({ ...prev, ...state })),
 | 
			
		||||
    [setState]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    function onGroupCallStateChanged() {
 | 
			
		||||
      updateState({
 | 
			
		||||
        state: groupCall.state,
 | 
			
		||||
        calls: [...groupCall.calls],
 | 
			
		||||
        localCallFeed: groupCall.localCallFeed,
 | 
			
		||||
        activeSpeaker: groupCall.activeSpeaker,
 | 
			
		||||
        activeSpeaker: groupCall.activeSpeaker ?? null,
 | 
			
		||||
        userMediaFeeds: [...groupCall.userMediaFeeds],
 | 
			
		||||
        microphoneMuted: groupCall.isMicrophoneMuted(),
 | 
			
		||||
        localVideoMuted: groupCall.isLocalVideoMuted(),
 | 
			
		||||
        isScreensharing: groupCall.isScreensharing(),
 | 
			
		||||
        localDesktopCapturerSourceId: groupCall.localDesktopCapturerSourceId,
 | 
			
		||||
        screenshareFeeds: [...groupCall.screenshareFeeds],
 | 
			
		||||
        participants: [...groupCall.participants],
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function onUserMediaFeedsChanged(userMediaFeeds: CallFeed[]): void {
 | 
			
		||||
      updateState({
 | 
			
		||||
        userMediaFeeds: [...userMediaFeeds],
 | 
			
		||||
        participants: getParticipants(groupCall),
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -152,9 +201,9 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
 | 
			
		|||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function onActiveSpeakerChanged(activeSpeaker: string): void {
 | 
			
		||||
    function onActiveSpeakerChanged(activeSpeaker: CallFeed | undefined): void {
 | 
			
		||||
      updateState({
 | 
			
		||||
        activeSpeaker: activeSpeaker,
 | 
			
		||||
        activeSpeaker: activeSpeaker ?? null,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -179,15 +228,31 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
 | 
			
		|||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function onCallsChanged(calls: MatrixCall[]): void {
 | 
			
		||||
      updateState({
 | 
			
		||||
        calls: [...calls],
 | 
			
		||||
      });
 | 
			
		||||
    const prevCalls = new Set<MatrixCall>();
 | 
			
		||||
 | 
			
		||||
    function onCallState(): void {
 | 
			
		||||
      updateState({ participants: getParticipants(groupCall) });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function onParticipantsChanged(participants: RoomMember[]): void {
 | 
			
		||||
    function onCallsChanged(
 | 
			
		||||
      calls: Map<RoomMember, Map<string, MatrixCall>>
 | 
			
		||||
    ): void {
 | 
			
		||||
      for (const call of prevCalls) call.off(CallEvent.State, onCallState);
 | 
			
		||||
      prevCalls.clear();
 | 
			
		||||
 | 
			
		||||
      for (const deviceMap of calls.values()) {
 | 
			
		||||
        for (const call of deviceMap.values()) {
 | 
			
		||||
          call.on(CallEvent.State, onCallState);
 | 
			
		||||
          prevCalls.add(call);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      updateState({ participants: getParticipants(groupCall) });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function onParticipantsChanged(): void {
 | 
			
		||||
      updateState({
 | 
			
		||||
        participants: [...participants],
 | 
			
		||||
        participants: getParticipants(groupCall),
 | 
			
		||||
        hasLocalParticipant: groupCall.hasLocalParticipant(),
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -218,16 +283,15 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
 | 
			
		|||
    updateState({
 | 
			
		||||
      error: null,
 | 
			
		||||
      state: groupCall.state,
 | 
			
		||||
      calls: [...groupCall.calls],
 | 
			
		||||
      localCallFeed: groupCall.localCallFeed,
 | 
			
		||||
      activeSpeaker: groupCall.activeSpeaker,
 | 
			
		||||
      activeSpeaker: groupCall.activeSpeaker ?? null,
 | 
			
		||||
      userMediaFeeds: [...groupCall.userMediaFeeds],
 | 
			
		||||
      microphoneMuted: groupCall.isMicrophoneMuted(),
 | 
			
		||||
      localVideoMuted: groupCall.isLocalVideoMuted(),
 | 
			
		||||
      isScreensharing: groupCall.isScreensharing(),
 | 
			
		||||
      localDesktopCapturerSourceId: groupCall.localDesktopCapturerSourceId,
 | 
			
		||||
      screenshareFeeds: [...groupCall.screenshareFeeds],
 | 
			
		||||
      participants: [...groupCall.participants],
 | 
			
		||||
      participants: getParticipants(groupCall),
 | 
			
		||||
      hasLocalParticipant: groupCall.hasLocalParticipant(),
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -264,7 +328,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
 | 
			
		|||
      groupCall.removeListener(GroupCallEvent.Error, onError);
 | 
			
		||||
      groupCall.leave();
 | 
			
		||||
    };
 | 
			
		||||
  }, [groupCall]);
 | 
			
		||||
  }, [groupCall, updateState]);
 | 
			
		||||
 | 
			
		||||
  usePageUnload(() => {
 | 
			
		||||
    groupCall.leave();
 | 
			
		||||
| 
						 | 
				
			
			@ -290,7 +354,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
 | 
			
		|||
      console.error(error);
 | 
			
		||||
      updateState({ error });
 | 
			
		||||
    });
 | 
			
		||||
  }, [groupCall]);
 | 
			
		||||
  }, [groupCall, updateState]);
 | 
			
		||||
 | 
			
		||||
  const leave = useCallback(() => groupCall.leave(), [groupCall]);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -341,7 +405,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
 | 
			
		|||
      // toggling off
 | 
			
		||||
      groupCall.setScreensharingEnabled(false);
 | 
			
		||||
    }
 | 
			
		||||
  }, [groupCall]);
 | 
			
		||||
  }, [groupCall, updateState]);
 | 
			
		||||
 | 
			
		||||
  const onScreenshareStart = useCallback(
 | 
			
		||||
    async (ev: CustomEvent<IWidgetApiRequest>) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -355,7 +419,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
 | 
			
		|||
      });
 | 
			
		||||
      await widget.api.transport.reply(ev.detail, {});
 | 
			
		||||
    },
 | 
			
		||||
    [groupCall]
 | 
			
		||||
    [groupCall, updateState]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const onScreenshareStop = useCallback(
 | 
			
		||||
| 
						 | 
				
			
			@ -364,7 +428,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
 | 
			
		|||
      await groupCall.setScreensharingEnabled(false);
 | 
			
		||||
      await widget.api.transport.reply(ev.detail, {});
 | 
			
		||||
    },
 | 
			
		||||
    [groupCall]
 | 
			
		||||
    [groupCall, updateState]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
| 
						 | 
				
			
			@ -402,7 +466,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
 | 
			
		|||
      console.error(error);
 | 
			
		||||
      updateState({ error });
 | 
			
		||||
    }
 | 
			
		||||
  }, [t]);
 | 
			
		||||
  }, [t, updateState]);
 | 
			
		||||
 | 
			
		||||
  const [spacebarHeld, setSpacebarHeld] = useState(false);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -468,7 +532,6 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
 | 
			
		|||
 | 
			
		||||
  return {
 | 
			
		||||
    state,
 | 
			
		||||
    calls,
 | 
			
		||||
    localCallFeed,
 | 
			
		||||
    activeSpeaker,
 | 
			
		||||
    userMediaFeeds,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,7 +21,8 @@ import { RoomMember } from "matrix-js-sdk";
 | 
			
		|||
import { VideoGrid, useVideoGridLayout } from "./VideoGrid";
 | 
			
		||||
import { VideoTile } from "./VideoTile";
 | 
			
		||||
import { Button } from "../button";
 | 
			
		||||
import { ConnectionState, TileDescriptor } from "../room/InCallView";
 | 
			
		||||
import { TileDescriptor } from "../room/InCallView";
 | 
			
		||||
import { ConnectionState } from "../room/useGroupCall";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  title: "VideoGrid",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,7 +23,7 @@ import styles from "./VideoTile.module.css";
 | 
			
		|||
import { ReactComponent as MicMutedIcon } from "../icons/MicMuted.svg";
 | 
			
		||||
import { ReactComponent as VideoMutedIcon } from "../icons/VideoMuted.svg";
 | 
			
		||||
import { AudioButton, FullscreenButton } from "../button/Button";
 | 
			
		||||
import { ConnectionState } from "../room/InCallView";
 | 
			
		||||
import { ConnectionState } from "../room/useGroupCall";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  name: string;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10196,9 +10196,9 @@ matrix-events-sdk@0.0.1:
 | 
			
		|||
  resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd"
 | 
			
		||||
  integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==
 | 
			
		||||
 | 
			
		||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#3f1c3392d45b0fc054c3788cc6c043cd5b4fb730":
 | 
			
		||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#f46ecf970c658ae34b8d5fc3e73369c31ac79e90":
 | 
			
		||||
  version "21.1.0"
 | 
			
		||||
  resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/3f1c3392d45b0fc054c3788cc6c043cd5b4fb730"
 | 
			
		||||
  resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/f46ecf970c658ae34b8d5fc3e73369c31ac79e90"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/runtime" "^7.12.5"
 | 
			
		||||
    "@types/sdp-transform" "^2.4.5"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue