Show correct call icon for joining a call. (#31489)

* Show correct call icon in header.

* fix import

* Simply useRoomCall output

* Add tests and a label

* update snap

* update test
This commit is contained in:
Will Hunt
2026-01-02 17:51:15 +00:00
committed by GitHub
parent cbe3eb1709
commit 7398a83ae4
5 changed files with 53 additions and 15 deletions
@@ -21,6 +21,7 @@ import PublicIcon from "@vector-im/compound-design-tokens/assets/web/icons/publi
import { JoinRule, type Room } from "matrix-js-sdk/src/matrix"; import { JoinRule, type Room } from "matrix-js-sdk/src/matrix";
import { type ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle"; import { type ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
import { Flex, Box } from "@element-hq/web-shared-components"; import { Flex, Box } from "@element-hq/web-shared-components";
import { CallType } from "matrix-js-sdk/src/webrtc/call";
import { useRoomName } from "../../../../hooks/useRoomName.ts"; import { useRoomName } from "../../../../hooks/useRoomName.ts";
import { RightPanelPhases } from "../../../../stores/right-panel/RightPanelStorePhases.ts"; import { RightPanelPhases } from "../../../../stores/right-panel/RightPanelStorePhases.ts";
@@ -73,7 +74,7 @@ function RoomHeaderButtons({
toggleCallMaximized: toggleCall, toggleCallMaximized: toggleCall,
isViewingCall, isViewingCall,
isConnectedToCall, isConnectedToCall,
hasActiveCallSession, activeCallSessionType,
callOptions, callOptions,
showVoiceCallButton, showVoiceCallButton,
showVideoCallButton, showVideoCallButton,
@@ -105,15 +106,26 @@ function RoomHeaderButtons({
); );
const joinCallButton = ( const joinCallButton = (
<Tooltip label={videoCallDisabledReason ?? _t("voip|video_call")}> <Tooltip
label={
videoCallDisabledReason ??
(activeCallSessionType === CallType.Voice ? _t("voip|voice_call") : _t("voip|video_call"))
}
>
<Button <Button
size="sm" size="sm"
onClick={videoClick} onClick={videoClick}
Icon={VideoCallIcon} // If we know this is a voice session, show the voice call. All other kinds of call are video calls.
Icon={activeCallSessionType === CallType.Voice ? VoiceCallIcon : VideoCallIcon}
className="mx_RoomHeader_join_button" className="mx_RoomHeader_join_button"
disabled={!!videoCallDisabledReason} disabled={!!videoCallDisabledReason}
color="primary" color="primary"
aria-label={videoCallDisabledReason ?? _t("action|join")} aria-label={
videoCallDisabledReason ??
(activeCallSessionType === CallType.Voice
? _t("room|header|join_voice_call")
: _t("room|header|join_video_call"))
}
data-testId="join-call-button" data-testId="join-call-button"
> >
{_t("action|join")} {_t("action|join")}
@@ -303,7 +315,7 @@ function RoomHeaderButtons({
{isViewingCall && <CallGuestLinkButton room={room} />} {isViewingCall && <CallGuestLinkButton room={room} />}
{hasActiveCallSession && !isConnectedToCall && !isViewingCall ? ( {activeCallSessionType && !isConnectedToCall && !isViewingCall ? (
joinCallButton joinCallButton
) : ( ) : (
<> <>
+13 -3
View File
@@ -93,7 +93,10 @@ export const useRoomCall = (
toggleCallMaximized: () => void; toggleCallMaximized: () => void;
isViewingCall: boolean; isViewingCall: boolean;
isConnectedToCall: boolean; isConnectedToCall: boolean;
hasActiveCallSession: boolean; /**
* The type of call in progress, or `null` if no call is ongoing.
*/
activeCallSessionType: CallType | null;
callOptions: PlatformCallType[]; callOptions: PlatformCallType[];
showVideoCallButton: boolean; showVideoCallButton: boolean;
showVoiceCallButton: boolean; showVoiceCallButton: boolean;
@@ -123,13 +126,20 @@ export const useRoomCall = (
const groupCall = useCall(room.roomId); const groupCall = useCall(room.roomId);
const isConnectedToCall = useConnectionState(groupCall) === ConnectionState.Connected; const isConnectedToCall = useConnectionState(groupCall) === ConnectionState.Connected;
const hasGroupCall = groupCall !== null; const hasGroupCall = groupCall !== null;
const hasActiveCallSession = useParticipantCount(groupCall) > 0;
const isViewingCall = useEventEmitterState( const isViewingCall = useEventEmitterState(
roomViewStore, roomViewStore,
UPDATE_EVENT, UPDATE_EVENT,
() => roomViewStore.isViewingCall() || isVideoRoom(room), () => roomViewStore.isViewingCall() || isVideoRoom(room),
); );
const participantCount = useParticipantCount(groupCall);
const activeCallSessionType = useMemo(() => {
if (!groupCall || participantCount === 0) {
return null;
}
return groupCall.callType;
}, [participantCount, groupCall]);
// room // room
const memberCount = useRoomMemberCount(room); const memberCount = useRoomMemberCount(room);
@@ -307,7 +317,7 @@ export const useRoomCall = (
toggleCallMaximized: toggleCallMaximized, toggleCallMaximized: toggleCallMaximized,
isViewingCall: isViewingCall, isViewingCall: isViewingCall,
isConnectedToCall: isConnectedToCall, isConnectedToCall: isConnectedToCall,
hasActiveCallSession: hasActiveCallSession, activeCallSessionType: activeCallSessionType,
callOptions, callOptions,
showVoiceCallButton: !hideVoiceCallButton, showVoiceCallButton: !hideVoiceCallButton,
showVideoCallButton: !hideVideoCallButton, showVideoCallButton: !hideVideoCallButton,
+2
View File
@@ -2029,6 +2029,8 @@
"forget_room": "Forget this room", "forget_room": "Forget this room",
"forget_space": "Forget this space", "forget_space": "Forget this space",
"header": { "header": {
"join_video_call": "Join video call",
"join_voice_call": "Join voice call",
"n_people_asking_to_join": { "n_people_asking_to_join": {
"one": "Asking to join", "one": "Asking to join",
"other": "%(count)s people asking to join" "other": "%(count)s people asking to join"
@@ -582,12 +582,21 @@ describe("RoomHeader", () => {
expect(videoButton).toHaveAttribute("aria-disabled", "true"); expect(videoButton).toHaveAttribute("aria-disabled", "true");
}); });
it("join button is shown if there is an ongoing call", async () => { it("join video call button is shown if there is an ongoing call", async () => {
mockRoomMembers(room, 3); mockRoomMembers(room, 3);
// Mock CallStore to return a call with 3 participants // Mock CallStore to return a call with 3 participants
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(createMockCall(ROOM_ID, 3)); jest.spyOn(CallStore.instance, "getCall").mockReturnValue(createMockCall(ROOM_ID, 3));
render(<RoomHeader room={room} />, getWrapper()); render(<RoomHeader room={room} />, getWrapper());
const joinButton = getByLabelText(document.body, "Join"); const joinButton = getByLabelText(document.body, "Join video call");
expect(joinButton).not.toHaveAttribute("aria-disabled", "true");
});
it("join voice call button is shown if there is an ongoing call", async () => {
mockRoomMembers(room, 3);
// Mock CallStore to return a call with 3 participants
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(createMockCall(ROOM_ID, 3, CallType.Voice));
render(<RoomHeader room={room} />, getWrapper());
const joinButton = getByLabelText(document.body, "Join voice call");
expect(joinButton).not.toHaveAttribute("aria-disabled", "true"); expect(joinButton).not.toHaveAttribute("aria-disabled", "true");
}); });
@@ -859,7 +868,11 @@ describe("RoomHeader", () => {
/** /**
* Creates a mock Call object with stable participants to prevent React dependency errors * Creates a mock Call object with stable participants to prevent React dependency errors
*/ */
function createMockCall(roomId: string = "!1:example.org", participantCount: number = 0): Call { function createMockCall(
roomId: string = "!1:example.org",
participantCount: number = 0,
callType: CallType = CallType.Video,
): Call {
const participants = new Map(); const participants = new Map();
// Create mock participants with devices // Create mock participants with devices
@@ -878,6 +891,7 @@ function createMockCall(roomId: string = "!1:example.org", participantCount: num
participants, participants,
widget: { id: "test-widget" }, widget: { id: "test-widget" },
connectionState: "disconnected", connectionState: "disconnected",
callType,
on: jest.fn(), on: jest.fn(),
off: jest.fn(), off: jest.fn(),
emit: jest.fn(), emit: jest.fn(),
@@ -56,7 +56,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
style="--cpd-icon-button-size: 100%;" style="--cpd-icon-button-size: 100%;"
> >
<svg <svg
aria-labelledby="_r_17e_" aria-labelledby="_r_18c_"
fill="currentColor" fill="currentColor"
height="1em" height="1em"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -83,7 +83,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
style="--cpd-icon-button-size: 100%;" style="--cpd-icon-button-size: 100%;"
> >
<svg <svg
aria-labelledby="_r_17j_" aria-labelledby="_r_18h_"
fill="currentColor" fill="currentColor"
height="1em" height="1em"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -98,7 +98,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
</button> </button>
<button <button
aria-label="Threads" aria-label="Threads"
aria-labelledby="_r_17o_" aria-labelledby="_r_18m_"
class="_icon-button_1pz9o_8" class="_icon-button_1pz9o_8"
data-kind="primary" data-kind="primary"
role="button" role="button"
@@ -125,7 +125,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
</button> </button>
<button <button
aria-label="Room info" aria-label="Room info"
aria-labelledby="_r_17t_" aria-labelledby="_r_18r_"
class="_icon-button_1pz9o_8" class="_icon-button_1pz9o_8"
data-kind="primary" data-kind="primary"
role="button" role="button"