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

Commit 9b1794e

Browse files
toger5andybalaam
authored andcommitted
New Header edgecase fixes: Close lobby button not shown, disable join button in various places, more... (#12235)
* Add missing tooltip Signed-off-by: Timo K <[email protected]> * fix incoming call toast (icon + disabled button if there is an ongoing call) Signed-off-by: Timo K <[email protected]> * 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 <[email protected]> * additional tests Signed-off-by: Timo K <[email protected]> * fix tests Signed-off-by: Timo K <[email protected]> * update snapshot Signed-off-by: Timo K <[email protected]> * fix not open menu if disabled Signed-off-by: Timo K <[email protected]> * add tooltip provider Signed-off-by: Timo K <[email protected]> * update snap class Signed-off-by: Timo K <[email protected]> * room header snap update Signed-off-by: Timo K <[email protected]> * fix snapshot Signed-off-by: Timo K <[email protected]> --------- Signed-off-by: Timo K <[email protected]>
1 parent 0508d93 commit 9b1794e

File tree

5 files changed

+132
-53
lines changed

5 files changed

+132
-53
lines changed

src/components/views/rooms/RoomHeader.tsx

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -125,30 +125,45 @@ export default function RoomHeader({
125125
</IconButton>
126126
</Tooltip>
127127
);
128+
128129
const joinCallButton = (
129-
<Button
130-
size="sm"
131-
onClick={videoClick}
132-
Icon={VideoCallIcon}
133-
className="mx_RoomHeader_join_button"
134-
color="primary"
135-
>
136-
{_t("action|join")}
137-
</Button>
130+
<Tooltip label={videoCallDisabledReason ?? _t("voip|video_call")}>
131+
<Button
132+
size="sm"
133+
onClick={videoClick}
134+
Icon={VideoCallIcon}
135+
className="mx_RoomHeader_join_button"
136+
disabled={!!videoCallDisabledReason}
137+
color="primary"
138+
aria-label={videoCallDisabledReason ?? _t("action|join")}
139+
>
140+
{_t("action|join")}
141+
</Button>
142+
</Tooltip>
138143
);
139-
const [menuOpen, setMenuOpen] = useState(false);
144+
140145
const callIconWithTooltip = (
141146
<Tooltip label={videoCallDisabledReason ?? _t("voip|video_call")}>
142147
<VideoCallIcon />
143148
</Tooltip>
144149
);
150+
151+
const [menuOpen, setMenuOpen] = useState(false);
152+
153+
const onOpenChange = useCallback(
154+
(newOpen: boolean) => {
155+
if (!videoCallDisabledReason) setMenuOpen(newOpen);
156+
},
157+
[videoCallDisabledReason],
158+
);
159+
145160
const startVideoCallButton = (
146161
<>
147162
{/* Can be either a menu or just a button depending on the number of call options.*/}
148163
{callOptions.length > 1 ? (
149164
<Menu
150165
open={menuOpen}
151-
onOpenChange={setMenuOpen}
166+
onOpenChange={onOpenChange}
152167
title={_t("voip|video_call_using")}
153168
trigger={
154169
<IconButton
@@ -165,6 +180,7 @@ export default function RoomHeader({
165180
<MenuItem
166181
key={option}
167182
label={getPlatformCallTypeLabel(option)}
183+
aria-label={getPlatformCallTypeLabel(option)}
168184
onClick={(ev) => videoCallClick(ev, option)}
169185
Icon={VideoCallIcon}
170186
onSelect={() => {} /* Dummy handler since we want the click event.*/}
@@ -195,7 +211,7 @@ export default function RoomHeader({
195211
);
196212
const closeLobbyButton = (
197213
<Tooltip label={_t("voip|close_lobby")}>
198-
<IconButton onClick={toggleCall}>
214+
<IconButton onClick={toggleCall} aria-label={_t("voip|close_lobby")}>
199215
<CloseCallIcon />
200216
</IconButton>
201217
</Tooltip>
@@ -296,7 +312,7 @@ export default function RoomHeader({
296312

297313
{((isConnectedToCall && isViewingCall) || isVideoRoom(room)) && <VideoRoomChatButton room={room} />}
298314

299-
{hasActiveCallSession && !isConnectedToCall ? (
315+
{hasActiveCallSession && !isConnectedToCall && !isViewingCall ? (
300316
joinCallButton
301317
) : (
302318
<>

src/hooks/room/useRoomCall.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ const enum State {
6161
NoPermission,
6262
Unpinned,
6363
Ongoing,
64+
NotJoined,
6465
}
6566

6667
/**
@@ -176,7 +177,7 @@ export const useRoomCall = (
176177
if (activeCalls.find((call) => call.roomId != room.roomId)) {
177178
return State.Ongoing;
178179
}
179-
if (hasGroupCall || hasJitsiWidget || hasManagedHybridWidget) {
180+
if (hasGroupCall && (hasJitsiWidget || hasManagedHybridWidget)) {
180181
return promptPinWidget ? State.Unpinned : State.Ongoing;
181182
}
182183
if (hasLegacyCall) {
@@ -243,6 +244,7 @@ export const useRoomCall = (
243244
videoCallDisabledReason = _t("voip|disabled_no_one_here");
244245
break;
245246
case State.Unpinned:
247+
case State.NotJoined:
246248
case State.NoCall:
247249
voiceCallDisabledReason = null;
248250
videoCallDisabledReason = null;

src/hooks/useCall.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,14 +74,14 @@ export const useParticipatingMembers = (call: Call): RoomMember[] => {
7474
}, [participants]);
7575
};
7676

77-
export const useFull = (call: Call): boolean => {
77+
export const useFull = (call: Call | null): boolean => {
7878
return (
7979
useParticipantCount(call) >=
8080
(SdkConfig.get("element_call").participant_limit ?? DEFAULTS.element_call.participant_limit!)
8181
);
8282
};
8383

84-
export const useJoinCallButtonDisabledTooltip = (call: Call): string | null => {
84+
export const useJoinCallButtonDisabledTooltip = (call: Call | null): string | null => {
8585
const isFull = useFull(call);
8686
const state = useConnectionState(call);
8787

src/toasts/IncomingCallToast.tsx

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import React, { useCallback, useEffect, useMemo } from "react";
17+
import React, { useCallback, useEffect, useMemo, useState } from "react";
1818
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
1919
// eslint-disable-next-line no-restricted-imports
2020
import { MatrixRTCSessionManagerEvents } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSessionManager";
2121
// eslint-disable-next-line no-restricted-imports
2222
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
23-
import { Button } from "@vector-im/compound-web";
23+
import { Button, Tooltip, TooltipProvider } from "@vector-im/compound-web";
2424
import { Icon as VideoCallIcon } from "@vector-im/compound-design-tokens/icons/video-call-solid.svg";
2525

2626
import { _t } from "../languageHandler";
@@ -41,30 +41,37 @@ import { useDispatcher } from "../hooks/useDispatcher";
4141
import { ActionPayload } from "../dispatcher/payloads";
4242
import { Call } from "../models/Call";
4343
import { AudioID } from "../LegacyCallHandler";
44-
import { useTypedEventEmitter } from "../hooks/useEventEmitter";
44+
import { useEventEmitter, useTypedEventEmitter } from "../hooks/useEventEmitter";
4545
import AccessibleTooltipButton from "../components/views/elements/AccessibleTooltipButton";
46+
import { CallStore, CallStoreEvent } from "../stores/CallStore";
4647

4748
export const getIncomingCallToastKey = (callId: string, roomId: string): string => `call_${callId}_${roomId}`;
4849
const MAX_RING_TIME_MS = 10 * 1000;
4950

5051
interface JoinCallButtonWithCallProps {
5152
onClick: (e: ButtonEvent) => void;
52-
call: Call;
53+
call: Call | null;
54+
disabledTooltip: string | undefined;
5355
}
5456

55-
function JoinCallButtonWithCall({ onClick, call }: JoinCallButtonWithCallProps): JSX.Element {
56-
const disabledTooltip = useJoinCallButtonDisabledTooltip(call);
57+
function JoinCallButtonWithCall({ onClick, call, disabledTooltip }: JoinCallButtonWithCallProps): JSX.Element {
58+
let disTooltip = disabledTooltip;
59+
const disabledBecauseFullTooltip = useJoinCallButtonDisabledTooltip(call);
60+
disTooltip = disabledTooltip ?? disabledBecauseFullTooltip ?? undefined;
5761

5862
return (
59-
<Button
60-
className="mx_IncomingCallToast_joinButton"
61-
onClick={onClick}
62-
disabled={disabledTooltip !== null}
63-
kind="primary"
64-
size="sm"
65-
>
66-
{_t("action|join")}
67-
</Button>
63+
<Tooltip label={disTooltip ?? _t("voip|video_call")}>
64+
<Button
65+
className="mx_IncomingCallToast_joinButton"
66+
onClick={onClick}
67+
disabled={disTooltip != undefined}
68+
kind="primary"
69+
Icon={VideoCallIcon}
70+
size="sm"
71+
>
72+
{_t("action|join")}
73+
</Button>
74+
</Tooltip>
6875
);
6976
}
7077

@@ -77,7 +84,11 @@ export function IncomingCallToast({ notifyEvent }: Props): JSX.Element {
7784
const room = MatrixClientPeg.safeGet().getRoom(roomId) ?? undefined;
7885
const call = useCall(roomId);
7986
const audio = useMemo(() => document.getElementById(AudioID.Ring) as HTMLMediaElement, []);
80-
87+
const [activeCalls, setActiveCalls] = useState<Call[]>(Array.from(CallStore.instance.activeCalls));
88+
useEventEmitter(CallStore.instance, CallStoreEvent.ActiveCalls, () => {
89+
setActiveCalls(Array.from(CallStore.instance.activeCalls));
90+
});
91+
const otherCallIsOngoing = activeCalls.find((call) => call.roomId !== roomId);
8192
// Start ringing if not already.
8293
useEffect(() => {
8394
const isRingToast = (notifyEvent.getContent() as unknown as { notify_type: string })["notify_type"] == "ring";
@@ -157,7 +168,7 @@ export function IncomingCallToast({ notifyEvent }: Props): JSX.Element {
157168
);
158169

159170
return (
160-
<React.Fragment>
171+
<TooltipProvider>
161172
<div>
162173
<RoomAvatar room={room ?? undefined} size="24px" />
163174
</div>
@@ -178,25 +189,17 @@ export function IncomingCallToast({ notifyEvent }: Props): JSX.Element {
178189
/>
179190
)}
180191
</div>
181-
{call ? (
182-
<JoinCallButtonWithCall onClick={onJoinClick} call={call} />
183-
) : (
184-
<Button
185-
className="mx_IncomingCallToast_joinButton"
186-
onClick={onJoinClick}
187-
kind="primary"
188-
size="sm"
189-
Icon={VideoCallIcon}
190-
>
191-
{_t("action|join")}
192-
</Button>
193-
)}
192+
<JoinCallButtonWithCall
193+
onClick={onJoinClick}
194+
call={call}
195+
disabledTooltip={otherCallIsOngoing ? "Ongoing call" : undefined}
196+
/>
194197
</div>
195198
<AccessibleTooltipButton
196199
className="mx_IncomingCallToast_closeButton"
197200
onClick={onCloseClick}
198201
title={_t("action|close")}
199202
/>
200-
</React.Fragment>
203+
</TooltipProvider>
201204
);
202205
}

test/components/views/rooms/RoomHeader-test.tsx

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ import { Call, ElementCall } from "../../../../src/models/Call";
5555
import * as ShieldUtils from "../../../../src/utils/ShieldUtils";
5656
import { Container, WidgetLayoutStore } from "../../../../src/stores/widgets/WidgetLayoutStore";
5757
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
58-
58+
import * as UseCall from "../../../../src/hooks/useCall";
59+
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
60+
import WidgetStore, { IApp } from "../../../../src/stores/WidgetStore";
5961
jest.mock("../../../../src/utils/ShieldUtils");
6062

6163
function getWrapper(): RenderOptions {
@@ -322,25 +324,30 @@ describe("RoomHeader", () => {
322324
// allow element calls
323325
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
324326
jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(true);
325-
326-
jest.spyOn(CallStore.instance, "getCall").mockReturnValue({ widget: {}, on: () => {} } as unknown as Call);
327-
327+
const widget = { type: "m.jitsi" } as IApp;
328+
jest.spyOn(CallStore.instance, "getCall").mockReturnValue({
329+
widget,
330+
on: () => {},
331+
} as unknown as Call);
332+
jest.spyOn(WidgetStore.instance, "getApps").mockReturnValue([widget]);
328333
const { container } = render(<RoomHeader room={room} />, getWrapper());
329334
expect(getByLabelText(container, "Ongoing call")).toHaveAttribute("aria-disabled", "true");
330335
});
331336

332337
it("clicking on ongoing (unpinned) call re-pins it", () => {
333-
jest.spyOn(SdkConfig, "get").mockReturnValue({ use_exclusively: true });
338+
mockRoomMembers(room, 3);
339+
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
334340
// allow calls
335341
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
336342
jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(false);
337343
const spy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer");
338344

339-
const widget = {};
345+
const widget = { type: "m.jitsi" } as IApp;
340346
jest.spyOn(CallStore.instance, "getCall").mockReturnValue({
341347
widget,
342348
on: () => {},
343349
} as unknown as Call);
350+
jest.spyOn(WidgetStore.instance, "getApps").mockReturnValue([widget]);
344351

345352
const { container } = render(<RoomHeader room={room} />, getWrapper());
346353
expect(getByLabelText(container, "Video call")).not.toHaveAttribute("aria-disabled", "true");
@@ -431,6 +438,57 @@ describe("RoomHeader", () => {
431438
fireEvent.click(videoButton);
432439
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ view_call: true }));
433440
});
441+
442+
it("buttons are disabled if there is an ongoing call", async () => {
443+
mockRoomMembers(room, 3);
444+
445+
jest.spyOn(CallStore.prototype, "activeCalls", "get").mockReturnValue(
446+
new Set([{ roomId: "some_other_room" } as Call]),
447+
);
448+
const { container } = render(<RoomHeader room={room} />, getWrapper());
449+
450+
const [videoButton, voiceButton] = getAllByLabelText(container, "Ongoing call");
451+
452+
expect(voiceButton).toHaveAttribute("aria-disabled", "true");
453+
expect(videoButton).toHaveAttribute("aria-disabled", "true");
454+
});
455+
456+
it("join button is shown if there is an ongoing call", async () => {
457+
mockRoomMembers(room, 3);
458+
jest.spyOn(UseCall, "useParticipantCount").mockReturnValue(3);
459+
const { container } = render(<RoomHeader room={room} />, getWrapper());
460+
const joinButton = getByLabelText(container, "Join");
461+
expect(joinButton).not.toHaveAttribute("aria-disabled", "true");
462+
});
463+
464+
it("join button is disabled if there is an other ongoing call", async () => {
465+
mockRoomMembers(room, 3);
466+
jest.spyOn(UseCall, "useParticipantCount").mockReturnValue(3);
467+
jest.spyOn(CallStore.prototype, "activeCalls", "get").mockReturnValue(
468+
new Set([{ roomId: "some_other_room" } as Call]),
469+
);
470+
const { container } = render(<RoomHeader room={room} />, getWrapper());
471+
const joinButton = getByLabelText(container, "Ongoing call");
472+
473+
expect(joinButton).toHaveAttribute("aria-disabled", "true");
474+
});
475+
476+
it("close lobby button is shown", async () => {
477+
mockRoomMembers(room, 3);
478+
479+
jest.spyOn(SdkContextClass.instance.roomViewStore, "isViewingCall").mockReturnValue(true);
480+
const { container } = render(<RoomHeader room={room} />, getWrapper());
481+
getByLabelText(container, "Close lobby");
482+
});
483+
484+
it("close lobby button is shown if there is an ongoing call but we are viewing the lobby", async () => {
485+
mockRoomMembers(room, 3);
486+
jest.spyOn(UseCall, "useParticipantCount").mockReturnValue(3);
487+
jest.spyOn(SdkContextClass.instance.roomViewStore, "isViewingCall").mockReturnValue(true);
488+
489+
const { container } = render(<RoomHeader room={room} />, getWrapper());
490+
getByLabelText(container, "Close lobby");
491+
});
434492
});
435493

436494
describe("public room", () => {

0 commit comments

Comments
 (0)