Skip lobby when coming from waitForInvite state. (#2753)

* Skip lobby if when coming from waitForInvite state.

* knock reject wording update (shorter/simpler)

* Automatically enter session also in non widget mode when skipLobby = true

* Update public/locales/en-GB/app.json

Co-authored-by: Robin <robin@robin.town>

* review

* review andrew

---------

Co-authored-by: Robin <robin@robin.town>
This commit is contained in:
Timo
2024-11-11 18:30:15 +01:00
committed by GitHub
parent 8465bb4c13
commit 3dad62f9e3
6 changed files with 144 additions and 189 deletions

View File

@@ -106,7 +106,7 @@ rc_message:
MSC3266 allows to request a room summary of rooms you are not joined. The MSC3266 allows to request a room summary of rooms you are not joined. The
summary contains the room join rules. We need that to decide if the user gets summary contains the room join rules. We need that to decide if the user gets
prompted with the option to knock ("ask to join"), a cannot join error or the prompted with the option to knock ("Request to join call"), a cannot join error or the
join view. join view.
Element Call requires a Livekit SFU alongside a [Livekit JWT Element Call requires a Livekit SFU alongside a [Livekit JWT

View File

@@ -81,8 +81,8 @@
"call_ended_heading": "Call ended", "call_ended_heading": "Call ended",
"failed_heading": "Failed to join", "failed_heading": "Failed to join",
"failed_text": "Call not found or is not accessible.", "failed_text": "Call not found or is not accessible.",
"knock_reject_body": "The room members declined your request to join.", "knock_reject_body": "Your request to join was declined.",
"knock_reject_heading": "Not allowed to join", "knock_reject_heading": "Access denied",
"reason": "Reason" "reason": "Reason"
}, },
"hangup_button_label": "End call", "hangup_button_label": "End call",
@@ -100,11 +100,11 @@
"layout_grid_label": "Grid", "layout_grid_label": "Grid",
"layout_spotlight_label": "Spotlight", "layout_spotlight_label": "Spotlight",
"lobby": { "lobby": {
"ask_to_join": "Ask to join call", "ask_to_join": "Request to join call",
"join_as_guest": "Join as guest", "join_as_guest": "Join as guest",
"join_button": "Join call", "join_button": "Join call",
"leave_button": "Back to recents", "leave_button": "Back to recents",
"waiting_for_invite": "Request sent" "waiting_for_invite": "Request sent! Waiting for permission to join…"
}, },
"log_in": "Log In", "log_in": "Log In",
"logging_in": "Logging in…", "logging_in": "Logging in…",

View File

@@ -1,77 +0,0 @@
/*
Copyright 2022-2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/
import { MatrixClient } from "matrix-js-sdk/src/client";
import { useTranslation } from "react-i18next";
import { MatrixError } from "matrix-js-sdk/src/matrix";
import { Heading, Text } from "@vector-im/compound-web";
import { Link } from "../button/Link";
import {
useLoadGroupCall,
GroupCallStatus,
CallTerminatedMessage,
} from "./useLoadGroupCall";
import { ErrorView, FullScreenView } from "../FullScreenView";
interface Props {
client: MatrixClient;
roomIdOrAlias: string;
viaServers: string[];
children: (groupCallState: GroupCallStatus) => JSX.Element;
}
export function GroupCallLoader({
client,
roomIdOrAlias,
viaServers,
children,
}: Props): JSX.Element {
const { t } = useTranslation();
const groupCallState = useLoadGroupCall(client, roomIdOrAlias, viaServers);
switch (groupCallState.kind) {
case "loaded":
case "waitForInvite":
case "canKnock":
return children(groupCallState);
case "loading":
return (
<FullScreenView>
<h1>{t("common.loading")}</h1>
</FullScreenView>
);
case "failed":
if ((groupCallState.error as MatrixError).errcode === "M_NOT_FOUND") {
return (
<FullScreenView>
<Heading>{t("group_call_loader.failed_heading")}</Heading>
<Text>{t("group_call_loader.failed_text")}</Text>
{/* XXX: A 'create it for me' button would be the obvious UX here. Two screens already have
dupes of this flow, let's make a common component and put it here. */}
<Link to="/">{t("common.home")}</Link>
</FullScreenView>
);
} else if (groupCallState.error instanceof CallTerminatedMessage) {
return (
<FullScreenView>
<Heading>{groupCallState.error.message}</Heading>
<Text>{groupCallState.error.messageBody}</Text>
{groupCallState.error.reason && (
<>
{t("group_call_loader.reason")}:
<Text size="sm">"{groupCallState.error.reason}"</Text>
</>
)}
<Link to="/">{t("common.home")}</Link>
</FullScreenView>
);
} else {
return <ErrorView error={groupCallState.error} />;
}
}
}

View File

@@ -177,29 +177,33 @@ export const GroupCallView: FC<Props> = ({
} }
}; };
if (widget && preload && skipLobby) { if (skipLobby) {
// In preload mode without lobby we wait for a join action before entering if (widget && preload) {
const onJoin = (ev: CustomEvent<IWidgetApiRequest>): void => { // In preload mode without lobby we wait for a join action before entering
const onJoin = (ev: CustomEvent<IWidgetApiRequest>): void => {
(async (): Promise<void> => {
await defaultDeviceSetup(ev.detail.data as unknown as JoinCallData);
await enterRTCSession(rtcSession, perParticipantE2EE);
widget!.api.transport.reply(ev.detail, {});
})().catch((e) => {
logger.error("Error joining RTC session", e);
});
};
widget.lazyActions.on(ElementWidgetActions.JoinCall, onJoin);
return (): void => {
widget!.lazyActions.off(ElementWidgetActions.JoinCall, onJoin);
};
} else if (widget && !preload) {
// No lobby and no preload: we enter the rtc session right away
(async (): Promise<void> => { (async (): Promise<void> => {
await defaultDeviceSetup(ev.detail.data as unknown as JoinCallData); await defaultDeviceSetup({ audioInput: null, videoInput: null });
await enterRTCSession(rtcSession, perParticipantE2EE); await enterRTCSession(rtcSession, perParticipantE2EE);
widget!.api.transport.reply(ev.detail, {});
})().catch((e) => { })().catch((e) => {
logger.error("Error joining RTC session", e); logger.error("Error joining RTC session", e);
}); });
}; } else {
widget.lazyActions.on(ElementWidgetActions.JoinCall, onJoin); void enterRTCSession(rtcSession, perParticipantE2EE);
return (): void => { }
widget!.lazyActions.off(ElementWidgetActions.JoinCall, onJoin);
};
} else if (widget && !preload && skipLobby) {
// No lobby and no preload: we enter the rtc session right away
(async (): Promise<void> => {
await defaultDeviceSetup({ audioInput: null, videoInput: null });
await enterRTCSession(rtcSession, perParticipantE2EE);
})().catch((e) => {
logger.error("Error joining RTC session", e);
});
} }
}, [rtcSession, preload, skipLobby, perParticipantE2EE]); }, [rtcSession, preload, skipLobby, perParticipantE2EE]);

View File

@@ -5,15 +5,16 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. Please see LICENSE in the repository root for full details.
*/ */
import { FC, useEffect, useState, useCallback, ReactNode } from "react"; import { FC, useEffect, useState, ReactNode, useRef } from "react";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { CheckIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { CheckIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { MatrixError } from "matrix-js-sdk/src/http-api";
import { Heading, Text } from "@vector-im/compound-web";
import { useClientLegacy } from "../ClientContext"; import { useClientLegacy } from "../ClientContext";
import { ErrorView, LoadingView } from "../FullScreenView"; import { ErrorView, FullScreenView, LoadingView } from "../FullScreenView";
import { RoomAuthView } from "./RoomAuthView"; import { RoomAuthView } from "./RoomAuthView";
import { GroupCallLoader } from "./GroupCallLoader";
import { GroupCallView } from "./GroupCallView"; import { GroupCallView } from "./GroupCallView";
import { useRoomIdentifier, useUrlParams } from "../UrlParams"; import { useRoomIdentifier, useUrlParams } from "../UrlParams";
import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser"; import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser";
@@ -21,13 +22,14 @@ import { HomePage } from "../home/HomePage";
import { platform } from "../Platform"; import { platform } from "../Platform";
import { AppSelectionModal } from "./AppSelectionModal"; import { AppSelectionModal } from "./AppSelectionModal";
import { widget } from "../widget"; import { widget } from "../widget";
import { GroupCallStatus } from "./useLoadGroupCall"; import { CallTerminatedMessage, useLoadGroupCall } from "./useLoadGroupCall";
import { LobbyView } from "./LobbyView"; import { LobbyView } from "./LobbyView";
import { E2eeType } from "../e2ee/e2eeType"; import { E2eeType } from "../e2ee/e2eeType";
import { useProfile } from "../profile/useProfile"; import { useProfile } from "../profile/useProfile";
import { useMuteStates } from "./MuteStates"; import { useMuteStates } from "./MuteStates";
import { useOptInAnalytics } from "../settings/settings"; import { useOptInAnalytics } from "../settings/settings";
import { Config } from "../config/Config"; import { Config } from "../config/Config";
import { Link } from "../button/Link";
export const RoomPage: FC = () => { export const RoomPage: FC = () => {
const { const {
@@ -53,6 +55,7 @@ export const RoomPage: FC = () => {
useClientLegacy(); useClientLegacy();
const { avatarUrl, displayName: userDisplayName } = useProfile(client); const { avatarUrl, displayName: userDisplayName } = useProfile(client);
const groupCallState = useLoadGroupCall(client, roomIdOrAlias, viaServers);
const muteStates = useMuteStates(); const muteStates = useMuteStates();
useEffect(() => { useEffect(() => {
@@ -82,82 +85,112 @@ export const RoomPage: FC = () => {
if (optInAnalytics === null && setOptInAnalytics) setOptInAnalytics(true); if (optInAnalytics === null && setOptInAnalytics) setOptInAnalytics(true);
}, [optInAnalytics, setOptInAnalytics]); }, [optInAnalytics, setOptInAnalytics]);
const groupCallView = useCallback( const wasInWaitForInviteState = useRef<boolean>(false);
(groupCallState: GroupCallStatus): JSX.Element => {
switch (groupCallState.kind) { useEffect(() => {
case "loaded": if (groupCallState.kind === "loaded" && wasInWaitForInviteState.current) {
return ( logger.log("Play join sound 'Not yet implemented'");
<GroupCallView }
client={client!} }, [groupCallState.kind]);
rtcSession={groupCallState.rtcSession}
isPasswordlessUser={passwordlessUser} const groupCallView = (): JSX.Element => {
confineToRoom={confineToRoom} switch (groupCallState.kind) {
preload={preload} case "loaded":
skipLobby={skipLobby} return (
hideHeader={hideHeader} <GroupCallView
muteStates={muteStates} client={client!}
/> rtcSession={groupCallState.rtcSession}
isPasswordlessUser={passwordlessUser}
confineToRoom={confineToRoom}
preload={preload}
skipLobby={skipLobby || wasInWaitForInviteState.current}
hideHeader={hideHeader}
muteStates={muteStates}
/>
);
case "waitForInvite":
case "canKnock": {
wasInWaitForInviteState.current =
wasInWaitForInviteState.current ||
groupCallState.kind === "waitForInvite";
const knock =
groupCallState.kind === "canKnock" ? groupCallState.knock : null;
const label: string | JSX.Element =
groupCallState.kind === "canKnock" ? (
t("lobby.ask_to_join")
) : (
<>
{t("lobby.waiting_for_invite")}
<CheckIcon />
</>
); );
case "waitForInvite": return (
case "canKnock": { <LobbyView
const knock = client={client!}
groupCallState.kind === "canKnock" ? groupCallState.knock : null; matrixInfo={{
const label: string | JSX.Element = userId: client!.getUserId() ?? "",
groupCallState.kind === "canKnock" ? ( displayName: userDisplayName ?? "",
t("lobby.ask_to_join") avatarUrl: avatarUrl ?? "",
) : ( roomAlias: null,
<> roomId: groupCallState.roomSummary.room_id,
{t("lobby.waiting_for_invite")} roomName: groupCallState.roomSummary.name ?? "",
<CheckIcon /> roomAvatar: groupCallState.roomSummary.avatar_url ?? null,
</> e2eeSystem: {
); kind: groupCallState.roomSummary["im.nheko.summary.encryption"]
return ( ? E2eeType.PER_PARTICIPANT
<LobbyView : E2eeType.NONE,
client={client!} },
matrixInfo={{ }}
userId: client!.getUserId() ?? "", onEnter={(): void => knock?.()}
displayName: userDisplayName ?? "", enterLabel={label}
avatarUrl: avatarUrl ?? "", waitingForInvite={groupCallState.kind === "waitForInvite"}
roomAlias: null, confineToRoom={confineToRoom}
roomId: groupCallState.roomSummary.room_id, hideHeader={hideHeader}
roomName: groupCallState.roomSummary.name ?? "", participantCount={null}
roomAvatar: groupCallState.roomSummary.avatar_url ?? null, muteStates={muteStates}
e2eeSystem: { onShareClick={null}
kind: groupCallState.roomSummary[ />
"im.nheko.summary.encryption" );
]
? E2eeType.PER_PARTICIPANT
: E2eeType.NONE,
},
}}
onEnter={(): void => knock?.()}
enterLabel={label}
waitingForInvite={groupCallState.kind === "waitForInvite"}
confineToRoom={confineToRoom}
hideHeader={hideHeader}
participantCount={null}
muteStates={muteStates}
onShareClick={null}
/>
);
}
default:
return <> </>;
} }
}, case "loading":
[ return (
client, <FullScreenView>
passwordlessUser, <h1>{t("common.loading")}</h1>
confineToRoom, </FullScreenView>
preload, );
skipLobby, case "failed":
hideHeader, wasInWaitForInviteState.current = false;
muteStates, if ((groupCallState.error as MatrixError).errcode === "M_NOT_FOUND") {
t, return (
userDisplayName, <FullScreenView>
avatarUrl, <Heading>{t("group_call_loader.failed_heading")}</Heading>
], <Text>{t("group_call_loader.failed_text")}</Text>
); {/* XXX: A 'create it for me' button would be the obvious UX here. Two screens already have
dupes of this flow, let's make a common component and put it here. */}
<Link to="/">{t("common.home")}</Link>
</FullScreenView>
);
} else if (groupCallState.error instanceof CallTerminatedMessage) {
return (
<FullScreenView>
<Heading>{groupCallState.error.message}</Heading>
<Text>{groupCallState.error.messageBody}</Text>
{groupCallState.error.reason && (
<>
{t("group_call_loader.reason")}:
<Text size="sm">"{groupCallState.error.reason}"</Text>
</>
)}
<Link to="/">{t("common.home")}</Link>
</FullScreenView>
);
} else {
return <ErrorView error={groupCallState.error} />;
}
default:
return <> </>;
}
};
let content: ReactNode; let content: ReactNode;
if (loading || isRegistering) { if (loading || isRegistering) {
@@ -170,15 +203,7 @@ export const RoomPage: FC = () => {
// TODO: This doesn't belong here, the app routes need to be reworked // TODO: This doesn't belong here, the app routes need to be reworked
content = <HomePage />; content = <HomePage />;
} else { } else {
content = ( content = groupCallView();
<GroupCallLoader
client={client}
roomIdOrAlias={roomIdOrAlias}
viaServers={viaServers}
>
{groupCallView}
</GroupCallLoader>
);
} }
return ( return (

View File

@@ -117,8 +117,8 @@ export class CallTerminatedMessage extends Error {
} }
export const useLoadGroupCall = ( export const useLoadGroupCall = (
client: MatrixClient, client: MatrixClient | undefined,
roomIdOrAlias: string, roomIdOrAlias: string | null,
viaServers: string[], viaServers: string[],
): GroupCallStatus => { ): GroupCallStatus => {
const [state, setState] = useState<GroupCallStatus>({ kind: "loading" }); const [state, setState] = useState<GroupCallStatus>({ kind: "loading" });
@@ -159,6 +159,9 @@ export const useLoadGroupCall = (
?.getContent().reason; ?.getContent().reason;
useEffect(() => { useEffect(() => {
if (!client || !roomIdOrAlias) {
return;
}
const getRoomByAlias = async (alias: string): Promise<Room> => { const getRoomByAlias = async (alias: string): Promise<Room> => {
// We lowercase the localpart when we create the room, so we must lowercase // 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 // it here too (we just do the whole alias). We can't do the same to room IDs