diff --git a/app/components/reaction_picker/__snapshots__/reaction_picker.test.js.snap b/app/components/reaction_picker/__snapshots__/reaction_picker.test.js.snap
new file mode 100644
index 000000000..c81fd78e8
--- /dev/null
+++ b/app/components/reaction_picker/__snapshots__/reaction_picker.test.js.snap
@@ -0,0 +1,529 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Reactions Should match snapshot with default emojis 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Reactions Should match snapshot with default emojis 2`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/app/components/reaction_picker/index.js b/app/components/reaction_picker/index.js
new file mode 100644
index 000000000..2d39f8308
--- /dev/null
+++ b/app/components/reaction_picker/index.js
@@ -0,0 +1,18 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {connect} from 'react-redux';
+import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
+import {getDimensions} from 'app/selectors/device';
+import ReactionPicker from './reaction_picker';
+
+function mapStateToProps(state) {
+ const {deviceWidth} = getDimensions(state);
+ return {
+ theme: getTheme(state),
+ recentEmojis: state.views.recentEmojis,
+ deviceWidth,
+ };
+}
+
+export default connect(mapStateToProps)(ReactionPicker);
diff --git a/app/components/reaction_picker/reaction_button.js b/app/components/reaction_picker/reaction_button.js
new file mode 100644
index 000000000..7c4b35003
--- /dev/null
+++ b/app/components/reaction_picker/reaction_button.js
@@ -0,0 +1,98 @@
+// 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,
+ TouchableWithoutFeedback,
+} from 'react-native';
+
+import Emoji from 'app/components/emoji';
+import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
+import {hapticFeedback} from 'app/utils/general';
+import {
+ LARGE_CONTAINER_SIZE,
+ LARGE_ICON_SIZE,
+} from 'app/constants/reaction_picker';
+
+export default class ReactionButton extends PureComponent {
+ static propTypes = {
+ addReaction: PropTypes.func.isRequired,
+ theme: PropTypes.object.isRequired,
+ emoji: PropTypes.string.isRequired,
+ iconSize: PropTypes.number,
+ containerSize: PropTypes.number,
+ }
+
+ static defaultProps = {
+ iconSize: LARGE_ICON_SIZE,
+ containerSize: LARGE_CONTAINER_SIZE,
+ };
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isSelected: false,
+ };
+ }
+
+ handlePress = () => {
+ hapticFeedback();
+ this.setState({isSelected: true}, () => {
+ this.props.addReaction(this.props.emoji);
+ });
+ }
+
+ render() {
+ const {
+ theme,
+ emoji,
+ iconSize,
+ containerSize,
+ } = this.props;
+ const style = getStyleSheet(theme);
+
+ return (
+
+
+
+
+
+ );
+ }
+}
+
+const getStyleSheet = makeStyleSheetFromTheme((theme) => {
+ return {
+ emoji: {
+ color: '#000',
+ fontWeight: 'bold',
+ },
+ highlight: {
+ backgroundColor: changeOpacity(theme.linkColor, 0.1),
+ },
+ reactionContainer: {
+ backgroundColor: changeOpacity(theme.centerChannelColor, 0.04),
+ borderRadius: 4,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ };
+});
diff --git a/app/components/reaction_picker/reaction_picker.js b/app/components/reaction_picker/reaction_picker.js
new file mode 100644
index 000000000..e61003f66
--- /dev/null
+++ b/app/components/reaction_picker/reaction_picker.js
@@ -0,0 +1,117 @@
+// 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 {
+ Image,
+ View,
+ TouchableWithoutFeedback,
+} from 'react-native';
+
+import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
+import addReactionIcon from 'assets/images/icons/reaction.png';
+import ReactionButton from './reaction_button';
+import {
+ REACTION_PICKER_HEIGHT,
+ DEFAULT_EMOJIS,
+ SMALL_ICON_BREAKPOINT,
+ SMALL_CONTAINER_SIZE,
+ LARGE_CONTAINER_SIZE,
+ SMALL_ICON_SIZE,
+ LARGE_ICON_SIZE,
+} from 'app/constants/reaction_picker';
+
+export default class ReactionPicker extends PureComponent {
+ static propTypes = {
+ addReaction: PropTypes.func.isRequired,
+ openReactionScreen: PropTypes.func.isRequired,
+ theme: PropTypes.object.isRequired,
+ recentEmojis: PropTypes.array,
+ deviceWidth: PropTypes.number,
+ }
+
+ handlePress = (emoji) => {
+ this.props.addReaction(emoji);
+ }
+
+ render() {
+ const {
+ theme,
+ deviceWidth,
+ } = this.props;
+ const style = getStyleSheet(theme);
+ const isSmallDevice = deviceWidth < SMALL_ICON_BREAKPOINT;
+
+ let containerSize = LARGE_CONTAINER_SIZE;
+ let iconSize = LARGE_ICON_SIZE;
+
+ if (isSmallDevice) {
+ containerSize = SMALL_CONTAINER_SIZE;
+ iconSize = SMALL_ICON_SIZE;
+ }
+
+ // Mixing recent emojis with default list and removing duplicates
+ const emojis = Array.from(new Set(this.props.recentEmojis.concat(DEFAULT_EMOJIS))).splice(0, 6);
+
+ const list = emojis.map((emoji) => {
+ return (
+
+ );
+ });
+
+ return (
+
+ {list}
+
+
+
+
+
+
+ );
+ }
+}
+
+const getStyleSheet = makeStyleSheetFromTheme((theme) => {
+ return {
+ iconImage: {
+ tintColor: theme.centerChannelColor,
+ },
+ reactionListContainer: {
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingLeft: 12,
+ paddingRight: 12,
+ height: REACTION_PICKER_HEIGHT,
+ justifyContent: 'space-between',
+ },
+ reactionContainer: {
+ backgroundColor: changeOpacity(theme.centerChannelColor, 0.04),
+ borderRadius: 4,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ };
+});
diff --git a/app/components/reaction_picker/reaction_picker.test.js b/app/components/reaction_picker/reaction_picker.test.js
new file mode 100644
index 000000000..476231621
--- /dev/null
+++ b/app/components/reaction_picker/reaction_picker.test.js
@@ -0,0 +1,35 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React from 'react';
+import {shallow} from 'enzyme';
+
+import ReactionPicker from './reaction_picker';
+
+import Preferences from 'mattermost-redux/constants/preferences';
+
+describe('Reactions', () => {
+ const baseProps = {
+ theme: Preferences.THEMES.default,
+ recentEmojis: [],
+ addReaction: jest.fn(),
+ openReactionScreen: jest.fn(),
+ };
+
+ test('Should match snapshot with default emojis', () => {
+ const wrapper = shallow();
+ expect(wrapper.getElement()).toMatchSnapshot();
+ });
+
+ test('Should match snapshot with default emojis', () => {
+ const props = {
+ ...baseProps,
+ recentEmojis: [
+ 'rocket',
+ '100',
+ ],
+ };
+ const wrapper = shallow();
+ expect(wrapper.getElement()).toMatchSnapshot();
+ });
+});
diff --git a/app/components/reactions/__snapshots__/reactions.test.js.snap b/app/components/reactions/__snapshots__/reactions.test.js.snap
index 5e9a4d9ba..f7f0aad32 100644
--- a/app/components/reactions/__snapshots__/reactions.test.js.snap
+++ b/app/components/reactions/__snapshots__/reactions.test.js.snap
@@ -153,7 +153,7 @@ exports[`Reactions should match snapshot 1`] = `
}
style={
Object {
- "height": 20,
+ "height": 23,
"tintColor": "rgba(61,60,64,0.5)",
"width": 23,
}
diff --git a/app/components/reactions/reactions.js b/app/components/reactions/reactions.js
index 508e6efff..5406cac1a 100644
--- a/app/components/reactions/reactions.js
+++ b/app/components/reactions/reactions.js
@@ -186,7 +186,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
addReaction: {
tintColor: changeOpacity(theme.centerChannelColor, 0.5),
width: 23,
- height: 20,
+ height: 23,
},
reaction: {
alignItems: 'center',
diff --git a/app/constants/reaction_picker.js b/app/constants/reaction_picker.js
new file mode 100644
index 000000000..c357cadd8
--- /dev/null
+++ b/app/constants/reaction_picker.js
@@ -0,0 +1,17 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+export const REACTION_PICKER_HEIGHT = 72;
+export const SMALL_ICON_BREAKPOINT = 410;
+export const SMALL_CONTAINER_SIZE = 44;
+export const LARGE_CONTAINER_SIZE = 48;
+export const SMALL_ICON_SIZE = 28;
+export const LARGE_ICON_SIZE = 32;
+export const DEFAULT_EMOJIS = [
+ 'thumbsup',
+ 'smiley',
+ 'white_check_mark',
+ 'heart',
+ 'eyes',
+ 'raised_hands',
+];
diff --git a/app/screens/post_options/__snapshots__/post_options.test.js.snap b/app/screens/post_options/__snapshots__/post_options.test.js.snap
index a706fa1f6..15a24c292 100644
--- a/app/screens/post_options/__snapshots__/post_options.test.js.snap
+++ b/app/screens/post_options/__snapshots__/post_options.test.js.snap
@@ -10,8 +10,8 @@ exports[`PostOptions should match snapshot, no option for system message to user
>
+
-
+
-
+
-
{
- const {canAddReaction} = this.props;
-
- if (canAddReaction) {
- const key = 'reaction';
- const icon = 'emoji';
- const message = {id: t('mobile.post_info.add_reaction'), defaultMessage: 'Add Reaction'};
- const onPress = this.handleAddReaction;
-
- return this.getOption(key, icon, message, onPress);
- }
-
- return null;
- };
-
getReplyOption = () => {
const {canReply} = this.props;
@@ -249,7 +237,6 @@ export default class PostOptions extends PureComponent {
getPostOptions = () => {
const actions = [
this.getReplyOption(),
- this.getAddReactionOption(),
this.getMarkAsUnreadOption(),
this.getCopyPermalink(),
this.getFlagOption(),
@@ -262,7 +249,7 @@ export default class PostOptions extends PureComponent {
return actions.filter((a) => a !== null);
};
- handleAddReaction = () => {
+ handleAddReactionScreen = () => {
const {theme} = this.props;
const {formatMessage} = this.context.intl;
@@ -287,6 +274,12 @@ export default class PostOptions extends PureComponent {
});
};
+ handleAddReaction = (emoji) => {
+ this.closeWithAnimation(() => {
+ this.handleAddReactionToPost(emoji);
+ });
+ }
+
handleAddReactionToPost = (emoji) => {
const {actions, post} = this.props;
@@ -402,13 +395,26 @@ export default class PostOptions extends PureComponent {
};
render() {
- const {deviceHeight, theme} = this.props;
+ const {deviceHeight, theme, canAddReaction} = this.props;
const options = this.getPostOptions();
+ let reactionHeight = 0;
+ let reactionPicker;
+
if (!options || !options.length) {
return null;
}
- const marginFromTop = deviceHeight - BOTTOM_MARGIN - ((options.length + 1) * OPTION_HEIGHT);
+ if (canAddReaction) {
+ reactionHeight = REACTION_PICKER_HEIGHT;
+ reactionPicker = (
+
+ );
+ }
+
+ const marginFromTop = deviceHeight - BOTTOM_MARGIN - ((options.length + 1) * OPTION_HEIGHT) - reactionHeight;
const initialPosition = getInitialPosition(deviceHeight, marginFromTop);
return (
@@ -422,6 +428,7 @@ export default class PostOptions extends PureComponent {
key={marginFromTop}
theme={theme}
>
+ {reactionPicker}
{options}
diff --git a/assets/base/images/icons/reaction.png b/assets/base/images/icons/reaction.png
index acf54ce13..eeec698d3 100644
Binary files a/assets/base/images/icons/reaction.png and b/assets/base/images/icons/reaction.png differ