Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
Use native js-sdk group call support (#9625)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
robintown authored Nov 28, 2022
1 parent 3c7781a commit 2c612d5
Show file tree
Hide file tree
Showing 20 changed files with 375 additions and 559 deletions.
35 changes: 21 additions & 14 deletions src/components/views/beacon/RoomCallBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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;
};

Expand Down
52 changes: 25 additions & 27 deletions src/components/views/messages/CallEvent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -35,38 +34,38 @@ 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;
}

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">
Expand All @@ -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>
Expand All @@ -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();
Expand Down Expand Up @@ -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}
/>;
});
Expand All @@ -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
Expand All @@ -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} />;
});
11 changes: 4 additions & 7 deletions src/components/views/rooms/LiveContentSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)}
/>;
}
6 changes: 3 additions & 3 deletions src/components/views/rooms/RoomHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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" />
Expand Down
9 changes: 3 additions & 6 deletions src/components/views/rooms/RoomTileCallSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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;
Expand All @@ -53,6 +50,6 @@ export const RoomTileCallSummary: FC<Props> = ({ call }) => {
type={LiveContentType.Video}
text={text}
active={active}
participantCount={participants.size}
participantCount={useParticipantCount(call)}
/>;
};
22 changes: 12 additions & 10 deletions src/components/views/voip/CallDuration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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} />;
};
Loading

0 comments on commit 2c612d5

Please sign in to comment.