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";