diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 413a18a15e..acc70e756f 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -341,6 +341,7 @@ @import "./views/settings/tabs/_SettingsSection.pcss"; @import "./views/settings/tabs/_SettingsTab.pcss"; @import "./views/settings/tabs/room/_NotificationSettingsTab.pcss"; +@import "./views/settings/tabs/room/_PeopleRoomSettingsTab.pcss"; @import "./views/settings/tabs/room/_RolesRoomSettingsTab.pcss"; @import "./views/settings/tabs/room/_SecurityRoomSettingsTab.pcss"; @import "./views/settings/tabs/user/_AppearanceUserSettingsTab.pcss"; diff --git a/res/css/views/dialogs/_RoomSettingsDialog.pcss b/res/css/views/dialogs/_RoomSettingsDialog.pcss index f57f636193..b0a08919aa 100644 --- a/res/css/views/dialogs/_RoomSettingsDialog.pcss +++ b/res/css/views/dialogs/_RoomSettingsDialog.pcss @@ -50,6 +50,10 @@ limitations under the License. mask-image: url("$(res)/img/element-icons/room/settings/advanced.svg"); } +.mx_RoomSettingsDialog_peopleIcon::before { + mask-image: url("$(res)/img/element-icons/group-members.svg"); +} + .mx_RoomSettingsDialog .mx_Dialog_title { -ms-text-overflow: ellipsis; text-overflow: ellipsis; diff --git a/res/css/views/elements/_AccessibleButton.pcss b/res/css/views/elements/_AccessibleButton.pcss index 35a5287fa9..172d8fc053 100644 --- a/res/css/views/elements/_AccessibleButton.pcss +++ b/res/css/views/elements/_AccessibleButton.pcss @@ -20,6 +20,8 @@ limitations under the License. &.mx_AccessibleButton_disabled { cursor: not-allowed; + &.mx_AccessibleButton_kind_icon_primary, + &.mx_AccessibleButton_kind_icon_primary_outline, &.mx_AccessibleButton_kind_primary, &.mx_AccessibleButton_kind_primary_outline, &.mx_AccessibleButton_kind_primary_sm, @@ -80,29 +82,37 @@ limitations under the License. } } - &.mx_AccessibleButton_kind_icon { + &.mx_AccessibleButton_kind_icon, + &.mx_AccessibleButton_kind_icon_primary, + &.mx_AccessibleButton_kind_icon_primary_outline { padding: 0; height: 32px; width: 32px; } } + &.mx_AccessibleButton_kind_icon_primary, + &.mx_AccessibleButton_kind_icon_primary_outline, &.mx_AccessibleButton_kind_primary, &.mx_AccessibleButton_kind_primary_outline, &.mx_AccessibleButton_kind_secondary { font-weight: var(--cpd-font-weight-semibold); } + &.mx_AccessibleButton_kind_icon_primary, + &.mx_AccessibleButton_kind_icon_primary_outline, &.mx_AccessibleButton_kind_primary, &.mx_AccessibleButton_kind_primary_outline { border: 1px solid $accent; } + &.mx_AccessibleButton_kind_icon_primary, &.mx_AccessibleButton_kind_primary { color: $button-primary-fg-color; background-color: $accent; } + &.mx_AccessibleButton_kind_icon_primary_outline, &.mx_AccessibleButton_kind_primary_outline { color: $accent; } diff --git a/res/css/views/settings/tabs/room/_PeopleRoomSettingsTab.pcss b/res/css/views/settings/tabs/room/_PeopleRoomSettingsTab.pcss new file mode 100644 index 0000000000..0b9c5c00a2 --- /dev/null +++ b/res/css/views/settings/tabs/room/_PeopleRoomSettingsTab.pcss @@ -0,0 +1,56 @@ +/* +Copyright 2023 Nordeck IT + Consulting GmbH + +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. +*/ + +.mx_PeopleRoomSettingsTab_knock { + display: flex; + margin-top: var(--cpd-space-2x); +} + +.mx_PeopleRoomSettingsTab_content { + flex-grow: 1; + margin: 0 var(--cpd-space-4x); +} + +.mx_PeopleRoomSettingsTab_name { + font-weight: var(--cpd-font-weight-semibold); +} + +.mx_PeopleRoomSettingsTab_timestamp { + color: $secondary-content; + margin-left: var(--cpd-space-1x); +} + +.mx_PeopleRoomSettingsTab_userId { + color: $secondary-content; + display: block; + font-size: var(--cpd-font-size-body-sm); +} + +.mx_PeopleRoomSettingsTab_seeMoreOrLess { + margin: var(--cpd-space-3x) 0 0; +} + +.mx_PeopleRoomSettingsTab_action { + flex-shrink: 0; + + + .mx_PeopleRoomSettingsTab_action { + margin-left: var(--cpd-space-3x); + } +} + +.mx_PeopleRoomSettingsTab_paragraph { + margin: 0; +} diff --git a/res/img/feather-customised/check.svg b/res/img/feather-customised/check.svg index 5c600f8649..85cd196511 100644 --- a/res/img/feather-customised/check.svg +++ b/res/img/feather-customised/check.svg @@ -1,3 +1,3 @@ - + diff --git a/res/img/feather-customised/x.svg b/res/img/feather-customised/x.svg index 5468caa8aa..a4f6c4a81a 100644 --- a/res/img/feather-customised/x.svg +++ b/res/img/feather-customised/x.svg @@ -1,4 +1,4 @@ - + diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx index a362b0d162..9b462291d4 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.tsx +++ b/src/components/views/dialogs/RoomSettingsDialog.tsx @@ -18,7 +18,7 @@ limitations under the License. */ import React from "react"; -import { RoomEvent, Room } from "matrix-js-sdk/src/matrix"; +import { RoomEvent, Room, RoomStateEvent, MatrixEvent, EventType } from "matrix-js-sdk/src/matrix"; import TabbedView, { Tab } from "../../structures/TabbedView"; import { _t, _td } from "../../../languageHandler"; @@ -39,9 +39,11 @@ import { ActionPayload } from "../../../dispatcher/payloads"; import { NonEmptyArray } from "../../../@types/common"; import { PollHistoryTab } from "../settings/tabs/room/PollHistoryTab"; import ErrorBoundary from "../elements/ErrorBoundary"; +import { PeopleRoomSettingsTab } from "../settings/tabs/room/PeopleRoomSettingsTab"; export const enum RoomSettingsTab { General = "ROOM_GENERAL_TAB", + People = "ROOM_PEOPLE_TAB", Voip = "ROOM_VOIP_TAB", Security = "ROOM_SECURITY_TAB", Roles = "ROOM_ROLES_TAB", @@ -74,6 +76,7 @@ class RoomSettingsDialog extends React.Component { public componentDidMount(): void { this.dispatcherRef = dis.register(this.onAction); MatrixClientPeg.safeGet().on(RoomEvent.Name, this.onRoomName); + MatrixClientPeg.safeGet().on(RoomStateEvent.Events, this.onStateEvent); this.onRoomName(); } @@ -90,6 +93,7 @@ class RoomSettingsDialog extends React.Component { } MatrixClientPeg.get()?.removeListener(RoomEvent.Name, this.onRoomName); + MatrixClientPeg.get()?.removeListener(RoomStateEvent.Events, this.onStateEvent); } /** @@ -120,6 +124,10 @@ class RoomSettingsDialog extends React.Component { this.forceUpdate(); }; + private onStateEvent = (event: MatrixEvent): void => { + if (event.getType() === EventType.RoomJoinRules) this.forceUpdate(); + }; + private getTabs(): NonEmptyArray> { const tabs: Tab[] = []; @@ -132,6 +140,16 @@ class RoomSettingsDialog extends React.Component { "RoomSettingsGeneral", ), ); + if (SettingsStore.getValue("feature_ask_to_join") && this.state.room.getJoinRule() === "knock") { + tabs.push( + new Tab( + RoomSettingsTab.People, + _td("People"), + "mx_RoomSettingsDialog_peopleIcon", + , + ), + ); + } if (SettingsStore.getValue("feature_group_calls")) { tabs.push( new Tab( diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index 10f18f1f31..e679955d8a 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -38,7 +38,9 @@ type AccessibleButtonKind = | "link_sm" | "confirm_sm" | "cancel_sm" - | "icon"; + | "icon" + | "icon_primary" + | "icon_primary_outline"; /** * This type construct allows us to specifically pass those props down to the element we’re creating that the element diff --git a/src/components/views/settings/tabs/room/PeopleRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/PeopleRoomSettingsTab.tsx new file mode 100644 index 0000000000..e861571ff1 --- /dev/null +++ b/src/components/views/settings/tabs/room/PeopleRoomSettingsTab.tsx @@ -0,0 +1,173 @@ +/* +Copyright 2023 Nordeck IT + Consulting GmbH + +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 { EventTimeline, MatrixError, Room, RoomMember, RoomStateEvent } from "matrix-js-sdk/src/matrix"; +import React, { useCallback, useState, VFC } from "react"; + +import { Icon as CheckIcon } from "../../../../../../res/img/feather-customised/check.svg"; +import { Icon as XIcon } from "../../../../../../res/img/feather-customised/x.svg"; +import { formatRelativeTime } from "../../../../../DateUtils"; +import { useTypedEventEmitterState } from "../../../../../hooks/useEventEmitter"; +import { _t } from "../../../../../languageHandler"; +import Modal, { IHandle } from "../../../../../Modal"; +import MemberAvatar from "../../../avatars/MemberAvatar"; +import ErrorDialog from "../../../dialogs/ErrorDialog"; +import AccessibleButton from "../../../elements/AccessibleButton"; +import SettingsFieldset from "../../SettingsFieldset"; +import { SettingsSection } from "../../shared/SettingsSection"; +import SettingsTab from "../SettingsTab"; + +const Timestamp: VFC<{ roomMember: RoomMember }> = ({ roomMember }) => { + const timestamp = roomMember.events.member?.event.origin_server_ts; + if (!timestamp) return null; + return ; +}; + +const SeeMoreOrLess: VFC<{ roomMember: RoomMember }> = ({ roomMember }) => { + const [seeMore, setSeeMore] = useState(false); + const reason = roomMember.events.member?.getContent().reason; + + if (!reason) return null; + + const truncateAt = 120; + const shouldTruncate = reason.length > truncateAt; + + return ( + <> +

+ {seeMore || !shouldTruncate ? reason : `${reason.substring(0, truncateAt)}…`} +

+ {shouldTruncate && ( + setSeeMore(!seeMore)}> + {seeMore ? _t("See less") : _t("See more")} + + )} + + ); +}; + +const Knock: VFC<{ + canInvite: boolean; + canKick: boolean; + onApprove: (userId: string) => Promise; + onDeny: (userId: string) => Promise; + roomMember: RoomMember; +}> = ({ canKick, canInvite, onApprove, onDeny, roomMember }) => { + const [disabled, setDisabled] = useState(false); + + const handleApprove = (userId: string): void => { + setDisabled(true); + onApprove(userId).catch(onError); + }; + + const handleDeny = (userId: string): void => { + setDisabled(true); + onDeny(userId).catch(onError); + }; + + const onError = (): void => setDisabled(false); + + return ( +
+ +
+ {roomMember.name} + + {roomMember.userId} + +
+ handleDeny(roomMember.userId)} + title={_t("Deny")} + > + + + handleApprove(roomMember.userId)} + title={_t("Approve")} + > + + +
+ ); +}; + +export const PeopleRoomSettingsTab: VFC<{ room: Room }> = ({ room }) => { + const client = room.client; + const userId = client.getUserId() || ""; + const canInvite = room.canInvite(userId); + const member = room.getMember(userId); + const state = room.getLiveTimeline().getState(EventTimeline.FORWARDS); + const canKick = member && state ? state.hasSufficientPowerLevelFor("kick", member.powerLevel) : false; + const roomId = room.roomId; + + const handleApprove = (userId: string): Promise => + new Promise((_, reject) => + client.invite(roomId, userId).catch((error) => { + onError(error); + reject(error); + }), + ); + + const handleDeny = (userId: string): Promise => + new Promise((_, reject) => + client.kick(roomId, userId).catch((error) => { + onError(error); + reject(error); + }), + ); + + const onError = (error: MatrixError): IHandle => + Modal.createDialog(ErrorDialog, { + title: error.name, + description: error.message, + }); + + const knockMembers = useTypedEventEmitterState( + room, + RoomStateEvent.Members, + useCallback(() => room.getMembersWithMembership("knock"), [room]), + ); + + return ( + + + + {knockMembers.length ? ( + knockMembers.map((knockMember) => ( + + )) + ) : ( +

{_t("No requests")}

+ )} +
+
+
+ ); +}; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 81fb487a06..a39e72bb78 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1698,6 +1698,12 @@ "Set a new custom sound": "Set a new custom sound", "Upload custom sound": "Upload custom sound", "Browse": "Browse", + "See less": "See less", + "See more": "See more", + "Deny": "Deny", + "Approve": "Approve", + "Asking to join": "Asking to join", + "No requests": "No requests", "Failed to unban": "Failed to unban", "Banned by %(displayName)s": "Banned by %(displayName)s", "Reason": "Reason", @@ -3123,7 +3129,6 @@ "Verification Request": "Verification Request", "Approve widget permissions": "Approve widget permissions", "This widget would like to:": "This widget would like to:", - "Approve": "Approve", "Decline All": "Decline All", "Remember my selection for this widget": "Remember my selection for this widget", "Allow this widget to verify your identity": "Allow this widget to verify your identity", diff --git a/test/components/views/dialogs/RoomSettingsDialog-test.tsx b/test/components/views/dialogs/RoomSettingsDialog-test.tsx index 566358de79..714d2527d6 100644 --- a/test/components/views/dialogs/RoomSettingsDialog-test.tsx +++ b/test/components/views/dialogs/RoomSettingsDialog-test.tsx @@ -16,7 +16,16 @@ limitations under the License. import React from "react"; import { fireEvent, render, screen } from "@testing-library/react"; -import { Room, Visibility } from "matrix-js-sdk/src/matrix"; +import { mkEvent } from "matrix-js-sdk/spec/test-utils/test-utils"; +import { + EventTimeline, + EventType, + JoinRule, + MatrixEvent, + Room, + RoomStateEvent, + Visibility, +} from "matrix-js-sdk/src/matrix"; import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../test-utils"; import RoomSettingsDialog from "../../../../src/components/views/dialogs/RoomSettingsDialog"; @@ -84,6 +93,54 @@ describe("", () => { expect(container.querySelectorAll(".mx_TabbedView_tabLabel")).toMatchSnapshot(); }); + describe("people settings tab", () => { + it("does not render when disabled and room join rule is not knock", () => { + jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Invite); + getComponent(); + expect(screen.queryByTestId("settings-tab-ROOM_PEOPLE_TAB")).not.toBeInTheDocument(); + }); + + it("does not render when disabled and room join rule is knock", () => { + jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Knock); + getComponent(); + expect(screen.queryByTestId("settings-tab-ROOM_PEOPLE_TAB")).not.toBeInTheDocument(); + }); + + it("does not render when enabled and room join rule is not knock", () => { + jest.spyOn(SettingsStore, "getValue").mockImplementation( + (setting) => setting === "feature_ask_to_join", + ); + jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Invite); + getComponent(); + expect(screen.queryByTestId("settings-tab-ROOM_PEOPLE_TAB")).not.toBeInTheDocument(); + }); + + it("renders when enabled and room join rule is knock", () => { + jest.spyOn(SettingsStore, "getValue").mockImplementation( + (setting) => setting === "feature_ask_to_join", + ); + jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Knock); + getComponent(); + expect(screen.getByTestId("settings-tab-ROOM_PEOPLE_TAB")).toBeInTheDocument(); + }); + + it("re-renders on room join rule changes", () => { + jest.spyOn(SettingsStore, "getValue").mockImplementation( + (setting) => setting === "feature_ask_to_join", + ); + jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Knock); + getComponent(); + jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Invite); + mockClient.emit( + RoomStateEvent.Events, + new MatrixEvent(mkEvent({ content: {}, type: EventType.RoomJoinRules })), + room.getLiveTimeline().getState(EventTimeline.FORWARDS)!, + null, + ); + expect(screen.queryByTestId("settings-tab-ROOM_PEOPLE_TAB")).not.toBeInTheDocument(); + }); + }); + it("renders voip settings tab when enabled", () => { jest.spyOn(SettingsStore, "getValue").mockImplementation( (settingName) => settingName === "feature_group_calls", diff --git a/test/components/views/settings/tabs/room/PeopleRoomSettingsTab-test.tsx b/test/components/views/settings/tabs/room/PeopleRoomSettingsTab-test.tsx new file mode 100644 index 0000000000..180b3609ea --- /dev/null +++ b/test/components/views/settings/tabs/room/PeopleRoomSettingsTab-test.tsx @@ -0,0 +1,217 @@ +/* +Copyright 2023 Nordeck IT + Consulting GmbH + +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 { act, fireEvent, render, screen, within } from "@testing-library/react"; +import { + EventTimeline, + EventType, + MatrixError, + MatrixEvent, + Room, + RoomMember, + RoomStateEvent, +} from "matrix-js-sdk/src/matrix"; +import React from "react"; + +import ErrorDialog from "../../../../../../src/components/views/dialogs/ErrorDialog"; +import { PeopleRoomSettingsTab } from "../../../../../../src/components/views/settings/tabs/room/PeopleRoomSettingsTab"; +import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext"; +import Modal from "../../../../../../src/Modal"; +import { flushPromises, getMockClientWithEventEmitter } from "../../../../../test-utils"; + +describe("PeopleRoomSettingsTab", () => { + const client = getMockClientWithEventEmitter({ + getUserId: jest.fn(), + invite: jest.fn(), + kick: jest.fn(), + mxcUrlToHttp: (mxcUrl: string) => mxcUrl, + }); + const roomId = "#ask-to-join:example.org"; + const userId = "@alice:example.org"; + const member = new RoomMember(roomId, userId); + const room = new Room(roomId, client, userId); + const state = room.getLiveTimeline().getState(EventTimeline.FORWARDS)!; + + const getButton = (name: "Approve" | "Deny" | "See less" | "See more") => screen.getByRole("button", { name }); + const getComponent = (room: Room) => + render( + + + , + ); + const getGroup = () => screen.getByRole("group", { name: "Asking to join" }); + const getParagraph = () => screen.getByRole("paragraph"); + + it("renders a heading", () => { + getComponent(room); + expect(screen.getByRole("heading")).toHaveTextContent("People"); + }); + + it('renders a group "asking to join"', () => { + getComponent(room); + expect(getGroup()).toBeInTheDocument(); + }); + + describe("without requests to join", () => { + it('renders a paragraph "no requests"', () => { + getComponent(room); + expect(getParagraph()).toHaveTextContent("No requests"); + }); + }); + + describe("with requests to join", () => { + const error = new MatrixError(); + const knockUserId = "@albert.einstein:example.org"; + const knockMember = new RoomMember(roomId, knockUserId); + const reason = + "There are only two ways to live your life. One is as though nothing is a miracle. The other is as though everything is a miracle."; + + beforeEach(() => { + jest.spyOn(Modal, "createDialog"); + jest.spyOn(room, "canInvite").mockReturnValue(true); + jest.spyOn(room, "getMember").mockReturnValue(member); + jest.spyOn(room, "getMembersWithMembership").mockReturnValue([knockMember]); + jest.spyOn(state, "hasSufficientPowerLevelFor").mockReturnValue(true); + + knockMember.setMembershipEvent( + new MatrixEvent({ + content: { + avatar_url: "mxc://example.org/albert-einstein.png", + displayname: "Albert Einstein", + membership: "knock", + reason, + }, + origin_server_ts: -464140800000, + type: EventType.RoomMember, + }), + ); + }); + + it("renders requests fully", () => { + getComponent(room); + expect(getGroup()).toMatchSnapshot(); + }); + + it("renders requests reduced", () => { + knockMember.setMembershipEvent( + new MatrixEvent({ + content: { + displayname: "albert.einstein", + membership: "knock", + }, + type: EventType.RoomMember, + }), + ); + getComponent(room); + expect(getGroup()).toMatchSnapshot(); + }); + + it("allows to expand a reason", () => { + getComponent(room); + fireEvent.click(getButton("See more")); + expect(within(getGroup()).getByRole("paragraph")).toHaveTextContent(reason); + }); + + it("allows to collapse a reason", () => { + getComponent(room); + fireEvent.click(getButton("See more")); + fireEvent.click(getButton("See less")); + expect(getParagraph()).toHaveTextContent(`${reason.substring(0, 120)}…`); + }); + + it("does not truncate a reason unnecessarily", () => { + const reason = "I have no special talents. I am only passionately curious."; + knockMember.setMembershipEvent( + new MatrixEvent({ + content: { + displayname: "albert.einstein", + membership: "knock", + reason, + }, + type: EventType.RoomMember, + }), + ); + getComponent(room); + expect(getParagraph()).toHaveTextContent(reason); + }); + + it("disables the deny button if the power level is insufficient", () => { + jest.spyOn(state, "hasSufficientPowerLevelFor").mockReturnValue(false); + getComponent(room); + expect(getButton("Deny")).toHaveAttribute("disabled"); + }); + + it("calls kick on deny", () => { + jest.spyOn(client, "kick").mockResolvedValue({}); + getComponent(room); + fireEvent.click(getButton("Deny")); + expect(client.kick).toHaveBeenCalledWith(roomId, knockUserId); + }); + + it("fails to deny a request", async () => { + jest.spyOn(client, "kick").mockRejectedValue(error); + getComponent(room); + fireEvent.click(getButton("Deny")); + await act(() => flushPromises()); + expect(Modal.createDialog).toHaveBeenCalledWith(ErrorDialog, { + title: error.name, + description: error.message, + }); + }); + + it("succeeds to deny a request", () => { + jest.spyOn(room, "getMembersWithMembership").mockReturnValue([]); + getComponent(room); + act(() => { + room.emit(RoomStateEvent.Members, new MatrixEvent(), state, knockMember); + }); + expect(getParagraph()).toHaveTextContent("No requests"); + }); + + it("disables the approve button if the power level is insufficient", () => { + jest.spyOn(room, "canInvite").mockReturnValue(false); + getComponent(room); + expect(getButton("Approve")).toHaveAttribute("disabled"); + }); + + it("calls invite on approve", () => { + jest.spyOn(client, "invite").mockResolvedValue({}); + getComponent(room); + fireEvent.click(getButton("Approve")); + expect(client.invite).toHaveBeenCalledWith(roomId, knockUserId); + }); + + it("fails to approve a request", async () => { + jest.spyOn(client, "invite").mockRejectedValue(error); + getComponent(room); + fireEvent.click(getButton("Approve")); + await act(() => flushPromises()); + expect(Modal.createDialog).toHaveBeenCalledWith(ErrorDialog, { + title: error.name, + description: error.message, + }); + }); + + it("succeeds to approve a request", () => { + jest.spyOn(room, "getMembersWithMembership").mockReturnValue([]); + getComponent(room); + act(() => { + room.emit(RoomStateEvent.Members, new MatrixEvent(), state, knockMember); + }); + expect(getParagraph()).toHaveTextContent("No requests"); + }); + }); +}); diff --git a/test/components/views/settings/tabs/room/__snapshots__/PeopleRoomSettingsTab-test.tsx.snap b/test/components/views/settings/tabs/room/__snapshots__/PeopleRoomSettingsTab-test.tsx.snap new file mode 100644 index 0000000000..1bc058bd08 --- /dev/null +++ b/test/components/views/settings/tabs/room/__snapshots__/PeopleRoomSettingsTab-test.tsx.snap @@ -0,0 +1,161 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PeopleRoomSettingsTab with requests to join renders requests fully 1`] = ` +
+ + Asking to join + +
+
+ +
+ + Albert Einstein + + + + @albert.einstein:example.org + +

+ There are only two ways to live your life. One is as though nothing is a miracle. The other is as though everything is a… +

+ +
+
+
+
+
+
+
+
+
+
+`; + +exports[`PeopleRoomSettingsTab with requests to join renders requests reduced 1`] = ` +
+ + Asking to join + +
+
+ + + + +
+ + albert.einstein + + + @albert.einstein:example.org + +
+
+
+
+
+
+
+
+
+
+`;