mirror of https://github.com/vector-im/riot-web
Introduce room knocks bar (#11475)
* Introduce room knocks bar Signed-off-by: Charly Nguyen <charly.nguyen@nordeck.net> * Apply PR feedback Signed-off-by: Charly Nguyen <charly.nguyen@nordeck.net> --------- Signed-off-by: Charly Nguyen <charly.nguyen@nordeck.net>t3chguy/dedup-icons-17oct
parent
f948a8f798
commit
45094bda7c
|
@ -298,6 +298,7 @@
|
||||||
@import "./views/rooms/_RoomCallBanner.pcss";
|
@import "./views/rooms/_RoomCallBanner.pcss";
|
||||||
@import "./views/rooms/_RoomHeader.pcss";
|
@import "./views/rooms/_RoomHeader.pcss";
|
||||||
@import "./views/rooms/_RoomInfoLine.pcss";
|
@import "./views/rooms/_RoomInfoLine.pcss";
|
||||||
|
@import "./views/rooms/_RoomKnocksBar.pcss";
|
||||||
@import "./views/rooms/_RoomList.pcss";
|
@import "./views/rooms/_RoomList.pcss";
|
||||||
@import "./views/rooms/_RoomListHeader.pcss";
|
@import "./views/rooms/_RoomListHeader.pcss";
|
||||||
@import "./views/rooms/_RoomPreviewBar.pcss";
|
@import "./views/rooms/_RoomPreviewBar.pcss";
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
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_RoomKnocksBar {
|
||||||
|
background-color: var(--cpd-color-bg-subtle-secondary);
|
||||||
|
display: flex;
|
||||||
|
padding: var(--cpd-space-2x) var(--cpd-space-4x);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomKnocksBar_content {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin: 0 var(--cpd-space-3x);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomKnocksBar_paragraph {
|
||||||
|
color: $secondary-content;
|
||||||
|
font-size: var(--cpd-font-size-body-sm);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomKnocksBar_link {
|
||||||
|
margin-left: var(--cpd-space-3x);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomKnocksBar_action,
|
||||||
|
.mx_RoomKnocksBar_avatar {
|
||||||
|
align-self: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomKnocksBar_action + .mx_RoomKnocksBar_action {
|
||||||
|
margin-left: var(--cpd-space-3x);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomKnocksBar_avatar + .mx_RoomKnocksBar_avatar {
|
||||||
|
margin-left: calc(var(--cpd-space-4x) * -1);
|
||||||
|
}
|
|
@ -36,6 +36,7 @@ import RoomTopic from "../elements/RoomTopic";
|
||||||
import RoomName from "../elements/RoomName";
|
import RoomName from "../elements/RoomName";
|
||||||
import { E2EStatus } from "../../../utils/ShieldUtils";
|
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||||
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
||||||
|
import { RoomKnocksBar } from "./RoomKnocksBar";
|
||||||
import { SearchScope } from "./SearchBar";
|
import { SearchScope } from "./SearchBar";
|
||||||
import { aboveLeftOf, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
|
import { aboveLeftOf, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
|
||||||
import RoomContextMenu from "../context_menus/RoomContextMenu";
|
import RoomContextMenu from "../context_menus/RoomContextMenu";
|
||||||
|
@ -820,6 +821,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
||||||
</div>
|
</div>
|
||||||
{!isVideoRoom && <RoomCallBanner roomId={this.props.room.roomId} />}
|
{!isVideoRoom && <RoomCallBanner roomId={this.props.room.roomId} />}
|
||||||
<RoomLiveShareWarning roomId={this.props.room.roomId} />
|
<RoomLiveShareWarning roomId={this.props.room.roomId} />
|
||||||
|
<RoomKnocksBar room={this.props.room} />
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,159 @@
|
||||||
|
/*
|
||||||
|
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, JoinRule, MatrixError, Room, RoomStateEvent } from "matrix-js-sdk/src/matrix";
|
||||||
|
import React, { ReactElement, ReactNode, 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 dis from "../../../dispatcher/dispatcher";
|
||||||
|
import { useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
import Modal from "../../../Modal";
|
||||||
|
import MemberAvatar from "../avatars/MemberAvatar";
|
||||||
|
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||||
|
import { RoomSettingsTab } from "../dialogs/RoomSettingsDialog";
|
||||||
|
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);
|
||||||
|
const member = room.getMember(userId);
|
||||||
|
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)
|
||||||
|
.finally(() => setDisabled(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeny = (userId: string): void => {
|
||||||
|
setDisabled(true);
|
||||||
|
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 });
|
||||||
|
|
||||||
|
let buttons: ReactElement = (
|
||||||
|
<AccessibleButton
|
||||||
|
className="mx_RoomKnocksBar_action"
|
||||||
|
kind="primary"
|
||||||
|
onClick={handleOpenRoomSettings}
|
||||||
|
title={_t("action|view")}
|
||||||
|
>
|
||||||
|
{_t("action|view")}
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
let names: string = knockMembers
|
||||||
|
.slice(0, 2)
|
||||||
|
.map((knockMember) => knockMember.name)
|
||||||
|
.join(", ");
|
||||||
|
let link: ReactNode = null;
|
||||||
|
switch (knockMembersCount) {
|
||||||
|
case 1: {
|
||||||
|
buttons = (
|
||||||
|
<>
|
||||||
|
<AccessibleButton
|
||||||
|
className="mx_RoomKnocksBar_action"
|
||||||
|
disabled={!canKick || disabled}
|
||||||
|
kind="icon_primary_outline"
|
||||||
|
onClick={() => handleDeny(knockMembers[0].userId)}
|
||||||
|
title={_t("action|deny")}
|
||||||
|
>
|
||||||
|
<XIcon width={18} height={18} />
|
||||||
|
</AccessibleButton>
|
||||||
|
<AccessibleButton
|
||||||
|
className="mx_RoomKnocksBar_action"
|
||||||
|
disabled={!canInvite || disabled}
|
||||||
|
kind="icon_primary"
|
||||||
|
onClick={() => handleApprove(knockMembers[0].userId)}
|
||||||
|
title={_t("action|approve")}
|
||||||
|
>
|
||||||
|
<CheckIcon width={18} height={18} />
|
||||||
|
</AccessibleButton>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
names = `${knockMembers[0].name} (${knockMembers[0].userId})`;
|
||||||
|
link = (
|
||||||
|
<AccessibleButton
|
||||||
|
className="mx_RoomKnocksBar_link"
|
||||||
|
element="a"
|
||||||
|
kind="link_inline"
|
||||||
|
onClick={handleOpenRoomSettings}
|
||||||
|
>
|
||||||
|
{_t("action|view_message")}
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
names = _t("%(names)s and %(name)s", { names: knockMembers[0].name, name: knockMembers[1].name });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
names = _t("%(names)s and %(name)s", { names, name: knockMembers[2].name });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
names = _t("%(names)s and %(count)s others", { names, count: knockMembersCount - 2 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_RoomKnocksBar">
|
||||||
|
{knockMembers.slice(0, 2).map((knockMember) => (
|
||||||
|
<MemberAvatar
|
||||||
|
className="mx_RoomKnocksBar_avatar"
|
||||||
|
key={knockMember.userId}
|
||||||
|
member={knockMember}
|
||||||
|
size="32px"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<div className="mx_RoomKnocksBar_content">
|
||||||
|
<Heading size="4">{_t("%(count)s people asking to join", { count: knockMembersCount })}</Heading>
|
||||||
|
<p className="mx_RoomKnocksBar_paragraph">
|
||||||
|
{names}
|
||||||
|
{link}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{buttons}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -67,6 +67,8 @@
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"quote": "Quote",
|
"quote": "Quote",
|
||||||
"unpin": "Unpin",
|
"unpin": "Unpin",
|
||||||
|
"view": "View",
|
||||||
|
"view_message": "View message",
|
||||||
"start_chat": "Start chat",
|
"start_chat": "Start chat",
|
||||||
"invites_list": "Invites",
|
"invites_list": "Invites",
|
||||||
"reject": "Reject",
|
"reject": "Reject",
|
||||||
|
@ -87,7 +89,6 @@
|
||||||
"report_content": "Report Content",
|
"report_content": "Report Content",
|
||||||
"resend": "Resend",
|
"resend": "Resend",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"view": "View",
|
|
||||||
"ask_to_join": "Ask to join",
|
"ask_to_join": "Ask to join",
|
||||||
"forward": "Forward",
|
"forward": "Forward",
|
||||||
"copy_link": "Copy link",
|
"copy_link": "Copy link",
|
||||||
|
@ -1861,6 +1862,15 @@
|
||||||
"Public room": "Public room",
|
"Public room": "Public room",
|
||||||
"Private space": "Private space",
|
"Private space": "Private space",
|
||||||
"Private room": "Private room",
|
"Private room": "Private room",
|
||||||
|
"%(names)s and %(name)s": "%(names)s and %(name)s",
|
||||||
|
"%(names)s and %(count)s others": {
|
||||||
|
"other": "%(names)s and %(count)s others",
|
||||||
|
"one": "%(names)s and %(count)s other"
|
||||||
|
},
|
||||||
|
"%(count)s people asking to join": {
|
||||||
|
"other": "%(count)s people asking to join",
|
||||||
|
"one": "Asking to join"
|
||||||
|
},
|
||||||
"Start new chat": "Start new chat",
|
"Start new chat": "Start new chat",
|
||||||
"Invite to space": "Invite to space",
|
"Invite to space": "Invite to space",
|
||||||
"You do not have permissions to invite people to this space": "You do not have permissions to invite people to this space",
|
"You do not have permissions to invite people to this space": "You do not have permissions to invite people to this space",
|
||||||
|
|
|
@ -0,0 +1,299 @@
|
||||||
|
/*
|
||||||
|
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 } from "@testing-library/react";
|
||||||
|
import {
|
||||||
|
EventTimeline,
|
||||||
|
EventType,
|
||||||
|
JoinRule,
|
||||||
|
MatrixError,
|
||||||
|
MatrixEvent,
|
||||||
|
Room,
|
||||||
|
RoomMember,
|
||||||
|
RoomStateEvent,
|
||||||
|
} from "matrix-js-sdk/src/matrix";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import ErrorDialog from "../../../../src/components/views/dialogs/ErrorDialog";
|
||||||
|
import { RoomSettingsTab } from "../../../../src/components/views/dialogs/RoomSettingsDialog";
|
||||||
|
import { RoomKnocksBar } from "../../../../src/components/views/rooms/RoomKnocksBar";
|
||||||
|
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||||
|
import dis from "../../../../src/dispatcher/dispatcher";
|
||||||
|
import Modal from "../../../../src/Modal";
|
||||||
|
import {
|
||||||
|
clearAllModals,
|
||||||
|
flushPromises,
|
||||||
|
getMockClientWithEventEmitter,
|
||||||
|
mockClientMethodsUser,
|
||||||
|
} from "../../../test-utils";
|
||||||
|
|
||||||
|
describe("RoomKnocksBar", () => {
|
||||||
|
const userId = "@alice:example.org";
|
||||||
|
const client = getMockClientWithEventEmitter({
|
||||||
|
...mockClientMethodsUser(userId),
|
||||||
|
invite: jest.fn(),
|
||||||
|
kick: jest.fn(),
|
||||||
|
});
|
||||||
|
const roomId = "#ask-to-join:example.org";
|
||||||
|
const member = new RoomMember(roomId, userId);
|
||||||
|
const room = new Room(roomId, client, userId);
|
||||||
|
const state = room.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
|
||||||
|
|
||||||
|
type ButtonNames = "Approve" | "Deny" | "View" | "View message";
|
||||||
|
const getButton = (name: ButtonNames) => screen.getByRole("button", { name });
|
||||||
|
const getComponent = (room: Room) =>
|
||||||
|
render(
|
||||||
|
<MatrixClientContext.Provider value={client}>
|
||||||
|
<RoomKnocksBar room={room} />
|
||||||
|
</MatrixClientContext.Provider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(room, "getMember").mockReturnValue(member);
|
||||||
|
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Knock);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render if the room join rule is not knock", () => {
|
||||||
|
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Invite);
|
||||||
|
jest.spyOn(room, "getMembersWithMembership").mockReturnValue([member]);
|
||||||
|
jest.spyOn(room, "canInvite").mockReturnValue(true);
|
||||||
|
jest.spyOn(state, "hasSufficientPowerLevelFor").mockReturnValue(true);
|
||||||
|
expect(getComponent(room).container.firstChild).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("without requests to join", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(room, "getMembersWithMembership").mockReturnValue([]);
|
||||||
|
jest.spyOn(room, "canInvite").mockReturnValue(true);
|
||||||
|
jest.spyOn(state, "hasSufficientPowerLevelFor").mockReturnValue(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render if user can neither approve nor deny", () => {
|
||||||
|
jest.spyOn(room, "canInvite").mockReturnValue(false);
|
||||||
|
jest.spyOn(state, "hasSufficientPowerLevelFor").mockReturnValue(false);
|
||||||
|
expect(getComponent(room).container.firstChild).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render if user cannot approve", () => {
|
||||||
|
jest.spyOn(room, "canInvite").mockReturnValue(false);
|
||||||
|
expect(getComponent(room).container.firstChild).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render if user cannot deny", () => {
|
||||||
|
jest.spyOn(state, "hasSufficientPowerLevelFor").mockReturnValue(false);
|
||||||
|
expect(getComponent(room).container.firstChild).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render if user can approve and deny", () => {
|
||||||
|
expect(getComponent(room).container.firstChild).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("with requests to join", () => {
|
||||||
|
const error = new MatrixError();
|
||||||
|
const bob = new RoomMember(roomId, "@bob:example.org");
|
||||||
|
const jane = new RoomMember(roomId, "@jane:example.org");
|
||||||
|
const john = new RoomMember(roomId, "@john:example.org");
|
||||||
|
const other = new RoomMember(roomId, "@doe:example.org");
|
||||||
|
|
||||||
|
bob.setMembershipEvent(
|
||||||
|
new MatrixEvent({ content: { displayname: "Bob", membership: "knock" }, type: EventType.RoomMember }),
|
||||||
|
);
|
||||||
|
jane.setMembershipEvent(
|
||||||
|
new MatrixEvent({ content: { displayname: "Jane", membership: "knock" }, type: EventType.RoomMember }),
|
||||||
|
);
|
||||||
|
john.setMembershipEvent(
|
||||||
|
new MatrixEvent({ content: { displayname: "John", membership: "knock" }, type: EventType.RoomMember }),
|
||||||
|
);
|
||||||
|
other.setMembershipEvent(new MatrixEvent({ content: { membership: "knock" }, type: EventType.RoomMember }));
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await clearAllModals();
|
||||||
|
jest.spyOn(room, "getMembersWithMembership").mockReturnValue([bob]);
|
||||||
|
jest.spyOn(room, "canInvite").mockReturnValue(true);
|
||||||
|
jest.spyOn(state, "hasSufficientPowerLevelFor").mockReturnValue(true);
|
||||||
|
jest.spyOn(Modal, "createDialog");
|
||||||
|
jest.spyOn(dis, "dispatch");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render if user can neither approve nor deny", () => {
|
||||||
|
jest.spyOn(room, "canInvite").mockReturnValue(false);
|
||||||
|
jest.spyOn(state, "hasSufficientPowerLevelFor").mockReturnValue(false);
|
||||||
|
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 and a paragraph with name and user ID", () => {
|
||||||
|
getComponent(room);
|
||||||
|
expect(screen.getByRole("heading")).toHaveTextContent("Asking to join");
|
||||||
|
expect(screen.getByRole("paragraph")).toHaveTextContent(`${bob.name} (${bob.userId})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders a link to open the room settings people tab", () => {
|
||||||
|
getComponent(room);
|
||||||
|
fireEvent.click(getButton("View message"));
|
||||||
|
expect(dis.dispatch).toHaveBeenCalledWith({
|
||||||
|
action: "open_room_settings",
|
||||||
|
initial_tab_id: RoomSettingsTab.People,
|
||||||
|
room_id: roomId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
type TestCase = [string, ButtonNames, () => void];
|
||||||
|
it.each<TestCase>([
|
||||||
|
["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", async () => {
|
||||||
|
jest.spyOn(client, "kick").mockResolvedValue({});
|
||||||
|
getComponent(room);
|
||||||
|
fireEvent.click(getButton("Deny"));
|
||||||
|
await act(() => flushPromises());
|
||||||
|
expect(client.kick).toHaveBeenCalledWith(roomId, bob.userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays an error when a deny request fails", 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("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", async () => {
|
||||||
|
jest.spyOn(client, "invite").mockResolvedValue({});
|
||||||
|
getComponent(room);
|
||||||
|
fireEvent.click(getButton("Approve"));
|
||||||
|
await act(() => flushPromises());
|
||||||
|
expect(client.invite).toHaveBeenCalledWith(roomId, bob.userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays an error when an approval fails", 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("hides the bar when someone else approves or denies the waiting person", () => {
|
||||||
|
getComponent(room);
|
||||||
|
jest.spyOn(room, "getMembersWithMembership").mockReturnValue([]);
|
||||||
|
act(() => {
|
||||||
|
room.emit(RoomStateEvent.Members, new MatrixEvent(), state, bob);
|
||||||
|
});
|
||||||
|
expect(getComponent(room).container.firstChild).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when knock members count is greater than 1", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(room, "getMembersWithMembership").mockReturnValue([bob, jane]);
|
||||||
|
getComponent(room);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders a heading with count", () => {
|
||||||
|
expect(screen.getByRole("heading")).toHaveTextContent("2 people asking to join");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders a button to open the room settings people tab", () => {
|
||||||
|
fireEvent.click(getButton("View"));
|
||||||
|
expect(dis.dispatch).toHaveBeenCalledWith({
|
||||||
|
action: "open_room_settings",
|
||||||
|
initial_tab_id: RoomSettingsTab.People,
|
||||||
|
room_id: roomId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when knock members count is 2", () => {
|
||||||
|
it("renders a paragraph with two names", () => {
|
||||||
|
jest.spyOn(room, "getMembersWithMembership").mockReturnValue([bob, jane]);
|
||||||
|
getComponent(room);
|
||||||
|
expect(screen.getByRole("paragraph")).toHaveTextContent(`${bob.name} and ${jane.name}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when knock members count is 3", () => {
|
||||||
|
it("renders a paragraph with three names", () => {
|
||||||
|
jest.spyOn(room, "getMembersWithMembership").mockReturnValue([bob, jane, john]);
|
||||||
|
getComponent(room);
|
||||||
|
expect(screen.getByRole("paragraph")).toHaveTextContent(`${bob.name}, ${jane.name} and ${john.name}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when knock count is greater than 3", () => {
|
||||||
|
it("renders a paragraph with two names and a count", () => {
|
||||||
|
jest.spyOn(room, "getMembersWithMembership").mockReturnValue([bob, jane, john, other]);
|
||||||
|
getComponent(room);
|
||||||
|
expect(screen.getByRole("paragraph")).toHaveTextContent(`${bob.name}, ${jane.name} and 2 others`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue