1
0
mirror of https://github.com/matrix-org/matrix-react-sdk.git synced 2025-07-31 13:44:28 +03:00

Add face pile to rooms (#11356)

* Add face pile to rooms

* Migrate FacePile to use Compound

* Fix CI

* Use FacePile component in room header

* Add facepile tests

* Make dead code CI happy

* Lint

* Fix tests

* Fix CSS selectors

* Update room face pile snapshot

* Use useMemo instead of useState and useEffect

* Remove unused imports

* Update snapshot

* Update snapshot
This commit is contained in:
Germain
2023-08-30 18:55:02 +01:00
committed by GitHub
parent af268b4a03
commit dc70ea5059
14 changed files with 445 additions and 131 deletions

View File

@ -23,7 +23,7 @@ describe("<FacePile />", () => {
const member = mkRoomMember("123", "456", "join");
const { asFragment } = render(
<FacePile members={[member]} size="36px" overflow={false} tooltip={<>tooltip</>} />,
<FacePile members={[member]} size="36px" overflow={false} tooltipLabel="tooltip" />,
);
expect(asFragment()).toMatchSnapshot();

View File

@ -0,0 +1,38 @@
/*
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 { render } from "@testing-library/react";
import React from "react";
import { mkRoom, mkRoomMember, stubClient, withClientContextRenderOptions } from "../../../test-utils";
import RoomFacePile from "../../../../src/components/views/elements/RoomFacePile";
import DMRoomMap from "../../../../src/utils/DMRoomMap";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
describe("<RoomFacePile />", () => {
it("renders", () => {
const cli = stubClient();
DMRoomMap.makeShared(cli);
const room = mkRoom(cli, "!123");
jest.spyOn(room, "getJoinedMembers").mockReturnValue([mkRoomMember(room.roomId, "@bob:example.org", "join")]);
const { asFragment } = render(
<RoomFacePile onlyKnownUsers={false} room={room} />,
withClientContextRenderOptions(MatrixClientPeg.get()!),
);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@ -4,11 +4,10 @@ exports[`<FacePile /> renders with a tooltip 1`] = `
<DocumentFragment>
<div
class="mx_FacePile"
data-state="closed"
>
<div
aria-describedby="mx_TooltipTarget_vY7Q4uEh"
class="mx_TextWithTooltip_target mx_FacePile_faces"
tabindex="0"
class="_stacked-avatars_2lhia_116"
>
<span
class="_avatar_2lhia_17 mx_BaseAvatar _avatar-imageless_2lhia_56"

View File

@ -0,0 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<RoomFacePile /> renders 1`] = `
<DocumentFragment>
<div
class="mx_FacePile"
data-state="closed"
>
<div
class="_stacked-avatars_2lhia_116"
>
<span
class="_avatar_2lhia_17 mx_BaseAvatar _avatar-imageless_2lhia_56"
data-color="8"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 28px;"
title=""
>
b
</span>
</div>
</div>
</DocumentFragment>
`;

View File

@ -15,12 +15,12 @@ limitations under the License.
*/
import React from "react";
import { getAllByTitle, getByText, getByTitle, render, screen } from "@testing-library/react";
import { Room, EventType, MatrixEvent, PendingEventOrdering, MatrixCall } from "matrix-js-sdk/src/matrix";
import userEvent from "@testing-library/user-event";
import { CallType } from "matrix-js-sdk/src/webrtc/call";
import { CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import { EventType, MatrixEvent, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix";
import { getAllByTitle, getByLabelText, getByText, getByTitle, render, screen } from "@testing-library/react";
import { stubClient } from "../../../test-utils";
import { mkEvent, stubClient, withClientContextRenderOptions } from "../../../test-utils";
import RoomHeader from "../../../../src/components/views/rooms/RoomHeader";
import DMRoomMap from "../../../../src/utils/DMRoomMap";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
@ -33,7 +33,7 @@ import dispatcher from "../../../../src/dispatcher/dispatcher";
import { CallStore } from "../../../../src/stores/CallStore";
import { Call, ElementCall } from "../../../../src/models/Call";
describe("Roomeader", () => {
describe("RoomHeader", () => {
let room: Room;
const ROOM_ID = "!1:example.org";
@ -57,7 +57,10 @@ describe("Roomeader", () => {
});
it("renders the room header", () => {
const { container } = render(<RoomHeader room={room} />);
const { container } = render(
<RoomHeader room={room} />,
withClientContextRenderOptions(MatrixClientPeg.get()!),
);
expect(container).toHaveTextContent(ROOM_ID);
});
@ -75,26 +78,129 @@ describe("Roomeader", () => {
});
await room.addLiveEvents([roomTopic]);
const { container } = render(<RoomHeader room={room} />);
const { container } = render(
<RoomHeader room={room} />,
withClientContextRenderOptions(MatrixClientPeg.get()!),
);
expect(container).toHaveTextContent(TOPIC);
});
it("opens the room summary", async () => {
const { container } = render(<RoomHeader room={room} />);
const { container } = render(
<RoomHeader room={room} />,
withClientContextRenderOptions(MatrixClientPeg.get()!),
);
await userEvent.click(getByText(container, ROOM_ID));
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.RoomSummary });
});
it("does not show the face pile for DMs", () => {
const client = MatrixClientPeg.get()!;
jest.spyOn(client, "getAccountData").mockReturnValue(
mkEvent({
event: true,
type: EventType.Direct,
user: client.getSafeUserId(),
content: {
"user@example.com": [room.roomId],
},
}),
);
room.getJoinedMembers = jest.fn().mockReturnValue([
{
userId: "@me:example.org",
name: "Member",
rawDisplayName: "Member",
roomId: room.roomId,
membership: "join",
getAvatarUrl: () => "mxc://avatar.url/image.png",
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
},
]);
const { asFragment } = render(
<RoomHeader room={room} />,
withClientContextRenderOptions(MatrixClientPeg.get()!),
);
expect(asFragment()).toMatchSnapshot();
});
it("shows a face pile for rooms", async () => {
const members = [
{
userId: "@me:example.org",
name: "Member",
rawDisplayName: "Member",
roomId: room.roomId,
membership: "join",
getAvatarUrl: () => "mxc://avatar.url/image.png",
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
},
{
userId: "@you:example.org",
name: "Member",
rawDisplayName: "Member",
roomId: room.roomId,
membership: "join",
getAvatarUrl: () => "mxc://avatar.url/image.png",
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
},
{
userId: "@them:example.org",
name: "Member",
rawDisplayName: "Member",
roomId: room.roomId,
membership: "join",
getAvatarUrl: () => "mxc://avatar.url/image.png",
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
},
{
userId: "@bot:example.org",
name: "Bot user",
rawDisplayName: "Bot user",
roomId: room.roomId,
membership: "join",
getAvatarUrl: () => "mxc://avatar.url/image.png",
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
},
];
room.currentState.setJoinedMemberCount(members.length);
room.getJoinedMembers = jest.fn().mockReturnValue(members);
const { container } = render(
<RoomHeader room={room} />,
withClientContextRenderOptions(MatrixClientPeg.get()!),
);
expect(container).toHaveTextContent("4");
const facePile = getByLabelText(container, "4 members");
expect(facePile).toHaveTextContent("4");
await userEvent.click(facePile);
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.RoomMemberList });
});
it("opens the thread panel", async () => {
const { container } = render(<RoomHeader room={room} />);
const { container } = render(
<RoomHeader room={room} />,
withClientContextRenderOptions(MatrixClientPeg.get()!),
);
await userEvent.click(getByTitle(container, "Threads"));
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.ThreadPanel });
});
it("opens the notifications panel", async () => {
const { container } = render(<RoomHeader room={room} />);
const { container } = render(
<RoomHeader room={room} />,
withClientContextRenderOptions(MatrixClientPeg.get()!),
);
await userEvent.click(getByTitle(container, "Notifications"));
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.NotificationPanel });
@ -103,7 +209,10 @@ describe("Roomeader", () => {
describe("groups call disabled", () => {
it("you can't call if you're alone", () => {
mockRoomMembers(room, 1);
const { container } = render(<RoomHeader room={room} />);
const { container } = render(
<RoomHeader room={room} />,
withClientContextRenderOptions(MatrixClientPeg.get()!),
);
for (const button of getAllByTitle(container, "There's no one here to call")) {
expect(button).toBeDisabled();
}
@ -111,7 +220,10 @@ describe("Roomeader", () => {
it("you can call when you're two in the room", async () => {
mockRoomMembers(room, 2);
const { container } = render(<RoomHeader room={room} />);
const { container } = render(
<RoomHeader room={room} />,
withClientContextRenderOptions(MatrixClientPeg.get()!),
);
const voiceButton = getByTitle(container, "Voice call");
const videoButton = getByTitle(container, "Video call");
expect(voiceButton).not.toBeDisabled();
@ -132,7 +244,10 @@ describe("Roomeader", () => {
// The JS-SDK does not export the class `MatrixCall` only the type
{} as MatrixCall,
);
const { container } = render(<RoomHeader room={room} />);
const { container } = render(
<RoomHeader room={room} />,
withClientContextRenderOptions(MatrixClientPeg.get()!),
);
for (const button of getAllByTitle(container, "Ongoing call")) {
expect(button).toBeDisabled();
}
@ -141,7 +256,10 @@ describe("Roomeader", () => {
it("can calls in large rooms if able to edit widgets", () => {
mockRoomMembers(room, 10);
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
const { container } = render(<RoomHeader room={room} />);
const { container } = render(
<RoomHeader room={room} />,
withClientContextRenderOptions(MatrixClientPeg.get()!),
);
expect(getByTitle(container, "Voice call")).not.toBeDisabled();
expect(getByTitle(container, "Video call")).not.toBeDisabled();
@ -150,7 +268,10 @@ describe("Roomeader", () => {
it("disable calls in large rooms by default", () => {
mockRoomMembers(room, 10);
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(false);
const { container } = render(<RoomHeader room={room} />);
const { container } = render(
<RoomHeader room={room} />,
withClientContextRenderOptions(MatrixClientPeg.get()!),
);
expect(getByTitle(container, "You do not have permission to start voice calls")).toBeDisabled();
expect(getByTitle(container, "You do not have permission to start video calls")).toBeDisabled();
});
@ -166,7 +287,10 @@ describe("Roomeader", () => {
// allow element calls
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
const { container } = render(<RoomHeader room={room} />);
const { container } = render(
<RoomHeader room={room} />,
withClientContextRenderOptions(MatrixClientPeg.get()!),
);
expect(screen.queryByTitle("Voice call")).toBeNull();
@ -187,7 +311,10 @@ describe("Roomeader", () => {
jest.spyOn(CallStore.instance, "getCall").mockReturnValue({} as Call);
const { container } = render(<RoomHeader room={room} />);
const { container } = render(
<RoomHeader room={room} />,
withClientContextRenderOptions(MatrixClientPeg.get()!),
);
expect(getByTitle(container, "Ongoing call")).toBeDisabled();
});
@ -197,7 +324,10 @@ describe("Roomeader", () => {
// The JS-SDK does not export the class `MatrixCall` only the type
{} as MatrixCall,
);
const { container } = render(<RoomHeader room={room} />);
const { container } = render(
<RoomHeader room={room} />,
withClientContextRenderOptions(MatrixClientPeg.get()!),
);
for (const button of getAllByTitle(container, "Ongoing call")) {
expect(button).toBeDisabled();
}
@ -205,7 +335,10 @@ describe("Roomeader", () => {
it("can't call if you have no friends", () => {
mockRoomMembers(room, 1);
const { container } = render(<RoomHeader room={room} />);
const { container } = render(
<RoomHeader room={room} />,
withClientContextRenderOptions(MatrixClientPeg.get()!),
);
for (const button of getAllByTitle(container, "There's no one here to call")) {
expect(button).toBeDisabled();
}
@ -213,7 +346,10 @@ describe("Roomeader", () => {
it("calls using legacy or jitsi", async () => {
mockRoomMembers(room, 2);
const { container } = render(<RoomHeader room={room} />);
const { container } = render(
<RoomHeader room={room} />,
withClientContextRenderOptions(MatrixClientPeg.get()!),
);
const voiceButton = getByTitle(container, "Voice call");
const videoButton = getByTitle(container, "Video call");
@ -236,7 +372,10 @@ describe("Roomeader", () => {
return false;
});
const { container } = render(<RoomHeader room={room} />);
const { container } = render(
<RoomHeader room={room} />,
withClientContextRenderOptions(MatrixClientPeg.get()!),
);
const voiceButton = getByTitle(container, "Voice call");
const videoButton = getByTitle(container, "Video call");
@ -260,7 +399,10 @@ describe("Roomeader", () => {
return false;
});
const { container } = render(<RoomHeader room={room} />);
const { container } = render(
<RoomHeader room={room} />,
withClientContextRenderOptions(MatrixClientPeg.get()!),
);
const voiceButton = getByTitle(container, "Voice call");
const videoButton = getByTitle(container, "Video call");

View File

@ -0,0 +1,75 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RoomHeader does not show the face pile for DMs 1`] = `
<DocumentFragment>
<header
class="mx_Flex mx_RoomHeader light-panel"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
>
<div
class="mx_DecoratedRoomAvatar"
>
<span
class="_avatar_2lhia_17 mx_BaseAvatar _avatar-imageless_2lhia_56"
data-color="7"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 40px;"
title=""
>
!
</span>
</div>
<div
class="mx_Box mx_RoomHeader_info mx_Box--flex"
style="--mx-box-flex: 1;"
>
<div
aria-level="1"
class="_font-body-lg-semibold_1g2sj_89"
dir="auto"
role="heading"
title="!1:example.org"
>
!1:example.org
</div>
</div>
<nav
class="mx_Flex"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);"
>
<button
class="_icon-button_yvmcf_17"
disabled=""
style="--cpd-icon-button-size: 32px;"
title="There's no one here to call"
>
<div />
</button>
<button
class="_icon-button_yvmcf_17"
disabled=""
style="--cpd-icon-button-size: 32px;"
title="There's no one here to call"
>
<div />
</button>
<button
class="_icon-button_yvmcf_17"
style="--cpd-icon-button-size: 32px;"
title="Threads"
>
<div />
</button>
<button
class="_icon-button_yvmcf_17"
style="--cpd-icon-button-size: 32px;"
title="Notifications"
>
<div />
</button>
</nav>
</header>
</DocumentFragment>
`;