diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 42a1b94ea4..ff1d61a259 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -821,7 +821,6 @@ export class RoomView extends React.Component { private onActiveCalls = (): void => { if (this.state.roomId === undefined) return; const activeCall = CallStore.instance.getActiveCall(this.state.roomId); - if (activeCall === null) { // We disconnected from the call, so stop viewing it dis.dispatch( diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index 6bd6b0a12b..9ec6921ae2 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -14,10 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useEffect, useMemo, useState } from "react"; -import { Body as BodyText, IconButton, Tooltip } from "@vector-im/compound-web"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { Body as BodyText, Button, IconButton, Menu, MenuItem, Tooltip } from "@vector-im/compound-web"; import { Icon as VideoCallIcon } from "@vector-im/compound-design-tokens/icons/video-call-solid.svg"; import { Icon as VoiceCallIcon } from "@vector-im/compound-design-tokens/icons/voice-call.svg"; +import { Icon as CloseCallIcon } from "@vector-im/compound-design-tokens/icons/close.svg"; import { Icon as ThreadsIcon } from "@vector-im/compound-design-tokens/icons/threads-solid.svg"; import { Icon as NotificationsIcon } from "@vector-im/compound-design-tokens/icons/notifications-solid.svg"; import { Icon as VerifiedIcon } from "@vector-im/compound-design-tokens/icons/verified.svg"; @@ -35,7 +36,7 @@ import { useRoomMemberCount, useRoomMembers } from "../../../hooks/useRoomMember import { _t } from "../../../languageHandler"; import { Flex } from "../../utils/Flex"; import { Box } from "../../utils/Box"; -import { useRoomCall } from "../../../hooks/room/useRoomCall"; +import { getPlatformCallTypeLabel, useRoomCall } from "../../../hooks/room/useRoomCall"; import { useRoomThreadNotifications } from "../../../hooks/room/useRoomThreadNotifications"; import { useGlobalNotificationState } from "../../../hooks/useGlobalNotificationState"; import SdkConfig from "../../../SdkConfig"; @@ -51,6 +52,7 @@ import { Linkify, topicToHtml } from "../../../HtmlUtils"; import PosthogTrackers from "../../../PosthogTrackers"; import { VideoRoomChatButton } from "./RoomHeader/VideoRoomChatButton"; import { RoomKnocksBar } from "./RoomKnocksBar"; +import { isVideoRoom } from "../../../utils/video-rooms"; import { notificationLevelToIndicator } from "../../../utils/notifications"; export default function RoomHeader({ @@ -69,7 +71,17 @@ export default function RoomHeader({ const members = useRoomMembers(room, 2500); const memberCount = useRoomMemberCount(room, { throttleWait: 2500 }); - const { voiceCallDisabledReason, voiceCallClick, videoCallDisabledReason, videoCallClick } = useRoomCall(room); + const { + voiceCallDisabledReason, + voiceCallClick, + videoCallDisabledReason, + videoCallClick, + toggleCallMaximized: toggleCall, + isViewingCall, + isConnectedToCall, + hasActiveCallSession, + callOptions, + } = useRoomCall(room); const groupCallsEnabled = useFeatureEnabled("feature_group_calls"); /** @@ -104,6 +116,97 @@ export default function RoomHeader({ const askToJoinEnabled = useFeatureEnabled("feature_ask_to_join"); + const videoClick = useCallback((ev) => videoCallClick(ev, callOptions[0]), [callOptions, videoCallClick]); + + const toggleCallButton = ( + + + + + + ); + const joinCallButton = ( + + ); + const [menuOpen, setMenuOpen] = useState(false); + const callIconWithTooltip = ( + + + + ); + const startVideoCallButton = ( + <> + {/* Can be either a menu or just a button depending on the number of call options.*/} + {callOptions.length > 1 ? ( + + {callIconWithTooltip} + + } + side="left" + align="start" + > + {callOptions.map((option) => ( + videoCallClick(ev, option)} + Icon={VideoCallIcon} + onSelect={() => {} /* Dummy handler since we want the click event.*/} + /> + ))} + + ) : ( + + {callIconWithTooltip} + + )} + + ); + const voiceCallButton = ( + + voiceCallClick(ev, callOptions[0])} + > + + + + ); + const closeLobbyButton = ( + + + + + + ); + let videoCallButton = startVideoCallButton; + if (isConnectedToCall) { + videoCallButton = toggleCallButton; + } else if (isViewingCall) { + videoCallButton = closeLobbyButton; + } + return ( <> @@ -190,29 +293,17 @@ export default function RoomHeader({ ); })} - - - - - - {!useElementCallExclusively && ( - - - - - - )} - {/* Renders nothing when room is not a video room */} - + {((isConnectedToCall && isViewingCall) || isVideoRoom(room)) && } + + {hasActiveCallSession && !isConnectedToCall ? ( + joinCallButton + ) : ( + <> + {!isVideoRoom(room) && videoCallButton} + {!useElementCallExclusively && !isVideoRoom(room) && voiceCallButton} + + )} = ({ room }) => { const sdkContext = useContext(SDKContext); - const isVideoRoom = calcIsVideoRoom(room); - - const notificationState = isVideoRoom ? sdkContext.roomNotificationStateStore.getRoomState(room) : undefined; + const notificationState = sdkContext.roomNotificationStateStore.getRoomState(room); const notificationColor = useEventEmitterState( notificationState, NotificationStateEvents.Update, () => notificationState?.level, ); - if (!isVideoRoom) { - return null; - } - const displayUnreadIndicator = !!notificationColor && [NotificationLevel.Activity, NotificationLevel.Notification, NotificationLevel.Highlight].includes( diff --git a/src/hooks/room/useRoomCall.ts b/src/hooks/room/useRoomCall.ts index 5d8d567dfd..fb76bdd87d 100644 --- a/src/hooks/room/useRoomCall.ts +++ b/src/hooks/room/useRoomCall.ts @@ -24,18 +24,37 @@ import { useEventEmitter, useEventEmitterState } from "../useEventEmitter"; import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../LegacyCallHandler"; import { useWidgets } from "../../components/views/right_panel/RoomSummaryCard"; import { WidgetType } from "../../widgets/WidgetType"; -import { useCall } from "../useCall"; +import { useCall, useConnectionState, useParticipantCount } from "../useCall"; import { useRoomMemberCount } from "../useRoomMembers"; -import { ElementCall } from "../../models/Call"; +import { Call, ConnectionState, ElementCall } from "../../models/Call"; import { placeCall } from "../../utils/room/placeCall"; import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore"; import { useRoomState } from "../useRoomState"; import { _t } from "../../languageHandler"; import { isManagedHybridWidget } from "../../widgets/ManagedHybrid"; import { IApp } from "../../stores/WidgetStore"; +import { SdkContextClass } from "../../contexts/SDKContext"; +import { UPDATE_EVENT } from "../../stores/AsyncStore"; +import defaultDispatcher from "../../dispatcher/dispatcher"; +import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; +import { Action } from "../../dispatcher/actions"; +import { CallStore, CallStoreEvent } from "../../stores/CallStore"; -export type PlatformCallType = "element_call" | "jitsi_or_element_call" | "legacy_or_jitsi"; - +export enum PlatformCallType { + ElementCall, + JitsiCall, + LegacyCall, +} +export const getPlatformCallTypeLabel = (platformCallType: PlatformCallType): string => { + switch (platformCallType) { + case PlatformCallType.ElementCall: + return _t("voip|element_call"); + case PlatformCallType.JitsiCall: + return _t("voip|jitsi_call"); + case PlatformCallType.LegacyCall: + return _t("voip|legacy_call"); + } +}; const enum State { NoCall, NoOneHere, @@ -53,9 +72,14 @@ export const useRoomCall = ( room: Room, ): { voiceCallDisabledReason: string | null; - voiceCallClick(evt: React.MouseEvent): void; + voiceCallClick(evt: React.MouseEvent | undefined, selectedType: PlatformCallType): void; videoCallDisabledReason: string | null; - videoCallClick(evt: React.MouseEvent): void; + videoCallClick(evt: React.MouseEvent | undefined, selectedType: PlatformCallType): void; + toggleCallMaximized: () => void; + isViewingCall: boolean; + isConnectedToCall: boolean; + hasActiveCallSession: boolean; + callOptions: PlatformCallType[]; } => { const groupCallsEnabled = useFeatureEnabled("feature_group_calls"); const useElementCallExclusively = useMemo(() => { @@ -75,69 +99,83 @@ export const useRoomCall = ( const hasManagedHybridWidget = !!managedHybridWidget; const groupCall = useCall(room.roomId); + const isConnectedToCall = useConnectionState(groupCall) === ConnectionState.Connected; const hasGroupCall = groupCall !== null; + const hasActiveCallSession = useParticipantCount(groupCall) > 0; + const isViewingCall = useEventEmitterState(SdkContextClass.instance.roomViewStore, UPDATE_EVENT, () => + SdkContextClass.instance.roomViewStore.isViewingCall(), + ); const memberCount = useRoomMemberCount(room); const [mayEditWidgets, mayCreateElementCalls] = useRoomState(room, () => [ room.currentState.mayClientSendStateEvent("im.vector.modular.widgets", room.client), - room.currentState.mayClientSendStateEvent(ElementCall.CALL_EVENT_TYPE.name, room.client), + room.currentState.mayClientSendStateEvent(ElementCall.MEMBER_EVENT_TYPE.name, room.client), ]); - const callType = useMemo((): PlatformCallType => { + // The options provided to the RoomHeader. + // If there are multiple options, the user will be prompted to choose. + const callOptions = useMemo((): PlatformCallType[] => { + const options = []; + if (memberCount <= 2) { + options.push(PlatformCallType.LegacyCall); + } else if (mayEditWidgets || hasJitsiWidget) { + options.push(PlatformCallType.JitsiCall); + } if (groupCallsEnabled) { - if (hasGroupCall) { - return "jitsi_or_element_call"; + if (hasGroupCall || mayCreateElementCalls) { + options.push(PlatformCallType.ElementCall); } - if (mayCreateElementCalls && hasJitsiWidget) { - return "jitsi_or_element_call"; + if (useElementCallExclusively && !hasJitsiWidget) { + return [PlatformCallType.ElementCall]; } - if (useElementCallExclusively) { - return "element_call"; - } - if (memberCount <= 2) { - return "legacy_or_jitsi"; - } - if (mayCreateElementCalls) { - return "element_call"; + if (hasGroupCall && WidgetType.CALL.matches(groupCall.widget.type)) { + // only allow joining joining the ongoing Element call if there is one. + return [PlatformCallType.ElementCall]; } } - return "legacy_or_jitsi"; + return options; }, [ + memberCount, + mayEditWidgets, + hasJitsiWidget, groupCallsEnabled, hasGroupCall, mayCreateElementCalls, - hasJitsiWidget, useElementCallExclusively, - memberCount, + groupCall?.widget.type, ]); let widget: IApp | undefined; - if (callType === "legacy_or_jitsi") { + if (callOptions.includes(PlatformCallType.JitsiCall) || callOptions.includes(PlatformCallType.LegacyCall)) { widget = jitsiWidget ?? managedHybridWidget; - } else if (callType === "element_call") { + } + if (callOptions.includes(PlatformCallType.ElementCall)) { widget = groupCall?.widget; } else { widget = groupCall?.widget ?? jitsiWidget; } - + const updateWidgetState = useCallback((): void => { + setCanPinWidget(WidgetLayoutStore.instance.canAddToContainer(room, Container.Top)); + setWidgetPinned(!!widget && WidgetLayoutStore.instance.isInContainer(room, widget, Container.Top)); + }, [room, widget]); + useEventEmitter(WidgetLayoutStore.instance, WidgetLayoutStore.emissionForRoom(room), updateWidgetState); + useEffect(() => { + updateWidgetState(); + }, [room, jitsiWidget, groupCall, updateWidgetState]); + const [activeCalls, setActiveCalls] = useState(Array.from(CallStore.instance.activeCalls)); + useEventEmitter(CallStore.instance, CallStoreEvent.ActiveCalls, () => { + setActiveCalls(Array.from(CallStore.instance.activeCalls)); + }); const [canPinWidget, setCanPinWidget] = useState(false); const [widgetPinned, setWidgetPinned] = useState(false); // We only want to prompt to pin the widget if it's not element call based. const isECWidget = WidgetType.CALL.matches(widget?.type ?? ""); const promptPinWidget = !isECWidget && canPinWidget && !widgetPinned; - - const updateWidgetState = useCallback((): void => { - setCanPinWidget(WidgetLayoutStore.instance.canAddToContainer(room, Container.Top)); - setWidgetPinned(!!widget && WidgetLayoutStore.instance.isInContainer(room, widget, Container.Top)); - }, [room, widget]); - - useEventEmitter(WidgetLayoutStore.instance, WidgetLayoutStore.emissionForRoom(room), updateWidgetState); - useEffect(() => { - updateWidgetState(); - }, [room, jitsiWidget, groupCall, updateWidgetState]); - const state = useMemo((): State => { + if (activeCalls.find((call) => call.roomId != room.roomId)) { + return State.Ongoing; + } if (hasGroupCall || hasJitsiWidget || hasManagedHybridWidget) { return promptPinWidget ? State.Unpinned : State.Ongoing; } @@ -152,9 +190,9 @@ export const useRoomCall = ( if (!mayCreateElementCalls && !mayEditWidgets) { return State.NoPermission; } - return State.NoCall; }, [ + activeCalls, hasGroupCall, hasJitsiWidget, hasLegacyCall, @@ -163,29 +201,30 @@ export const useRoomCall = ( mayEditWidgets, memberCount, promptPinWidget, + room.roomId, ]); const voiceCallClick = useCallback( - (evt: React.MouseEvent): void => { - evt.stopPropagation(); + (evt: React.MouseEvent | undefined, callPlatformType: PlatformCallType): void => { + evt?.stopPropagation(); if (widget && promptPinWidget) { WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top); } else { - placeCall(room, CallType.Voice, callType, evt.shiftKey); + placeCall(room, CallType.Voice, callPlatformType, evt?.shiftKey ?? false); } }, - [promptPinWidget, room, widget, callType], + [promptPinWidget, room, widget], ); const videoCallClick = useCallback( - (evt: React.MouseEvent): void => { - evt.stopPropagation(); + (evt: React.MouseEvent | undefined, callPlatformType: PlatformCallType): void => { + evt?.stopPropagation(); if (widget && promptPinWidget) { WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top); } else { - placeCall(room, CallType.Video, callType, evt.shiftKey); + placeCall(room, CallType.Video, callPlatformType, evt?.shiftKey ?? false); } }, - [widget, promptPinWidget, room, callType], + [widget, promptPinWidget, room], ); let voiceCallDisabledReason: string | null; @@ -208,6 +247,14 @@ export const useRoomCall = ( voiceCallDisabledReason = null; videoCallDisabledReason = null; } + const toggleCallMaximized = useCallback(() => { + defaultDispatcher.dispatch({ + action: Action.ViewRoom, + room_id: room.roomId, + metricsTrigger: undefined, + view_call: !isViewingCall, + }); + }, [isViewingCall, room.roomId]); /** * We've gone through all the steps @@ -217,5 +264,10 @@ export const useRoomCall = ( voiceCallClick, videoCallDisabledReason, videoCallClick, + toggleCallMaximized: toggleCallMaximized, + isViewingCall: isViewingCall, + isConnectedToCall: isConnectedToCall, + hasActiveCallSession: hasActiveCallSession, + callOptions, }; }; diff --git a/src/hooks/useCall.ts b/src/hooks/useCall.ts index db091f1526..67aadaf4b5 100644 --- a/src/hooks/useCall.ts +++ b/src/hooks/useCall.ts @@ -36,21 +36,22 @@ export const useCallForWidget = (widgetId: string, roomId: string): Call | null return call?.widget.id === widgetId ? call : null; }; -export const useConnectionState = (call: Call): ConnectionState => +export const useConnectionState = (call: Call | null): ConnectionState => useTypedEventEmitterState( - call, + call ?? undefined, CallEvent.ConnectionState, - useCallback((state) => state ?? call.connectionState, [call]), + useCallback((state) => state ?? call?.connectionState ?? ConnectionState.Disconnected, [call]), ); -export const useParticipants = (call: Call): Map> => - useTypedEventEmitterState( - call, +export const useParticipants = (call: Call | null): Map> => { + return useTypedEventEmitterState( + call ?? undefined, CallEvent.Participants, - useCallback((state) => state ?? call.participants, [call]), + useCallback((state) => state ?? call?.participants ?? [], [call]), ); +}; -export const useParticipantCount = (call: Call): number => { +export const useParticipantCount = (call: Call | null): number => { const participants = useParticipants(call); return useMemo(() => { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 6a8a156603..489d529ffc 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -3807,6 +3807,7 @@ "camera_enabled": "Your camera is still enabled", "cannot_call_yourself_description": "You cannot place a call with yourself.", "change_input_device": "Change input device", + "close_lobby": "Close lobby", "connecting": "Connecting", "connection_lost": "Connectivity to the server has been lost", "connection_lost_description": "You cannot place calls without a connection to the server.", @@ -3820,6 +3821,7 @@ "disabled_no_perms_start_video_call": "You do not have permission to start video calls", "disabled_no_perms_start_voice_call": "You do not have permission to start voice calls", "disabled_ongoing_call": "Ongoing call", + "element_call": "Element Call", "enable_camera": "Turn on camera", "enable_microphone": "Unmute microphone", "expand": "Return to call", @@ -3828,9 +3830,13 @@ "hangup": "Hangup", "hide_sidebar_button": "Hide sidebar", "input_devices": "Input devices", + "jitsi_call": "Jitsi Conference", "join_button_tooltip_call_full": "Sorry — this call is currently full", "join_button_tooltip_connecting": "Connecting", + "legacy_call": "Legacy Call", "maximise": "Fill screen", + "maximise_call": "Maximise call", + "minimise_call": "Minimise call", "misconfigured_server": "Call failed due to misconfigured server", "misconfigured_server_description": "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.", "misconfigured_server_fallback": "Alternatively, you can try to use the public server at , but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.", @@ -3878,6 +3884,7 @@ "user_is_presenting": "%(sharerName)s is presenting", "video_call": "Video call", "video_call_started": "Video call started", + "video_call_using": "Video call using:", "voice_call": "Voice call", "you_are_presenting": "You are presenting" }, diff --git a/src/models/Call.ts b/src/models/Call.ts index 243a1fe0c2..9daca1589c 100644 --- a/src/models/Call.ts +++ b/src/models/Call.ts @@ -65,18 +65,26 @@ const waitForEvent = async ( emitter: EventEmitter, event: string, pred: (...args: any[]) => boolean = () => true, + customTimeout?: number | false, ): Promise => { let listener: (...args: any[]) => void; const wait = new Promise((resolve) => { listener = (...args) => { - if (pred(...args)) resolve(); + if (pred(...args)) { + resolve(); + if (customTimeout === false) { + emitter.off(event, listener!); + } + } }; emitter.on(event, listener); }); - const timedOut = (await timeout(wait, false, TIMEOUT_MS)) === false; - emitter.off(event, listener!); - if (timedOut) throw new Error("Timed out"); + if (customTimeout !== false) { + const timedOut = (await timeout(wait, false, customTimeout ?? TIMEOUT_MS)) === false; + emitter.off(event, listener!); + if (timedOut) throw new Error("Timed out"); + } }; export enum ConnectionState { @@ -899,6 +907,7 @@ export class ElementCall extends Call { MatrixRTCSessionEvent.MembershipsChanged, (_, newMemberships: CallMembership[]) => newMemberships.some((m) => m.sender === this.client.getUserId()), + false, // allow user to wait as long as they want (no timeout) ); } else { await waitForEvent( @@ -906,6 +915,7 @@ export class ElementCall extends Call { MatrixRTCSessionManagerEvents.SessionStarted, (roomId: string, session: MatrixRTCSession) => this.session.callId === session.callId && roomId === this.roomId, + false, // allow user to wait as long as they want (no timeout) ); } this.sendCallNotify(); diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index 75529bfc1f..c131a7e766 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -345,8 +345,11 @@ export class StopGapWidget extends EventEmitter { if (this.messaging?.hasCapability(MatrixCapabilities.AlwaysOnScreen)) { ev.preventDefault(); this.messaging.transport.reply(ev.detail, {}); // ack - - if (this.stickyPromise) await this.stickyPromise(); + if (ev.detail.data.value) { + // If the widget wants to become sticky we wait for the stickyPromise to resolve + if (this.stickyPromise) await this.stickyPromise(); + } + // Stop being persistent can be done instantly ActiveWidgetStore.instance.setWidgetPersistence( this.mockWidget.id, this.roomId ?? null, diff --git a/src/toasts/IncomingCallToast.tsx b/src/toasts/IncomingCallToast.tsx index 5909307417..6e239404ef 100644 --- a/src/toasts/IncomingCallToast.tsx +++ b/src/toasts/IncomingCallToast.tsx @@ -20,6 +20,8 @@ import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import { MatrixRTCSessionManagerEvents } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSessionManager"; // eslint-disable-next-line no-restricted-imports import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; +import { Button } from "@vector-im/compound-web"; +import { Icon as VideoCallIcon } from "@vector-im/compound-design-tokens/icons/video-call-solid.svg"; import { _t } from "../languageHandler"; import RoomAvatar from "../components/views/avatars/RoomAvatar"; @@ -28,7 +30,6 @@ import defaultDispatcher from "../dispatcher/dispatcher"; import { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload"; import { Action } from "../dispatcher/actions"; import ToastStore from "../stores/ToastStore"; -import AccessibleTooltipButton from "../components/views/elements/AccessibleTooltipButton"; import { LiveContentSummary, LiveContentSummaryWithCall, @@ -41,6 +42,7 @@ import { ActionPayload } from "../dispatcher/payloads"; import { Call } from "../models/Call"; import { AudioID } from "../LegacyCallHandler"; import { useTypedEventEmitter } from "../hooks/useEventEmitter"; +import AccessibleTooltipButton from "../components/views/elements/AccessibleTooltipButton"; export const getIncomingCallToastKey = (callId: string, roomId: string): string => `call_${callId}_${roomId}`; const MAX_RING_TIME_MS = 10 * 1000; @@ -54,15 +56,15 @@ function JoinCallButtonWithCall({ onClick, call }: JoinCallButtonWithCallProps): const disabledTooltip = useJoinCallButtonDisabledTooltip(call); return ( - {_t("action|join")} - + ); } @@ -179,13 +181,15 @@ export function IncomingCallToast({ notifyEvent }: Props): JSX.Element { {call ? ( ) : ( - {_t("action|join")} - + )} => { - switch (platformCallType) { - case "legacy_or_jitsi": - await LegacyCallHandler.instance.placeCall(room.roomId, callType); - break; - // TODO: Remove the jitsi_or_element_call case and - // use the commented code below - case "element_call": - case "jitsi_or_element_call": - defaultDispatcher.dispatch({ - action: Action.ViewRoom, - room_id: room.roomId, - view_call: true, - metricsTrigger: undefined, - skipLobby, - }); - break; - - // case "jitsi_or_element_call": - // TODO: Open dropdown menu to choice between - // EC and Jitsi. Waiting on Compound's dropdown - // component - // break; + if (platformCallType == PlatformCallType.LegacyCall || platformCallType == PlatformCallType.JitsiCall) { + await LegacyCallHandler.instance.placeCall(room.roomId, callType); + } else if (platformCallType == PlatformCallType.ElementCall) { + defaultDispatcher.dispatch({ + action: Action.ViewRoom, + room_id: room.roomId, + view_call: true, + skipLobby, + metricsTrigger: undefined, + }); } }; diff --git a/test/components/views/rooms/RoomHeader-test.tsx b/test/components/views/rooms/RoomHeader-test.tsx index 7aa911d13a..b13da1e4fe 100644 --- a/test/components/views/rooms/RoomHeader-test.tsx +++ b/test/components/views/rooms/RoomHeader-test.tsx @@ -323,7 +323,7 @@ describe("RoomHeader", () => { jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true); jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(true); - jest.spyOn(CallStore.instance, "getCall").mockReturnValue({ widget: {} } as Call); + jest.spyOn(CallStore.instance, "getCall").mockReturnValue({ widget: {}, on: () => {} } as unknown as Call); const { container } = render(, getWrapper()); expect(getByLabelText(container, "Ongoing call")).toHaveAttribute("aria-disabled", "true"); @@ -336,8 +336,11 @@ describe("RoomHeader", () => { jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(false); const spy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer"); - const widget = { eventId: "some_id_so_it_is_interpreted_as_non_virtual_widget" }; - jest.spyOn(CallStore.instance, "getCall").mockReturnValue({ widget } as Call); + const widget = {}; + jest.spyOn(CallStore.instance, "getCall").mockReturnValue({ + widget, + on: () => {}, + } as unknown as Call); const { container } = render(, getWrapper()); expect(getByLabelText(container, "Video call")).not.toHaveAttribute("aria-disabled", "true"); @@ -367,6 +370,10 @@ describe("RoomHeader", () => { it("calls using legacy or jitsi", async () => { mockRoomMembers(room, 2); + jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => { + if (key === "im.vector.modular.widgets") return true; + return false; + }); const { container } = render(, getWrapper()); const voiceButton = getByLabelText(container, "Voice call"); @@ -409,8 +416,7 @@ describe("RoomHeader", () => { mockRoomMembers(room, 3); jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => { - if (key === "im.vector.modular.widgets") return true; - if (key === ElementCall.CALL_EVENT_TYPE.name) return true; + if (key === ElementCall.MEMBER_EVENT_TYPE.name) return true; return false; }); diff --git a/test/components/views/rooms/RoomHeader/VideoRoomChatButton-test.tsx b/test/components/views/rooms/RoomHeader/VideoRoomChatButton-test.tsx index 7981a1b03d..222da72764 100644 --- a/test/components/views/rooms/RoomHeader/VideoRoomChatButton-test.tsx +++ b/test/components/views/rooms/RoomHeader/VideoRoomChatButton-test.tsx @@ -80,20 +80,6 @@ describe("", () => { jest.restoreAllMocks(); }); - it("does not render button when room is not a video room", () => { - const room = makeRoom(false); - getComponent(room); - - expect(screen.queryByLabelText("Chat")).not.toBeInTheDocument(); - }); - - it("renders button when room is a video room", () => { - const room = makeRoom(); - getComponent(room); - - expect(screen.getByLabelText("Chat")).toMatchSnapshot(); - }); - it("toggles timeline in right panel on click", () => { const room = makeRoom(); getComponent(room); diff --git a/test/components/views/rooms/RoomHeader/__snapshots__/VideoRoomChatButton-test.tsx.snap b/test/components/views/rooms/RoomHeader/__snapshots__/VideoRoomChatButton-test.tsx.snap index 2d5e226d30..e26d89f001 100644 --- a/test/components/views/rooms/RoomHeader/__snapshots__/VideoRoomChatButton-test.tsx.snap +++ b/test/components/views/rooms/RoomHeader/__snapshots__/VideoRoomChatButton-test.tsx.snap @@ -1,23 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` renders button when room is a video room 1`] = ` - -`; - exports[` renders button with an unread marker when room is unread 1`] = `