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

Commit f9f2e79

Browse files
authored
Merge pull request #11576 from matrix-org/t3chguy/cr/72
Make video & voice call buttons pin conference widget if unpinned
2 parents 3fbf38f + ea3067b commit f9f2e79

File tree

11 files changed

+332
-227
lines changed

11 files changed

+332
-227
lines changed

src/components/views/rooms/RoomHeader.tsx

Lines changed: 8 additions & 16 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";
@@ -35,13 +34,12 @@ import { useRoomMemberCount, useRoomMembers } from "../../../hooks/useRoomMember
3534
import { _t } from "../../../languageHandler";
3635
import { Flex } from "../../utils/Flex";
3736
import { Box } from "../../utils/Box";
38-
import { useRoomCallStatus } from "../../../hooks/room/useRoomCallStatus";
37+
import { useRoomCall } from "../../../hooks/room/useRoomCall";
3938
import { useRoomThreadNotifications } from "../../../hooks/room/useRoomThreadNotifications";
4039
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
4140
import { useGlobalNotificationState } from "../../../hooks/useGlobalNotificationState";
4241
import SdkConfig from "../../../SdkConfig";
4342
import { useFeatureEnabled } from "../../../hooks/useSettings";
44-
import { placeCall } from "../../../utils/room/placeCall";
4543
import { useEncryptionStatus } from "../../../hooks/useEncryptionStatus";
4644
import { E2EStatus } from "../../../utils/ShieldUtils";
4745
import FacePile from "../elements/FacePile";
@@ -74,7 +72,7 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
7472
const members = useRoomMembers(room, 2500);
7573
const memberCount = useRoomMemberCount(room, { throttleWait: 2500 });
7674

77-
const { voiceCallDisabledReason, voiceCallType, videoCallDisabledReason, videoCallType } = useRoomCallStatus(room);
75+
const { voiceCallDisabledReason, voiceCallClick, videoCallDisabledReason, videoCallClick } = useRoomCall(room);
7876

7977
const groupCallsEnabled = useFeatureEnabled("feature_group_calls");
8078
/**
@@ -170,11 +168,8 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
170168
<Tooltip label={!voiceCallDisabledReason ? _t("voip|voice_call") : voiceCallDisabledReason!}>
171169
<IconButton
172170
disabled={!!voiceCallDisabledReason}
173-
title={!voiceCallDisabledReason ? _t("voip|voice_call") : voiceCallDisabledReason!}
174-
onClick={(evt) => {
175-
evt.stopPropagation();
176-
placeCall(room, CallType.Voice, voiceCallType);
177-
}}
171+
aria-label={!voiceCallDisabledReason ? _t("voip|voice_call") : voiceCallDisabledReason!}
172+
onClick={voiceCallClick}
178173
>
179174
<VoiceCallIcon />
180175
</IconButton>
@@ -183,11 +178,8 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
183178
<Tooltip label={!videoCallDisabledReason ? _t("voip|video_call") : videoCallDisabledReason!}>
184179
<IconButton
185180
disabled={!!videoCallDisabledReason}
186-
title={!videoCallDisabledReason ? _t("voip|video_call") : videoCallDisabledReason!}
187-
onClick={(evt) => {
188-
evt.stopPropagation();
189-
placeCall(room, CallType.Video, videoCallType);
190-
}}
181+
aria-label={!videoCallDisabledReason ? _t("voip|video_call") : videoCallDisabledReason!}
182+
onClick={videoCallClick}
191183
>
192184
<VideoCallIcon />
193185
</IconButton>
@@ -199,7 +191,7 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
199191
evt.stopPropagation();
200192
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.ThreadPanel);
201193
}}
202-
title={_t("common|threads")}
194+
aria-label={_t("common|threads")}
203195
>
204196
<ThreadsIcon />
205197
</IconButton>
@@ -212,7 +204,7 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
212204
evt.stopPropagation();
213205
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.NotificationPanel);
214206
}}
215-
title={_t("Notifications")}
207+
aria-label={_t("Notifications")}
216208
>
217209
<NotificationsIcon />
218210
</IconButton>

src/hooks/room/useRoomCall.ts

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
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+
import { isManagedHybridWidget } from "../../widgets/ManagedHybrid";
35+
import { IApp } from "../../stores/WidgetStore";
36+
37+
export type PlatformCallType = "element_call" | "jitsi_or_element_call" | "legacy_or_jitsi";
38+
39+
const enum State {
40+
NoCall,
41+
NoOneHere,
42+
NoPermission,
43+
Unpinned,
44+
Ongoing,
45+
}
46+
47+
/**
48+
* Utility hook for resolving state and click handlers for Voice & Video call buttons in the room header
49+
* @param room the room to track
50+
* @returns the call button attributes for the given room
51+
*/
52+
export const useRoomCall = (
53+
room: Room,
54+
): {
55+
voiceCallDisabledReason: string | null;
56+
voiceCallClick(evt: React.MouseEvent): void;
57+
videoCallDisabledReason: string | null;
58+
videoCallClick(evt: React.MouseEvent): void;
59+
} => {
60+
const groupCallsEnabled = useFeatureEnabled("feature_group_calls");
61+
const useElementCallExclusively = useMemo(() => {
62+
return SdkConfig.get("element_call").use_exclusively;
63+
}, []);
64+
65+
const hasLegacyCall = useEventEmitterState(
66+
LegacyCallHandler.instance,
67+
LegacyCallHandlerEvent.CallsChanged,
68+
() => LegacyCallHandler.instance.getCallForRoom(room.roomId) !== null,
69+
);
70+
71+
const widgets = useWidgets(room);
72+
const jitsiWidget = useMemo(() => widgets.find((widget) => WidgetType.JITSI.matches(widget.type)), [widgets]);
73+
const hasJitsiWidget = !!jitsiWidget;
74+
const managedHybridWidget = useMemo(() => widgets.find(isManagedHybridWidget), [widgets]);
75+
const hasManagedHybridWidget = !!managedHybridWidget;
76+
77+
const groupCall = useCall(room.roomId);
78+
const hasGroupCall = groupCall !== null;
79+
80+
const memberCount = useRoomMemberCount(room);
81+
82+
const [mayEditWidgets, mayCreateElementCalls] = useRoomState(room, () => [
83+
room.currentState.mayClientSendStateEvent("im.vector.modular.widgets", room.client),
84+
room.currentState.mayClientSendStateEvent(ElementCall.CALL_EVENT_TYPE.name, room.client),
85+
]);
86+
87+
const callType = useMemo((): PlatformCallType => {
88+
if (groupCallsEnabled) {
89+
if (hasGroupCall) {
90+
return "jitsi_or_element_call";
91+
}
92+
if (mayCreateElementCalls && hasJitsiWidget) {
93+
return "jitsi_or_element_call";
94+
}
95+
if (useElementCallExclusively) {
96+
return "element_call";
97+
}
98+
if (memberCount <= 2) {
99+
return "legacy_or_jitsi";
100+
}
101+
if (mayCreateElementCalls) {
102+
return "element_call";
103+
}
104+
}
105+
return "legacy_or_jitsi";
106+
}, [
107+
groupCallsEnabled,
108+
hasGroupCall,
109+
mayCreateElementCalls,
110+
hasJitsiWidget,
111+
useElementCallExclusively,
112+
memberCount,
113+
]);
114+
115+
let widget: IApp | undefined;
116+
if (callType === "legacy_or_jitsi") {
117+
widget = jitsiWidget ?? managedHybridWidget;
118+
} else if (callType === "element_call") {
119+
widget = groupCall?.widget;
120+
} else {
121+
widget = groupCall?.widget ?? jitsiWidget;
122+
}
123+
124+
const [canPinWidget, setCanPinWidget] = useState(false);
125+
const [widgetPinned, setWidgetPinned] = useState(false);
126+
const promptPinWidget = canPinWidget && !widgetPinned;
127+
128+
const updateWidgetState = useCallback((): void => {
129+
setCanPinWidget(WidgetLayoutStore.instance.canAddToContainer(room, Container.Top));
130+
setWidgetPinned(!!widget && WidgetLayoutStore.instance.isInContainer(room, widget, Container.Top));
131+
}, [room, widget]);
132+
133+
useEventEmitter(WidgetLayoutStore.instance, WidgetLayoutStore.emissionForRoom(room), updateWidgetState);
134+
useEffect(() => {
135+
updateWidgetState();
136+
}, [room, jitsiWidget, groupCall, updateWidgetState]);
137+
138+
const state = useMemo((): State => {
139+
if (hasGroupCall || hasJitsiWidget || hasManagedHybridWidget) {
140+
return promptPinWidget ? State.Unpinned : State.Ongoing;
141+
}
142+
if (hasLegacyCall) {
143+
return State.Ongoing;
144+
}
145+
146+
if (memberCount <= 1) {
147+
return State.NoOneHere;
148+
}
149+
150+
if (!mayCreateElementCalls && !mayEditWidgets) {
151+
return State.NoPermission;
152+
}
153+
154+
return State.NoCall;
155+
}, [
156+
hasGroupCall,
157+
hasJitsiWidget,
158+
hasLegacyCall,
159+
hasManagedHybridWidget,
160+
mayCreateElementCalls,
161+
mayEditWidgets,
162+
memberCount,
163+
promptPinWidget,
164+
]);
165+
166+
const voiceCallClick = useCallback(
167+
(evt: React.MouseEvent): void => {
168+
evt.stopPropagation();
169+
if (widget && promptPinWidget) {
170+
WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top);
171+
} else {
172+
placeCall(room, CallType.Voice, callType);
173+
}
174+
},
175+
[promptPinWidget, room, widget, callType],
176+
);
177+
const videoCallClick = useCallback(
178+
(evt: React.MouseEvent): void => {
179+
evt.stopPropagation();
180+
if (widget && promptPinWidget) {
181+
WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top);
182+
} else {
183+
placeCall(room, CallType.Video, callType);
184+
}
185+
},
186+
[widget, promptPinWidget, room, callType],
187+
);
188+
189+
let voiceCallDisabledReason: string | null;
190+
let videoCallDisabledReason: string | null;
191+
switch (state) {
192+
case State.NoPermission:
193+
voiceCallDisabledReason = _t("You do not have permission to start voice calls");
194+
videoCallDisabledReason = _t("You do not have permission to start video calls");
195+
break;
196+
case State.Ongoing:
197+
voiceCallDisabledReason = _t("Ongoing call");
198+
videoCallDisabledReason = _t("Ongoing call");
199+
break;
200+
case State.NoOneHere:
201+
voiceCallDisabledReason = _t("There's no one here to call");
202+
videoCallDisabledReason = _t("There's no one here to call");
203+
break;
204+
case State.Unpinned:
205+
case State.NoCall:
206+
voiceCallDisabledReason = null;
207+
videoCallDisabledReason = null;
208+
}
209+
210+
/**
211+
* We've gone through all the steps
212+
*/
213+
return {
214+
voiceCallDisabledReason,
215+
voiceCallClick,
216+
videoCallDisabledReason,
217+
videoCallClick,
218+
};
219+
};

0 commit comments

Comments
 (0)