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;
+}