summaryrefslogtreecommitdiffstats
path: root/webapp/components/post_view/reaction
diff options
context:
space:
mode:
Diffstat (limited to 'webapp/components/post_view/reaction')
-rw-r--r--webapp/components/post_view/reaction/index.js47
-rw-r--r--webapp/components/post_view/reaction/reaction.jsx239
2 files changed, 286 insertions, 0 deletions
diff --git a/webapp/components/post_view/reaction/index.js b/webapp/components/post_view/reaction/index.js
new file mode 100644
index 000000000..9bb2524a1
--- /dev/null
+++ b/webapp/components/post_view/reaction/index.js
@@ -0,0 +1,47 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {connect} from 'react-redux';
+import {bindActionCreators} from 'redux';
+import {getCurrentUserId, makeGetProfilesForReactions} from 'mattermost-redux/selectors/entities/users';
+import {getMissingProfilesByIds} from 'mattermost-redux/actions/users';
+import {addReaction, removeReaction} from 'mattermost-redux/actions/posts';
+import {getEmojiImageUrl} from 'mattermost-redux/utils/emoji_utils';
+import * as Emoji from 'utils/emoji.jsx';
+
+import Reaction from './reaction.jsx';
+
+function makeMapStateToProps() {
+ const getProfilesForReactions = makeGetProfilesForReactions();
+
+ return function mapStateToProps(state, ownProps) {
+ const profiles = getProfilesForReactions(state, ownProps.reactions);
+ let emoji;
+ if (Emoji.EmojiIndicesByAlias.has(ownProps.emojiName)) {
+ emoji = Emoji.Emojis[Emoji.EmojiIndicesByAlias.get(ownProps.emojiName)];
+ } else {
+ emoji = ownProps.emojis[ownProps.emojiName];
+ }
+
+ return {
+ ...ownProps,
+ profiles,
+ otherUsersCount: ownProps.reactions.length - profiles.length,
+ currentUserId: getCurrentUserId(state),
+ reactionCount: ownProps.reactions.length,
+ emojiImageUrl: getEmojiImageUrl(emoji)
+ };
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators({
+ addReaction,
+ removeReaction,
+ getMissingProfilesByIds
+ }, dispatch)
+ };
+}
+
+export default connect(makeMapStateToProps, mapDispatchToProps)(Reaction);
diff --git a/webapp/components/post_view/reaction/reaction.jsx b/webapp/components/post_view/reaction/reaction.jsx
new file mode 100644
index 000000000..5b65e604f
--- /dev/null
+++ b/webapp/components/post_view/reaction/reaction.jsx
@@ -0,0 +1,239 @@
+// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import {OverlayTrigger, Tooltip} from 'react-bootstrap';
+import {FormattedMessage} from 'react-intl';
+
+import * as Utils from 'utils/utils.jsx';
+
+export default class Reaction extends React.PureComponent {
+ static propTypes = {
+
+ /*
+ * The post to render the reaction for
+ */
+ post: PropTypes.object.isRequired,
+
+ /*
+ * The user id of the logged in user
+ */
+ currentUserId: PropTypes.string.isRequired,
+
+ /*
+ * The name of the emoji for the reaction
+ */
+ emojiName: PropTypes.string.isRequired,
+
+ /*
+ * The number of reactions to this post for this emoji
+ */
+ reactionCount: PropTypes.number.isRequired,
+
+ /*
+ * Array of users who reacted to this post
+ */
+ profiles: PropTypes.array.isRequired,
+
+ /*
+ * The number of users not in the profile list who have reacted with this emoji
+ */
+ otherUsersCount: PropTypes.number.isRequired,
+
+ /*
+ * The URL of the emoji image
+ */
+ emojiImageUrl: PropTypes.string.isRequired,
+
+ actions: PropTypes.shape({
+
+ /*
+ * Function to add a reaction to a post
+ */
+ addReaction: PropTypes.func.isRequired,
+
+ /*
+ * Function to get non-loaded profiles by id
+ */
+ getMissingProfilesByIds: PropTypes.func.isRequired,
+
+ /*
+ * Function to remove a reaction from a post
+ */
+ removeReaction: PropTypes.func.isRequired
+ })
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.addReaction = this.addReaction.bind(this);
+ this.removeReaction = this.removeReaction.bind(this);
+ }
+
+ addReaction(e) {
+ e.preventDefault();
+ this.props.actions.addReaction(this.props.post.id, this.props.emojiName);
+ }
+
+ removeReaction(e) {
+ e.preventDefault();
+ this.props.actions.removeReaction(this.props.post.id, this.props.emojiName);
+ }
+
+ render() {
+ let currentUserReacted = false;
+ const users = [];
+ const otherUsersCount = this.props.otherUsersCount;
+ for (const user of this.props.profiles) {
+ if (user.id === this.props.currentUserId) {
+ currentUserReacted = true;
+ } else {
+ users.push(Utils.displayUsernameForUser(user));
+ }
+ }
+
+ // Sort users in alphabetical order with "you" being first if the current user reacted
+ users.sort();
+ if (currentUserReacted) {
+ users.unshift(Utils.localizeMessage('reaction.you', 'You'));
+ }
+
+ let names;
+ if (otherUsersCount > 0) {
+ if (users.length > 0) {
+ names = (
+ <FormattedMessage
+ id='reaction.usersAndOthersReacted'
+ defaultMessage='{users} and {otherUsers, number} other {otherUsers, plural, one {user} other {users}}'
+ values={{
+ users: users.join(', '),
+ otherUsers: otherUsersCount
+ }}
+ />
+ );
+ } else {
+ names = (
+ <FormattedMessage
+ id='reaction.othersReacted'
+ defaultMessage='{otherUsers, number} {otherUsers, plural, one {user} other {users}}'
+ values={{
+ otherUsers: otherUsersCount
+ }}
+ />
+ );
+ }
+ } else if (users.length > 1) {
+ names = (
+ <FormattedMessage
+ id='reaction.usersReacted'
+ defaultMessage='{users} and {lastUser}'
+ values={{
+ users: users.slice(0, -1).join(', '),
+ lastUser: users[users.length - 1]
+ }}
+ />
+ );
+ } else {
+ names = users[0];
+ }
+
+ let reactionVerb;
+ if (users.length + otherUsersCount > 1) {
+ if (currentUserReacted) {
+ reactionVerb = (
+ <FormattedMessage
+ id='reaction.reactionVerb.youAndUsers'
+ defaultMessage='reacted'
+ />
+ );
+ } else {
+ reactionVerb = (
+ <FormattedMessage
+ id='reaction.reactionVerb.users'
+ defaultMessage='reacted'
+ />
+ );
+ }
+ } else if (currentUserReacted) {
+ reactionVerb = (
+ <FormattedMessage
+ id='reaction.reactionVerb.you'
+ defaultMessage='reacted'
+ />
+ );
+ } else {
+ reactionVerb = (
+ <FormattedMessage
+ id='reaction.reactionVerb.user'
+ defaultMessage='reacted'
+ />
+ );
+ }
+
+ const tooltip = (
+ <FormattedMessage
+ id='reaction.reacted'
+ defaultMessage='{users} {reactionVerb} with {emoji}'
+ values={{
+ users: <b>{names}</b>,
+ reactionVerb,
+ emoji: <b>{':' + this.props.emojiName + ':'}</b>
+ }}
+ />
+ );
+
+ let handleClick;
+ let clickTooltip;
+ let className = 'post-reaction';
+ if (currentUserReacted) {
+ handleClick = this.removeReaction;
+ clickTooltip = (
+ <FormattedMessage
+ id='reaction.clickToRemove'
+ defaultMessage='(click to remove)'
+ />
+ );
+
+ className += ' post-reaction--current-user';
+ } else {
+ handleClick = this.addReaction;
+ clickTooltip = (
+ <FormattedMessage
+ id='reaction.clickToAdd'
+ defaultMessage='(click to add)'
+ />
+ );
+ }
+
+ return (
+ <OverlayTrigger
+ delayShow={1000}
+ placement='top'
+ shouldUpdatePosition={true}
+ overlay={
+ <Tooltip id={`${this.props.post.id}-${this.props.emojiName}-reaction`}>
+ {tooltip}
+ <br/>
+ {clickTooltip}
+ </Tooltip>
+ }
+ onEnter={this.props.actions.getMissingProfilesByIds}
+ >
+ <div
+ className={className}
+ onClick={handleClick}
+ >
+ <span
+ className='post-reaction__emoji emoticon'
+ style={{backgroundImage: 'url(' + this.props.emojiImageUrl + ')'}}
+ />
+ <span className='post-reaction__count'>
+ {this.props.reactionCount}
+ </span>
+ </div>
+ </OverlayTrigger>
+ );
+ }
+}