From dc4fccd291a4e91874b767585d2f35b00a40e832 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 9 May 2019 19:11:40 +0100 Subject: [PATCH] Send and redact reaction events This updates both the reaction row and action bar UIs to send and redact reaction events as appropriate based on user interactions. Fixes https://github.com/vector-im/riot-web/issues/9574 Fixes https://github.com/vector-im/riot-web/issues/9572 --- .../views/messages/MessageActionBar.js | 2 + .../views/messages/ReactionDimension.js | 66 ++++++++++++------- src/components/views/messages/ReactionsRow.js | 26 ++++++++ .../views/messages/ReactionsRowButton.js | 48 +++++++------- 4 files changed, 95 insertions(+), 47 deletions(-) diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 903abfc2d2..52630d7b0e 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -106,6 +106,7 @@ export default class MessageActionBar extends React.PureComponent { title={_t("Agree or Disagree")} options={["👍", "👎"]} reactions={this.props.reactions} + mxEvent={this.props.mxEvent} />; } @@ -119,6 +120,7 @@ export default class MessageActionBar extends React.PureComponent { title={_t("Like or Dislike")} options={["🙂", "😔"]} reactions={this.props.reactions} + mxEvent={this.props.mxEvent} />; } diff --git a/src/components/views/messages/ReactionDimension.js b/src/components/views/messages/ReactionDimension.js index 13d43804ac..6e8b8ddc99 100644 --- a/src/components/views/messages/ReactionDimension.js +++ b/src/components/views/messages/ReactionDimension.js @@ -22,6 +22,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg'; export default class ReactionDimension extends React.PureComponent { static propTypes = { + mxEvent: PropTypes.object.isRequired, // Array of strings containing the emoji for each option options: PropTypes.array.isRequired, title: PropTypes.string, @@ -32,9 +33,7 @@ export default class ReactionDimension extends React.PureComponent { constructor(props) { super(props); - this.state = { - selected: this.getSelection(), - }; + this.state = this.getSelection(); if (props.reactions) { props.reactions.on("Relations.add", this.onReactionsChange); @@ -63,35 +62,42 @@ export default class ReactionDimension extends React.PureComponent { } onReactionsChange = () => { - this.setState({ - selected: this.getSelection(), - }); + this.setState(this.getSelection()); } getSelection() { const myReactions = this.getMyReactions(); if (!myReactions) { - return null; + return { + selectedOption: null, + selectedReactionEvent: null, + }; } const { options } = this.props; - let selected = null; + let selectedOption = null; + let selectedReactionEvent = null; for (const option of options) { - const reactionExists = myReactions.some(mxEvent => { + const reactionForOption = myReactions.find(mxEvent => { if (mxEvent.isRedacted()) { return false; } return mxEvent.getContent()["m.relates_to"].key === option; }); - if (reactionExists) { - if (selected) { - // If there are multiple selected values (only expected to occur via - // non-Riot clients), then act as if none are selected. - return null; - } - selected = option; + if (!reactionForOption) { + continue; } + if (selectedOption) { + // If there are multiple selected values (only expected to occur via + // non-Riot clients), then act as if none are selected. + return { + selectedOption: null, + selectedReactionEvent: null, + }; + } + selectedOption = option; + selectedReactionEvent = reactionForOption; } - return selected; + return { selectedOption, selectedReactionEvent }; } getMyReactions() { @@ -109,20 +115,34 @@ export default class ReactionDimension extends React.PureComponent { } toggleDimension(key) { - const state = this.state.selected; - const newState = state !== key ? key : null; + const { selectedOption, selectedReactionEvent } = this.state; + const newSelectedOption = selectedOption !== key ? key : null; this.setState({ - selected: newState, + selectedOption: newSelectedOption, }); - // TODO: Send the reaction event + if (selectedReactionEvent) { + MatrixClientPeg.get().redactEvent( + this.props.mxEvent.getRoomId(), + selectedReactionEvent.getId(), + ); + } + if (newSelectedOption) { + MatrixClientPeg.get().sendEvent(this.props.mxEvent.getRoomId(), "m.reaction", { + "m.relates_to": { + "rel_type": "m.annotation", + "event_id": this.props.mxEvent.getId(), + "key": newSelectedOption, + }, + }); + } } render() { - const { selected } = this.state; + const { selectedOption } = this.state; const { options } = this.props; const items = options.map(option => { - const disabled = selected && selected !== option; + const disabled = selectedOption && selectedOption !== option; const classes = classNames({ mx_ReactionDimension_disabled: disabled, }); diff --git a/src/components/views/messages/ReactionsRow.js b/src/components/views/messages/ReactionsRow.js index c49f5cba75..3172b8b44c 100644 --- a/src/components/views/messages/ReactionsRow.js +++ b/src/components/views/messages/ReactionsRow.js @@ -19,6 +19,7 @@ import PropTypes from 'prop-types'; import sdk from '../../../index'; import { isContentActionable } from '../../../utils/EventUtils'; +import MatrixClientPeg from '../../../MatrixClientPeg'; export default class ReactionsRow extends React.PureComponent { static propTypes = { @@ -35,6 +36,10 @@ export default class ReactionsRow extends React.PureComponent { props.reactions.on("Relations.add", this.onReactionsChange); props.reactions.on("Relations.redaction", this.onReactionsChange); } + + this.state = { + myReactions: this.getMyReactions(), + }; } componentWillReceiveProps(nextProps) { @@ -59,11 +64,24 @@ export default class ReactionsRow extends React.PureComponent { onReactionsChange = () => { // TODO: Call `onHeightChanged` as needed + this.setState({ + myReactions: this.getMyReactions(), + }); this.forceUpdate(); } + getMyReactions() { + const reactions = this.props.reactions; + if (!reactions) { + return null; + } + const userId = MatrixClientPeg.get().getUserId(); + return reactions.getAnnotationsBySender()[userId]; + } + render() { const { mxEvent, reactions } = this.props; + const { myReactions } = this.state; if (!reactions || !isContentActionable(mxEvent)) { return null; @@ -75,10 +93,18 @@ export default class ReactionsRow extends React.PureComponent { if (!count) { return null; } + const myReactionEvent = myReactions && myReactions.find(mxEvent => { + if (mxEvent.isRedacted()) { + return false; + } + return mxEvent.getContent()["m.relates_to"].key === content; + }); return ; }); diff --git a/src/components/views/messages/ReactionsRowButton.js b/src/components/views/messages/ReactionsRowButton.js index 985479a237..721147cdb8 100644 --- a/src/components/views/messages/ReactionsRowButton.js +++ b/src/components/views/messages/ReactionsRowButton.js @@ -18,48 +18,48 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; +import MatrixClientPeg from '../../../MatrixClientPeg'; + export default class ReactionsRowButton extends React.PureComponent { static propTypes = { + // The event we're displaying reactions for + mxEvent: PropTypes.object.isRequired, 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, - }; + // A possible Matrix event if the current user has voted for this type + myReactionEvent: PropTypes.object, } onClick = (ev) => { - const state = this.state.selected; - this.setState({ - selected: !state, - }); - // TODO: Send the reaction event + 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, count } = this.props; - const { selected } = this.state; + const { content, count, myReactionEvent } = this.props; const classes = classNames({ mx_ReactionsRowButton: true, - mx_ReactionsRowButton_selected: selected, + mx_ReactionsRowButton_selected: !!myReactionEvent, }); - let adjustedCount = count; - if (selected) { - adjustedCount++; - } - return - {content} {adjustedCount} + {content} {count} ; } }