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:
@@ -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
|
||||||
|
|||||||
@@ -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…",
|
||||||
|
|||||||
@@ -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} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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]);
|
||||||
|
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user