2022-07-15 12:58:53 -04:00
|
|
|
/*
|
2023-01-03 16:55:26 +00:00
|
|
|
Copyright 2022 New Vector Ltd
|
2022-07-15 12:58:53 -04:00
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
*/
|
|
|
|
|
|
2024-04-23 15:15:13 +02:00
|
|
|
import { useState, useEffect, useRef, useCallback } from "react";
|
2022-09-01 11:41:22 +01:00
|
|
|
import { logger } from "matrix-js-sdk/src/logger";
|
2024-04-23 15:15:13 +02:00
|
|
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
|
|
|
|
import {
|
|
|
|
|
ClientEvent,
|
|
|
|
|
MatrixClient,
|
|
|
|
|
RoomSummary,
|
|
|
|
|
} from "matrix-js-sdk/src/client";
|
2022-09-23 10:50:42 +01:00
|
|
|
import { SyncState } from "matrix-js-sdk/src/sync";
|
2023-08-16 18:41:27 +01:00
|
|
|
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
2024-04-23 15:15:13 +02:00
|
|
|
import { RoomEvent, Room } from "matrix-js-sdk/src/models/room";
|
|
|
|
|
import { KnownMembership } from "matrix-js-sdk/src/types";
|
|
|
|
|
import { JoinRule } from "matrix-js-sdk";
|
|
|
|
|
import { useTranslation } from "react-i18next";
|
2022-07-15 12:58:53 -04:00
|
|
|
|
2024-04-23 15:15:13 +02:00
|
|
|
import { widget } from "../widget";
|
2023-04-27 09:30:34 +02:00
|
|
|
|
2023-07-03 19:14:03 +01:00
|
|
|
export type GroupCallLoaded = {
|
|
|
|
|
kind: "loaded";
|
2023-08-16 18:41:27 +01:00
|
|
|
rtcSession: MatrixRTCSession;
|
2023-07-03 19:14:03 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type GroupCallLoadFailed = {
|
|
|
|
|
kind: "failed";
|
|
|
|
|
error: Error;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type GroupCallLoading = {
|
|
|
|
|
kind: "loading";
|
|
|
|
|
};
|
|
|
|
|
|
2024-04-23 15:15:13 +02:00
|
|
|
export type GroupCallWaitForInvite = {
|
|
|
|
|
kind: "waitForInvite";
|
|
|
|
|
roomSummary: RoomSummary;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type GroupCallCanKnock = {
|
|
|
|
|
kind: "canKnock";
|
|
|
|
|
roomSummary: RoomSummary;
|
|
|
|
|
knock: () => void;
|
|
|
|
|
};
|
|
|
|
|
|
2023-07-03 19:14:03 +01:00
|
|
|
export type GroupCallStatus =
|
|
|
|
|
| GroupCallLoaded
|
|
|
|
|
| GroupCallLoadFailed
|
2024-04-23 15:15:13 +02:00
|
|
|
| GroupCallLoading
|
|
|
|
|
| GroupCallWaitForInvite
|
|
|
|
|
| GroupCallCanKnock;
|
2023-07-03 19:14:03 +01:00
|
|
|
|
2024-04-23 15:15:13 +02:00
|
|
|
export class CallTerminatedMessage extends Error {
|
|
|
|
|
/**
|
|
|
|
|
* @param messageBody The message explaining the kind of termination (kick, ban, knock reject, etc.) (translated)
|
|
|
|
|
*/
|
|
|
|
|
public messageBody: string;
|
|
|
|
|
/**
|
|
|
|
|
* @param reason The user provided reason for the termination (kick/ban)
|
|
|
|
|
*/
|
|
|
|
|
public reason?: string;
|
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @param messageTitle The title of the call ended screen message (translated)
|
|
|
|
|
* @param messageBody The message explaining the kind of termination (kick, ban, knock reject, etc.) (translated)
|
|
|
|
|
* @param reason The user provided reason for the termination (kick/ban)
|
|
|
|
|
*/
|
|
|
|
|
public constructor(
|
|
|
|
|
messageTitle: string,
|
|
|
|
|
messageBody: string,
|
|
|
|
|
reason?: string,
|
|
|
|
|
) {
|
|
|
|
|
super(messageTitle);
|
|
|
|
|
this.messageBody = messageBody;
|
|
|
|
|
this.reason = reason;
|
|
|
|
|
}
|
2022-07-15 12:58:53 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const useLoadGroupCall = (
|
|
|
|
|
client: MatrixClient,
|
|
|
|
|
roomIdOrAlias: string,
|
2023-10-11 10:42:04 -04:00
|
|
|
viaServers: string[],
|
2023-07-03 19:14:03 +01:00
|
|
|
): GroupCallStatus => {
|
|
|
|
|
const [state, setState] = useState<GroupCallStatus>({ kind: "loading" });
|
2024-04-23 15:15:13 +02:00
|
|
|
const activeRoom = useRef<Room>();
|
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
|
|
|
|
|
const bannedError = useCallback(
|
|
|
|
|
(): CallTerminatedMessage =>
|
|
|
|
|
new CallTerminatedMessage(
|
|
|
|
|
t("group_call_loader.banned_heading"),
|
|
|
|
|
t("group_call_loader.banned_body"),
|
|
|
|
|
leaveReason(),
|
|
|
|
|
),
|
|
|
|
|
[t],
|
|
|
|
|
);
|
|
|
|
|
const knockRejectError = useCallback(
|
|
|
|
|
(): CallTerminatedMessage =>
|
|
|
|
|
new CallTerminatedMessage(
|
|
|
|
|
t("group_call_loader.knock_reject_heading"),
|
|
|
|
|
t("group_call_loader.knock_reject_body"),
|
|
|
|
|
leaveReason(),
|
|
|
|
|
),
|
|
|
|
|
[t],
|
|
|
|
|
);
|
|
|
|
|
const removeNoticeError = useCallback(
|
|
|
|
|
(): CallTerminatedMessage =>
|
|
|
|
|
new CallTerminatedMessage(
|
|
|
|
|
t("group_call_loader.call_ended_heading"),
|
|
|
|
|
t("group_call_loader.call_ended_body"),
|
|
|
|
|
leaveReason(),
|
|
|
|
|
),
|
|
|
|
|
[t],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const leaveReason = (): string =>
|
|
|
|
|
activeRoom.current?.currentState
|
|
|
|
|
.getStateEvents(EventType.RoomMember, activeRoom.current?.myUserId)
|
|
|
|
|
?.getContent().reason;
|
2022-07-15 12:58:53 -04:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
2024-04-23 15:15:13 +02:00
|
|
|
const getRoomByAlias = async (alias: string): Promise<Room> => {
|
|
|
|
|
// We lowercase the localpart when we create the room, so we must lowercase
|
|
|
|
|
// it here too (we just do the whole alias). We can't do the same to room IDs
|
|
|
|
|
// though.
|
|
|
|
|
// Also, we explicitly look up the room alias here. We previously just tried to
|
|
|
|
|
// join anyway but the js-sdk recreates the room if you pass the alias for a
|
|
|
|
|
// room you're already joined to (which it probably ought not to).
|
|
|
|
|
let room: Room | null = null;
|
|
|
|
|
const lookupResult = await client.getRoomIdForAlias(alias.toLowerCase());
|
|
|
|
|
logger.info(`${alias} resolved to ${lookupResult.room_id}`);
|
|
|
|
|
room = client.getRoom(lookupResult.room_id);
|
|
|
|
|
if (!room) {
|
|
|
|
|
logger.info(`Room ${lookupResult.room_id} not found, joining.`);
|
|
|
|
|
room = await client.joinRoom(lookupResult.room_id, {
|
|
|
|
|
viaServers: lookupResult.servers,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
logger.info(`Already in room ${lookupResult.room_id}, not rejoining.`);
|
|
|
|
|
}
|
|
|
|
|
return room;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getRoomByKnocking = async (
|
|
|
|
|
roomId: string,
|
|
|
|
|
viaServers: string[],
|
|
|
|
|
onKnockSent: () => void,
|
|
|
|
|
): Promise<Room> => {
|
|
|
|
|
let joinedRoom: Room | null = null;
|
|
|
|
|
await client.knockRoom(roomId, { viaServers });
|
|
|
|
|
onKnockSent();
|
|
|
|
|
const invitePromise = new Promise<void>((resolve, reject) => {
|
|
|
|
|
client.on(
|
|
|
|
|
RoomEvent.MyMembership,
|
|
|
|
|
async (room, membership, prevMembership) => {
|
|
|
|
|
if (roomId !== room.roomId) return;
|
|
|
|
|
activeRoom.current = room;
|
2024-05-07 16:58:30 +09:00
|
|
|
if (
|
|
|
|
|
membership === KnownMembership.Invite &&
|
|
|
|
|
prevMembership === KnownMembership.Knock
|
|
|
|
|
) {
|
2024-04-23 15:15:13 +02:00
|
|
|
await client.joinRoom(room.roomId, { viaServers });
|
|
|
|
|
joinedRoom = room;
|
|
|
|
|
logger.log("Auto-joined %s", room.roomId);
|
|
|
|
|
resolve();
|
|
|
|
|
}
|
|
|
|
|
if (membership === KnownMembership.Ban) reject(bannedError());
|
|
|
|
|
if (membership === KnownMembership.Leave)
|
|
|
|
|
reject(knockRejectError());
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
await invitePromise;
|
|
|
|
|
if (!joinedRoom) {
|
|
|
|
|
throw new Error("Failed to join room after knocking.");
|
|
|
|
|
}
|
|
|
|
|
return joinedRoom;
|
|
|
|
|
};
|
|
|
|
|
|
2022-07-15 12:58:53 -04:00
|
|
|
const fetchOrCreateRoom = async (): Promise<Room> => {
|
2023-09-22 18:56:34 +01:00
|
|
|
let room: Room | null = null;
|
|
|
|
|
if (roomIdOrAlias[0] === "#") {
|
2024-04-23 15:15:13 +02:00
|
|
|
const alias = roomIdOrAlias;
|
|
|
|
|
// The call uses a room alias
|
|
|
|
|
room = await getRoomByAlias(alias);
|
|
|
|
|
activeRoom.current = room;
|
|
|
|
|
} else {
|
|
|
|
|
// The call uses a room_id
|
|
|
|
|
const roomId = roomIdOrAlias;
|
|
|
|
|
|
|
|
|
|
// first try if the room already exists
|
|
|
|
|
// - in widget mode
|
|
|
|
|
// - in SPA mode if the user already joined the room
|
|
|
|
|
room = client.getRoom(roomId);
|
|
|
|
|
activeRoom.current = room ?? undefined;
|
2024-05-04 01:37:58 +09:00
|
|
|
const membership = room?.getMyMembership();
|
|
|
|
|
if (membership === KnownMembership.Join) {
|
2024-04-23 15:15:13 +02:00
|
|
|
// room already joined so we are done here already.
|
|
|
|
|
return room!;
|
|
|
|
|
}
|
|
|
|
|
if (widget)
|
|
|
|
|
// in widget mode we never should reach this point. (getRoom should return the room.)
|
|
|
|
|
throw new Error(
|
|
|
|
|
"Room not found. The widget-api did not pass over the relevant room events/information.",
|
2023-09-22 18:56:34 +01:00
|
|
|
);
|
2024-04-23 15:15:13 +02:00
|
|
|
|
2024-05-04 01:37:58 +09:00
|
|
|
if (membership === KnownMembership.Ban) {
|
2024-04-23 15:15:13 +02:00
|
|
|
throw bannedError();
|
2024-05-04 01:37:58 +09:00
|
|
|
} else if (membership === KnownMembership.Invite) {
|
|
|
|
|
room = await client.joinRoom(roomId, {
|
|
|
|
|
viaServers,
|
|
|
|
|
});
|
2024-04-23 15:15:13 +02:00
|
|
|
} else {
|
2024-06-19 16:41:52 +02:00
|
|
|
// If the room does not exist we first search for it with viaServers
|
|
|
|
|
const roomSummary = await client.getRoomSummary(roomId, viaServers);
|
2024-04-23 15:15:13 +02:00
|
|
|
if (roomSummary.join_rule === JoinRule.Public) {
|
|
|
|
|
room = await client.joinRoom(roomSummary.room_id, {
|
|
|
|
|
viaServers,
|
|
|
|
|
});
|
|
|
|
|
} else if (roomSummary.join_rule === JoinRule.Knock) {
|
|
|
|
|
let knock: () => void = () => {};
|
|
|
|
|
const userPressedAskToJoinPromise: Promise<void> = new Promise(
|
|
|
|
|
(resolve) => {
|
|
|
|
|
if (roomSummary.membership !== KnownMembership.Knock) {
|
|
|
|
|
knock = resolve;
|
|
|
|
|
} else {
|
|
|
|
|
// resolve immediately if the user already knocked
|
|
|
|
|
resolve();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
setState({ kind: "canKnock", roomSummary, knock });
|
|
|
|
|
await userPressedAskToJoinPromise;
|
|
|
|
|
room = await getRoomByKnocking(
|
|
|
|
|
roomSummary.room_id,
|
|
|
|
|
viaServers,
|
|
|
|
|
() => setState({ kind: "waitForInvite", roomSummary }),
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`Room ${roomSummary.room_id} is not joinable. This likely means, that the conference owner has changed the room settings to private.`,
|
|
|
|
|
);
|
|
|
|
|
}
|
2023-09-22 18:56:34 +01:00
|
|
|
}
|
|
|
|
|
}
|
2023-09-19 17:57:16 +01:00
|
|
|
|
|
|
|
|
logger.info(
|
2023-10-11 10:42:04 -04:00
|
|
|
`Joined ${roomIdOrAlias}, waiting room to be ready for group calls`,
|
2023-09-19 17:57:16 +01:00
|
|
|
);
|
|
|
|
|
await client.waitUntilRoomReadyForGroupCalls(room.roomId);
|
2023-09-22 18:56:34 +01:00
|
|
|
logger.info(`${roomIdOrAlias}, is ready for group calls`);
|
2023-09-19 17:57:16 +01:00
|
|
|
return room;
|
2022-07-15 12:58:53 -04:00
|
|
|
};
|
|
|
|
|
|
2023-08-16 18:41:27 +01:00
|
|
|
const fetchOrCreateGroupCall = async (): Promise<MatrixRTCSession> => {
|
2022-07-15 12:58:53 -04:00
|
|
|
const room = await fetchOrCreateRoom();
|
2024-04-23 15:15:13 +02:00
|
|
|
activeRoom.current = room;
|
2022-09-01 11:41:22 +01:00
|
|
|
logger.debug(`Fetched / joined room ${roomIdOrAlias}`);
|
2022-07-15 16:08:26 -04:00
|
|
|
|
2023-08-16 18:41:27 +01:00
|
|
|
const rtcSession = client.matrixRTC.getRoomSession(room);
|
|
|
|
|
return rtcSession;
|
2022-07-15 12:58:53 -04:00
|
|
|
};
|
|
|
|
|
|
2023-09-22 18:05:13 -04:00
|
|
|
const waitForClientSyncing = async (): Promise<void> => {
|
2022-09-23 10:50:42 +01:00
|
|
|
if (client.getSyncState() !== SyncState.Syncing) {
|
|
|
|
|
logger.debug(
|
2023-10-11 10:42:04 -04:00
|
|
|
"useLoadGroupCall: waiting for client to start syncing...",
|
2022-09-23 10:50:42 +01:00
|
|
|
);
|
|
|
|
|
await new Promise<void>((resolve) => {
|
2023-09-22 18:05:13 -04:00
|
|
|
const onSync = (): void => {
|
2022-09-23 10:50:42 +01:00
|
|
|
if (client.getSyncState() === SyncState.Syncing) {
|
|
|
|
|
client.off(ClientEvent.Sync, onSync);
|
|
|
|
|
return resolve();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
client.on(ClientEvent.Sync, onSync);
|
|
|
|
|
});
|
|
|
|
|
logger.debug("useLoadGroupCall: client is now syncing.");
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2024-04-23 15:15:13 +02:00
|
|
|
const observeMyMembership = async (): Promise<void> => {
|
|
|
|
|
await new Promise((_, reject) => {
|
|
|
|
|
client.on(RoomEvent.MyMembership, async (_, membership) => {
|
|
|
|
|
if (membership === KnownMembership.Leave) reject(removeNoticeError());
|
|
|
|
|
if (membership === KnownMembership.Ban) reject(bannedError());
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (state.kind === "loading") {
|
|
|
|
|
logger.log("Start loading group call");
|
|
|
|
|
waitForClientSyncing()
|
|
|
|
|
.then(fetchOrCreateGroupCall)
|
|
|
|
|
.then((rtcSession) => setState({ kind: "loaded", rtcSession }))
|
|
|
|
|
.then(observeMyMembership)
|
|
|
|
|
.catch((error) => setState({ kind: "failed", error }));
|
|
|
|
|
}
|
|
|
|
|
}, [
|
|
|
|
|
bannedError,
|
|
|
|
|
client,
|
|
|
|
|
knockRejectError,
|
|
|
|
|
removeNoticeError,
|
|
|
|
|
roomIdOrAlias,
|
|
|
|
|
state,
|
|
|
|
|
t,
|
|
|
|
|
viaServers,
|
|
|
|
|
]);
|
2022-07-15 12:58:53 -04:00
|
|
|
|
|
|
|
|
return state;
|
|
|
|
|
};
|