From fac8317343ae43b551340c4467415b6a27722bb5 Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 27 Dec 2023 19:52:31 +0100 Subject: [PATCH 01/22] New room header - add chat button during call - close lobby button in lobby - join button if session exists - allow to toggle call <-> timeline during call with call button Compound style for join button in call notify toast. Signed-off-by: Timo K --- src/components/views/rooms/RoomHeader.tsx | 89 +++++++++++++++---- .../rooms/RoomHeader/VideoRoomChatButton.tsx | 12 +-- src/hooks/room/useRoomCall.ts | 27 ++++++ src/i18n/strings/en_EN.json | 3 + src/toasts/IncomingCallToast.tsx | 17 ++-- 5 files changed, 113 insertions(+), 35 deletions(-) diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index ec95e2a15c8..255079008e0 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -15,9 +15,10 @@ limitations under the License. */ import React, { useEffect, useMemo, useState } from "react"; -import { Body as BodyText, IconButton, Tooltip } from "@vector-im/compound-web"; +import { Body as BodyText, Button, IconButton, 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"; @@ -52,6 +53,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"; /** * A helper to transform a notification color to the what the Compound Icon Button @@ -83,7 +85,16 @@ 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, + } = useRoomCall(room); const groupCallsEnabled = useFeatureEnabled("feature_group_calls"); /** @@ -118,6 +129,47 @@ export default function RoomHeader({ const askToJoinEnabled = useFeatureEnabled("feature_ask_to_join"); + const toggleCallButton = ( + + + + + + ); + const joinCallButton = ( + + ); + const startCallButton = ( + + + + + + ); + const closeLobbyButton = ( + + + + + + ); + let videoCallButton = startCallButton; + if (isConnectedToCall) { + videoCallButton = toggleCallButton; + } else { + if (hasActiveCallSession && !isViewingCall) { + videoCallButton = joinCallButton; + } + if (isViewingCall) { + videoCallButton = closeLobbyButton; + } + } return ( <> @@ -204,15 +256,11 @@ export default function RoomHeader({ ); })} - - - - - + + {((isConnectedToCall && isViewingCall) || isVideoRoom(room)) && } + + {videoCallButton} + {!useElementCallExclusively && ( )} - - {/* Renders nothing when room is not a video room */} - - + {notificationsEnabled && ( + + { + evt.stopPropagation(); + RightPanelStore.instance.showOrHidePanel(RightPanelPhases.ThreadPanel); + PosthogTrackers.trackInteraction("WebRoomHeaderButtonsThreadsButton", evt); + }} + aria-label={_t("common|threads")} + > + + + + )} {notificationsEnabled && ( = ({ 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 c1438eaca4a..d1086be377e 100644 --- a/src/hooks/room/useRoomCall.ts +++ b/src/hooks/room/useRoomCall.ts @@ -33,6 +33,11 @@ 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"; export type PlatformCallType = "element_call" | "jitsi_or_element_call" | "legacy_or_jitsi"; @@ -56,6 +61,10 @@ export const useRoomCall = ( voiceCallClick(evt: React.MouseEvent): void; videoCallDisabledReason: string | null; videoCallClick(evt: React.MouseEvent): void; + toggleCallMaximized: () => void; + isViewingCall: boolean; + isConnectedToCall: boolean; + hasActiveCallSession: boolean; } => { const groupCallsEnabled = useFeatureEnabled("feature_group_calls"); const useElementCallExclusively = useMemo(() => { @@ -135,6 +144,12 @@ export const useRoomCall = ( updateWidgetState(); }, [room, jitsiWidget, groupCall, updateWidgetState]); + const [isViewingCall, setIsViewingCall] = useState(SdkContextClass.instance.roomViewStore.isViewingCall()); + const onRoomViewStoreUpdate = useCallback(() => { + setIsViewingCall(SdkContextClass.instance.roomViewStore.isViewingCall()); + }, []); + + useEventEmitter(SdkContextClass.instance.roomViewStore, UPDATE_EVENT, onRoomViewStoreUpdate); const state = useMemo((): State => { if (hasGroupCall || hasJitsiWidget || hasManagedHybridWidget) { return promptPinWidget ? State.Unpinned : State.Ongoing; @@ -206,6 +221,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 @@ -215,5 +238,9 @@ export const useRoomCall = ( voiceCallClick, videoCallDisabledReason, videoCallClick, + toggleCallMaximized: toggleCallMaximized, + isViewingCall: isViewingCall, + isConnectedToCall: groupCall?.connected ?? false, + hasActiveCallSession: hasGroupCall, }; }; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7dc63919823..cb011cbc536 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -3806,6 +3806,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.", @@ -3830,6 +3831,8 @@ "join_button_tooltip_call_full": "Sorry — this call is currently full", "join_button_tooltip_connecting": "Connecting", "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.", diff --git a/src/toasts/IncomingCallToast.tsx b/src/toasts/IncomingCallToast.tsx index 1bd8dad4cc2..b2645d9726f 100644 --- a/src/toasts/IncomingCallToast.tsx +++ b/src/toasts/IncomingCallToast.tsx @@ -20,6 +20,7 @@ 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 { _t } from "../languageHandler"; import RoomAvatar from "../components/views/avatars/RoomAvatar"; @@ -28,7 +29,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 +41,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 +55,15 @@ function JoinCallButtonWithCall({ onClick, call }: JoinCallButtonWithCallProps): const disabledTooltip = useJoinCallButtonDisabledTooltip(call); return ( - {_t("action|join")} - + ); } @@ -178,13 +179,9 @@ export function IncomingCallToast({ notifyEvent }: Props): JSX.Element { {call ? ( ) : ( - + )} Date: Wed, 27 Dec 2023 20:20:40 +0100 Subject: [PATCH 02/22] dont show start call, join button in video rooms. Signed-off-by: Timo K --- src/components/views/rooms/RoomHeader.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index 255079008e0..94cadc9e306 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -259,9 +259,9 @@ export default function RoomHeader({ {((isConnectedToCall && isViewingCall) || isVideoRoom(room)) && } - {videoCallButton} + {!isVideoRoom(room) && videoCallButton} - {!useElementCallExclusively && ( + {!useElementCallExclusively && !isVideoRoom(room) && ( Date: Wed, 24 Jan 2024 11:32:20 +0100 Subject: [PATCH 03/22] Make active call check based on participant count Not based on available call object Signed-off-by: Timo K --- src/hooks/room/useRoomCall.ts | 17 +++++++++++------ src/hooks/useCall.ts | 11 ++++++----- src/stores/WidgetStore.ts | 4 ++++ 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/hooks/room/useRoomCall.ts b/src/hooks/room/useRoomCall.ts index d1086be377e..60f0199e6d1 100644 --- a/src/hooks/room/useRoomCall.ts +++ b/src/hooks/room/useRoomCall.ts @@ -24,7 +24,7 @@ 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, useParticipantCount } from "../useCall"; import { useRoomMemberCount } from "../useRoomMembers"; import { ElementCall } from "../../models/Call"; import { placeCall } from "../../utils/room/placeCall"; @@ -32,7 +32,7 @@ import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutS import { useRoomState } from "../useRoomState"; import { _t } from "../../languageHandler"; import { isManagedHybridWidget } from "../../widgets/ManagedHybrid"; -import { IApp } from "../../stores/WidgetStore"; +import { IApp, isVirtualWidget } from "../../stores/WidgetStore"; import { SdkContextClass } from "../../contexts/SDKContext"; import { UPDATE_EVENT } from "../../stores/AsyncStore"; import defaultDispatcher from "../../dispatcher/dispatcher"; @@ -85,11 +85,13 @@ export const useRoomCall = ( const groupCall = useCall(room.roomId); const hasGroupCall = groupCall !== null; + const hasActiveCallSession = useParticipantCount(groupCall) > 0; const memberCount = useRoomMemberCount(room); - const [mayEditWidgets, mayCreateElementCalls] = useRoomState(room, () => [ + const [mayEditWidgets, mayCreateElementCalls, mayCreateWidgetCall] = useRoomState(room, () => [ room.currentState.mayClientSendStateEvent("im.vector.modular.widgets", room.client), + room.currentState.mayClientSendStateEvent(ElementCall.MEMBER_EVENT_TYPE.name, room.client), room.currentState.mayClientSendStateEvent(ElementCall.CALL_EVENT_TYPE.name, room.client), ]); @@ -98,7 +100,7 @@ export const useRoomCall = ( if (hasGroupCall) { return "jitsi_or_element_call"; } - if (mayCreateElementCalls && hasJitsiWidget) { + if (mayCreateElementCalls && (mayCreateWidgetCall || hasJitsiWidget)) { return "jitsi_or_element_call"; } if (useElementCallExclusively) { @@ -116,6 +118,7 @@ export const useRoomCall = ( groupCallsEnabled, hasGroupCall, mayCreateElementCalls, + mayCreateWidgetCall, hasJitsiWidget, useElementCallExclusively, memberCount, @@ -132,7 +135,9 @@ export const useRoomCall = ( const [canPinWidget, setCanPinWidget] = useState(false); const [widgetPinned, setWidgetPinned] = useState(false); - const promptPinWidget = canPinWidget && !widgetPinned; + // We only want to prompt to pin the widget if it's not virtual (not element call based) + const isECWidget = widget ? isVirtualWidget(widget) : false; + const promptPinWidget = !isECWidget && canPinWidget && !widgetPinned; const updateWidgetState = useCallback((): void => { setCanPinWidget(WidgetLayoutStore.instance.canAddToContainer(room, Container.Top)); @@ -241,6 +246,6 @@ export const useRoomCall = ( toggleCallMaximized: toggleCallMaximized, isViewingCall: isViewingCall, isConnectedToCall: groupCall?.connected ?? false, - hasActiveCallSession: hasGroupCall, + hasActiveCallSession: hasActiveCallSession, }; }; diff --git a/src/hooks/useCall.ts b/src/hooks/useCall.ts index db091f15269..36468752034 100644 --- a/src/hooks/useCall.ts +++ b/src/hooks/useCall.ts @@ -43,14 +43,15 @@ export const useConnectionState = (call: Call): ConnectionState => useCallback((state) => state ?? call.connectionState, [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/stores/WidgetStore.ts b/src/stores/WidgetStore.ts index fd092e7fbb4..e0cf19a75a2 100644 --- a/src/stores/WidgetStore.ts +++ b/src/stores/WidgetStore.ts @@ -41,6 +41,10 @@ export function isAppWidget(widget: IWidget | IApp): widget is IApp { return "roomId" in widget && typeof widget.roomId === "string"; } +export function isVirtualWidget(widget: IApp): boolean { + return widget.eventId === undefined; +} + interface IRoomWidgets { widgets: IApp[]; } From d11c4a7b24cdb1dc9008db5b566a18de3c12fc5c Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 24 Jan 2024 12:19:07 +0100 Subject: [PATCH 04/22] fix room header tests Signed-off-by: Timo K --- test/components/views/rooms/RoomHeader-test.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/test/components/views/rooms/RoomHeader-test.tsx b/test/components/views/rooms/RoomHeader-test.tsx index e2a54ae77c4..0a4cbbe4c6f 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,14 @@ describe("RoomHeader", () => { jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(false); const spy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer"); - const widget = {}; - jest.spyOn(CallStore.instance, "getCall").mockReturnValue({ widget } as Call); + const widget = { + // We add an event id to mark the widget as a non virtual widget. + eventId: "someId", + }; + 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"); From d5a2525efc9111e23b347f44aa3e1bdddbc9f06f Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 24 Jan 2024 13:05:55 +0100 Subject: [PATCH 05/22] fix room header tests Signed-off-by: Timo K --- src/hooks/room/useRoomCall.ts | 2 +- test/components/views/rooms/RoomHeader-test.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/room/useRoomCall.ts b/src/hooks/room/useRoomCall.ts index 60f0199e6d1..e562c9339a9 100644 --- a/src/hooks/room/useRoomCall.ts +++ b/src/hooks/room/useRoomCall.ts @@ -100,7 +100,7 @@ export const useRoomCall = ( if (hasGroupCall) { return "jitsi_or_element_call"; } - if (mayCreateElementCalls && (mayCreateWidgetCall || hasJitsiWidget)) { + if (mayCreateElementCalls && mayCreateWidgetCall && hasJitsiWidget) { return "jitsi_or_element_call"; } if (useElementCallExclusively) { diff --git a/test/components/views/rooms/RoomHeader-test.tsx b/test/components/views/rooms/RoomHeader-test.tsx index 0a4cbbe4c6f..035b60ca6e6 100644 --- a/test/components/views/rooms/RoomHeader-test.tsx +++ b/test/components/views/rooms/RoomHeader-test.tsx @@ -416,7 +416,7 @@ describe("RoomHeader", () => { 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; }); From 0aa5e005f83bd92cb7ab1a2453672888d695ca78 Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 24 Jan 2024 13:39:20 +0100 Subject: [PATCH 06/22] remove chat button test for displaying. Chat button display logic is now part of the RoomHeader. Signed-off-by: Timo K --- .../rooms/RoomHeader/VideoRoomChatButton-test.tsx | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/test/components/views/rooms/RoomHeader/VideoRoomChatButton-test.tsx b/test/components/views/rooms/RoomHeader/VideoRoomChatButton-test.tsx index bc9eb10b560..85616ac6a52 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); From 992f43eafc3a11c8ab7e9d1aa0e0d9dceb3a2dd8 Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 24 Jan 2024 14:22:00 +0100 Subject: [PATCH 07/22] remove duplicate notification Tread icon Signed-off-by: Timo K --- src/components/views/rooms/RoomHeader.tsx | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index 94cadc9e306..9fbc314aa17 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -285,21 +285,6 @@ export default function RoomHeader({ - {notificationsEnabled && ( - - { - evt.stopPropagation(); - RightPanelStore.instance.showOrHidePanel(RightPanelPhases.ThreadPanel); - PosthogTrackers.trackInteraction("WebRoomHeaderButtonsThreadsButton", evt); - }} - aria-label={_t("common|threads")} - > - - - - )} {notificationsEnabled && ( Date: Wed, 24 Jan 2024 15:36:30 +0100 Subject: [PATCH 08/22] remove obsolete jest snapshot Signed-off-by: Timo K --- .../__snapshots__/VideoRoomChatButton-test.tsx.snap | 13 ------------- 1 file changed, 13 deletions(-) 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 0c21eaa7131..e59eda8fc11 100644 --- a/test/components/views/rooms/RoomHeader/__snapshots__/VideoRoomChatButton-test.tsx.snap +++ b/test/components/views/rooms/RoomHeader/__snapshots__/VideoRoomChatButton-test.tsx.snap @@ -1,18 +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`] = ` ); const startCallButton = ( - + From 5d1d1b82feb060949fcb42931d50c6b3ad40d0d5 Mon Sep 17 00:00:00 2001 From: Timo K Date: Thu, 25 Jan 2024 17:16:42 +0100 Subject: [PATCH 10/22] update isECWidget logic Signed-off-by: Timo K --- src/hooks/room/useRoomCall.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/room/useRoomCall.ts b/src/hooks/room/useRoomCall.ts index e562c9339a9..952cf894d6b 100644 --- a/src/hooks/room/useRoomCall.ts +++ b/src/hooks/room/useRoomCall.ts @@ -32,7 +32,7 @@ import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutS import { useRoomState } from "../useRoomState"; import { _t } from "../../languageHandler"; import { isManagedHybridWidget } from "../../widgets/ManagedHybrid"; -import { IApp, isVirtualWidget } from "../../stores/WidgetStore"; +import { IApp } from "../../stores/WidgetStore"; import { SdkContextClass } from "../../contexts/SDKContext"; import { UPDATE_EVENT } from "../../stores/AsyncStore"; import defaultDispatcher from "../../dispatcher/dispatcher"; @@ -136,7 +136,7 @@ export const useRoomCall = ( const [canPinWidget, setCanPinWidget] = useState(false); const [widgetPinned, setWidgetPinned] = useState(false); // We only want to prompt to pin the widget if it's not virtual (not element call based) - const isECWidget = widget ? isVirtualWidget(widget) : false; + const isECWidget = WidgetType.CALL.matches(widget?.type ?? ""); const promptPinWidget = !isECWidget && canPinWidget && !widgetPinned; const updateWidgetState = useCallback((): void => { From 9ec091352275a8f78835d6ea362b0ea0743b3fb8 Mon Sep 17 00:00:00 2001 From: Timo K Date: Thu, 25 Jan 2024 17:30:17 +0100 Subject: [PATCH 11/22] remove dead code Signed-off-by: Timo K --- src/stores/WidgetStore.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/stores/WidgetStore.ts b/src/stores/WidgetStore.ts index e0cf19a75a2..fd092e7fbb4 100644 --- a/src/stores/WidgetStore.ts +++ b/src/stores/WidgetStore.ts @@ -41,10 +41,6 @@ export function isAppWidget(widget: IWidget | IApp): widget is IApp { return "roomId" in widget && typeof widget.roomId === "string"; } -export function isVirtualWidget(widget: IApp): boolean { - return widget.eventId === undefined; -} - interface IRoomWidgets { widgets: IApp[]; } From dc1c02850ed96b327c5ed4314b7ae73d47e7aa95 Mon Sep 17 00:00:00 2001 From: Timo K Date: Thu, 25 Jan 2024 18:47:57 +0100 Subject: [PATCH 12/22] refactor call options Add menu to choose if there are multiple options Signed-off-by: Timo K --- src/components/views/rooms/RoomHeader.tsx | 60 ++++++++++++++---- src/hooks/room/useRoomCall.ts | 74 ++++++++++++++--------- src/utils/room/placeCall.ts | 33 ++++------ 3 files changed, 105 insertions(+), 62 deletions(-) diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index 2775bea98a0..6a0fa37e4eb 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React, { useEffect, useMemo, useState } from "react"; -import { Body as BodyText, Button, IconButton, Tooltip } from "@vector-im/compound-web"; +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"; @@ -36,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 { NotificationLevel } from "../../../stores/notifications/NotificationLevel"; import { useGlobalNotificationState } from "../../../hooks/useGlobalNotificationState"; @@ -94,6 +94,7 @@ export default function RoomHeader({ isViewingCall, isConnectedToCall, hasActiveCallSession, + callOptions, } = useRoomCall(room); const groupCallsEnabled = useFeatureEnabled("feature_group_calls"); @@ -137,21 +138,56 @@ export default function RoomHeader({ ); const joinCallButton = ( - ); - const startCallButton = ( + const [menuOpen, setMenuOpen] = useState(false); + const callIconWithTooltip = ( - - - + ); + const startCallButton = ( + <> + {/* 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.*/} + /> + ))} + + ) : ( + videoCallClick(ev, callOptions[0])} + > + {callIconWithTooltip} + + )} + + ); const closeLobbyButton = ( @@ -266,7 +302,7 @@ export default function RoomHeader({ voiceCallClick(ev, callOptions[0])} > diff --git a/src/hooks/room/useRoomCall.ts b/src/hooks/room/useRoomCall.ts index 952cf894d6b..558c92951ce 100644 --- a/src/hooks/room/useRoomCall.ts +++ b/src/hooks/room/useRoomCall.ts @@ -39,8 +39,22 @@ import defaultDispatcher from "../../dispatcher/dispatcher"; import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; import { Action } from "../../dispatcher/actions"; -export type PlatformCallType = "element_call" | "jitsi_or_element_call" | "legacy_or_jitsi"; - +// 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 "Element Call"; + case PlatformCallType.JitsiCall: + return "Jitsi Conference"; + case PlatformCallType.LegacyCall: + return "Legacy Call"; + } +}; const enum State { NoCall, NoOneHere, @@ -58,13 +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(() => { @@ -95,25 +110,24 @@ export const useRoomCall = ( room.currentState.mayClientSendStateEvent(ElementCall.CALL_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 (mayCreateWidgetCall || hasJitsiWidget) { + options.push(PlatformCallType.JitsiCall); + } if (groupCallsEnabled) { - if (hasGroupCall) { - return "jitsi_or_element_call"; + if (hasGroupCall || mayCreateElementCalls) { + options.push(PlatformCallType.ElementCall); } - if (mayCreateElementCalls && mayCreateWidgetCall && hasJitsiWidget) { - return "jitsi_or_element_call"; - } - if (useElementCallExclusively) { - return "element_call"; - } - if (memberCount <= 2) { - return "legacy_or_jitsi"; - } - if (mayCreateElementCalls) { - return "element_call"; + if (useElementCallExclusively && !hasJitsiWidget) { + return [PlatformCallType.ElementCall]; } } - return "legacy_or_jitsi"; + return options; }, [ groupCallsEnabled, hasGroupCall, @@ -125,9 +139,10 @@ export const useRoomCall = ( ]); 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; @@ -184,26 +199,26 @@ export const useRoomCall = ( ]); 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); + placeCall(room, CallType.Voice, callPlatformType); } }, - [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); + placeCall(room, CallType.Video, callPlatformType); } }, - [widget, promptPinWidget, room, callType], + [widget, promptPinWidget, room], ); let voiceCallDisabledReason: string | null; @@ -247,5 +262,6 @@ export const useRoomCall = ( isViewingCall: isViewingCall, isConnectedToCall: groupCall?.connected ?? false, hasActiveCallSession: hasActiveCallSession, + callOptions, }; }; diff --git a/src/utils/room/placeCall.ts b/src/utils/room/placeCall.ts index a50a7f2725d..5ab1b588d9f 100644 --- a/src/utils/room/placeCall.ts +++ b/src/utils/room/placeCall.ts @@ -30,26 +30,17 @@ import { Action } from "../../dispatcher/actions"; * @param platformCallType the platform to pass the call on */ export const placeCall = async (room: Room, callType: CallType, platformCallType: PlatformCallType): Promise => { - 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, - }); - 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); + return; + } + if (platformCallType == PlatformCallType.ElementCall) { + defaultDispatcher.dispatch({ + action: Action.ViewRoom, + room_id: room.roomId, + view_call: true, + metricsTrigger: undefined, + }); + return; } }; From 076df3bc9960f41b2033b84a41ab9ccd8df00f59 Mon Sep 17 00:00:00 2001 From: Timo K Date: Thu, 25 Jan 2024 19:53:49 +0100 Subject: [PATCH 13/22] join ec when clicking join button (dont start jitsi) Use icon buttons don't show call icon when join button is visible Signed-off-by: Timo K --- src/components/views/rooms/RoomHeader.tsx | 55 +++++++++++++---------- src/hooks/room/useRoomCall.ts | 11 +++-- src/toasts/IncomingCallToast.tsx | 9 +++- 3 files changed, 48 insertions(+), 27 deletions(-) diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index 6a0fa37e4eb..7f16b38e39e 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -138,8 +138,14 @@ export default function RoomHeader({ ); const joinCallButton = ( - ); const [menuOpen, setMenuOpen] = useState(false); @@ -148,7 +154,7 @@ export default function RoomHeader({ ); - const startCallButton = ( + const startVideoCallButton = ( <> {/* Can be either a menu or just a button depending on the number of call options.*/} {callOptions.length > 1 ? ( @@ -188,6 +194,17 @@ export default function RoomHeader({ )} ); + const voiceCallButton = ( + + voiceCallClick(ev, callOptions[0])} + > + + + + ); const closeLobbyButton = ( @@ -195,17 +212,13 @@ export default function RoomHeader({ ); - let videoCallButton = startCallButton; + let videoCallButton = startVideoCallButton; if (isConnectedToCall) { videoCallButton = toggleCallButton; - } else { - if (hasActiveCallSession && !isViewingCall) { - videoCallButton = joinCallButton; - } - if (isViewingCall) { - videoCallButton = closeLobbyButton; - } + } else if (isViewingCall) { + videoCallButton = closeLobbyButton; } + return ( <> @@ -295,19 +308,15 @@ export default function RoomHeader({ {((isConnectedToCall && isViewingCall) || isVideoRoom(room)) && } - {!isVideoRoom(room) && videoCallButton} - - {!useElementCallExclusively && !isVideoRoom(room) && ( - - voiceCallClick(ev, callOptions[0])} - > - - - + {hasActiveCallSession && !isConnectedToCall ? ( + joinCallButton + ) : ( + <> + {!isVideoRoom(room) && videoCallButton} + {!useElementCallExclusively && !isVideoRoom(room) && voiceCallButton} + )} + ) : ( - )} From 0fa4ee76994000552790101f81a29fd9aa1b626c Mon Sep 17 00:00:00 2001 From: Timo K Date: Thu, 25 Jan 2024 20:10:46 +0100 Subject: [PATCH 14/22] refactor isViewingCall Signed-off-by: Timo K --- src/hooks/room/useRoomCall.ts | 28 ++++++++++++---------------- src/hooks/useCall.ts | 6 +++--- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/hooks/room/useRoomCall.ts b/src/hooks/room/useRoomCall.ts index 0c044268c33..65a97c21aed 100644 --- a/src/hooks/room/useRoomCall.ts +++ b/src/hooks/room/useRoomCall.ts @@ -24,9 +24,9 @@ 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, useParticipantCount } from "../useCall"; +import { useCall, useConnectionState, useParticipantCount } from "../useCall"; import { useRoomMemberCount } from "../useRoomMembers"; -import { ElementCall } from "../../models/Call"; +import { ConnectionState, ElementCall } from "../../models/Call"; import { placeCall } from "../../utils/room/placeCall"; import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore"; import { useRoomState } from "../useRoomState"; @@ -99,8 +99,12 @@ 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); @@ -152,29 +156,21 @@ export const useRoomCall = ( } else { widget = groupCall?.widget ?? jitsiWidget; } - - const [canPinWidget, setCanPinWidget] = useState(false); - const [widgetPinned, setWidgetPinned] = useState(false); - // We only want to prompt to pin the widget if it's not virtual (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 [isViewingCall, setIsViewingCall] = useState(SdkContextClass.instance.roomViewStore.isViewingCall()); - const onRoomViewStoreUpdate = useCallback(() => { - setIsViewingCall(SdkContextClass.instance.roomViewStore.isViewingCall()); - }, []); + const [canPinWidget, setCanPinWidget] = useState(false); + const [widgetPinned, setWidgetPinned] = useState(false); + // We only want to prompt to pin the widget if it's not virtual (not element call based) + const isECWidget = WidgetType.CALL.matches(widget?.type ?? ""); + const promptPinWidget = !isECWidget && canPinWidget && !widgetPinned; - useEventEmitter(SdkContextClass.instance.roomViewStore, UPDATE_EVENT, onRoomViewStoreUpdate); const state = useMemo((): State => { if (hasGroupCall || hasJitsiWidget || hasManagedHybridWidget) { return promptPinWidget ? State.Unpinned : State.Ongoing; @@ -265,7 +261,7 @@ export const useRoomCall = ( videoCallClick, toggleCallMaximized: toggleCallMaximized, isViewingCall: isViewingCall, - isConnectedToCall: groupCall?.connected ?? false, + isConnectedToCall: isConnectedToCall, hasActiveCallSession: hasActiveCallSession, callOptions, }; diff --git a/src/hooks/useCall.ts b/src/hooks/useCall.ts index 36468752034..67aadaf4b54 100644 --- a/src/hooks/useCall.ts +++ b/src/hooks/useCall.ts @@ -36,11 +36,11 @@ 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 | null): Map> => { From 7f2d3110d76f4ba91eee0a15dcd790cd7a6b13bb Mon Sep 17 00:00:00 2001 From: Timo K Date: Fri, 26 Jan 2024 01:28:06 +0100 Subject: [PATCH 15/22] fix room header tests Signed-off-by: Timo K --- src/components/views/rooms/RoomHeader.tsx | 6 ++-- src/hooks/room/useRoomCall.ts | 7 ++--- .../views/rooms/RoomHeader-test.tsx | 5 +++- .../__snapshots__/RoomHeader-test.tsx.snap | 28 +++++-------------- 4 files changed, 17 insertions(+), 29 deletions(-) diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index 4bc65f10c4a..bb8268b4d95 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -151,7 +151,7 @@ export default function RoomHeader({ trigger={ {callIconWithTooltip} @@ -181,10 +181,10 @@ export default function RoomHeader({ ); const voiceCallButton = ( - + voiceCallClick(ev, callOptions[0])} > diff --git a/src/hooks/room/useRoomCall.ts b/src/hooks/room/useRoomCall.ts index 65a97c21aed..b72a189fc91 100644 --- a/src/hooks/room/useRoomCall.ts +++ b/src/hooks/room/useRoomCall.ts @@ -108,10 +108,9 @@ export const useRoomCall = ( const memberCount = useRoomMemberCount(room); - const [mayEditWidgets, mayCreateElementCalls, mayCreateWidgetCall] = useRoomState(room, () => [ + const [mayEditWidgets, mayCreateElementCalls] = useRoomState(room, () => [ room.currentState.mayClientSendStateEvent("im.vector.modular.widgets", room.client), room.currentState.mayClientSendStateEvent(ElementCall.MEMBER_EVENT_TYPE.name, room.client), - room.currentState.mayClientSendStateEvent(ElementCall.CALL_EVENT_TYPE.name, room.client), ]); // The options provided to the RoomHeader. @@ -120,7 +119,7 @@ export const useRoomCall = ( const options = []; if (memberCount <= 2) { options.push(PlatformCallType.LegacyCall); - } else if (mayCreateWidgetCall || hasJitsiWidget) { + } else if (mayEditWidgets || hasJitsiWidget) { options.push(PlatformCallType.JitsiCall); } if (groupCallsEnabled) { @@ -138,7 +137,7 @@ export const useRoomCall = ( return options; }, [ memberCount, - mayCreateWidgetCall, + mayEditWidgets, hasJitsiWidget, groupCallsEnabled, hasGroupCall, diff --git a/test/components/views/rooms/RoomHeader-test.tsx b/test/components/views/rooms/RoomHeader-test.tsx index 035b60ca6e6..940d1d8bb12 100644 --- a/test/components/views/rooms/RoomHeader-test.tsx +++ b/test/components/views/rooms/RoomHeader-test.tsx @@ -373,6 +373,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"); @@ -415,7 +419,6 @@ describe("RoomHeader", () => { mockRoomMembers(room, 3); jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => { - if (key === "im.vector.modular.widgets") return true; if (key === ElementCall.MEMBER_EVENT_TYPE.name) return true; return false; }); diff --git a/test/components/views/rooms/__snapshots__/RoomHeader-test.tsx.snap b/test/components/views/rooms/__snapshots__/RoomHeader-test.tsx.snap index 792d177727a..c0befacda3d 100644 --- a/test/components/views/rooms/__snapshots__/RoomHeader-test.tsx.snap +++ b/test/components/views/rooms/__snapshots__/RoomHeader-test.tsx.snap @@ -46,49 +46,35 @@ exports[`RoomHeader does not show the face pile for DMs 1`] = ` From 2b6cb96d1ccc29b86b215d43b1687d00fb1f1402 Mon Sep 17 00:00:00 2001 From: Timo K Date: Fri, 26 Jan 2024 02:38:23 +0100 Subject: [PATCH 16/22] fix header snapshot Signed-off-by: Timo K --- .../__snapshots__/RoomHeader-test.tsx.snap | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/test/components/views/rooms/__snapshots__/RoomHeader-test.tsx.snap b/test/components/views/rooms/__snapshots__/RoomHeader-test.tsx.snap index c0befacda3d..d3a7d8b5b64 100644 --- a/test/components/views/rooms/__snapshots__/RoomHeader-test.tsx.snap +++ b/test/components/views/rooms/__snapshots__/RoomHeader-test.tsx.snap @@ -46,35 +46,50 @@ exports[`RoomHeader does not show the face pile for DMs 1`] = ` From 887cfaf5e21938b0086c2f39685bb46113cb4981 Mon Sep 17 00:00:00 2001 From: Timo K Date: Fri, 26 Jan 2024 03:02:04 +0100 Subject: [PATCH 17/22] sonar proposals Signed-off-by: Timo K --- src/components/views/rooms/RoomHeader.tsx | 8 ++++---- src/hooks/room/useRoomCall.ts | 1 - src/utils/room/placeCall.ts | 5 +---- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index bb8268b4d95..d649ff3531c 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -151,7 +151,7 @@ export default function RoomHeader({ trigger={ {callIconWithTooltip} @@ -172,7 +172,7 @@ export default function RoomHeader({ ) : ( videoCallClick(ev, callOptions[0])} > {callIconWithTooltip} @@ -181,10 +181,10 @@ export default function RoomHeader({ ); const voiceCallButton = ( - + voiceCallClick(ev, callOptions[0])} > diff --git a/src/hooks/room/useRoomCall.ts b/src/hooks/room/useRoomCall.ts index b72a189fc91..24091a80c5e 100644 --- a/src/hooks/room/useRoomCall.ts +++ b/src/hooks/room/useRoomCall.ts @@ -39,7 +39,6 @@ import defaultDispatcher from "../../dispatcher/dispatcher"; import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; import { Action } from "../../dispatcher/actions"; -// export type PlatformCallType = "element_call" | "jitsi_or_element_call" | "legacy_or_jitsi"; export enum PlatformCallType { ElementCall, JitsiCall, diff --git a/src/utils/room/placeCall.ts b/src/utils/room/placeCall.ts index 5ab1b588d9f..46e22265b69 100644 --- a/src/utils/room/placeCall.ts +++ b/src/utils/room/placeCall.ts @@ -32,15 +32,12 @@ import { Action } from "../../dispatcher/actions"; export const placeCall = async (room: Room, callType: CallType, platformCallType: PlatformCallType): Promise => { if (platformCallType == PlatformCallType.LegacyCall || platformCallType == PlatformCallType.JitsiCall) { await LegacyCallHandler.instance.placeCall(room.roomId, callType); - return; - } - if (platformCallType == PlatformCallType.ElementCall) { + } else if (platformCallType == PlatformCallType.ElementCall) { defaultDispatcher.dispatch({ action: Action.ViewRoom, room_id: room.roomId, view_call: true, metricsTrigger: undefined, }); - return; } }; From aeecf8d449d7def114fb8e3b7bad5244d8cbd904 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 29 Jan 2024 17:35:55 +0100 Subject: [PATCH 18/22] fix event shiftKey may be undefined Signed-off-by: Timo K --- src/hooks/room/useRoomCall.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/room/useRoomCall.ts b/src/hooks/room/useRoomCall.ts index f2eeec6a31d..d76aa3d171e 100644 --- a/src/hooks/room/useRoomCall.ts +++ b/src/hooks/room/useRoomCall.ts @@ -203,7 +203,7 @@ export const useRoomCall = ( if (widget && promptPinWidget) { WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top); } else { - placeCall(room, CallType.Voice, callPlatformType, evt.shiftKey); + placeCall(room, CallType.Voice, callPlatformType, evt?.shiftKey ?? false); } }, [promptPinWidget, room, widget], @@ -214,7 +214,7 @@ export const useRoomCall = ( if (widget && promptPinWidget) { WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top); } else { - placeCall(room, CallType.Video, callPlatformType, evt.shiftKey); + placeCall(room, CallType.Video, callPlatformType, evt?.shiftKey ?? false); } }, [widget, promptPinWidget, room], From 5373658c7109ad59b0aecc003621642109ac4b54 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 29 Jan 2024 23:23:49 +0100 Subject: [PATCH 19/22] more lobby time before timeout only await sticky promise on becoming sticky. Signed-off-by: Timo K --- src/models/Call.ts | 5 ++++- src/stores/widgets/StopGapWidget.ts | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/models/Call.ts b/src/models/Call.ts index 243a1fe0c2c..d60afe5a3e4 100644 --- a/src/models/Call.ts +++ b/src/models/Call.ts @@ -65,6 +65,7 @@ const waitForEvent = async ( emitter: EventEmitter, event: string, pred: (...args: any[]) => boolean = () => true, + customTimeout?: number, ): Promise => { let listener: (...args: any[]) => void; const wait = new Promise((resolve) => { @@ -74,7 +75,7 @@ const waitForEvent = async ( emitter.on(event, listener); }); - const timedOut = (await timeout(wait, false, TIMEOUT_MS)) === false; + const timedOut = (await timeout(wait, false, customTimeout ?? TIMEOUT_MS)) === false; emitter.off(event, listener!); if (timedOut) throw new Error("Timed out"); }; @@ -899,6 +900,7 @@ export class ElementCall extends Call { MatrixRTCSessionEvent.MembershipsChanged, (_, newMemberships: CallMembership[]) => newMemberships.some((m) => m.sender === this.client.getUserId()), + 60000, // allow user to wait in the lobby for 1 min ); } else { await waitForEvent( @@ -906,6 +908,7 @@ export class ElementCall extends Call { MatrixRTCSessionManagerEvents.SessionStarted, (roomId: string, session: MatrixRTCSession) => this.session.callId === session.callId && roomId === this.roomId, + 60000, // allow user to wait in the lobby for 1 min ); } this.sendCallNotify(); diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index 75529bfc1f5..c131a7e7666 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, From 0498e743588b596189a0f928c1cb43d732490e4d Mon Sep 17 00:00:00 2001 From: Timo K Date: Tue, 30 Jan 2024 11:56:05 +0100 Subject: [PATCH 20/22] don't allow starting new calls if there is an ongoing other call. Signed-off-by: Timo K --- src/components/structures/RoomView.tsx | 1 - src/hooks/room/useRoomCall.ts | 15 +++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 42a1b94ea46..ff1d61a259d 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/hooks/room/useRoomCall.ts b/src/hooks/room/useRoomCall.ts index d76aa3d171e..d9eb107dde4 100644 --- a/src/hooks/room/useRoomCall.ts +++ b/src/hooks/room/useRoomCall.ts @@ -26,7 +26,7 @@ import { useWidgets } from "../../components/views/right_panel/RoomSummaryCard"; import { WidgetType } from "../../widgets/WidgetType"; import { useCall, useConnectionState, useParticipantCount } from "../useCall"; import { useRoomMemberCount } from "../useRoomMembers"; -import { ConnectionState, 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"; @@ -38,6 +38,7 @@ 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 enum PlatformCallType { ElementCall, @@ -162,14 +163,19 @@ export const useRoomCall = ( 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 state = useMemo((): State => { + if (activeCalls.find((call) => call.roomId != room.roomId)) { + return State.Ongoing; + } if (hasGroupCall || hasJitsiWidget || hasManagedHybridWidget) { return promptPinWidget ? State.Unpinned : State.Ongoing; } @@ -184,9 +190,9 @@ export const useRoomCall = ( if (!mayCreateElementCalls && !mayEditWidgets) { return State.NoPermission; } - return State.NoCall; }, [ + activeCalls, hasGroupCall, hasJitsiWidget, hasLegacyCall, @@ -195,6 +201,7 @@ export const useRoomCall = ( mayEditWidgets, memberCount, promptPinWidget, + room.roomId, ]); const voiceCallClick = useCallback( From 5ac670cb9f1211838027186bf64ffad26a22bdfc Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 31 Jan 2024 14:42:31 +0100 Subject: [PATCH 21/22] review Signed-off-by: Timo K --- src/components/views/rooms/RoomHeader.tsx | 10 ++++++---- src/hooks/room/useRoomCall.ts | 6 +++--- src/i18n/strings/en_EN.json | 6 ++++++ src/models/Call.ts | 21 ++++++++++++++------- 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index d649ff3531c..da243b4adbd 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useEffect, useMemo, useState } from "react"; +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"; @@ -116,6 +116,8 @@ export default function RoomHeader({ const askToJoinEnabled = useFeatureEnabled("feature_ask_to_join"); + const videoClick = useCallback((ev) => videoCallClick(ev, callOptions[0]), [callOptions, videoCallClick]); + const toggleCallButton = ( @@ -126,7 +128,7 @@ export default function RoomHeader({ const joinCallButton = (