Add Pin/Unpin action in quick access of the message action bar (#12897)
* Add Pin/Unpin action in quick access of the message action bar * Add tests for `MessageActionBar` * Add tests for `PinningUtils` * Fix `MessageContextMenu-test` * Add e2e test to pin/unpin from message action bar
This commit is contained in:
parent
4064db1d02
commit
3d80eff65b
9 changed files with 503 additions and 105 deletions
|
@ -100,13 +100,35 @@ export class Helpers {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pin the given message
|
* Pin the given message from the quick actions
|
||||||
|
* @param message
|
||||||
|
* @param unpin
|
||||||
|
*/
|
||||||
|
async pinMessageFromQuickActions(message: string, unpin = false) {
|
||||||
|
const timelineMessage = this.page.locator(".mx_MTextBody", { hasText: message });
|
||||||
|
await timelineMessage.hover();
|
||||||
|
await this.page.getByRole("button", { name: unpin ? "Unpin" : "Pin", exact: true }).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pin the given messages from the quick actions
|
||||||
|
* @param messages
|
||||||
|
* @param unpin
|
||||||
|
*/
|
||||||
|
async pinMessagesFromQuickActions(messages: string[], unpin = false) {
|
||||||
|
for (const message of messages) {
|
||||||
|
await this.pinMessageFromQuickActions(message, unpin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pin the given message from the contextual menu
|
||||||
* @param message
|
* @param message
|
||||||
*/
|
*/
|
||||||
async pinMessage(message: string) {
|
async pinMessage(message: string) {
|
||||||
const timelineMessage = this.page.locator(".mx_MTextBody", { hasText: message });
|
const timelineMessage = this.page.locator(".mx_MTextBody", { hasText: message });
|
||||||
await timelineMessage.click({ button: "right" });
|
await timelineMessage.click({ button: "right" });
|
||||||
await this.page.getByRole("menuitem", { name: "Pin" }).click();
|
await this.page.getByRole("menuitem", { name: "Pin", exact: true }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -76,4 +76,15 @@ test.describe("Pinned messages", () => {
|
||||||
await util.backPinnedMessagesList();
|
await util.backPinnedMessagesList();
|
||||||
await util.assertPinnedCountInRoomInfo(0);
|
await util.assertPinnedCountInRoomInfo(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should be able to pin and unpin from the quick actions", async ({ page, app, room1, util }) => {
|
||||||
|
await util.goTo(room1);
|
||||||
|
await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]);
|
||||||
|
await util.pinMessagesFromQuickActions(["Msg1"]);
|
||||||
|
await util.openRoomInfo();
|
||||||
|
await util.assertPinnedCountInRoomInfo(1);
|
||||||
|
|
||||||
|
await util.pinMessagesFromQuickActions(["Msg1"], true);
|
||||||
|
await util.assertPinnedCountInRoomInfo(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -81,11 +81,11 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MessageContextMenu_iconPin::before {
|
.mx_MessageContextMenu_iconPin::before {
|
||||||
mask-image: url("$(res)/img/element-icons/room/pin-upright.svg");
|
mask-image: url("@vector-im/compound-design-tokens/icons/pin.svg");
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MessageContextMenu_iconUnpin::before {
|
.mx_MessageContextMenu_iconUnpin::before {
|
||||||
mask-image: url("$(res)/img/element-icons/room/pin.svg");
|
mask-image: url("@vector-im/compound-design-tokens/icons/unpin.svg");
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MessageContextMenu_iconCopy::before {
|
.mx_MessageContextMenu_iconCopy::before {
|
||||||
|
|
|
@ -36,9 +36,8 @@ import Modal from "../../../Modal";
|
||||||
import Resend from "../../../Resend";
|
import Resend from "../../../Resend";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { isUrlPermitted } from "../../../HtmlUtils";
|
import { isUrlPermitted } from "../../../HtmlUtils";
|
||||||
import { canEditContent, canPinEvent, editEvent, isContentActionable } from "../../../utils/EventUtils";
|
import { canEditContent, editEvent, isContentActionable } from "../../../utils/EventUtils";
|
||||||
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu";
|
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu";
|
||||||
import { ReadPinsEventId } from "../right_panel/types";
|
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||||
|
@ -60,6 +59,7 @@ import { getForwardableEvent } from "../../../events/forward/getForwardableEvent
|
||||||
import { getShareableLocationEvent } from "../../../events/location/getShareableLocationEvent";
|
import { getShareableLocationEvent } from "../../../events/location/getShareableLocationEvent";
|
||||||
import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";
|
import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";
|
||||||
import { CardContext } from "../right_panel/context";
|
import { CardContext } from "../right_panel/context";
|
||||||
|
import PinningUtils from "../../../utils/PinningUtils";
|
||||||
|
|
||||||
interface IReplyInThreadButton {
|
interface IReplyInThreadButton {
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
|
@ -177,24 +177,11 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
||||||
this.props.mxEvent.getType() !== EventType.RoomServerAcl &&
|
this.props.mxEvent.getType() !== EventType.RoomServerAcl &&
|
||||||
this.props.mxEvent.getType() !== EventType.RoomEncryption;
|
this.props.mxEvent.getType() !== EventType.RoomEncryption;
|
||||||
|
|
||||||
let canPin =
|
const canPin = PinningUtils.canPinOrUnpin(cli, this.props.mxEvent);
|
||||||
!!room?.currentState.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli) &&
|
|
||||||
canPinEvent(this.props.mxEvent);
|
|
||||||
|
|
||||||
// HACK: Intentionally say we can't pin if the user doesn't want to use the functionality
|
|
||||||
if (!SettingsStore.getValue("feature_pinning")) canPin = false;
|
|
||||||
|
|
||||||
this.setState({ canRedact, canPin });
|
this.setState({ canRedact, canPin });
|
||||||
};
|
};
|
||||||
|
|
||||||
private isPinned(): boolean {
|
|
||||||
const room = MatrixClientPeg.safeGet().getRoom(this.props.mxEvent.getRoomId());
|
|
||||||
const pinnedEvent = room?.currentState.getStateEvents(EventType.RoomPinnedEvents, "");
|
|
||||||
if (!pinnedEvent) return false;
|
|
||||||
const content = pinnedEvent.getContent();
|
|
||||||
return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
private canEndPoll(mxEvent: MatrixEvent): boolean {
|
private canEndPoll(mxEvent: MatrixEvent): boolean {
|
||||||
return (
|
return (
|
||||||
M_POLL_START.matches(mxEvent.getType()) &&
|
M_POLL_START.matches(mxEvent.getType()) &&
|
||||||
|
@ -257,22 +244,8 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
||||||
};
|
};
|
||||||
|
|
||||||
private onPinClick = (): void => {
|
private onPinClick = (): void => {
|
||||||
const cli = MatrixClientPeg.safeGet();
|
// Pin or unpin in background
|
||||||
const room = cli.getRoom(this.props.mxEvent.getRoomId());
|
PinningUtils.pinOrUnpinEvent(MatrixClientPeg.safeGet(), this.props.mxEvent);
|
||||||
if (!room) return;
|
|
||||||
const eventId = this.props.mxEvent.getId();
|
|
||||||
|
|
||||||
const pinnedIds = room.currentState?.getStateEvents(EventType.RoomPinnedEvents, "")?.getContent().pinned || [];
|
|
||||||
|
|
||||||
if (pinnedIds.includes(eventId)) {
|
|
||||||
pinnedIds.splice(pinnedIds.indexOf(eventId), 1);
|
|
||||||
} else {
|
|
||||||
pinnedIds.push(eventId);
|
|
||||||
cli.setRoomAccountData(room.roomId, ReadPinsEventId, {
|
|
||||||
event_ids: [...(room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids || []), eventId],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
cli.sendStateEvent(room.roomId, EventType.RoomPinnedEvents, { pinned: pinnedIds }, "");
|
|
||||||
this.closeMenu();
|
this.closeMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -452,17 +425,6 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let pinButton: JSX.Element | undefined;
|
|
||||||
if (contentActionable && this.state.canPin) {
|
|
||||||
pinButton = (
|
|
||||||
<IconizedContextMenuOption
|
|
||||||
iconClassName="mx_MessageContextMenu_iconPin"
|
|
||||||
label={this.isPinned() ? _t("action|unpin") : _t("action|pin")}
|
|
||||||
onClick={this.onPinClick}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is specifically not behind the developerMode flag to give people insight into the Matrix
|
// This is specifically not behind the developerMode flag to give people insight into the Matrix
|
||||||
const viewSourceButton = (
|
const viewSourceButton = (
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
|
@ -649,6 +611,18 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let pinButton: JSX.Element | undefined;
|
||||||
|
if (rightClick && this.state.canPin) {
|
||||||
|
const isPinned = PinningUtils.isPinned(MatrixClientPeg.safeGet(), this.props.mxEvent);
|
||||||
|
pinButton = (
|
||||||
|
<IconizedContextMenuOption
|
||||||
|
iconClassName={isPinned ? "mx_MessageContextMenu_iconUnpin" : "mx_MessageContextMenu_iconPin"}
|
||||||
|
label={isPinned ? _t("action|unpin") : _t("action|pin")}
|
||||||
|
onClick={this.onPinClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let viewInRoomButton: JSX.Element | undefined;
|
let viewInRoomButton: JSX.Element | undefined;
|
||||||
if (isThreadRootEvent) {
|
if (isThreadRootEvent) {
|
||||||
viewInRoomButton = (
|
viewInRoomButton = (
|
||||||
|
@ -671,13 +645,14 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
||||||
}
|
}
|
||||||
|
|
||||||
let quickItemsList: JSX.Element | undefined;
|
let quickItemsList: JSX.Element | undefined;
|
||||||
if (editButton || replyButton || reactButton) {
|
if (editButton || replyButton || reactButton || pinButton) {
|
||||||
quickItemsList = (
|
quickItemsList = (
|
||||||
<IconizedContextMenuOptionList>
|
<IconizedContextMenuOptionList>
|
||||||
{reactButton}
|
{reactButton}
|
||||||
{replyButton}
|
{replyButton}
|
||||||
{replyInThreadButton}
|
{replyInThreadButton}
|
||||||
{editButton}
|
{editButton}
|
||||||
|
{pinButton}
|
||||||
</IconizedContextMenuOptionList>
|
</IconizedContextMenuOptionList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -688,7 +663,6 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
||||||
{openInMapSiteButton}
|
{openInMapSiteButton}
|
||||||
{endPollButton}
|
{endPollButton}
|
||||||
{forwardButton}
|
{forwardButton}
|
||||||
{pinButton}
|
|
||||||
{permalinkButton}
|
{permalinkButton}
|
||||||
{reportEventButton}
|
{reportEventButton}
|
||||||
{externalURLButton}
|
{externalURLButton}
|
||||||
|
|
|
@ -26,6 +26,8 @@ import {
|
||||||
M_BEACON_INFO,
|
M_BEACON_INFO,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { Icon as PinIcon } from "@vector-im/compound-design-tokens/icons/pin.svg";
|
||||||
|
import { Icon as UnpinIcon } from "@vector-im/compound-design-tokens/icons/unpin.svg";
|
||||||
|
|
||||||
import { Icon as ContextMenuIcon } from "../../../../res/img/element-icons/context-menu.svg";
|
import { Icon as ContextMenuIcon } from "../../../../res/img/element-icons/context-menu.svg";
|
||||||
import { Icon as EditIcon } from "../../../../res/img/element-icons/room/message-bar/edit.svg";
|
import { Icon as EditIcon } from "../../../../res/img/element-icons/room/message-bar/edit.svg";
|
||||||
|
@ -61,6 +63,7 @@ import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayloa
|
||||||
import { GetRelationsForEvent, IEventTileType } from "../rooms/EventTile";
|
import { GetRelationsForEvent, IEventTileType } from "../rooms/EventTile";
|
||||||
import { VoiceBroadcastInfoEventType } from "../../../voice-broadcast/types";
|
import { VoiceBroadcastInfoEventType } from "../../../voice-broadcast/types";
|
||||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||||
|
import PinningUtils from "../../../utils/PinningUtils";
|
||||||
|
|
||||||
interface IOptionsButtonProps {
|
interface IOptionsButtonProps {
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
|
@ -384,6 +387,17 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pin or unpin the event.
|
||||||
|
*/
|
||||||
|
private onPinClick = async (event: ButtonEvent): Promise<void> => {
|
||||||
|
// Don't open the regular browser or our context menu on right-click
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
await PinningUtils.pinOrUnpinEvent(MatrixClientPeg.safeGet(), this.props.mxEvent);
|
||||||
|
};
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
public render(): React.ReactNode {
|
||||||
const toolbarOpts: JSX.Element[] = [];
|
const toolbarOpts: JSX.Element[] = [];
|
||||||
if (canEditContent(MatrixClientPeg.safeGet(), this.props.mxEvent)) {
|
if (canEditContent(MatrixClientPeg.safeGet(), this.props.mxEvent)) {
|
||||||
|
@ -401,6 +415,22 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (PinningUtils.canPinOrUnpin(MatrixClientPeg.safeGet(), this.props.mxEvent)) {
|
||||||
|
const isPinned = PinningUtils.isPinned(MatrixClientPeg.safeGet(), this.props.mxEvent);
|
||||||
|
toolbarOpts.push(
|
||||||
|
<RovingAccessibleButton
|
||||||
|
className="mx_MessageActionBar_iconButton"
|
||||||
|
title={isPinned ? _t("action|unpin") : _t("action|pin")}
|
||||||
|
onClick={this.onPinClick}
|
||||||
|
onContextMenu={this.onPinClick}
|
||||||
|
key="pin"
|
||||||
|
placement="left"
|
||||||
|
>
|
||||||
|
{isPinned ? <UnpinIcon /> : <PinIcon />}
|
||||||
|
</RovingAccessibleButton>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const cancelSendingButton = (
|
const cancelSendingButton = (
|
||||||
<RovingAccessibleButton
|
<RovingAccessibleButton
|
||||||
className="mx_MessageActionBar_iconButton"
|
className="mx_MessageActionBar_iconButton"
|
||||||
|
|
|
@ -14,13 +14,17 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MatrixEvent, EventType, M_POLL_START } from "matrix-js-sdk/src/matrix";
|
import { MatrixEvent, EventType, M_POLL_START, MatrixClient, EventTimeline } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { canPinEvent, isContentActionable } from "./EventUtils";
|
||||||
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
|
import { ReadPinsEventId } from "../components/views/right_panel/types";
|
||||||
|
|
||||||
export default class PinningUtils {
|
export default class PinningUtils {
|
||||||
/**
|
/**
|
||||||
* Event types that may be pinned.
|
* Event types that may be pinned.
|
||||||
*/
|
*/
|
||||||
public static pinnableEventTypes: (EventType | string)[] = [
|
public static readonly PINNABLE_EVENT_TYPES: (EventType | string)[] = [
|
||||||
EventType.RoomMessage,
|
EventType.RoomMessage,
|
||||||
M_POLL_START.name,
|
M_POLL_START.name,
|
||||||
M_POLL_START.altName,
|
M_POLL_START.altName,
|
||||||
|
@ -33,9 +37,80 @@ export default class PinningUtils {
|
||||||
*/
|
*/
|
||||||
public static isPinnable(event: MatrixEvent): boolean {
|
public static isPinnable(event: MatrixEvent): boolean {
|
||||||
if (!event) return false;
|
if (!event) return false;
|
||||||
if (!this.pinnableEventTypes.includes(event.getType())) return false;
|
if (!this.PINNABLE_EVENT_TYPES.includes(event.getType())) return false;
|
||||||
if (event.isRedacted()) return false;
|
if (event.isRedacted()) return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the given event is pinned.
|
||||||
|
* @param matrixClient
|
||||||
|
* @param mxEvent
|
||||||
|
*/
|
||||||
|
public static isPinned(matrixClient: MatrixClient, mxEvent: MatrixEvent): boolean {
|
||||||
|
const room = matrixClient.getRoom(mxEvent.getRoomId());
|
||||||
|
if (!room) return false;
|
||||||
|
|
||||||
|
const pinnedEvent = room
|
||||||
|
.getLiveTimeline()
|
||||||
|
.getState(EventTimeline.FORWARDS)
|
||||||
|
?.getStateEvents(EventType.RoomPinnedEvents, "");
|
||||||
|
if (!pinnedEvent) return false;
|
||||||
|
const content = pinnedEvent.getContent();
|
||||||
|
return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(mxEvent.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the given event may be pinned or unpinned.
|
||||||
|
* @param matrixClient
|
||||||
|
* @param mxEvent
|
||||||
|
*/
|
||||||
|
public static canPinOrUnpin(matrixClient: MatrixClient, mxEvent: MatrixEvent): boolean {
|
||||||
|
if (!SettingsStore.getValue("feature_pinning")) return false;
|
||||||
|
if (!isContentActionable(mxEvent)) return false;
|
||||||
|
|
||||||
|
const room = matrixClient.getRoom(mxEvent.getRoomId());
|
||||||
|
if (!room) return false;
|
||||||
|
|
||||||
|
return Boolean(
|
||||||
|
room
|
||||||
|
.getLiveTimeline()
|
||||||
|
.getState(EventTimeline.FORWARDS)
|
||||||
|
?.mayClientSendStateEvent(EventType.RoomPinnedEvents, matrixClient) && canPinEvent(mxEvent),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pin or unpin the given event.
|
||||||
|
* @param matrixClient
|
||||||
|
* @param mxEvent
|
||||||
|
*/
|
||||||
|
public static async pinOrUnpinEvent(matrixClient: MatrixClient, mxEvent: MatrixEvent): Promise<void> {
|
||||||
|
const room = matrixClient.getRoom(mxEvent.getRoomId());
|
||||||
|
if (!room) return;
|
||||||
|
|
||||||
|
const eventId = mxEvent.getId();
|
||||||
|
if (!eventId) return;
|
||||||
|
|
||||||
|
// Get the current pinned events of the room
|
||||||
|
const pinnedIds: Array<string> =
|
||||||
|
room
|
||||||
|
.getLiveTimeline()
|
||||||
|
.getState(EventTimeline.FORWARDS)
|
||||||
|
?.getStateEvents(EventType.RoomPinnedEvents, "")
|
||||||
|
?.getContent().pinned || [];
|
||||||
|
|
||||||
|
// If the event is already pinned, unpin it
|
||||||
|
if (pinnedIds.includes(eventId)) {
|
||||||
|
pinnedIds.splice(pinnedIds.indexOf(eventId), 1);
|
||||||
|
} else {
|
||||||
|
// Otherwise, pin it
|
||||||
|
pinnedIds.push(eventId);
|
||||||
|
await matrixClient.setRoomAccountData(room.roomId, ReadPinsEventId, {
|
||||||
|
event_ids: [...(room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids || []), eventId],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await matrixClient.sendStateEvent(room.roomId, EventType.RoomPinnedEvents, { pinned: pinnedIds }, "");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { fireEvent, render, RenderResult } from "@testing-library/react";
|
import { fireEvent, render, RenderResult, screen, waitFor } from "@testing-library/react";
|
||||||
import {
|
import {
|
||||||
EventStatus,
|
EventStatus,
|
||||||
MatrixEvent,
|
MatrixEvent,
|
||||||
|
@ -28,9 +28,11 @@ import {
|
||||||
FeatureSupport,
|
FeatureSupport,
|
||||||
Thread,
|
Thread,
|
||||||
M_POLL_KIND_DISCLOSED,
|
M_POLL_KIND_DISCLOSED,
|
||||||
|
EventTimeline,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import { PollStartEvent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent";
|
import { PollStartEvent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent";
|
||||||
import { mocked } from "jest-mock";
|
import { mocked } from "jest-mock";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
|
||||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||||
import RoomContext, { TimelineRenderingType } from "../../../../src/contexts/RoomContext";
|
import RoomContext, { TimelineRenderingType } from "../../../../src/contexts/RoomContext";
|
||||||
|
@ -83,8 +85,16 @@ describe("MessageContextMenu", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("message pinning", () => {
|
describe("message pinning", () => {
|
||||||
|
let room: Room;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
room = makeDefaultRoom();
|
||||||
|
|
||||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
|
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
|
||||||
|
jest.spyOn(
|
||||||
|
room.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
|
||||||
|
"mayClientSendStateEvent",
|
||||||
|
).mockReturnValue(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
|
@ -95,25 +105,23 @@ describe("MessageContextMenu", () => {
|
||||||
const eventContent = createMessageEventContent("hello");
|
const eventContent = createMessageEventContent("hello");
|
||||||
const event = new MatrixEvent({ type: EventType.RoomMessage, content: eventContent });
|
const event = new MatrixEvent({ type: EventType.RoomMessage, content: eventContent });
|
||||||
|
|
||||||
const room = makeDefaultRoom();
|
|
||||||
// mock permission to disallow adding pinned messages to room
|
// mock permission to disallow adding pinned messages to room
|
||||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(false);
|
jest.spyOn(
|
||||||
|
room.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
|
||||||
|
"mayClientSendStateEvent",
|
||||||
|
).mockReturnValue(false);
|
||||||
|
|
||||||
createMenu(event, {}, {}, undefined, room);
|
createMenu(event, { rightClick: true }, {}, undefined, room);
|
||||||
|
|
||||||
expect(document.querySelector('li[aria-label="Pin"]')).toBeFalsy();
|
expect(screen.queryByRole("menuitem", { name: "Pin" })).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not show pin option for beacon_info event", () => {
|
it("does not show pin option for beacon_info event", () => {
|
||||||
const deadBeaconEvent = makeBeaconInfoEvent("@alice:server.org", roomId, { isLive: false });
|
const deadBeaconEvent = makeBeaconInfoEvent("@alice:server.org", roomId, { isLive: false });
|
||||||
|
|
||||||
const room = makeDefaultRoom();
|
createMenu(deadBeaconEvent, { rightClick: true }, {}, undefined, room);
|
||||||
// mock permission to allow adding pinned messages to room
|
|
||||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
|
||||||
|
|
||||||
createMenu(deadBeaconEvent, {}, {}, undefined, room);
|
expect(screen.queryByRole("menuitem", { name: "Pin" })).toBeFalsy();
|
||||||
|
|
||||||
expect(document.querySelector('li[aria-label="Pin"]')).toBeFalsy();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not show pin option when pinning feature is disabled", () => {
|
it("does not show pin option when pinning feature is disabled", () => {
|
||||||
|
@ -124,15 +132,12 @@ describe("MessageContextMenu", () => {
|
||||||
room_id: roomId,
|
room_id: roomId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const room = makeDefaultRoom();
|
|
||||||
// mock permission to allow adding pinned messages to room
|
|
||||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
|
||||||
// disable pinning feature
|
// disable pinning feature
|
||||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
||||||
|
|
||||||
createMenu(pinnableEvent, {}, {}, undefined, room);
|
createMenu(pinnableEvent, { rightClick: true }, {}, undefined, room);
|
||||||
|
|
||||||
expect(document.querySelector('li[aria-label="Pin"]')).toBeFalsy();
|
expect(screen.queryByRole("menuitem", { name: "Pin" })).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows pin option when pinning feature is enabled", () => {
|
it("shows pin option when pinning feature is enabled", () => {
|
||||||
|
@ -143,16 +148,12 @@ describe("MessageContextMenu", () => {
|
||||||
room_id: roomId,
|
room_id: roomId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const room = makeDefaultRoom();
|
createMenu(pinnableEvent, { rightClick: true }, {}, undefined, room);
|
||||||
// mock permission to allow adding pinned messages to room
|
|
||||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
|
||||||
|
|
||||||
createMenu(pinnableEvent, {}, {}, undefined, room);
|
expect(screen.getByRole("menuitem", { name: "Pin" })).toBeTruthy();
|
||||||
|
|
||||||
expect(document.querySelector('li[aria-label="Pin"]')).toBeTruthy();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("pins event on pin option click", () => {
|
it("pins event on pin option click", async () => {
|
||||||
const onFinished = jest.fn();
|
const onFinished = jest.fn();
|
||||||
const eventContent = createMessageEventContent("hello");
|
const eventContent = createMessageEventContent("hello");
|
||||||
const pinnableEvent = new MatrixEvent({
|
const pinnableEvent = new MatrixEvent({
|
||||||
|
@ -162,43 +163,48 @@ describe("MessageContextMenu", () => {
|
||||||
});
|
});
|
||||||
pinnableEvent.event.event_id = "!3";
|
pinnableEvent.event.event_id = "!3";
|
||||||
const client = MatrixClientPeg.safeGet();
|
const client = MatrixClientPeg.safeGet();
|
||||||
const room = makeDefaultRoom();
|
|
||||||
|
|
||||||
// mock permission to allow adding pinned messages to room
|
jest.spyOn(room.getLiveTimeline().getState(EventTimeline.FORWARDS)!, "getStateEvents").mockReturnValue({
|
||||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
// @ts-ignore
|
||||||
|
getContent: () => ({ pinned: ["!1", "!2"] }),
|
||||||
|
});
|
||||||
|
|
||||||
// mock read pins account data
|
// mock read pins account data
|
||||||
const pinsAccountData = new MatrixEvent({ content: { event_ids: ["!1", "!2"] } });
|
const pinsAccountData = new MatrixEvent({ content: { event_ids: ["!1", "!2"] } });
|
||||||
jest.spyOn(room, "getAccountData").mockReturnValue(pinsAccountData);
|
jest.spyOn(room, "getAccountData").mockReturnValue(pinsAccountData);
|
||||||
|
|
||||||
createMenu(pinnableEvent, { onFinished }, {}, undefined, room);
|
createMenu(pinnableEvent, { onFinished, rightClick: true }, {}, undefined, room);
|
||||||
|
|
||||||
fireEvent.click(document.querySelector('li[aria-label="Pin"]')!);
|
await userEvent.click(screen.getByRole("menuitem", { name: "Pin" }));
|
||||||
|
|
||||||
// added to account data
|
// added to account data
|
||||||
expect(client.setRoomAccountData).toHaveBeenCalledWith(roomId, ReadPinsEventId, {
|
await waitFor(() =>
|
||||||
event_ids: [
|
expect(client.setRoomAccountData).toHaveBeenCalledWith(roomId, ReadPinsEventId, {
|
||||||
// from account data
|
event_ids: [
|
||||||
"!1",
|
// from account data
|
||||||
"!2",
|
"!1",
|
||||||
pinnableEvent.getId(),
|
"!2",
|
||||||
],
|
pinnableEvent.getId(),
|
||||||
});
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// add to room's pins
|
// add to room's pins
|
||||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
await waitFor(() =>
|
||||||
roomId,
|
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||||
EventType.RoomPinnedEvents,
|
roomId,
|
||||||
{
|
EventType.RoomPinnedEvents,
|
||||||
pinned: [pinnableEvent.getId()],
|
{
|
||||||
},
|
pinned: ["!1", "!2", pinnableEvent.getId()],
|
||||||
"",
|
},
|
||||||
|
"",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(onFinished).toHaveBeenCalled();
|
expect(onFinished).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("unpins event on pin option click when event is pinned", () => {
|
it("unpins event on pin option click when event is pinned", async () => {
|
||||||
const eventContent = createMessageEventContent("hello");
|
const eventContent = createMessageEventContent("hello");
|
||||||
const pinnableEvent = new MatrixEvent({
|
const pinnableEvent = new MatrixEvent({
|
||||||
type: EventType.RoomMessage,
|
type: EventType.RoomMessage,
|
||||||
|
@ -207,7 +213,6 @@ describe("MessageContextMenu", () => {
|
||||||
});
|
});
|
||||||
pinnableEvent.event.event_id = "!3";
|
pinnableEvent.event.event_id = "!3";
|
||||||
const client = MatrixClientPeg.safeGet();
|
const client = MatrixClientPeg.safeGet();
|
||||||
const room = makeDefaultRoom();
|
|
||||||
|
|
||||||
// make the event already pinned in the room
|
// make the event already pinned in the room
|
||||||
const pinEvent = new MatrixEvent({
|
const pinEvent = new MatrixEvent({
|
||||||
|
@ -216,18 +221,15 @@ describe("MessageContextMenu", () => {
|
||||||
state_key: "",
|
state_key: "",
|
||||||
content: { pinned: [pinnableEvent.getId(), "!another-event"] },
|
content: { pinned: [pinnableEvent.getId(), "!another-event"] },
|
||||||
});
|
});
|
||||||
room.currentState.setStateEvents([pinEvent]);
|
room.getLiveTimeline().getState(EventTimeline.FORWARDS)!.setStateEvents([pinEvent]);
|
||||||
|
|
||||||
// mock permission to allow adding pinned messages to room
|
|
||||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
|
||||||
|
|
||||||
// mock read pins account data
|
// mock read pins account data
|
||||||
const pinsAccountData = new MatrixEvent({ content: { event_ids: ["!1", "!2"] } });
|
const pinsAccountData = new MatrixEvent({ content: { event_ids: ["!1", "!2"] } });
|
||||||
jest.spyOn(room, "getAccountData").mockReturnValue(pinsAccountData);
|
jest.spyOn(room, "getAccountData").mockReturnValue(pinsAccountData);
|
||||||
|
|
||||||
createMenu(pinnableEvent, {}, {}, undefined, room);
|
createMenu(pinnableEvent, { rightClick: true }, {}, undefined, room);
|
||||||
|
|
||||||
fireEvent.click(document.querySelector('li[aria-label="Unpin"]')!);
|
await userEvent.click(screen.getByRole("menuitem", { name: "Unpin" }));
|
||||||
|
|
||||||
expect(client.setRoomAccountData).not.toHaveBeenCalled();
|
expect(client.setRoomAccountData).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ import {
|
||||||
Room,
|
Room,
|
||||||
FeatureSupport,
|
FeatureSupport,
|
||||||
Thread,
|
Thread,
|
||||||
|
EventTimeline,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import MessageActionBar from "../../../../src/components/views/messages/MessageActionBar";
|
import MessageActionBar from "../../../../src/components/views/messages/MessageActionBar";
|
||||||
|
@ -51,6 +52,8 @@ describe("<MessageActionBar />", () => {
|
||||||
...mockClientMethodsUser(userId),
|
...mockClientMethodsUser(userId),
|
||||||
...mockClientMethodsEvents(),
|
...mockClientMethodsEvents(),
|
||||||
getRoom: jest.fn(),
|
getRoom: jest.fn(),
|
||||||
|
setRoomAccountData: jest.fn(),
|
||||||
|
sendStateEvent: jest.fn(),
|
||||||
});
|
});
|
||||||
const room = new Room(roomId, client, userId);
|
const room = new Room(roomId, client, userId);
|
||||||
|
|
||||||
|
@ -442,10 +445,10 @@ describe("<MessageActionBar />", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([["React"], ["Reply"], ["Reply in thread"], ["Edit"]])(
|
it.each([["React"], ["Reply"], ["Reply in thread"], ["Edit"], ["Pin"]])(
|
||||||
"does not show context menu when right-clicking",
|
"does not show context menu when right-clicking",
|
||||||
(buttonLabel: string) => {
|
(buttonLabel: string) => {
|
||||||
// For favourite button
|
// For favourite and pin buttons
|
||||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
|
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
|
||||||
|
|
||||||
const event = new MouseEvent("contextmenu", {
|
const event = new MouseEvent("contextmenu", {
|
||||||
|
@ -468,4 +471,33 @@ describe("<MessageActionBar />", () => {
|
||||||
fireEvent.contextMenu(queryByLabelText("Options")!);
|
fireEvent.contextMenu(queryByLabelText("Options")!);
|
||||||
expect(queryByTestId("mx_MessageContextMenu")).toBeTruthy();
|
expect(queryByTestId("mx_MessageContextMenu")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("pin button", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// enable pin button
|
||||||
|
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.spyOn(
|
||||||
|
room.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
|
||||||
|
"mayClientSendStateEvent",
|
||||||
|
).mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not render pin button when user can't send state event", () => {
|
||||||
|
jest.spyOn(
|
||||||
|
room.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
|
||||||
|
"mayClientSendStateEvent",
|
||||||
|
).mockReturnValue(false);
|
||||||
|
|
||||||
|
const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent });
|
||||||
|
expect(queryByLabelText("Pin")).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render pin button", () => {
|
||||||
|
const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent });
|
||||||
|
expect(queryByLabelText("Pin")).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
252
test/utils/PinningUtils-test.ts
Normal file
252
test/utils/PinningUtils-test.ts
Normal file
|
@ -0,0 +1,252 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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, EventType, IEvent, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { mocked } from "jest-mock";
|
||||||
|
|
||||||
|
import { createTestClient } from "../test-utils";
|
||||||
|
import PinningUtils from "../../src/utils/PinningUtils";
|
||||||
|
import SettingsStore from "../../src/settings/SettingsStore";
|
||||||
|
import { canPinEvent, isContentActionable } from "../../src/utils/EventUtils";
|
||||||
|
import { ReadPinsEventId } from "../../src/components/views/right_panel/types";
|
||||||
|
|
||||||
|
jest.mock("../../src/utils/EventUtils", () => {
|
||||||
|
return {
|
||||||
|
isContentActionable: jest.fn(),
|
||||||
|
canPinEvent: jest.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("PinningUtils", () => {
|
||||||
|
const roomId = "!room:example.org";
|
||||||
|
const userId = "@alice:example.org";
|
||||||
|
|
||||||
|
const mockedIsContentActionable = mocked(isContentActionable);
|
||||||
|
const mockedCanPinEvent = mocked(canPinEvent);
|
||||||
|
|
||||||
|
let matrixClient: MatrixClient;
|
||||||
|
let room: Room;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a pinned event with the given content.
|
||||||
|
* @param content
|
||||||
|
*/
|
||||||
|
function makePinEvent(content?: Partial<IEvent>) {
|
||||||
|
return new MatrixEvent({
|
||||||
|
type: EventType.RoomMessage,
|
||||||
|
sender: userId,
|
||||||
|
content: {
|
||||||
|
body: "First pinned message",
|
||||||
|
msgtype: "m.text",
|
||||||
|
},
|
||||||
|
room_id: roomId,
|
||||||
|
origin_server_ts: 0,
|
||||||
|
event_id: "$eventId",
|
||||||
|
...content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Enable feature pinning
|
||||||
|
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
|
||||||
|
mockedIsContentActionable.mockImplementation(() => true);
|
||||||
|
mockedCanPinEvent.mockImplementation(() => true);
|
||||||
|
|
||||||
|
matrixClient = createTestClient();
|
||||||
|
room = new Room(roomId, matrixClient, userId);
|
||||||
|
matrixClient.getRoom = jest.fn().mockReturnValue(room);
|
||||||
|
|
||||||
|
jest.spyOn(
|
||||||
|
matrixClient.getRoom(roomId)!.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
|
||||||
|
"mayClientSendStateEvent",
|
||||||
|
).mockReturnValue(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isPinnable", () => {
|
||||||
|
test.each(PinningUtils.PINNABLE_EVENT_TYPES)("should return true for pinnable event types", (eventType) => {
|
||||||
|
const event = makePinEvent({ type: eventType });
|
||||||
|
expect(PinningUtils.isPinnable(event)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return false for a non pinnable event type", () => {
|
||||||
|
const event = makePinEvent({ type: EventType.RoomCreate });
|
||||||
|
expect(PinningUtils.isPinnable(event)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return false for a redacted event", () => {
|
||||||
|
const event = makePinEvent({ unsigned: { redacted_because: "because" as unknown as IEvent } });
|
||||||
|
expect(PinningUtils.isPinnable(event)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isPinned", () => {
|
||||||
|
test("should return false if no room", () => {
|
||||||
|
matrixClient.getRoom = jest.fn().mockReturnValue(undefined);
|
||||||
|
const event = makePinEvent();
|
||||||
|
|
||||||
|
expect(PinningUtils.isPinned(matrixClient, event)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return false if no pinned event", () => {
|
||||||
|
jest.spyOn(
|
||||||
|
matrixClient.getRoom(roomId)!.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
|
||||||
|
"getStateEvents",
|
||||||
|
).mockReturnValue(null);
|
||||||
|
|
||||||
|
const event = makePinEvent();
|
||||||
|
expect(PinningUtils.isPinned(matrixClient, event)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return false if pinned events do not contain the event id", () => {
|
||||||
|
jest.spyOn(
|
||||||
|
matrixClient.getRoom(roomId)!.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
|
||||||
|
"getStateEvents",
|
||||||
|
).mockReturnValue({
|
||||||
|
// @ts-ignore
|
||||||
|
getContent: () => ({ pinned: ["$otherEventId"] }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const event = makePinEvent();
|
||||||
|
expect(PinningUtils.isPinned(matrixClient, event)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return true if pinned events contains the event id", () => {
|
||||||
|
const event = makePinEvent();
|
||||||
|
jest.spyOn(
|
||||||
|
matrixClient.getRoom(roomId)!.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
|
||||||
|
"getStateEvents",
|
||||||
|
).mockReturnValue({
|
||||||
|
// @ts-ignore
|
||||||
|
getContent: () => ({ pinned: [event.getId()] }),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(PinningUtils.isPinned(matrixClient, event)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("canPinOrUnpin", () => {
|
||||||
|
test("should return false if pinning is disabled", () => {
|
||||||
|
// Disable feature pinning
|
||||||
|
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
||||||
|
const event = makePinEvent();
|
||||||
|
|
||||||
|
expect(PinningUtils.canPinOrUnpin(matrixClient, event)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return false if event is not actionable", () => {
|
||||||
|
mockedIsContentActionable.mockImplementation(() => false);
|
||||||
|
const event = makePinEvent();
|
||||||
|
|
||||||
|
expect(PinningUtils.canPinOrUnpin(matrixClient, event)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return false if no room", () => {
|
||||||
|
matrixClient.getRoom = jest.fn().mockReturnValue(undefined);
|
||||||
|
const event = makePinEvent();
|
||||||
|
|
||||||
|
expect(PinningUtils.canPinOrUnpin(matrixClient, event)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return false if client cannot send state event", () => {
|
||||||
|
jest.spyOn(
|
||||||
|
matrixClient.getRoom(roomId)!.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
|
||||||
|
"mayClientSendStateEvent",
|
||||||
|
).mockReturnValue(false);
|
||||||
|
const event = makePinEvent();
|
||||||
|
|
||||||
|
expect(PinningUtils.canPinOrUnpin(matrixClient, event)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return false if event is not pinnable", () => {
|
||||||
|
mockedCanPinEvent.mockReturnValue(false);
|
||||||
|
const event = makePinEvent();
|
||||||
|
|
||||||
|
expect(PinningUtils.canPinOrUnpin(matrixClient, event)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return true if all conditions are met", () => {
|
||||||
|
const event = makePinEvent();
|
||||||
|
|
||||||
|
expect(PinningUtils.canPinOrUnpin(matrixClient, event)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("pinOrUnpinEvent", () => {
|
||||||
|
test("should do nothing if no room", async () => {
|
||||||
|
matrixClient.getRoom = jest.fn().mockReturnValue(undefined);
|
||||||
|
const event = makePinEvent();
|
||||||
|
|
||||||
|
await PinningUtils.pinOrUnpinEvent(matrixClient, event);
|
||||||
|
expect(matrixClient.sendStateEvent).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should do nothing if no event id", async () => {
|
||||||
|
const event = makePinEvent({ event_id: undefined });
|
||||||
|
|
||||||
|
await PinningUtils.pinOrUnpinEvent(matrixClient, event);
|
||||||
|
expect(matrixClient.sendStateEvent).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should pin the event if not pinned", async () => {
|
||||||
|
jest.spyOn(
|
||||||
|
matrixClient.getRoom(roomId)!.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
|
||||||
|
"getStateEvents",
|
||||||
|
).mockReturnValue({
|
||||||
|
// @ts-ignore
|
||||||
|
getContent: () => ({ pinned: ["$otherEventId"] }),
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.spyOn(room, "getAccountData").mockReturnValue({
|
||||||
|
getContent: jest.fn().mockReturnValue({
|
||||||
|
event_ids: ["$otherEventId"],
|
||||||
|
}),
|
||||||
|
} as unknown as MatrixEvent);
|
||||||
|
|
||||||
|
const event = makePinEvent();
|
||||||
|
await PinningUtils.pinOrUnpinEvent(matrixClient, event);
|
||||||
|
|
||||||
|
expect(matrixClient.setRoomAccountData).toHaveBeenCalledWith(roomId, ReadPinsEventId, {
|
||||||
|
event_ids: ["$otherEventId", event.getId()],
|
||||||
|
});
|
||||||
|
expect(matrixClient.sendStateEvent).toHaveBeenCalledWith(
|
||||||
|
roomId,
|
||||||
|
EventType.RoomPinnedEvents,
|
||||||
|
{ pinned: ["$otherEventId", event.getId()] },
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should unpin the event if already pinned", async () => {
|
||||||
|
const event = makePinEvent();
|
||||||
|
|
||||||
|
jest.spyOn(
|
||||||
|
matrixClient.getRoom(roomId)!.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
|
||||||
|
"getStateEvents",
|
||||||
|
).mockReturnValue({
|
||||||
|
// @ts-ignore
|
||||||
|
getContent: () => ({ pinned: [event.getId(), "$otherEventId"] }),
|
||||||
|
});
|
||||||
|
|
||||||
|
await PinningUtils.pinOrUnpinEvent(matrixClient, event);
|
||||||
|
expect(matrixClient.sendStateEvent).toHaveBeenCalledWith(
|
||||||
|
roomId,
|
||||||
|
EventType.RoomPinnedEvents,
|
||||||
|
{ pinned: ["$otherEventId"] },
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in a new issue