From b4a56f6dd74438367f989d79610674380575473d Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 15 Jul 2022 11:25:09 -0400 Subject: [PATCH 1/5] Wait for the created room to come down sync before placing a group call --- src/matrix-utils.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/matrix-utils.ts b/src/matrix-utils.ts index 3e576e78..81da90c2 100644 --- a/src/matrix-utils.ts +++ b/src/matrix-utils.ts @@ -8,6 +8,7 @@ import { MemoryCryptoStore } from "matrix-js-sdk/src/crypto/store/memory-crypto- import { createClient, MatrixClient } from "matrix-js-sdk/src/matrix"; import { ICreateClientOpts } from "matrix-js-sdk/src/matrix"; import { ClientEvent } from "matrix-js-sdk/src/client"; +import { Room } from "matrix-js-sdk/src/models/room"; import { Visibility, Preset } from "matrix-js-sdk/src/@types/partials"; import { GroupCallIntent, @@ -226,6 +227,23 @@ export async function createRoom( name: string, isPtt = false ): Promise { + let setExpectedRoomId: (roomId: string) => void; + const expectedRoomId = new Promise( + (resolve) => (setExpectedRoomId = resolve) + ); + + // There's no telling whether the new room will come down sync in the middle + // of the createRoom request, or after it, so start watching for it beforehand + const roomReceived = new Promise((resolve) => { + const onRoom = async (room: Room) => { + if (room.roomId === (await expectedRoomId)) { + resolve(); + client.off(ClientEvent.Room, onRoom); + } + }; + client.on(ClientEvent.Room, onRoom); + }); + const createRoomResult = await client.createRoom({ visibility: Visibility.Private, preset: Preset.PublicChat, @@ -256,6 +274,10 @@ export async function createRoom( }, }); + // Wait for the room to come down sync before doing anything with it + setExpectedRoomId(createRoomResult.room_id); + await roomReceived; + console.log(`Creating ${isPtt ? "PTT" : "video"} group call room`); await client.createGroupCall( From fae4c504c9ee0d060b1cc56f979befde755c921e Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 15 Jul 2022 12:58:53 -0400 Subject: [PATCH 2/5] Consolidate all group call creation into useLoadGroupCall This enables us to automatically create a group call in rooms that exist, but contain no calls. --- src/matrix-utils.ts | 16 +---- src/room/GroupCallLoader.jsx | 1 - src/room/useLoadGroupCall.js | 114 -------------------------------- src/room/useLoadGroupCall.ts | 123 +++++++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 130 deletions(-) delete mode 100644 src/room/useLoadGroupCall.js create mode 100644 src/room/useLoadGroupCall.ts diff --git a/src/matrix-utils.ts b/src/matrix-utils.ts index 81da90c2..e7bf120d 100644 --- a/src/matrix-utils.ts +++ b/src/matrix-utils.ts @@ -10,10 +10,6 @@ import { ICreateClientOpts } from "matrix-js-sdk/src/matrix"; import { ClientEvent } from "matrix-js-sdk/src/client"; import { Room } from "matrix-js-sdk/src/models/room"; import { Visibility, Preset } from "matrix-js-sdk/src/@types/partials"; -import { - GroupCallIntent, - GroupCallType, -} from "matrix-js-sdk/src/webrtc/groupCall"; import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync"; import { logger } from "matrix-js-sdk/src/logger"; @@ -224,8 +220,7 @@ export function isLocalRoomId(roomId: string): boolean { export async function createRoom( client: MatrixClient, - name: string, - isPtt = false + name: string ): Promise { let setExpectedRoomId: (roomId: string) => void; const expectedRoomId = new Promise( @@ -278,15 +273,6 @@ export async function createRoom( setExpectedRoomId(createRoomResult.room_id); await roomReceived; - console.log(`Creating ${isPtt ? "PTT" : "video"} group call room`); - - await client.createGroupCall( - createRoomResult.room_id, - isPtt ? GroupCallType.Voice : GroupCallType.Video, - isPtt, - GroupCallIntent.Prompt - ); - return fullAliasFromRoomName(name, client); } diff --git a/src/room/GroupCallLoader.jsx b/src/room/GroupCallLoader.jsx index f0741284..70791a20 100644 --- a/src/room/GroupCallLoader.jsx +++ b/src/room/GroupCallLoader.jsx @@ -30,7 +30,6 @@ export function GroupCallLoader({ client, roomId, viaServers, - true, createPtt ); diff --git a/src/room/useLoadGroupCall.js b/src/room/useLoadGroupCall.js deleted file mode 100644 index b4ec628f..00000000 --- a/src/room/useLoadGroupCall.js +++ /dev/null @@ -1,114 +0,0 @@ -/* -Copyright 2022 Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { useState, useEffect } from "react"; -import { isLocalRoomId, createRoom, roomNameFromRoomId } from "../matrix-utils"; - -async function fetchGroupCall( - client, - roomIdOrAlias, - viaServers = undefined, - timeout = 5000 -) { - const { roomId } = await client.joinRoom(roomIdOrAlias, { viaServers }); - - return new Promise((resolve, reject) => { - let timeoutId; - - function onGroupCallIncoming(groupCall) { - if (groupCall && groupCall.room.roomId === roomId) { - clearTimeout(timeoutId); - client.removeListener("GroupCall.incoming", onGroupCallIncoming); - resolve(groupCall); - } - } - - const groupCall = client.getGroupCallForRoom(roomId); - - if (groupCall) { - resolve(groupCall); - } - - client.on("GroupCall.incoming", onGroupCallIncoming); - - if (timeout) { - timeoutId = setTimeout(() => { - client.removeListener("GroupCall.incoming", onGroupCallIncoming); - reject(new Error("Fetching group call timed out.")); - }, timeout); - } - }); -} - -export function useLoadGroupCall( - client, - roomId, - viaServers, - createIfNotFound, - createPtt -) { - const [state, setState] = useState({ - loading: true, - error: undefined, - groupCall: undefined, - }); - - useEffect(() => { - async function fetchOrCreateGroupCall() { - try { - const groupCall = await fetchGroupCall( - client, - roomId, - viaServers, - 30000 - ); - return groupCall; - } catch (error) { - if ( - createIfNotFound && - (error.errcode === "M_NOT_FOUND" || - (error.message && - error.message.indexOf("Failed to fetch alias") !== -1)) && - isLocalRoomId(roomId) - ) { - const roomName = roomNameFromRoomId(roomId); - await createRoom(client, roomName, createPtt); - const groupCall = await fetchGroupCall( - client, - roomId, - viaServers, - 30000 - ); - return groupCall; - } - - throw error; - } - } - - setState({ loading: true }); - - fetchOrCreateGroupCall() - .then((groupCall) => - setState((prevState) => ({ ...prevState, loading: false, groupCall })) - ) - .catch((error) => - setState((prevState) => ({ ...prevState, loading: false, error })) - ); - }, [client, roomId, state.reloadId, createIfNotFound, viaServers, createPtt]); - - return state; -} diff --git a/src/room/useLoadGroupCall.ts b/src/room/useLoadGroupCall.ts new file mode 100644 index 00000000..27744d7b --- /dev/null +++ b/src/room/useLoadGroupCall.ts @@ -0,0 +1,123 @@ +/* +Copyright 2022 Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { useState, useEffect } from "react"; +import { EventType } from "matrix-js-sdk/src/@types/event"; +import { + GroupCallType, + GroupCallIntent, +} from "matrix-js-sdk/src/webrtc/groupCall"; +import { GroupCallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/groupCallEventHandler"; + +import type { MatrixClient } from "matrix-js-sdk/src/client"; +import type { Room } from "matrix-js-sdk/src/models/room"; +import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall"; +import { isLocalRoomId, createRoom, roomNameFromRoomId } from "../matrix-utils"; + +export interface GroupCallLoadState { + loading: boolean; + error?: Error; + groupCall?: GroupCall; +} + +export const useLoadGroupCall = ( + client: MatrixClient, + roomIdOrAlias: string, + viaServers: string[], + createPtt: boolean +) => { + const [state, setState] = useState({ loading: true }); + + useEffect(() => { + setState({ loading: true }); + + const fetchOrCreateRoom = async (): Promise => { + try { + return await client.joinRoom(roomIdOrAlias, { viaServers }); + } catch (error) { + if ( + isLocalRoomId(roomIdOrAlias) && + (error.errcode === "M_NOT_FOUND" || + (error.message && + error.message.indexOf("Failed to fetch alias") !== -1)) + ) { + // The room doesn't exist, but we can create it + await createRoom(client, roomNameFromRoomId(roomIdOrAlias)); + return await client.joinRoom(roomIdOrAlias, { viaServers }); + } else { + throw error; + } + } + }; + + const fetchOrCreateGroupCall = async (): Promise => { + const room = await fetchOrCreateRoom(); + const groupCall = client.getGroupCallForRoom(room.roomId); + + if (groupCall) { + return groupCall; + } else if ( + room.currentState.mayClientSendStateEvent( + EventType.GroupCallPrefix, + client + ) + ) { + // The call doesn't exist, but we can create it + console.log(`Creating ${createPtt ? "PTT" : "video"} group call room`); + return await client.createGroupCall( + room.roomId, + createPtt ? GroupCallType.Voice : GroupCallType.Video, + createPtt, + GroupCallIntent.Room + ); + } else { + // We don't have permission to create the call, so all we can do is wait + // for one to come in + return new Promise((resolve, reject) => { + const onGroupCallIncoming = (groupCall: GroupCall) => { + if (groupCall?.room.roomId === room.roomId) { + clearTimeout(timeout); + client.off( + GroupCallEventHandlerEvent.Incoming, + onGroupCallIncoming + ); + resolve(groupCall); + } + }; + client.on(GroupCallEventHandlerEvent.Incoming, onGroupCallIncoming); + + const timeout = setTimeout(() => { + client.off( + GroupCallEventHandlerEvent.Incoming, + onGroupCallIncoming + ); + reject(new Error("Fetching group call timed out.")); + }, 30000); + }); + } + }; + + fetchOrCreateGroupCall() + .then((groupCall) => + setState((prevState) => ({ ...prevState, loading: false, groupCall })) + ) + .catch((error) => + setState((prevState) => ({ ...prevState, loading: false, error })) + ); + }, [client, roomIdOrAlias, viaServers, createPtt]); + + return state; +}; From 982398b32fcc240f66e7f98d1a424a4249b50f54 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 15 Jul 2022 13:05:06 -0400 Subject: [PATCH 3/5] Remove unnecessary complexity from createRoom With fae4c504c9ee0d060b1cc56f979befde755c921e, the changes from b4a56f6dd74438367f989d79610674380575473d are no longer necessary. --- src/matrix-utils.ts | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/src/matrix-utils.ts b/src/matrix-utils.ts index e7bf120d..9b6a005b 100644 --- a/src/matrix-utils.ts +++ b/src/matrix-utils.ts @@ -8,7 +8,6 @@ import { MemoryCryptoStore } from "matrix-js-sdk/src/crypto/store/memory-crypto- import { createClient, MatrixClient } from "matrix-js-sdk/src/matrix"; import { ICreateClientOpts } from "matrix-js-sdk/src/matrix"; import { ClientEvent } from "matrix-js-sdk/src/client"; -import { Room } from "matrix-js-sdk/src/models/room"; import { Visibility, Preset } from "matrix-js-sdk/src/@types/partials"; import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync"; import { logger } from "matrix-js-sdk/src/logger"; @@ -222,24 +221,7 @@ export async function createRoom( client: MatrixClient, name: string ): Promise { - let setExpectedRoomId: (roomId: string) => void; - const expectedRoomId = new Promise( - (resolve) => (setExpectedRoomId = resolve) - ); - - // There's no telling whether the new room will come down sync in the middle - // of the createRoom request, or after it, so start watching for it beforehand - const roomReceived = new Promise((resolve) => { - const onRoom = async (room: Room) => { - if (room.roomId === (await expectedRoomId)) { - resolve(); - client.off(ClientEvent.Room, onRoom); - } - }; - client.on(ClientEvent.Room, onRoom); - }); - - const createRoomResult = await client.createRoom({ + await client.createRoom({ visibility: Visibility.Private, preset: Preset.PublicChat, name, @@ -269,10 +251,6 @@ export async function createRoom( }, }); - // Wait for the room to come down sync before doing anything with it - setExpectedRoomId(createRoomResult.room_id); - await roomReceived; - return fullAliasFromRoomName(name, client); } From daeecc9b689f367ef393c9b5e8c805ae5937db17 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 15 Jul 2022 13:07:19 -0400 Subject: [PATCH 4/5] Add a missing type --- src/room/useLoadGroupCall.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/room/useLoadGroupCall.ts b/src/room/useLoadGroupCall.ts index 27744d7b..bb108036 100644 --- a/src/room/useLoadGroupCall.ts +++ b/src/room/useLoadGroupCall.ts @@ -38,7 +38,7 @@ export const useLoadGroupCall = ( roomIdOrAlias: string, viaServers: string[], createPtt: boolean -) => { +): GroupCallLoadState => { const [state, setState] = useState({ loading: true }); useEffect(() => { From 996c5f86c16f9cb1184b6766cbf35d0658be9962 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 15 Jul 2022 16:08:26 -0400 Subject: [PATCH 5/5] Refactor to use fewer else's --- src/room/useLoadGroupCall.ts | 43 +++++++++++++++++------------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/src/room/useLoadGroupCall.ts b/src/room/useLoadGroupCall.ts index bb108036..0d7fb7c7 100644 --- a/src/room/useLoadGroupCall.ts +++ b/src/room/useLoadGroupCall.ts @@ -67,9 +67,9 @@ export const useLoadGroupCall = ( const room = await fetchOrCreateRoom(); const groupCall = client.getGroupCallForRoom(room.roomId); - if (groupCall) { - return groupCall; - } else if ( + if (groupCall) return groupCall; + + if ( room.currentState.mayClientSendStateEvent( EventType.GroupCallPrefix, client @@ -83,31 +83,28 @@ export const useLoadGroupCall = ( createPtt, GroupCallIntent.Room ); - } else { - // We don't have permission to create the call, so all we can do is wait - // for one to come in - return new Promise((resolve, reject) => { - const onGroupCallIncoming = (groupCall: GroupCall) => { - if (groupCall?.room.roomId === room.roomId) { - clearTimeout(timeout); - client.off( - GroupCallEventHandlerEvent.Incoming, - onGroupCallIncoming - ); - resolve(groupCall); - } - }; - client.on(GroupCallEventHandlerEvent.Incoming, onGroupCallIncoming); + } - const timeout = setTimeout(() => { + // We don't have permission to create the call, so all we can do is wait + // for one to come in + return new Promise((resolve, reject) => { + const onGroupCallIncoming = (groupCall: GroupCall) => { + if (groupCall?.room.roomId === room.roomId) { + clearTimeout(timeout); client.off( GroupCallEventHandlerEvent.Incoming, onGroupCallIncoming ); - reject(new Error("Fetching group call timed out.")); - }, 30000); - }); - } + resolve(groupCall); + } + }; + client.on(GroupCallEventHandlerEvent.Incoming, onGroupCallIncoming); + + const timeout = setTimeout(() => { + client.off(GroupCallEventHandlerEvent.Incoming, onGroupCallIncoming); + reject(new Error("Fetching group call timed out.")); + }, 30000); + }); }; fetchOrCreateGroupCall()