Message Pinning: rework the message pinning list in the right panel (#12825)
* Fix pinning event loading after restart * Update deps * Replace pinned event list * Add a dialog to confirm to unpin all messages * Use `EmptyState` when there is no pinned messages * Rework `PinnedEventTile` tests * Add comments and refactor `PinnedMessageCard` * Rework `PinnedMessageCard` tests * Add tests for `UnpinAllDialog` * Add e2e tests for pinned messages * Replace 3px custom gap by 4px gap * Use string interpolation for `Pin` action. * Update playright sceenshot for empty state
This commit is contained in:
@@ -15,37 +15,44 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, act, RenderResult, fireEvent, waitForElementToBeRemoved, screen } from "@testing-library/react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { render, act, RenderResult, waitForElementToBeRemoved, screen } from "@testing-library/react";
|
||||
import { mocked, MockedObject } from "jest-mock";
|
||||
import {
|
||||
MatrixEvent,
|
||||
RoomStateEvent,
|
||||
IEvent,
|
||||
Room,
|
||||
EventTimelineSet,
|
||||
IMinimalEvent,
|
||||
EventType,
|
||||
RelationType,
|
||||
MsgType,
|
||||
M_POLL_KIND_DISCLOSED,
|
||||
EventTimeline,
|
||||
MatrixClient,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { PollStartEvent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent";
|
||||
import { PollResponseEvent } from "matrix-js-sdk/src/extensible_events_v1/PollResponseEvent";
|
||||
import { PollEndEvent } from "matrix-js-sdk/src/extensible_events_v1/PollEndEvent";
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { stubClient, mkEvent, mkMessage, flushPromises } from "../../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import PinnedMessagesCard from "../../../../src/components/views/right_panel/PinnedMessagesCard";
|
||||
import { PinnedMessagesCard } from "../../../../src/components/views/right_panel/PinnedMessagesCard";
|
||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||
import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks";
|
||||
import Modal from "../../../../src/Modal";
|
||||
import { UnpinAllDialog } from "../../../../src/components/views/dialogs/UnpinAllDialog";
|
||||
|
||||
describe("<PinnedMessagesCard />", () => {
|
||||
stubClient();
|
||||
const cli = mocked(MatrixClientPeg.safeGet());
|
||||
cli.getUserId.mockReturnValue("@alice:example.org");
|
||||
cli.setRoomAccountData.mockResolvedValue({});
|
||||
cli.relations.mockResolvedValue({ originalEvent: {} as unknown as MatrixEvent, events: [] });
|
||||
let cli: MockedObject<MatrixClient>;
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
cli = mocked(MatrixClientPeg.safeGet());
|
||||
cli.getUserId.mockReturnValue("@alice:example.org");
|
||||
cli.setRoomAccountData.mockResolvedValue({});
|
||||
cli.relations.mockResolvedValue({ originalEvent: {} as unknown as MatrixEvent, events: [] });
|
||||
});
|
||||
|
||||
const mkRoom = (localPins: MatrixEvent[], nonLocalPins: MatrixEvent[]): Room => {
|
||||
const room = new Room("!room:example.org", cli, "@me:example.org");
|
||||
@@ -53,27 +60,27 @@ describe("<PinnedMessagesCard />", () => {
|
||||
const pins = () => [...localPins, ...nonLocalPins];
|
||||
|
||||
// Insert pin IDs into room state
|
||||
jest.spyOn(room.currentState, "getStateEvents").mockImplementation((): any =>
|
||||
mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomPinnedEvents,
|
||||
content: {
|
||||
pinned: pins().map((e) => e.getId()),
|
||||
},
|
||||
user: "@user:example.org",
|
||||
room: "!room:example.org",
|
||||
}),
|
||||
jest.spyOn(room.getLiveTimeline().getState(EventTimeline.FORWARDS)!, "getStateEvents").mockImplementation(
|
||||
(): any =>
|
||||
mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomPinnedEvents,
|
||||
content: {
|
||||
pinned: pins().map((e) => e.getId()),
|
||||
},
|
||||
user: "@user:example.org",
|
||||
room: "!room:example.org",
|
||||
}),
|
||||
);
|
||||
|
||||
jest.spyOn(room.currentState, "on");
|
||||
|
||||
// Insert local pins into local timeline set
|
||||
room.getUnfilteredTimelineSet = () =>
|
||||
({
|
||||
getTimelineForEvent: () => ({
|
||||
getEvents: () => localPins,
|
||||
}),
|
||||
}) as unknown as EventTimelineSet;
|
||||
jest.spyOn(room.getLiveTimeline().getState(EventTimeline.FORWARDS)!, "mayClientSendStateEvent").mockReturnValue(
|
||||
true,
|
||||
);
|
||||
// poll end event validates against this
|
||||
jest.spyOn(
|
||||
room.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
|
||||
"maySendRedactionForEvent",
|
||||
).mockReturnValue(true);
|
||||
|
||||
// Return all pins over fetchRoomEvent
|
||||
cli.fetchRoomEvent.mockImplementation((roomId, eventId) => {
|
||||
@@ -86,8 +93,8 @@ describe("<PinnedMessagesCard />", () => {
|
||||
return room;
|
||||
};
|
||||
|
||||
const mountPins = async (room: Room): Promise<RenderResult> => {
|
||||
const pins = render(
|
||||
async function renderMessagePinList(room: Room): Promise<RenderResult> {
|
||||
const renderResult = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<PinnedMessagesCard
|
||||
room={room}
|
||||
@@ -99,22 +106,60 @@ describe("<PinnedMessagesCard />", () => {
|
||||
// Wait a tick for state updates
|
||||
await act(() => sleep(0));
|
||||
|
||||
return pins;
|
||||
};
|
||||
|
||||
const emitPinUpdates = async (room: Room) => {
|
||||
const pinListener = mocked(room.currentState).on.mock.calls.find(
|
||||
([eventName, listener]) => eventName === RoomStateEvent.Events,
|
||||
)![1];
|
||||
return renderResult;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param room
|
||||
*/
|
||||
async function emitPinUpdate(room: Room) {
|
||||
await act(async () => {
|
||||
// Emit the update
|
||||
// @ts-ignore what is going on here?
|
||||
pinListener(room.currentState.getStateEvents());
|
||||
// Wait a tick for state updates
|
||||
await sleep(0);
|
||||
const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
|
||||
roomState.emit(
|
||||
RoomStateEvent.Events,
|
||||
new MatrixEvent({ type: EventType.RoomPinnedEvents }),
|
||||
roomState,
|
||||
null,
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the pinned messages card with the given pinned messages.
|
||||
* Return the room, testing library helpers and functions to add and remove pinned messages.
|
||||
* @param localPins
|
||||
* @param nonLocalPins
|
||||
*/
|
||||
async function initPinnedMessagesCard(localPins: MatrixEvent[], nonLocalPins: MatrixEvent[]) {
|
||||
const room = mkRoom(localPins, nonLocalPins);
|
||||
const addLocalPinEvent = async (event: MatrixEvent) => {
|
||||
localPins.push(event);
|
||||
await emitPinUpdate(room);
|
||||
};
|
||||
const removeLastLocalPinEvent = async () => {
|
||||
localPins.pop();
|
||||
await emitPinUpdate(room);
|
||||
};
|
||||
const addNonLocalPinEvent = async (event: MatrixEvent) => {
|
||||
nonLocalPins.push(event);
|
||||
await emitPinUpdate(room);
|
||||
};
|
||||
const removeLastNonLocalPinEvent = async () => {
|
||||
nonLocalPins.pop();
|
||||
await emitPinUpdate(room);
|
||||
};
|
||||
const renderResult = await renderMessagePinList(room);
|
||||
|
||||
return {
|
||||
...renderResult,
|
||||
addLocalPinEvent,
|
||||
removeLastLocalPinEvent,
|
||||
addNonLocalPinEvent,
|
||||
removeLastNonLocalPinEvent,
|
||||
room,
|
||||
};
|
||||
}
|
||||
|
||||
const pin1 = mkMessage({
|
||||
event: true,
|
||||
@@ -129,75 +174,66 @@ describe("<PinnedMessagesCard />", () => {
|
||||
msg: "The second one",
|
||||
});
|
||||
|
||||
it("updates when messages are pinned", async () => {
|
||||
it("should show spinner whilst loading", async () => {
|
||||
const room = mkRoom([], [pin1]);
|
||||
render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<PinnedMessagesCard
|
||||
room={room}
|
||||
onClose={jest.fn()}
|
||||
permalinkCreator={new RoomPermalinkCreator(room, room.roomId)}
|
||||
/>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByRole("progressbar"));
|
||||
});
|
||||
|
||||
it("should show the empty state when there are no pins", async () => {
|
||||
const { asFragment } = await initPinnedMessagesCard([], []);
|
||||
|
||||
expect(screen.getByText("Pin important messages so that they can be easily discovered")).toBeInTheDocument();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should show two pinned messages", async () => {
|
||||
//const room = mkRoom([pin1], [pin2]);
|
||||
const { asFragment } = await initPinnedMessagesCard([pin1], [pin2]);
|
||||
|
||||
expect(screen.queryAllByRole("listitem")).toHaveLength(2);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should updates when messages are pinned", async () => {
|
||||
// Start with nothing pinned
|
||||
const localPins: MatrixEvent[] = [];
|
||||
const nonLocalPins: MatrixEvent[] = [];
|
||||
const room = mkRoom(localPins, nonLocalPins);
|
||||
const pins = await mountPins(room);
|
||||
expect(pins.container.querySelectorAll(".mx_PinnedEventTile")).toHaveLength(0);
|
||||
const { addLocalPinEvent, addNonLocalPinEvent } = await initPinnedMessagesCard([], []);
|
||||
|
||||
expect(screen.queryAllByRole("listitem")).toHaveLength(0);
|
||||
|
||||
// Pin the first message
|
||||
localPins.push(pin1);
|
||||
await emitPinUpdates(room);
|
||||
expect(pins.container.querySelectorAll(".mx_PinnedEventTile")).toHaveLength(1);
|
||||
await addLocalPinEvent(pin1);
|
||||
expect(screen.getAllByRole("listitem")).toHaveLength(1);
|
||||
|
||||
// Pin the second message
|
||||
nonLocalPins.push(pin2);
|
||||
await emitPinUpdates(room);
|
||||
expect(pins.container.querySelectorAll(".mx_PinnedEventTile")).toHaveLength(2);
|
||||
await addNonLocalPinEvent(pin2);
|
||||
expect(screen.getAllByRole("listitem")).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("updates when messages are unpinned", async () => {
|
||||
it("should updates when messages are unpinned", async () => {
|
||||
// Start with two pins
|
||||
const localPins = [pin1];
|
||||
const nonLocalPins = [pin2];
|
||||
const room = mkRoom(localPins, nonLocalPins);
|
||||
const pins = await mountPins(room);
|
||||
expect(pins.container.querySelectorAll(".mx_PinnedEventTile")).toHaveLength(2);
|
||||
const { removeLastLocalPinEvent, removeLastNonLocalPinEvent } = await initPinnedMessagesCard([pin1], [pin2]);
|
||||
expect(screen.getAllByRole("listitem")).toHaveLength(2);
|
||||
|
||||
// Unpin the first message
|
||||
localPins.pop();
|
||||
await emitPinUpdates(room);
|
||||
expect(pins.container.querySelectorAll(".mx_PinnedEventTile")).toHaveLength(1);
|
||||
await removeLastLocalPinEvent();
|
||||
expect(screen.getAllByRole("listitem")).toHaveLength(1);
|
||||
|
||||
// Unpin the second message
|
||||
nonLocalPins.pop();
|
||||
await emitPinUpdates(room);
|
||||
expect(pins.container.querySelectorAll(".mx_PinnedEventTile")).toHaveLength(0);
|
||||
await removeLastNonLocalPinEvent();
|
||||
expect(screen.queryAllByRole("listitem")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("hides unpinnable events found in local timeline", async () => {
|
||||
// Redacted messages are unpinnable
|
||||
const pin = mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
content: {},
|
||||
unsigned: { redacted_because: {} as unknown as IEvent },
|
||||
room: "!room:example.org",
|
||||
user: "@alice:example.org",
|
||||
});
|
||||
|
||||
const pins = await mountPins(mkRoom([pin], []));
|
||||
expect(pins.container.querySelectorAll(".mx_PinnedEventTile")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("hides unpinnable events not found in local timeline", async () => {
|
||||
// Redacted messages are unpinnable
|
||||
const pin = mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
content: {},
|
||||
unsigned: { redacted_because: {} as unknown as IEvent },
|
||||
room: "!room:example.org",
|
||||
user: "@alice:example.org",
|
||||
});
|
||||
|
||||
const pins = await mountPins(mkRoom([], [pin]));
|
||||
expect(pins.container.querySelectorAll(".mx_PinnedEventTile")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("accounts for edits", async () => {
|
||||
it("should display an edited pinned event", async () => {
|
||||
const messageEvent = mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
@@ -221,13 +257,78 @@ describe("<PinnedMessagesCard />", () => {
|
||||
events: [messageEvent],
|
||||
});
|
||||
|
||||
const pins = await mountPins(mkRoom([], [pin1]));
|
||||
const pinTile = pins.container.querySelectorAll(".mx_PinnedEventTile");
|
||||
expect(pinTile.length).toBe(1);
|
||||
expect(pinTile[0].querySelector(".mx_EventTile_body")!).toHaveTextContent("First pinned message, edited");
|
||||
await initPinnedMessagesCard([], [pin1]);
|
||||
expect(screen.getByText("First pinned message, edited")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays votes on polls not found in local timeline", async () => {
|
||||
describe("unpinnable event", () => {
|
||||
it("should hide unpinnable events found in local timeline", async () => {
|
||||
// Redacted messages are unpinnable
|
||||
const pin = mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
content: {},
|
||||
unsigned: { redacted_because: {} as unknown as IEvent },
|
||||
room: "!room:example.org",
|
||||
user: "@alice:example.org",
|
||||
});
|
||||
await initPinnedMessagesCard([pin], []);
|
||||
expect(screen.queryAllByRole("listitem")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("hides unpinnable events not found in local timeline", async () => {
|
||||
// Redacted messages are unpinnable
|
||||
const pin = mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
content: {},
|
||||
unsigned: { redacted_because: {} as unknown as IEvent },
|
||||
room: "!room:example.org",
|
||||
user: "@alice:example.org",
|
||||
});
|
||||
await initPinnedMessagesCard([], [pin]);
|
||||
expect(screen.queryAllByRole("listitem")).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("unpin all", () => {
|
||||
it("should not allow to unpinall", async () => {
|
||||
const room = mkRoom([pin1], [pin2]);
|
||||
jest.spyOn(
|
||||
room.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
|
||||
"mayClientSendStateEvent",
|
||||
).mockReturnValue(false);
|
||||
|
||||
const { asFragment } = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<PinnedMessagesCard
|
||||
room={room}
|
||||
onClose={jest.fn()}
|
||||
permalinkCreator={new RoomPermalinkCreator(room, room.roomId)}
|
||||
/>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
// Wait a tick for state updates
|
||||
await act(() => sleep(0));
|
||||
|
||||
expect(screen.queryByText("Unpin all messages")).toBeNull();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should allow unpinning all messages", async () => {
|
||||
jest.spyOn(Modal, "createDialog");
|
||||
|
||||
const { room } = await initPinnedMessagesCard([pin1], [pin2]);
|
||||
expect(screen.getByText("Unpin all messages")).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByText("Unpin all messages"));
|
||||
// Should open the UnpinAllDialog dialog
|
||||
expect(Modal.createDialog).toHaveBeenCalledWith(UnpinAllDialog, { roomId: room.roomId, matrixClient: cli });
|
||||
});
|
||||
});
|
||||
|
||||
it("should displays votes on polls not found in local timeline", async () => {
|
||||
const poll = mkEvent({
|
||||
...PollStartEvent.from("A poll", ["Option 1", "Option 2"], M_POLL_KIND_DISCLOSED).serialize(),
|
||||
event: true,
|
||||
@@ -270,11 +371,8 @@ describe("<PinnedMessagesCard />", () => {
|
||||
return { originalEvent: undefined as unknown as MatrixEvent, events: [] };
|
||||
});
|
||||
|
||||
const room = mkRoom([], [poll]);
|
||||
// poll end event validates against this
|
||||
jest.spyOn(room.currentState, "maySendRedactionForEvent").mockReturnValue(true);
|
||||
const { room } = await initPinnedMessagesCard([], [poll]);
|
||||
|
||||
const pins = await mountPins(room);
|
||||
// two pages of results
|
||||
await flushPromises();
|
||||
await flushPromises();
|
||||
@@ -282,34 +380,12 @@ describe("<PinnedMessagesCard />", () => {
|
||||
const pollInstance = room.polls.get(poll.getId()!);
|
||||
expect(pollInstance).toBeTruthy();
|
||||
|
||||
const pinTile = pins.container.querySelectorAll(".mx_MPollBody");
|
||||
expect(screen.getByText("A poll")).toBeInTheDocument();
|
||||
|
||||
expect(pinTile).toHaveLength(1);
|
||||
expect(pinTile[0].querySelectorAll(".mx_PollOption_ended")).toHaveLength(2);
|
||||
expect(pinTile[0].querySelectorAll(".mx_PollOption_optionVoteCount")[0]).toHaveTextContent("2 votes");
|
||||
expect([...pinTile[0].querySelectorAll(".mx_PollOption_optionVoteCount")].at(-1)).toHaveTextContent("1 vote");
|
||||
});
|
||||
expect(screen.getByText("Option 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("2 votes")).toBeInTheDocument();
|
||||
|
||||
it("should allow admins to unpin messages", async () => {
|
||||
const nonLocalPins = [pin1];
|
||||
const room = mkRoom([], nonLocalPins);
|
||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
||||
const sendStateEvent = jest.spyOn(cli, "sendStateEvent");
|
||||
|
||||
const pins = await mountPins(room);
|
||||
const pinTile = pins.container.querySelectorAll(".mx_PinnedEventTile");
|
||||
expect(pinTile).toHaveLength(1);
|
||||
|
||||
fireEvent.click(pinTile[0].querySelector(".mx_PinnedEventTile_unpinButton")!);
|
||||
expect(sendStateEvent).toHaveBeenCalledWith(room.roomId, "m.room.pinned_events", { pinned: [] }, "");
|
||||
|
||||
nonLocalPins.pop();
|
||||
await Promise.all([waitForElementToBeRemoved(pinTile[0]), emitPinUpdates(room)]);
|
||||
});
|
||||
|
||||
it("should show spinner whilst loading", async () => {
|
||||
const room = mkRoom([], [pin1]);
|
||||
mountPins(room);
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByRole("progressbar"));
|
||||
expect(screen.getByText("Option 2")).toBeInTheDocument();
|
||||
expect(screen.getByText("1 vote")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,457 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<PinnedMessagesCard /> should show the empty state when there are no pins 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_BaseCard mx_PinnedMessagesCard"
|
||||
>
|
||||
<div
|
||||
class="mx_BaseCard_header"
|
||||
>
|
||||
<div
|
||||
class="mx_BaseCard_header_title"
|
||||
>
|
||||
<h4
|
||||
class="mx_Heading_h4 mx_BaseCard_header_title_heading"
|
||||
>
|
||||
Pinned message
|
||||
</h4>
|
||||
</div>
|
||||
<button
|
||||
class="_icon-button_bh2qc_17 _subtle-bg_bh2qc_38"
|
||||
data-testid="base-card-close-button"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 28px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_133tf_26"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AutoHideScrollbar"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Flex mx_EmptyState"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-4x);"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="32px"
|
||||
viewBox="0 0 24 24"
|
||||
width="32px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M6.119 2a.5.5 0 0 0-.35.857L7.85 4.9a.5.5 0 0 1 .15.357v4.487a.5.5 0 0 1-.15.356l-3.7 3.644A.5.5 0 0 0 4 14.1v1.4a.5.5 0 0 0 .5.5H11v6a1 1 0 1 0 2 0v-6h6.5a.5.5 0 0 0 .5-.5v-1.4a.5.5 0 0 0-.15-.356l-3.7-3.644a.5.5 0 0 1-.15-.356V5.257a.5.5 0 0 1 .15-.357l2.081-2.043a.5.5 0 0 0-.35-.857H6.119ZM10 4h4v5.744a2.5 2.5 0 0 0 .746 1.781L17.26 14H6.74l2.514-2.475A2.5 2.5 0 0 0 10 9.744V4Z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<p
|
||||
class="_typography_yh5dq_162 _font-body-lg-semibold_yh5dq_83"
|
||||
>
|
||||
Pin important messages so that they can be easily discovered
|
||||
</p>
|
||||
<p
|
||||
class="_typography_yh5dq_162 _font-body-md-regular_yh5dq_59"
|
||||
>
|
||||
Select a message and choose “Pin” to it include here.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<PinnedMessagesCard /> should show two pinned messages 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_BaseCard mx_PinnedMessagesCard"
|
||||
>
|
||||
<div
|
||||
class="mx_BaseCard_header"
|
||||
>
|
||||
<div
|
||||
class="mx_BaseCard_header_title"
|
||||
>
|
||||
<h4
|
||||
class="mx_Heading_h4 mx_BaseCard_header_title_heading"
|
||||
>
|
||||
2 Pinned messages
|
||||
</h4>
|
||||
</div>
|
||||
<button
|
||||
class="_icon-button_bh2qc_17 _subtle-bg_bh2qc_38"
|
||||
data-testid="base-card-close-button"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 28px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_133tf_26"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AutoHideScrollbar"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_PinnedMessagesCard_wrapper mx_PinnedMessagesCard_wrapper_unpin_all"
|
||||
role="list"
|
||||
>
|
||||
<div
|
||||
class="mx_PinnedEventTile"
|
||||
role="listitem"
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar mx_PinnedEventTile_senderAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
a
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_PinnedEventTile_wrapper"
|
||||
>
|
||||
<div
|
||||
class="mx_PinnedEventTile_top"
|
||||
>
|
||||
<span
|
||||
class="_typography_yh5dq_162 _font-body-md-semibold_yh5dq_64 mx_PinnedEventTile_sender mx_Username_color3"
|
||||
>
|
||||
@alice:example.org
|
||||
</span>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="Open menu"
|
||||
class="_icon-button_bh2qc_17"
|
||||
data-state="closed"
|
||||
id="radix-2"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 24px;"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_133tf_26"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<div />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="mx_MTextBody mx_EventTile_content"
|
||||
>
|
||||
<div
|
||||
class="mx_EventTile_body translate"
|
||||
dir="auto"
|
||||
>
|
||||
The second one
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_separator_144s5_17 mx_PinnedMessagesCard_Separator"
|
||||
data-kind="primary"
|
||||
data-orientation="horizontal"
|
||||
role="separator"
|
||||
/>
|
||||
<div
|
||||
class="mx_PinnedEventTile"
|
||||
role="listitem"
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar mx_PinnedEventTile_senderAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
a
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_PinnedEventTile_wrapper"
|
||||
>
|
||||
<div
|
||||
class="mx_PinnedEventTile_top"
|
||||
>
|
||||
<span
|
||||
class="_typography_yh5dq_162 _font-body-md-semibold_yh5dq_64 mx_PinnedEventTile_sender mx_Username_color3"
|
||||
>
|
||||
@alice:example.org
|
||||
</span>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="Open menu"
|
||||
class="_icon-button_bh2qc_17"
|
||||
data-state="closed"
|
||||
id="radix-3"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 24px;"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_133tf_26"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<div />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="mx_MTextBody mx_EventTile_content"
|
||||
>
|
||||
<div
|
||||
class="mx_EventTile_body translate"
|
||||
dir="auto"
|
||||
>
|
||||
First pinned message
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_PinnedMessagesCard_unpin"
|
||||
>
|
||||
<button
|
||||
class="_button_zt6rp_17"
|
||||
data-kind="tertiary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Unpin all messages
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<PinnedMessagesCard /> unpin all should not allow to unpinall 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_BaseCard mx_PinnedMessagesCard"
|
||||
>
|
||||
<div
|
||||
class="mx_BaseCard_header"
|
||||
>
|
||||
<div
|
||||
class="mx_BaseCard_header_title"
|
||||
>
|
||||
<h4
|
||||
class="mx_Heading_h4 mx_BaseCard_header_title_heading"
|
||||
>
|
||||
2 Pinned messages
|
||||
</h4>
|
||||
</div>
|
||||
<button
|
||||
class="_icon-button_bh2qc_17 _subtle-bg_bh2qc_38"
|
||||
data-testid="base-card-close-button"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 28px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_133tf_26"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AutoHideScrollbar"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_PinnedMessagesCard_wrapper"
|
||||
role="list"
|
||||
>
|
||||
<div
|
||||
class="mx_PinnedEventTile"
|
||||
role="listitem"
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar mx_PinnedEventTile_senderAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
a
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_PinnedEventTile_wrapper"
|
||||
>
|
||||
<div
|
||||
class="mx_PinnedEventTile_top"
|
||||
>
|
||||
<span
|
||||
class="_typography_yh5dq_162 _font-body-md-semibold_yh5dq_64 mx_PinnedEventTile_sender mx_Username_color3"
|
||||
>
|
||||
@alice:example.org
|
||||
</span>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="Open menu"
|
||||
class="_icon-button_bh2qc_17"
|
||||
data-state="closed"
|
||||
id="radix-18"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 24px;"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_133tf_26"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<div />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="mx_MTextBody mx_EventTile_content"
|
||||
>
|
||||
<div
|
||||
class="mx_EventTile_body translate"
|
||||
dir="auto"
|
||||
>
|
||||
The second one
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_separator_144s5_17 mx_PinnedMessagesCard_Separator"
|
||||
data-kind="primary"
|
||||
data-orientation="horizontal"
|
||||
role="separator"
|
||||
/>
|
||||
<div
|
||||
class="mx_PinnedEventTile"
|
||||
role="listitem"
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar mx_PinnedEventTile_senderAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 32px;"
|
||||
>
|
||||
a
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_PinnedEventTile_wrapper"
|
||||
>
|
||||
<div
|
||||
class="mx_PinnedEventTile_top"
|
||||
>
|
||||
<span
|
||||
class="_typography_yh5dq_162 _font-body-md-semibold_yh5dq_64 mx_PinnedEventTile_sender mx_Username_color3"
|
||||
>
|
||||
@alice:example.org
|
||||
</span>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="Open menu"
|
||||
class="_icon-button_bh2qc_17"
|
||||
data-state="closed"
|
||||
id="radix-19"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 24px;"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_133tf_26"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<div />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="mx_MTextBody mx_EventTile_content"
|
||||
>
|
||||
<div
|
||||
class="mx_EventTile_body translate"
|
||||
dir="auto"
|
||||
>
|
||||
First pinned message
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
Reference in New Issue
Block a user