diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index 6b871e4f24a..3a2d057ac5c 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -29,9 +29,12 @@ import Modal from '../../../Modal'; import Resend from '../../../Resend'; import SettingsStore from '../../../settings/SettingsStore'; import { isUrlPermitted } from '../../../HtmlUtils'; -import { isContentActionable } from '../../../utils/EventUtils'; +import { isContentActionable, canEditContent } from '../../../utils/EventUtils'; import {MenuItem} from "../../structures/ContextMenu"; import {EventType} from "matrix-js-sdk/src/@types/event"; +import {useRovingTabIndex} from "../../../accessibility/RovingTabIndex"; +import {aboveLeftOf, ContextMenu, useContextMenu} from '../../structures/ContextMenu'; +import RoomContext from "../../../contexts/RoomContext"; function canCancel(eventStatus) { return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; @@ -50,6 +53,9 @@ export default class MessageContextMenu extends React.Component { /* callback called when the menu is dismissed */ onFinished: PropTypes.func, + + /* if true show react button */ + rightClick: PropTypes.bool, }; state = { @@ -57,6 +63,8 @@ export default class MessageContextMenu extends React.Component { canPin: false, }; + static contextType = RoomContext; + componentDidMount() { MatrixClientPeg.get().on('RoomMember.powerLevel', this._checkPermissions); this._checkPermissions(); @@ -288,6 +296,22 @@ export default class MessageContextMenu extends React.Component { return this._getReactions(e => e.status === EventStatus.NOT_SENT); } + onEditClick = (ev) => { + dis.dispatch({ + action: 'edit_event', + event: this.props.mxEvent, + }); + this.closeMenu(); + }; + + onReplyClick = (ev) => { + dis.dispatch({ + action: 'reply_to_event', + event: this.props.mxEvent, + }); + this.closeMenu(); + }; + render() { const cli = MatrixClientPeg.get(); const me = cli.getUserId(); @@ -314,6 +338,9 @@ export default class MessageContextMenu extends React.Component { let externalURLButton; let quoteButton; let collapseReplyThread; + let reactButton; + let replyButton; + let editButton; // status is SENT before remote-echo, null after const isSent = !eventStatus || eventStatus === EventStatus.SENT; @@ -470,8 +497,32 @@ export default class MessageContextMenu extends React.Component { ); } + if (this.props.rightClick) { + if (isContentActionable(this.props.mxEvent)) { + if (this.context.canReact) { + reactButton = ; + } + if (this.context.canReply) { + replyButton = + {_t("Reply")} + ; + } + } + if (canEditContent(this.props.mxEvent)) { + editButton = + {_t("Edit")} + ; + } + } + return (
+ { reactButton } + { replyButton } + { editButton } { resendButton } { resendEditButton } { resendReactionsButton } @@ -492,3 +543,29 @@ export default class MessageContextMenu extends React.Component { ); } } + +const ReactButton = ({mxEvent, onCloseContextMenu}) => { + const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); + const ref = useRovingTabIndex(button)[2]; + + const closeReactionPicker = () => { + closeMenu(); + onCloseContextMenu(); + }; + + let reactionPicker; + if (menuDisplayed) { + const buttonRect = button.current.getBoundingClientRect(); + const ReactionPicker = sdk.getComponent('emojipicker.ReactionPicker'); + reactionPicker = + + ; + } + + return + + {_t("React")} + + { reactionPicker } + ; +}; diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 11277daa573..8bac7fdd169 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -37,6 +37,7 @@ import {E2E_STATE} from "./E2EIcon"; import {toRem} from "../../../utils/units"; import {WidgetType} from "../../../widgets/WidgetType"; import RoomAvatar from "../avatars/RoomAvatar"; +import {ContextMenu, ChevronFace} from '../../structures/ContextMenu'; const eventTileTypes = { 'm.room.message': 'messages.MessageEvent', @@ -254,6 +255,8 @@ export default class EventTile extends React.Component { previouslyRequestedKeys: false, // The Relations model from the JS SDK for reactions to `mxEvent` reactions: this.getReactions(), + + contextMenuPosition: null, }; // don't do RR animations until we are mounted @@ -632,6 +635,56 @@ export default class EventTile extends React.Component { }); }; + renderMenu() { + let contextMenu = null; + if (this.state.contextMenuPosition) { + const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu'); + + const tile = this.getTile && this.getTile(); + const replyThread = this.getReplyThread && this.getReplyThread(); + const collapseReplyThread = replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined; + + contextMenu = ( + + + + ); + } + + return ( + + { contextMenu } + + ); + } + + onContextMenu = (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + this.setState({ + contextMenuPosition: { + left: ev.clientX, + top: ev.clientY, + height: 0, + }, + }); + }; + + onCloseMenu = () => { + this.setState({contextMenuPosition: null}); + } + render() { const MessageTimestamp = sdk.getComponent('messages.MessageTimestamp'); const SenderProfile = sdk.getComponent('messages.SenderProfile'); @@ -853,6 +906,7 @@ export default class EventTile extends React.Component { const room = this.context.getRoom(this.props.mxEvent.getRoomId()); return (
+ {this.renderMenu()}
@@ -866,7 +920,7 @@ export default class EventTile extends React.Component { { timestamp }
-
+
-
+ {this.renderMenu()} +
+
{ groupTimestamp } { groupPadlock } { thread } @@ -947,13 +1002,14 @@ export default class EventTile extends React.Component { // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers return (
+ {this.renderMenu()} { ircTimestamp }
{ readAvatars }
{ sender } { ircPadlock } -
+
{ groupTimestamp } { groupPadlock } { thread }