MVVM userinfo basic component (#30305)
* feat: mvvm userinfo basic component * test: mvvm userinfobasic component * chore: apply review. rename views, add comment and move some codes * chore(review): move openDM method into viewmodel
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
EventType,
|
||||
KnownMembership,
|
||||
type MatrixClient,
|
||||
MatrixEvent,
|
||||
type Room,
|
||||
RoomMember,
|
||||
type User,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { renderHook, waitFor } from "jest-matrix-react";
|
||||
|
||||
import { Action } from "../../../../../../src/dispatcher/actions";
|
||||
import Modal from "../../../../../../src/Modal";
|
||||
import MultiInviter from "../../../../../../src/utils/MultiInviter";
|
||||
import { createTestClient, mkRoom, withClientContextRenderOptions } from "../../../../../test-utils";
|
||||
import dis from "../../../../../../src/dispatcher/dispatcher";
|
||||
import { useUserInfoBasicOptionsViewModel } from "../../../../../../src/components/viewmodels/right_panel/user_info/UserInfoBasicOptionsViewModel";
|
||||
import DMRoomMap from "../../../../../../src/utils/DMRoomMap";
|
||||
import ErrorDialog from "../../../../../../src/components/views/dialogs/ErrorDialog";
|
||||
|
||||
jest.mock("../../../../../../src/dispatcher/dispatcher");
|
||||
|
||||
describe("<UserOptionsSection />", () => {
|
||||
const defaultRoomId = "!fkfk";
|
||||
const defaultUserId = "@user:example.com";
|
||||
const meUserId = "@me:example.com";
|
||||
|
||||
const defaultMember = new RoomMember(defaultRoomId, defaultUserId);
|
||||
let defaultProps: { room: Room; member: User | RoomMember };
|
||||
let mockClient: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = createTestClient();
|
||||
room = mkRoom(mockClient, defaultRoomId);
|
||||
defaultProps = {
|
||||
member: defaultMember,
|
||||
room,
|
||||
};
|
||||
DMRoomMap.makeShared(mockClient);
|
||||
});
|
||||
|
||||
const renderUserInfoBasicOptionsViewModelHook = (
|
||||
props: {
|
||||
member: User | RoomMember;
|
||||
room: Room;
|
||||
} = defaultProps,
|
||||
) => {
|
||||
return renderHook(
|
||||
() => useUserInfoBasicOptionsViewModel(props.room, props.member),
|
||||
withClientContextRenderOptions(mockClient),
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// Mock the current user account id. Which is different to the defaultMember which is the selected one
|
||||
// When we want to mock the current user, needs to override this value
|
||||
jest.spyOn(mockClient, "getUserId").mockReturnValue(meUserId);
|
||||
jest.spyOn(mockClient, "getRoom").mockReturnValue(room);
|
||||
});
|
||||
|
||||
it("should showInviteButton if current user can invite and selected user membership is LEAVE", () => {
|
||||
// cant use mkRoomMember because instanceof check will failed in this case
|
||||
const member: RoomMember = new RoomMember(defaultMember.userId, defaultMember.roomId);
|
||||
const me: RoomMember = new RoomMember(meUserId, defaultMember.roomId);
|
||||
|
||||
console.log("member instanceof RoomMember", member instanceof RoomMember);
|
||||
|
||||
member.powerLevel = 1;
|
||||
member.membership = KnownMembership.Leave;
|
||||
me.powerLevel = 50;
|
||||
me.membership = KnownMembership.Join;
|
||||
const powerLevelEvents = new MatrixEvent({
|
||||
type: EventType.RoomPowerLevels,
|
||||
content: {
|
||||
invite: 50,
|
||||
state_default: 0,
|
||||
},
|
||||
});
|
||||
jest.spyOn(room.currentState, "getStateEvents").mockReturnValue(powerLevelEvents);
|
||||
// used to get the current me user
|
||||
jest.spyOn(room, "getMember").mockReturnValue(me);
|
||||
const { result } = renderUserInfoBasicOptionsViewModelHook({ ...defaultProps, member });
|
||||
|
||||
expect(result.current.showInviteButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should not showInviteButton if current cannot invite", () => {
|
||||
const member: RoomMember = new RoomMember(defaultMember.userId, defaultMember.roomId);
|
||||
const me: RoomMember = new RoomMember(meUserId, defaultMember.roomId);
|
||||
member.powerLevel = 50;
|
||||
member.membership = KnownMembership.Leave;
|
||||
me.powerLevel = 0;
|
||||
me.membership = KnownMembership.Join;
|
||||
const powerLevelEvents = new MatrixEvent({
|
||||
type: EventType.RoomPowerLevels,
|
||||
content: {
|
||||
invite: 50,
|
||||
state_default: 0,
|
||||
},
|
||||
});
|
||||
jest.spyOn(room.currentState, "getStateEvents").mockReturnValue(powerLevelEvents);
|
||||
// used to get the current me user
|
||||
jest.spyOn(room, "getMember").mockReturnValue(me);
|
||||
const { result } = renderUserInfoBasicOptionsViewModelHook({ ...defaultProps, member });
|
||||
|
||||
expect(result.current.showInviteButton).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should not showInviteButton if selected user membership is not LEAVE", () => {
|
||||
const member: RoomMember = new RoomMember(defaultMember.userId, defaultMember.roomId);
|
||||
const me: RoomMember = new RoomMember(meUserId, defaultMember.roomId);
|
||||
member.powerLevel = 50;
|
||||
member.membership = KnownMembership.Join;
|
||||
me.powerLevel = 50;
|
||||
me.membership = KnownMembership.Join;
|
||||
const powerLevelEvents = new MatrixEvent({
|
||||
type: EventType.RoomPowerLevels,
|
||||
content: {
|
||||
invite: 50,
|
||||
state_default: 0,
|
||||
},
|
||||
});
|
||||
jest.spyOn(room.currentState, "getStateEvents").mockReturnValue(powerLevelEvents);
|
||||
jest.spyOn(room, "getMember").mockReturnValue(me);
|
||||
const { result } = renderUserInfoBasicOptionsViewModelHook({ ...defaultProps, member });
|
||||
|
||||
expect(result.current.showInviteButton).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should showInsertPillButton if room is not a space", () => {
|
||||
jest.spyOn(room, "isSpaceRoom").mockReturnValue(false);
|
||||
const { result } = renderUserInfoBasicOptionsViewModelHook();
|
||||
expect(result.current.showInsertPillButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should not showInsertPillButton if room is a space", () => {
|
||||
jest.spyOn(room, "isSpaceRoom").mockReturnValue(true);
|
||||
const { result } = renderUserInfoBasicOptionsViewModelHook();
|
||||
expect(result.current.showInsertPillButton).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should readReceiptButtonDisabled be true if all messages where read", () => {
|
||||
jest.spyOn(room, "getEventReadUpTo").mockReturnValue(null);
|
||||
const { result } = renderUserInfoBasicOptionsViewModelHook();
|
||||
expect(result.current.readReceiptButtonDisabled).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should readReceiptButtonDisabled be false if some messages are available", () => {
|
||||
jest.spyOn(room, "getEventReadUpTo").mockReturnValue("aneventId");
|
||||
const { result } = renderUserInfoBasicOptionsViewModelHook();
|
||||
expect(result.current.readReceiptButtonDisabled).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should readReceiptButtonDisabled be true if room is a space", () => {
|
||||
jest.spyOn(room, "getEventReadUpTo").mockReturnValue("aneventId");
|
||||
jest.spyOn(room, "isSpaceRoom").mockReturnValue(true);
|
||||
const { result } = renderUserInfoBasicOptionsViewModelHook();
|
||||
expect(result.current.readReceiptButtonDisabled).toBeTruthy();
|
||||
});
|
||||
|
||||
it("firing onReadReceiptButton calls dispatch with correct event_id", () => {
|
||||
const eventId = "aneventId";
|
||||
jest.spyOn(room, "getEventReadUpTo").mockReturnValue(eventId);
|
||||
jest.spyOn(room, "isSpaceRoom").mockReturnValue(false);
|
||||
const { result } = renderUserInfoBasicOptionsViewModelHook();
|
||||
|
||||
result.current.onReadReceiptButton();
|
||||
|
||||
expect(dis.dispatch).toHaveBeenCalledWith({
|
||||
action: "view_room",
|
||||
event_id: eventId,
|
||||
highlighted: true,
|
||||
metricsTrigger: undefined,
|
||||
room_id: defaultRoomId,
|
||||
});
|
||||
});
|
||||
|
||||
it("calling onInsertPillButton should calls dispatch", () => {
|
||||
const { result } = renderUserInfoBasicOptionsViewModelHook();
|
||||
|
||||
result.current.onInsertPillButton();
|
||||
|
||||
expect(dis.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ComposerInsert,
|
||||
userId: defaultMember.userId,
|
||||
timelineRenderingType: "Room",
|
||||
});
|
||||
});
|
||||
|
||||
it("calling onInviteUserButton will call MultiInviter.invite", async () => {
|
||||
// to save mocking, we will reject the call to .invite
|
||||
const mockErrorMessage = new Error("test error message");
|
||||
const spy = jest.spyOn(MultiInviter.prototype, "invite");
|
||||
spy.mockRejectedValue(mockErrorMessage);
|
||||
jest.spyOn(Modal, "createDialog");
|
||||
|
||||
const { result } = renderUserInfoBasicOptionsViewModelHook();
|
||||
result.current.onInviteUserButton(new Event("click"));
|
||||
|
||||
// check that we have called .invite
|
||||
expect(spy).toHaveBeenCalledWith([defaultMember.userId]);
|
||||
|
||||
await waitFor(() => {
|
||||
// check that the test error message is displayed
|
||||
expect(Modal.createDialog).toHaveBeenCalledWith(ErrorDialog, {
|
||||
description: "test error message",
|
||||
title: "Failed to invite",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
import React from "react";
|
||||
import { EventType, type MatrixClient, MatrixEvent, type Room, RoomMember, type User } from "matrix-js-sdk/src/matrix";
|
||||
import { renderHook, waitFor } from "jest-matrix-react";
|
||||
|
||||
import { createTestClient, mkRoom, withClientContextRenderOptions } from "../../../../../test-utils";
|
||||
import { useUserInfoBasicViewModel } from "../../../../../../src/components/viewmodels/right_panel/user_info/UserInfoBasicViewModel";
|
||||
import DMRoomMap from "../../../../../../src/utils/DMRoomMap";
|
||||
import Modal from "../../../../../../src/Modal";
|
||||
import QuestionDialog from "../../../../../../src/components/views/dialogs/QuestionDialog";
|
||||
|
||||
jest.mock("../../../../../../src/customisations/UserIdentifier", () => {
|
||||
return {
|
||||
getDisplayUserIdentifier: jest.fn().mockReturnValue("customUserIdentifier"),
|
||||
};
|
||||
});
|
||||
|
||||
describe("useUserInfoHeaderViewModel", () => {
|
||||
const defaultRoomId = "!fkfk";
|
||||
const defaultUserId = "@user:example.com";
|
||||
|
||||
const defaultMember = new RoomMember(defaultRoomId, defaultUserId);
|
||||
let mockClient: MatrixClient;
|
||||
|
||||
let defaultProps: {
|
||||
member: User | RoomMember;
|
||||
room: Room;
|
||||
};
|
||||
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = createTestClient();
|
||||
mockClient.isSynapseAdministrator = jest.fn().mockResolvedValue(true);
|
||||
mockClient.deactivateSynapseUser = jest.fn().mockResolvedValue({
|
||||
id_server_unbind_result: "success",
|
||||
});
|
||||
|
||||
room = mkRoom(mockClient, defaultRoomId);
|
||||
defaultProps = {
|
||||
member: defaultMember,
|
||||
room,
|
||||
};
|
||||
DMRoomMap.makeShared(mockClient);
|
||||
jest.spyOn(mockClient, "getRoom").mockReturnValue(room);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const renderUserInfoBasicViewModelHook = (
|
||||
props: {
|
||||
member: User | RoomMember;
|
||||
room: Room;
|
||||
} = defaultProps,
|
||||
) => {
|
||||
return renderHook(
|
||||
() => useUserInfoBasicViewModel(props.room, props.member),
|
||||
withClientContextRenderOptions(mockClient),
|
||||
);
|
||||
};
|
||||
|
||||
it("should set showDeactivateButton value to true", async () => {
|
||||
jest.spyOn(mockClient, "getDomain").mockReturnValue("example.com");
|
||||
const { result } = renderUserInfoBasicViewModelHook();
|
||||
// checking the synpase admin is an async operation, that is why we wait for it
|
||||
await waitFor(() => {
|
||||
expect(result.current.showDeactivateButton).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("should set showDeactivateButton value to false because domain is not the same", async () => {
|
||||
jest.spyOn(mockClient, "getDomain").mockReturnValue("toto.com");
|
||||
const { result } = renderUserInfoBasicViewModelHook();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.showDeactivateButton).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("should give powerlevels values", () => {
|
||||
const powerLevelEvents = new MatrixEvent({
|
||||
type: EventType.RoomPowerLevels,
|
||||
content: {
|
||||
invite: 1,
|
||||
state_default: 1,
|
||||
},
|
||||
});
|
||||
jest.spyOn(room.currentState, "getStateEvents").mockReturnValue(powerLevelEvents);
|
||||
const { result } = renderUserInfoBasicViewModelHook();
|
||||
expect(result.current.powerLevels).toStrictEqual({
|
||||
invite: 1,
|
||||
state_default: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("should set isRoomDMForMember to true if found in dmroommap", () => {
|
||||
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue("id");
|
||||
const { result } = renderUserInfoBasicViewModelHook();
|
||||
expect(result.current.isRoomDMForMember).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should set isRoomDMForMember to false if not found in dmroommap", () => {
|
||||
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(undefined);
|
||||
const { result } = renderUserInfoBasicViewModelHook();
|
||||
expect(result.current.isRoomDMForMember).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should display modal and call deactivateSynapseUser when calling onSynapaseDeactivate", async () => {
|
||||
const powerLevelEvents = new MatrixEvent({
|
||||
type: EventType.RoomPowerLevels,
|
||||
content: {
|
||||
invite: 1,
|
||||
state_default: 1,
|
||||
},
|
||||
});
|
||||
jest.spyOn(room.currentState, "getStateEvents").mockReturnValue(powerLevelEvents);
|
||||
jest.spyOn(Modal, "createDialog").mockReturnValue({
|
||||
finished: Promise.resolve([true, true, false]),
|
||||
close: jest.fn(),
|
||||
});
|
||||
|
||||
const { result } = renderUserInfoBasicViewModelHook();
|
||||
|
||||
await waitFor(() => result.current.onSynapseDeactivate());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(Modal.createDialog).toHaveBeenLastCalledWith(QuestionDialog, {
|
||||
button: "Deactivate user",
|
||||
danger: true,
|
||||
description: (
|
||||
<div>
|
||||
Deactivating this user will log them out and prevent them from logging back in. Additionally,
|
||||
they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want
|
||||
to deactivate this user?
|
||||
</div>
|
||||
),
|
||||
title: "Deactivate user?",
|
||||
});
|
||||
});
|
||||
expect(mockClient.deactivateSynapseUser).toHaveBeenCalledWith(defaultMember.userId);
|
||||
});
|
||||
});
|
||||
@@ -28,24 +28,16 @@ import {
|
||||
type CryptoApi,
|
||||
} from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import UserInfo, {
|
||||
disambiguateDevices,
|
||||
getPowerLevels,
|
||||
UserOptionsSection,
|
||||
} from "../../../../../src/components/views/right_panel/UserInfo";
|
||||
import dis from "../../../../../src/dispatcher/dispatcher";
|
||||
import UserInfo, { disambiguateDevices } from "../../../../../src/components/views/right_panel/UserInfo";
|
||||
import { getPowerLevels } from "../../../../../src/components/viewmodels/right_panel/user_info/UserInfoBasicViewModel";
|
||||
import { RightPanelPhases } from "../../../../../src/stores/right-panel/RightPanelStorePhases";
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import MultiInviter from "../../../../../src/utils/MultiInviter";
|
||||
import Modal from "../../../../../src/Modal";
|
||||
import { DirectoryMember, startDmOnFirstMessage } from "../../../../../src/utils/direct-messages";
|
||||
import { clearAllModals, flushPromises } from "../../../../test-utils";
|
||||
import ErrorDialog from "../../../../../src/components/views/dialogs/ErrorDialog";
|
||||
import { shouldShowComponent } from "../../../../../src/customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../../../src/settings/UIFeature";
|
||||
import { Action } from "../../../../../src/dispatcher/actions";
|
||||
import { ShareDialog } from "../../../../../src/components/views/dialogs/ShareDialog";
|
||||
|
||||
jest.mock("../../../../../src/utils/direct-messages", () => ({
|
||||
...jest.requireActual("../../../../../src/utils/direct-messages"),
|
||||
@@ -449,216 +441,6 @@ describe("<UserInfo />", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("<UserOptionsSection />", () => {
|
||||
const member = new RoomMember(defaultRoomId, defaultUserId);
|
||||
const defaultProps = { member, canInvite: false, isSpace: false };
|
||||
|
||||
const renderComponent = (props = {}) => {
|
||||
const Wrapper = (wrapperProps = {}) => {
|
||||
return <MatrixClientContext.Provider value={mockClient} {...wrapperProps} />;
|
||||
};
|
||||
|
||||
return render(<UserOptionsSection {...defaultProps} {...props} />, {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
};
|
||||
|
||||
const inviteSpy = jest.spyOn(MultiInviter.prototype, "invite");
|
||||
|
||||
beforeEach(() => {
|
||||
inviteSpy.mockReset();
|
||||
mockClient.setIgnoredUsers.mockClear();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await clearAllModals();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
inviteSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("always shows share user button and clicking it should produce a ShareDialog", async () => {
|
||||
const spy = jest.spyOn(Modal, "createDialog");
|
||||
|
||||
renderComponent();
|
||||
await userEvent.click(screen.getByRole("button", { name: "Share profile" }));
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(ShareDialog, { target: defaultProps.member });
|
||||
});
|
||||
|
||||
it("does not show ignore or direct message buttons when member userId matches client userId", () => {
|
||||
mockClient.getSafeUserId.mockReturnValueOnce(member.userId);
|
||||
mockClient.getUserId.mockReturnValueOnce(member.userId);
|
||||
renderComponent();
|
||||
|
||||
expect(screen.queryByRole("button", { name: /ignore/i })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: /message/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows direct message and mention buttons when member userId does not match client userId", () => {
|
||||
// call to client.getUserId returns undefined, which will not match member.userId
|
||||
renderComponent();
|
||||
|
||||
expect(screen.getByRole("button", { name: "Send message" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Mention" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("mention button fires ComposerInsert Action", async () => {
|
||||
renderComponent();
|
||||
|
||||
const button = screen.getByRole("button", { name: "Mention" });
|
||||
await userEvent.click(button);
|
||||
expect(dis.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ComposerInsert,
|
||||
timelineRenderingType: "Room",
|
||||
userId: "@user:example.com",
|
||||
});
|
||||
});
|
||||
|
||||
it("when call to client.getRoom is null, shows disabled read receipt button", () => {
|
||||
mockClient.getRoom.mockReturnValueOnce(null);
|
||||
renderComponent();
|
||||
|
||||
expect(screen.queryByRole("button", { name: "Jump to read receipt" })).toBeDisabled();
|
||||
});
|
||||
|
||||
it("when call to client.getRoom is non-null and room.getEventReadUpTo is null, shows disabled read receipt button", () => {
|
||||
mockRoom.getEventReadUpTo.mockReturnValueOnce(null);
|
||||
mockClient.getRoom.mockReturnValueOnce(mockRoom);
|
||||
renderComponent();
|
||||
|
||||
expect(screen.queryByRole("button", { name: "Jump to read receipt" })).toBeDisabled();
|
||||
});
|
||||
|
||||
it("when calls to client.getRoom and room.getEventReadUpTo are non-null, shows read receipt button", () => {
|
||||
mockRoom.getEventReadUpTo.mockReturnValueOnce("1234");
|
||||
mockClient.getRoom.mockReturnValueOnce(mockRoom);
|
||||
renderComponent();
|
||||
|
||||
expect(screen.getByRole("button", { name: "Jump to read receipt" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("clicking the read receipt button calls dispatch with correct event_id", async () => {
|
||||
const mockEventId = "1234";
|
||||
mockRoom.getEventReadUpTo.mockReturnValue(mockEventId);
|
||||
mockClient.getRoom.mockReturnValue(mockRoom);
|
||||
renderComponent();
|
||||
|
||||
const readReceiptButton = screen.getByRole("button", { name: "Jump to read receipt" });
|
||||
|
||||
expect(readReceiptButton).toBeInTheDocument();
|
||||
await userEvent.click(readReceiptButton);
|
||||
expect(dis.dispatch).toHaveBeenCalledWith({
|
||||
action: "view_room",
|
||||
event_id: mockEventId,
|
||||
highlighted: true,
|
||||
metricsTrigger: undefined,
|
||||
room_id: "!fkfk",
|
||||
});
|
||||
|
||||
mockRoom.getEventReadUpTo.mockReset();
|
||||
mockClient.getRoom.mockReset();
|
||||
});
|
||||
|
||||
it("firing the read receipt event handler with a null event_id calls dispatch with undefined not null", async () => {
|
||||
const mockEventId = "1234";
|
||||
// the first call is the check to see if we should render the button, second call is
|
||||
// when the button is clicked
|
||||
mockRoom.getEventReadUpTo.mockReturnValueOnce(mockEventId).mockReturnValueOnce(null);
|
||||
mockClient.getRoom.mockReturnValue(mockRoom);
|
||||
renderComponent();
|
||||
|
||||
const readReceiptButton = screen.getByRole("button", { name: "Jump to read receipt" });
|
||||
|
||||
expect(readReceiptButton).toBeInTheDocument();
|
||||
await userEvent.click(readReceiptButton);
|
||||
expect(dis.dispatch).toHaveBeenCalledWith({
|
||||
action: "view_room",
|
||||
event_id: undefined,
|
||||
highlighted: true,
|
||||
metricsTrigger: undefined,
|
||||
room_id: "!fkfk",
|
||||
});
|
||||
|
||||
mockClient.getRoom.mockReset();
|
||||
});
|
||||
|
||||
it("does not show the invite button when canInvite is false", () => {
|
||||
renderComponent();
|
||||
expect(screen.queryByRole("button", { name: /invite/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows the invite button when canInvite is true", () => {
|
||||
renderComponent({ canInvite: true });
|
||||
expect(screen.getByRole("button", { name: /invite/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("clicking the invite button will call MultiInviter.invite", async () => {
|
||||
// to save mocking, we will reject the call to .invite
|
||||
const mockErrorMessage = new Error("test error message");
|
||||
inviteSpy.mockRejectedValue(mockErrorMessage);
|
||||
|
||||
// render the component and click the button
|
||||
renderComponent({ canInvite: true });
|
||||
const inviteButton = screen.getByRole("button", { name: /invite/i });
|
||||
expect(inviteButton).toBeInTheDocument();
|
||||
await userEvent.click(inviteButton);
|
||||
|
||||
// check that we have called .invite
|
||||
expect(inviteSpy).toHaveBeenCalledWith([member.userId]);
|
||||
|
||||
// check that the test error message is displayed
|
||||
await expect(screen.findByText(mockErrorMessage.message)).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("if calling .invite throws something strange, show default error message", async () => {
|
||||
inviteSpy.mockRejectedValue({ this: "could be anything" });
|
||||
|
||||
// render the component and click the button
|
||||
renderComponent({ canInvite: true });
|
||||
const inviteButton = screen.getByRole("button", { name: /invite/i });
|
||||
expect(inviteButton).toBeInTheDocument();
|
||||
await userEvent.click(inviteButton);
|
||||
|
||||
// check that the default test error message is displayed
|
||||
await expect(screen.findByText(/operation failed/i)).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["for a RoomMember", member, member.getMxcAvatarUrl()],
|
||||
["for a User", defaultUser, defaultUser.avatarUrl],
|
||||
])(
|
||||
"clicking »message« %s should start a DM",
|
||||
async (test: string, member: RoomMember | User, expectedAvatarUrl: string | undefined) => {
|
||||
const deferred = Promise.withResolvers<string>();
|
||||
mocked(startDmOnFirstMessage).mockReturnValue(deferred.promise);
|
||||
|
||||
renderComponent({ member });
|
||||
await userEvent.click(screen.getByRole("button", { name: "Send message" }));
|
||||
|
||||
// Checking the attribute, because the button is a DIV and toBeDisabled() does not work.
|
||||
expect(screen.getByRole("button", { name: "Send message" })).toBeDisabled();
|
||||
|
||||
expect(startDmOnFirstMessage).toHaveBeenCalledWith(mockClient, [
|
||||
new DirectoryMember({
|
||||
user_id: member.userId,
|
||||
display_name: member.rawDisplayName,
|
||||
avatar_url: expectedAvatarUrl,
|
||||
}),
|
||||
]);
|
||||
|
||||
await act(async () => {
|
||||
deferred.resolve("!dm:example.com");
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
// Checking the attribute, because the button is a DIV and toBeDisabled() does not work.
|
||||
expect(screen.getByRole("button", { name: "Send message" })).not.toBeDisabled();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("disambiguateDevices", () => {
|
||||
it("does not add ambiguous key to unique names", () => {
|
||||
const initialDevices = [
|
||||
|
||||
@@ -10,16 +10,16 @@ import { render, screen, fireEvent } from "jest-matrix-react";
|
||||
import { type Room, type RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { UserInfoAdminToolsContainer } from "../../../../../src/components/views/right_panel/user_info/UserInfoAdminToolsContainer";
|
||||
import { useUserInfoAdminToolsContainerViewModel } from "../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel";
|
||||
import { useRoomKickButtonViewModel } from "../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoKickButtonViewModel";
|
||||
import { useBanButtonViewModel } from "../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoBanButtonViewModel";
|
||||
import { useMuteButtonViewModel } from "../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoMuteButtonViewModel";
|
||||
import { useRedactMessagesButtonViewModel } from "../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoRedactButtonViewModel";
|
||||
import { stubClient } from "../../../../test-utils";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import { UserInfoAdminToolsContainer } from "../../../../../../src/components/views/right_panel/user_info/UserInfoAdminToolsContainer";
|
||||
import { useUserInfoAdminToolsContainerViewModel } from "../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel";
|
||||
import { useRoomKickButtonViewModel } from "../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoKickButtonViewModel";
|
||||
import { useBanButtonViewModel } from "../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoBanButtonViewModel";
|
||||
import { useMuteButtonViewModel } from "../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoMuteButtonViewModel";
|
||||
import { useRedactMessagesButtonViewModel } from "../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoRedactButtonViewModel";
|
||||
import { stubClient } from "../../../../../test-utils";
|
||||
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
|
||||
|
||||
jest.mock("../../../../../src/utils/DMRoomMap", () => {
|
||||
jest.mock("../../../../../../src/utils/DMRoomMap", () => {
|
||||
const mock = {
|
||||
getUserIdForRoomId: jest.fn(),
|
||||
getDMRoomsForUserId: jest.fn(),
|
||||
@@ -32,7 +32,7 @@ jest.mock("../../../../../src/utils/DMRoomMap", () => {
|
||||
});
|
||||
|
||||
jest.mock(
|
||||
"../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel",
|
||||
"../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoAdminToolsContainerViewModel",
|
||||
() => ({
|
||||
useUserInfoAdminToolsContainerViewModel: jest.fn().mockReturnValue({
|
||||
isCurrentUserInTheRoom: true,
|
||||
@@ -44,34 +44,43 @@ jest.mock(
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock("../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoKickButtonViewModel", () => ({
|
||||
useRoomKickButtonViewModel: jest.fn().mockReturnValue({
|
||||
canUserBeKicked: true,
|
||||
kickLabel: "Kick",
|
||||
onKickClick: jest.fn(),
|
||||
jest.mock(
|
||||
"../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoKickButtonViewModel",
|
||||
() => ({
|
||||
useRoomKickButtonViewModel: jest.fn().mockReturnValue({
|
||||
canUserBeKicked: true,
|
||||
kickLabel: "Kick",
|
||||
onKickClick: jest.fn(),
|
||||
}),
|
||||
}),
|
||||
}));
|
||||
);
|
||||
|
||||
jest.mock("../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoBanButtonViewModel", () => ({
|
||||
jest.mock("../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoBanButtonViewModel", () => ({
|
||||
useBanButtonViewModel: jest.fn().mockReturnValue({
|
||||
banLabel: "Ban",
|
||||
onBanOrUnbanClick: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock("../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoMuteButtonViewModel", () => ({
|
||||
useMuteButtonViewModel: jest.fn().mockReturnValue({
|
||||
isMemberInTheRoom: true,
|
||||
muteLabel: "Mute",
|
||||
onMuteButtonClick: jest.fn(),
|
||||
jest.mock(
|
||||
"../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoMuteButtonViewModel",
|
||||
() => ({
|
||||
useMuteButtonViewModel: jest.fn().mockReturnValue({
|
||||
isMemberInTheRoom: true,
|
||||
muteLabel: "Mute",
|
||||
onMuteButtonClick: jest.fn(),
|
||||
}),
|
||||
}),
|
||||
}));
|
||||
);
|
||||
|
||||
jest.mock("../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoRedactButtonViewModel", () => ({
|
||||
useRedactMessagesButtonViewModel: jest.fn().mockReturnValue({
|
||||
onRedactAllMessagesClick: jest.fn(),
|
||||
jest.mock(
|
||||
"../../../../../../src/components/viewmodels/right_panel/user_info/admin/UserInfoRedactButtonViewModel",
|
||||
() => ({
|
||||
useRedactMessagesButtonViewModel: jest.fn().mockReturnValue({
|
||||
onRedactAllMessagesClick: jest.fn(),
|
||||
}),
|
||||
}),
|
||||
}));
|
||||
);
|
||||
|
||||
const defaultRoomId = "!fkfk";
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { type MatrixClient, type Room, RoomMember, type User } from "matrix-js-sdk/src/matrix";
|
||||
import { logRoles, render, screen } from "jest-matrix-react";
|
||||
|
||||
import { createTestClient, mkStubRoom } from "../../../../../test-utils";
|
||||
import {
|
||||
type UserInfoBasicState,
|
||||
useUserInfoBasicViewModel,
|
||||
} from "../../../../../../src/components/viewmodels/right_panel/user_info/UserInfoBasicViewModel";
|
||||
import { UserInfoBasicView } from "../../../../../../src/components/views/right_panel/user_info/UserInfoBasicView";
|
||||
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
|
||||
|
||||
const defaultRoomPermissions = {
|
||||
canEdit: true,
|
||||
canInvite: true,
|
||||
modifyLevelMax: -1,
|
||||
};
|
||||
jest.mock("../../../../../../src/components/viewmodels/right_panel/user_info/UserInfoBasicViewModel", () => ({
|
||||
useUserInfoBasicViewModel: jest.fn(),
|
||||
useRoomPermissions: () => defaultRoomPermissions,
|
||||
}));
|
||||
|
||||
describe("<UserInfoBasic />", () => {
|
||||
const defaultValue: UserInfoBasicState = {
|
||||
powerLevels: {},
|
||||
roomPermissions: defaultRoomPermissions,
|
||||
pendingUpdateCount: 0,
|
||||
isMe: false,
|
||||
isRoomDMForMember: false,
|
||||
showDeactivateButton: true,
|
||||
onSynapseDeactivate: jest.fn(),
|
||||
startUpdating: jest.fn(),
|
||||
stopUpdating: jest.fn(),
|
||||
};
|
||||
|
||||
const defaultRoomId = "!fkfk";
|
||||
const defaultUserId = "@user:example.com";
|
||||
|
||||
const defaultMember = new RoomMember(defaultRoomId, defaultUserId);
|
||||
let defaultRoom: Room;
|
||||
|
||||
let defaultProps: { member: User | RoomMember; room: Room };
|
||||
let matrixClient: MatrixClient;
|
||||
|
||||
const renderComponent = (props = defaultProps) => {
|
||||
return render(
|
||||
<MatrixClientContext.Provider value={matrixClient}>
|
||||
<UserInfoBasicView {...props} />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
};
|
||||
beforeEach(() => {
|
||||
matrixClient = createTestClient();
|
||||
defaultRoom = mkStubRoom(defaultRoomId, defaultRoomId, matrixClient);
|
||||
defaultProps = {
|
||||
member: defaultMember,
|
||||
room: defaultRoom,
|
||||
};
|
||||
});
|
||||
|
||||
it("should display the defaut values", () => {
|
||||
mocked(useUserInfoBasicViewModel).mockReturnValue(defaultValue);
|
||||
const { container } = renderComponent();
|
||||
logRoles(container);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should not show ignore button if user is me", () => {
|
||||
const state: UserInfoBasicState = { ...defaultValue, isMe: true };
|
||||
mocked(useUserInfoBasicViewModel).mockReturnValue(state);
|
||||
renderComponent();
|
||||
|
||||
const ignoreButton = screen.queryByRole("button", { name: "Ignore" });
|
||||
expect(ignoreButton).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not show deactivate button", () => {
|
||||
const state: UserInfoBasicState = { ...defaultValue, showDeactivateButton: false };
|
||||
mocked(useUserInfoBasicViewModel).mockReturnValue(state);
|
||||
renderComponent();
|
||||
|
||||
const deactivateButton = screen.queryByRole("button", { name: "Deactivate user" });
|
||||
expect(deactivateButton).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not show powerlevels selector for dm", () => {
|
||||
const state: UserInfoBasicState = { ...defaultValue, isRoomDMForMember: true };
|
||||
mocked(useUserInfoBasicViewModel).mockReturnValue(state);
|
||||
const { container } = renderComponent();
|
||||
|
||||
logRoles(container);
|
||||
const powserlevel = screen.queryByRole("option", { name: "Default" });
|
||||
expect(powserlevel).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show spinner if pending update is > 0", () => {
|
||||
const state: UserInfoBasicState = { ...defaultValue, pendingUpdateCount: 2 };
|
||||
mocked(useUserInfoBasicViewModel).mockReturnValue(state);
|
||||
renderComponent();
|
||||
|
||||
const spinner = screen.getByTestId("spinner");
|
||||
expect(spinner).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { type Room, RoomMember, type User } from "matrix-js-sdk/src/matrix";
|
||||
import { fireEvent, render, screen } from "jest-matrix-react";
|
||||
|
||||
import { mkStubRoom, stubClient } from "../../../../../test-utils";
|
||||
import {
|
||||
useUserInfoBasicOptionsViewModel,
|
||||
type UserInfoBasicOptionsState,
|
||||
} from "../../../../../../src/components/viewmodels/right_panel/user_info/UserInfoBasicOptionsViewModel";
|
||||
import { UserInfoBasicOptionsView } from "../../../../../../src/components/views/right_panel/user_info/UserInfoBasicOptionsView";
|
||||
import { UIComponent } from "../../../../../../src/settings/UIFeature";
|
||||
import { shouldShowComponent } from "../../../../../../src/customisations/helpers/UIComponents";
|
||||
import { type Member } from "../../../../../../src/components/views/right_panel/UserInfo";
|
||||
|
||||
jest.mock("../../../../../../src/components/viewmodels/right_panel/user_info/UserInfoBasicOptionsViewModel", () => ({
|
||||
useUserInfoBasicOptionsViewModel: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../../../../src/customisations/helpers/UIComponents", () => {
|
||||
const original = jest.requireActual("../../../../../../src/customisations/helpers/UIComponents");
|
||||
return {
|
||||
shouldShowComponent: jest.fn().mockImplementation(original.shouldShowComponent),
|
||||
};
|
||||
});
|
||||
|
||||
describe("<UserOptionsSection />", () => {
|
||||
const defaultValue: UserInfoBasicOptionsState = {
|
||||
isMe: false,
|
||||
showInviteButton: false,
|
||||
showInsertPillButton: false,
|
||||
readReceiptButtonDisabled: false,
|
||||
onInsertPillButton: () => jest.fn(),
|
||||
onReadReceiptButton: () => jest.fn(),
|
||||
onShareUserClick: () => jest.fn(),
|
||||
onInviteUserButton: (evt: Event) => Promise.resolve(),
|
||||
onOpenDmForUser: (member: Member) => Promise.resolve(),
|
||||
};
|
||||
|
||||
const defaultRoomId = "!fkfk";
|
||||
const defaultUserId = "@user:example.com";
|
||||
|
||||
const defaultMember = new RoomMember(defaultRoomId, defaultUserId);
|
||||
let defaultRoom: Room;
|
||||
|
||||
let defaultProps: { member: User | RoomMember; room: Room };
|
||||
|
||||
beforeEach(() => {
|
||||
const matrixClient = stubClient();
|
||||
defaultRoom = mkStubRoom(defaultRoomId, defaultRoomId, matrixClient);
|
||||
defaultProps = {
|
||||
member: defaultMember,
|
||||
room: defaultRoom,
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should always display sharedButton when user is not me", () => {
|
||||
// User is not me by default
|
||||
mocked(useUserInfoBasicOptionsViewModel).mockReturnValue({ ...defaultValue });
|
||||
render(<UserInfoBasicOptionsView {...defaultProps} />);
|
||||
const sharedButton = screen.getByRole("button", { name: "Share profile" });
|
||||
expect(sharedButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should always display sharedButton when user is me", () => {
|
||||
const propsWithMe = { ...defaultProps };
|
||||
const onShareUserClick = jest.fn();
|
||||
const state = { ...defaultValue, isMe: true, onShareUserClick };
|
||||
|
||||
mocked(useUserInfoBasicOptionsViewModel).mockReturnValue(state);
|
||||
render(<UserInfoBasicOptionsView {...propsWithMe} />);
|
||||
|
||||
const sharedButton2 = screen.getByRole("button", { name: "Share profile" });
|
||||
expect(sharedButton2).toBeInTheDocument();
|
||||
|
||||
// clicking on the share profile button
|
||||
fireEvent.click(sharedButton2);
|
||||
|
||||
expect(onShareUserClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should show insert pill button when user is not me and showinsertpill is true", () => {
|
||||
const onInsertPillButton = jest.fn();
|
||||
const state = { ...defaultValue, showInsertPillButton: true, onInsertPillButton };
|
||||
// User is not me and showInsertpill is true
|
||||
mocked(useUserInfoBasicOptionsViewModel).mockReturnValue(state);
|
||||
render(<UserInfoBasicOptionsView {...defaultProps} />);
|
||||
|
||||
const insertPillButton = screen.getByRole("button", { name: "Mention" });
|
||||
expect(insertPillButton).toBeInTheDocument();
|
||||
|
||||
// clicking on the insert pill button
|
||||
fireEvent.click(insertPillButton);
|
||||
|
||||
expect(onInsertPillButton).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not show insert pill button when user is not me and showinsertpill is false", () => {
|
||||
mocked(useUserInfoBasicOptionsViewModel).mockReturnValue({ ...defaultValue, showInsertPillButton: false });
|
||||
render(<UserInfoBasicOptionsView {...defaultProps} />);
|
||||
const insertPillButton = screen.queryByRole("button", { name: "Mention" });
|
||||
expect(insertPillButton).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not show insert pill button when user is me", () => {
|
||||
// User is me, should not see the insert button even when show insertpill is true
|
||||
mocked(useUserInfoBasicOptionsViewModel).mockReturnValue({
|
||||
...defaultValue,
|
||||
showInsertPillButton: true,
|
||||
isMe: true,
|
||||
});
|
||||
const propsWithMe = { ...defaultProps };
|
||||
render(<UserInfoBasicOptionsView {...propsWithMe} />);
|
||||
const insertPillButton = screen.queryByRole("button", { name: "Mention" });
|
||||
expect(insertPillButton).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not show readreceiptbutton when user is me", () => {
|
||||
mocked(useUserInfoBasicOptionsViewModel).mockReturnValue({
|
||||
...defaultValue,
|
||||
readReceiptButtonDisabled: true,
|
||||
isMe: true,
|
||||
});
|
||||
const propsWithMe = { ...defaultProps };
|
||||
render(<UserInfoBasicOptionsView {...propsWithMe} />);
|
||||
|
||||
const readReceiptButton = screen.queryByRole("button", { name: "Jump to read receipt" });
|
||||
expect(readReceiptButton).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show disable readreceiptbutton when readReceiptButtonDisabled is true", () => {
|
||||
mocked(useUserInfoBasicOptionsViewModel).mockReturnValue({ ...defaultValue, readReceiptButtonDisabled: true });
|
||||
render(<UserInfoBasicOptionsView {...defaultProps} />);
|
||||
|
||||
const readReceiptButton = screen.getByRole("button", { name: "Jump to read receipt" });
|
||||
expect(readReceiptButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should not show disable readreceiptbutton when readReceiptButtonDisabled is false", () => {
|
||||
const onReadReceiptButton = jest.fn();
|
||||
const state = { ...defaultValue, readReceiptButtonDisabled: false, onReadReceiptButton };
|
||||
mocked(useUserInfoBasicOptionsViewModel).mockReturnValue(state);
|
||||
render(<UserInfoBasicOptionsView {...defaultProps} />);
|
||||
|
||||
const readReceiptButton = screen.getByRole("button", { name: "Jump to read receipt" });
|
||||
expect(readReceiptButton).not.toBeDisabled();
|
||||
|
||||
// clicking on the read receipt button
|
||||
fireEvent.click(readReceiptButton);
|
||||
|
||||
expect(onReadReceiptButton).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should show not show invite button if shouldShowComponent is false", () => {
|
||||
mocked(useUserInfoBasicOptionsViewModel).mockReturnValue({ ...defaultValue, showInviteButton: true });
|
||||
mocked(shouldShowComponent).mockReturnValue(false);
|
||||
render(<UserInfoBasicOptionsView {...defaultProps} />);
|
||||
|
||||
const inviteButton = screen.queryByRole("button", { name: "Invite" });
|
||||
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.InviteUsers);
|
||||
expect(inviteButton).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show show invite button if shouldShowComponent is true", () => {
|
||||
const onInviteUserButton = jest.fn();
|
||||
const state = { ...defaultValue, showInviteButton: true, onInviteUserButton };
|
||||
mocked(useUserInfoBasicOptionsViewModel).mockReturnValue(state);
|
||||
mocked(shouldShowComponent).mockReturnValue(true);
|
||||
render(<UserInfoBasicOptionsView {...defaultProps} />);
|
||||
|
||||
const inviteButton = screen.getByRole("button", { name: "Invite" });
|
||||
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.InviteUsers);
|
||||
expect(inviteButton).toBeInTheDocument();
|
||||
|
||||
// clicking on the invite button
|
||||
fireEvent.click(inviteButton);
|
||||
expect(onInviteUserButton).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should show directMessageButton when user is not me", () => {
|
||||
// User is not me, direct message button should display
|
||||
mocked(useUserInfoBasicOptionsViewModel).mockReturnValue(defaultValue);
|
||||
mocked(shouldShowComponent).mockReturnValue(true);
|
||||
render(<UserInfoBasicOptionsView {...defaultProps} />);
|
||||
const dmButton = screen.getByRole("button", { name: "Send message" });
|
||||
expect(dmButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not show directMessageButton when user is me", () => {
|
||||
mocked(useUserInfoBasicOptionsViewModel).mockReturnValue({ ...defaultValue, isMe: true });
|
||||
mocked(shouldShowComponent).mockReturnValue(true);
|
||||
const propsWithMe = { ...defaultProps };
|
||||
render(<UserInfoBasicOptionsView {...propsWithMe} />);
|
||||
const dmButton = screen.queryByRole("button", { name: "Send message" });
|
||||
expect(dmButton).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -12,10 +12,10 @@ import { Device, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { render, waitFor, screen } from "jest-matrix-react";
|
||||
import React from "react";
|
||||
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import { UserInfoHeaderVerificationView } from "../../../../../src/components/views/right_panel/user_info/UserInfoHeaderVerificationView";
|
||||
import { createTestClient } from "../../../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
|
||||
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
|
||||
import { UserInfoHeaderVerificationView } from "../../../../../../src/components/views/right_panel/user_info/UserInfoHeaderVerificationView";
|
||||
import { createTestClient } from "../../../../../test-utils";
|
||||
|
||||
describe("<UserInfoHeaderVerificationView />", () => {
|
||||
const defaultRoomId = "!fkfk";
|
||||
@@ -12,14 +12,14 @@ import { Device, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { fireEvent, render, screen } from "jest-matrix-react";
|
||||
import React from "react";
|
||||
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import { UserInfoHeaderView } from "../../../../../src/components/views/right_panel/user_info/UserInfoHeaderView";
|
||||
import { createTestClient } from "../../../../test-utils";
|
||||
import { useUserfoHeaderViewModel } from "../../../../../src/components/viewmodels/right_panel/user_info/UserInfoHeaderViewModel";
|
||||
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
|
||||
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
|
||||
import { UserInfoHeaderView } from "../../../../../../src/components/views/right_panel/user_info/UserInfoHeaderView";
|
||||
import { createTestClient } from "../../../../../test-utils";
|
||||
import { useUserfoHeaderViewModel } from "../../../../../../src/components/viewmodels/right_panel/user_info/UserInfoHeaderViewModel";
|
||||
|
||||
// Mock the viewmodel hooks
|
||||
jest.mock("../../../../../src/components/viewmodels/right_panel/user_info/UserInfoHeaderViewModel", () => ({
|
||||
jest.mock("../../../../../../src/components/viewmodels/right_panel/user_info/UserInfoHeaderViewModel", () => ({
|
||||
useUserfoHeaderViewModel: jest.fn().mockReturnValue({
|
||||
onMemberAvatarClick: jest.fn(),
|
||||
precenseInfo: {
|
||||
@@ -0,0 +1,315 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<UserInfoBasic /> should display the defaut values 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_UserInfo_container"
|
||||
>
|
||||
<div
|
||||
class="mx_UserInfo_profileField"
|
||||
>
|
||||
<div
|
||||
class="mx_PowerSelector"
|
||||
>
|
||||
<div
|
||||
class="mx_Field mx_Field_select"
|
||||
>
|
||||
<select
|
||||
data-testid="power-level-select-element"
|
||||
id="mx_Field_1"
|
||||
label="Power level"
|
||||
placeholder="Power level"
|
||||
type="text"
|
||||
>
|
||||
<option
|
||||
data-testid="power-level-option-0"
|
||||
value="0"
|
||||
>
|
||||
Default
|
||||
</option>
|
||||
<option
|
||||
data-testid="power-level-option-SELECT_VALUE_CUSTOM"
|
||||
value="SELECT_VALUE_CUSTOM"
|
||||
>
|
||||
Custom level
|
||||
</option>
|
||||
</select>
|
||||
<label
|
||||
for="mx_Field_1"
|
||||
>
|
||||
Power level
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="_item_dyt4i_8 _interactive_dyt4i_26"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_icon_dyt4i_50"
|
||||
fill="currentColor"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m1.5 21.25 1.45-4.95a10.2 10.2 0 0 1-.712-2.1A10.2 10.2 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22q-1.125 0-2.2-.238a10.2 10.2 0 0 1-2.1-.712L2.75 22.5a.94.94 0 0 1-1-.25.94.94 0 0 1-.25-1m2.45-1.2 3.2-.95a1 1 0 0 1 .275-.062q.15-.013.275-.013.225 0 .438.038.212.036.412.137a7.4 7.4 0 0 0 1.675.6Q11.1 20 12 20q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4 6.325 6.325 4 12q0 .9.2 1.775t.6 1.675q.176.325.188.688t-.088.712z"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_dyt4i_34"
|
||||
>
|
||||
Send message
|
||||
</span>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_nav-hint_dyt4i_59"
|
||||
fill="currentColor"
|
||||
height="24"
|
||||
viewBox="8 0 8 24"
|
||||
width="8"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8.7 17.3a.95.95 0 0 1-.275-.7q0-.425.275-.7l3.9-3.9-3.9-3.9a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l4.6 4.6q.15.15.213.325.062.175.062.375t-.062.375a.9.9 0 0 1-.213.325l-4.6 4.6a.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="_item_dyt4i_8 _interactive_dyt4i_26"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_icon_dyt4i_50"
|
||||
fill="currentColor"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10 12q-1.65 0-2.825-1.175T6 8t1.175-2.825T10 4t2.825 1.175T14 8t-1.175 2.825T10 12m-8 6v-.8q0-.85.438-1.562.437-.713 1.162-1.088a14.8 14.8 0 0 1 3.15-1.163A13.8 13.8 0 0 1 10 13q1.65 0 3.25.387 1.6.388 3.15 1.163.724.375 1.163 1.087Q18 16.35 18 17.2v.8q0 .824-.587 1.413A1.93 1.93 0 0 1 16 20H4q-.824 0-1.412-.587A1.93 1.93 0 0 1 2 18m2 0h12v-.8a.97.97 0 0 0-.5-.85q-1.35-.675-2.725-1.012a11.6 11.6 0 0 0-5.55 0Q5.85 15.675 4.5 16.35a.97.97 0 0 0-.5.85zm6-8q.825 0 1.412-.588Q12 8.826 12 8q0-.824-.588-1.412A1.93 1.93 0 0 0 10 6q-.825 0-1.412.588A1.93 1.93 0 0 0 8 8q0 .825.588 1.412Q9.175 10 10 10m7 1h2v2q0 .424.288.713.287.287.712.287.424 0 .712-.287A.97.97 0 0 0 21 13v-2h2q.424 0 .712-.287A.97.97 0 0 0 24 10a.97.97 0 0 0-.288-.713A.97.97 0 0 0 23 9h-2V7a.97.97 0 0 0-.288-.713A.97.97 0 0 0 20 6a.97.97 0 0 0-.712.287A.97.97 0 0 0 19 7v2h-2a.97.97 0 0 0-.712.287A.97.97 0 0 0 16 10q0 .424.288.713.287.287.712.287"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_dyt4i_34"
|
||||
>
|
||||
Invite
|
||||
</span>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_nav-hint_dyt4i_59"
|
||||
fill="currentColor"
|
||||
height="24"
|
||||
viewBox="8 0 8 24"
|
||||
width="8"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8.7 17.3a.95.95 0 0 1-.275-.7q0-.425.275-.7l3.9-3.9-3.9-3.9a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l4.6 4.6q.15.15.213.325.062.175.062.375t-.062.375a.9.9 0 0 1-.213.325l-4.6 4.6a.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="_item_dyt4i_8 _interactive_dyt4i_26 _disabled_dyt4i_118"
|
||||
data-kind="primary"
|
||||
disabled=""
|
||||
role="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_icon_dyt4i_50"
|
||||
fill="currentColor"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_dyt4i_34"
|
||||
>
|
||||
Jump to read receipt
|
||||
</span>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_nav-hint_dyt4i_59"
|
||||
fill="currentColor"
|
||||
height="24"
|
||||
viewBox="8 0 8 24"
|
||||
width="8"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8.7 17.3a.95.95 0 0 1-.275-.7q0-.425.275-.7l3.9-3.9-3.9-3.9a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l4.6 4.6q.15.15.213.325.062.175.062.375t-.062.375a.9.9 0 0 1-.213.325l-4.6 4.6a.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="_item_dyt4i_8 _interactive_dyt4i_26"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_icon_dyt4i_50"
|
||||
fill="currentColor"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 16a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 15V7.85L9.125 9.725q-.3.3-.7.3T7.7 9.7a.93.93 0 0 1-.288-.713A.98.98 0 0 1 7.7 8.3l3.6-3.6q.15-.15.325-.213.175-.062.375-.062t.375.062a.9.9 0 0 1 .325.213l3.6 3.6q.3.3.287.712a.98.98 0 0 1-.287.688q-.3.3-.713.313a.93.93 0 0 1-.712-.288L13 7.85V15q0 .424-.287.713A.97.97 0 0 1 12 16m-6 4q-.824 0-1.412-.587A1.93 1.93 0 0 1 4 18v-2q0-.424.287-.713A.97.97 0 0 1 5 15q.424 0 .713.287Q6 15.576 6 16v2h12v-2q0-.424.288-.713A.97.97 0 0 1 19 15q.424 0 .712.287.288.288.288.713v2q0 .824-.587 1.413A1.93 1.93 0 0 1 18 20z"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_dyt4i_34"
|
||||
>
|
||||
Share profile
|
||||
</span>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_nav-hint_dyt4i_59"
|
||||
fill="currentColor"
|
||||
height="24"
|
||||
viewBox="8 0 8 24"
|
||||
width="8"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8.7 17.3a.95.95 0 0 1-.275-.7q0-.425.275-.7l3.9-3.9-3.9-3.9a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l4.6 4.6q.15.15.213.325.062.175.062.375t-.062.375a.9.9 0 0 1-.213.325l-4.6 4.6a.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="_item_dyt4i_8 _interactive_dyt4i_26"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_icon_dyt4i_50"
|
||||
fill="currentColor"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 4a8 8 0 1 0 0 16 1 1 0 1 1 0 2C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10v1.5a3.5 3.5 0 0 1-6.396 1.966A5 5 0 1 1 17 12v1.5a1.5 1.5 0 0 0 3 0V12a8 8 0 0 0-8-8m3 8a3 3 0 1 0-6 0 3 3 0 0 0 6 0"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_dyt4i_34"
|
||||
>
|
||||
Mention
|
||||
</span>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_nav-hint_dyt4i_59"
|
||||
fill="currentColor"
|
||||
height="24"
|
||||
viewBox="8 0 8 24"
|
||||
width="8"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8.7 17.3a.95.95 0 0 1-.275-.7q0-.425.275-.7l3.9-3.9-3.9-3.9a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l4.6 4.6q.15.15.213.325.062.175.062.375t-.062.375a.9.9 0 0 1-.213.325l-4.6 4.6a.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="mx_UserInfo_container"
|
||||
>
|
||||
<button
|
||||
class="_item_dyt4i_8 _interactive_dyt4i_26"
|
||||
data-kind="critical"
|
||||
role="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_icon_dyt4i_50"
|
||||
fill="currentColor"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7 21q-.824 0-1.412-.587A1.93 1.93 0 0 1 5 19V6a.97.97 0 0 1-.713-.287A.97.97 0 0 1 4 5q0-.424.287-.713A.97.97 0 0 1 5 4h4q0-.424.287-.712A.97.97 0 0 1 10 3h4q.424 0 .713.288Q15 3.575 15 4h4q.424 0 .712.287Q20 4.576 20 5t-.288.713A.97.97 0 0 1 19 6v13q0 .824-.587 1.413A1.93 1.93 0 0 1 17 21zM7 6v13h10V6zm2 10q0 .424.287.712Q9.576 17 10 17t.713-.288A.97.97 0 0 0 11 16V9a.97.97 0 0 0-.287-.713A.97.97 0 0 0 10 8a.97.97 0 0 0-.713.287A.97.97 0 0 0 9 9zm4 0q0 .424.287.712.288.288.713.288.424 0 .713-.288A.97.97 0 0 0 15 16V9a.97.97 0 0 0-.287-.713A.97.97 0 0 0 14 8a.97.97 0 0 0-.713.287A.97.97 0 0 0 13 9z"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_dyt4i_34"
|
||||
>
|
||||
Deactivate user
|
||||
</span>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_nav-hint_dyt4i_59"
|
||||
fill="currentColor"
|
||||
height="24"
|
||||
viewBox="8 0 8 24"
|
||||
width="8"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8.7 17.3a.95.95 0 0 1-.275-.7q0-.425.275-.7l3.9-3.9-3.9-3.9a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l4.6 4.6q.15.15.213.325.062.175.062.375t-.062.375a.9.9 0 0 1-.213.325l-4.6 4.6a.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="mx_UserInfo_container"
|
||||
>
|
||||
<button
|
||||
class="_item_dyt4i_8 _interactive_dyt4i_26"
|
||||
data-kind="critical"
|
||||
role="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_icon_dyt4i_50"
|
||||
fill="currentColor"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 22a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22m0-2q3.35 0 5.675-2.325T20 12q0-1.35-.437-2.6A8 8 0 0 0 18.3 7.1L7.1 18.3q1.05.825 2.3 1.262T12 20m-6.3-3.1L16.9 5.7a8 8 0 0 0-2.3-1.263A7.8 7.8 0 0 0 12 4Q8.65 4 6.325 6.325T4 12q0 1.35.438 2.6A8 8 0 0 0 5.7 16.9"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_dyt4i_34"
|
||||
>
|
||||
Ignore
|
||||
</span>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_nav-hint_dyt4i_59"
|
||||
fill="currentColor"
|
||||
height="24"
|
||||
viewBox="8 0 8 24"
|
||||
width="8"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8.7 17.3a.95.95 0 0 1-.275-.7q0-.425.275-.7l3.9-3.9-3.9-3.9a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l4.6 4.6q.15.15.213.325.062.175.062.375t-.062.375a.9.9 0 0 1-.213.325l-4.6 4.6a.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
Reference in New Issue
Block a user