Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Send and undo reaction events #2954

Merged
merged 14 commits into from
May 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions res/css/views/messages/_ReactionsRowButton.scss
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ limitations under the License.
border-radius: 10px;
background-color: $reaction-row-button-bg-color;
cursor: pointer;
user-select: none;

&:hover {
border-color: $reaction-row-button-hover-border-color;
Expand Down
5 changes: 4 additions & 1 deletion src/MatrixClientPeg.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ class MatrixClientPeg {
}

_createClient(creds: MatrixClientCreds) {
const aggregateRelations = SettingsStore.isFeatureEnabled("feature_reactions");

const opts = {
baseUrl: creds.homeserverUrl,
idBaseUrl: creds.identityServerUrl,
Expand All @@ -183,7 +185,8 @@ class MatrixClientPeg {
deviceId: creds.deviceId,
timelineSupport: true,
forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer', false),
verificationMethods: [verificationMethods.SAS]
verificationMethods: [verificationMethods.SAS],
unstableClientRelationAggregation: aggregateRelations,
};

this.matrixClient = createMatrixClient(opts);
Expand Down
40 changes: 24 additions & 16 deletions src/components/structures/MessagePanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ module.exports = React.createClass({

// show timestamps always
alwaysShowTimestamps: PropTypes.bool,

// helper function to access relations for an event
getRelationsForEvent: PropTypes.func,
},

componentWillMount: function() {
Expand Down Expand Up @@ -511,22 +514,27 @@ module.exports = React.createClass({
readReceipts = this._getReadReceiptsForEvent(mxEv);
}
ret.push(
<li key={eventId}
ref={this._collectEventNode.bind(this, eventId)}
data-scroll-tokens={scrollToken}>
<EventTile mxEvent={mxEv} continuation={continuation}
isRedacted={mxEv.isRedacted()}
onHeightChanged={this._onHeightChanged}
readReceipts={readReceipts}
readReceiptMap={this._readReceiptMap}
showUrlPreview={this.props.showUrlPreview}
checkUnmounting={this._isUnmounting}
eventSendStatus={mxEv.status}
tileShape={this.props.tileShape}
isTwelveHour={this.props.isTwelveHour}
permalinkCreator={this.props.permalinkCreator}
last={last} isSelectedEvent={highlight} />
</li>,
<li key={eventId}
ref={this._collectEventNode.bind(this, eventId)}
data-scroll-tokens={scrollToken}
>
<EventTile mxEvent={mxEv}
continuation={continuation}
isRedacted={mxEv.isRedacted()}
onHeightChanged={this._onHeightChanged}
readReceipts={readReceipts}
readReceiptMap={this._readReceiptMap}
showUrlPreview={this.props.showUrlPreview}
checkUnmounting={this._isUnmounting}
eventSendStatus={mxEv.status}
tileShape={this.props.tileShape}
isTwelveHour={this.props.isTwelveHour}
permalinkCreator={this.props.permalinkCreator}
last={last}
isSelectedEvent={highlight}
getRelationsForEvent={this.props.getRelationsForEvent}
/>
</li>,
);

return ret;
Expand Down
55 changes: 30 additions & 25 deletions src/components/structures/TimelinePanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -1168,6 +1168,10 @@ const TimelinePanel = React.createClass({
});
},

getRelationsForEvent(...args) {
return this.props.timelineSet.getRelationsForEvent(...args);
},

render: function() {
const MessagePanel = sdk.getComponent("structures.MessagePanel");
const Loader = sdk.getComponent("elements.Spinner");
Expand All @@ -1193,9 +1197,9 @@ const TimelinePanel = React.createClass({

if (this.state.events.length == 0 && !this.state.canBackPaginate && this.props.empty) {
return (
<div className={this.props.className + " mx_RoomView_messageListWrapper"}>
<div className="mx_RoomView_empty">{ this.props.empty }</div>
</div>
<div className={this.props.className + " mx_RoomView_messageListWrapper"}>
<div className="mx_RoomView_empty">{this.props.empty}</div>
</div>
);
}

Expand All @@ -1217,28 +1221,29 @@ const TimelinePanel = React.createClass({
);
return (
<MessagePanel ref="messagePanel"
room={this.props.timelineSet.room}
permalinkCreator={this.props.permalinkCreator}
hidden={this.props.hidden}
backPaginating={this.state.backPaginating}
forwardPaginating={forwardPaginating}
events={this.state.events}
highlightedEventId={this.props.highlightedEventId}
readMarkerEventId={this.state.readMarkerEventId}
readMarkerVisible={this.state.readMarkerVisible}
suppressFirstDateSeparator={this.state.canBackPaginate}
showUrlPreview={this.props.showUrlPreview}
showReadReceipts={this.props.showReadReceipts}
ourUserId={MatrixClientPeg.get().credentials.userId}
stickyBottom={stickyBottom}
onScroll={this.onMessageListScroll}
onFillRequest={this.onMessageListFillRequest}
onUnfillRequest={this.onMessageListUnfillRequest}
isTwelveHour={this.state.isTwelveHour}
alwaysShowTimestamps={this.state.alwaysShowTimestamps}
className={this.props.className}
tileShape={this.props.tileShape}
resizeNotifier={this.props.resizeNotifier}
room={this.props.timelineSet.room}
permalinkCreator={this.props.permalinkCreator}
hidden={this.props.hidden}
backPaginating={this.state.backPaginating}
forwardPaginating={forwardPaginating}
events={this.state.events}
highlightedEventId={this.props.highlightedEventId}
readMarkerEventId={this.state.readMarkerEventId}
readMarkerVisible={this.state.readMarkerVisible}
suppressFirstDateSeparator={this.state.canBackPaginate}
showUrlPreview={this.props.showUrlPreview}
showReadReceipts={this.props.showReadReceipts}
ourUserId={MatrixClientPeg.get().credentials.userId}
stickyBottom={stickyBottom}
onScroll={this.onMessageListScroll}
onFillRequest={this.onMessageListFillRequest}
onUnfillRequest={this.onMessageListUnfillRequest}
isTwelveHour={this.state.isTwelveHour}
alwaysShowTimestamps={this.state.alwaysShowTimestamps}
className={this.props.className}
tileShape={this.props.tileShape}
resizeNotifier={this.props.resizeNotifier}
getRelationsForEvent={this.getRelationsForEvent}
/>
);
},
Expand Down
30 changes: 8 additions & 22 deletions src/components/views/messages/MessageActionBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import { isContentActionable } from '../../../utils/EventUtils';
export default class MessageActionBar extends React.PureComponent {
static propTypes = {
mxEvent: PropTypes.object.isRequired,
// The Relations model from the JS SDK for reactions to `mxEvent`
reactions: PropTypes.object,
permalinkCreator: PropTypes.object,
getTile: PropTypes.func,
getReplyThread: PropTypes.func,
Expand Down Expand Up @@ -100,19 +102,11 @@ export default class MessageActionBar extends React.PureComponent {
}

const ReactionDimension = sdk.getComponent('messages.ReactionDimension');
const options = [
{
key: "agree",
content: "👍",
},
{
key: "disagree",
content: "👎",
},
];
return <ReactionDimension
title={_t("Agree or Disagree")}
options={options}
options={["👍", "👎"]}
reactions={this.props.reactions}
mxEvent={this.props.mxEvent}
/>;
}

Expand All @@ -122,19 +116,11 @@ export default class MessageActionBar extends React.PureComponent {
}

const ReactionDimension = sdk.getComponent('messages.ReactionDimension');
const options = [
{
key: "like",
content: "🙂",
},
{
key: "dislike",
content: "😔",
},
];
return <ReactionDimension
title={_t("Like or Dislike")}
options={options}
options={["🙂", "😔"]}
reactions={this.props.reactions}
mxEvent={this.props.mxEvent}
/>;
}

Expand Down
120 changes: 106 additions & 14 deletions src/components/views/messages/ReactionDimension.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,49 +18,141 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';

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,
// The Relations model from the JS SDK for reactions
reactions: PropTypes.object,
};

constructor(props) {
super(props);

this.state = {
selected: null,
};
this.state = this.getSelection();

if (props.reactions) {
props.reactions.on("Relations.add", this.onReactionsChange);
props.reactions.on("Relations.redaction", this.onReactionsChange);
}
}

componentDidUpdate(prevProps) {
if (prevProps.reactions !== this.props.reactions) {
this.props.reactions.on("Relations.add", 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.redaction",
this.onReactionsChange,
);
}
}

onReactionsChange = () => {
this.setState(this.getSelection());
}

getSelection() {
const myReactions = this.getMyReactions();
if (!myReactions) {
return {
selectedOption: null,
selectedReactionEvent: null,
};
}
const { options } = this.props;
let selectedOption = null;
let selectedReactionEvent = null;
for (const option of options) {
const reactionForOption = myReactions.find(mxEvent => {
if (mxEvent.isRedacted()) {
return false;
}
return mxEvent.getContent()["m.relates_to"].key === 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 { selectedOption, selectedReactionEvent };
}

getMyReactions() {
const reactions = this.props.reactions;
if (!reactions) {
return null;
}
const userId = MatrixClientPeg.get().getUserId();
return reactions.getAnnotationsBySender()[userId];
}

onOptionClick = (ev) => {
const { key } = ev.target.dataset;
this.toggleDimensionValue(key);
this.toggleDimension(key);
}

toggleDimensionValue(value) {
const state = this.state.selected;
const newState = state !== value ? value : null;
toggleDimension(key) {
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.key;
const disabled = selectedOption && selectedOption !== option;
const classes = classNames({
mx_ReactionDimension_disabled: disabled,
});
return <span key={option.key}
data-key={option.key}
return <span key={option}
data-key={option}
className={classes}
onClick={this.onOptionClick}
>
{option.content}
{option}
</span>;
});

Expand Down
Loading