diff --git a/src/components/views/rooms/RoomKnocksBar.tsx b/src/components/views/rooms/RoomKnocksBar.tsx index 8c10842687e..877e7a7c723 100644 --- a/src/components/views/rooms/RoomKnocksBar.tsx +++ b/src/components/views/rooms/RoomKnocksBar.tsx @@ -30,6 +30,16 @@ import AccessibleButton from "../elements/AccessibleButton"; import Heading from "../typography/Heading"; export const RoomKnocksBar: VFC<{ room: Room }> = ({ room }) => { + const [disabled, setDisabled] = useState(false); + const knockMembers = useTypedEventEmitterState( + room, + RoomStateEvent.Members, + useCallback(() => room.getMembersWithMembership("knock"), [room]), + ); + const knockMembersCount = knockMembers.length; + + if (room.getJoinRule() !== JoinRule.Knock || knockMembersCount === 0) return null; + const client = room.client; const userId = client.getUserId() || ""; const canInvite = room.canInvite(userId); @@ -37,34 +47,31 @@ export const RoomKnocksBar: VFC<{ room: Room }> = ({ room }) => { const state = room.getLiveTimeline().getState(EventTimeline.FORWARDS); const canKick = member && state ? state.hasSufficientPowerLevelFor("kick", member.powerLevel) : false; + if (!canInvite && !canKick) return null; + + const onError = (error: MatrixError): void => { + Modal.createDialog(ErrorDialog, { title: error.name, description: error.message }); + }; + const handleApprove = (userId: string): void => { setDisabled(true); - client.invite(room.roomId, userId).catch(onError); + client + .invite(room.roomId, userId) + .catch(onError) + .finally(() => setDisabled(false)); }; const handleDeny = (userId: string): void => { setDisabled(true); - client.kick(room.roomId, userId).catch(onError); + client + .kick(room.roomId, userId) + .catch(onError) + .finally(() => setDisabled(false)); }; const handleOpenRoomSettings = (): void => dis.dispatch({ action: "open_room_settings", room_id: room.roomId, initial_tab_id: RoomSettingsTab.People }); - const onError = (error: MatrixError): void => { - setDisabled(false); - Modal.createDialog(ErrorDialog, { title: error.name, description: error.message }); - }; - - const [disabled, setDisabled] = useState(false); - const knockMembers = useTypedEventEmitterState( - room, - RoomStateEvent.Members, - useCallback(() => room.getMembersWithMembership("knock"), [room]), - ); - const knockMembersCount = knockMembers.length; - - if (room.getJoinRule() !== JoinRule.Knock || knockMembersCount === 0 || (!canInvite && !canKick)) return null; - let buttons: ReactElement = ( = ({ room }) => { disabled={!canKick || disabled} kind="icon_primary_outline" onClick={() => handleDeny(knockMembers[0].userId)} - title={_t("Deny")} + title={_t("action|deny")} > @@ -98,7 +105,7 @@ export const RoomKnocksBar: VFC<{ room: Room }> = ({ room }) => { disabled={!canInvite || disabled} kind="icon_primary" onClick={() => handleApprove(knockMembers[0].userId)} - title={_t("Approve")} + title={_t("action|approve")} > diff --git a/test/components/views/rooms/RoomKnocksBar-test.tsx b/test/components/views/rooms/RoomKnocksBar-test.tsx index 0256947d565..6a323a6d2e5 100644 --- a/test/components/views/rooms/RoomKnocksBar-test.tsx +++ b/test/components/views/rooms/RoomKnocksBar-test.tsx @@ -52,7 +52,8 @@ describe("RoomKnocksBar", () => { const room = new Room(roomId, client, userId); const state = room.getLiveTimeline().getState(EventTimeline.FORWARDS)!; - const getButton = (name: "Approve" | "Deny" | "View" | "View message") => screen.getByRole("button", { name }); + type ButtonNames = "Approve" | "Deny" | "View" | "View message"; + const getButton = (name: ButtonNames) => screen.getByRole("button", { name }); const getComponent = (room: Room) => render( @@ -134,16 +135,33 @@ describe("RoomKnocksBar", () => { expect(getComponent(room).container.firstChild).toBeNull(); }); + it("unhides the bar when a new knock request appears", () => { + jest.spyOn(room, "getMembersWithMembership").mockReturnValue([]); + const { container } = getComponent(room); + expect(container.firstChild).toBeNull(); + jest.spyOn(room, "getMembersWithMembership").mockReturnValue([bob]); + act(() => { + room.emit(RoomStateEvent.Members, new MatrixEvent(), state, bob); + }); + expect(container.firstChild).not.toBeNull(); + }); + + it("updates when the list of knocking users changes", () => { + getComponent(room); + expect(screen.getByRole("heading")).toHaveTextContent("Asking to join"); + jest.spyOn(room, "getMembersWithMembership").mockReturnValue([bob, jane]); + act(() => { + room.emit(RoomStateEvent.Members, new MatrixEvent(), state, jane); + }); + expect(screen.getByRole("heading")).toHaveTextContent("2 people asking to join"); + }); + describe("when knock members count is 1", () => { beforeEach(() => jest.spyOn(room, "getMembersWithMembership").mockReturnValue([bob])); - it("renders a heading", () => { + it("renders a heading and a paragraph with name and user ID", () => { getComponent(room); expect(screen.getByRole("heading")).toHaveTextContent("Asking to join"); - }); - - it("renders a paragraph", () => { - getComponent(room); expect(screen.getByRole("paragraph")).toHaveTextContent(`${bob.name} (${bob.userId})`); }); @@ -157,20 +175,38 @@ describe("RoomKnocksBar", () => { }); }); + type TestCase = [string, ButtonNames, () => void]; + it.each([ + ["deny request fails", "Deny", () => jest.spyOn(client, "kick").mockRejectedValue(error)], + ["deny request succeeds", "Deny", () => jest.spyOn(client, "kick").mockResolvedValue({})], + ["approve request fails", "Approve", () => jest.spyOn(client, "invite").mockRejectedValue(error)], + ["approve request succeeds", "Approve", () => jest.spyOn(client, "invite").mockResolvedValue({})], + ])("toggles the disabled attribute for the buttons when a %s", async (_, buttonName, setup) => { + setup(); + getComponent(room); + fireEvent.click(getButton(buttonName)); + expect(getButton("Deny")).toHaveAttribute("disabled"); + expect(getButton("Approve")).toHaveAttribute("disabled"); + await act(() => flushPromises()); + expect(getButton("Deny")).not.toHaveAttribute("disabled"); + expect(getButton("Approve")).not.toHaveAttribute("disabled"); + }); + 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", () => { + it("calls kick on deny", async () => { jest.spyOn(client, "kick").mockResolvedValue({}); getComponent(room); fireEvent.click(getButton("Deny")); + await act(() => flushPromises()); expect(client.kick).toHaveBeenCalledWith(roomId, bob.userId); }); - it("fails to deny a request", async () => { + it("displays an error when a deny request fails", async () => { jest.spyOn(client, "kick").mockRejectedValue(error); getComponent(room); fireEvent.click(getButton("Deny")); @@ -187,14 +223,15 @@ describe("RoomKnocksBar", () => { expect(getButton("Approve")).toHaveAttribute("disabled"); }); - it("calls invite on approve", () => { + it("calls invite on approve", async () => { jest.spyOn(client, "invite").mockResolvedValue({}); getComponent(room); fireEvent.click(getButton("Approve")); - expect(client.kick).toHaveBeenCalledWith(roomId, bob.userId); + await act(() => flushPromises()); + expect(client.invite).toHaveBeenCalledWith(roomId, bob.userId); }); - it("fails to approve a request", async () => { + it("displays an error when an approval fails", async () => { jest.spyOn(client, "invite").mockRejectedValue(error); getComponent(room); fireEvent.click(getButton("Approve")); @@ -205,7 +242,7 @@ describe("RoomKnocksBar", () => { }); }); - it("succeeds to deny/approve a request", () => { + it("hides the bar when someone else approves or denies the waiting person", () => { getComponent(room); jest.spyOn(room, "getMembersWithMembership").mockReturnValue([]); act(() => {