diff --git a/res/css/_components.scss b/res/css/_components.scss
index fa388c4e6a..fac149380e 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -118,6 +118,7 @@
@import "./views/messages/_MessageActionBar.scss";
@import "./views/messages/_MessageTimestamp.scss";
@import "./views/messages/_ReactionDimension.scss";
+@import "./views/messages/_ReactionTooltipButton.scss";
@import "./views/messages/_ReactionsRow.scss";
@import "./views/messages/_ReactionsRowButton.scss";
@import "./views/messages/_ReactionsRowButtonTooltip.scss";
diff --git a/res/css/views/messages/_ReactionTooltipButton.scss b/res/css/views/messages/_ReactionTooltipButton.scss
new file mode 100644
index 0000000000..7cb754a8fd
--- /dev/null
+++ b/res/css/views/messages/_ReactionTooltipButton.scss
@@ -0,0 +1,24 @@
+/*
+Copyright 2019 The Matrix.org Foundation C.I.C.
+
+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_ReactionTooltipButton {
+ font-size: 16px;
+ user-select: none;
+}
+
+.mx_ReactionTooltipButton_selected {
+ opacity: 0.4;
+}
diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js
index aaedf46200..410c0762b3 100644
--- a/src/components/views/messages/MessageActionBar.js
+++ b/src/components/views/messages/MessageActionBar.js
@@ -136,8 +136,13 @@ export default class MessageActionBar extends React.PureComponent {
return null;
}
- return ;
}
diff --git a/src/components/views/messages/ReactMessageAction.js b/src/components/views/messages/ReactMessageAction.js
new file mode 100644
index 0000000000..804a154c9c
--- /dev/null
+++ b/src/components/views/messages/ReactMessageAction.js
@@ -0,0 +1,97 @@
+/*
+Copyright 2019 The Matrix.org Foundation C.I.C.
+
+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 sdk from '../../../index';
+
+export default class ReactMessageAction extends React.PureComponent {
+ static propTypes = {
+ mxEvent: PropTypes.object.isRequired,
+ // The Relations model from the JS SDK for reactions to `mxEvent`
+ reactions: PropTypes.object,
+ onFocusChange: PropTypes.func,
+ }
+
+ constructor(props) {
+ super(props);
+
+ if (props.reactions) {
+ props.reactions.on("Relations.add", this.onReactionsChange);
+ props.reactions.on("Relations.remove", this.onReactionsChange);
+ props.reactions.on("Relations.redaction", this.onReactionsChange);
+ }
+ }
+
+ onFocusChange = (focused) => {
+ if (!this.props.onFocusChange) {
+ return;
+ }
+ this.props.onFocusChange(focused);
+ }
+
+ componentDidUpdate(prevProps) {
+ if (prevProps.reactions !== this.props.reactions) {
+ this.props.reactions.on("Relations.add", this.onReactionsChange);
+ this.props.reactions.on("Relations.remove", this.onReactionsChange);
+ this.props.reactions.on("Relations.redaction", this.onReactionsChange);
+ this.onReactionsChange();
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.props.reactions) {
+ this.props.reactions.removeListener(
+ "Relations.add",
+ this.onReactionsChange,
+ );
+ this.props.reactions.removeListener(
+ "Relations.remove",
+ this.onReactionsChange,
+ );
+ this.props.reactions.removeListener(
+ "Relations.redaction",
+ this.onReactionsChange,
+ );
+ }
+ }
+
+ onReactionsChange = () => {
+ // Force a re-render of the tooltip because a change in the reactions
+ // set means the event tile's layout may have changed and possibly
+ // altered the location where the tooltip should be shown.
+ this.forceUpdate();
+ }
+
+ render() {
+ const ReactionsQuickTooltip = sdk.getComponent('messages.ReactionsQuickTooltip');
+ const InteractiveTooltip = sdk.getComponent('elements.InteractiveTooltip');
+ const { mxEvent, reactions } = this.props;
+
+ const content = ;
+
+ return
+
+ ;
+ }
+}
diff --git a/src/components/views/messages/ReactionTooltipButton.js b/src/components/views/messages/ReactionTooltipButton.js
new file mode 100644
index 0000000000..a3f0256758
--- /dev/null
+++ b/src/components/views/messages/ReactionTooltipButton.js
@@ -0,0 +1,67 @@
+/*
+Copyright 2019 The Matrix.org Foundation C.I.C.
+
+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';
+
+import MatrixClientPeg from '../../../MatrixClientPeg';
+
+export default class ReactionTooltipButton extends React.PureComponent {
+ static propTypes = {
+ mxEvent: PropTypes.object.isRequired,
+ // The reaction content / key / emoji
+ content: PropTypes.string.isRequired,
+ title: PropTypes.string,
+ // A possible Matrix event if the current user has voted for this type
+ myReactionEvent: PropTypes.object,
+ };
+
+ onClick = (ev) => {
+ const { mxEvent, myReactionEvent, content } = this.props;
+ if (myReactionEvent) {
+ MatrixClientPeg.get().redactEvent(
+ mxEvent.getRoomId(),
+ myReactionEvent.getId(),
+ );
+ } else {
+ MatrixClientPeg.get().sendEvent(mxEvent.getRoomId(), "m.reaction", {
+ "m.relates_to": {
+ "rel_type": "m.annotation",
+ "event_id": mxEvent.getId(),
+ "key": content,
+ },
+ });
+ }
+ }
+
+ render() {
+ const { content, myReactionEvent } = this.props;
+
+ const classes = classNames({
+ mx_ReactionTooltipButton: true,
+ mx_ReactionTooltipButton_selected: !!myReactionEvent,
+ });
+
+ return
+ {content}
+ ;
+ }
+}
diff --git a/src/components/views/messages/ReactionsQuickTooltip.js b/src/components/views/messages/ReactionsQuickTooltip.js
new file mode 100644
index 0000000000..74cfe82baf
--- /dev/null
+++ b/src/components/views/messages/ReactionsQuickTooltip.js
@@ -0,0 +1,151 @@
+/*
+Copyright 2019 The Matrix.org Foundation C.I.C.
+
+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 MatrixClientPeg from '../../../MatrixClientPeg';
+
+export default class ReactionsQuickTooltip extends React.PureComponent {
+ static propTypes = {
+ mxEvent: PropTypes.object.isRequired,
+ // The Relations model from the JS SDK for reactions to `mxEvent`
+ reactions: PropTypes.object,
+ };
+
+ constructor(props) {
+ super(props);
+
+ if (props.reactions) {
+ props.reactions.on("Relations.add", this.onReactionsChange);
+ props.reactions.on("Relations.remove", this.onReactionsChange);
+ props.reactions.on("Relations.redaction", this.onReactionsChange);
+ }
+
+ this.state = {
+ myReactions: this.getMyReactions(),
+ };
+ }
+
+ componentDidUpdate(prevProps) {
+ if (prevProps.reactions !== this.props.reactions) {
+ this.props.reactions.on("Relations.add", this.onReactionsChange);
+ this.props.reactions.on("Relations.remove", this.onReactionsChange);
+ this.props.reactions.on("Relations.redaction", this.onReactionsChange);
+ this.onReactionsChange();
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.props.reactions) {
+ this.props.reactions.removeListener(
+ "Relations.add",
+ this.onReactionsChange,
+ );
+ this.props.reactions.removeListener(
+ "Relations.remove",
+ this.onReactionsChange,
+ );
+ this.props.reactions.removeListener(
+ "Relations.redaction",
+ this.onReactionsChange,
+ );
+ }
+ }
+
+ onReactionsChange = () => {
+ this.setState({
+ myReactions: this.getMyReactions(),
+ });
+ }
+
+ getMyReactions() {
+ const reactions = this.props.reactions;
+ if (!reactions) {
+ return null;
+ }
+ const userId = MatrixClientPeg.get().getUserId();
+ const myReactions = reactions.getAnnotationsBySender()[userId];
+ if (!myReactions) {
+ return null;
+ }
+ return [...myReactions.values()];
+ }
+
+ render() {
+ const { mxEvent } = this.props;
+ const { myReactions } = this.state;
+ const ReactionTooltipButton = sdk.getComponent('messages.ReactionTooltipButton');
+
+ const items = [
+ {
+ content: "👍",
+ title: _t("Agree"),
+ },
+ {
+ content: "👎",
+ title: _t("Disagree"),
+ },
+ {
+ content: "😄",
+ title: _t("Happy"),
+ },
+ {
+ content: "🎉",
+ title: _t("Party Popper"),
+ },
+ {
+ content: "😕",
+ title: _t("Confused"),
+ },
+ {
+ content: "❤️",
+ title: _t("Heart"),
+ },
+ {
+ content: "🚀",
+ title: _t("Rocket"),
+ },
+ {
+ content: "👀",
+ title: _t("Eyes"),
+ },
+ ];
+
+ const buttons = items.map(({ content, title }) => {
+ const myReactionEvent = myReactions && myReactions.find(mxEvent => {
+ if (mxEvent.isRedacted()) {
+ return false;
+ }
+ return mxEvent.getRelation().key === content;
+ });
+
+ return ;
+ });
+
+ return
+ {buttons}
+
;
+ }
+}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 9564c45172..02c0658bcb 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -930,7 +930,6 @@
"Error decrypting audio": "Error decrypting audio",
"Agree or Disagree": "Agree or Disagree",
"Like or Dislike": "Like or Dislike",
- "React": "React",
"Reply": "Reply",
"Edit": "Edit",
"Options": "Options",
@@ -941,6 +940,12 @@
"Invalid file%(extra)s": "Invalid file%(extra)s",
"Error decrypting image": "Error decrypting image",
"Error decrypting video": "Error decrypting video",
+ "Agree": "Agree",
+ "Disagree": "Disagree",
+ "Happy": "Happy",
+ "Party Popper": "Party Popper",
+ "Confused": "Confused",
+ "Eyes": "Eyes",
"reacted with %(shortName)s": "reacted with %(shortName)s",
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s changed the avatar for %(roomName)s",
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s removed the room avatar.",