forked from Ivasoft/mattermost-mobile
[MM-9490] Add reaction list (#2125)
* add reaction list * update styles and add tests * update styles * update per comment
This commit is contained in:
@@ -329,7 +329,14 @@ export default class PostBody extends PureComponent {
|
||||
};
|
||||
|
||||
renderReactions = () => {
|
||||
const {hasReactions, isSearchResult, postId, onAddReaction, showLongPost} = this.props;
|
||||
const {
|
||||
hasReactions,
|
||||
isSearchResult,
|
||||
navigator,
|
||||
onAddReaction,
|
||||
postId,
|
||||
showLongPost,
|
||||
} = this.props;
|
||||
|
||||
if (!hasReactions || isSearchResult || showLongPost) {
|
||||
return null;
|
||||
@@ -343,6 +350,7 @@ export default class PostBody extends PureComponent {
|
||||
<Reactions
|
||||
postId={postId}
|
||||
onAddReaction={onAddReaction}
|
||||
navigator={navigator}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ export default class Reaction extends PureComponent {
|
||||
emojiName: PropTypes.string.isRequired,
|
||||
highlight: PropTypes.bool.isRequired,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
onLongPress: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
@@ -26,12 +27,19 @@ export default class Reaction extends PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {count, emojiName, highlight, theme} = this.props;
|
||||
const {
|
||||
count,
|
||||
emojiName,
|
||||
highlight,
|
||||
onLongPress,
|
||||
theme,
|
||||
} = this.props;
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={this.handlePress}
|
||||
onLongPress={onLongPress}
|
||||
style={[styles.reaction, (highlight && styles.highlight)]}
|
||||
>
|
||||
<Emoji
|
||||
|
||||
@@ -9,6 +9,9 @@ import {
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
|
||||
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import addReactionIcon from 'assets/images/icons/reaction.png';
|
||||
@@ -23,6 +26,7 @@ export default class Reactions extends PureComponent {
|
||||
removeReaction: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
highlightedReactions: PropTypes.array.isRequired,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
onAddReaction: PropTypes.func.isRequired,
|
||||
position: PropTypes.oneOf(['right', 'left']),
|
||||
postId: PropTypes.string.isRequired,
|
||||
@@ -36,6 +40,18 @@ export default class Reactions extends PureComponent {
|
||||
position: 'right',
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
MaterialIcon.getImageSource('close', 20, props.theme.sidebarHeaderTextColor).then((source) => {
|
||||
this.closeButton = source;
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {actions, postId} = this.props;
|
||||
actions.getReactionsForPost(postId);
|
||||
@@ -50,8 +66,37 @@ export default class Reactions extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
showReactionList = () => {
|
||||
const {navigator, postId, theme} = this.props;
|
||||
const {formatMessage} = this.context.intl;
|
||||
const options = {
|
||||
screen: 'ReactionList',
|
||||
title: formatMessage({id: 'mobile.reaction_list.title', defaultMessage: 'Reactions'}),
|
||||
animationType: 'slide-up',
|
||||
animated: true,
|
||||
backButtonTitle: '',
|
||||
navigatorStyle: {
|
||||
navBarTextColor: theme.sidebarHeaderTextColor,
|
||||
navBarBackgroundColor: theme.sidebarHeaderBg,
|
||||
navBarButtonColor: theme.sidebarHeaderTextColor,
|
||||
screenBackgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
navigatorButtons: {
|
||||
leftButtons: [{
|
||||
id: 'close-reaction-list',
|
||||
icon: this.closeButton,
|
||||
}],
|
||||
},
|
||||
passProps: {
|
||||
postId,
|
||||
},
|
||||
};
|
||||
|
||||
navigator.showModal(options);
|
||||
}
|
||||
|
||||
renderReactions = () => {
|
||||
const {highlightedReactions, reactions, theme} = this.props;
|
||||
const {highlightedReactions, navigator, reactions, theme, postId} = this.props;
|
||||
|
||||
return Array.from(reactions.keys()).map((r) => {
|
||||
return (
|
||||
@@ -60,7 +105,10 @@ export default class Reactions extends PureComponent {
|
||||
count={reactions.get(r).length}
|
||||
emojiName={r}
|
||||
highlight={highlightedReactions.includes(r)}
|
||||
navigator={navigator}
|
||||
onPress={this.handleReactionPress}
|
||||
onLongPress={this.showReactionList}
|
||||
postId={postId}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
|
||||
4
app/constants/emoji.js
Normal file
4
app/constants/emoji.js
Normal file
@@ -0,0 +1,4 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export const ALL_EMOJIS = 'all_emojis';
|
||||
@@ -46,6 +46,7 @@ export function registerScreens(store, Provider) {
|
||||
Navigation.registerComponent('NotificationSettingsMobile', () => wrapWithContextProvider(require('app/screens/settings/notification_settings_mobile').default), store, Provider);
|
||||
Navigation.registerComponent('OptionsModal', () => wrapWithContextProvider(require('app/screens/options_modal').default), store, Provider);
|
||||
Navigation.registerComponent('Permalink', () => wrapWithContextProvider(require('app/screens/permalink').default), store, Provider);
|
||||
Navigation.registerComponent('ReactionList', () => wrapWithContextProvider(require('app/screens/reaction_list').default), store, Provider);
|
||||
Navigation.registerComponent('RecentMentions', () => wrapWithContextProvider(require('app/screens/recent_mentions').default), store, Provider);
|
||||
Navigation.registerComponent('Root', () => require('app/screens/root').default, store, Provider);
|
||||
Navigation.registerComponent('Search', () => wrapWithContextProvider(require('app/screens/search').default), store, Provider);
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ReactionHeader should match snapshot 1`] = `
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"height": 37,
|
||||
"paddingHorizontal": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<ScrollView
|
||||
alwaysBounceHorizontal={false}
|
||||
horizontal={true}
|
||||
overScrollMode="never"
|
||||
>
|
||||
<ReactionHeaderItem
|
||||
count={2}
|
||||
emojiName="smile"
|
||||
highlight={true}
|
||||
onPress={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<ReactionHeaderItem
|
||||
count={1}
|
||||
emojiName="+1"
|
||||
highlight={false}
|
||||
onPress={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</ScrollView>
|
||||
</Component>
|
||||
`;
|
||||
|
||||
exports[`ReactionHeader should match snapshot, renderContent 1`] = `
|
||||
Array [
|
||||
<ReactionHeaderItem
|
||||
count={2}
|
||||
emojiName="smile"
|
||||
highlight={true}
|
||||
onPress={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>,
|
||||
<ReactionHeaderItem
|
||||
count={1}
|
||||
emojiName="+1"
|
||||
highlight={false}
|
||||
onPress={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>,
|
||||
]
|
||||
`;
|
||||
@@ -0,0 +1,94 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ReactionHeaderItem should match snapshot 1`] = `
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.2}
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"height": 35,
|
||||
"marginBottom": 5,
|
||||
"marginRight": 6,
|
||||
"marginTop": 3,
|
||||
"paddingHorizontal": 6,
|
||||
"paddingVertical": 2,
|
||||
},
|
||||
Object {
|
||||
"borderBottomWidth": 2,
|
||||
"borderColor": "#ffffff",
|
||||
},
|
||||
false,
|
||||
]
|
||||
}
|
||||
>
|
||||
<React.Fragment>
|
||||
<Connect(Emoji)
|
||||
emojiName="smile"
|
||||
padding={5}
|
||||
size={16}
|
||||
/>
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"color": "#2389d7",
|
||||
"fontSize": 16,
|
||||
"marginLeft": 4,
|
||||
}
|
||||
}
|
||||
>
|
||||
3
|
||||
</Component>
|
||||
</React.Fragment>
|
||||
</TouchableOpacity>
|
||||
`;
|
||||
|
||||
exports[`ReactionHeaderItem should match snapshot, renderContent 1`] = `
|
||||
<React.Fragment>
|
||||
<Connect(Emoji)
|
||||
emojiName="smile"
|
||||
padding={5}
|
||||
size={16}
|
||||
/>
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"color": "#2389d7",
|
||||
"fontSize": 16,
|
||||
"marginLeft": 4,
|
||||
}
|
||||
}
|
||||
>
|
||||
3
|
||||
</Component>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
||||
exports[`ReactionHeaderItem should match snapshot, renderContent 2`] = `
|
||||
<React.Fragment>
|
||||
<FormattedText
|
||||
defaultMessage="All"
|
||||
id="mobile.reaction_header.all_emojis"
|
||||
style={
|
||||
Object {
|
||||
"color": "#2389d7",
|
||||
"fontSize": 16,
|
||||
"marginLeft": 4,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"color": "#2389d7",
|
||||
"fontSize": 16,
|
||||
"marginLeft": 4,
|
||||
}
|
||||
}
|
||||
>
|
||||
3
|
||||
</Component>
|
||||
</React.Fragment>
|
||||
`;
|
||||
@@ -0,0 +1,388 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ReactionList should match snapshot 1`] = `
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"borderBottomWidth": 1,
|
||||
"borderColor": "rgba(61,60,64,0.2)",
|
||||
"height": 38,
|
||||
}
|
||||
}
|
||||
>
|
||||
<ReactionHeader
|
||||
onSelectReaction={[Function]}
|
||||
reactions={
|
||||
Array [
|
||||
Object {
|
||||
"count": 2,
|
||||
"name": "all_emojis",
|
||||
},
|
||||
Object {
|
||||
"count": 1,
|
||||
"name": "+1",
|
||||
"reactions": Array [
|
||||
Object {
|
||||
"emoji_name": "+1",
|
||||
"user_id": "user_id_2",
|
||||
},
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"count": 1,
|
||||
"name": "smile",
|
||||
"reactions": Array [
|
||||
Object {
|
||||
"emoji_name": "smile",
|
||||
"user_id": "user_id_1",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
selected="all_emojis"
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Component>
|
||||
<KeyboardAwareScrollView
|
||||
bounces={true}
|
||||
enableAutomaticScroll={true}
|
||||
enableOnAndroid={false}
|
||||
enableResetScrollToCoords={true}
|
||||
extraHeight={75}
|
||||
extraScrollHeight={0}
|
||||
innerRef={[Function]}
|
||||
keyboardOpeningTime={250}
|
||||
viewIsInsideTabBar={false}
|
||||
>
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"height": 45,
|
||||
"justifyContent": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
<ReactionRow
|
||||
emojiName="+1"
|
||||
navigator={
|
||||
Object {
|
||||
"setOnNavigatorEvent": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
[Function],
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"isThrow": false,
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
teammateNameDisplay="username"
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
user={
|
||||
Object {
|
||||
"id": "user_id_2",
|
||||
"username": "username_2",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,0.2)",
|
||||
"height": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Component>
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"height": 45,
|
||||
"justifyContent": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
<ReactionRow
|
||||
emojiName="smile"
|
||||
navigator={
|
||||
Object {
|
||||
"setOnNavigatorEvent": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
[Function],
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"isThrow": false,
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
teammateNameDisplay="username"
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
user={
|
||||
Object {
|
||||
"id": "user_id_1",
|
||||
"username": "username_1",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,0.2)",
|
||||
"height": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Component>
|
||||
</KeyboardAwareScrollView>
|
||||
</Component>
|
||||
`;
|
||||
|
||||
exports[`ReactionList should match snapshot, renderReactionRows 1`] = `
|
||||
Array [
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"height": 45,
|
||||
"justifyContent": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
<ReactionRow
|
||||
emojiName="+1"
|
||||
navigator={
|
||||
Object {
|
||||
"setOnNavigatorEvent": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
[Function],
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"isThrow": false,
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
teammateNameDisplay="username"
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
user={
|
||||
Object {
|
||||
"id": "user_id_2",
|
||||
"username": "username_2",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,0.2)",
|
||||
"height": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Component>,
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"height": 45,
|
||||
"justifyContent": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
<ReactionRow
|
||||
emojiName="smile"
|
||||
navigator={
|
||||
Object {
|
||||
"setOnNavigatorEvent": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
[Function],
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"isThrow": false,
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
teammateNameDisplay="username"
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
user={
|
||||
Object {
|
||||
"id": "user_id_1",
|
||||
"username": "username_1",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,0.2)",
|
||||
"height": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Component>,
|
||||
]
|
||||
`;
|
||||
@@ -0,0 +1,93 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ReactionRow should match snapshot, renderContent 1`] = `
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#ffffff",
|
||||
"flexDirection": "row",
|
||||
"height": 44,
|
||||
"justifyContent": "flex-start",
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"width": "13%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.2}
|
||||
onPress={[Function]}
|
||||
>
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"paddingTop": 3,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Connect(ProfilePicture)
|
||||
showStatus={false}
|
||||
size={24}
|
||||
userId="user_id"
|
||||
/>
|
||||
</Component>
|
||||
</TouchableOpacity>
|
||||
</Component>
|
||||
<Component
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Object {
|
||||
"flexDirection": "row",
|
||||
"width": "74%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 14,
|
||||
"paddingRight": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
@username
|
||||
</Component>
|
||||
<Component>
|
||||
|
||||
</Component>
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(61,60,64,0.5)",
|
||||
"fontSize": 14,
|
||||
}
|
||||
}
|
||||
>
|
||||
username
|
||||
</Component>
|
||||
</Component>
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"width": "13%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Connect(Emoji)
|
||||
emojiName="smile"
|
||||
size={24}
|
||||
/>
|
||||
</Component>
|
||||
</Component>
|
||||
`;
|
||||
42
app/screens/reaction_list/index.js
Normal file
42
app/screens/reaction_list/index.js
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getMissingProfilesByIds} from 'mattermost-redux/actions/users';
|
||||
import {makeGetReactionsForPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getCurrentUserId, makeGetProfilesByIdsAndUsernames} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {getUniqueUserIds} from 'app/utils/reaction';
|
||||
|
||||
import ReactionList from './reaction_list';
|
||||
|
||||
function makeMapStateToProps() {
|
||||
const getReactionsForPostSelector = makeGetReactionsForPost();
|
||||
const getProfilesByIdsAndUsernames = makeGetProfilesByIdsAndUsernames();
|
||||
|
||||
return function mapStateToProps(state, ownProps) {
|
||||
const reactions = getReactionsForPostSelector(state, ownProps.postId);
|
||||
const allUserIds = getUniqueUserIds(reactions);
|
||||
|
||||
return {
|
||||
currentUserId: getCurrentUserId(state),
|
||||
reactions,
|
||||
teammateNameDisplay: getTeammateNameDisplaySetting(state),
|
||||
theme: getTheme(state),
|
||||
userProfiles: getProfilesByIdsAndUsernames(state, {allUserIds}) || [],
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
getMissingProfilesByIds,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(ReactionList);
|
||||
68
app/screens/reaction_list/reaction_header.js
Normal file
68
app/screens/reaction_list/reaction_header.js
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
ScrollView,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import ReactionHeaderItem from './reaction_header_item';
|
||||
|
||||
export default class ReactionHeader extends PureComponent {
|
||||
static propTypes = {
|
||||
selected: PropTypes.string.isRequired,
|
||||
onSelectReaction: PropTypes.func.isRequired,
|
||||
reactions: PropTypes.array.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
handleOnPress = (emoji) => {
|
||||
this.props.onSelectReaction(emoji);
|
||||
};
|
||||
|
||||
renderReactionHeaderItems = () => {
|
||||
const {selected, reactions, theme} = this.props;
|
||||
|
||||
return reactions.map((reaction) => (
|
||||
<ReactionHeaderItem
|
||||
key={reaction.name}
|
||||
count={reaction.count}
|
||||
emojiName={reaction.name}
|
||||
highlight={selected === reaction.name}
|
||||
onPress={this.handleOnPress}
|
||||
theme={theme}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
render() {
|
||||
const {theme} = this.props;
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ScrollView
|
||||
alwaysBounceHorizontal={false}
|
||||
horizontal={true}
|
||||
overScrollMode='never'
|
||||
>
|
||||
{this.renderReactionHeaderItems()}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
height: 37,
|
||||
paddingHorizontal: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
48
app/screens/reaction_list/reaction_header.test.js
Normal file
48
app/screens/reaction_list/reaction_header.test.js
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
import {ScrollView} from 'react-native';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import ReactionHeader from './reaction_header';
|
||||
|
||||
describe('ReactionHeader', () => {
|
||||
const baseProps = {
|
||||
selected: 'smile',
|
||||
onSelectReaction: jest.fn(),
|
||||
reactions: [{name: 'smile', count: 2}, {name: '+1', count: 1}],
|
||||
theme: Preferences.THEMES.default,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<ReactionHeader {...baseProps}/>
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
expect(wrapper.find(ScrollView).exists()).toEqual(true);
|
||||
});
|
||||
|
||||
test('should match snapshot, renderContent', () => {
|
||||
const wrapper = shallow(
|
||||
<ReactionHeader {...baseProps}/>
|
||||
);
|
||||
|
||||
expect(wrapper.instance().renderReactionHeaderItems()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should call props.onSelectReaction on handlePress', () => {
|
||||
const onSelectReaction = jest.fn();
|
||||
const wrapper = shallow(
|
||||
<ReactionHeader
|
||||
{...baseProps}
|
||||
onSelectReaction={onSelectReaction}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper.instance().handleOnPress();
|
||||
expect(onSelectReaction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
105
app/screens/reaction_list/reaction_header_item.js
Normal file
105
app/screens/reaction_list/reaction_header_item.js
Normal file
@@ -0,0 +1,105 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
} from 'react-native';
|
||||
|
||||
import Emoji from 'app/components/emoji';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
|
||||
import {ALL_EMOJIS} from 'app/constants/emoji';
|
||||
|
||||
export default class ReactionHeaderItem extends PureComponent {
|
||||
static propTypes = {
|
||||
count: PropTypes.number.isRequired,
|
||||
emojiName: PropTypes.string.isRequired,
|
||||
highlight: PropTypes.bool.isRequired,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
handleOnPress = () => {
|
||||
const {emojiName, highlight, onPress} = this.props;
|
||||
onPress(emojiName, highlight);
|
||||
}
|
||||
|
||||
renderContent = () => {
|
||||
const {count, emojiName, theme} = this.props;
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
if (emojiName === ALL_EMOJIS) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<FormattedText
|
||||
id='mobile.reaction_header.all_emojis'
|
||||
defaultMessage={'All'}
|
||||
style={styles.text}
|
||||
/>
|
||||
<Text style={styles.text}>{count}</Text>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Emoji
|
||||
emojiName={emojiName}
|
||||
size={16}
|
||||
padding={5}
|
||||
/>
|
||||
<Text style={styles.text}>{count}</Text>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {emojiName, highlight, theme} = this.props;
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={this.handleOnPress}
|
||||
style={[styles.reaction, (highlight ? styles.highlight : styles.regular), (emojiName === ALL_EMOJIS && styles.allText)]}
|
||||
>
|
||||
{this.renderContent()}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
allText: {
|
||||
marginLeft: 7,
|
||||
},
|
||||
text: {
|
||||
color: theme.linkColor,
|
||||
marginLeft: 4,
|
||||
fontSize: 16,
|
||||
},
|
||||
highlight: {
|
||||
borderColor: changeOpacity(theme.linkColor, 1),
|
||||
borderBottomWidth: 2,
|
||||
},
|
||||
regular: {
|
||||
borderColor: theme.centerChannelBg,
|
||||
borderBottomWidth: 2,
|
||||
},
|
||||
reaction: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
height: 35,
|
||||
marginRight: 6,
|
||||
marginBottom: 5,
|
||||
marginTop: 3,
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 6,
|
||||
},
|
||||
};
|
||||
});
|
||||
55
app/screens/reaction_list/reaction_header_item.test.js
Normal file
55
app/screens/reaction_list/reaction_header_item.test.js
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
import {TouchableOpacity} from 'react-native';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import ReactionHeaderItem from './reaction_header_item';
|
||||
|
||||
import {ALL_EMOJIS} from 'app/constants/emoji';
|
||||
|
||||
describe('ReactionHeaderItem', () => {
|
||||
const baseProps = {
|
||||
count: 3,
|
||||
emojiName: 'smile',
|
||||
highlight: false,
|
||||
onPress: jest.fn(),
|
||||
theme: Preferences.THEMES.default,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<ReactionHeaderItem {...baseProps}/>
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
expect(wrapper.find(TouchableOpacity).exists()).toEqual(true);
|
||||
});
|
||||
|
||||
test('should match snapshot, renderContent', () => {
|
||||
const wrapper = shallow(
|
||||
<ReactionHeaderItem {...baseProps}/>
|
||||
);
|
||||
|
||||
expect(wrapper.instance().renderContent()).toMatchSnapshot();
|
||||
|
||||
wrapper.setProps({emojiName: ALL_EMOJIS});
|
||||
expect(wrapper.instance().renderContent()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should call props.onPress on handleOnPress', () => {
|
||||
const onPress = jest.fn();
|
||||
const wrapper = shallow(
|
||||
<ReactionHeaderItem
|
||||
{...baseProps}
|
||||
onPress={onPress}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper.instance().handleOnPress();
|
||||
expect(onPress).toHaveBeenCalledTimes(1);
|
||||
expect(onPress).toHaveBeenCalledWith(baseProps.emojiName, baseProps.highlight);
|
||||
});
|
||||
});
|
||||
210
app/screens/reaction_list/reaction_list.js
Normal file
210
app/screens/reaction_list/reaction_list.js
Normal file
@@ -0,0 +1,210 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {View} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
|
||||
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import {
|
||||
generateUserProfilesById,
|
||||
getMissingUserIds,
|
||||
getReactionsByName,
|
||||
getSortedReactionsForHeader,
|
||||
getUniqueUserIds,
|
||||
sortReactions,
|
||||
} from 'app/utils/reaction';
|
||||
|
||||
import ReactionHeader from './reaction_header';
|
||||
import ReactionRow from './reaction_row';
|
||||
|
||||
import {ALL_EMOJIS} from 'app/constants/emoji';
|
||||
|
||||
export default class ReactionList extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
getMissingProfilesByIds: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
navigator: PropTypes.object,
|
||||
reactions: PropTypes.array.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
teammateNameDisplay: PropTypes.string,
|
||||
userProfiles: PropTypes.array,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const {reactions, userProfiles} = props;
|
||||
|
||||
const reactionsByName = getReactionsByName(reactions);
|
||||
|
||||
this.state = {
|
||||
allUserIds: getUniqueUserIds(reactions),
|
||||
reactions,
|
||||
reactionsByName,
|
||||
selected: ALL_EMOJIS,
|
||||
sortedReactions: sortReactions(reactionsByName),
|
||||
sortedReactionsForHeader: getSortedReactionsForHeader(reactionsByName),
|
||||
userProfiles,
|
||||
userProfilesById: generateUserProfilesById(userProfiles),
|
||||
};
|
||||
|
||||
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
let newState = null;
|
||||
if (nextProps.reactions !== prevState.reactions) {
|
||||
const {reactions} = nextProps;
|
||||
const reactionsByName = getReactionsByName(reactions);
|
||||
|
||||
newState = {
|
||||
allUserIds: getUniqueUserIds(reactions),
|
||||
reactions,
|
||||
reactionsByName,
|
||||
sortedReactions: sortReactions(reactionsByName),
|
||||
sortedReactionsForHeader: getSortedReactionsForHeader(reactionsByName),
|
||||
};
|
||||
}
|
||||
|
||||
if (nextProps.userProfiles !== prevState.userProfiles) {
|
||||
const userProfilesById = generateUserProfilesById(nextProps.userProfiles);
|
||||
if (newState) {
|
||||
newState.userProfilesById = userProfilesById;
|
||||
} else {
|
||||
newState = {userProfilesById};
|
||||
}
|
||||
}
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getMissingProfiles();
|
||||
}
|
||||
|
||||
componentDidUpdate(_, prevState) {
|
||||
if (prevState.allUserIds !== this.state.allUserIds) {
|
||||
this.getMissingProfiles();
|
||||
}
|
||||
}
|
||||
|
||||
onNavigatorEvent = (event) => {
|
||||
if (event.type === 'NavBarButtonPress') {
|
||||
if (event.id === 'close-reaction-list') {
|
||||
this.props.navigator.dismissModal({
|
||||
animationType: 'slide-down',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
getMissingProfiles = () => {
|
||||
const {allUserIds, userProfiles, userProfilesById} = this.state;
|
||||
if (userProfiles.length !== allUserIds.length) {
|
||||
const missingUserIds = getMissingUserIds(userProfilesById, allUserIds);
|
||||
|
||||
if (missingUserIds.length > 0) {
|
||||
this.props.actions.getMissingProfilesByIds(missingUserIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scrollViewRef = (ref) => {
|
||||
this.scrollView = ref;
|
||||
};
|
||||
|
||||
handleOnSelectReaction = (emoji) => {
|
||||
this.setState({selected: emoji});
|
||||
}
|
||||
|
||||
renderReactionRows = () => {
|
||||
const {
|
||||
navigator,
|
||||
teammateNameDisplay,
|
||||
theme,
|
||||
} = this.props;
|
||||
const {
|
||||
reactionsByName,
|
||||
selected,
|
||||
sortedReactions,
|
||||
userProfilesById,
|
||||
} = this.state;
|
||||
const style = getStyleSheet(theme);
|
||||
const reactions = selected === ALL_EMOJIS ? sortedReactions : reactionsByName[selected];
|
||||
|
||||
return reactions.map(({emoji_name: emojiName, user_id: userId}) => (
|
||||
<View
|
||||
key={emojiName + userId}
|
||||
style={style.rowContainer}
|
||||
>
|
||||
<ReactionRow
|
||||
emojiName={emojiName}
|
||||
navigator={navigator}
|
||||
teammateNameDisplay={teammateNameDisplay}
|
||||
theme={theme}
|
||||
user={userProfilesById[userId]}
|
||||
/>
|
||||
<View style={style.separator}/>
|
||||
</View>
|
||||
));
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
theme,
|
||||
} = this.props;
|
||||
const {
|
||||
selected,
|
||||
sortedReactionsForHeader,
|
||||
} = this.state;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={style.flex}>
|
||||
<View style={style.headerContainer}>
|
||||
<ReactionHeader
|
||||
selected={selected}
|
||||
onSelectReaction={this.handleOnSelectReaction}
|
||||
reactions={sortedReactionsForHeader}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
<KeyboardAwareScrollView
|
||||
bounces={true}
|
||||
innerRef={this.scrollViewRef}
|
||||
>
|
||||
{this.renderReactionRows()}
|
||||
</KeyboardAwareScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
flex: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
flex: 1,
|
||||
},
|
||||
headerContainer: {
|
||||
height: 38,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
rowContainer: {
|
||||
justifyContent: 'center',
|
||||
height: 45,
|
||||
},
|
||||
separator: {
|
||||
height: 1,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
},
|
||||
};
|
||||
});
|
||||
55
app/screens/reaction_list/reaction_list.test.js
Normal file
55
app/screens/reaction_list/reaction_list.test.js
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import ReactionList from './reaction_list';
|
||||
|
||||
jest.mock('react-intl');
|
||||
|
||||
describe('ReactionList', () => {
|
||||
const baseProps = {
|
||||
actions: {
|
||||
getMissingProfilesByIds: jest.fn(),
|
||||
},
|
||||
allUserIds: ['user_id_1', 'user_id_2'],
|
||||
navigator: {setOnNavigatorEvent: jest.fn()},
|
||||
reactions: [{emoji_name: 'smile', user_id: 'user_id_1'}, {emoji_name: '+1', user_id: 'user_id_2'}],
|
||||
theme: Preferences.THEMES.default,
|
||||
teammateNameDisplay: 'username',
|
||||
userProfiles: [{id: 'user_id_1', username: 'username_1'}, {id: 'user_id_2', username: 'username_2'}],
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<ReactionList {...baseProps}/>,
|
||||
{context: {intl: {formatMessage: jest.fn()}}},
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
expect(wrapper.find(KeyboardAwareScrollView).exists()).toEqual(true);
|
||||
});
|
||||
|
||||
test('should match snapshot, renderReactionRows', () => {
|
||||
const wrapper = shallow(
|
||||
<ReactionList {...baseProps}/>,
|
||||
{context: {intl: {formatMessage: jest.fn()}}},
|
||||
);
|
||||
|
||||
expect(wrapper.instance().renderReactionRows()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match state on handleOnSelectReaction', () => {
|
||||
const wrapper = shallow(
|
||||
<ReactionList {...baseProps}/>,
|
||||
{context: {intl: {formatMessage: jest.fn()}}},
|
||||
);
|
||||
|
||||
wrapper.setState({selected: 'smile'});
|
||||
wrapper.instance().handleOnSelectReaction('+1');
|
||||
expect(wrapper.state('selected')).toEqual('+1');
|
||||
});
|
||||
});
|
||||
153
app/screens/reaction_list/reaction_row.js
Normal file
153
app/screens/reaction_list/reaction_row.js
Normal file
@@ -0,0 +1,153 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import {displayUsername} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import ProfilePicture from 'app/components/profile_picture';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
import Emoji from 'app/components/emoji';
|
||||
|
||||
export default class ReactionRow extends React.PureComponent {
|
||||
static propTypes = {
|
||||
emojiName: PropTypes.string.isRequired,
|
||||
navigator: PropTypes.object,
|
||||
teammateNameDisplay: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
user: {},
|
||||
}
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
goToUserProfile = () => {
|
||||
const {navigator, theme, user} = this.props;
|
||||
const {formatMessage} = this.context.intl;
|
||||
|
||||
const options = {
|
||||
screen: 'UserProfile',
|
||||
title: formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}),
|
||||
animated: true,
|
||||
backButtonTitle: '',
|
||||
passProps: {
|
||||
userId: user.id,
|
||||
},
|
||||
navigatorStyle: {
|
||||
navBarTextColor: theme.sidebarHeaderTextColor,
|
||||
navBarBackgroundColor: theme.sidebarHeaderBg,
|
||||
navBarButtonColor: theme.sidebarHeaderTextColor,
|
||||
screenBackgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
};
|
||||
|
||||
navigator.push(options);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
emojiName,
|
||||
teammateNameDisplay,
|
||||
theme,
|
||||
user,
|
||||
} = this.props;
|
||||
|
||||
if (!user.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {id, username} = user;
|
||||
const style = getStyleFromTheme(theme);
|
||||
const usernameDisplay = '@' + username;
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<View style={style.profileContainer}>
|
||||
<TouchableOpacity
|
||||
key={user.id}
|
||||
onPress={preventDoubleTap(this.goToUserProfile)}
|
||||
>
|
||||
<View style={style.profile}>
|
||||
<ProfilePicture
|
||||
userId={id}
|
||||
showStatus={false}
|
||||
size={24}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Text
|
||||
style={style.textContainer}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
<Text style={style.username}>
|
||||
{usernameDisplay}
|
||||
</Text>
|
||||
<Text>{' '}</Text>
|
||||
<Text style={style.displayName}>
|
||||
{displayUsername(user, teammateNameDisplay)}
|
||||
</Text>
|
||||
</Text>
|
||||
<View style={style.emoji}>
|
||||
<Emoji
|
||||
emojiName={emojiName}
|
||||
size={24}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start',
|
||||
height: 44,
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
},
|
||||
profileContainer: {
|
||||
alignItems: 'center',
|
||||
width: '13%',
|
||||
},
|
||||
profile: {
|
||||
paddingTop: 3,
|
||||
},
|
||||
textContainer: {
|
||||
width: '74%',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
username: {
|
||||
fontSize: 14,
|
||||
color: theme.centerChannelColor,
|
||||
paddingRight: 5,
|
||||
},
|
||||
displayName: {
|
||||
fontSize: 14,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
},
|
||||
emoji: {
|
||||
alignItems: 'center',
|
||||
width: '13%',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
};
|
||||
});
|
||||
26
app/screens/reaction_list/reaction_row.test.js
Normal file
26
app/screens/reaction_list/reaction_row.test.js
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import ReactionRow from './reaction_row';
|
||||
|
||||
describe('ReactionRow', () => {
|
||||
const baseProps = {
|
||||
emojiName: 'smile',
|
||||
navigator: {},
|
||||
teammateNameDisplay: 'username',
|
||||
theme: Preferences.THEMES.default,
|
||||
user: {id: 'user_id', username: 'username'},
|
||||
};
|
||||
|
||||
test('should match snapshot, renderContent', () => {
|
||||
const wrapper = shallow(
|
||||
<ReactionRow {...baseProps}/>
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
67
app/utils/reaction.js
Normal file
67
app/utils/reaction.js
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {ALL_EMOJIS} from 'app/constants/emoji';
|
||||
|
||||
export function generateUserProfilesById(userProfiles = []) {
|
||||
return userProfiles.reduce((acc, userProfile) => {
|
||||
acc[userProfile.id] = userProfile;
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function getMissingUserIds(userProfilesById = {}, allUserIds = []) {
|
||||
return allUserIds.reduce((acc, userId) => {
|
||||
if (userProfilesById[userId]) {
|
||||
acc.push(userId);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function compareReactions(a, b) {
|
||||
if (a.count !== b.count) {
|
||||
return b.count - a.count;
|
||||
}
|
||||
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
export function getReactionsByName(reactions = []) {
|
||||
return reactions.reduce((acc, reaction) => {
|
||||
const byName = acc[reaction.emoji_name] || [];
|
||||
acc[reaction.emoji_name] = [...byName, reaction];
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function sortReactionsByName(reactionsByName = {}) {
|
||||
return Object.entries(reactionsByName).
|
||||
map(([name, reactions]) => ({name, reactions, count: reactions.length})).
|
||||
sort(compareReactions);
|
||||
}
|
||||
|
||||
export function sortReactions(reactionsByName = {}) {
|
||||
return sortReactionsByName(reactionsByName).
|
||||
reduce((acc, {reactions}) => {
|
||||
reactions.forEach((r) => acc.push(r));
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function getSortedReactionsForHeader(reactionsByName = {}) {
|
||||
const sortedReactionsForHeader = sortReactionsByName(reactionsByName);
|
||||
|
||||
const totalCount = sortedReactionsForHeader.reduce((acc, reaction) => {
|
||||
return acc + reaction.count;
|
||||
}, 0);
|
||||
|
||||
return [{name: ALL_EMOJIS, count: totalCount}, ...sortedReactionsForHeader];
|
||||
}
|
||||
|
||||
export function getUniqueUserIds(reactions = []) {
|
||||
return reactions.map((reaction) => reaction.user_id).filter((id, index, arr) => arr.indexOf(id) === index);
|
||||
}
|
||||
@@ -333,6 +333,7 @@
|
||||
"mobile.post.failed_title": "Unable to send your message",
|
||||
"mobile.post.retry": "Refresh",
|
||||
"mobile.posts_view.moreMsg": "More New Messages Above",
|
||||
"mobile.reaction_list.title": "Reactions",
|
||||
"mobile.recent_mentions.empty_description": "Messages containing your username and other words that trigger mentions will appear here.",
|
||||
"mobile.recent_mentions.empty_title": "No Recent Mentions",
|
||||
"mobile.rename_channel.display_name_maxLength": "Channel name must be less than {maxLength, number} characters",
|
||||
|
||||
Reference in New Issue
Block a user