diff --git a/src/components/views/rooms/MemberList.tsx b/src/components/views/rooms/MemberList.tsx index a0b9cc0f70..582d329310 100644 --- a/src/components/views/rooms/MemberList.tsx +++ b/src/components/views/rooms/MemberList.tsx @@ -28,7 +28,6 @@ import { RoomStateEvent, User, UserEvent, - JoinRule, EventType, ClientEvent, } from "matrix-js-sdk/src/matrix"; @@ -54,6 +53,8 @@ import { shouldShowComponent } from "../../../customisations/helpers/UIComponent import { UIComponent } from "../../../settings/UIFeature"; import PosthogTrackers from "../../../PosthogTrackers"; import { SDKContext } from "../../../contexts/SDKContext"; +import { canInviteTo } from "../../../utils/room/canInviteTo"; +import { inviteToRoom } from "../../../utils/room/inviteToRoom"; const INITIAL_LOAD_NUM_MEMBERS = 30; const INITIAL_LOAD_NUM_INVITED = 5; @@ -132,9 +133,7 @@ export default class MemberList extends React.Component<IProps, IState> { const cli = MatrixClientPeg.safeGet(); const room = cli.getRoom(this.props.roomId); - return ( - !!room?.canInvite(cli.getSafeUserId()) || !!(room?.isSpaceRoom() && room.getJoinRule() === JoinRule.Public) - ); + return !!room && canInviteTo(room); } private getMembersState(invitedMembers: Array<RoomMember>, joinedMembers: Array<RoomMember>): IState { @@ -365,32 +364,25 @@ export default class MemberList extends React.Component<IProps, IState> { let inviteButton: JSX.Element | undefined; if (room?.getMyMembership() === "join" && shouldShowComponent(UIComponent.InviteUsers)) { - let inviteButtonText = _t("room|invite_this_room"); - if (room.isSpaceRoom()) { - inviteButtonText = _t("space|invite_this_space"); - } + const inviteButtonText = room.isSpaceRoom() ? _t("space|invite_this_space") : _t("room|invite_this_room"); + + const button = ( + <Button + size="sm" + kind="secondary" + className="mx_MemberList_invite" + onClick={this.onInviteButtonClick} + disabled={!this.state.canInvite} + > + <UserAddIcon width="1em" height="1em" /> + {inviteButtonText} + </Button> + ); if (this.state.canInvite) { - inviteButton = ( - <Button - size="sm" - kind="secondary" - className="mx_MemberList_invite" - onClick={this.onInviteButtonClick} - > - <UserAddIcon width="1em" height="1em" /> - {inviteButtonText} - </Button> - ); + inviteButton = button; } else { - inviteButton = ( - <Tooltip label={_t("member_list|invite_button_no_perms_tooltip")}> - <Button size="sm" kind="secondary" className="mx_MemberList_invite" onClick={() => {}}> - <UserAddIcon width="1em" height="1em" /> - {inviteButtonText} - </Button> - </Tooltip> - ); + inviteButton = <Tooltip label={_t("member_list|invite_button_no_perms_tooltip")}>{button}</Tooltip>; } } @@ -454,15 +446,9 @@ export default class MemberList extends React.Component<IProps, IState> { private onInviteButtonClick = (ev: ButtonEvent): void => { PosthogTrackers.trackInteraction("WebRightPanelMemberListInviteButton", ev); - if (MatrixClientPeg.safeGet().isGuest()) { - dis.dispatch({ action: "require_registration" }); - return; - } + const cli = MatrixClientPeg.safeGet(); + const room = cli.getRoom(this.props.roomId)!; - // open the room inviter - dis.dispatch({ - action: "view_invite", - roomId: this.props.roomId, - }); + inviteToRoom(room); }; } diff --git a/test/components/views/rooms/MemberList-test.tsx b/test/components/views/rooms/MemberList-test.tsx index f9f591b373..1ba7151a32 100644 --- a/test/components/views/rooms/MemberList-test.tsx +++ b/test/components/views/rooms/MemberList-test.tsx @@ -16,21 +16,37 @@ limitations under the License. */ import React from "react"; -import { act, render, RenderResult, screen } from "@testing-library/react"; +import { act, fireEvent, render, RenderResult, screen } from "@testing-library/react"; import { Room, MatrixClient, RoomState, RoomMember, User, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { compare } from "matrix-js-sdk/src/utils"; +import { mocked, MockedObject } from "jest-mock"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import * as TestUtils from "../../../test-utils"; import MemberList from "../../../../src/components/views/rooms/MemberList"; import { SDKContext } from "../../../../src/contexts/SDKContext"; import { TestSdkContext } from "../../../TestSdkContext"; +import { + filterConsole, + flushPromises, + getMockClientWithEventEmitter, + mockClientMethodsUser, +} from "../../../test-utils"; +import { shouldShowComponent } from "../../../../src/customisations/helpers/UIComponents"; +import defaultDispatcher from "../../../../src/dispatcher/dispatcher"; + +jest.mock("../../../../src/customisations/helpers/UIComponents", () => ({ + shouldShowComponent: jest.fn(), +})); function generateRoomId() { return "!" + Math.random().toString().slice(2, 10) + ":domain"; } describe("MemberList", () => { + filterConsole( + "Age for event was not available, using `now - origin_server_ts` as a fallback. If the device clock is not correct issues might occur.", + ); function createRoom(opts = {}) { const room = new Room(generateRoomId(), client, client.getUserId()!); if (opts) { @@ -331,5 +347,93 @@ describe("MemberList", () => { ); expect(await screen.findByText(/User's server unreachable/)).toBeInTheDocument(); }); + + describe("Invite button", () => { + const roomId = "!room:server.org"; + let client!: MockedObject<MatrixClient>; + let room!: Room; + + beforeEach(function () { + mocked(shouldShowComponent).mockReturnValue(true); + client = getMockClientWithEventEmitter({ + ...mockClientMethodsUser(), + getRoom: jest.fn(), + hasLazyLoadMembersEnabled: jest.fn(), + }); + room = new Room(roomId, client, client.getSafeUserId()); + client.getRoom.mockReturnValue(room); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + const renderComponent = () => { + const context = new TestSdkContext(); + context.client = client; + render( + <SDKContext.Provider value={context}> + <MemberList + searchQuery="" + onClose={jest.fn()} + onSearchQueryChanged={jest.fn()} + roomId={room.roomId} + /> + </SDKContext.Provider>, + ); + }; + + it("does not render invite button when current user is not a member", async () => { + renderComponent(); + await flushPromises(); + + expect(screen.queryByText("Invite to this room")).not.toBeInTheDocument(); + }); + + it("does not render invite button UI customisation hides invites", async () => { + mocked(shouldShowComponent).mockReturnValue(false); + renderComponent(); + await flushPromises(); + + expect(screen.queryByText("Invite to this room")).not.toBeInTheDocument(); + }); + + it("renders disabled invite button when current user is a member but does not have rights to invite", async () => { + jest.spyOn(room, "getMyMembership").mockReturnValue("join"); + jest.spyOn(room, "canInvite").mockReturnValue(false); + + renderComponent(); + await flushPromises(); + + // button rendered but disabled + expect(screen.getByText("Invite to this room")).toBeDisabled(); + }); + + it("renders enabled invite button when current user is a member and has rights to invite", async () => { + jest.spyOn(room, "getMyMembership").mockReturnValue("join"); + jest.spyOn(room, "canInvite").mockReturnValue(true); + + renderComponent(); + await flushPromises(); + + expect(screen.getByText("Invite to this room")).not.toBeDisabled(); + }); + + it("opens room inviter on button click", async () => { + jest.spyOn(defaultDispatcher, "dispatch"); + jest.spyOn(room, "getMyMembership").mockReturnValue("join"); + jest.spyOn(room, "canInvite").mockReturnValue(true); + + renderComponent(); + await flushPromises(); + + fireEvent.click(screen.getByText("Invite to this room")); + + expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ + action: "view_invite", + roomId, + }); + }); + }); }); });