diff --git a/app/actions/views/emoji.js b/app/actions/views/emoji.js new file mode 100644 index 000000000..6692fe113 --- /dev/null +++ b/app/actions/views/emoji.js @@ -0,0 +1,17 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {addReaction} from 'mattermost-redux/actions/posts'; +import {getPostsInCurrentChannel, makeGetPostsForThread} from 'mattermost-redux/selectors/entities/posts'; + +const getPostsForThread = makeGetPostsForThread(); + +export function addReactionToLatestPost(emoji, rootId) { + return async (dispatch, getState) => { + const state = getState(); + const posts = rootId ? getPostsForThread(state, {rootId}) : getPostsInCurrentChannel(state); + const lastPost = posts[0]; + + dispatch(addReaction(lastPost.id, emoji)); + }; +} diff --git a/app/components/autocomplete/emoji_suggestion/emoji_suggestion.js b/app/components/autocomplete/emoji_suggestion/emoji_suggestion.js index acafd07a2..33896b903 100644 --- a/app/components/autocomplete/emoji_suggestion/emoji_suggestion.js +++ b/app/components/autocomplete/emoji_suggestion/emoji_suggestion.js @@ -13,15 +13,19 @@ import { import Emoji from 'app/components/emoji'; import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme'; -const EMOJI_REGEX = /\B(:([^:\r\n\s]*))$/i; +const EMOJI_REGEX = /(^|\s|^\+|^-)(:([^:\s]*))$/i; export default class EmojiSuggestion extends Component { static propTypes = { + actions: PropTypes.shape({ + addReactionToLatestPost: PropTypes.func.isRequired + }).isRequired, cursorPosition: PropTypes.number, emojis: PropTypes.array.isRequired, postDraft: PropTypes.string, theme: PropTypes.object.isRequired, - onChangeText: PropTypes.func.isRequired + onChangeText: PropTypes.func.isRequired, + rootId: PropTypes.string }; static defaultProps = { @@ -47,7 +51,7 @@ export default class EmojiSuggestion extends Component { return; } - const matchTerm = match[2]; + const matchTerm = match[3]; if (matchTerm !== this.state.matchTerm) { this.setState({ matchTerm @@ -70,16 +74,22 @@ export default class EmojiSuggestion extends Component { } completeSuggestion = (emoji) => { - const {cursorPosition, onChangeText, postDraft} = this.props; + const {actions, cursorPosition, onChangeText, postDraft, rootId} = this.props; const emojiPart = postDraft.substring(0, cursorPosition); - let completedDraft = emojiPart.replace(EMOJI_REGEX, `:${emoji}: `); + if (emojiPart.startsWith('+:')) { + actions.addReactionToLatestPost(emoji, rootId); + onChangeText(''); + } else { + let completedDraft = emojiPart.replace(EMOJI_REGEX, `:${emoji}: `); - if (postDraft.length > cursorPosition) { - completedDraft += postDraft.substring(cursorPosition); + if (postDraft.length > cursorPosition) { + completedDraft += postDraft.substring(cursorPosition); + } + + onChangeText(completedDraft); } - onChangeText(completedDraft); this.setState({ active: false, emojiComplete: true diff --git a/app/components/autocomplete/emoji_suggestion/index.js b/app/components/autocomplete/emoji_suggestion/index.js index 847a36264..dce939e2a 100644 --- a/app/components/autocomplete/emoji_suggestion/index.js +++ b/app/components/autocomplete/emoji_suggestion/index.js @@ -3,9 +3,11 @@ import {connect} from 'react-redux'; import {createSelector} from 'reselect'; +import {bindActionCreators} from 'redux'; import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis'; +import {addReactionToLatestPost} from 'app/actions/views/emoji'; import {getTheme} from 'app/selectors/preferences'; import {EmojiIndicesByAlias} from 'app/utils/emojis'; @@ -47,4 +49,12 @@ function mapStateToProps(state, ownProps) { }; } -export default connect(mapStateToProps)(EmojiSuggestion); +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + addReactionToLatestPost + }, dispatch) + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(EmojiSuggestion); diff --git a/app/components/post_textbox/index.js b/app/components/post_textbox/index.js index 7be3b3ddb..26f0ee94c 100644 --- a/app/components/post_textbox/index.js +++ b/app/components/post_textbox/index.js @@ -6,6 +6,7 @@ import {connect} from 'react-redux'; import {createPost} from 'mattermost-redux/actions/posts'; import {userTyping} from 'mattermost-redux/actions/websocket'; +import {addReactionToLatestPost} from 'app/actions/views/emoji'; import {handleClearFiles, handleRemoveLastFile, handleUploadFiles} from 'app/actions/views/file_upload'; import {getTheme} from 'app/selectors/preferences'; import {canUploadFilesOnMobile} from 'mattermost-redux/selectors/entities/general'; @@ -29,6 +30,7 @@ function mapStateToProps(state, ownProps) { function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ + addReactionToLatestPost, createPost, handleClearFiles, handleRemoveLastFile, diff --git a/app/components/post_textbox/post_textbox.js b/app/components/post_textbox/post_textbox.js index 03485a910..ce9a0e0e8 100644 --- a/app/components/post_textbox/post_textbox.js +++ b/app/components/post_textbox/post_textbox.js @@ -27,10 +27,12 @@ import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; const INITIAL_HEIGHT = Platform.OS === 'ios' ? 34 : 36; const MAX_CONTENT_HEIGHT = 100; const MAX_MESSAGE_LENGTH = 4000; +const IS_REACTION_REGEX = /(^\+:([^:\s]*):)$/i; class PostTextbox extends PureComponent { static propTypes = { actions: PropTypes.shape({ + addReactionToLatestPost: PropTypes.func.isRequired, createPost: PropTypes.func.isRequired, handleClearFiles: PropTypes.func.isRequired, handleRemoveLastFile: PropTypes.func.isRequired, @@ -137,7 +139,16 @@ class PostTextbox extends PureComponent { return; } - const hasFailedImages = this.props.files.some((f) => f.failed); + const {files, value} = this.props; + + const isReactionMatch = value.match(IS_REACTION_REGEX); + if (isReactionMatch) { + const emoji = isReactionMatch[2]; + this.sendReaction(emoji); + return; + } + + const hasFailedImages = files.some((f) => f.failed); if (hasFailedImages) { const {intl} = this.props; @@ -163,19 +174,21 @@ class PostTextbox extends PureComponent { }; sendMessage = () => { - const files = this.props.files.filter((f) => !f.failed); + const {actions, currentUserId, channelId, files, rootId, value} = this.props; + + const postFiles = files.filter((f) => !f.failed); const post = { - user_id: this.props.currentUserId, - channel_id: this.props.channelId, - root_id: this.props.rootId, - parent_id: this.props.rootId, - message: this.props.value + user_id: currentUserId, + channel_id: channelId, + root_id: rootId, + parent_id: rootId, + message: value }; - this.props.actions.createPost(post, files); + actions.createPost(post, postFiles); this.handleTextChange(''); - if (this.props.files.length) { - this.props.actions.handleClearFiles(this.props.channelId, this.props.rootId); + if (postFiles.length) { + actions.handleClearFiles(channelId, rootId); } // Shrink the input textbox since the layout events lag slightly @@ -184,6 +197,12 @@ class PostTextbox extends PureComponent { }); }; + sendReaction = (emoji) => { + const {actions, rootId} = this.props; + actions.addReactionToLatestPost(emoji, rootId); + this.handleTextChange(''); + } + handleTextChange = (text) => { const { onChangeText,