diff --git a/res/css/_components.scss b/res/css/_components.scss index bb09b873a3..36648f4982 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -115,6 +115,8 @@ @import "./views/messages/_MTextBody.scss"; @import "./views/messages/_MessageActionBar.scss"; @import "./views/messages/_MessageTimestamp.scss"; +@import "./views/messages/_ReactionsRow.scss"; +@import "./views/messages/_ReactionsRowButton.scss"; @import "./views/messages/_RoomAvatarEvent.scss"; @import "./views/messages/_SenderProfile.scss"; @import "./views/messages/_TextualEvent.scss"; diff --git a/res/css/views/messages/_ReactionsRow.scss b/res/css/views/messages/_ReactionsRow.scss new file mode 100644 index 0000000000..fb66ffbb8c --- /dev/null +++ b/res/css/views/messages/_ReactionsRow.scss @@ -0,0 +1,19 @@ +/* +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_ReactionsRow { + margin: 6px 0; +} diff --git a/res/css/views/messages/_ReactionsRowButton.scss b/res/css/views/messages/_ReactionsRowButton.scss new file mode 100644 index 0000000000..49e3930979 --- /dev/null +++ b/res/css/views/messages/_ReactionsRowButton.scss @@ -0,0 +1,36 @@ +/* +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_ReactionsRowButton { + display: inline-block; + height: 20px; + line-height: 21px; + margin-right: 6px; + padding: 0 6px; + border: 1px solid $reaction-row-button-border-color; + border-radius: 10px; + background-color: $reaction-row-button-bg-color; + cursor: pointer; + + &:hover { + border-color: $reaction-row-button-hover-border-color; + } + + &.mx_ReactionsRowButton_selected { + background-color: $reaction-row-button-selected-bg-color; + border-color: $reaction-row-button-selected-border-color; + } +} diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 7c0f8ef9ab..592b1a1887 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -151,6 +151,12 @@ $message-action-bar-fg-color: $header-panel-text-primary-color; $message-action-bar-border-color: #616b7f; $message-action-bar-hover-border-color: $header-panel-text-primary-color; +$reaction-row-button-bg-color: $header-panel-bg-color; +$reaction-row-button-border-color: #616b7f; +$reaction-row-button-hover-border-color: $header-panel-text-primary-color; +$reaction-row-button-selected-bg-color: #1f6954; +$reaction-row-button-selected-border-color: $accent-color; + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 7451a23991..adadd39333 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -259,6 +259,12 @@ $message-action-bar-fg-color: $primary-fg-color; $message-action-bar-border-color: #e9edf1; $message-action-bar-hover-border-color: #b8c1d2; +$reaction-row-button-bg-color: $header-panel-bg-color; +$reaction-row-button-border-color: #e9edf1; +$reaction-row-button-hover-border-color: #bebebe; +$reaction-row-button-selected-bg-color: #e9fff9; +$reaction-row-button-selected-border-color: $accent-color; + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index 1191b6d66e..2e4611f7d0 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -27,6 +27,7 @@ import Modal from '../../../Modal'; import Resend from '../../../Resend'; import SettingsStore from '../../../settings/SettingsStore'; import { isUrlPermitted } from '../../../HtmlUtils'; +import { isContentActionable } from '../../../utils/EventUtils'; module.exports = React.createClass({ displayName: 'MessageContextMenu', @@ -247,22 +248,19 @@ module.exports = React.createClass({ ); } - if (isSent && mxEvent.getType() === 'm.room.message') { - const content = mxEvent.getContent(); - if (content.msgtype && content.msgtype !== 'm.bad.encrypted' && content.hasOwnProperty('body')) { - forwardButton = ( -
- { _t('Forward Message') } + if (isContentActionable(mxEvent)) { + forwardButton = ( +
+ { _t('Forward Message') } +
+ ); + + if (this.state.canPin) { + pinButton = ( +
+ { this._isPinned() ? _t('Unpin Message') : _t('Pin Message') }
); - - if (this.state.canPin) { - pinButton = ( -
- { this._isPinned() ? _t('Unpin Message') : _t('Pin Message') } -
- ); - } } } diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 276a142ccb..76bae137f5 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -16,7 +16,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import {EventStatus} from 'matrix-js-sdk'; +import classNames from 'classnames'; import { _t } from '../../../languageHandler'; import sdk from '../../../index'; @@ -24,7 +24,7 @@ import dis from '../../../dispatcher'; import Modal from '../../../Modal'; import { createMenu } from '../../structures/ContextualMenu'; import SettingsStore from '../../../settings/SettingsStore'; -import classNames from 'classnames'; +import { isContentActionable } from '../../../utils/EventUtils'; export default class MessageActionBar extends React.PureComponent { static propTypes = { @@ -123,27 +123,6 @@ export default class MessageActionBar extends React.PureComponent { this.onFocusChange(true); } - isContentActionable() { - const { mxEvent } = this.props; - const { status: eventStatus } = mxEvent; - - // status is SENT before remote-echo, null after - const isSent = !eventStatus || eventStatus === EventStatus.SENT; - - if (isSent && mxEvent.getType() === 'm.room.message') { - const content = mxEvent.getContent(); - if ( - content.msgtype && - content.msgtype !== 'm.bad.encrypted' && - content.hasOwnProperty('body') - ) { - return true; - } - } - - return false; - } - isReactionsEnabled() { return SettingsStore.isFeatureEnabled("feature_reactions"); } @@ -220,7 +199,7 @@ export default class MessageActionBar extends React.PureComponent { let likeDimensionReactionButtons; let replyButton; - if (this.isContentActionable()) { + if (isContentActionable(this.props.mxEvent)) { agreeDimensionReactionButtons = this.renderAgreeDimension(); likeDimensionReactionButtons = this.renderLikeDimension(); replyButton = { + return ; + }); + + return
+ {items} +
; + } +} diff --git a/src/components/views/messages/ReactionsRowButton.js b/src/components/views/messages/ReactionsRowButton.js new file mode 100644 index 0000000000..985479a237 --- /dev/null +++ b/src/components/views/messages/ReactionsRowButton.js @@ -0,0 +1,65 @@ +/* +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 classNames from 'classnames'; + +export default class ReactionsRowButton extends React.PureComponent { + static propTypes = { + content: PropTypes.string.isRequired, + count: PropTypes.number.isRequired, + } + + constructor(props) { + super(props); + + // TODO: This should be derived from actual reactions you may have sent + // once we have some API to read them. + this.state = { + selected: false, + }; + } + + onClick = (ev) => { + const state = this.state.selected; + this.setState({ + selected: !state, + }); + // TODO: Send the reaction event + }; + + render() { + const { content, count } = this.props; + const { selected } = this.state; + + const classes = classNames({ + mx_ReactionsRowButton: true, + mx_ReactionsRowButton_selected: selected, + }); + + let adjustedCount = count; + if (selected) { + adjustedCount++; + } + + return + {content} {adjustedCount} + ; + } +} diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index dd0a7aa47b..6bec3f4fff 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -622,6 +622,14 @@ module.exports = withMatrixClient(React.createClass({
: null; + let reactions; + if (SettingsStore.isFeatureEnabled("feature_reactions")) { + const ReactionsRow = sdk.getComponent('messages.ReactionsRow'); + reactions = ; + } + switch (this.props.tileShape) { case 'notif': { const EmojiText = sdk.getComponent('elements.EmojiText'); @@ -734,6 +742,7 @@ module.exports = withMatrixClient(React.createClass({ showUrlPreview={this.props.showUrlPreview} onHeightChanged={this.props.onHeightChanged} /> { keyRequestInfo } + { reactions } { actionBar } { diff --git a/src/utils/EventUtils.js b/src/utils/EventUtils.js new file mode 100644 index 0000000000..911257f95c --- /dev/null +++ b/src/utils/EventUtils.js @@ -0,0 +1,45 @@ +/* +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 { EventStatus } from 'matrix-js-sdk'; + +/** + * Returns whether an event should allow actions like reply, reactions, edit, etc. + * which effectively checks whether it's a regular message that has been sent and that we + * can display. + * + * @param {MatrixEvent} mxEvent The event to check + * @returns {boolean} true if actionable + */ +export function isContentActionable(mxEvent) { + const { status: eventStatus } = mxEvent; + + // status is SENT before remote-echo, null after + const isSent = !eventStatus || eventStatus === EventStatus.SENT; + + if (isSent && mxEvent.getType() === 'm.room.message') { + const content = mxEvent.getContent(); + if ( + content.msgtype && + content.msgtype !== 'm.bad.encrypted' && + content.hasOwnProperty('body') + ) { + return true; + } + } + + return false; +}