Use native js-sdk group call support (#9625)

* Use native js-sdk group call support

Now that the js-sdk supports group calls natively, our group call implementation can be simplified a bit. Switching to the js-sdk implementation also brings the react-sdk up to date with recent MSC3401 changes, and adds support for joining calls from multiple devices. (So, the previous logic which sent to-device messages to prevent multi-device sessions is no longer necessary.)

* Fix strings

* Fix strict type errors
This commit is contained in:
Robin 2022-11-28 16:37:32 -05:00 committed by GitHub
parent 3c7781a561
commit 2c612d5aa1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 383 additions and 567 deletions

View file

@ -15,34 +15,34 @@ limitations under the License.
*/
import React, { useCallback } from "react";
import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "../../../languageHandler";
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
import dispatcher, { defaultDispatcher } from "../../../dispatcher/dispatcher";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { Action } from "../../../dispatcher/actions";
import { Call, ConnectionState, ElementCall } from "../../../models/Call";
import { ConnectionState, ElementCall } from "../../../models/Call";
import { useCall } from "../../../hooks/useCall";
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
import {
OwnBeaconStore,
OwnBeaconStoreEvent,
} from "../../../stores/OwnBeaconStore";
import { CallDurationFromEvent } from "../voip/CallDuration";
import { GroupCallDuration } from "../voip/CallDuration";
import { SdkContextClass } from "../../../contexts/SDKContext";
interface RoomCallBannerProps {
roomId: Room["roomId"];
call: Call;
call: ElementCall;
}
const RoomCallBannerInner: React.FC<RoomCallBannerProps> = ({
roomId,
call,
}) => {
const callEvent: MatrixEvent | null = (call as ElementCall)?.groupCall;
const connect = useCallback(
(ev: ButtonEvent) => {
ev.preventDefault();
@ -57,15 +57,23 @@ const RoomCallBannerInner: React.FC<RoomCallBannerProps> = ({
);
const onClick = useCallback(() => {
const event = call.groupCall.room.currentState.getStateEvents(
EventType.GroupCallPrefix, call.groupCall.groupCallId,
);
if (event === null) {
logger.error("Couldn't find a group call event to jump to");
return;
}
dispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: roomId,
metricsTrigger: undefined,
event_id: callEvent.getId(),
event_id: event.getId(),
scroll_into_view: true,
highlighted: true,
});
}, [callEvent, roomId]);
}, [call, roomId]);
return (
<div
@ -74,7 +82,7 @@ const RoomCallBannerInner: React.FC<RoomCallBannerProps> = ({
>
<div className="mx_RoomCallBanner_text">
<span className="mx_RoomCallBanner_label">{ _t("Video call") }</span>
<CallDurationFromEvent mxEvent={callEvent} />
<GroupCallDuration groupCall={call.groupCall} />
</div>
<AccessibleButton
@ -119,12 +127,11 @@ const RoomCallBanner: React.FC<Props> = ({ roomId }) => {
}
// Split into outer/inner to avoid watching various parts if there is no call
if (call) {
// No banner if the call is connected (or connecting/disconnecting)
if (call.connectionState !== ConnectionState.Disconnected) return null;
return <RoomCallBannerInner call={call} roomId={roomId} />;
// No banner if the call is connected (or connecting/disconnecting)
if (call !== null && call.connectionState === ConnectionState.Disconnected) {
return <RoomCallBannerInner call={call as ElementCall} roomId={roomId} />;
}
return null;
};

View file

@ -18,14 +18,13 @@ import React, { forwardRef, useCallback, useContext, useMemo } from "react";
import type { MatrixEvent } from "matrix-js-sdk/src/models/event";
import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { Call, ConnectionState } from "../../../models/Call";
import { ConnectionState, ElementCall } from "../../../models/Call";
import { _t } from "../../../languageHandler";
import {
useCall,
useConnectionState,
useJoinCallButtonDisabled,
useJoinCallButtonTooltip,
useParticipants,
useJoinCallButtonDisabledTooltip,
useParticipatingMembers,
} from "../../../hooks/useCall";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
@ -35,18 +34,18 @@ import MemberAvatar from "../avatars/MemberAvatar";
import { LiveContentSummary, LiveContentType } from "../rooms/LiveContentSummary";
import FacePile from "../elements/FacePile";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { CallDuration, CallDurationFromEvent } from "../voip/CallDuration";
import { CallDuration, GroupCallDuration } from "../voip/CallDuration";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
const MAX_FACES = 8;
interface ActiveCallEventProps {
mxEvent: MatrixEvent;
participants: Set<RoomMember>;
call: ElementCall | null;
participatingMembers: RoomMember[];
buttonText: string;
buttonKind: string;
buttonTooltip?: string;
buttonDisabled?: boolean;
buttonDisabledTooltip?: string;
onButtonClick: ((ev: ButtonEvent) => void) | null;
}
@ -54,19 +53,19 @@ const ActiveCallEvent = forwardRef<any, ActiveCallEventProps>(
(
{
mxEvent,
participants,
call,
participatingMembers,
buttonText,
buttonKind,
buttonDisabled,
buttonTooltip,
buttonDisabledTooltip,
onButtonClick,
},
ref,
) => {
const senderName = useMemo(() => mxEvent.sender?.name ?? mxEvent.getSender(), [mxEvent]);
const facePileMembers = useMemo(() => [...participants].slice(0, MAX_FACES), [participants]);
const facePileOverflow = participants.size > facePileMembers.length;
const facePileMembers = useMemo(() => participatingMembers.slice(0, MAX_FACES), [participatingMembers]);
const facePileOverflow = participatingMembers.length > facePileMembers.length;
return <div className="mx_CallEvent_wrapper" ref={ref}>
<div className="mx_CallEvent mx_CallEvent_active">
@ -85,17 +84,17 @@ const ActiveCallEvent = forwardRef<any, ActiveCallEventProps>(
type={LiveContentType.Video}
text={_t("Video call")}
active={false}
participantCount={participants.size}
participantCount={participatingMembers.length}
/>
<FacePile members={facePileMembers} faceSize={24} overflow={facePileOverflow} />
</div>
<CallDurationFromEvent mxEvent={mxEvent} />
{ call && <GroupCallDuration groupCall={call.groupCall} /> }
<AccessibleTooltipButton
className="mx_CallEvent_button"
kind={buttonKind}
disabled={onButtonClick === null || buttonDisabled}
disabled={onButtonClick === null || buttonDisabledTooltip !== undefined}
onClick={onButtonClick}
tooltip={buttonTooltip}
tooltip={buttonDisabledTooltip}
>
{ buttonText }
</AccessibleTooltipButton>
@ -106,14 +105,13 @@ const ActiveCallEvent = forwardRef<any, ActiveCallEventProps>(
interface ActiveLoadedCallEventProps {
mxEvent: MatrixEvent;
call: Call;
call: ElementCall;
}
const ActiveLoadedCallEvent = forwardRef<any, ActiveLoadedCallEventProps>(({ mxEvent, call }, ref) => {
const connectionState = useConnectionState(call);
const participants = useParticipants(call);
const joinCallButtonTooltip = useJoinCallButtonTooltip(call);
const joinCallButtonDisabled = useJoinCallButtonDisabled(call);
const participatingMembers = useParticipatingMembers(call);
const joinCallButtonDisabledTooltip = useJoinCallButtonDisabledTooltip(call);
const connect = useCallback((ev: ButtonEvent) => {
ev.preventDefault();
@ -142,11 +140,11 @@ const ActiveLoadedCallEvent = forwardRef<any, ActiveLoadedCallEventProps>(({ mxE
return <ActiveCallEvent
ref={ref}
mxEvent={mxEvent}
participants={participants}
call={call}
participatingMembers={participatingMembers}
buttonText={buttonText}
buttonKind={buttonKind}
buttonDisabled={joinCallButtonDisabled}
buttonTooltip={joinCallButtonTooltip}
buttonDisabledTooltip={joinCallButtonDisabledTooltip ?? undefined}
onButtonClick={onButtonClick}
/>;
});
@ -159,7 +157,6 @@ interface CallEventProps {
* An event tile representing an active or historical Element call.
*/
export const CallEvent = forwardRef<any, CallEventProps>(({ mxEvent }, ref) => {
const noParticipants = useMemo(() => new Set<RoomMember>(), []);
const client = useContext(MatrixClientContext);
const call = useCall(mxEvent.getRoomId()!);
const latestEvent = client.getRoom(mxEvent.getRoomId())!.currentState
@ -180,12 +177,13 @@ export const CallEvent = forwardRef<any, CallEventProps>(({ mxEvent }, ref) => {
return <ActiveCallEvent
ref={ref}
mxEvent={mxEvent}
participants={noParticipants}
call={null}
participatingMembers={[]}
buttonText={_t("Join")}
buttonKind="primary"
onButtonClick={null}
/>;
}
return <ActiveLoadedCallEvent mxEvent={mxEvent} call={call} ref={ref} />;
return <ActiveLoadedCallEvent mxEvent={mxEvent} call={call as ElementCall} ref={ref} />;
});

View file

@ -19,7 +19,7 @@ import classNames from "classnames";
import { _t } from "../../../languageHandler";
import { Call } from "../../../models/Call";
import { useParticipants } from "../../../hooks/useCall";
import { useParticipantCount } from "../../../hooks/useCall";
export enum LiveContentType {
Video,
@ -62,13 +62,10 @@ interface LiveContentSummaryWithCallProps {
call: Call;
}
export function LiveContentSummaryWithCall({ call }: LiveContentSummaryWithCallProps) {
const participants = useParticipants(call);
return <LiveContentSummary
export const LiveContentSummaryWithCall: FC<LiveContentSummaryWithCallProps> = ({ call }) =>
<LiveContentSummary
type={LiveContentType.Video}
text={_t("Video")}
active={false}
participantCount={participants.size}
participantCount={useParticipantCount(call)}
/>;
}

View file

@ -66,7 +66,7 @@ import IconizedContextMenu, {
IconizedContextMenuRadio,
} from "../context_menus/IconizedContextMenu";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { CallDurationFromEvent } from "../voip/CallDuration";
import { GroupCallDuration } from "../voip/CallDuration";
import { Alignment } from "../elements/Tooltip";
import RoomCallBanner from '../beacon/RoomCallBanner';
@ -512,7 +512,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
}
if (this.props.viewingCall && this.props.activeCall instanceof ElementCall) {
startButtons.push(<CallLayoutSelector call={this.props.activeCall} />);
startButtons.push(<CallLayoutSelector key="layout" call={this.props.activeCall} />);
}
if (!this.props.viewingCall && this.props.onForgetClick) {
@ -685,7 +685,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
{ _t("Video call") }
</div>
{ this.props.activeCall instanceof ElementCall && (
<CallDurationFromEvent mxEvent={this.props.activeCall.groupCall} />
<GroupCallDuration groupCall={this.props.activeCall.groupCall} />
) }
{ /* Empty topic element to fill out space */ }
<div className="mx_RoomHeader_topic" />

View file

@ -18,7 +18,7 @@ import React, { FC } from "react";
import type { Call } from "../../../models/Call";
import { _t } from "../../../languageHandler";
import { useConnectionState, useParticipants } from "../../../hooks/useCall";
import { useConnectionState, useParticipantCount } from "../../../hooks/useCall";
import { ConnectionState } from "../../../models/Call";
import { LiveContentSummary, LiveContentType } from "./LiveContentSummary";
@ -27,13 +27,10 @@ interface Props {
}
export const RoomTileCallSummary: FC<Props> = ({ call }) => {
const connectionState = useConnectionState(call);
const participants = useParticipants(call);
let text: string;
let active: boolean;
switch (connectionState) {
switch (useConnectionState(call)) {
case ConnectionState.Disconnected:
text = _t("Video");
active = false;
@ -53,6 +50,6 @@ export const RoomTileCallSummary: FC<Props> = ({ call }) => {
type={LiveContentType.Video}
text={text}
active={active}
participantCount={participants.size}
participantCount={useParticipantCount(call)}
/>;
};

View file

@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { FC, useState, useEffect } from "react";
import React, { FC, useState, useEffect, memo } from "react";
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import type { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { formatCallTime } from "../../../DateUtils";
interface CallDurationProps {
@ -26,26 +26,28 @@ interface CallDurationProps {
/**
* A call duration counter.
*/
export const CallDuration: FC<CallDurationProps> = ({ delta }) => {
export const CallDuration: FC<CallDurationProps> = memo(({ delta }) => {
// Clock desync could lead to a negative duration, so just hide it if that happens
if (delta <= 0) return null;
return <div className="mx_CallDuration">{ formatCallTime(new Date(delta)) }</div>;
};
});
interface CallDurationFromEventProps {
mxEvent: MatrixEvent;
interface GroupCallDurationProps {
groupCall: GroupCall;
}
/**
* A call duration counter that automatically counts up, given the event that
* started the call.
* A call duration counter that automatically counts up, given a live GroupCall
* object.
*/
export const CallDurationFromEvent: FC<CallDurationFromEventProps> = ({ mxEvent }) => {
export const GroupCallDuration: FC<GroupCallDurationProps> = ({ groupCall }) => {
const [now, setNow] = useState(() => Date.now());
useEffect(() => {
const timer = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(timer);
}, []);
return <CallDuration delta={now - mxEvent.getTs()} />;
return groupCall.creationTs === null
? null
: <CallDuration delta={now - groupCall.creationTs} />;
};

View file

@ -25,9 +25,8 @@ import { Call, CallEvent, ElementCall, isConnected } from "../../../models/Call"
import {
useCall,
useConnectionState,
useJoinCallButtonDisabled,
useJoinCallButtonTooltip,
useParticipants,
useJoinCallButtonDisabledTooltip,
useParticipatingMembers,
} from "../../../hooks/useCall";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import AppTile from "../elements/AppTile";
@ -116,12 +115,11 @@ const MAX_FACES = 8;
interface LobbyProps {
room: Room;
connect: () => Promise<void>;
joinCallButtonTooltip?: string;
joinCallButtonDisabled?: boolean;
joinCallButtonDisabledTooltip?: string;
children?: ReactNode;
}
export const Lobby: FC<LobbyProps> = ({ room, joinCallButtonDisabled, joinCallButtonTooltip, connect, children }) => {
export const Lobby: FC<LobbyProps> = ({ room, joinCallButtonDisabledTooltip, connect, children }) => {
const [connecting, setConnecting] = useState(false);
const me = useMemo(() => room.getMember(room.myUserId)!, [room]);
const videoRef = useRef<HTMLVideoElement>(null);
@ -246,10 +244,10 @@ export const Lobby: FC<LobbyProps> = ({ room, joinCallButtonDisabled, joinCallBu
<AccessibleTooltipButton
className="mx_CallView_connectButton"
kind="primary"
disabled={connecting || joinCallButtonDisabled}
disabled={connecting || joinCallButtonDisabledTooltip !== undefined}
onClick={onConnectClick}
label={_t("Join")}
tooltip={connecting ? _t("Connecting") : joinCallButtonTooltip}
tooltip={connecting ? _t("Connecting") : joinCallButtonDisabledTooltip}
alignment={Alignment.Bottom}
/>
</div>;
@ -331,9 +329,8 @@ interface JoinCallViewProps {
const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call }) => {
const cli = useContext(MatrixClientContext);
const connected = isConnected(useConnectionState(call));
const participants = useParticipants(call);
const joinCallButtonTooltip = useJoinCallButtonTooltip(call);
const joinCallButtonDisabled = useJoinCallButtonDisabled(call);
const members = useParticipatingMembers(call);
const joinCallButtonDisabledTooltip = useJoinCallButtonDisabledTooltip(call);
const connect = useCallback(async () => {
// Disconnect from any other active calls first, since we don't yet support holding
@ -347,12 +344,12 @@ const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call }) => {
let lobby: JSX.Element | null = null;
if (!connected) {
let facePile: JSX.Element | null = null;
if (participants.size) {
const shownMembers = [...participants].slice(0, MAX_FACES);
const overflow = participants.size > shownMembers.length;
if (members.length) {
const shownMembers = members.slice(0, MAX_FACES);
const overflow = members.length > shownMembers.length;
facePile = <div className="mx_CallView_participants">
{ _t("%(count)s people joined", { count: participants.size }) }
{ _t("%(count)s people joined", { count: members.length }) }
<FacePile members={shownMembers} faceSize={24} overflow={overflow} />
</div>;
}
@ -360,8 +357,7 @@ const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call }) => {
lobby = <Lobby
room={room}
connect={connect}
joinCallButtonTooltip={joinCallButtonTooltip ?? undefined}
joinCallButtonDisabled={joinCallButtonDisabled}
joinCallButtonDisabledTooltip={joinCallButtonDisabledTooltip ?? undefined}
>
{ facePile }
</Lobby>;