From 530c92e03d847098867898a52c017fc6ef1d1842 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 26 Apr 2019 11:01:29 +0100 Subject: [PATCH 1/4] Rename event edit button to options button This naming is clearer as it doesn't really edit at all (it shows a context menu). This should also be less confusing with actual editing when it arrives. --- res/css/views/rooms/_EventTile.scss | 8 ++++---- src/components/views/rooms/EventTile.js | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 6363750f4c..bbca1c3498 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -219,7 +219,7 @@ limitations under the License. width: auto; } -.mx_EventTile_editButton { +.mx_EventTile_optionsButton { position: absolute; display: inline-block; visibility: hidden; @@ -232,8 +232,8 @@ limitations under the License. user-select: none; } -.mx_EventTile:hover .mx_EventTile_editButton, -.mx_EventTile.menu .mx_EventTile_editButton { +.mx_EventTile:hover .mx_EventTile_optionsButton, +.mx_EventTile.menu .mx_EventTile_optionsButton { visibility: visible; } @@ -551,7 +551,7 @@ limitations under the License. top: 3px; } - .mx_EventTile_editButton { + .mx_EventTile_optionsButton { top: 3px; } diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index d7c9f1e443..933f134be7 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -309,7 +309,7 @@ module.exports = withMatrixClient(React.createClass({ return actions.tweaks.highlight; }, - onEditClicked: function(e) { + onOptionsClicked: function(e) { const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu'); const buttonRect = e.target.getBoundingClientRect(); @@ -602,8 +602,8 @@ module.exports = withMatrixClient(React.createClass({ } } - const editButton = ( - + const optionsButton = ( + ); const timestamp = this.props.mxEvent.getTs() ? @@ -755,7 +755,7 @@ module.exports = withMatrixClient(React.createClass({ showUrlPreview={this.props.showUrlPreview} onHeightChanged={this.props.onHeightChanged} /> { keyRequestInfo } - { editButton } + { optionsButton } { // The avatar goes after the event tile as it's absolutly positioned to be over the From ed8bbc70820d0b067af0f5b9b528f9442a6f83cb Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 26 Apr 2019 12:14:30 +0100 Subject: [PATCH 2/4] Extract message options button to action bar This adds a new action bar component to hold multiple per-message actions. This existing options button has moved to this new component, and is currently the only action. --- res/css/_components.scss | 1 + res/css/views/messages/_MessageActionBar.scss | 37 ++++++++ res/css/views/rooms/_EventTile.scss | 25 +----- .../views/messages/MessageActionBar.js | 87 +++++++++++++++++++ src/components/views/rooms/EventTile.js | 57 ++++-------- src/i18n/strings/en_EN.json | 2 +- 6 files changed, 148 insertions(+), 61 deletions(-) create mode 100644 res/css/views/messages/_MessageActionBar.scss create mode 100644 src/components/views/messages/MessageActionBar.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 8bea138acb..bb09b873a3 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -113,6 +113,7 @@ @import "./views/messages/_MNoticeBody.scss"; @import "./views/messages/_MStickerBody.scss"; @import "./views/messages/_MTextBody.scss"; +@import "./views/messages/_MessageActionBar.scss"; @import "./views/messages/_MessageTimestamp.scss"; @import "./views/messages/_RoomAvatarEvent.scss"; @import "./views/messages/_SenderProfile.scss"; diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss new file mode 100644 index 0000000000..917f82dc66 --- /dev/null +++ b/res/css/views/messages/_MessageActionBar.scss @@ -0,0 +1,37 @@ +/* +Copyright 2019 New Vector Ltd + +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. +*/ + +.mx_MessageActionBar { + position: absolute; + visibility: hidden; + cursor: pointer; + top: 6px; + right: 6px; + user-select: none; +} + +.mx_MessageActionBar_optionsButton { + display: inline-block; + width: 19px; + height: 19px; + background-image: url($edit-button-url); +} + +.mx_MatrixChat_useCompactLayout { + .mx_MessageActionBar { + top: 3px; + } +} diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index bbca1c3498..f4c12bb734 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -121,7 +121,7 @@ limitations under the License. } .mx_EventTile:hover .mx_EventTile_line, -.mx_EventTile.menu .mx_EventTile_line +.mx_EventTile.mx_EventTile_actionBarFocused .mx_EventTile_line { background-color: $event-selected-color; } @@ -206,7 +206,7 @@ limitations under the License. // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) .mx_EventTile_last > div > a > .mx_MessageTimestamp, .mx_EventTile:hover > div > a > .mx_MessageTimestamp, -.mx_EventTile.menu > div > a > .mx_MessageTimestamp { +.mx_EventTile.mx_EventTile_actionBarFocused > div > a > .mx_MessageTimestamp { visibility: visible; } @@ -219,21 +219,8 @@ limitations under the License. width: auto; } -.mx_EventTile_optionsButton { - position: absolute; - display: inline-block; - visibility: hidden; - cursor: pointer; - top: 6px; - right: 6px; - width: 19px; - height: 19px; - background-image: url($edit-button-url); - user-select: none; -} - -.mx_EventTile:hover .mx_EventTile_optionsButton, -.mx_EventTile.menu .mx_EventTile_optionsButton { +.mx_EventTile:hover .mx_MessageActionBar, +.mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar { visibility: visible; } @@ -551,10 +538,6 @@ limitations under the License. top: 3px; } - .mx_EventTile_optionsButton { - top: 3px; - } - .mx_EventTile_readAvatars { top: 27px; } diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js new file mode 100644 index 0000000000..c4c271e1a5 --- /dev/null +++ b/src/components/views/messages/MessageActionBar.js @@ -0,0 +1,87 @@ +/* +Copyright 2019 New Vector Ltd + +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 React from 'react'; +import PropTypes from 'prop-types'; +import { _t } from '../../../languageHandler'; +import sdk from '../../../index'; +import Modal from '../../../Modal'; +import { createMenu } from '../../structures/ContextualMenu'; + +export default class MessageActionBar extends React.PureComponent { + static propTypes = { + mxEvent: PropTypes.object.isRequired, + permalinkCreator: PropTypes.object, + tile: PropTypes.element, + replyThread: PropTypes.element, + onFocusChange: PropTypes.func, + }; + + onFocusChange = (focused) => { + if (!this.props.onFocusChange) { + return; + } + this.props.onFocusChange(focused); + } + + onCryptoClicked = () => { + const event = this.props.mxEvent; + Modal.createTrackedDialogAsync('Encrypted Event Dialog', '', + import('../../../async-components/views/dialogs/EncryptedEventDialog'), + {event}, + ); + } + + onOptionsClicked = (ev) => { + const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu'); + const buttonRect = ev.target.getBoundingClientRect(); + + // The window X and Y offsets are to adjust position when zoomed in to page + const x = buttonRect.right + window.pageXOffset; + const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19; + + const {tile, replyThread} = this.props; + + let e2eInfoCallback = null; + if (this.props.mxEvent.isEncrypted()) { + e2eInfoCallback = () => this.onCryptoClicked(); + } + + createMenu(MessageContextMenu, { + chevronOffset: 10, + mxEvent: this.props.mxEvent, + left: x, + top: y, + permalinkCreator: this.props.permalinkCreator, + eventTileOps: tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined, + collapseReplyThread: replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined, + e2eInfoCallback: e2eInfoCallback, + onFinished: () => { + this.onFocusChange(false); + }, + }); + + this.onFocusChange(true); + } + + render() { + // TODO: Move the bar to a separate element once there are several buttons + return ; + } +} diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 933f134be7..dd0a7aa47b 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -17,7 +17,6 @@ limitations under the License. 'use strict'; - import ReplyThread from "../elements/ReplyThread"; const React = require('react'); @@ -30,7 +29,6 @@ const sdk = require('../../../index'); const TextForEvent = require('../../../TextForEvent'); import withMatrixClient from '../../../wrappers/withMatrixClient'; -const ContextualMenu = require('../../structures/ContextualMenu'); import dis from '../../../dispatcher'; import SettingsStore from "../../../settings/SettingsStore"; import {EventStatus} from 'matrix-js-sdk'; @@ -172,8 +170,8 @@ module.exports = withMatrixClient(React.createClass({ getInitialState: function() { return { - // Whether the context menu is being displayed. - menu: false, + // Whether the action bar is focused. + actionBarFocused: false, // Whether all read receipts are being displayed. If not, only display // a truncation of them. allReadAvatars: false, @@ -309,36 +307,6 @@ module.exports = withMatrixClient(React.createClass({ return actions.tweaks.highlight; }, - onOptionsClicked: function(e) { - const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu'); - const buttonRect = e.target.getBoundingClientRect(); - - // The window X and Y offsets are to adjust position when zoomed in to page - const x = buttonRect.right + window.pageXOffset; - const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19; - const self = this; - - const {tile, replyThread} = this.refs; - - let e2eInfoCallback = null; - if (this.props.mxEvent.isEncrypted()) e2eInfoCallback = () => this.onCryptoClicked(); - - ContextualMenu.createMenu(MessageContextMenu, { - chevronOffset: 10, - mxEvent: this.props.mxEvent, - left: x, - top: y, - permalinkCreator: this.props.permalinkCreator, - eventTileOps: tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined, - collapseReplyThread: replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined, - e2eInfoCallback: e2eInfoCallback, - onFinished: function() { - self.setState({menu: false}); - }, - }); - this.setState({menu: true}); - }, - toggleAllReadAvatars: function() { this.setState({ allReadAvatars: !this.state.allReadAvatars, @@ -490,6 +458,12 @@ module.exports = withMatrixClient(React.createClass({ return null; }, + onActionBarFocusChange(focused) { + this.setState({ + actionBarFocused: focused, + }); + }, + render: function() { const MessageTimestamp = sdk.getComponent('messages.MessageTimestamp'); const SenderProfile = sdk.getComponent('messages.SenderProfile'); @@ -536,7 +510,7 @@ module.exports = withMatrixClient(React.createClass({ mx_EventTile_continuation: this.props.tileShape ? '' : this.props.continuation, mx_EventTile_last: this.props.last, mx_EventTile_contextual: this.props.contextual, - menu: this.state.menu, + mx_EventTile_actionBarFocused: this.state.actionBarFocused, mx_EventTile_verified: this.state.verified === true, mx_EventTile_unverified: this.state.verified === false, mx_EventTile_bad: isEncryptionFailure, @@ -602,9 +576,14 @@ module.exports = withMatrixClient(React.createClass({ } } - const optionsButton = ( - - ); + const MessageActionBar = sdk.getComponent('messages.MessageActionBar'); + const actionBar = ; const timestamp = this.props.mxEvent.getTs() ? : null; @@ -755,7 +734,7 @@ module.exports = withMatrixClient(React.createClass({ showUrlPreview={this.props.showUrlPreview} onHeightChanged={this.props.onHeightChanged} /> { keyRequestInfo } - { optionsButton } + { actionBar } { // The avatar goes after the event tile as it's absolutly positioned to be over the diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d1ff8b2695..d7d2b108a2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -671,7 +671,6 @@ "%(senderName)s sent an image": "%(senderName)s sent an image", "%(senderName)s sent a video": "%(senderName)s sent a video", "%(senderName)s uploaded a file": "%(senderName)s uploaded a file", - "Options": "Options", "Your key share request has been sent - please check your other devices for key share requests.": "Your key share request has been sent - please check your other devices for key share requests.", "Key share requests are sent to your other devices automatically. If you rejected or dismissed the key share request on your other devices, click here to request the keys for this session again.": "Key share requests are sent to your other devices automatically. If you rejected or dismissed the key share request on your other devices, click here to request the keys for this session again.", "If your other devices do not have the key for this message you will not be able to decrypt them.": "If your other devices do not have the key for this message you will not be able to decrypt them.", @@ -889,6 +888,7 @@ "Today": "Today", "Yesterday": "Yesterday", "Error decrypting audio": "Error decrypting audio", + "Options": "Options", "Attachment": "Attachment", "Error decrypting attachment": "Error decrypting attachment", "Decrypt %(text)s": "Decrypt %(text)s", From 8ef9fe951dca64db7f83eb3dd60032a2f77b4ace Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 29 Apr 2019 13:04:16 +0100 Subject: [PATCH 3/4] Update styling of message action bar for multiple buttons This applies the new design for multiple buttons in the message action bar, paving the way for more things to appear here. In addition, this changes the existing options button to use the three vertical dots icon. Some theme colors are also tweaked to align with what they were meant to be from the unified palette. --- res/css/views/messages/_MessageActionBar.scss | 56 +++++++++++++++---- res/img/icon_context_message.svg | 15 ----- res/themes/dark/css/_dark.scss | 3 + res/themes/light/css/_light.scss | 8 ++- .../views/messages/MessageActionBar.js | 11 ++-- 5 files changed, 58 insertions(+), 35 deletions(-) delete mode 100644 res/img/icon_context_message.svg diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss index 917f82dc66..7bd5fa5cf9 100644 --- a/res/css/views/messages/_MessageActionBar.scss +++ b/res/css/views/messages/_MessageActionBar.scss @@ -18,20 +18,52 @@ limitations under the License. position: absolute; visibility: hidden; cursor: pointer; - top: 6px; - right: 6px; + display: flex; + height: 24px; + border-radius: 4px; + background: $primary-bg-color; + top: -13px; + right: 8px; user-select: none; -} -.mx_MessageActionBar_optionsButton { - display: inline-block; - width: 19px; - height: 19px; - background-image: url($edit-button-url); -} + > * { + display: inline-block; + position: relative; + width: 27px; + border: 1px solid $message-action-bar-border-color; + margin-left: -1px; -.mx_MatrixChat_useCompactLayout { - .mx_MessageActionBar { - top: 3px; + &:hover { + border-color: $message-action-bar-hover-border-color; + z-index: 1; + } + + &:first-child { + border-radius: 3px 0 0 3px; + } + + &:last-child { + border-radius: 0 3px 3px 0; + } + + &:only-child { + border-radius: 3px; + } + + &::after { + content: ''; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + mask-repeat: no-repeat; + mask-position: center; + background-color: $primary-fg-color; + } } } + +.mx_MessageActionBar_optionsButton::after { + mask-image: url('$(res)/img/icon_context.svg'); +} diff --git a/res/img/icon_context_message.svg b/res/img/icon_context_message.svg deleted file mode 100644 index f2ceccfa78..0000000000 --- a/res/img/icon_context_message.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - ED5D3E59-2561-4AC1-9B43-82FBC51767FC - Created with sketchtool. - - - - - - - - - - diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index c433a028a3..0d3b40b64f 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -146,6 +146,9 @@ $room-warning-bg-color: $header-panel-bg-color; $dark-panel-bg-color: $header-panel-bg-color; $panel-gradient: rgba(34, 38, 46, 0), rgba(34, 38, 46, 1); +$message-action-bar-border-color: $input-darker-bg-color; +$message-action-bar-hover-border-color: $text-secondary-color; + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index ca2e4cf58d..bdd5f10cc9 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -73,7 +73,7 @@ $primary-hairline-color: #e5e5e5; // used for the border of input text fields $input-border-color: #e7e7e7; -$input-darker-bg-color: rgba(193, 201, 214, 0.29); +$input-darker-bg-color: #e3e8f0; $input-darker-fg-color: #9fa9ba; $input-lighter-bg-color: #f2f5f8; $input-lighter-fg-color: $input-darker-fg-color; @@ -153,7 +153,7 @@ $roomheader-button-color: #91A1C0; $groupheader-button-color: #91A1C0; $rightpanel-button-color: #91A1C0; $composer-button-color: #91A1C0; -$roomtopic-color: #9fa9ba; +$roomtopic-color: #9e9e9e; $eventtile-meta-color: $roomtopic-color; $composer-e2e-icon-color: #c9ced6; @@ -203,7 +203,6 @@ $event-redacted-border-color: #cccccc; // event timestamp $event-timestamp-color: #acacac; -$edit-button-url: "$(res)/img/icon_context_message.svg"; $copy-button-url: "$(res)/img/icon_copy_message.svg"; // e2e @@ -255,6 +254,9 @@ $authpage-secondary-color: #61708b; $dark-panel-bg-color: $secondary-accent-color; $panel-gradient: rgba(242, 245, 248, 0), rgba(242, 245, 248, 1); +$message-action-bar-border-color: $input-darker-bg-color; +$message-action-bar-hover-border-color: $roomtopic-color; + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index c4c271e1a5..3543c41ace 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -78,10 +78,11 @@ export default class MessageActionBar extends React.PureComponent { } render() { - // TODO: Move the bar to a separate element once there are several buttons - return ; + return
+ +
; } } From 739c8c0314e7a04a130e0b93fd1cea39ce205e39 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 29 Apr 2019 15:16:16 +0100 Subject: [PATCH 4/4] Promote reply button up to message action bar This moves the reply action out of the existing options menu and up to the message action bar for easier access. --- res/css/views/messages/_MessageActionBar.scss | 8 +++- res/img/reply.svg | 6 +++ .../views/context_menus/MessageContextMenu.js | 16 -------- .../views/messages/MessageActionBar.js | 37 ++++++++++++++++++- 4 files changed, 47 insertions(+), 20 deletions(-) create mode 100644 res/img/reply.svg diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss index 7bd5fa5cf9..fc73d16d8f 100644 --- a/res/css/views/messages/_MessageActionBar.scss +++ b/res/css/views/messages/_MessageActionBar.scss @@ -55,8 +55,8 @@ limitations under the License. position: absolute; top: 0; left: 0; - bottom: 0; - right: 0; + height: 100%; + width: 100%; mask-repeat: no-repeat; mask-position: center; background-color: $primary-fg-color; @@ -64,6 +64,10 @@ limitations under the License. } } +.mx_MessageActionBar_replyButton::after { + mask-image: url('$(res)/img/reply.svg'); +} + .mx_MessageActionBar_optionsButton::after { mask-image: url('$(res)/img/icon_context.svg'); } diff --git a/res/img/reply.svg b/res/img/reply.svg new file mode 100644 index 0000000000..8cbbad3550 --- /dev/null +++ b/res/img/reply.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index 504729f629..1191b6d66e 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -201,14 +201,6 @@ module.exports = React.createClass({ this.closeMenu(); }, - onReplyClick: function() { - dis.dispatch({ - action: 'reply_to_event', - event: this.props.mxEvent, - }); - this.closeMenu(); - }, - onCollapseReplyThreadClick: function() { this.props.collapseReplyThread(); this.closeMenu(); @@ -226,7 +218,6 @@ module.exports = React.createClass({ let unhidePreviewButton; let externalURLButton; let quoteButton; - let replyButton; let collapseReplyThread; // status is SENT before remote-echo, null after @@ -265,12 +256,6 @@ module.exports = React.createClass({ ); - replyButton = ( -
- { _t('Reply') } -
- ); - if (this.state.canPin) { pinButton = (
@@ -368,7 +353,6 @@ module.exports = React.createClass({ { unhidePreviewButton } { permalinkButton } { quoteButton } - { replyButton } { externalURLButton } { collapseReplyThread } { e2eInfo } diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 3543c41ace..fa020612c4 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -16,8 +16,11 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import {EventStatus} from 'matrix-js-sdk'; + import { _t } from '../../../languageHandler'; import sdk from '../../../index'; +import dis from '../../../dispatcher'; import Modal from '../../../Modal'; import { createMenu } from '../../structures/ContextualMenu'; @@ -45,7 +48,14 @@ export default class MessageActionBar extends React.PureComponent { ); } - onOptionsClicked = (ev) => { + onReplyClick = (ev) => { + dis.dispatch({ + action: 'reply_to_event', + event: this.props.mxEvent, + }); + } + + onOptionsClick = (ev) => { const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu'); const buttonRect = ev.target.getBoundingClientRect(); @@ -78,10 +88,33 @@ export default class MessageActionBar extends React.PureComponent { } render() { + const { mxEvent } = this.props; + const { status: eventStatus } = mxEvent; + + // status is SENT before remote-echo, null after + const isSent = !eventStatus || eventStatus === EventStatus.SENT; + + let replyButton; + + if (isSent && mxEvent.getType() === 'm.room.message') { + const content = mxEvent.getContent(); + if ( + content.msgtype && + content.msgtype !== 'm.bad.encrypted' && + content.hasOwnProperty('body') + ) { + replyButton = ; + } + } + return
+ {replyButton}
; }