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:
Timo
2024-04-23 15:15:13 +02:00
committed by GitHub
parent be25d77e8b
commit 5284479ece
26 changed files with 631 additions and 299 deletions

View File

@@ -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;
};