Support using Element Call for voice calls in DMs (#30817)
* Add voiceOnly options. * tweaks * Nearly working demo * Lots of minor fixes * Better working version * remove unused payload * bits and pieces * Cleanup based on new hints * Simple refactor for skipLobby (and remove returnToLobby) * Tidyup * Remove unused tests * Update tests for voice calls * Add video room support. * Add a test for video rooms * tidy * remove console log line * lint and tests * Bunch of fixes * Fixes * Use correct title * make linter happier * Update tests * cleanup * Drop only * update snaps * Document * lint * Update snapshots * Remove duplicate test * add brackets * fix jest
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { type Room, RoomEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||
|
||||
import dispatcher from "../../../dispatcher/dispatcher";
|
||||
import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
@@ -19,7 +20,7 @@ import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
||||
import { useEventEmitter, useEventEmitterState, useTypedEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import { DefaultTagID } from "../../../stores/room-list/models";
|
||||
import { useCall, useConnectionState, useParticipantCount } from "../../../hooks/useCall";
|
||||
import { type ConnectionState } from "../../../models/Call";
|
||||
import { CallEvent, type ConnectionState } from "../../../models/Call";
|
||||
import { NotificationStateEvents } from "../../../stores/notifications/NotificationState";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
|
||||
@@ -67,6 +68,10 @@ export interface RoomListItemViewState {
|
||||
* Whether there are participants in the call.
|
||||
*/
|
||||
hasParticipantInCall: boolean;
|
||||
/**
|
||||
* Whether the call is a voice or video call.
|
||||
*/
|
||||
callType: CallType | undefined;
|
||||
/**
|
||||
* Pre-rendered and translated preview for the latest message in the room, or undefined
|
||||
* if no preview should be shown.
|
||||
@@ -123,10 +128,10 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState {
|
||||
// EC video call or video room
|
||||
const call = useCall(room.roomId);
|
||||
const connectionState = useConnectionState(call);
|
||||
const hasParticipantInCall = useParticipantCount(call) > 0;
|
||||
const participantCount = useParticipantCount(call);
|
||||
const callConnectionState = call ? connectionState : null;
|
||||
|
||||
const showNotificationDecoration = hasVisibleNotification || hasParticipantInCall;
|
||||
const showNotificationDecoration = hasVisibleNotification || participantCount > 0;
|
||||
|
||||
// Actions
|
||||
|
||||
@@ -138,6 +143,9 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState {
|
||||
});
|
||||
}, [room]);
|
||||
|
||||
const [callType, setCallType] = useState<CallType>(CallType.Video);
|
||||
useTypedEventEmitter(call ?? undefined, CallEvent.CallTypeChanged, setCallType);
|
||||
|
||||
return {
|
||||
name,
|
||||
notificationState,
|
||||
@@ -148,9 +156,10 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState {
|
||||
isBold,
|
||||
isVideoRoom,
|
||||
callConnectionState,
|
||||
hasParticipantInCall,
|
||||
hasParticipantInCall: participantCount > 0,
|
||||
messagePreview,
|
||||
showNotificationDecoration,
|
||||
callType: call ? callType : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,12 +10,10 @@ import React, { type FC } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { type Call } from "../../../models/Call";
|
||||
import { useParticipantCount } from "../../../hooks/useCall";
|
||||
|
||||
export enum LiveContentType {
|
||||
Video,
|
||||
// More coming soon
|
||||
Voice,
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -33,6 +31,7 @@ export const LiveContentSummary: FC<Props> = ({ type, text, active, participantC
|
||||
<span
|
||||
className={classNames("mx_LiveContentSummary_text", {
|
||||
mx_LiveContentSummary_text_video: type === LiveContentType.Video,
|
||||
mx_LiveContentSummary_text_voice: type === LiveContentType.Voice,
|
||||
mx_LiveContentSummary_text_active: active,
|
||||
})}
|
||||
>
|
||||
@@ -51,16 +50,3 @@ export const LiveContentSummary: FC<Props> = ({ type, text, active, participantC
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
|
||||
interface LiveContentSummaryWithCallProps {
|
||||
call: Call;
|
||||
}
|
||||
|
||||
export const LiveContentSummaryWithCall: FC<LiveContentSummaryWithCallProps> = ({ call }) => (
|
||||
<LiveContentSummary
|
||||
type={LiveContentType.Video}
|
||||
text={_t("common|video")}
|
||||
active={false}
|
||||
participantCount={useParticipantCount(call)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,8 @@ import NotificationOffIcon from "@vector-im/compound-design-tokens/assets/web/ic
|
||||
import VideoCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/video-call-solid";
|
||||
import EmailIcon from "@vector-im/compound-design-tokens/assets/web/icons/email-solid";
|
||||
import { UnreadCounter, Unread } from "@vector-im/compound-web";
|
||||
import VoiceCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/voice-call-solid";
|
||||
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||
import { Flex } from "@element-hq/web-shared-components";
|
||||
|
||||
import { type RoomNotificationState } from "../../../stores/notifications/RoomNotificationState";
|
||||
@@ -24,9 +26,9 @@ interface NotificationDecorationProps extends HTMLProps<HTMLDivElement> {
|
||||
*/
|
||||
notificationState: RoomNotificationState;
|
||||
/**
|
||||
* Whether the room has a video call.
|
||||
* Whether the room has a voice or video call.
|
||||
*/
|
||||
hasVideoCall: boolean;
|
||||
callType?: CallType;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,7 +36,7 @@ interface NotificationDecorationProps extends HTMLProps<HTMLDivElement> {
|
||||
*/
|
||||
export function NotificationDecoration({
|
||||
notificationState,
|
||||
hasVideoCall,
|
||||
callType,
|
||||
...props
|
||||
}: NotificationDecorationProps): JSX.Element | null {
|
||||
// Listen to the notification state and update the component when it changes
|
||||
@@ -58,7 +60,7 @@ export function NotificationDecoration({
|
||||
muted: notificationState.muted,
|
||||
}));
|
||||
|
||||
if (!hasAnyNotificationOrActivity && !muted && !hasVideoCall) return null;
|
||||
if (!hasAnyNotificationOrActivity && !muted && !callType) return null;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -69,7 +71,12 @@ export function NotificationDecoration({
|
||||
data-testid="notification-decoration"
|
||||
>
|
||||
{isUnsentMessage && <ErrorIcon width="20px" height="20px" fill="var(--cpd-color-icon-critical-primary)" />}
|
||||
{hasVideoCall && <VideoCallIcon width="20px" height="20px" fill="var(--cpd-color-icon-accent-primary)" />}
|
||||
{callType === CallType.Video && (
|
||||
<VideoCallIcon width="20px" height="20px" fill="var(--cpd-color-icon-accent-primary)" />
|
||||
)}
|
||||
{callType === CallType.Voice && (
|
||||
<VoiceCallIcon width="20px" height="20px" fill="var(--cpd-color-icon-accent-primary)" />
|
||||
)}
|
||||
{invited && <EmailIcon width="20px" height="20px" fill="var(--cpd-color-icon-accent-primary)" />}
|
||||
{isMention && <MentionIcon width="20px" height="20px" fill="var(--cpd-color-icon-accent-primary)" />}
|
||||
{(isMention || isNotification) && <UnreadCounter count={count || null} />}
|
||||
|
||||
@@ -132,7 +132,7 @@ export const RoomListItemView = memo(function RoomListItemView({
|
||||
<NotificationDecoration
|
||||
notificationState={vm.notificationState}
|
||||
aria-hidden={true}
|
||||
hasVideoCall={vm.hasParticipantInCall}
|
||||
callType={vm.callType}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -121,7 +121,7 @@ export enum Action {
|
||||
UpdateSystemFont = "update_system_font",
|
||||
|
||||
/**
|
||||
* Changes room based on payload parameters. Should be used with JoinRoomPayload.
|
||||
* Changes room based on payload parameters. Should be used with ViewRoomPayload.
|
||||
*/
|
||||
ViewRoom = "view_room",
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ interface BaseViewRoomPayload extends Pick<ActionPayload, "action"> {
|
||||
clear_search?: boolean; // Whether to clear the room list search
|
||||
view_call?: boolean; // Whether to view the call or call lobby for the room
|
||||
skipLobby?: boolean; // Whether to skip the call lobby when showing the call (only supported for element calls)
|
||||
voiceOnly?: boolean; // Whether the call is voice only (only supported for element calls)
|
||||
opts?: JoinRoomPayload["opts"];
|
||||
|
||||
deferred_action?: ActionPayload; // Action to fire after MatrixChat handles this ViewRoom action
|
||||
|
||||
@@ -142,11 +142,6 @@ export const useRoomCall = (
|
||||
// If there are multiple options, the user will be prompted to choose.
|
||||
const callOptions = useMemo((): PlatformCallType[] => {
|
||||
const options: PlatformCallType[] = [];
|
||||
if (memberCount <= 2) {
|
||||
options.push(PlatformCallType.LegacyCall);
|
||||
} else if (mayEditWidgets || hasJitsiWidget) {
|
||||
options.push(PlatformCallType.JitsiCall);
|
||||
}
|
||||
if (groupCallsEnabled) {
|
||||
if (hasGroupCall || mayCreateElementCalls) {
|
||||
options.push(PlatformCallType.ElementCall);
|
||||
@@ -155,6 +150,11 @@ export const useRoomCall = (
|
||||
return [PlatformCallType.ElementCall];
|
||||
}
|
||||
}
|
||||
if (memberCount <= 2) {
|
||||
options.push(PlatformCallType.LegacyCall);
|
||||
} else if (mayEditWidgets || hasJitsiWidget) {
|
||||
options.push(PlatformCallType.JitsiCall);
|
||||
}
|
||||
if (hasGroupCall && WidgetType.CALL.matches(groupCall.widget.type)) {
|
||||
// only allow joining the ongoing Element call if there is one.
|
||||
return [PlatformCallType.ElementCall];
|
||||
@@ -231,7 +231,7 @@ export const useRoomCall = (
|
||||
if (widget && promptPinWidget) {
|
||||
WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top);
|
||||
} else {
|
||||
placeCall(room, CallType.Voice, callPlatformType, evt?.shiftKey || undefined);
|
||||
placeCall(room, CallType.Voice, callPlatformType, evt?.shiftKey || undefined, true);
|
||||
}
|
||||
},
|
||||
[promptPinWidget, room, widget],
|
||||
@@ -244,7 +244,7 @@ export const useRoomCall = (
|
||||
} else {
|
||||
// If we have pressed shift then always skip the lobby, otherwise `undefined` will defer
|
||||
// to the defaults of the call implementation.
|
||||
placeCall(room, CallType.Video, callPlatformType, evt?.shiftKey || undefined);
|
||||
placeCall(room, CallType.Video, callPlatformType, evt?.shiftKey || undefined, false);
|
||||
}
|
||||
},
|
||||
[widget, promptPinWidget, room],
|
||||
@@ -279,7 +279,13 @@ export const useRoomCall = (
|
||||
const roomDoesNotExist = room instanceof LocalRoom && room.state !== LocalRoomState.CREATED;
|
||||
|
||||
// We hide the voice call button if it'd have the same effect as the video call button
|
||||
let hideVoiceCallButton = isManagedHybridWidgetEnabled(room) || !callOptions.includes(PlatformCallType.LegacyCall);
|
||||
let hideVoiceCallButton =
|
||||
isManagedHybridWidgetEnabled(room) ||
|
||||
// Disable voice calls if Legacy calls are disabled
|
||||
(!callOptions.includes(PlatformCallType.LegacyCall) &&
|
||||
// Disable voice calls in ECall if the room is a group (we only present video calls for groups of users)
|
||||
(!callOptions.includes(PlatformCallType.ElementCall) || memberCount > 2));
|
||||
|
||||
let hideVideoCallButton = false;
|
||||
// We hide both buttons if:
|
||||
// - they require widgets but widgets are disabled
|
||||
|
||||
@@ -603,6 +603,7 @@
|
||||
"video": "Video",
|
||||
"video_room": "Video room",
|
||||
"view_message": "View message",
|
||||
"voice": "Voice",
|
||||
"warning": "Warning"
|
||||
},
|
||||
"composer": {
|
||||
@@ -4096,9 +4097,11 @@
|
||||
"user_busy_description": "The user you called is busy.",
|
||||
"user_is_presenting": "%(sharerName)s is presenting",
|
||||
"video_call": "Video call",
|
||||
"video_call_incoming": "Incoming video call",
|
||||
"video_call_started": "Video call started",
|
||||
"video_call_using": "Video call using:",
|
||||
"voice_call": "Voice call",
|
||||
"voice_call_incoming": "Incoming voice call",
|
||||
"you_are_presenting": "You are presenting"
|
||||
},
|
||||
"web_default_device_name": "%(appName)s: %(browserName)s on %(osName)s",
|
||||
|
||||
@@ -84,6 +84,7 @@ export enum CallEvent {
|
||||
Participants = "participants",
|
||||
Close = "close",
|
||||
Destroy = "destroy",
|
||||
CallTypeChanged = "call_type_changed",
|
||||
}
|
||||
|
||||
interface CallEventHandlerMap {
|
||||
@@ -94,6 +95,7 @@ interface CallEventHandlerMap {
|
||||
) => void;
|
||||
[CallEvent.Close]: () => void;
|
||||
[CallEvent.Destroy]: () => void;
|
||||
[CallEvent.CallTypeChanged]: (callType: CallType) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,6 +105,18 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
|
||||
protected readonly widgetUid: string;
|
||||
protected readonly room: Room;
|
||||
|
||||
private _callType: CallType = CallType.Video;
|
||||
public get callType(): CallType {
|
||||
return this._callType;
|
||||
}
|
||||
|
||||
protected set callType(callType: CallType) {
|
||||
if (this._callType !== callType) {
|
||||
this.emit(CallEvent.CallTypeChanged, callType);
|
||||
}
|
||||
this._callType = callType;
|
||||
}
|
||||
|
||||
/**
|
||||
* The time after which device member state should be considered expired.
|
||||
*/
|
||||
@@ -544,7 +558,24 @@ export enum ElementCallIntent {
|
||||
StartCall = "start_call",
|
||||
JoinExisting = "join_existing",
|
||||
StartCallDM = "start_call_dm",
|
||||
StartCallDMVoice = "start_call_dm_voice",
|
||||
JoinExistingDM = "join_existing_dm",
|
||||
JoinExistingDMVoice = "join_existing_dm_voice",
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters to be passed during widget creation.
|
||||
* These parameters are hints only, and may not be accepted by the implementation.
|
||||
*/
|
||||
export interface WidgetGenerationParameters {
|
||||
/**
|
||||
* Skip showing the lobby screen of a call.
|
||||
*/
|
||||
skipLobby?: boolean;
|
||||
/**
|
||||
* Does the user intent to start a voice call?
|
||||
*/
|
||||
voiceOnly?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -586,7 +617,12 @@ export class ElementCall extends Call {
|
||||
* @param client The current client.
|
||||
* @param roomId The room ID for the call.
|
||||
*/
|
||||
private static appendRoomParams(params: URLSearchParams, client: MatrixClient, roomId: string): void {
|
||||
private static appendRoomParams(
|
||||
params: URLSearchParams,
|
||||
client: MatrixClient,
|
||||
roomId: string,
|
||||
{ voiceOnly }: WidgetGenerationParameters,
|
||||
): void {
|
||||
const room = client.getRoom(roomId);
|
||||
if (!room) {
|
||||
// If the room isn't known, or the room is a video room then skip setting an intent.
|
||||
@@ -610,13 +646,17 @@ export class ElementCall extends Call {
|
||||
// is released and upgraded.
|
||||
if (isDM) {
|
||||
if (hasCallStarted) {
|
||||
params.append("intent", ElementCallIntent.JoinExistingDM);
|
||||
params.append(
|
||||
"intent",
|
||||
voiceOnly ? ElementCallIntent.JoinExistingDMVoice : ElementCallIntent.JoinExistingDM,
|
||||
);
|
||||
params.append("preload", "false");
|
||||
} else {
|
||||
params.append("intent", ElementCallIntent.StartCallDM);
|
||||
params.append("intent", voiceOnly ? ElementCallIntent.StartCallDMVoice : ElementCallIntent.StartCallDM);
|
||||
params.append("preload", "false");
|
||||
}
|
||||
} else {
|
||||
// Group chats do not have a voice option.
|
||||
if (hasCallStarted) {
|
||||
params.append("intent", ElementCallIntent.JoinExisting);
|
||||
params.append("preload", "false");
|
||||
@@ -717,7 +757,7 @@ export class ElementCall extends Call {
|
||||
.forEach((font) => params.append("font", font));
|
||||
}
|
||||
this.appendAnalyticsParams(params, client);
|
||||
this.appendRoomParams(params, client, roomId);
|
||||
this.appendRoomParams(params, client, roomId, opts);
|
||||
|
||||
const replacedUrl = params.toString().replace(/%24/g, "$");
|
||||
url.hash = `#?${replacedUrl}`;
|
||||
@@ -751,11 +791,43 @@ export class ElementCall extends Call {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the correct intent for a widget, so that Element Call presents the correct
|
||||
* default config.
|
||||
* @param client The matrix client.
|
||||
* @param roomId
|
||||
* @param voiceOnly Should the call be voice-only, or video (default).
|
||||
*/
|
||||
public static getWidgetIntent(client: MatrixClient, roomId: string, voiceOnly?: boolean): ElementCallIntent {
|
||||
const room = client.getRoom(roomId);
|
||||
if (room !== null && !isVideoRoom(room)) {
|
||||
const isDM = !!DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
||||
const oldestCallMember = client.matrixRTC.getRoomSession(room).getOldestMembership();
|
||||
const hasCallStarted = !!oldestCallMember && oldestCallMember.sender !== client.getSafeUserId();
|
||||
if (isDM) {
|
||||
if (hasCallStarted) {
|
||||
return voiceOnly ? ElementCallIntent.JoinExistingDMVoice : ElementCallIntent.JoinExistingDM;
|
||||
} else {
|
||||
return voiceOnly ? ElementCallIntent.StartCallDMVoice : ElementCallIntent.StartCallDM;
|
||||
}
|
||||
} else {
|
||||
if (hasCallStarted) {
|
||||
return ElementCallIntent.JoinExisting;
|
||||
} else {
|
||||
return ElementCallIntent.StartCall;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If unknown, default to joining an existing call.
|
||||
return ElementCallIntent.JoinExisting;
|
||||
}
|
||||
|
||||
private static getWidgetData(
|
||||
client: MatrixClient,
|
||||
roomId: string,
|
||||
currentData: IWidgetData,
|
||||
overwriteData: IWidgetData,
|
||||
voiceOnly?: boolean,
|
||||
): IWidgetData {
|
||||
let perParticipantE2EE = false;
|
||||
if (
|
||||
@@ -763,9 +835,13 @@ export class ElementCall extends Call {
|
||||
!SettingsStore.getValue("feature_disable_call_per_sender_encryption")
|
||||
)
|
||||
perParticipantE2EE = true;
|
||||
|
||||
const intent = ElementCall.getWidgetIntent(client, roomId, voiceOnly);
|
||||
|
||||
return {
|
||||
...currentData,
|
||||
...overwriteData,
|
||||
intent,
|
||||
perParticipantE2EE,
|
||||
};
|
||||
}
|
||||
@@ -791,7 +867,7 @@ export class ElementCall extends Call {
|
||||
this.updateParticipants();
|
||||
}
|
||||
|
||||
public static get(room: Room): ElementCall | null {
|
||||
public static get(room: Room, voiceOnly?: boolean): ElementCall | null {
|
||||
const apps = WidgetStore.instance.getApps(room.roomId);
|
||||
const hasEcWidget = apps.some((app) => WidgetType.CALL.matches(app.type));
|
||||
const session = room.client.matrixRTC.getRoomSession(room);
|
||||
@@ -874,7 +950,10 @@ export class ElementCall extends Call {
|
||||
if (this.session.memberships.length === 0 && !this.presented && !this.room.isCallRoom()) this.destroy();
|
||||
};
|
||||
|
||||
private readonly onMembershipChanged = (): void => this.updateParticipants();
|
||||
private readonly onMembershipChanged = (): void => {
|
||||
this.updateParticipants();
|
||||
this.callType = this.session.getConsensusCallIntent() === "audio" ? CallType.Voice : CallType.Video;
|
||||
};
|
||||
|
||||
private updateParticipants(): void {
|
||||
const participants = new Map<RoomMember, Set<string>>();
|
||||
|
||||
@@ -365,7 +365,9 @@ export class RoomViewStore extends EventEmitter {
|
||||
call.presented = true;
|
||||
// Immediately start the call. This will connect to all required widget events
|
||||
// and allow the widget to show the lobby.
|
||||
if (call.connectionState === ConnectionState.Disconnected) call.start({ skipLobby: payload.skipLobby });
|
||||
if (call.connectionState === ConnectionState.Disconnected) {
|
||||
call.start({ skipLobby: payload.skipLobby, voiceOnly: payload.voiceOnly });
|
||||
}
|
||||
}
|
||||
// If we switch to a different room from the call, we are no longer presenting it
|
||||
const prevRoomCall = this.state.roomId ? CallStore.instance.getCall(this.state.roomId) : null;
|
||||
|
||||
@@ -14,6 +14,7 @@ import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check"
|
||||
import CrossIcon from "@vector-im/compound-design-tokens/assets/web/icons/close";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { type IRTCNotificationContent } from "matrix-js-sdk/src/matrixrtc";
|
||||
import { VoiceCallIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { AvatarWithDetails } from "@element-hq/web-shared-components";
|
||||
|
||||
import { _t } from "../languageHandler";
|
||||
@@ -23,12 +24,8 @@ import defaultDispatcher from "../dispatcher/dispatcher";
|
||||
import { type ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload";
|
||||
import { Action } from "../dispatcher/actions";
|
||||
import ToastStore from "../stores/ToastStore";
|
||||
import {
|
||||
LiveContentSummary,
|
||||
LiveContentSummaryWithCall,
|
||||
LiveContentType,
|
||||
} from "../components/views/rooms/LiveContentSummary";
|
||||
import { useCall, useJoinCallButtonDisabledTooltip } from "../hooks/useCall";
|
||||
import { LiveContentSummary, LiveContentType } from "../components/views/rooms/LiveContentSummary";
|
||||
import { useCall, useJoinCallButtonDisabledTooltip, useParticipantCount } from "../hooks/useCall";
|
||||
import AccessibleButton, { type ButtonEvent } from "../components/views/elements/AccessibleButton";
|
||||
import { useDispatcher } from "../hooks/useDispatcher";
|
||||
import { type ActionPayload } from "../dispatcher/payloads";
|
||||
@@ -36,6 +33,7 @@ import { type Call, CallEvent } from "../models/Call";
|
||||
import LegacyCallHandler, { AudioID } from "../LegacyCallHandler";
|
||||
import { useEventEmitter } from "../hooks/useEventEmitter";
|
||||
import { CallStore, CallStoreEvent } from "../stores/CallStore";
|
||||
import DMRoomMap from "../utils/DMRoomMap";
|
||||
|
||||
/**
|
||||
* Get the key for the incoming call toast. A combination of the event ID and room ID.
|
||||
@@ -71,9 +69,15 @@ interface JoinCallButtonWithCallProps {
|
||||
onClick: (e: ButtonEvent) => void;
|
||||
call: Call | null;
|
||||
disabledTooltip: string | undefined;
|
||||
isRinging: boolean;
|
||||
}
|
||||
|
||||
function JoinCallButtonWithCall({ onClick, call, disabledTooltip }: JoinCallButtonWithCallProps): JSX.Element {
|
||||
function JoinCallButtonWithCall({
|
||||
onClick,
|
||||
call,
|
||||
disabledTooltip,
|
||||
isRinging,
|
||||
}: JoinCallButtonWithCallProps): JSX.Element {
|
||||
let disTooltip = disabledTooltip;
|
||||
const disabledBecauseFullTooltip = useJoinCallButtonDisabledTooltip(call);
|
||||
disTooltip = disabledTooltip ?? disabledBecauseFullTooltip ?? undefined;
|
||||
@@ -88,7 +92,7 @@ function JoinCallButtonWithCall({ onClick, call, disabledTooltip }: JoinCallButt
|
||||
Icon={CheckIcon}
|
||||
size="sm"
|
||||
>
|
||||
{_t("action|join")}
|
||||
{isRinging ? _t("action|accept") : _t("action|join")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
@@ -152,7 +156,7 @@ export function IncomingCallToast({ notificationEvent }: Props): JSX.Element {
|
||||
// This section can race, so we use a ref to keep track of whether we have started trying to play.
|
||||
// This is because `LegacyCallHandler.play` tries to load the sound and then play it asynchonously
|
||||
// and `LegacyCallHandler.isPlaying` will not be `true` until the sound starts playing.
|
||||
const isRingToast = notificationContent.notification_type == "ring";
|
||||
const isRingToast = notificationContent.notification_type === "ring";
|
||||
if (isRingToast && !soundHasStarted.current && !LegacyCallHandler.instance.isPlaying(AudioID.Ring)) {
|
||||
// Start ringing if not already.
|
||||
soundHasStarted.current = true;
|
||||
@@ -243,10 +247,11 @@ export function IncomingCallToast({ notificationEvent }: Props): JSX.Element {
|
||||
room_id: room?.roomId,
|
||||
view_call: true,
|
||||
skipLobby: ("shiftKey" in e && e.shiftKey) || skipLobbyToggle,
|
||||
voiceOnly: notificationContent["m.call.intent"] === "audio",
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
},
|
||||
[room, skipLobbyToggle],
|
||||
[room, skipLobbyToggle, notificationContent],
|
||||
);
|
||||
|
||||
// Dismiss on closing toast.
|
||||
@@ -262,34 +267,53 @@ export function IncomingCallToast({ notificationEvent }: Props): JSX.Element {
|
||||
useEventEmitter(CallStore.instance, CallStoreEvent.Call, onCall);
|
||||
useEventEmitter(call ?? undefined, CallEvent.Participants, onParticipantChange);
|
||||
useEventEmitter(room, RoomEvent.Timeline, onTimelineChange);
|
||||
const isVoice = notificationContent["m.call.intent"] === "audio";
|
||||
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||
const participantCount = useParticipantCount(call);
|
||||
const detailsInformation =
|
||||
notificationContent.notification_type === "ring" ? (
|
||||
<span>{otherUserId}</span>
|
||||
) : (
|
||||
<LiveContentSummary
|
||||
type={isVoice ? LiveContentType.Voice : LiveContentType.Video}
|
||||
text={isVoice ? _t("common|voice") : _t("common|video")}
|
||||
active={false}
|
||||
participantCount={participantCount}
|
||||
/>
|
||||
);
|
||||
|
||||
const callLiveContentSummary = call ? (
|
||||
<LiveContentSummaryWithCall call={call} />
|
||||
) : (
|
||||
<LiveContentSummary
|
||||
type={LiveContentType.Video}
|
||||
text={_t("common|video")}
|
||||
active={false}
|
||||
participantCount={0}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<>
|
||||
<div className="mx_IncomingCallToast_content">
|
||||
<div className="mx_IncomingCallToast_message">
|
||||
<VideoCallIcon width="20px" height="20px" style={{ position: "relative", top: "4px" }} />{" "}
|
||||
{_t("voip|video_call_started")}
|
||||
</div>
|
||||
{isVoice ? (
|
||||
<div className="mx_IncomingCallToast_message">
|
||||
<VoiceCallIcon width="20px" height="20px" style={{ position: "relative", top: "4px" }} />{" "}
|
||||
{_t("voip|voice_call_incoming")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mx_IncomingCallToast_message">
|
||||
<VideoCallIcon width="20px" height="20px" style={{ position: "relative", top: "4px" }} />{" "}
|
||||
{notificationContent.notification_type === "ring"
|
||||
? _t("voip|video_call_incoming")
|
||||
: _t("voip|video_call_started")}
|
||||
</div>
|
||||
)}
|
||||
<AvatarWithDetails
|
||||
avatar={<RoomAvatar room={room ?? undefined} size="32px" />}
|
||||
details={callLiveContentSummary}
|
||||
details={detailsInformation}
|
||||
title={room ? room.name : _t("voip|call_toast_unknown_room")}
|
||||
className="mx_IncomingCallToast_AvatarWithDetails"
|
||||
/>
|
||||
<div className="mx_IncomingCallToast_toggleWithLabel">
|
||||
<span>{_t("voip|skip_lobby_toggle_option")}</span>
|
||||
<ToggleInput onChange={(e) => setSkipLobbyToggle(e.target.checked)} checked={skipLobbyToggle} />
|
||||
</div>
|
||||
{!isVoice && (
|
||||
<div className="mx_IncomingCallToast_toggleWithLabel">
|
||||
<span>{_t("voip|skip_lobby_toggle_option")}</span>
|
||||
<ToggleInput
|
||||
onChange={(e) => setSkipLobbyToggle(e.target.checked)}
|
||||
checked={skipLobbyToggle}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="mx_IncomingCallToast_buttons">
|
||||
<DeclineCallButtonWithNotificationEvent
|
||||
notificationEvent={notificationEvent}
|
||||
@@ -299,6 +323,7 @@ export function IncomingCallToast({ notificationEvent }: Props): JSX.Element {
|
||||
<JoinCallButtonWithCall
|
||||
onClick={onJoinClick}
|
||||
call={call}
|
||||
isRinging={notificationContent.notification_type === "ring"}
|
||||
disabledTooltip={otherCallIsOngoing ? "Ongoing call" : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,8 @@ export const placeCall = async (
|
||||
room: Room,
|
||||
callType: CallType,
|
||||
platformCallType: PlatformCallType,
|
||||
skipLobby?: boolean,
|
||||
skipLobby: boolean | undefined,
|
||||
voiceOnly: boolean,
|
||||
): Promise<void> => {
|
||||
const { analyticsName } = getPlatformCallTypeProps(platformCallType);
|
||||
PosthogTrackers.trackInteraction(analyticsName);
|
||||
@@ -39,6 +40,7 @@ export const placeCall = async (
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
view_call: true,
|
||||
voiceOnly,
|
||||
skipLobby,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user