diff --git a/app/components/autocomplete/at_mention/at_mention.js b/app/components/autocomplete/at_mention/at_mention.js index 85f606880..fc4284aca 100644 --- a/app/components/autocomplete/at_mention/at_mention.js +++ b/app/components/autocomplete/at_mention/at_mention.js @@ -27,16 +27,16 @@ export default class AtMention extends PureComponent { matchTerm: PropTypes.string, onChangeText: PropTypes.func.isRequired, outChannel: PropTypes.array, - postDraft: PropTypes.string, requestStatus: PropTypes.string.isRequired, teamMembers: PropTypes.array, - theme: PropTypes.object.isRequired + theme: PropTypes.object.isRequired, + value: PropTypes.string }; static defaultProps = { defaultChannel: {}, - postDraft: '', - isSearch: false + isSearch: false, + value: '' }; constructor(props) { @@ -144,8 +144,8 @@ export default class AtMention extends PureComponent { }; completeMention = (mention) => { - const {cursorPosition, isSearch, onChangeText, postDraft} = this.props; - const mentionPart = postDraft.substring(0, cursorPosition); + const {cursorPosition, isSearch, onChangeText, value} = this.props; + const mentionPart = value.substring(0, cursorPosition); let completedDraft; if (isSearch) { @@ -154,8 +154,8 @@ export default class AtMention extends PureComponent { completedDraft = mentionPart.replace(AT_MENTION_REGEX, `@${mention} `); } - if (postDraft.length > cursorPosition) { - completedDraft += postDraft.substring(cursorPosition); + if (value.length > cursorPosition) { + completedDraft += value.substring(cursorPosition); } onChangeText(completedDraft, true); diff --git a/app/components/autocomplete/at_mention/index.js b/app/components/autocomplete/at_mention/index.js index f6b5c1435..767797b61 100644 --- a/app/components/autocomplete/at_mention/index.js +++ b/app/components/autocomplete/at_mention/index.js @@ -19,25 +19,10 @@ import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import AtMention from './at_mention'; function mapStateToProps(state, ownProps) { - const {cursorPosition, isSearch, rootId} = ownProps; + const {cursorPosition, isSearch} = ownProps; const currentChannelId = getCurrentChannelId(state); - let postDraft = ''; - if (isSearch) { - postDraft = state.views.search; - } else if (ownProps.rootId) { - const threadDraft = state.views.thread.drafts[rootId]; - if (threadDraft) { - postDraft = threadDraft.draft; - } - } else if (currentChannelId) { - const channelDraft = state.views.channel.drafts[currentChannelId]; - if (channelDraft) { - postDraft = channelDraft.draft; - } - } - - const value = postDraft.substring(0, cursorPosition); + const value = ownProps.value.substring(0, cursorPosition); const matchTerm = getMatchTermForAtMention(value, isSearch); let teamMembers; @@ -54,14 +39,12 @@ function mapStateToProps(state, ownProps) { currentChannelId, currentTeamId: getCurrentTeamId(state), defaultChannel: getDefaultChannel(state), - postDraft, matchTerm, teamMembers, inChannel, outChannel, requestStatus: state.requests.users.autocompleteUsers.status, - theme: getTheme(state), - ...ownProps + theme: getTheme(state) }; } diff --git a/app/components/autocomplete/channel_mention/channel_mention.js b/app/components/autocomplete/channel_mention/channel_mention.js index af898187c..ad6eb0605 100644 --- a/app/components/autocomplete/channel_mention/channel_mention.js +++ b/app/components/autocomplete/channel_mention/channel_mention.js @@ -24,16 +24,16 @@ export default class ChannelMention extends PureComponent { myChannels: PropTypes.array, otherChannels: PropTypes.array, onChangeText: PropTypes.func.isRequired, - postDraft: PropTypes.string, privateChannels: PropTypes.array, publicChannels: PropTypes.array, requestStatus: PropTypes.string.isRequired, - theme: PropTypes.object.isRequired + theme: PropTypes.object.isRequired, + value: PropTypes.string }; static defaultProps = { - postDraft: '', - isSearch: false + isSearch: false, + value: '' }; constructor(props) { @@ -116,8 +116,8 @@ export default class ChannelMention extends PureComponent { } completeMention = (mention) => { - const {cursorPosition, isSearch, onChangeText, postDraft} = this.props; - const mentionPart = postDraft.substring(0, cursorPosition); + const {cursorPosition, isSearch, onChangeText, value} = this.props; + const mentionPart = value.substring(0, cursorPosition); let completedDraft; if (isSearch) { @@ -127,8 +127,8 @@ export default class ChannelMention extends PureComponent { completedDraft = mentionPart.replace(CHANNEL_MENTION_REGEX, `~${mention} `); } - if (postDraft.length > cursorPosition) { - completedDraft += postDraft.substring(cursorPosition); + if (value.length > cursorPosition) { + completedDraft += value.substring(cursorPosition); } onChangeText(completedDraft, true); diff --git a/app/components/autocomplete/channel_mention/index.js b/app/components/autocomplete/channel_mention/index.js index 54c3740b8..273aaaead 100644 --- a/app/components/autocomplete/channel_mention/index.js +++ b/app/components/autocomplete/channel_mention/index.js @@ -5,7 +5,6 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {searchChannels} from 'mattermost-redux/actions/channels'; -import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'; import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; import { @@ -20,25 +19,9 @@ import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import ChannelMention from './channel_mention'; function mapStateToProps(state, ownProps) { - const {cursorPosition, isSearch, rootId} = ownProps; - const currentChannelId = getCurrentChannelId(state); + const {cursorPosition, isSearch} = ownProps; - let postDraft = ''; - if (isSearch) { - postDraft = state.views.search; - } else if (rootId) { - const threadDraft = state.views.thread.drafts[rootId]; - if (threadDraft) { - postDraft = threadDraft.draft; - } - } else if (currentChannelId) { - const channelDraft = state.views.channel.drafts[currentChannelId]; - if (channelDraft) { - postDraft = channelDraft.draft; - } - } - - const value = postDraft.substring(0, cursorPosition); + const value = ownProps.value.substring(0, cursorPosition); const matchTerm = getMatchTermForChannelMention(value, isSearch); let myChannels; @@ -61,7 +44,6 @@ function mapStateToProps(state, ownProps) { privateChannels, currentTeamId: getCurrentTeamId(state), matchTerm, - postDraft, requestStatus: state.requests.channels.getChannels.status, theme: getTheme(state) }; diff --git a/app/components/autocomplete/emoji_suggestion/emoji_suggestion.js b/app/components/autocomplete/emoji_suggestion/emoji_suggestion.js index 33896b903..d4bf71806 100644 --- a/app/components/autocomplete/emoji_suggestion/emoji_suggestion.js +++ b/app/components/autocomplete/emoji_suggestion/emoji_suggestion.js @@ -22,25 +22,25 @@ export default class EmojiSuggestion extends Component { }).isRequired, cursorPosition: PropTypes.number, emojis: PropTypes.array.isRequired, - postDraft: PropTypes.string, theme: PropTypes.object.isRequired, onChangeText: PropTypes.func.isRequired, - rootId: PropTypes.string + rootId: PropTypes.string, + value: PropTypes.string }; static defaultProps = { defaultChannel: {}, - postDraft: '' + value: '' }; state = { active: false, dataSource: [] - } + }; componentWillReceiveProps(nextProps) { const regex = EMOJI_REGEX; - const match = nextProps.postDraft.substring(0, nextProps.cursorPosition).match(regex); + const match = nextProps.value.substring(0, nextProps.cursorPosition).match(regex); if (!match || this.state.emojiComplete) { this.setState({ @@ -74,8 +74,8 @@ export default class EmojiSuggestion extends Component { } completeSuggestion = (emoji) => { - const {actions, cursorPosition, onChangeText, postDraft, rootId} = this.props; - const emojiPart = postDraft.substring(0, cursorPosition); + const {actions, cursorPosition, onChangeText, value, rootId} = this.props; + const emojiPart = value.substring(0, cursorPosition); if (emojiPart.startsWith('+:')) { actions.addReactionToLatestPost(emoji, rootId); @@ -83,8 +83,8 @@ export default class EmojiSuggestion extends Component { } else { let completedDraft = emojiPart.replace(EMOJI_REGEX, `:${emoji}: `); - if (postDraft.length > cursorPosition) { - completedDraft += postDraft.substring(cursorPosition); + if (value.length > cursorPosition) { + completedDraft += value.substring(cursorPosition); } onChangeText(completedDraft); diff --git a/app/components/autocomplete/emoji_suggestion/index.js b/app/components/autocomplete/emoji_suggestion/index.js index 81ea1d342..e4fd2e6bf 100644 --- a/app/components/autocomplete/emoji_suggestion/index.js +++ b/app/components/autocomplete/emoji_suggestion/index.js @@ -25,26 +25,11 @@ const getEmojisByName = createSelector( } ); -function mapStateToProps(state, ownProps) { - const {currentChannelId} = state.entities.channels; +function mapStateToProps(state) { const emojis = getEmojisByName(state); - let postDraft; - if (ownProps.rootId) { - const threadDraft = state.views.thread.drafts[ownProps.rootId]; - if (threadDraft) { - postDraft = threadDraft.draft; - } - } else if (currentChannelId) { - const channelDraft = state.views.channel.drafts[currentChannelId]; - if (channelDraft) { - postDraft = channelDraft.draft; - } - } - return { emojis, - postDraft, theme: getTheme(state) }; } diff --git a/app/components/autocomplete/index.js b/app/components/autocomplete/index.js index 96001bdfb..1b2877272 100644 --- a/app/components/autocomplete/index.js +++ b/app/components/autocomplete/index.js @@ -17,7 +17,8 @@ export default class Autocomplete extends PureComponent { static propTypes = { onChangeText: PropTypes.func.isRequired, rootId: PropTypes.string, - isSearch: PropTypes.bool + isSearch: PropTypes.bool, + value: PropTypes.string }; static defaultProps = { diff --git a/app/components/post_textbox/components/paper_clip_icon.js b/app/components/post_textbox/components/paper_clip_icon.js index 4d494051a..aa988e383 100644 --- a/app/components/post_textbox/components/paper_clip_icon.js +++ b/app/components/post_textbox/components/paper_clip_icon.js @@ -16,7 +16,7 @@ export default class PaperClipIcon extends Component { if (this.root) { this.root.setNativeProps(nativeProps); } - } + }; render() { return ( diff --git a/app/components/post_textbox/post_textbox.js b/app/components/post_textbox/post_textbox.js index 857527700..83c5f255b 100644 --- a/app/components/post_textbox/post_textbox.js +++ b/app/components/post_textbox/post_textbox.js @@ -3,16 +3,7 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; -import { - Alert, - BackHandler, - Keyboard, - Platform, - Text, - TextInput, - TouchableOpacity, - View -} from 'react-native'; +import {Alert, BackHandler, Keyboard, Platform, Text, TextInput, TouchableOpacity, View} from 'react-native'; import {injectIntl, intlShape} from 'react-intl'; import {RequestStatus} from 'mattermost-redux/constants'; @@ -62,11 +53,16 @@ class PostTextbox extends PureComponent { value: '' }; - state = { - contentHeight: INITIAL_HEIGHT, - inputWidth: null, - keyboardType: 'default' - }; + constructor(props) { + super(props); + + this.state = { + contentHeight: INITIAL_HEIGHT, + inputWidth: null, + keyboardType: 'default', + value: props.value + }; + } componentDidMount() { if (Platform.OS === 'android') { @@ -76,8 +72,54 @@ class PostTextbox extends PureComponent { } componentWillReceiveProps(nextProps) { + if (nextProps.channelId !== this.props.channelId || nextProps.rootId !== this.props.rootId) { + this.setState({value: nextProps.value}); + } + } + + componentWillUnmount() { + if (Platform.OS === 'android') { + Keyboard.removeListener('keyboardDidHide', this.handleAndroidKeyboard); + BackHandler.removeEventListener('hardwareBackPress', this.handleAndroidBack); + } + } + + attachAutocomplete = (c) => { + this.autocomplete = c; + }; + + blur = () => { + this.refs.input.blur(); + }; + + canSend = () => { + const {files, uploadFileRequestStatus} = this.props; + const {value} = this.state; + const valueLength = value.trim().length; + + if (files.length) { + return valueLength <= MAX_MESSAGE_LENGTH && uploadFileRequestStatus !== RequestStatus.STARTED && files.filter((f) => !f.failed).length > 0; + } + + return valueLength > 0 && valueLength <= MAX_MESSAGE_LENGTH; + }; + + changeDraft = (text) => { + const { + actions, + channelId, + rootId + } = this.props; + + if (rootId) { + actions.handleCommentDraftChanged(rootId, text); + } else { + actions.handlePostDraftChanged(channelId, text); + } + }; + + checkMessageLength = (value) => { const {intl} = this.props; - const {value} = nextProps; const valueLength = value.trim().length; if (valueLength > MAX_MESSAGE_LENGTH) { @@ -95,28 +137,6 @@ class PostTextbox extends PureComponent { }) ); } - } - - componentWillUnmount() { - if (Platform.OS === 'android') { - Keyboard.removeListener('keyboardDidHide', this.handleAndroidKeyboard); - BackHandler.removeEventListener('hardwareBackPress', this.handleAndroidBack); - } - } - - blur = () => { - this.refs.input.blur(); - }; - - canSend = () => { - const {files, uploadFileRequestStatus, value} = this.props; - const valueLength = value.trim().length; - - if (files.length) { - return valueLength <= MAX_MESSAGE_LENGTH && uploadFileRequestStatus !== RequestStatus.STARTED && files.filter((f) => !f.failed).length > 0; - } - - return valueLength > 0 && valueLength <= MAX_MESSAGE_LENGTH; }; handleAndroidKeyboard = () => { @@ -132,12 +152,47 @@ class PostTextbox extends PureComponent { return false; }; + handleContentSizeChange = (event) => { + let contentHeight = event.nativeEvent.layout.height; + if (contentHeight < INITIAL_HEIGHT) { + contentHeight = INITIAL_HEIGHT; + } + + this.setState({ + contentHeight + }); + }; + + handleEndEditing = (e) => { + if (e && e.nativeEvent) { + this.changeDraft(e.nativeEvent.text || ''); + } + }; + + handleInputSizeChange = (event) => { + this.setState({ + inputWidth: event.nativeEvent.layout.width + }); + }; + + handlePostDraftSelectionChanged = (event) => { + const cursorPosition = event.nativeEvent.selection.end; + if (this.props.rootId) { + this.props.actions.handleCommentDraftSelectionChanged(this.props.rootId, cursorPosition); + } else { + this.props.actions.handlePostDraftSelectionChanged(this.props.channelId, cursorPosition); + } + + this.autocomplete.handleSelectionChange(event); + }; + handleSendMessage = () => { if (!this.canSend()) { return; } - const {files, value} = this.props; + const {files} = this.props; + const {value} = this.state; const isReactionMatch = value.match(IS_REACTION_REGEX); if (isReactionMatch) { @@ -171,8 +226,85 @@ class PostTextbox extends PureComponent { } }; + handleSubmit = () => { + // Workaround for android as the multiline is not working + if (Platform.OS === 'android') { + if (this.timeout) { + clearTimeout(this.timeout); + } + this.timeout = setTimeout(() => { + let {value: msg} = this.props; + msg += '\n'; + this.handleTextChange(msg); + }, 10); + } + }; + + handleTextChange = (value) => { + const { + actions, + channelId, + rootId + } = this.props; + + this.checkMessageLength(value); + this.setState({value}); + + if (value) { + actions.userTyping(channelId, rootId); + } + }; + + handleUploadFiles = (images) => { + this.props.actions.handleUploadFiles(images, this.props.rootId); + }; + + renderDisabledSendButton = () => { + const {theme} = this.props; + const style = getStyleSheet(theme); + + return ( + + + + + + ); + }; + + renderSendButton = () => { + const {theme, uploadFileRequestStatus} = this.props; + const style = getStyleSheet(theme); + + if (uploadFileRequestStatus === RequestStatus.STARTED) { + return this.renderDisabledSendButton(); + } else if (this.canSend()) { + return ( + + + + + + ); + } + + return null; + }; + sendMessage = () => { - const {actions, currentUserId, channelId, files, rootId, value} = this.props; + const {actions, currentUserId, channelId, files, rootId} = this.props; + const {value} = this.state; const postFiles = files.filter((f) => !f.failed); const post = { @@ -185,6 +317,7 @@ class PostTextbox extends PureComponent { actions.createPost(post, postFiles); this.handleTextChange(''); + this.changeDraft(''); if (postFiles.length) { actions.handleClearFiles(channelId, rootId); } @@ -205,139 +338,25 @@ class PostTextbox extends PureComponent { this.setState(nextState, callback); }; - handleUploadFiles = (images) => { - this.props.actions.handleUploadFiles(images, this.props.rootId); - }; - - changeDraft = (text) => { - const { - actions, - channelId, - rootId - } = this.props; - - if (rootId) { - actions.handleCommentDraftChanged(rootId, text); - } else { - actions.handlePostDraftChanged(channelId, text); - } - } - sendReaction = (emoji) => { const {actions, rootId} = this.props; actions.addReactionToLatestPost(emoji, rootId); this.handleTextChange(''); - } - - handleTextChange = (text) => { - const { - actions, - channelId, - rootId - } = this.props; - - this.changeDraft(text); - actions.userTyping(channelId, rootId); + this.changeDraft(''); }; - handleContentSizeChange = (event) => { - let contentHeight = event.nativeEvent.layout.height; - if (contentHeight < INITIAL_HEIGHT) { - contentHeight = INITIAL_HEIGHT; - } - - this.setState({ - contentHeight - }); - }; - - handleInputSizeChange = (event) => { - this.setState({ - inputWidth: event.nativeEvent.layout.width - }); - }; - - handleSubmit = () => { - // Workaround for android as the multiline is not working - if (Platform.OS === 'android') { - if (this.timeout) { - clearTimeout(this.timeout); - } - this.timeout = setTimeout(() => { - let {value: msg} = this.props; - msg += '\n'; - this.handleTextChange(msg); - }, 10); - } - }; - - attachAutocomplete = (c) => { - this.autocomplete = c; - }; - - renderDisabledSendButton = () => { - const {theme} = this.props; - const style = getStyleSheet(theme); - - return ( - - - - ); - } - - renderSendButton = () => { - const {theme, uploadFileRequestStatus} = this.props; - const style = getStyleSheet(theme); - - if (uploadFileRequestStatus === RequestStatus.STARTED) { - return this.renderDisabledSendButton(); - } else if (this.canSend()) { - return ( - - - - ); - } - - return null; - } - - handlePostDraftSelectionChanged = (event) => { - const cursorPosition = event.nativeEvent.selection.end; - if (this.props.rootId) { - this.props.actions.handleCommentDraftSelectionChanged(this.props.rootId, cursorPosition); - } else { - this.props.actions.handlePostDraftSelectionChanged(this.props.channelId, cursorPosition); - } - - this.autocomplete.handleSelectionChange(event); - } - render() { const { canUploadFiles, channelIsLoading, intl, - theme, - value + theme } = this.props; const style = getStyleSheet(theme); const textInputHeight = Math.min(this.state.contentHeight, MAX_CONTENT_HEIGHT); - const textValue = channelIsLoading ? '' : value; + const textValue = channelIsLoading ? '' : this.state.value; let placeholder; if (this.props.rootId) { @@ -378,8 +397,8 @@ class PostTextbox extends PureComponent { /> {attachmentButton} @@ -392,13 +411,14 @@ class PostTextbox extends PureComponent { placeholder={intl.formatMessage(placeholder)} placeholderTextColor={changeOpacity('#000', 0.5)} multiline={true} - numberOfLines={10} + numberOfLines={5} blurOnSubmit={false} underlineColorAndroid='transparent' style={[style.input, {height: textInputHeight}]} onSubmitEditing={this.handleSubmit} onLayout={this.handleInputSizeChange} keyboardType={this.state.keyboardType} + onEndEditing={this.handleEndEditing} /> {this.renderSendButton()} @@ -435,8 +455,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { flex: 1, flexDirection: 'row', backgroundColor: '#fff', - alignItems: 'flex-end', - marginRight: 10 + alignItems: 'flex-end' }, inputContainerWithoutFileUpload: { marginLeft: 10 @@ -449,6 +468,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { borderTopWidth: 1, borderTopColor: changeOpacity(theme.centerChannelColor, 0.20) }, + sendButtonContainer: { + paddingRight: 10 + }, sendButton: { backgroundColor: theme.buttonBg, borderRadius: 18, diff --git a/app/screens/search/search.js b/app/screens/search/search.js index 28fe618d2..5781f88f7 100644 --- a/app/screens/search/search.js +++ b/app/screens/search/search.js @@ -603,6 +603,7 @@ class Search extends PureComponent { ref={this.attachAutocomplete} onChangeText={this.handleTextChanged} isSearch={true} + value={value} /> {previewComponent}