RN-153 Show emoji reactions on posts (#745)
* RN-153 Show emoji reactions on posts * Review Feedback * Change makeMapStateToProps in export
This commit is contained in:
parent
93d7697dcc
commit
9db0a38e8b
7 changed files with 208 additions and 3 deletions
|
|
@ -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 (
|
||||
<Image
|
||||
style={{width: size, height: size, padding: 10}}
|
||||
style={{width: size, height: size, padding}}
|
||||
source={{uri: imageUrl}}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 && <Reactions postId={postId}/>}
|
||||
</OptionsContext>
|
||||
</View>
|
||||
{isFailed &&
|
||||
|
|
|
|||
54
app/components/reactions/index.js
Normal file
54
app/components/reactions/index.js
Normal file
|
|
@ -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);
|
||||
70
app/components/reactions/reaction.js
Normal file
70
app/components/reactions/reaction.js
Normal file
|
|
@ -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 (
|
||||
<TouchableOpacity
|
||||
onPress={this.handlePress}
|
||||
style={[styles.reaction, (highlight && styles.highlight)]}
|
||||
>
|
||||
<Emoji
|
||||
emojiName={emojiName}
|
||||
size={15}
|
||||
padding={5}
|
||||
/>
|
||||
<Text style={styles.count}>{count}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
});
|
||||
});
|
||||
72
app/components/reactions/reactions.js
Normal file
72
app/components/reactions/reactions.js
Normal file
|
|
@ -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 (
|
||||
<Reaction
|
||||
key={r}
|
||||
count={reactions.get(r).length}
|
||||
emojiName={r}
|
||||
highlight={highlightedReactions.includes(r)}
|
||||
onPress={this.handleReactionPress}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={style.reactions}>
|
||||
{this.renderReactions()}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
reactions: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'flex-start'
|
||||
}
|
||||
});
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue