From 13def24f7e68e9246d339c4e14d40e7298586be0 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 21 Nov 2022 12:39:48 -0500 Subject: [PATCH 1/3] Enable users to join calls from multiple devices --- package.json | 2 +- src/ClientContext.tsx | 64 +++++------- src/Facepile.tsx | 17 ++- src/home/CallList.tsx | 2 +- src/room/GroupCallView.tsx | 24 ++--- src/room/InCallView.tsx | 150 ++++++++------------------- src/room/PTTCallView.tsx | 18 +++- src/room/useGroupCall.ts | 129 +++++++++++++++++------ src/video-grid/VideoGrid.stories.tsx | 3 +- src/video-grid/VideoTile.tsx | 2 +- yarn.lock | 4 +- 11 files changed, 199 insertions(+), 216 deletions(-) diff --git a/package.json b/package.json index de7eec7e..750afc3b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/ClientContext.tsx b/src/ClientContext.tsx index 23d87a4d..8eba226f 100644 --- a/src/ClientContext.tsx +++ b/src/ClientContext.tsx @@ -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 = ({ 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( () => ({ diff --git a/src/Facepile.tsx b/src/Facepile.tsx index 86c5c6f5..79e27883 100644 --- a/src/Facepile.tsx +++ b/src/Facepile.tsx @@ -32,7 +32,7 @@ const overlapMap: Partial> = { interface Props extends HTMLAttributes { className: string; client: MatrixClient; - participants: RoomMember[]; + members: RoomMember[]; max?: number; size?: Size; } @@ -40,7 +40,7 @@ interface Props extends HTMLAttributes { 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( + return members.reduce( (prev, curr) => prev === null ? curr.name : t("{{names}}, {{name}}", { names: prev, name: curr.name }), null ) as string; - }, [participants, t]); + }, [members, t]); return (
- {participants.slice(0, max).map((member, i) => { + {members.slice(0, max).map((member, i) => { const avatarUrl = member.getMxcAvatarUrl(); return ( ); })} - {participants.length > max && ( + {members.length > max && ( diff --git a/src/home/CallList.tsx b/src/home/CallList.tsx index ddf63821..55819616 100644 --- a/src/home/CallList.tsx +++ b/src/home/CallList.tsx @@ -92,7 +92,7 @@ function CallTile({ )}
diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 7da620b3..b101b578 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -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 ( - -

{t("Entering room…")}

-
- ); } else if (left) { if (isPasswordlessUser) { return ; diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index a75da5f4..431d8b06 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -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>; 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(); - 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() - ); - - 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 diff --git a/src/room/PTTCallView.tsx b/src/room/PTTCallView.tsx index 922bd148..d1c41e0e 100644 --- a/src/room/PTTCallView.tsx +++ b/src/room/PTTCallView.tsx @@ -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>; userMediaFeeds: CallFeed[]; onLeave: () => void; isEmbedded: boolean; @@ -152,6 +153,15 @@ export const PTTCallView: React.FC = ({ 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 = ({

{t("{{count}} people connected", { - count: participants.length, + count: participatingMembers.length, })}

= ({ max={8} className={styles.facepile} client={client} - participants={participants} + members={participatingMembers} />
diff --git a/src/room/useGroupCall.ts b/src/room/useGroupCall.ts index 14e40d14..ec496ffe 100644 --- a/src/room/useGroupCall.ts +++ b/src/room/useGroupCall.ts @@ -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>; hasLocalParticipant: boolean; unencryptedEventsFromUsers: Set; } 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>; hasLocalParticipant: boolean; } +function getParticipants( + groupCall: GroupCall +): Map> { + const participants = new Map>(); + + for (const [member, participantsStateMap] of groupCall.participants) { + const callMap = groupCall.calls.get(member); + const participantInfoMap = new Map(); + 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: 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() ); - const updateState = (state: Partial) => - setState((prevState) => ({ ...prevState, ...state })); + const updateState = useCallback( + (state: Partial) => 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(); + + function onCallState(): void { + updateState({ participants: getParticipants(groupCall) }); } - function onParticipantsChanged(participants: RoomMember[]): void { + function onCallsChanged( + calls: Map> + ): 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) => { @@ -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, diff --git a/src/video-grid/VideoGrid.stories.tsx b/src/video-grid/VideoGrid.stories.tsx index fc3317c4..e73ff890 100644 --- a/src/video-grid/VideoGrid.stories.tsx +++ b/src/video-grid/VideoGrid.stories.tsx @@ -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", diff --git a/src/video-grid/VideoTile.tsx b/src/video-grid/VideoTile.tsx index 365d88e5..a7792c6f 100644 --- a/src/video-grid/VideoTile.tsx +++ b/src/video-grid/VideoTile.tsx @@ -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; diff --git a/yarn.lock b/yarn.lock index 271686be..22b0d550 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" From 5ba7267164d1692617d7c6464ee21819d7666d2e Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 28 Nov 2022 16:15:47 -0500 Subject: [PATCH 2/3] Fix lints --- package.json | 2 +- public/locales/en-GB/app.json | 3 +- src/ClientContext.tsx | 5 +- src/useEvents.ts | 2 +- yarn.lock | 97 +++++++++++++++++++++++++++++++---- 5 files changed, 94 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 750afc3b..02b22230 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "i18next-parser": "^6.6.0", "identity-obj-proxy": "^3.0.0", "jest": "^29.2.2", - "jest-environment-jsdom": "^29.2.2", + "jest-environment-jsdom": "^29.3.1", "prettier": "^2.6.2", "sass": "^1.42.1", "storybook-builder-vite": "^0.1.12", diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json index 1e0b996e..78c86257 100644 --- a/public/locales/en-GB/app.json +++ b/public/locales/en-GB/app.json @@ -43,8 +43,6 @@ "Display name": "Display name", "Download debug logs": "Download debug logs", "Element Call Home": "Element Call Home", - "Single-key keyboard shortcuts": "Single-key keyboard shortcuts", - "Entering room…": "Entering room…", "Exit full screen": "Exit full screen", "Fetching group call timed out.": "Fetching group call timed out.", "Freedom": "Freedom", @@ -105,6 +103,7 @@ "Show call inspector": "Show call inspector", "Sign in": "Sign in", "Sign out": "Sign out", + "Single-key keyboard shortcuts": "Single-key keyboard shortcuts", "Spatial audio": "Spatial audio", "Speaker": "Speaker", "Speaker {{n}}": "Speaker {{n}}", diff --git a/src/ClientContext.tsx b/src/ClientContext.tsx index 8eba226f..d660039f 100644 --- a/src/ClientContext.tsx +++ b/src/ClientContext.tsx @@ -55,7 +55,8 @@ export interface Session { tempPassword?: string; } -const loadChannel = new BroadcastChannel("load"); +const loadChannel = + "BroadcastChannel" in window ? new BroadcastChannel("load") : null; const loadSession = (): Session => { const data = localStorage.getItem("matrix-auth-store"); @@ -299,7 +300,7 @@ export const ClientProvider: FC = ({ children }) => { // 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(() => { - if (!widget) loadChannel.postMessage({}); + if (!widget) loadChannel?.postMessage({}); }, []); useEventTarget( diff --git a/src/useEvents.ts b/src/useEvents.ts index 6f1332b6..54f23693 100644 --- a/src/useEvents.ts +++ b/src/useEvents.ts @@ -24,7 +24,7 @@ import type { // Shortcut for registering a listener on an EventTarget export const useEventTarget = ( - target: EventTarget, + target: EventTarget | null | undefined, eventType: string, listener: (event: T) => void, options?: AddEventListenerOptions diff --git a/yarn.lock b/yarn.lock index 22b0d550..b9fc4e32 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1581,6 +1581,16 @@ "@types/node" "*" jest-mock "^29.2.2" +"@jest/environment@^29.3.1": + version "29.3.1" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.3.1.tgz#eb039f726d5fcd14698acd072ac6576d41cfcaa6" + integrity sha512-pMmvfOPmoa1c1QpfFW0nXYtNLpofqo4BrCIk6f2kW4JFeNlHV2t3vd+3iDLf31e2ot2Mec0uqZfmI+U0K2CFag== + dependencies: + "@jest/fake-timers" "^29.3.1" + "@jest/types" "^29.3.1" + "@types/node" "*" + jest-mock "^29.3.1" + "@jest/expect-utils@^29.2.2": version "29.2.2" resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.2.2.tgz#460a5b5a3caf84d4feb2668677393dd66ff98665" @@ -1608,6 +1618,18 @@ jest-mock "^29.2.2" jest-util "^29.2.1" +"@jest/fake-timers@^29.3.1": + version "29.3.1" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.3.1.tgz#b140625095b60a44de820876d4c14da1aa963f67" + integrity sha512-iHTL/XpnDlFki9Tq0Q1GGuVeQ8BHZGIYsvCO5eN/O/oJaRzofG9Xndd9HuSDBI/0ZS79pg0iwn07OMTQ7ngF2A== + dependencies: + "@jest/types" "^29.3.1" + "@sinonjs/fake-timers" "^9.1.2" + "@types/node" "*" + jest-message-util "^29.3.1" + jest-mock "^29.3.1" + jest-util "^29.3.1" + "@jest/globals@^29.2.2": version "29.2.2" resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.2.2.tgz#205ff1e795aa774301c2c0ba0be182558471b845" @@ -1717,6 +1739,18 @@ "@types/yargs" "^17.0.8" chalk "^4.0.0" +"@jest/types@^29.3.1": + version "29.3.1" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.3.1.tgz#7c5a80777cb13e703aeec6788d044150341147e3" + integrity sha512-d0S0jmmTpjnhCmNpApgX3jrUZgZ22ivKJRvL2lli5hpCRoNnp1f85r2/wpKfXuYu8E7Jjh1hGfhPyup1NM5AmA== + dependencies: + "@jest/schemas" "^29.0.0" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + "@joshwooding/vite-plugin-react-docgen-typescript@0.0.2": version "0.0.2" resolved "https://registry.yarnpkg.com/@joshwooding/vite-plugin-react-docgen-typescript/-/vite-plugin-react-docgen-typescript-0.0.2.tgz#e0ae8c94f468da3a273a7b0acf23ba3565f86cbc" @@ -9457,18 +9491,18 @@ jest-each@^29.2.1: jest-util "^29.2.1" pretty-format "^29.2.1" -jest-environment-jsdom@^29.2.2: - version "29.2.2" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-29.2.2.tgz#1e2d9f1f017fbaa7362a83e670b569158b4b8527" - integrity sha512-5mNtTcky1+RYv9kxkwMwt7fkzyX4EJUarV7iI+NQLigpV4Hz4sgfOdP4kOpCHXbkRWErV7tgXoXLm2CKtucr+A== +jest-environment-jsdom@^29.3.1: + version "29.3.1" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-29.3.1.tgz#14ca63c3e0ef5c63c5bcb46033e50bc649e3b639" + integrity sha512-G46nKgiez2Gy4zvYNhayfMEAFlVHhWfncqvqS6yCd0i+a4NsSUD2WtrKSaYQrYiLQaupHXxCRi8xxVL2M9PbhA== dependencies: - "@jest/environment" "^29.2.2" - "@jest/fake-timers" "^29.2.2" - "@jest/types" "^29.2.1" + "@jest/environment" "^29.3.1" + "@jest/fake-timers" "^29.3.1" + "@jest/types" "^29.3.1" "@types/jsdom" "^20.0.0" "@types/node" "*" - jest-mock "^29.2.2" - jest-util "^29.2.1" + jest-mock "^29.3.1" + jest-util "^29.3.1" jsdom "^20.0.0" jest-environment-node@^29.2.2: @@ -9540,6 +9574,21 @@ jest-message-util@^29.2.1: slash "^3.0.0" stack-utils "^2.0.3" +jest-message-util@^29.3.1: + version "29.3.1" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.3.1.tgz#37bc5c468dfe5120712053dd03faf0f053bd6adb" + integrity sha512-lMJTbgNcDm5z+6KDxWtqOFWlGQxD6XaYwBqHR8kmpkP+WWWG90I35kdtQHY67Ay5CSuydkTBbJG+tH9JShFCyA== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^29.3.1" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^29.3.1" + slash "^3.0.0" + stack-utils "^2.0.3" + jest-mock@^29.2.2: version "29.2.2" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.2.2.tgz#9045618b3f9d27074bbcf2d55bdca6a5e2e8bca7" @@ -9549,6 +9598,15 @@ jest-mock@^29.2.2: "@types/node" "*" jest-util "^29.2.1" +jest-mock@^29.3.1: + version "29.3.1" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.3.1.tgz#60287d92e5010979d01f218c6b215b688e0f313e" + integrity sha512-H8/qFDtDVMFvFP4X8NuOT3XRDzOUTz+FeACjufHzsOIBAxivLqkB1PoLCaJx9iPPQ8dZThHPp/G3WRWyMgA3JA== + dependencies: + "@jest/types" "^29.3.1" + "@types/node" "*" + jest-util "^29.3.1" + jest-pnp-resolver@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" @@ -9679,6 +9737,18 @@ jest-util@^29.2.1: graceful-fs "^4.2.9" picomatch "^2.2.3" +jest-util@^29.3.1: + version "29.3.1" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.3.1.tgz#1dda51e378bbcb7e3bc9d8ab651445591ed373e1" + integrity sha512-7YOVZaiX7RJLv76ZfHt4nbNEzzTRiMW/IiOG7ZOKmTXmoGBxUDefgMAxQubu6WPVqP5zSzAdZG0FfLcC7HOIFQ== + dependencies: + "@jest/types" "^29.3.1" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + jest-validate@^29.2.2: version "29.2.2" resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.2.2.tgz#e43ce1931292dfc052562a11bc681af3805eadce" @@ -11745,6 +11815,15 @@ pretty-format@^29.0.0, pretty-format@^29.2.1: ansi-styles "^5.0.0" react-is "^18.0.0" +pretty-format@^29.3.1: + version "29.3.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.3.1.tgz#1841cac822b02b4da8971dacb03e8a871b4722da" + integrity sha512-FyLnmb1cYJV8biEIiRyzRFvs2lry7PPIvOqKVe1GCUEYg4YGmlx1qG9EJNMxArYm7piII4qb8UV1Pncq5dxmcg== + dependencies: + "@jest/schemas" "^29.0.0" + ansi-styles "^5.0.0" + react-is "^18.0.0" + pretty-hrtime@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" From aec034182bd4a2940d211f74ed23204c56b45480 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 28 Nov 2022 16:34:50 -0500 Subject: [PATCH 3/3] Update matrix-js-sdk --- package.json | 2 +- yarn.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 02b22230..8a089928 100644 --- a/package.json +++ b/package.json @@ -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#f46ecf970c658ae34b8d5fc3e73369c31ac79e90", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#9d3ac66cf847fe8cb0359d64e1f1d67902b982a1", "matrix-widget-api": "^1.0.0", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", diff --git a/yarn.lock b/yarn.lock index b9fc4e32..54909a98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10266,9 +10266,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#f46ecf970c658ae34b8d5fc3e73369c31ac79e90": - version "21.1.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/f46ecf970c658ae34b8d5fc3e73369c31ac79e90" +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#9d3ac66cf847fe8cb0359d64e1f1d67902b982a1": + version "21.2.0" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/9d3ac66cf847fe8cb0359d64e1f1d67902b982a1" dependencies: "@babel/runtime" "^7.12.5" "@types/sdp-transform" "^2.4.5"