Knocking support (#2281)
* Add joining with knock room creation flow. Also add `WaitForInviteView` after knocking. And appropriate error views when knock failed or gets rejected. Signed-off-by: Timo K <toger5@hotmail.de> * Refactor encryption information. We had lots of enums and booleans to describe the encryption situation. Now we only use the `EncryptionSystem` "enum" which contains the additional information like sharedKey. (and we don't use the isRoomE2EE function that is somewhat confusing since it checks `return widget === null && !room.getCanonicalAlias();` which is only indirectly related to e2ee) Signed-off-by: Timo K <toger5@hotmail.de> * Update recent list. - Don't use deprecated `groupCallEventHander` anymore (it used the old `m.call` state event.) - make the recent list reactive (getting removed from a call removes the item from the list) - support having rooms without shared secret but actual matrix encryption in the recent list - change the share link creation button so that we create a link with pwd for sharedKey rooms and with `perParticipantE2EE=true` for matrix encrypted rooms. Signed-off-by: Timo K <toger5@hotmail.de> * fix types Signed-off-by: Timo K <toger5@hotmail.de> * patch js-sdk for linter Signed-off-by: Timo K <toger5@hotmail.de> * ignore ts expect error Signed-off-by: Timo K <toger5@hotmail.de> * Fix error in widget mode. We cannot call client.getRoomSummary in widget mode. The code path needs to throw before reaching this call. (In general we should never call getRoomSummary if getRoom returns a room) Signed-off-by: Timo K <toger5@hotmail.de> * tempDemo Signed-off-by: Timo K <toger5@hotmail.de> * remove wait for invite view Signed-off-by: Timo K <toger5@hotmail.de> * yarn i18n Signed-off-by: Timo K <toger5@hotmail.de> * reset back mute participant count * add logic to show error view when getting removed * include reason whenever someone gets removed from a call. * fix activeRoom not beeing early enough * fix lints * add comment about encryption situation Signed-off-by: Timo K <toger5@hotmail.de> * Fix lockfile * Use (unmerged!) RoomSummary type from the js-sdk Temporarily change the js-sdk dependency to the PR branch that provides that type * review Signed-off-by: Timo K <toger5@hotmail.de> * review (remove participant count unknown) Signed-off-by: Timo K <toger5@hotmail.de> * remove error for unencrypted calls (allow intentional unencrypted calls) Signed-off-by: Timo K <toger5@hotmail.de> * update js-sdk Signed-off-by: Timo K <toger5@hotmail.de> --------- Signed-off-by: Timo K <toger5@hotmail.de> Co-authored-by: Andrew Ferrazzutti <andrewf@element.io>
This commit is contained in:
@@ -14,15 +14,22 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import {
|
||||
ClientEvent,
|
||||
MatrixClient,
|
||||
RoomSummary,
|
||||
} from "matrix-js-sdk/src/client";
|
||||
import { SyncState } from "matrix-js-sdk/src/sync";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||
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";
|
||||
|
||||
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
import { widget } from "../widget";
|
||||
|
||||
export type GroupCallLoaded = {
|
||||
kind: "loaded";
|
||||
@@ -38,14 +45,48 @@ export type GroupCallLoading = {
|
||||
kind: "loading";
|
||||
};
|
||||
|
||||
export type GroupCallWaitForInvite = {
|
||||
kind: "waitForInvite";
|
||||
roomSummary: RoomSummary;
|
||||
};
|
||||
|
||||
export type GroupCallCanKnock = {
|
||||
kind: "canKnock";
|
||||
roomSummary: RoomSummary;
|
||||
knock: () => void;
|
||||
};
|
||||
|
||||
export type GroupCallStatus =
|
||||
| GroupCallLoaded
|
||||
| GroupCallLoadFailed
|
||||
| GroupCallLoading;
|
||||
| GroupCallLoading
|
||||
| GroupCallWaitForInvite
|
||||
| GroupCallCanKnock;
|
||||
|
||||
export interface GroupCallLoadState {
|
||||
error?: Error;
|
||||
groupCall?: GroupCall;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
export const useLoadGroupCall = (
|
||||
@@ -53,36 +94,159 @@ export const useLoadGroupCall = (
|
||||
roomIdOrAlias: string,
|
||||
viaServers: string[],
|
||||
): GroupCallStatus => {
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState<GroupCallStatus>({ kind: "loading" });
|
||||
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;
|
||||
|
||||
useEffect(() => {
|
||||
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;
|
||||
if (membership === KnownMembership.Invite) {
|
||||
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;
|
||||
};
|
||||
|
||||
const fetchOrCreateRoom = async (): Promise<Room> => {
|
||||
let room: Room | null = null;
|
||||
if (roomIdOrAlias[0] === "#") {
|
||||
// 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).
|
||||
const lookupResult = await client.getRoomIdForAlias(
|
||||
roomIdOrAlias.toLowerCase(),
|
||||
);
|
||||
logger.info(`${roomIdOrAlias} 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.`,
|
||||
);
|
||||
}
|
||||
const alias = roomIdOrAlias;
|
||||
// The call uses a room alias
|
||||
room = await getRoomByAlias(alias);
|
||||
activeRoom.current = room;
|
||||
} else {
|
||||
room = await client.joinRoom(roomIdOrAlias, { viaServers });
|
||||
// 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;
|
||||
if (room?.getMyMembership() === KnownMembership.Join) {
|
||||
// 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.",
|
||||
);
|
||||
|
||||
// If the room does not exist we first search for it with viaServers
|
||||
const roomSummary = await client.getRoomSummary(roomId, viaServers);
|
||||
if (room?.getMyMembership() === KnownMembership.Ban) {
|
||||
throw bannedError();
|
||||
} else {
|
||||
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.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
@@ -95,6 +259,7 @@ export const useLoadGroupCall = (
|
||||
|
||||
const fetchOrCreateGroupCall = async (): Promise<MatrixRTCSession> => {
|
||||
const room = await fetchOrCreateRoom();
|
||||
activeRoom.current = room;
|
||||
logger.debug(`Fetched / joined room ${roomIdOrAlias}`);
|
||||
|
||||
const rtcSession = client.matrixRTC.getRoomSession(room);
|
||||
@@ -119,11 +284,33 @@ export const useLoadGroupCall = (
|
||||
}
|
||||
};
|
||||
|
||||
waitForClientSyncing()
|
||||
.then(fetchOrCreateGroupCall)
|
||||
.then((rtcSession) => setState({ kind: "loaded", rtcSession }))
|
||||
.catch((error) => setState({ kind: "failed", error }));
|
||||
}, [client, roomIdOrAlias, viaServers, t]);
|
||||
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,
|
||||
]);
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user