diff --git a/res/css/views/context_menus/_MessageContextMenu.pcss b/res/css/views/context_menus/_MessageContextMenu.pcss index be113c770f6c..28529eabf983 100644 --- a/res/css/views/context_menus/_MessageContextMenu.pcss +++ b/res/css/views/context_menus/_MessageContextMenu.pcss @@ -81,11 +81,11 @@ limitations under the License. } .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 { - 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 { diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index 801ab0b023e7..2d5a81a89b62 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -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 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 }; 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 ); } - let pinButton: JSX.Element | undefined; - if (contentActionable && this.state.canPin) { - pinButton = ( - - ); - } - // This is specifically not behind the developerMode flag to give people insight into the Matrix const viewSourceButton = ( ); } + let pinButton: JSX.Element | undefined; + if (rightClick && this.state.canPin) { + const isPinned = PinningUtils.isPinned(MatrixClientPeg.safeGet(), this.props.mxEvent); + pinButton = ( + + ); + } + let viewInRoomButton: JSX.Element | undefined; if (isThreadRootEvent) { viewInRoomButton = ( @@ -671,13 +645,14 @@ export default class MessageContextMenu extends React.Component } let quickItemsList: JSX.Element | undefined; - if (editButton || replyButton || reactButton) { + if (editButton || replyButton || reactButton || pinButton) { quickItemsList = ( {reactButton} {replyButton} {replyInThreadButton} {editButton} + {pinButton} ); } @@ -688,7 +663,6 @@ export default class MessageContextMenu extends React.Component {openInMapSiteButton} {endPollButton} {forwardButton} - {pinButton} {permalinkButton} {reportEventButton} {externalURLButton} diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index 25547c78363d..20a87abc9d0c 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -26,6 +26,8 @@ import { M_BEACON_INFO, } from "matrix-js-sdk/src/matrix"; 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 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 { VoiceBroadcastInfoEventType } from "../../../voice-broadcast/types"; import { ButtonEvent } from "../elements/AccessibleButton"; +import PinningUtils from "../../../utils/PinningUtils"; interface IOptionsButtonProps { mxEvent: MatrixEvent; @@ -384,6 +387,13 @@ export default class MessageActionBar extends React.PureComponent => { + await PinningUtils.pinOrUnpinEvent(MatrixClientPeg.safeGet(), this.props.mxEvent); + }; + public render(): React.ReactNode { const toolbarOpts: JSX.Element[] = []; if (canEditContent(MatrixClientPeg.safeGet(), this.props.mxEvent)) { @@ -401,6 +411,22 @@ export default class MessageActionBar extends React.PureComponent + {isPinned ? : } + , + ); + } + const cancelSendingButton = ( { + 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 = + 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 }, ""); + } }