From fd2723585fb22d08f202652d4971c10a85106480 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 25 Jun 2019 15:10:41 +0100 Subject: [PATCH] Add quick reaction buttons in tooltip This adds the set of quick reactions as buttons in a new tooltip accessed via the react action in the message action bar. Part of https://github.com/vector-im/riot-web/issues/9753 --- res/css/_components.scss | 1 + .../messages/_ReactionTooltipButton.scss | 24 +++ .../views/messages/MessageActionBar.js | 9 +- .../views/messages/ReactMessageAction.js | 97 +++++++++++ .../views/messages/ReactionTooltipButton.js | 67 ++++++++ .../views/messages/ReactionsQuickTooltip.js | 151 ++++++++++++++++++ src/i18n/strings/en_EN.json | 7 +- 7 files changed, 353 insertions(+), 3 deletions(-) create mode 100644 res/css/views/messages/_ReactionTooltipButton.scss create mode 100644 src/components/views/messages/ReactMessageAction.js create mode 100644 src/components/views/messages/ReactionTooltipButton.js create mode 100644 src/components/views/messages/ReactionsQuickTooltip.js 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.",