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:
Florian Duros 2024-08-21 10:50:00 +02:00 committed by GitHub
parent 4064db1d02
commit 3d80eff65b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 503 additions and 105 deletions

View file

@ -36,9 +36,8 @@ import Modal from "../../../Modal";
import Resend from "../../../Resend";
import SettingsStore from "../../../settings/SettingsStore";
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 { ReadPinsEventId } from "../right_panel/types";
import { Action } from "../../../dispatcher/actions";
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import { ButtonEvent } from "../elements/AccessibleButton";
@ -60,6 +59,7 @@ import { getForwardableEvent } from "../../../events/forward/getForwardableEvent
import { getShareableLocationEvent } from "../../../events/location/getShareableLocationEvent";
import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";
import { CardContext } from "../right_panel/context";
import PinningUtils from "../../../utils/PinningUtils";
interface IReplyInThreadButton {
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.RoomEncryption;
let canPin =
!!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;
const canPin = PinningUtils.canPinOrUnpin(cli, this.props.mxEvent);
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 {
return (
M_POLL_START.matches(mxEvent.getType()) &&
@ -257,22 +244,8 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
};
private onPinClick = (): void => {
const cli = MatrixClientPeg.safeGet();
const room = cli.getRoom(this.props.mxEvent.getRoomId());
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 }, "");
// Pin or unpin in background
PinningUtils.pinOrUnpinEvent(MatrixClientPeg.safeGet(), this.props.mxEvent);
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
const viewSourceButton = (
<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;
if (isThreadRootEvent) {
viewInRoomButton = (
@ -671,13 +645,14 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
}
let quickItemsList: JSX.Element | undefined;
if (editButton || replyButton || reactButton) {
if (editButton || replyButton || reactButton || pinButton) {
quickItemsList = (
<IconizedContextMenuOptionList>
{reactButton}
{replyButton}
{replyInThreadButton}
{editButton}
{pinButton}
</IconizedContextMenuOptionList>
);
}
@ -688,7 +663,6 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
{openInMapSiteButton}
{endPollButton}
{forwardButton}
{pinButton}
{permalinkButton}
{reportEventButton}
{externalURLButton}