From a4354a74d319c3360a7f255e507a347a33b1ff2d Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 7 Feb 2024 13:07:57 +0100 Subject: [PATCH 01/11] Add missing tooltip Signed-off-by: Timo K --- src/components/views/rooms/RoomHeader.tsx | 27 ++++++++----- src/toasts/IncomingCallToast.tsx | 49 +++++++++++------------ 2 files changed, 40 insertions(+), 36 deletions(-) diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index 9ec6921ae22..498786ef35d 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -125,23 +125,30 @@ export default function RoomHeader({ ); + const joinCallButton = ( - + + + ); - const [menuOpen, setMenuOpen] = useState(false); + const callIconWithTooltip = ( ); + + const [menuOpen, setMenuOpen] = useState(false); const startVideoCallButton = ( <> {/* Can be either a menu or just a button depending on the number of call options.*/} diff --git a/src/toasts/IncomingCallToast.tsx b/src/toasts/IncomingCallToast.tsx index 6e239404ef5..4c9c36ce1b8 100644 --- a/src/toasts/IncomingCallToast.tsx +++ b/src/toasts/IncomingCallToast.tsx @@ -20,7 +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 { Button, Tooltip } from "@vector-im/compound-web"; import { Icon as VideoCallIcon } from "@vector-im/compound-design-tokens/icons/video-call-solid.svg"; import { _t } from "../languageHandler"; @@ -49,22 +49,27 @@ const MAX_RING_TIME_MS = 10 * 1000; interface JoinCallButtonWithCallProps { onClick: (e: ButtonEvent) => void; - call: Call; + disabledTooltip: string | undefined; } -function JoinCallButtonWithCall({ onClick, call }: JoinCallButtonWithCallProps): JSX.Element { - const disabledTooltip = useJoinCallButtonDisabledTooltip(call); +function JoinCallButtonWithCall({ onClick, call, disabledTooltip }: JoinCallButtonWithCallProps): JSX.Element { + let disTooltip = disabledTooltip; + const disabledBecauseFullTooltip = useJoinCallButtonDisabledTooltip(call); + disTooltip = disabledTooltip ?? disabledBecauseFullTooltip ?? undefined; return ( - + + + ); } @@ -178,19 +183,11 @@ export function IncomingCallToast({ notifyEvent }: Props): JSX.Element { /> )} - {call ? ( - - ) : ( - - )} + Date: Wed, 7 Feb 2024 14:30:52 +0100 Subject: [PATCH 02/11] fix incoming call toast (icon + disabled button if there is an ongoing call) Signed-off-by: Timo K --- src/hooks/useCall.ts | 4 ++-- src/toasts/IncomingCallToast.tsx | 12 +++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/hooks/useCall.ts b/src/hooks/useCall.ts index 67aadaf4b54..87cc556f3a8 100644 --- a/src/hooks/useCall.ts +++ b/src/hooks/useCall.ts @@ -74,14 +74,14 @@ export const useParticipatingMembers = (call: Call): RoomMember[] => { }, [participants]); }; -export const useFull = (call: Call): boolean => { +export const useFull = (call: Call | null): boolean => { return ( useParticipantCount(call) >= (SdkConfig.get("element_call").participant_limit ?? DEFAULTS.element_call.participant_limit!) ); }; -export const useJoinCallButtonDisabledTooltip = (call: Call): string | null => { +export const useJoinCallButtonDisabledTooltip = (call: Call | null): string | null => { const isFull = useFull(call); const state = useConnectionState(call); diff --git a/src/toasts/IncomingCallToast.tsx b/src/toasts/IncomingCallToast.tsx index 4c9c36ce1b8..6aee242b7dd 100644 --- a/src/toasts/IncomingCallToast.tsx +++ b/src/toasts/IncomingCallToast.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useCallback, useEffect, useMemo } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { MatrixEvent } from "matrix-js-sdk/src/matrix"; // eslint-disable-next-line no-restricted-imports import { MatrixRTCSessionManagerEvents } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSessionManager"; @@ -41,14 +41,16 @@ import { useDispatcher } from "../hooks/useDispatcher"; import { ActionPayload } from "../dispatcher/payloads"; import { Call } from "../models/Call"; import { AudioID } from "../LegacyCallHandler"; -import { useTypedEventEmitter } from "../hooks/useEventEmitter"; +import { useEventEmitter, useTypedEventEmitter } from "../hooks/useEventEmitter"; import AccessibleTooltipButton from "../components/views/elements/AccessibleTooltipButton"; +import { CallStore, CallStoreEvent } from "../stores/CallStore"; export const getIncomingCallToastKey = (callId: string, roomId: string): string => `call_${callId}_${roomId}`; const MAX_RING_TIME_MS = 10 * 1000; interface JoinCallButtonWithCallProps { onClick: (e: ButtonEvent) => void; + call: Call | null; disabledTooltip: string | undefined; } @@ -82,7 +84,11 @@ export function IncomingCallToast({ notifyEvent }: Props): JSX.Element { const room = MatrixClientPeg.safeGet().getRoom(roomId) ?? undefined; const call = useCall(roomId); const audio = useMemo(() => document.getElementById(AudioID.Ring) as HTMLMediaElement, []); - + const [activeCalls, setActiveCalls] = useState(Array.from(CallStore.instance.activeCalls)); + useEventEmitter(CallStore.instance, CallStoreEvent.ActiveCalls, () => { + setActiveCalls(Array.from(CallStore.instance.activeCalls)); + }); + const otherCallIsOngoing = activeCalls.find((call) => call.roomId !== roomId); // Start ringing if not already. useEffect(() => { const isRingToast = (notifyEvent.getContent() as unknown as { notify_type: string })["notify_type"] == "ring"; From 61a3913b57231e0b2a65dfecae1ff268285e4591 Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 7 Feb 2024 14:34:18 +0100 Subject: [PATCH 03/11] room header - fix join button not getting disabled if there is an ongoing call - fix close lobby button not shown (instead we see the join button) Signed-off-by: Timo K --- src/components/views/rooms/RoomHeader.tsx | 4 ++-- src/hooks/room/useRoomCall.ts | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index 498786ef35d..ad76cba6e36 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -202,7 +202,7 @@ export default function RoomHeader({ ); const closeLobbyButton = ( - + @@ -303,7 +303,7 @@ export default function RoomHeader({ {((isConnectedToCall && isViewingCall) || isVideoRoom(room)) && } - {hasActiveCallSession && !isConnectedToCall ? ( + {hasActiveCallSession && !isConnectedToCall && !isViewingCall ? ( joinCallButton ) : ( <> diff --git a/src/hooks/room/useRoomCall.ts b/src/hooks/room/useRoomCall.ts index fb76bdd87dd..8d9045825c1 100644 --- a/src/hooks/room/useRoomCall.ts +++ b/src/hooks/room/useRoomCall.ts @@ -61,6 +61,7 @@ const enum State { NoPermission, Unpinned, Ongoing, + NotJoined, } /** @@ -176,7 +177,7 @@ export const useRoomCall = ( if (activeCalls.find((call) => call.roomId != room.roomId)) { return State.Ongoing; } - if (hasGroupCall || hasJitsiWidget || hasManagedHybridWidget) { + if (hasGroupCall && (hasJitsiWidget || hasManagedHybridWidget)) { return promptPinWidget ? State.Unpinned : State.Ongoing; } if (hasLegacyCall) { @@ -243,6 +244,7 @@ export const useRoomCall = ( videoCallDisabledReason = _t("voip|disabled_no_one_here"); break; case State.Unpinned: + case State.NotJoined: case State.NoCall: voiceCallDisabledReason = null; videoCallDisabledReason = null; From 0a56b64bf930bccd528f8096195d33aad16be82a Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 7 Feb 2024 14:34:47 +0100 Subject: [PATCH 04/11] additional tests Signed-off-by: Timo K --- .../views/rooms/RoomHeader-test.tsx | 54 ++++++++++++++++++- .../__snapshots__/RoomHeader-test.tsx.snap | 6 +-- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/test/components/views/rooms/RoomHeader-test.tsx b/test/components/views/rooms/RoomHeader-test.tsx index b13da1e4fec..ad180e6e616 100644 --- a/test/components/views/rooms/RoomHeader-test.tsx +++ b/test/components/views/rooms/RoomHeader-test.tsx @@ -55,7 +55,8 @@ import { Call, ElementCall } from "../../../../src/models/Call"; import * as ShieldUtils from "../../../../src/utils/ShieldUtils"; import { Container, WidgetLayoutStore } from "../../../../src/stores/widgets/WidgetLayoutStore"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; - +import * as UseCall from "../../../../src/hooks/useCall"; +import { SdkContextClass } from "../../../../src/contexts/SDKContext"; jest.mock("../../../../src/utils/ShieldUtils"); function getWrapper(): RenderOptions { @@ -431,6 +432,57 @@ describe("RoomHeader", () => { fireEvent.click(videoButton); expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ view_call: true })); }); + + it("buttons are disabled if there is an ongoing call", async () => { + mockRoomMembers(room, 3); + + jest.spyOn(CallStore.prototype, "activeCalls", "get").mockReturnValue( + new Set([{ roomId: "some_other_room" } as Call]), + ); + const { container } = render(, getWrapper()); + + const [videoButton, voiceButton] = getAllByLabelText(container, "Ongoing call"); + + expect(voiceButton).toHaveAttribute("aria-disabled", "true"); + expect(videoButton).toHaveAttribute("aria-disabled", "true"); + }); + + it("join button is shown if there is an ongoing call", async () => { + mockRoomMembers(room, 3); + jest.spyOn(UseCall, "useParticipantCount").mockReturnValue(3); + const { container } = render(, getWrapper()); + const joinButton = getByLabelText(container, "Join"); + expect(joinButton).not.toHaveAttribute("aria-disabled", "true"); + }); + + it("join button is disabled if there is an other ongoing call", async () => { + mockRoomMembers(room, 3); + jest.spyOn(UseCall, "useParticipantCount").mockReturnValue(3); + jest.spyOn(CallStore.prototype, "activeCalls", "get").mockReturnValue( + new Set([{ roomId: "some_other_room" } as Call]), + ); + const { container } = render(, getWrapper()); + const joinButton = getByLabelText(container, "Ongoing call"); + + expect(joinButton).toHaveAttribute("aria-disabled", "true"); + }); + + it("close lobby button is shown", async () => { + mockRoomMembers(room, 3); + + jest.spyOn(SdkContextClass.instance.roomViewStore, "isViewingCall").mockReturnValue(true); + const { container } = render(, getWrapper()); + getByLabelText(container, "Close lobby"); + }); + + it("close lobby button is shown if there is an ongoing call but we are viewing the lobby", async () => { + mockRoomMembers(room, 3); + jest.spyOn(UseCall, "useParticipantCount").mockReturnValue(3); + jest.spyOn(SdkContextClass.instance.roomViewStore, "isViewingCall").mockReturnValue(true); + + const { container } = render(, getWrapper()); + getByLabelText(container, "Close lobby"); + }); }); describe("public room", () => { diff --git a/test/components/views/rooms/__snapshots__/RoomHeader-test.tsx.snap b/test/components/views/rooms/__snapshots__/RoomHeader-test.tsx.snap index fd7dd0c0b4d..d3a7d8b5b64 100644 --- a/test/components/views/rooms/__snapshots__/RoomHeader-test.tsx.snap +++ b/test/components/views/rooms/__snapshots__/RoomHeader-test.tsx.snap @@ -52,7 +52,7 @@ exports[`RoomHeader does not show the face pile for DMs 1`] = ` tabindex="0" >
@@ -85,7 +85,7 @@ exports[`RoomHeader does not show the face pile for DMs 1`] = ` tabindex="0" >
From 3fd27b62c354b503f8dd98e2bdb3fe425d24d0dd Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 7 Feb 2024 15:15:20 +0100 Subject: [PATCH 05/11] fix tests Signed-off-by: Timo K --- src/components/views/rooms/RoomHeader.tsx | 1 + test/components/views/rooms/RoomHeader-test.tsx | 16 +++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index ad76cba6e36..eade8fcd5a7 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -172,6 +172,7 @@ export default function RoomHeader({ videoCallClick(ev, option)} Icon={VideoCallIcon} onSelect={() => {} /* Dummy handler since we want the click event.*/} diff --git a/test/components/views/rooms/RoomHeader-test.tsx b/test/components/views/rooms/RoomHeader-test.tsx index ad180e6e616..f051738c8a4 100644 --- a/test/components/views/rooms/RoomHeader-test.tsx +++ b/test/components/views/rooms/RoomHeader-test.tsx @@ -57,6 +57,7 @@ import { Container, WidgetLayoutStore } from "../../../../src/stores/widgets/Wid import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; import * as UseCall from "../../../../src/hooks/useCall"; import { SdkContextClass } from "../../../../src/contexts/SDKContext"; +import WidgetStore, { IApp } from "../../../../src/stores/WidgetStore"; jest.mock("../../../../src/utils/ShieldUtils"); function getWrapper(): RenderOptions { @@ -323,25 +324,30 @@ describe("RoomHeader", () => { // allow element calls jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true); jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(true); - - jest.spyOn(CallStore.instance, "getCall").mockReturnValue({ widget: {}, on: () => {} } as unknown as Call); - + const widget = { type: "m.jitsi" } as IApp; + jest.spyOn(CallStore.instance, "getCall").mockReturnValue({ + widget, + on: () => {}, + } as unknown as Call); + jest.spyOn(WidgetStore.instance, "getApps").mockReturnValue([widget]); const { container } = render(, getWrapper()); expect(getByLabelText(container, "Ongoing call")).toHaveAttribute("aria-disabled", "true"); }); it("clicking on ongoing (unpinned) call re-pins it", () => { - jest.spyOn(SdkConfig, "get").mockReturnValue({ use_exclusively: true }); + mockRoomMembers(room, 3); + jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); // allow calls jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true); jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(false); const spy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer"); - const widget = {}; + const widget = { type: "m.jitsi" } as IApp; jest.spyOn(CallStore.instance, "getCall").mockReturnValue({ widget, on: () => {}, } as unknown as Call); + jest.spyOn(WidgetStore.instance, "getApps").mockReturnValue([widget]); const { container } = render(, getWrapper()); expect(getByLabelText(container, "Video call")).not.toHaveAttribute("aria-disabled", "true"); From bb5e1eeb34a63cfa8cac7eab505ff5bc79fd42d4 Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 7 Feb 2024 15:38:22 +0100 Subject: [PATCH 06/11] update snapshot Signed-off-by: Timo K --- .../RoomHeader/__snapshots__/VideoRoomChatButton-test.tsx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 74fa81087a0..e26d89f0015 100644 --- a/test/components/views/rooms/RoomHeader/__snapshots__/VideoRoomChatButton-test.tsx.snap +++ b/test/components/views/rooms/RoomHeader/__snapshots__/VideoRoomChatButton-test.tsx.snap @@ -11,7 +11,7 @@ exports[` renders button with an unread marker when room tabindex="0" >
From 70b5ee78f7e34f6f9bbc3b688a51194fef1d3a6d Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 7 Feb 2024 15:58:33 +0100 Subject: [PATCH 07/11] fix not open menu if disabled Signed-off-by: Timo K --- src/components/views/rooms/RoomHeader.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index eade8fcd5a7..6d1ea9f8eb6 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -149,13 +149,21 @@ export default function RoomHeader({ ); const [menuOpen, setMenuOpen] = useState(false); + + const onOpenChange = useCallback( + (newOpen: boolean) => { + if (!videoCallDisabledReason) setMenuOpen(newOpen); + }, + [videoCallDisabledReason], + ); + const startVideoCallButton = ( <> {/* Can be either a menu or just a button depending on the number of call options.*/} {callOptions.length > 1 ? ( Date: Wed, 7 Feb 2024 18:17:42 +0100 Subject: [PATCH 08/11] add tooltip provider Signed-off-by: Timo K --- src/toasts/IncomingCallToast.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/toasts/IncomingCallToast.tsx b/src/toasts/IncomingCallToast.tsx index 6aee242b7dd..5fc64fc3de7 100644 --- a/src/toasts/IncomingCallToast.tsx +++ b/src/toasts/IncomingCallToast.tsx @@ -20,7 +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, Tooltip } from "@vector-im/compound-web"; +import { Button, Tooltip, TooltipProvider } from "@vector-im/compound-web"; import { Icon as VideoCallIcon } from "@vector-im/compound-design-tokens/icons/video-call-solid.svg"; import { _t } from "../languageHandler"; @@ -168,7 +168,7 @@ export function IncomingCallToast({ notifyEvent }: Props): JSX.Element { ); return ( - +
@@ -200,6 +200,6 @@ export function IncomingCallToast({ notifyEvent }: Props): JSX.Element { onClick={onCloseClick} title={_t("action|close")} /> -
+ ); } From da9ec195a740ce964e9fd5faf10ba1cffe05424f Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 7 Feb 2024 18:32:42 +0100 Subject: [PATCH 09/11] update snap class Signed-off-by: Timo K --- .../RoomHeader/__snapshots__/VideoRoomChatButton-test.tsx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e26d89f0015..93291c2efb2 100644 --- a/test/components/views/rooms/RoomHeader/__snapshots__/VideoRoomChatButton-test.tsx.snap +++ b/test/components/views/rooms/RoomHeader/__snapshots__/VideoRoomChatButton-test.tsx.snap @@ -3,7 +3,7 @@ exports[` renders button with an unread marker when room is unread 1`] = `