diff --git a/app/components/emoji/emoji.js b/app/components/emoji/emoji.js index 0f5b64c57..33e80b2af 100644 --- a/app/components/emoji/emoji.js +++ b/app/components/emoji/emoji.js @@ -15,13 +15,15 @@ export default class Emoji extends React.PureComponent { customEmojis: PropTypes.object, emojiName: PropTypes.string.isRequired, literal: PropTypes.string, + padding: PropTypes.number, size: PropTypes.number.isRequired, textStyle: CustomPropTypes.Style } static defaultProps = { customEmojis: new Map(), - literal: '' + literal: '', + padding: 10 } render() { @@ -29,6 +31,7 @@ export default class Emoji extends React.PureComponent { customEmojis, emojiName, literal, + padding, size, textStyle } = this.props; @@ -48,7 +51,7 @@ export default class Emoji extends React.PureComponent { return ( ); diff --git a/app/components/post_body/index.js b/app/components/post_body/index.js index f0ddaa9aa..a5d3001ba 100644 --- a/app/components/post_body/index.js +++ b/app/components/post_body/index.js @@ -23,6 +23,7 @@ function mapStateToProps(state, ownProps) { attachments: post.props && post.props.attachments, fileIds: post.file_ids, hasBeenDeleted: post.state === Posts.POST_DELETED, + hasReactions: post.has_reactions, isFailed: post.failed, isFlagged: isPostFlagged(post.id, myPreferences), isPending: post.id === post.pending_post_id, diff --git a/app/components/post_body/post_body.js b/app/components/post_body/post_body.js index 8ae34006d..367f43dc1 100644 --- a/app/components/post_body/post_body.js +++ b/app/components/post_body/post_body.js @@ -21,6 +21,7 @@ import SlackAttachments from 'app/components/slack_attachments'; import {emptyFunction} from 'app/utils/general'; import {getMarkdownTextStyles, getMarkdownBlockStyles} from 'app/utils/markdown'; import {makeStyleSheetFromTheme} from 'app/utils/theme'; +import Reactions from 'app/components/reactions'; class PostBody extends PureComponent { static propTypes = { @@ -33,6 +34,7 @@ class PostBody extends PureComponent { canEdit: PropTypes.bool, fileIds: PropTypes.array, hasBeenDeleted: PropTypes.bool, + hasReactions: PropTypes.bool, intl: intlShape.isRequired, isFailed: PropTypes.bool, isFlagged: PropTypes.bool, @@ -132,6 +134,7 @@ class PostBody extends PureComponent { canDelete, canEdit, hasBeenDeleted, + hasReactions, isFailed, isFlagged, isPending, @@ -143,6 +146,7 @@ class PostBody extends PureComponent { onPostDelete, onPostEdit, onPress, + postId, renderReplyBar, theme, toggleSelected @@ -219,6 +223,7 @@ class PostBody extends PureComponent { {messageComponent} {this.renderSlackAttachments(messageStyle, blockStyles, textStyles)} {this.renderFileAttachments()} + {hasReactions && } {isFailed && diff --git a/app/components/reactions/index.js b/app/components/reactions/index.js new file mode 100644 index 000000000..5c156e462 --- /dev/null +++ b/app/components/reactions/index.js @@ -0,0 +1,54 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; + +import {addReaction, getReactionsForPost, removeReaction} from 'mattermost-redux/actions/posts'; +import {makeGetReactionsForPost} from 'mattermost-redux/selectors/entities/posts'; +import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; + +import {getTheme} from 'app/selectors/preferences'; + +import Reactions from './reactions'; + +function makeMapStateToProps() { + const getReactionsForPostSelector = makeGetReactionsForPost(); + return function mapStateToProps(state, ownProps) { + const currentUserId = getCurrentUser(state); + const reactionsForPost = getReactionsForPostSelector(state, ownProps.postId); + + const highlightedReactions = []; + const reactionsByName = reactionsForPost.reduce((reactions, reaction) => { + if (reactions.has(reaction.emoji_name)) { + reactions.get(reaction.emoji_name).push(reaction); + } else { + reactions.set(reaction.emoji_name, [reaction]); + } + + if (reaction.user_id === currentUserId) { + highlightedReactions.push(reaction.emoji_name); + } + + return reactions; + }, new Map()); + + return { + highlightedReactions, + reactions: reactionsByName, + theme: getTheme(state) + }; + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + addReaction, + getReactionsForPost, + removeReaction + }, dispatch) + }; +} + +export default connect(makeMapStateToProps, mapDispatchToProps)(Reactions); diff --git a/app/components/reactions/reaction.js b/app/components/reactions/reaction.js new file mode 100644 index 000000000..2aa429526 --- /dev/null +++ b/app/components/reactions/reaction.js @@ -0,0 +1,70 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React, {PureComponent} from 'react'; +import PropTypes from 'prop-types'; +import { + StyleSheet, + Text, + TouchableOpacity +} from 'react-native'; + +import Emoji from 'app/components/emoji'; +import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; + +export default class Reaction extends PureComponent { + static propTypes = { + count: PropTypes.number.isRequired, + emojiName: PropTypes.string.isRequired, + highlight: PropTypes.bool.isRequired, + onPress: PropTypes.func.isRequired, + theme: PropTypes.object.isRequired + } + + handlePress = () => { + const {emojiName, highlight, onPress} = this.props; + onPress(emojiName, highlight); + } + + render() { + const {count, emojiName, highlight, theme} = this.props; + const styles = getStyleSheet(theme); + + return ( + + + {count} + + ); + } +} + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return StyleSheet.create({ + count: { + color: theme.linkColor, + marginLeft: 6 + }, + highlight: { + backgroundColor: changeOpacity(theme.linkColor, 0.1) + }, + reaction: { + alignItems: 'center', + borderRadius: 2, + borderColor: changeOpacity(theme.linkColor, 0.4), + borderWidth: 1, + flexDirection: 'row', + marginRight: 6, + marginVertical: 5, + paddingVertical: 2, + paddingHorizontal: 6 + } + }); +}); diff --git a/app/components/reactions/reactions.js b/app/components/reactions/reactions.js new file mode 100644 index 000000000..1772e4499 --- /dev/null +++ b/app/components/reactions/reactions.js @@ -0,0 +1,72 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React, {PureComponent} from 'react'; +import PropTypes from 'prop-types'; +import { + StyleSheet, + View +} from 'react-native'; + +import Reaction from './reaction'; + +export default class Reactions extends PureComponent { + static propTypes = { + actions: PropTypes.shape({ + addReaction: PropTypes.func.isRequired, + getReactionsForPost: PropTypes.func.isRequired, + removeReaction: PropTypes.func.isRequired + }).isRequired, + highlightedReactions: PropTypes.array.isRequired, + postId: PropTypes.string.isRequired, + reactions: PropTypes.object.isRequired, + theme: PropTypes.object.isRequired + } + + componentDidMount() { + const {actions, postId} = this.props; + actions.getReactionsForPost(postId); + } + + handleReactionPress = (emoji, remove) => { + const {actions, postId} = this.props; + if (remove) { + actions.removeReaction(postId, emoji); + } else { + actions.addReaction(postId, emoji); + } + } + + renderReactions = () => { + const {highlightedReactions, reactions, theme} = this.props; + + return Array.from(reactions.keys()).map((r) => { + return ( + + ); + }); + } + + render() { + return ( + + {this.renderReactions()} + + ); + } +} + +const style = StyleSheet.create({ + reactions: { + flexDirection: 'row', + flexWrap: 'wrap', + alignItems: 'flex-start' + } +}); diff --git a/yarn.lock b/yarn.lock index e2b5d6cc2..7868c0487 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3645,7 +3645,7 @@ makeerror@1.0.x: mattermost-redux@mattermost/mattermost-redux#master: version "0.0.1" - resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/e3b029e3df3d894485a27a9ca3cc7cbe7711ddb9" + resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/0d2b682cabc3893a0124f6674bcbfb653eead4a5" dependencies: deep-equal "1.0.1" harmony-reflect "1.5.1"