From 5a2595a093cb81530d3e139b3badad3cfd23d0d0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 6 Sep 2023 17:41:52 +0100 Subject: [PATCH] Rebuild hook around room header call management And tweak behaviour around ongoing yet unpinned calls to add a shortcut to quickly pin --- src/components/views/rooms/RoomHeader.tsx | 16 +- src/hooks/room/useRoomCall.ts | 204 ++++++++++++++++++++++ src/hooks/room/useRoomCallStatus.ts | 154 ---------------- src/hooks/useRoomState.ts | 18 +- src/utils/room/placeCall.ts | 2 +- 5 files changed, 222 insertions(+), 172 deletions(-) create mode 100644 src/hooks/room/useRoomCall.ts delete mode 100644 src/hooks/room/useRoomCallStatus.ts diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index d0581178de..49591ef414 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -23,7 +23,6 @@ import { Icon as NotificationsIcon } from "@vector-im/compound-design-tokens/ico import { Icon as VerifiedIcon } from "@vector-im/compound-design-tokens/icons/verified.svg"; import { Icon as ErrorIcon } from "@vector-im/compound-design-tokens/icons/error.svg"; import { Icon as PublicIcon } from "@vector-im/compound-design-tokens/icons/public.svg"; -import { CallType } from "matrix-js-sdk/src/webrtc/call"; import { EventType, JoinRule, type Room } from "matrix-js-sdk/src/matrix"; import { useRoomName } from "../../../hooks/useRoomName"; @@ -36,13 +35,12 @@ import { useRoomMemberCount, useRoomMembers } from "../../../hooks/useRoomMember import { _t } from "../../../languageHandler"; import { Flex } from "../../utils/Flex"; import { Box } from "../../utils/Box"; -import { useRoomCallStatus } from "../../../hooks/room/useRoomCallStatus"; +import { useRoomCall } from "../../../hooks/room/useRoomCall"; import { useRoomThreadNotifications } from "../../../hooks/room/useRoomThreadNotifications"; import { NotificationColor } from "../../../stores/notifications/NotificationColor"; import { useGlobalNotificationState } from "../../../hooks/useGlobalNotificationState"; import SdkConfig from "../../../SdkConfig"; import { useFeatureEnabled } from "../../../hooks/useSettings"; -import { placeCall } from "../../../utils/room/placeCall"; import { useEncryptionStatus } from "../../../hooks/useEncryptionStatus"; import { E2EStatus } from "../../../utils/ShieldUtils"; import FacePile from "../elements/FacePile"; @@ -84,7 +82,7 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element { const members = useRoomMembers(room, 2500); const memberCount = useRoomMemberCount(room, { throttleWait: 2500 }); - const { voiceCallDisabledReason, voiceCallType, videoCallDisabledReason, videoCallType } = useRoomCallStatus(room); + const { voiceCallDisabledReason, voiceCallClick, videoCallDisabledReason, videoCallClick } = useRoomCall(room); const groupCallsEnabled = useFeatureEnabled("feature_group_calls"); /** @@ -179,10 +177,7 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element { { - evt.stopPropagation(); - placeCall(room, CallType.Voice, voiceCallType); - }} + onClick={voiceCallClick} > @@ -192,10 +187,7 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element { { - evt.stopPropagation(); - placeCall(room, CallType.Video, videoCallType); - }} + onClick={videoCallClick} > diff --git a/src/hooks/room/useRoomCall.ts b/src/hooks/room/useRoomCall.ts new file mode 100644 index 0000000000..2d0e36d2a3 --- /dev/null +++ b/src/hooks/room/useRoomCall.ts @@ -0,0 +1,204 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { Room } from "matrix-js-sdk/src/matrix"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { CallType } from "matrix-js-sdk/src/webrtc/call"; + +import { useFeatureEnabled } from "../useSettings"; +import SdkConfig from "../../SdkConfig"; +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 { useRoomMemberCount } from "../useRoomMembers"; +import { 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"; + +export type PlatformCallType = "element_call" | "jitsi_or_element_call" | "legacy_or_jitsi"; + +const enum State { + NoCall, + NoOneHere, + NoPermission, + Unpinned, + Ongoing, +} + +/** + * Utility hook for resolving state and click handlers for Voice & Video call buttons in the room header + * @param room the room to track + * @returns the call button attributes for the given room + */ +export const useRoomCall = ( + room: Room, +): { + voiceCallDisabledReason: string | null; + voiceCallClick(evt: React.MouseEvent): void; + videoCallDisabledReason: string | null; + videoCallClick(evt: React.MouseEvent): void; +} => { + const groupCallsEnabled = useFeatureEnabled("feature_group_calls"); + const useElementCallExclusively = useMemo(() => { + return SdkConfig.get("element_call").use_exclusively; + }, []); + + const hasLegacyCall = useEventEmitterState( + LegacyCallHandler.instance, + LegacyCallHandlerEvent.CallsChanged, + () => LegacyCallHandler.instance.getCallForRoom(room.roomId) !== null, + ); + + const widgets = useWidgets(room); + const jitsiWidget = useMemo(() => widgets.find((widget) => WidgetType.JITSI.matches(widget.type)), [widgets]); + const hasJitsiWidget = !!jitsiWidget; + + const groupCall = useCall(room.roomId); + const hasGroupCall = groupCall !== null; + + 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), + ]); + + const callType = useMemo((): PlatformCallType => { + if (groupCallsEnabled) { + if (hasGroupCall) { + return "jitsi_or_element_call"; + } + if (mayCreateElementCalls && hasJitsiWidget) { + return "jitsi_or_element_call"; + } + if (useElementCallExclusively || mayCreateElementCalls) { + // Looks like for Audio this was previously legacy_or_jitsi + return "element_call"; + } + if (mayEditWidgets) { + return "jitsi_or_element_call"; + } + } + return "legacy_or_jitsi"; + }, [ + groupCallsEnabled, + hasGroupCall, + mayCreateElementCalls, + hasJitsiWidget, + useElementCallExclusively, + mayEditWidgets, + ]); + const widget = callType === "element_call" ? groupCall?.widget : jitsiWidget; + + const [canPinWidget, setCanPinWidget] = useState(false); + const [widgetPinned, setWidgetPinned] = useState(false); + const promptPinWidget = 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 (hasGroupCall || hasJitsiWidget) { + return promptPinWidget ? State.Unpinned : State.Ongoing; + } + if (hasLegacyCall) { + return State.Ongoing; + } + + if (memberCount <= 1) { + return State.NoOneHere; + } + + if (!mayCreateElementCalls && !mayEditWidgets) { + return State.NoPermission; + } + + return State.NoCall; + }, [ + hasGroupCall, + hasJitsiWidget, + hasLegacyCall, + mayCreateElementCalls, + mayEditWidgets, + memberCount, + promptPinWidget, + ]); + + const voiceCallClick = useCallback( + (evt: React.MouseEvent): void => { + evt.stopPropagation(); + if (widget && promptPinWidget) { + WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top); + } else { + placeCall(room, CallType.Voice, callType); + } + }, + [promptPinWidget, room, widget, callType], + ); + const videoCallClick = useCallback( + (evt: React.MouseEvent): void => { + evt.stopPropagation(); + if (widget && promptPinWidget) { + WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top); + } else { + placeCall(room, CallType.Video, callType); + } + }, + [widget, promptPinWidget, room, callType], + ); + + let voiceCallDisabledReason: string | null; + let videoCallDisabledReason: string | null; + switch (state) { + case State.NoPermission: + voiceCallDisabledReason = _t("You do not have permission to start voice calls"); + videoCallDisabledReason = _t("You do not have permission to start voice calls"); + break; + case State.Ongoing: + voiceCallDisabledReason = _t("Ongoing call"); + videoCallDisabledReason = _t("Ongoing call"); + break; + case State.NoOneHere: + voiceCallDisabledReason = _t("There's no one here to call"); + videoCallDisabledReason = _t("There's no one here to call"); + break; + case State.Unpinned: + case State.NoCall: + voiceCallDisabledReason = null; + videoCallDisabledReason = null; + } + + /** + * We've gone through all the steps + */ + return { + voiceCallDisabledReason, + voiceCallClick, + videoCallDisabledReason, + videoCallClick, + }; +}; diff --git a/src/hooks/room/useRoomCallStatus.ts b/src/hooks/room/useRoomCallStatus.ts deleted file mode 100644 index 7afd1f9ce4..0000000000 --- a/src/hooks/room/useRoomCallStatus.ts +++ /dev/null @@ -1,154 +0,0 @@ -/* -Copyright 2023 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { Room, RoomStateEvent } from "matrix-js-sdk/src/matrix"; -import { useCallback, useEffect, useMemo, useState } from "react"; - -import { useFeatureEnabled } from "../useSettings"; -import SdkConfig from "../../SdkConfig"; -import { useEventEmitterState, useTypedEventEmitterState } 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 { _t } from "../../languageHandler"; -import { useRoomMemberCount } from "../useRoomMembers"; -import { ElementCall } from "../../models/Call"; - -export type PlatformCallType = "element_call" | "jitsi_or_element_call" | "legacy_or_jitsi"; - -const DEFAULT_DISABLED_REASON = null; -const DEFAULT_CALL_TYPE = "jitsi_or_element_call"; - -/** - * Reports the call capabilities for the current room - * @param room the room to track - * @returns the call status for a room - */ -export const useRoomCallStatus = ( - room: Room, -): { - voiceCallDisabledReason: string | null; - voiceCallType: PlatformCallType; - videoCallDisabledReason: string | null; - videoCallType: PlatformCallType; -} => { - const [voiceCallDisabledReason, setVoiceCallDisabledReason] = useState(DEFAULT_DISABLED_REASON); - const [videoCallDisabledReason, setVideoCallDisabledReason] = useState(DEFAULT_DISABLED_REASON); - const [voiceCallType, setVoiceCallType] = useState(DEFAULT_CALL_TYPE); - const [videoCallType, setVideoCallType] = useState(DEFAULT_CALL_TYPE); - - const groupCallsEnabled = useFeatureEnabled("feature_group_calls"); - const useElementCallExclusively = useMemo(() => { - return SdkConfig.get("element_call").use_exclusively; - }, []); - - const hasLegacyCall = useEventEmitterState( - LegacyCallHandler.instance, - LegacyCallHandlerEvent.CallsChanged, - () => LegacyCallHandler.instance.getCallForRoom(room.roomId) !== null, - ); - - const widgets = useWidgets(room); - const hasJitsiWidget = useMemo(() => widgets.some((widget) => WidgetType.JITSI.matches(widget.type)), [widgets]); - - const hasGroupCall = useCall(room.roomId) !== null; - - const memberCount = useRoomMemberCount(room); - - const [mayEditWidgets, mayCreateElementCalls] = useTypedEventEmitterState( - room, - RoomStateEvent.Update, - useCallback( - () => [ - room.currentState.mayClientSendStateEvent("im.vector.modular.widgets", room.client), - room.currentState.mayClientSendStateEvent(ElementCall.CALL_EVENT_TYPE.name, room.client), - ], - [room], - ), - ); - - useEffect(() => { - // First reset all state to their default value - setVoiceCallDisabledReason(DEFAULT_DISABLED_REASON); - setVideoCallDisabledReason(DEFAULT_DISABLED_REASON); - setVoiceCallType(DEFAULT_CALL_TYPE); - setVideoCallType(DEFAULT_CALL_TYPE); - - // And then run the logic to figure out their correct state - if (groupCallsEnabled) { - if (useElementCallExclusively) { - if (hasGroupCall) { - setVideoCallDisabledReason(_t("Ongoing call")); - } else if (mayCreateElementCalls) { - setVideoCallType("element_call"); - } else { - setVideoCallDisabledReason(_t("You do not have permission to start video calls")); - } - } else if (hasLegacyCall || hasJitsiWidget || hasGroupCall) { - setVoiceCallDisabledReason(_t("Ongoing call")); - setVideoCallDisabledReason(_t("Ongoing call")); - } else if (memberCount <= 1) { - setVoiceCallDisabledReason(_t("There's no one here to call")); - setVideoCallDisabledReason(_t("There's no one here to call")); - } else if (memberCount === 2) { - setVoiceCallType("legacy_or_jitsi"); - setVideoCallType("legacy_or_jitsi"); - } else if (mayEditWidgets) { - setVoiceCallType("legacy_or_jitsi"); - setVideoCallType(mayCreateElementCalls ? "jitsi_or_element_call" : "legacy_or_jitsi"); - } else { - setVoiceCallDisabledReason(_t("You do not have permission to start voice calls")); - if (mayCreateElementCalls) { - setVideoCallType("element_call"); - } else { - setVideoCallDisabledReason(_t("You do not have permission to start video calls")); - } - } - } else if (hasLegacyCall || hasJitsiWidget) { - setVoiceCallDisabledReason(_t("Ongoing call")); - setVideoCallDisabledReason(_t("Ongoing call")); - } else if (memberCount <= 1) { - setVoiceCallDisabledReason(_t("There's no one here to call")); - setVideoCallDisabledReason(_t("There's no one here to call")); - } else if (memberCount === 2 || mayEditWidgets) { - setVoiceCallType("legacy_or_jitsi"); - setVideoCallType("legacy_or_jitsi"); - } else { - setVoiceCallDisabledReason(_t("You do not have permission to start voice calls")); - setVideoCallDisabledReason(_t("You do not have permission to start video calls")); - } - }, [ - memberCount, - groupCallsEnabled, - hasGroupCall, - hasJitsiWidget, - hasLegacyCall, - mayCreateElementCalls, - mayEditWidgets, - useElementCallExclusively, - ]); - - /** - * We've gone through all the steps - */ - return { - voiceCallDisabledReason, - voiceCallType, - videoCallDisabledReason, - videoCallType, - }; -}; diff --git a/src/hooks/useRoomState.ts b/src/hooks/useRoomState.ts index 1c8c27c285..dc3e4c2df1 100644 --- a/src/hooks/useRoomState.ts +++ b/src/hooks/useRoomState.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { Room, RoomState, RoomStateEvent } from "matrix-js-sdk/src/matrix"; import { useTypedEventEmitter } from "./useEventEmitter"; @@ -28,19 +28,27 @@ export const useRoomState = ( room?: Room, mapper: Mapper = defaultMapper as Mapper, ): T => { + // Create a ref that stores mapper + const savedMapper = useRef(mapper); + + // Update ref.current value if mapper changes. + useEffect(() => { + savedMapper.current = mapper; + }, [mapper]); + const [value, setValue] = useState(room ? mapper(room.currentState) : (undefined as T)); const update = useCallback(() => { if (!room) return; - setValue(mapper(room.currentState)); - }, [room, mapper]); + setValue(savedMapper.current(room.currentState)); + }, [room]); useTypedEventEmitter(room?.currentState, RoomStateEvent.Update, update); useEffect(() => { update(); return () => { - setValue(room ? mapper(room.currentState) : (undefined as T)); + setValue(room ? savedMapper.current(room.currentState) : (undefined as T)); }; - }, [room, mapper, update]); + }, [room, update]); return value; }; diff --git a/src/utils/room/placeCall.ts b/src/utils/room/placeCall.ts index b684b494ea..a50a7f2725 100644 --- a/src/utils/room/placeCall.ts +++ b/src/utils/room/placeCall.ts @@ -18,7 +18,7 @@ import { CallType } from "matrix-js-sdk/src/webrtc/call"; import { Room } from "matrix-js-sdk/src/matrix"; import LegacyCallHandler from "../../LegacyCallHandler"; -import { PlatformCallType } from "../../hooks/room/useRoomCallStatus"; +import { PlatformCallType } from "../../hooks/room/useRoomCall"; import defaultDispatcher from "../../dispatcher/dispatcher"; import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; import { Action } from "../../dispatcher/actions";