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