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(() => {