Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 5a2595a

Browse files
committed
Rebuild hook around room header call management
And tweak behaviour around ongoing yet unpinned calls to add a shortcut to quickly pin
1 parent 5f0501a commit 5a2595a

File tree

5 files changed

+222
-172
lines changed

5 files changed

+222
-172
lines changed

src/components/views/rooms/RoomHeader.tsx

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import { Icon as NotificationsIcon } from "@vector-im/compound-design-tokens/ico
2323
import { Icon as VerifiedIcon } from "@vector-im/compound-design-tokens/icons/verified.svg";
2424
import { Icon as ErrorIcon } from "@vector-im/compound-design-tokens/icons/error.svg";
2525
import { Icon as PublicIcon } from "@vector-im/compound-design-tokens/icons/public.svg";
26-
import { CallType } from "matrix-js-sdk/src/webrtc/call";
2726
import { EventType, JoinRule, type Room } from "matrix-js-sdk/src/matrix";
2827

2928
import { useRoomName } from "../../../hooks/useRoomName";
@@ -36,13 +35,12 @@ import { useRoomMemberCount, useRoomMembers } from "../../../hooks/useRoomMember
3635
import { _t } from "../../../languageHandler";
3736
import { Flex } from "../../utils/Flex";
3837
import { Box } from "../../utils/Box";
39-
import { useRoomCallStatus } from "../../../hooks/room/useRoomCallStatus";
38+
import { useRoomCall } from "../../../hooks/room/useRoomCall";
4039
import { useRoomThreadNotifications } from "../../../hooks/room/useRoomThreadNotifications";
4140
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
4241
import { useGlobalNotificationState } from "../../../hooks/useGlobalNotificationState";
4342
import SdkConfig from "../../../SdkConfig";
4443
import { useFeatureEnabled } from "../../../hooks/useSettings";
45-
import { placeCall } from "../../../utils/room/placeCall";
4644
import { useEncryptionStatus } from "../../../hooks/useEncryptionStatus";
4745
import { E2EStatus } from "../../../utils/ShieldUtils";
4846
import FacePile from "../elements/FacePile";
@@ -84,7 +82,7 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
8482
const members = useRoomMembers(room, 2500);
8583
const memberCount = useRoomMemberCount(room, { throttleWait: 2500 });
8684

87-
const { voiceCallDisabledReason, voiceCallType, videoCallDisabledReason, videoCallType } = useRoomCallStatus(room);
85+
const { voiceCallDisabledReason, voiceCallClick, videoCallDisabledReason, videoCallClick } = useRoomCall(room);
8886

8987
const groupCallsEnabled = useFeatureEnabled("feature_group_calls");
9088
/**
@@ -179,10 +177,7 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
179177
<IconButton
180178
disabled={!!voiceCallDisabledReason}
181179
aria-label={!voiceCallDisabledReason ? _t("voip|voice_call") : voiceCallDisabledReason!}
182-
onClick={(evt) => {
183-
evt.stopPropagation();
184-
placeCall(room, CallType.Voice, voiceCallType);
185-
}}
180+
onClick={voiceCallClick}
186181
>
187182
<VoiceCallIcon />
188183
</IconButton>
@@ -192,10 +187,7 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
192187
<IconButton
193188
disabled={!!videoCallDisabledReason}
194189
aria-label={!videoCallDisabledReason ? _t("voip|video_call") : videoCallDisabledReason!}
195-
onClick={(evt) => {
196-
evt.stopPropagation();
197-
placeCall(room, CallType.Video, videoCallType);
198-
}}
190+
onClick={videoCallClick}
199191
>
200192
<VideoCallIcon />
201193
</IconButton>

src/hooks/room/useRoomCall.ts

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/*
2+
Copyright 2023 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { Room } from "matrix-js-sdk/src/matrix";
18+
import React, { useCallback, useEffect, useMemo, useState } from "react";
19+
import { CallType } from "matrix-js-sdk/src/webrtc/call";
20+
21+
import { useFeatureEnabled } from "../useSettings";
22+
import SdkConfig from "../../SdkConfig";
23+
import { useEventEmitter, useEventEmitterState } from "../useEventEmitter";
24+
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../LegacyCallHandler";
25+
import { useWidgets } from "../../components/views/right_panel/RoomSummaryCard";
26+
import { WidgetType } from "../../widgets/WidgetType";
27+
import { useCall } from "../useCall";
28+
import { useRoomMemberCount } from "../useRoomMembers";
29+
import { ElementCall } from "../../models/Call";
30+
import { placeCall } from "../../utils/room/placeCall";
31+
import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
32+
import { useRoomState } from "../useRoomState";
33+
import { _t } from "../../languageHandler";
34+
35+
export type PlatformCallType = "element_call" | "jitsi_or_element_call" | "legacy_or_jitsi";
36+
37+
const enum State {
38+
NoCall,
39+
NoOneHere,
40+
NoPermission,
41+
Unpinned,
42+
Ongoing,
43+
}
44+
45+
/**
46+
* Utility hook for resolving state and click handlers for Voice & Video call buttons in the room header
47+
* @param room the room to track
48+
* @returns the call button attributes for the given room
49+
*/
50+
export const useRoomCall = (
51+
room: Room,
52+
): {
53+
voiceCallDisabledReason: string | null;
54+
voiceCallClick(evt: React.MouseEvent): void;
55+
videoCallDisabledReason: string | null;
56+
videoCallClick(evt: React.MouseEvent): void;
57+
} => {
58+
const groupCallsEnabled = useFeatureEnabled("feature_group_calls");
59+
const useElementCallExclusively = useMemo(() => {
60+
return SdkConfig.get("element_call").use_exclusively;
61+
}, []);
62+
63+
const hasLegacyCall = useEventEmitterState(
64+
LegacyCallHandler.instance,
65+
LegacyCallHandlerEvent.CallsChanged,
66+
() => LegacyCallHandler.instance.getCallForRoom(room.roomId) !== null,
67+
);
68+
69+
const widgets = useWidgets(room);
70+
const jitsiWidget = useMemo(() => widgets.find((widget) => WidgetType.JITSI.matches(widget.type)), [widgets]);
71+
const hasJitsiWidget = !!jitsiWidget;
72+
73+
const groupCall = useCall(room.roomId);
74+
const hasGroupCall = groupCall !== null;
75+
76+
const memberCount = useRoomMemberCount(room);
77+
78+
const [mayEditWidgets, mayCreateElementCalls] = useRoomState(room, () => [
79+
room.currentState.mayClientSendStateEvent("im.vector.modular.widgets", room.client),
80+
room.currentState.mayClientSendStateEvent(ElementCall.CALL_EVENT_TYPE.name, room.client),
81+
]);
82+
83+
const callType = useMemo((): PlatformCallType => {
84+
if (groupCallsEnabled) {
85+
if (hasGroupCall) {
86+
return "jitsi_or_element_call";
87+
}
88+
if (mayCreateElementCalls && hasJitsiWidget) {
89+
return "jitsi_or_element_call";
90+
}
91+
if (useElementCallExclusively || mayCreateElementCalls) {
92+
// Looks like for Audio this was previously legacy_or_jitsi
93+
return "element_call";
94+
}
95+
if (mayEditWidgets) {
96+
return "jitsi_or_element_call";
97+
}
98+
}
99+
return "legacy_or_jitsi";
100+
}, [
101+
groupCallsEnabled,
102+
hasGroupCall,
103+
mayCreateElementCalls,
104+
hasJitsiWidget,
105+
useElementCallExclusively,
106+
mayEditWidgets,
107+
]);
108+
const widget = callType === "element_call" ? groupCall?.widget : jitsiWidget;
109+
110+
const [canPinWidget, setCanPinWidget] = useState(false);
111+
const [widgetPinned, setWidgetPinned] = useState(false);
112+
const promptPinWidget = canPinWidget && !widgetPinned;
113+
114+
const updateWidgetState = useCallback((): void => {
115+
setCanPinWidget(WidgetLayoutStore.instance.canAddToContainer(room, Container.Top));
116+
setWidgetPinned(!!widget && WidgetLayoutStore.instance.isInContainer(room, widget, Container.Top));
117+
}, [room, widget]);
118+
119+
useEventEmitter(WidgetLayoutStore.instance, WidgetLayoutStore.emissionForRoom(room), updateWidgetState);
120+
useEffect(() => {
121+
updateWidgetState();
122+
}, [room, jitsiWidget, groupCall, updateWidgetState]);
123+
124+
const state = useMemo((): State => {
125+
if (hasGroupCall || hasJitsiWidget) {
126+
return promptPinWidget ? State.Unpinned : State.Ongoing;
127+
}
128+
if (hasLegacyCall) {
129+
return State.Ongoing;
130+
}
131+
132+
if (memberCount <= 1) {
133+
return State.NoOneHere;
134+
}
135+
136+
if (!mayCreateElementCalls && !mayEditWidgets) {
137+
return State.NoPermission;
138+
}
139+
140+
return State.NoCall;
141+
}, [
142+
hasGroupCall,
143+
hasJitsiWidget,
144+
hasLegacyCall,
145+
mayCreateElementCalls,
146+
mayEditWidgets,
147+
memberCount,
148+
promptPinWidget,
149+
]);
150+
151+
const voiceCallClick = useCallback(
152+
(evt: React.MouseEvent): void => {
153+
evt.stopPropagation();
154+
if (widget && promptPinWidget) {
155+
WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top);
156+
} else {
157+
placeCall(room, CallType.Voice, callType);
158+
}
159+
},
160+
[promptPinWidget, room, widget, callType],
161+
);
162+
const videoCallClick = useCallback(
163+
(evt: React.MouseEvent): void => {
164+
evt.stopPropagation();
165+
if (widget && promptPinWidget) {
166+
WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top);
167+
} else {
168+
placeCall(room, CallType.Video, callType);
169+
}
170+
},
171+
[widget, promptPinWidget, room, callType],
172+
);
173+
174+
let voiceCallDisabledReason: string | null;
175+
let videoCallDisabledReason: string | null;
176+
switch (state) {
177+
case State.NoPermission:
178+
voiceCallDisabledReason = _t("You do not have permission to start voice calls");
179+
videoCallDisabledReason = _t("You do not have permission to start voice calls");
180+
break;
181+
case State.Ongoing:
182+
voiceCallDisabledReason = _t("Ongoing call");
183+
videoCallDisabledReason = _t("Ongoing call");
184+
break;
185+
case State.NoOneHere:
186+
voiceCallDisabledReason = _t("There's no one here to call");
187+
videoCallDisabledReason = _t("There's no one here to call");
188+
break;
189+
case State.Unpinned:
190+
case State.NoCall:
191+
voiceCallDisabledReason = null;
192+
videoCallDisabledReason = null;
193+
}
194+
195+
/**
196+
* We've gone through all the steps
197+
*/
198+
return {
199+
voiceCallDisabledReason,
200+
voiceCallClick,
201+
videoCallDisabledReason,
202+
videoCallClick,
203+
};
204+
};

0 commit comments

Comments
 (0)