mirror of
https://github.com/vector-im/element-call.git
synced 2026-04-06 07:20:25 +00:00
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>
This commit is contained in:
committed by
Andrew Ferrazzutti
parent
379050ff80
commit
620c3f5d34
@@ -60,8 +60,12 @@
|
||||
"disconnected_banner": "Connectivity to the server has been lost.",
|
||||
"full_screen_view_description": "<0>Submitting debug logs will help us track down the problem.</0>",
|
||||
"full_screen_view_h1": "<0>Oops, something's gone wrong.</0>",
|
||||
"group_call_loader_banned_heading": "Not allowed to join",
|
||||
"group_call_loader_banned_message": "You are banned from this room and therefore cannot send a request to join.",
|
||||
"group_call_loader_failed_heading": "Call not found",
|
||||
"group_call_loader_failed_text": "Calls are now end-to-end encrypted and need to be created from the home page. This helps make sure everyone's using the same encryption key.",
|
||||
"group_call_loader_reject_message": "The room members declined your request to join.",
|
||||
"group_call_loader_rejected_heading": "Not allowed to join",
|
||||
"hangup_button_label": "End call",
|
||||
"header_label": "Element Call Home",
|
||||
"header_participants_label": "Participants",
|
||||
@@ -150,5 +154,8 @@
|
||||
"sfu_participant_local": "You",
|
||||
"volume": "Volume"
|
||||
},
|
||||
"wait_for_invite": {
|
||||
"wait_message": "Waiting for the meeting host to let you in."
|
||||
},
|
||||
"waiting_for_participants": "Waiting for other participants…"
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ interface ErrorViewProps {
|
||||
|
||||
export const ErrorView: FC<ErrorViewProps> = ({ error }) => {
|
||||
const location = useLocation();
|
||||
const { confineToRoom } = useUrlParams();
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -78,25 +79,26 @@ export const ErrorView: FC<ErrorViewProps> = ({ error }) => {
|
||||
: error.message}
|
||||
</p>
|
||||
<RageshakeButton description={`***Error View***: ${error.message}`} />
|
||||
{location.pathname === "/" ? (
|
||||
<Button
|
||||
size="lg"
|
||||
variant="default"
|
||||
className={styles.homeLink}
|
||||
onPress={onReload}
|
||||
>
|
||||
{t("return_home_button")}
|
||||
</Button>
|
||||
) : (
|
||||
<LinkButton
|
||||
size="lg"
|
||||
variant="default"
|
||||
className={styles.homeLink}
|
||||
to="/"
|
||||
>
|
||||
{t("return_home_button")}
|
||||
</LinkButton>
|
||||
)}
|
||||
{!confineToRoom &&
|
||||
(location.pathname === "/" ? (
|
||||
<Button
|
||||
size="lg"
|
||||
variant="default"
|
||||
className={styles.homeLink}
|
||||
onPress={onReload}
|
||||
>
|
||||
{t("return_home_button")}
|
||||
</Button>
|
||||
) : (
|
||||
<LinkButton
|
||||
size="lg"
|
||||
variant="default"
|
||||
className={styles.homeLink}
|
||||
to="/"
|
||||
>
|
||||
{t("return_home_button")}
|
||||
</LinkButton>
|
||||
))}
|
||||
</FullScreenView>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,19 +17,23 @@ limitations under the License.
|
||||
import { ReactNode, useCallback } from "react";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||
import { MatrixError } from "matrix-js-sdk";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { Heading, Link, Text } from "@vector-im/compound-web";
|
||||
|
||||
import { useLoadGroupCall } from "./useLoadGroupCall";
|
||||
import {
|
||||
useLoadGroupCall,
|
||||
GroupCallStatus,
|
||||
KnockRejectError,
|
||||
BannedError,
|
||||
} from "./useLoadGroupCall";
|
||||
import { ErrorView, FullScreenView } from "../FullScreenView";
|
||||
|
||||
interface Props {
|
||||
client: MatrixClient;
|
||||
roomIdOrAlias: string;
|
||||
viaServers: string[];
|
||||
children: (rtcSession: MatrixRTCSession) => ReactNode;
|
||||
children: (groupCallState: GroupCallStatus) => ReactNode;
|
||||
}
|
||||
|
||||
export function GroupCallLoader({
|
||||
@@ -51,14 +55,15 @@ export function GroupCallLoader({
|
||||
);
|
||||
|
||||
switch (groupCallState.kind) {
|
||||
case "loaded":
|
||||
case "waitForInvite":
|
||||
return <>{children(groupCallState)}</>;
|
||||
case "loading":
|
||||
return (
|
||||
<FullScreenView>
|
||||
<h1>{t("common.loading")}</h1>
|
||||
</FullScreenView>
|
||||
);
|
||||
case "loaded":
|
||||
return <>{children(groupCallState.rtcSession)}</>;
|
||||
case "failed":
|
||||
if ((groupCallState.error as MatrixError).errcode === "M_NOT_FOUND") {
|
||||
return (
|
||||
@@ -72,6 +77,26 @@ export function GroupCallLoader({
|
||||
</Link>
|
||||
</FullScreenView>
|
||||
);
|
||||
} else if (groupCallState.error instanceof KnockRejectError) {
|
||||
return (
|
||||
<FullScreenView>
|
||||
<Heading>{t("group_call_loader_rejected_heading")}</Heading>
|
||||
<Text>{groupCallState.error.message}</Text>
|
||||
<Link href="/" onClick={onHomeClick}>
|
||||
{t("common.home")}
|
||||
</Link>
|
||||
</FullScreenView>
|
||||
);
|
||||
} else if (groupCallState.error instanceof BannedError) {
|
||||
return (
|
||||
<FullScreenView>
|
||||
<Heading>{t("group_call_loader_banned_heading")}</Heading>
|
||||
<Text>{t("group_call_loader_banned_message")}</Text>
|
||||
<Link href="/" onClick={onHomeClick}>
|
||||
{t("common.home")}
|
||||
</Link>
|
||||
</FullScreenView>
|
||||
);
|
||||
} else {
|
||||
return <ErrorView error={groupCallState.error} />;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { FC, useEffect, useState, useCallback, ReactNode } from "react";
|
||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { useClientLegacy } from "../ClientContext";
|
||||
@@ -30,6 +29,8 @@ import { HomePage } from "../home/HomePage";
|
||||
import { platform } from "../Platform";
|
||||
import { AppSelectionModal } from "./AppSelectionModal";
|
||||
import { widget } from "../widget";
|
||||
import { GroupCallStatus } from "./useLoadGroupCall";
|
||||
import { WaitForInviteView } from "./WaitForInviteView";
|
||||
|
||||
export const RoomPage: FC = () => {
|
||||
const {
|
||||
@@ -78,17 +79,30 @@ export const RoomPage: FC = () => {
|
||||
]);
|
||||
|
||||
const groupCallView = useCallback(
|
||||
(rtcSession: MatrixRTCSession) => (
|
||||
<GroupCallView
|
||||
client={client!}
|
||||
rtcSession={rtcSession}
|
||||
isPasswordlessUser={passwordlessUser}
|
||||
confineToRoom={confineToRoom}
|
||||
preload={preload}
|
||||
skipLobby={skipLobby}
|
||||
hideHeader={hideHeader}
|
||||
/>
|
||||
),
|
||||
(groupCallState: GroupCallStatus) => {
|
||||
switch (groupCallState.kind) {
|
||||
case "loaded":
|
||||
return (
|
||||
<GroupCallView
|
||||
client={client!}
|
||||
rtcSession={groupCallState.rtcSession}
|
||||
isPasswordlessUser={passwordlessUser}
|
||||
confineToRoom={confineToRoom}
|
||||
preload={preload}
|
||||
skipLobby={skipLobby}
|
||||
hideHeader={hideHeader}
|
||||
/>
|
||||
);
|
||||
case "waitForInvite":
|
||||
return (
|
||||
<WaitForInviteView
|
||||
hideHeader={hideHeader}
|
||||
client={client!}
|
||||
roomSummary={groupCallState.roomSummary}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
[client, passwordlessUser, confineToRoom, preload, hideHeader, skipLobby],
|
||||
);
|
||||
|
||||
|
||||
95
src/room/WaitForInviteView.tsx
Normal file
95
src/room/WaitForInviteView.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
Copyright 2022-2023 New Vector Ltd
|
||||
|
||||
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 { FC, useCallback, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import classNames from "classnames";
|
||||
import { MatrixClient } from "matrix-js-sdk";
|
||||
|
||||
import styles from "./LobbyView.module.css";
|
||||
import inCallStyles from "./InCallView.module.css";
|
||||
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
|
||||
import { useLocationNavigation } from "../useLocationNavigation";
|
||||
import { SettingsButton } from "../button/Button";
|
||||
import { SettingsModal, defaultSettingsTab } from "../settings/SettingsModal";
|
||||
import { RoomSummary } from "./useLoadGroupCall";
|
||||
|
||||
interface Props {
|
||||
hideHeader: boolean;
|
||||
client: MatrixClient;
|
||||
roomSummary: RoomSummary;
|
||||
}
|
||||
|
||||
export const WaitForInviteView: FC<Props> = ({
|
||||
hideHeader,
|
||||
client,
|
||||
roomSummary,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
useLocationNavigation();
|
||||
|
||||
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
|
||||
const [settingsTab, setSettingsTab] = useState(defaultSettingsTab);
|
||||
|
||||
const openSettings = useCallback(
|
||||
() => setSettingsModalOpen(true),
|
||||
[setSettingsModalOpen],
|
||||
);
|
||||
const closeSettings = useCallback(
|
||||
() => setSettingsModalOpen(false),
|
||||
[setSettingsModalOpen],
|
||||
);
|
||||
|
||||
// TODO: Unify this component with InCallView, so we can get slick joining
|
||||
// animations and don't have to feel bad about reusing its CSS
|
||||
return (
|
||||
<>
|
||||
<div className={classNames(styles.room, inCallStyles.inRoom)}>
|
||||
{!hideHeader && (
|
||||
<Header>
|
||||
<LeftNav>
|
||||
<RoomHeaderInfo
|
||||
id={roomSummary.room_id ?? "unknown"}
|
||||
name={roomSummary.name ?? "unknown"}
|
||||
avatarUrl={roomSummary.avatar_url ?? null}
|
||||
encrypted={roomSummary.is_encrypted}
|
||||
participantCount={roomSummary.num_joined_members}
|
||||
/>
|
||||
</LeftNav>
|
||||
<RightNav> </RightNav>
|
||||
</Header>
|
||||
)}
|
||||
<div className={styles.content}>
|
||||
{t("wait_for_invite.wait_message")}
|
||||
</div>
|
||||
<div className={inCallStyles.footer}>
|
||||
<div className={inCallStyles.buttons}>
|
||||
<SettingsButton onPress={openSettings} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{client && (
|
||||
<SettingsModal
|
||||
client={client}
|
||||
open={settingsModalOpen}
|
||||
onDismiss={closeSettings}
|
||||
tab={settingsTab}
|
||||
onTabChange={setSettingsTab}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -18,10 +18,12 @@ import { useState, useEffect } from "react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { ClientEvent, MatrixClient } 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, type Room } from "matrix-js-sdk/src/models/room";
|
||||
import { KnownMembership, Membership, RoomType } 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";
|
||||
|
||||
export type GroupCallLoaded = {
|
||||
@@ -38,51 +40,140 @@ export type GroupCallLoading = {
|
||||
kind: "loading";
|
||||
};
|
||||
|
||||
export type GroupCallWaitForInvite = {
|
||||
kind: "waitForInvite";
|
||||
roomSummary: RoomSummary;
|
||||
};
|
||||
|
||||
export type GroupCallStatus =
|
||||
| GroupCallLoaded
|
||||
| GroupCallLoadFailed
|
||||
| GroupCallLoading;
|
||||
|
||||
| GroupCallLoading
|
||||
| GroupCallWaitForInvite;
|
||||
export interface GroupCallLoadState {
|
||||
error?: Error;
|
||||
error?: Error | KnockRejectError;
|
||||
groupCall?: GroupCall;
|
||||
}
|
||||
|
||||
export class KnockRejectError extends Error {}
|
||||
export class BannedError extends Error {}
|
||||
|
||||
// RoomSummary from the js-sdk (this is not public so we copy it here)
|
||||
export interface RoomSummary {
|
||||
room_id: string;
|
||||
name?: string;
|
||||
avatar_url?: string;
|
||||
topic?: string;
|
||||
canonical_alias?: string;
|
||||
aliases?: string[];
|
||||
world_readable: boolean;
|
||||
guest_can_join: boolean;
|
||||
num_joined_members: number;
|
||||
join_rule?: JoinRule.Knock | JoinRule.Public; // Added by MSC2403
|
||||
room_type?: RoomType;
|
||||
membership?: Membership;
|
||||
is_encrypted: boolean;
|
||||
}
|
||||
|
||||
export const useLoadGroupCall = (
|
||||
client: MatrixClient,
|
||||
roomIdOrAlias: string,
|
||||
viaServers: string[],
|
||||
): GroupCallStatus => {
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState<GroupCallStatus>({ kind: "loading" });
|
||||
const { t } = useTranslation();
|
||||
|
||||
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[],
|
||||
): Promise<Room> => {
|
||||
let joinedRoom: Room | null = null;
|
||||
await client.knockRoom(roomId);
|
||||
|
||||
const invitePromise = new Promise<void>((resolve, reject) => {
|
||||
client.on(
|
||||
RoomEvent.MyMembership,
|
||||
async (room, membership, prevMembership) => {
|
||||
if (membership === KnownMembership.Invite) {
|
||||
await client.joinRoom(room.roomId, { viaServers });
|
||||
joinedRoom = room;
|
||||
logger.log("Auto-joined %s", room.roomId);
|
||||
resolve();
|
||||
}
|
||||
if (
|
||||
membership === KnownMembership.Ban ||
|
||||
membership === KnownMembership.Leave
|
||||
) {
|
||||
// also resolve in case of rejection
|
||||
// we will check if joining worked in the next step
|
||||
reject(
|
||||
new KnockRejectError(t("group_call_loader_reject_message")),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
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.`,
|
||||
);
|
||||
}
|
||||
// The call uses a room alias
|
||||
room = await getRoomByAlias(roomIdOrAlias);
|
||||
} else {
|
||||
room = await client.joinRoom(roomIdOrAlias, { viaServers });
|
||||
// The call uses a room_id
|
||||
const roomSummary = await client.getRoomSummary(
|
||||
roomIdOrAlias,
|
||||
viaServers,
|
||||
);
|
||||
room = client.getRoom(roomSummary.room_id);
|
||||
if (room?.getMyMembership() === KnownMembership.Join) {
|
||||
// room already joined so we are done here already.
|
||||
return room;
|
||||
} else if (room?.getMyMembership() === KnownMembership.Ban) {
|
||||
throw new BannedError();
|
||||
} else {
|
||||
if (roomSummary.join_rule === JoinRule.Public) {
|
||||
room = await client.joinRoom(roomSummary.room_id, {
|
||||
viaServers,
|
||||
});
|
||||
} else if (roomSummary.join_rule === JoinRule.Knock) {
|
||||
setState({ kind: "waitForInvite", roomSummary });
|
||||
room = await getRoomByKnocking(roomSummary.room_id, viaServers);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Room ${roomIdOrAlias} is not joinable. This likely means, that the conference owner has changed the room settings to private.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
@@ -119,11 +210,15 @@ export const useLoadGroupCall = (
|
||||
}
|
||||
};
|
||||
|
||||
waitForClientSyncing()
|
||||
.then(fetchOrCreateGroupCall)
|
||||
.then((rtcSession) => setState({ kind: "loaded", rtcSession }))
|
||||
.catch((error) => setState({ kind: "failed", error }));
|
||||
}, [client, roomIdOrAlias, viaServers, t]);
|
||||
if (state.kind === "loading") {
|
||||
logger.log("Start loading group call");
|
||||
waitForClientSyncing()
|
||||
.then(fetchOrCreateGroupCall)
|
||||
.then((rtcSession) => setState({ kind: "loaded", rtcSession }))
|
||||
.catch((error) => setState({ kind: "failed", error }));
|
||||
}
|
||||
}, [client, roomIdOrAlias, state, t, viaServers]);
|
||||
|
||||
// state === undefined is used to make sure we only run the effect once. But outside the hook it is equivalent to "loading".
|
||||
return state;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user