From 9d79580946695f4bc53dfeb4a6aef3de9835d679 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Mon, 27 May 2019 13:19:39 -0300 Subject: [PATCH] [IMPROVEMENT] Messagebox typing and buttons refactor (#920) * Debounce onChangeText * Refactor FilesActions * Clear input asap * Different buttons on iOS/Android * Minor fragment refactor * Import emoji keyboard on android only --- app/containers/MessageBox/FilesActions.js | 63 ---- .../MessageBox/LeftButtons.android.js | 29 ++ app/containers/MessageBox/LeftButtons.ios.js | 21 ++ .../MessageBox/RightButtons.android.js | 27 ++ app/containers/MessageBox/RightButtons.ios.js | 21 ++ .../MessageBox/buttons/AudioButton.js | 19 ++ .../MessageBox/buttons/BaseButton.js | 31 ++ .../MessageBox/buttons/CancelEditingButton.js | 19 ++ .../MessageBox/buttons/FileButton.js | 19 ++ .../MessageBox/buttons/SendButton.js | 19 ++ .../MessageBox/buttons/ToggleEmojiButton.js | 33 +++ app/containers/MessageBox/buttons/index.js | 13 + app/containers/MessageBox/index.js | 279 +++++++----------- 13 files changed, 350 insertions(+), 243 deletions(-) delete mode 100644 app/containers/MessageBox/FilesActions.js create mode 100644 app/containers/MessageBox/LeftButtons.android.js create mode 100644 app/containers/MessageBox/LeftButtons.ios.js create mode 100644 app/containers/MessageBox/RightButtons.android.js create mode 100644 app/containers/MessageBox/RightButtons.ios.js create mode 100644 app/containers/MessageBox/buttons/AudioButton.js create mode 100644 app/containers/MessageBox/buttons/BaseButton.js create mode 100644 app/containers/MessageBox/buttons/CancelEditingButton.js create mode 100644 app/containers/MessageBox/buttons/FileButton.js create mode 100644 app/containers/MessageBox/buttons/SendButton.js create mode 100644 app/containers/MessageBox/buttons/ToggleEmojiButton.js create mode 100644 app/containers/MessageBox/buttons/index.js diff --git a/app/containers/MessageBox/FilesActions.js b/app/containers/MessageBox/FilesActions.js deleted file mode 100644 index 1850bacf..00000000 --- a/app/containers/MessageBox/FilesActions.js +++ /dev/null @@ -1,63 +0,0 @@ -import { PureComponent } from 'react'; -import PropTypes from 'prop-types'; -import ActionSheet from 'react-native-action-sheet'; - -import I18n from '../../i18n'; - -export default class FilesActions extends PureComponent { - static propTypes = { - hideActions: PropTypes.func.isRequired, - takePhoto: PropTypes.func.isRequired, - chooseFromLibrary: PropTypes.func.isRequired - } - - constructor(props) { - super(props); - - // Cancel - this.options = [I18n.t('Cancel')]; - this.CANCEL_INDEX = 0; - - // Photo - this.options.push(I18n.t('Take_a_photo')); - this.PHOTO_INDEX = 1; - - // Library - this.options.push(I18n.t('Choose_from_library')); - this.LIBRARY_INDEX = 2; - - setTimeout(() => { - this.showActionSheet(); - }); - } - - showActionSheet = () => { - ActionSheet.showActionSheetWithOptions({ - options: this.options, - cancelButtonIndex: this.CANCEL_INDEX - }, (actionIndex) => { - this.handleActionPress(actionIndex); - }); - } - - handleActionPress = (actionIndex) => { - const { takePhoto, chooseFromLibrary, hideActions } = this.props; - switch (actionIndex) { - case this.PHOTO_INDEX: - takePhoto(); - break; - case this.LIBRARY_INDEX: - chooseFromLibrary(); - break; - default: - break; - } - hideActions(); - } - - render() { - return ( - null - ); - } -} diff --git a/app/containers/MessageBox/LeftButtons.android.js b/app/containers/MessageBox/LeftButtons.android.js new file mode 100644 index 00000000..ed21b839 --- /dev/null +++ b/app/containers/MessageBox/LeftButtons.android.js @@ -0,0 +1,29 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { CancelEditingButton, ToggleEmojiButton } from './buttons'; + +const LeftButtons = React.memo(({ + showEmojiKeyboard, editing, editCancel, openEmoji, closeEmoji +}) => { + if (editing) { + return ; + } + return ( + + ); +}); + +LeftButtons.propTypes = { + showEmojiKeyboard: PropTypes.bool, + openEmoji: PropTypes.func.isRequired, + closeEmoji: PropTypes.func.isRequired, + editing: PropTypes.bool, + editCancel: PropTypes.func.isRequired +}; + +export default LeftButtons; diff --git a/app/containers/MessageBox/LeftButtons.ios.js b/app/containers/MessageBox/LeftButtons.ios.js new file mode 100644 index 00000000..76a58d61 --- /dev/null +++ b/app/containers/MessageBox/LeftButtons.ios.js @@ -0,0 +1,21 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { CancelEditingButton, FileButton } from './buttons'; + +const LeftButtons = React.memo(({ + showFileActions, editing, editCancel +}) => { + if (editing) { + return ; + } + return ; +}); + +LeftButtons.propTypes = { + showFileActions: PropTypes.func.isRequired, + editing: PropTypes.bool, + editCancel: PropTypes.func.isRequired +}; + +export default LeftButtons; diff --git a/app/containers/MessageBox/RightButtons.android.js b/app/containers/MessageBox/RightButtons.android.js new file mode 100644 index 00000000..6384fabf --- /dev/null +++ b/app/containers/MessageBox/RightButtons.android.js @@ -0,0 +1,27 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { SendButton, AudioButton, FileButton } from './buttons'; + +const RightButtons = React.memo(({ + showSend, submit, recordAudioMessage, showFileActions +}) => { + if (showSend) { + return ; + } + return ( + + + + + ); +}); + +RightButtons.propTypes = { + showSend: PropTypes.bool, + submit: PropTypes.func.isRequired, + recordAudioMessage: PropTypes.func.isRequired, + showFileActions: PropTypes.func.isRequired +}; + +export default RightButtons; diff --git a/app/containers/MessageBox/RightButtons.ios.js b/app/containers/MessageBox/RightButtons.ios.js new file mode 100644 index 00000000..344e2fea --- /dev/null +++ b/app/containers/MessageBox/RightButtons.ios.js @@ -0,0 +1,21 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { SendButton, AudioButton } from './buttons'; + +const RightButtons = React.memo(({ + showSend, submit, recordAudioMessage +}) => { + if (showSend) { + return ; + } + return ; +}); + +RightButtons.propTypes = { + showSend: PropTypes.bool, + submit: PropTypes.func.isRequired, + recordAudioMessage: PropTypes.func.isRequired +}; + +export default RightButtons; diff --git a/app/containers/MessageBox/buttons/AudioButton.js b/app/containers/MessageBox/buttons/AudioButton.js new file mode 100644 index 00000000..a2beaecf --- /dev/null +++ b/app/containers/MessageBox/buttons/AudioButton.js @@ -0,0 +1,19 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import BaseButton from './BaseButton'; + +const AudioButton = React.memo(({ onPress }) => ( + +)); + +AudioButton.propTypes = { + onPress: PropTypes.func.isRequired +}; + +export default AudioButton; diff --git a/app/containers/MessageBox/buttons/BaseButton.js b/app/containers/MessageBox/buttons/BaseButton.js new file mode 100644 index 00000000..b8ab18c4 --- /dev/null +++ b/app/containers/MessageBox/buttons/BaseButton.js @@ -0,0 +1,31 @@ +import React from 'react'; +import { BorderlessButton } from 'react-native-gesture-handler'; +import PropTypes from 'prop-types'; + +import { COLOR_PRIMARY } from '../../../constants/colors'; +import { CustomIcon } from '../../../lib/Icons'; +import styles from '../styles'; +import I18n from '../../../i18n'; + +const BaseButton = React.memo(({ + onPress, testID, accessibilityLabel, icon +}) => ( + + + +)); + +BaseButton.propTypes = { + onPress: PropTypes.func.isRequired, + testID: PropTypes.string.isRequired, + accessibilityLabel: PropTypes.string.isRequired, + icon: PropTypes.string.isRequired +}; + +export default BaseButton; diff --git a/app/containers/MessageBox/buttons/CancelEditingButton.js b/app/containers/MessageBox/buttons/CancelEditingButton.js new file mode 100644 index 00000000..6cb44d76 --- /dev/null +++ b/app/containers/MessageBox/buttons/CancelEditingButton.js @@ -0,0 +1,19 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import BaseButton from './BaseButton'; + +const CancelEditingButton = React.memo(({ onPress }) => ( + +)); + +CancelEditingButton.propTypes = { + onPress: PropTypes.func.isRequired +}; + +export default CancelEditingButton; diff --git a/app/containers/MessageBox/buttons/FileButton.js b/app/containers/MessageBox/buttons/FileButton.js new file mode 100644 index 00000000..45f09815 --- /dev/null +++ b/app/containers/MessageBox/buttons/FileButton.js @@ -0,0 +1,19 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import BaseButton from './BaseButton'; + +const FileButton = React.memo(({ onPress }) => ( + +)); + +FileButton.propTypes = { + onPress: PropTypes.func.isRequired +}; + +export default FileButton; diff --git a/app/containers/MessageBox/buttons/SendButton.js b/app/containers/MessageBox/buttons/SendButton.js new file mode 100644 index 00000000..0c429183 --- /dev/null +++ b/app/containers/MessageBox/buttons/SendButton.js @@ -0,0 +1,19 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import BaseButton from './BaseButton'; + +const SendButton = React.memo(({ onPress }) => ( + +)); + +SendButton.propTypes = { + onPress: PropTypes.func.isRequired +}; + +export default SendButton; diff --git a/app/containers/MessageBox/buttons/ToggleEmojiButton.js b/app/containers/MessageBox/buttons/ToggleEmojiButton.js new file mode 100644 index 00000000..2a96ae50 --- /dev/null +++ b/app/containers/MessageBox/buttons/ToggleEmojiButton.js @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import BaseButton from './BaseButton'; + +const ToggleEmojiButton = React.memo(({ show, open, close }) => { + if (show) { + return ( + + ); + } + return ( + + ); +}); + +ToggleEmojiButton.propTypes = { + show: PropTypes.bool, + open: PropTypes.func.isRequired, + close: PropTypes.func.isRequired +}; + +export default ToggleEmojiButton; diff --git a/app/containers/MessageBox/buttons/index.js b/app/containers/MessageBox/buttons/index.js new file mode 100644 index 00000000..5046ca50 --- /dev/null +++ b/app/containers/MessageBox/buttons/index.js @@ -0,0 +1,13 @@ +import CancelEditingButton from './CancelEditingButton'; +import ToggleEmojiButton from './ToggleEmojiButton'; +import SendButton from './SendButton'; +import AudioButton from './AudioButton'; +import FileButton from './FileButton'; + +export { + CancelEditingButton, + ToggleEmojiButton, + SendButton, + AudioButton, + FileButton +}; diff --git a/app/containers/MessageBox/index.js b/app/containers/MessageBox/index.js index d22da363..ba07f598 100644 --- a/app/containers/MessageBox/index.js +++ b/app/containers/MessageBox/index.js @@ -7,8 +7,8 @@ import { connect } from 'react-redux'; import { emojify } from 'react-emojione'; import { KeyboardAccessoryView } from 'react-native-keyboard-input'; import ImagePicker from 'react-native-image-crop-picker'; -import { BorderlessButton } from 'react-native-gesture-handler'; import equal from 'deep-equal'; +import ActionSheet from 'react-native-action-sheet'; import { userTyping as userTypingAction } from '../../actions/room'; import { @@ -23,15 +23,15 @@ import Avatar from '../Avatar'; import CustomEmoji from '../EmojiPicker/CustomEmoji'; import { emojis } from '../../emojis'; import Recording from './Recording'; -import FilesActions from './FilesActions'; import UploadModal from './UploadModal'; -import './EmojiKeyboard'; import log from '../../utils/log'; import I18n from '../../i18n'; import ReplyPreview from './ReplyPreview'; -import { CustomIcon } from '../../lib/Icons'; import debounce from '../../utils/debounce'; -import { COLOR_PRIMARY, COLOR_TEXT_DESCRIPTION } from '../../constants/colors'; +import { COLOR_TEXT_DESCRIPTION } from '../../constants/colors'; +import LeftButtons from './LeftButtons'; +import RightButtons from './RightButtons'; +import { isAndroid } from '../../utils/deviceInfo'; const MENTIONS_TRACKING_TYPE_USERS = '@'; const MENTIONS_TRACKING_TYPE_EMOJIS = ':'; @@ -48,6 +48,17 @@ const imagePickerConfig = { cropperCancelText: I18n.t('Cancel') }; +const fileOptions = [I18n.t('Cancel')]; +const FILE_CANCEL_INDEX = 0; + +// Photo +fileOptions.push(I18n.t('Take_a_photo')); +const FILE_PHOTO_INDEX = 1; + +// Library +fileOptions.push(I18n.t('Choose_from_library')); +const FILE_LIBRARY_INDEX = 2; + class MessageBox extends Component { static propTypes = { rid: PropTypes.string.isRequired, @@ -77,7 +88,6 @@ class MessageBox extends Component { this.state = { mentions: [], showEmojiKeyboard: false, - showFilesAction: false, showSend: false, recording: false, trackingType: '', @@ -111,6 +121,10 @@ class MessageBox extends Component { this.setInput(msg); this.setShowSend(true); } + + if (isAndroid) { + require('./EmojiKeyboard'); + } } componentWillReceiveProps(nextProps) { @@ -133,7 +147,7 @@ class MessageBox extends Component { shouldComponentUpdate(nextProps, nextState) { const { - showEmojiKeyboard, showFilesAction, showSend, recording, mentions, file + showEmojiKeyboard, showSend, recording, mentions, file } = this.state; const { roomType, replying, editing, isFocused @@ -153,9 +167,6 @@ class MessageBox extends Component { if (nextState.showEmojiKeyboard !== showEmojiKeyboard) { return true; } - if (nextState.showFilesAction !== showFilesAction) { - return true; - } if (nextState.showSend !== showSend) { return true; } @@ -171,32 +182,25 @@ class MessageBox extends Component { return false; } - onChangeText = (text) => { + onChangeText = debounce((text) => { const isTextEmpty = text.length === 0; this.setShowSend(!isTextEmpty); this.handleTyping(!isTextEmpty); - this.debouncedOnChangeText(text); - } - - // eslint-disable-next-line react/sort-comp - debouncedOnChangeText = debounce((text) => { this.setInput(text); if (this.component) { - requestAnimationFrame(() => { - const { start, end } = this.component._lastNativeSelection; - const cursor = Math.max(start, end); - const lastNativeText = this.component._lastNativeText; - const regexp = /(#|@|:)([a-z0-9._-]+)$/im; - const result = lastNativeText.substr(0, cursor).match(regexp); - if (!result) { - return this.stopTrackingMention(); - } - const [, lastChar, name] = result; - this.identifyMentionKeyword(name, lastChar); - }); + const { start, end } = this.component._lastNativeSelection; + const cursor = Math.max(start, end); + const lastNativeText = this.component._lastNativeText; + const regexp = /(#|@|:)([a-z0-9._-]+)$/im; + const result = lastNativeText.substr(0, cursor).match(regexp); + if (!result) { + return this.stopTrackingMention(); + } + const [, lastChar, name] = result; + this.identifyMentionKeyword(name, lastChar); } - }, 100); + }, 100) onKeyboardResigned = () => { this.closeEmoji(); @@ -239,106 +243,6 @@ class MessageBox extends Component { this.setShowSend(true); } - get leftButtons() { - const { showEmojiKeyboard } = this.state; - const { editing } = this.props; - - if (editing) { - return ( - - - - ); - } - return !showEmojiKeyboard - ? ( - - - - ) - : ( - - - - ); - } - - get rightButtons() { - const { showSend } = this.state; - const icons = []; - - if (showSend) { - icons.push( - - - - ); - return icons; - } - icons.push( - - - - ); - icons.push( - - - - ); - return icons; - } - getPermalink = async(message) => { try { return await RocketChat.getPermalink(message); @@ -495,10 +399,6 @@ class MessageBox extends Component { this.setShowSend(false); } - toggleFilesActions = () => { - this.setState(prevState => ({ showFilesAction: !prevState.showFilesAction })); - } - sendImageMessage = async(file) => { const { rid, tmid } = this.props; @@ -540,6 +440,28 @@ class MessageBox extends Component { this.setState({ file: { ...file, isVisible: true } }); } + showFileActions = () => { + ActionSheet.showActionSheetWithOptions({ + options: fileOptions, + cancelButtonIndex: FILE_CANCEL_INDEX + }, (actionIndex) => { + this.handleFileActionPress(actionIndex); + }); + } + + handleFileActionPress = (actionIndex) => { + switch (actionIndex) { + case FILE_PHOTO_INDEX: + this.takePhoto(); + break; + case FILE_LIBRARY_INDEX: + this.chooseFromLibrary(); + break; + default: + break; + } + } + editCancel = () => { const { editCancel } = this.props; editCancel(); @@ -585,6 +507,7 @@ class MessageBox extends Component { } = this.props; const message = this.text; + this.clearInput(); this.closeEmoji(); this.stopTrackingMention(); this.handleTyping(false); @@ -629,7 +552,6 @@ class MessageBox extends Component { } else { onSubmit(message); } - this.clearInput(); } updateMentions = (keyword, type) => { @@ -713,23 +635,27 @@ class MessageBox extends Component { testID={`mention-item-${ trackingType === MENTIONS_TRACKING_TYPE_EMOJIS ? item.name || item : item.username || item.name }`} > {trackingType === MENTIONS_TRACKING_TYPE_EMOJIS - ? [ - this.renderMentionEmoji(item), - :{ item.name || item }: - ] - : [ - , - { item.username || item.name } - ] + ? ( + + {this.renderMentionEmoji(item)} + :{ item.name || item }: + + ) + : ( + + + { item.username || item.name } + + ) } ); @@ -741,7 +667,7 @@ class MessageBox extends Component { return null; } return ( - + ; }; - renderFilesActions = () => { - const { showFilesAction } = this.state; - - if (!showFilesAction) { - return null; - } - return ( - - ); - } - renderContent = () => { - const { recording } = this.state; + const { recording, showEmojiKeyboard, showSend } = this.state; const { editing } = this.props; if (recording) { return (); } return ( - [ - this.renderMentions(), + + {this.renderMentions()} {this.renderReplyPreview()} - {this.leftButtons} + this.component = component} style={styles.textBoxInput} @@ -810,19 +727,23 @@ class MessageBox extends Component { placeholderTextColor={COLOR_TEXT_DESCRIPTION} testID='messagebox-input' /> - {this.rightButtons} + - ] + ); } render() { const { showEmojiKeyboard, file } = this.state; return ( - [ + , - this.renderFilesActions(), + /> this.setState({ file: {} })} submit={this.sendImageMessage} /> - ] + ); } }