Signed-off-by: Timo K <toger5@hotmail.de>
This commit is contained in:
Timo K
2024-04-05 16:51:27 +02:00
committed by Andrew Ferrazzutti
parent 7d58d37ebc
commit 7aafeee369
8 changed files with 153 additions and 38 deletions

View File

@@ -81,8 +81,10 @@
"layout_grid_label": "Grid",
"layout_spotlight_label": "Spotlight",
"lobby": {
"ask_to_join": "Ask to join call",
"join_button": "Join call",
"leave_button": "Back to recents"
"leave_button": "Back to recents",
"waiting_for_invite": "Request sent"
},
"log_in": "Log In",
"logging_in": "Logging in…",

View File

@@ -57,6 +57,7 @@ export function GroupCallLoader({
switch (groupCallState.kind) {
case "loaded":
case "waitForInvite":
case "canKnock":
return <>{children(groupCallState)}</>;
case "loading":
return (

View File

@@ -37,7 +37,7 @@ import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { useProfile } from "../profile/useProfile";
import { findDeviceByName } from "../media-utils";
import { ActiveCall } from "./InCallView";
import { MuteStates, useMuteStates } from "./MuteStates";
import { MUTE_PARTICIPANT_COUNT, MuteStates } from "./MuteStates";
import { useMediaDevices, MediaDevices } from "../livekit/MediaDevicesContext";
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
import { enterRTCSession, leaveRTCSession } from "../rtcSessionHelpers";
@@ -64,6 +64,7 @@ interface Props {
skipLobby: boolean;
hideHeader: boolean;
rtcSession: MatrixRTCSession;
muteStates: MuteStates;
}
export const GroupCallView: FC<Props> = ({
@@ -74,10 +75,23 @@ export const GroupCallView: FC<Props> = ({
skipLobby,
hideHeader,
rtcSession,
muteStates,
}) => {
const memberships = useMatrixRTCSessionMemberships(rtcSession);
const isJoined = useMatrixRTCSessionJoinState(rtcSession);
// The mute state reactively gets updated once the participant count reaches the threshold.
// The user then still is able to unmute again.
// The more common case is that the user is muted from the start (participant count is already over the threshold).
const [autoMuteHappened, setAutoMuteHappened] = useState(false);
useEffect(() => {
if (autoMuteHappened) return;
if (memberships.length >= MUTE_PARTICIPANT_COUNT) {
muteStates.audio.setEnabled?.(false);
setAutoMuteHappened(true);
}
}, [autoMuteHappened, memberships, muteStates.audio]);
useEffect(() => {
window.rtcSession = rtcSession;
return () => {
@@ -122,7 +136,6 @@ export const GroupCallView: FC<Props> = ({
const latestDevices = useRef<MediaDevices>();
latestDevices.current = deviceContext;
const muteStates = useMuteStates(memberships.length);
const latestMuteStates = useRef<MuteStates>();
latestMuteStates.current = muteStates;

View File

@@ -24,6 +24,10 @@ limitations under the License.
overflow: hidden;
height: 100%;
}
.wait {
color: black !important;
background-color: rgba(255, 255, 255, 0.67) !important;
}
@media (max-width: 500px) {
.join {

View File

@@ -21,8 +21,8 @@ import { Button, Link } from "@vector-im/compound-web";
import classNames from "classnames";
import { useHistory } from "react-router-dom";
import styles from "./LobbyView.module.css";
import inCallStyles from "./InCallView.module.css";
import styles from "./LobbyView.module.css";
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
import { useLocationNavigation } from "../useLocationNavigation";
import { MatrixInfo, VideoPreview } from "./VideoPreview";
@@ -43,10 +43,12 @@ interface Props {
matrixInfo: MatrixInfo;
muteStates: MuteStates;
onEnter: () => void;
enterLabel?: JSX.Element;
confineToRoom: boolean;
hideHeader: boolean;
participantCount: number;
onShareClick: (() => void) | null;
waitingForInvite?: boolean;
}
export const LobbyView: FC<Props> = ({
@@ -54,10 +56,12 @@ export const LobbyView: FC<Props> = ({
matrixInfo,
muteStates,
onEnter,
enterLabel,
confineToRoom,
hideHeader,
participantCount,
onShareClick,
waitingForInvite,
}) => {
const { t } = useTranslation();
useLocationNavigation();
@@ -117,12 +121,18 @@ export const LobbyView: FC<Props> = ({
<div className={styles.content}>
<VideoPreview matrixInfo={matrixInfo} muteStates={muteStates}>
<Button
className={styles.join}
size="lg"
onClick={onEnter}
className={classNames(
"cpd-theme-dark",
styles.join,
waitingForInvite ? styles.wait : "",
)}
size={waitingForInvite ? "sm" : "lg"}
onClick={() => {
if (!waitingForInvite) onEnter();
}}
data-testid="lobby_joinCall"
>
{t("lobby.join_button")}
{enterLabel ?? t("lobby.join_button")}
</Button>
</VideoPreview>
{!recentsButtonInFooter && recentsButton}

View File

@@ -20,10 +20,11 @@ import { MediaDevice, useMediaDevices } from "../livekit/MediaDevicesContext";
import { useReactiveState } from "../useReactiveState";
/**
* If there already is this many participants in the call, we automatically mute
* the user
* If there already are this many participants in the call, we automatically mute
* the user.
*/
const MUTE_PARTICIPANT_COUNT = 8;
// TODO set back to 8. for demo purposes we use 1 here.
export const MUTE_PARTICIPANT_COUNT = 1;
interface DeviceAvailable {
enabled: boolean;
@@ -51,26 +52,27 @@ function useMuteState(
device: MediaDevice,
enabledByDefault: () => boolean,
): MuteState {
const [enabled, setEnabled] = useReactiveState<boolean>(
(prev) => device.available.length > 0 && (prev ?? enabledByDefault()),
const [enabled, setEnabled] = useReactiveState<boolean | undefined>(
(prev) =>
device.available.length > 0 ? prev ?? enabledByDefault() : undefined,
[device],
);
return useMemo(
() =>
device.available.length === 0
? deviceUnavailable
: { enabled, setEnabled },
: {
enabled: enabled ?? false,
setEnabled: setEnabled as Dispatch<SetStateAction<boolean>>,
},
[device, enabled, setEnabled],
);
}
export function useMuteStates(participantCount: number): MuteStates {
export function useMuteStates(): MuteStates {
const devices = useMediaDevices();
const audio = useMuteState(
devices.audioInput,
() => participantCount <= MUTE_PARTICIPANT_COUNT,
);
const audio = useMuteState(devices.audioInput, () => true);
const video = useMuteState(devices.videoInput, () => true);
return useMemo(() => ({ audio, video }), [audio, video]);

View File

@@ -16,6 +16,8 @@ limitations under the License.
import { FC, useEffect, useState, useCallback, ReactNode } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { useTranslation } from "react-i18next";
import CheckIcon from "@vector-im/compound-design-tokens/icons/check.svg?react";
import { useClientLegacy } from "../ClientContext";
import { ErrorView, LoadingView } from "../FullScreenView";
@@ -30,7 +32,10 @@ import { platform } from "../Platform";
import { AppSelectionModal } from "./AppSelectionModal";
import { widget } from "../widget";
import { GroupCallStatus } from "./useLoadGroupCall";
import { WaitForInviteView } from "./WaitForInviteView";
import { LobbyView } from "./LobbyView";
import { E2eeType } from "../e2ee/e2eeType";
import { useProfile } from "../profile/useProfile";
import { useMuteStates } from "./MuteStates";
export const RoomPage: FC = () => {
const {
@@ -41,7 +46,7 @@ export const RoomPage: FC = () => {
displayName,
skipLobby,
} = useUrlParams();
const { t } = useTranslation();
const { roomAlias, roomId, viaServers } = useRoomIdentifier();
const roomIdOrAlias = roomId ?? roomAlias;
@@ -49,17 +54,14 @@ export const RoomPage: FC = () => {
logger.error("No room specified");
}
const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics();
const { registerPasswordlessUser } = useRegisterPasswordlessUser();
const [isRegistering, setIsRegistering] = useState(false);
useEffect(() => {
// During the beta, opt into analytics by default
if (optInAnalytics === null && setOptInAnalytics) setOptInAnalytics(true);
}, [optInAnalytics, setOptInAnalytics]);
const { loading, authenticated, client, error, passwordlessUser } =
useClientLegacy();
const { avatarUrl, displayName: userDisplayName } = useProfile(client);
const muteStates = useMuteStates();
useEffect(() => {
// If we've finished loading, are not already authed and we've been given a display name as
@@ -78,6 +80,12 @@ export const RoomPage: FC = () => {
registerPasswordlessUser,
]);
const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics();
useEffect(() => {
// During the beta, opt into analytics by default
if (optInAnalytics === null && setOptInAnalytics) setOptInAnalytics(true);
}, [optInAnalytics, setOptInAnalytics]);
const groupCallView = useCallback(
(groupCallState: GroupCallStatus) => {
switch (groupCallState.kind) {
@@ -91,19 +99,66 @@ export const RoomPage: FC = () => {
preload={preload}
skipLobby={skipLobby}
hideHeader={hideHeader}
muteStates={muteStates}
/>
);
case "waitForInvite":
case "canKnock": {
const knock =
groupCallState.kind === "canKnock" ? groupCallState.knock : null;
const label =
groupCallState.kind === "canKnock" ? (
<>{t("lobby.ask_to_join")}</>
) : (
<>
{t("lobby.waiting_for_invite")}
<CheckIcon />
</>
);
return (
<WaitForInviteView
hideHeader={hideHeader}
<LobbyView
client={client!}
roomSummary={groupCallState.roomSummary}
matrixInfo={{
userId: client!.getUserId() ?? "",
displayName: userDisplayName ?? "",
avatarUrl: avatarUrl ?? "",
roomAlias: groupCallState.roomSummary.canonical_alias ?? "",
roomId: groupCallState.roomSummary.room_id,
roomName: groupCallState.roomSummary.name ?? "",
roomAvatar: groupCallState.roomSummary.avatar_url ?? null,
e2eeSystem: {
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={0}
muteStates={muteStates}
onShareClick={null}
/>
);
}
}
},
[client, passwordlessUser, confineToRoom, preload, hideHeader, skipLobby],
[
client,
passwordlessUser,
confineToRoom,
preload,
skipLobby,
hideHeader,
muteStates,
t,
userDisplayName,
avatarUrl,
],
);
let content: ReactNode;

View File

@@ -46,11 +46,18 @@ export type GroupCallWaitForInvite = {
roomSummary: RoomSummary;
};
export type GroupCallCanKnock = {
kind: "canKnock";
roomSummary: RoomSummary;
knock: () => void;
};
export type GroupCallStatus =
| GroupCallLoaded
| GroupCallLoadFailed
| GroupCallLoading
| GroupCallWaitForInvite;
| GroupCallWaitForInvite
| GroupCallCanKnock;
export interface GroupCallLoadState {
error?: Error | KnockRejectError;
groupCall?: GroupCall;
@@ -73,7 +80,7 @@ export interface RoomSummary {
join_rule?: JoinRule.Knock | JoinRule.Public; // Added by MSC2403
room_type?: RoomType;
membership?: Membership;
is_encrypted: boolean;
"im.nheko.summary.encryption"?: boolean;
}
export const useLoadGroupCall = (
@@ -110,10 +117,11 @@ export const useLoadGroupCall = (
const getRoomByKnocking = async (
roomId: string,
viaServers: string[],
onKnockSent: () => void,
): Promise<Room> => {
let joinedRoom: Room | null = null;
await client.knockRoom(roomId);
onKnockSent();
const invitePromise = new Promise<void>((resolve, reject) => {
client.on(
RoomEvent.MyMembership,
@@ -168,8 +176,12 @@ export const useLoadGroupCall = (
"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 the room does not exist we first search for it with viaServers
// We need to cast it to our own RoomSummary interface since we use the im.nheko.summary.encryption field.
const roomSummary = (await client.getRoomSummary(
roomId,
viaServers,
)) as unknown as RoomSummary;
if (room?.getMyMembership() === KnownMembership.Ban) {
throw new BannedError();
} else {
@@ -178,8 +190,24 @@ export const useLoadGroupCall = (
viaServers,
});
} else if (roomSummary.join_rule === JoinRule.Knock) {
setState({ kind: "waitForInvite", roomSummary });
room = await getRoomByKnocking(roomSummary.room_id, viaServers);
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.`,