diff --git a/app/containers/MessageBox/FilesActions.js b/app/containers/MessageBox/FilesActions.js deleted file mode 100644 index 1850bacf804276f68ba7df7e2cb191afd41f2d18..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..ed21b839f672eef4728092193361ce4ab023d3a5 --- /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 <CancelEditingButton onPress={editCancel} />; + } + return ( + <ToggleEmojiButton + show={showEmojiKeyboard} + open={openEmoji} + close={closeEmoji} + /> + ); +}); + +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 0000000000000000000000000000000000000000..76a58d61b3e75efcecf77335d1333ce56ce3e19e --- /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 <CancelEditingButton onPress={editCancel} />; + } + return <FileButton onPress={showFileActions} />; +}); + +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 0000000000000000000000000000000000000000..6384fabf35fcf7b5a6dc563109107e1769243ead --- /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 <SendButton onPress={submit} />; + } + return ( + <React.Fragment> + <AudioButton onPress={recordAudioMessage} /> + <FileButton onPress={showFileActions} /> + </React.Fragment> + ); +}); + +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 0000000000000000000000000000000000000000..344e2fea504f640643ae5101b77c6163df217cdd --- /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 <SendButton onPress={submit} />; + } + return <AudioButton onPress={recordAudioMessage} />; +}); + +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 0000000000000000000000000000000000000000..a2beaecf875f1af11117810a6924aa63993d70ba --- /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 }) => ( + <BaseButton + onPress={onPress} + testID='messagebox-send-audio' + accessibilityLabel='Send_audio_message' + icon='mic' + /> +)); + +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 0000000000000000000000000000000000000000..b8ab18c4fa755aa339e5505d55160bdc2d98503d --- /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 +}) => ( + <BorderlessButton + onPress={onPress} + style={styles.actionButton} + testID={testID} + accessibilityLabel={I18n.t(accessibilityLabel)} + accessibilityTraits='button' + > + <CustomIcon name={icon} size={23} color={COLOR_PRIMARY} /> + </BorderlessButton> +)); + +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 0000000000000000000000000000000000000000..6cb44d765594fa2844f4d489af33109fc57c76fd --- /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 }) => ( + <BaseButton + onPress={onPress} + testID='messagebox-cancel-editing' + accessibilityLabel='Cancel_editing' + icon='cross' + /> +)); + +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 0000000000000000000000000000000000000000..45f09815e9ddf274cbbd18937ab49421c05044fa --- /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 }) => ( + <BaseButton + onPress={onPress} + testID='messagebox-actions' + accessibilityLabel='Message_actions' + icon='plus' + /> +)); + +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 0000000000000000000000000000000000000000..0c4291833ad611747d22689a33c198ff35a2e1a6 --- /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 }) => ( + <BaseButton + onPress={onPress} + testID='messagebox-send-message' + accessibilityLabel='Send_message' + icon='send1' + /> +)); + +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 0000000000000000000000000000000000000000..2a96ae5029b5fcc37b96009c900b9f41bd7ecc62 --- /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 ( + <BaseButton + onPress={close} + testID='messagebox-close-emoji' + accessibilityLabel='Close_emoji_selector' + icon='keyboard' + /> + ); + } + return ( + <BaseButton + onPress={open} + testID='messagebox-open-emoji' + accessibilityLabel='Open_emoji_selector' + icon='emoji' + /> + ); +}); + +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 0000000000000000000000000000000000000000..5046ca50e8867c429c704e523ff9982f733697c6 --- /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 d22da36385257f7628c0e6b2fb4b5deaf58aeade..ba07f598aa9efd26bf0df6872edf3cd43a5d7699 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 ( - <BorderlessButton - onPress={this.editCancel} - accessibilityLabel={I18n.t('Cancel_editing')} - accessibilityTraits='button' - style={styles.actionButton} - testID='messagebox-cancel-editing' - > - <CustomIcon - size={22} - color={COLOR_PRIMARY} - name='cross' - /> - </BorderlessButton> - ); - } - return !showEmojiKeyboard - ? ( - <BorderlessButton - onPress={this.openEmoji} - accessibilityLabel={I18n.t('Open_emoji_selector')} - accessibilityTraits='button' - style={styles.actionButton} - testID='messagebox-open-emoji' - > - <CustomIcon - size={22} - color={COLOR_PRIMARY} - name='emoji' - /> - </BorderlessButton> - ) - : ( - <BorderlessButton - onPress={this.closeEmoji} - accessibilityLabel={I18n.t('Close_emoji_selector')} - accessibilityTraits='button' - style={styles.actionButton} - testID='messagebox-close-emoji' - > - <CustomIcon - size={22} - color={COLOR_PRIMARY} - name='keyboard' - /> - </BorderlessButton> - ); - } - - get rightButtons() { - const { showSend } = this.state; - const icons = []; - - if (showSend) { - icons.push( - <BorderlessButton - key='send-message' - onPress={this.submit} - style={styles.actionButton} - testID='messagebox-send-message' - accessibilityLabel={I18n.t('Send message')} - accessibilityTraits='button' - > - <CustomIcon name='send1' size={23} color={COLOR_PRIMARY} /> - </BorderlessButton> - ); - return icons; - } - icons.push( - <BorderlessButton - key='audio-message' - onPress={this.recordAudioMessage} - style={styles.actionButton} - testID='messagebox-send-audio' - accessibilityLabel={I18n.t('Send audio message')} - accessibilityTraits='button' - > - <CustomIcon name='mic' size={23} color={COLOR_PRIMARY} /> - </BorderlessButton> - ); - icons.push( - <BorderlessButton - key='file-message' - onPress={this.toggleFilesActions} - style={styles.actionButton} - testID='messagebox-actions' - accessibilityLabel={I18n.t('Message actions')} - accessibilityTraits='button' - > - <CustomIcon name='plus' size={23} color={COLOR_PRIMARY} /> - </BorderlessButton> - ); - 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), - <Text key='mention-item-name' style={styles.mentionText}>:{ item.name || item }:</Text> - ] - : [ - <Avatar - key='mention-item-avatar' - style={{ margin: 8 }} - text={item.username || item.name} - size={30} - type={item.username ? 'd' : 'c'} - baseUrl={baseUrl} - userId={user.id} - token={user.token} - />, - <Text key='mention-item-name' style={styles.mentionText}>{ item.username || item.name }</Text> - ] + ? ( + <React.Fragment> + {this.renderMentionEmoji(item)} + <Text key='mention-item-name' style={styles.mentionText}>:{ item.name || item }:</Text> + </React.Fragment> + ) + : ( + <React.Fragment> + <Avatar + key='mention-item-avatar' + style={{ margin: 8 }} + text={item.username || item.name} + size={30} + type={item.username ? 'd' : 'c'} + baseUrl={baseUrl} + userId={user.id} + token={user.token} + /> + <Text key='mention-item-name' style={styles.mentionText}>{ item.username || item.name }</Text> + </React.Fragment> + ) } </TouchableOpacity> ); @@ -741,7 +667,7 @@ class MessageBox extends Component { return null; } return ( - <View key='messagebox-container' testID='messagebox-container'> + <View testID='messagebox-container'> <FlatList style={styles.mentionList} data={mentions} @@ -763,39 +689,30 @@ class MessageBox extends Component { return <ReplyPreview key='reply-preview' message={replyMessage} close={closeReply} username={user.username} />; }; - renderFilesActions = () => { - const { showFilesAction } = this.state; - - if (!showFilesAction) { - return null; - } - return ( - <FilesActions - key='files-actions' - hideActions={this.toggleFilesActions} - takePhoto={this.takePhoto} - chooseFromLibrary={this.chooseFromLibrary} - /> - ); - } - renderContent = () => { - const { recording } = this.state; + const { recording, showEmojiKeyboard, showSend } = this.state; const { editing } = this.props; if (recording) { return (<Recording onFinish={this.finishAudioMessage} />); } return ( - [ - this.renderMentions(), + <React.Fragment> + {this.renderMentions()} <View style={styles.composer} key='messagebox'> {this.renderReplyPreview()} <View style={[styles.textArea, editing && styles.editing]} testID='messagebox' > - {this.leftButtons} + <LeftButtons + showEmojiKeyboard={showEmojiKeyboard} + editing={editing} + showFileActions={this.showFileActions} + editCancel={this.editCancel} + openEmoji={this.openEmoji} + closeEmoji={this.closeEmoji} + /> <TextInput ref={component => this.component = component} style={styles.textBoxInput} @@ -810,19 +727,23 @@ class MessageBox extends Component { placeholderTextColor={COLOR_TEXT_DESCRIPTION} testID='messagebox-input' /> - {this.rightButtons} + <RightButtons + showSend={showSend} + submit={this.submit} + recordAudioMessage={this.recordAudioMessage} + showFileActions={this.showFileActions} + /> </View> </View> - ] + </React.Fragment> ); } render() { const { showEmojiKeyboard, file } = this.state; return ( - [ + <React.Fragment> <KeyboardAccessoryView - key='input' renderContent={this.renderContent} kbInputRef={this.component} kbComponent={showEmojiKeyboard ? 'EmojiKeyboard' : null} @@ -832,16 +753,14 @@ class MessageBox extends Component { // revealKeyboardInteractive requiresSameParentToManageScrollView addBottomView - />, - this.renderFilesActions(), + /> <UploadModal - key='upload-modal' isVisible={(file && file.isVisible)} file={file} close={() => this.setState({ file: {} })} submit={this.sendImageMessage} /> - ] + </React.Fragment> ); } }