diff --git a/__mocks__/react-native-device-info.js b/__mocks__/react-native-device-info.js index 270ca77ea..d3fa5fa66 100644 --- a/__mocks__/react-native-device-info.js +++ b/__mocks__/react-native-device-info.js @@ -1,5 +1,6 @@ export default { getModel: () => '', getReadableVersion: () => '', - getBundleId: () => '' + getBundleId: () => '', + isTablet: () => false }; diff --git a/__tests__/__snapshots__/Storyshots.test.js.snap b/__tests__/__snapshots__/Storyshots.test.js.snap index 83b88dfc3..3009165ff 100644 --- a/__tests__/__snapshots__/Storyshots.test.js.snap +++ b/__tests__/__snapshots__/Storyshots.test.js.snap @@ -9953,7 +9953,6 @@ exports[`Storyshots Message list 1`] = ` "borderColor": "#e1e5e8", "borderRadius": 4, "borderWidth": 1, - "maxWidth": 400, "minHeight": 200, "width": "100%", }, @@ -10211,7 +10210,6 @@ exports[`Storyshots Message list 1`] = ` "borderColor": "#e1e5e8", "borderRadius": 4, "borderWidth": 1, - "maxWidth": 400, "minHeight": 200, "width": "100%", }, @@ -11009,17 +11007,20 @@ exports[`Storyshots Message list 1`] = ` View @@ -11334,17 +11335,20 @@ exports[`Storyshots Message list 1`] = ` View @@ -11540,17 +11544,20 @@ exports[`Storyshots Message list 1`] = ` View @@ -11718,17 +11725,20 @@ exports[`Storyshots Message list 1`] = ` View @@ -12130,7 +12140,7 @@ exports[`Storyshots Message list 1`] = ` style={ Object { "alignItems": "center", - "alignSelf": "flex-end", + "alignSelf": "flex-start", "backgroundColor": "#f3f4f5", "borderColor": "#e1e5e8", "borderRadius": 4, @@ -12493,7 +12503,7 @@ exports[`Storyshots Message list 1`] = ` style={ Object { "alignItems": "center", - "alignSelf": "flex-end", + "alignSelf": "flex-start", "backgroundColor": "#f3f4f5", "borderColor": "#e1e5e8", "borderRadius": 4, @@ -19491,7 +19501,7 @@ exports[`Storyshots Message list 1`] = ` style={ Object { "alignItems": "center", - "alignSelf": "flex-end", + "alignSelf": "flex-start", "backgroundColor": "#f3f4f5", "borderColor": "#e1e5e8", "borderRadius": 4, @@ -20028,7 +20038,7 @@ exports[`Storyshots Message list 1`] = ` style={ Object { "alignItems": "center", - "alignSelf": "flex-end", + "alignSelf": "flex-start", "backgroundColor": "#f3f4f5", "borderColor": "#e1e5e8", "borderRadius": 4, @@ -20222,7 +20232,7 @@ exports[`Storyshots Message list 1`] = ` style={ Object { "alignItems": "center", - "alignSelf": "flex-end", + "alignSelf": "flex-start", "backgroundColor": "#f3f4f5", "borderColor": "#e1e5e8", "borderRadius": 4, diff --git a/android/app/src/main/res/layout/launch_screen.xml b/android/app/src/main/res/layout/launch_screen.xml index 7ec70ce1e..97c4ac273 100644 --- a/android/app/src/main/res/layout/launch_screen.xml +++ b/android/app/src/main/res/layout/launch_screen.xml @@ -1,6 +1,13 @@ - + \ No newline at end of file diff --git a/app/commands.js b/app/commands.js new file mode 100644 index 000000000..5c0ffd599 --- /dev/null +++ b/app/commands.js @@ -0,0 +1,187 @@ +/* eslint-disable no-bitwise */ +import { constants } from 'react-native-keycommands'; + +import I18n from './i18n'; + +const KEY_TYPING = '\t'; +const KEY_PREFERENCES = 'p'; +const KEY_SEARCH = 'f'; +const KEY_PREVIOUS_ROOM = '['; +const KEY_NEXT_ROOM = ']'; +const KEY_NEW_ROOM = __DEV__ ? 'e' : 'n'; +const KEY_ROOM_ACTIONS = __DEV__ ? 'b' : 'i'; +const KEY_UPLOAD = 'u'; +const KEY_REPLY = ';'; +const KEY_SERVER_SELECTION = __DEV__ ? 'o' : '`'; +const KEY_ADD_SERVER = __DEV__ ? 'l' : 'n'; +const KEY_SEND_MESSAGE = '\r'; +const KEY_SELECT = '123456789'; + +export const defaultCommands = [ + { + // Focus messageBox + input: KEY_TYPING, + modifierFlags: 0, + discoverabilityTitle: I18n.t('Type_message') + }, + { + // Send message on textInput to current room + input: KEY_SEND_MESSAGE, + modifierFlags: 0, + discoverabilityTitle: I18n.t('Send') + } +]; + +export const keyCommands = [ + { + // Open Preferences Modal + input: KEY_PREFERENCES, + modifierFlags: constants.keyModifierCommand, + discoverabilityTitle: I18n.t('Preferences') + }, + { + // Focus Room Search + input: KEY_SEARCH, + modifierFlags: constants.keyModifierCommand | constants.keyModifierAlternate, + discoverabilityTitle: I18n.t('Room_search') + }, + { + // Select a room by order using 1-9 + input: '1...9', + modifierFlags: constants.keyModifierCommand, + discoverabilityTitle: I18n.t('Room_selection') + }, + { + // Change room to next on Rooms List + input: KEY_NEXT_ROOM, + modifierFlags: constants.keyModifierCommand, + discoverabilityTitle: I18n.t('Next_room') + }, + { + // Change room to previous on Rooms List + input: KEY_PREVIOUS_ROOM, + modifierFlags: constants.keyModifierCommand, + discoverabilityTitle: I18n.t('Previous_room') + }, + { + // Open New Room Modal + input: KEY_NEW_ROOM, + modifierFlags: constants.keyModifierCommand, + discoverabilityTitle: I18n.t('New_room') + }, + { + // Open Room Actions + input: KEY_ROOM_ACTIONS, + modifierFlags: constants.keyModifierCommand, + discoverabilityTitle: I18n.t('Room_actions') + }, + { + // Upload a file to room + input: KEY_UPLOAD, + modifierFlags: constants.keyModifierCommand, + discoverabilityTitle: I18n.t('Upload_room') + }, + { + // Search Messages on current room + input: KEY_SEARCH, + modifierFlags: constants.keyModifierCommand, + discoverabilityTitle: I18n.t('Search_messages') + }, + { + // Scroll messages on current room + input: '↑ ↓', + modifierFlags: constants.keyModifierAlternate, + discoverabilityTitle: I18n.t('Scroll_messages') + }, + { + // Scroll up messages on current room + input: constants.keyInputUpArrow, + modifierFlags: constants.keyModifierAlternate + }, + { + // Scroll down messages on current room + input: constants.keyInputDownArrow, + modifierFlags: constants.keyModifierAlternate + }, + { + // Reply latest message with Quote + input: KEY_REPLY, + modifierFlags: constants.keyModifierCommand, + discoverabilityTitle: I18n.t('Reply_latest') + }, + { + // Open server dropdown + input: KEY_SERVER_SELECTION, + modifierFlags: constants.keyModifierCommand | constants.keyModifierAlternate, + discoverabilityTitle: I18n.t('Server_selection') + }, + { + // Select a server by order using 1-9 + input: '1...9', + modifierFlags: constants.keyModifierCommand | constants.keyModifierAlternate, + discoverabilityTitle: I18n.t('Server_selection_numbers') + }, + { + // Navigate to add new server + input: KEY_ADD_SERVER, + modifierFlags: constants.keyModifierCommand | constants.keyModifierAlternate, + discoverabilityTitle: I18n.t('Add_server') + }, + // Refers to select rooms on list + ...([1, 2, 3, 4, 5, 6, 7, 8, 9].map(value => ({ + input: `${ value }`, + modifierFlags: constants.keyModifierCommand + }))), + // Refers to select servers on list + ...([1, 2, 3, 4, 5, 6, 7, 8, 9].map(value => ({ + input: `${ value }`, + modifierFlags: constants.keyModifierCommand | constants.keyModifierAlternate + }))) +]; + +export const KEY_COMMAND = 'KEY_COMMAND'; + +export const commandHandle = (event, key, flags = []) => { + const { input, modifierFlags } = event; + let _flags = 0; + if (flags.includes('command') && flags.includes('alternate')) { + _flags = constants.keyModifierCommand | constants.keyModifierAlternate; + } else if (flags.includes('command')) { + _flags = constants.keyModifierCommand; + } else if (flags.includes('alternate')) { + _flags = constants.keyModifierAlternate; + } + return key.includes(input) && modifierFlags === _flags; +}; + +export const handleCommandTyping = event => commandHandle(event, KEY_TYPING); + +export const handleCommandSubmit = event => commandHandle(event, KEY_SEND_MESSAGE); + +export const handleCommandShowUpload = event => commandHandle(event, KEY_UPLOAD, ['command']); + +export const handleCommandScroll = event => commandHandle(event, [constants.keyInputUpArrow, constants.keyInputDownArrow], ['alternate']); + +export const handleCommandRoomActions = event => commandHandle(event, KEY_ROOM_ACTIONS, ['command']); + +export const handleCommandSearchMessages = event => commandHandle(event, KEY_SEARCH, ['command']); + +export const handleCommandReplyLatest = event => commandHandle(event, KEY_REPLY, ['command']); + +export const handleCommandSelectServer = event => commandHandle(event, KEY_SELECT, ['command', 'alternate']); + +export const handleCommandShowPreferences = event => commandHandle(event, KEY_PREFERENCES, ['command']); + +export const handleCommandSearching = event => commandHandle(event, KEY_SEARCH, ['command', 'alternate']); + +export const handleCommandSelectRoom = event => commandHandle(event, KEY_SELECT, ['command']); + +export const handleCommandPreviousRoom = event => commandHandle(event, KEY_PREVIOUS_ROOM, ['command']); + +export const handleCommandNextRoom = event => commandHandle(event, KEY_NEXT_ROOM, ['command']); + +export const handleCommandShowNewMessage = event => commandHandle(event, KEY_NEW_ROOM, ['command']); + +export const handleCommandAddNewServer = event => commandHandle(event, KEY_ADD_SERVER, ['command', 'alternate']); + +export const handleCommandOpenServerDropdown = event => commandHandle(event, KEY_SERVER_SELECTION, ['command', 'alternate']); diff --git a/app/constants/tablet.js b/app/constants/tablet.js new file mode 100644 index 000000000..16e62f6d0 --- /dev/null +++ b/app/constants/tablet.js @@ -0,0 +1,4 @@ +export const MAX_SIDEBAR_WIDTH = 321; +export const MAX_CONTENT_WIDTH = '90%'; +export const MAX_SCREEN_CONTENT_WIDTH = '45%'; +export const MIN_WIDTH_SPLIT_LAYOUT = 700; diff --git a/app/containers/Avatar.js b/app/containers/Avatar.js index c616788cf..1ad9e93ba 100644 --- a/app/containers/Avatar.js +++ b/app/containers/Avatar.js @@ -2,13 +2,14 @@ import React from 'react'; import PropTypes from 'prop-types'; import { View } from 'react-native'; import FastImage from 'react-native-fast-image'; +import Touch from '../utils/touch'; const formatUrl = (url, baseUrl, uriSize, avatarAuthURLFragment) => ( `${ baseUrl }${ url }?format=png&width=${ uriSize }&height=${ uriSize }${ avatarAuthURLFragment }` ); const Avatar = React.memo(({ - text, size, baseUrl, borderRadius, style, avatar, type, children, userId, token + text, size, baseUrl, borderRadius, style, avatar, type, children, userId, token, onPress }) => { const avatarStyle = { width: size, @@ -39,7 +40,7 @@ const Avatar = React.memo(({ } - const image = ( + let image = ( ); + if (onPress) { + image = ( + + {image} + + ); + } + return ( {image} @@ -67,7 +76,8 @@ Avatar.propTypes = { type: PropTypes.string, children: PropTypes.object, userId: PropTypes.string, - token: PropTypes.string + token: PropTypes.string, + onPress: PropTypes.func }; Avatar.defaultProps = { diff --git a/app/containers/EmojiPicker/EmojiCategory.js b/app/containers/EmojiPicker/EmojiCategory.js index e5b572d97..e7a71f513 100644 --- a/app/containers/EmojiPicker/EmojiCategory.js +++ b/app/containers/EmojiPicker/EmojiCategory.js @@ -1,20 +1,18 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Text, TouchableOpacity } from 'react-native'; +import { Text, TouchableOpacity, FlatList } from 'react-native'; import { shortnameToUnicode } from 'emoji-toolkit'; import { responsive } from 'react-native-responsive-ui'; -import { OptimizedFlatList } from 'react-native-optimized-flatlist'; import styles from './styles'; import CustomEmoji from './CustomEmoji'; import scrollPersistTaps from '../../utils/scrollPersistTaps'; -import { isIOS } from '../../utils/deviceInfo'; -const EMOJIS_PER_ROW = isIOS ? 8 : 9; +const EMOJI_SIZE = 50; const renderEmoji = (emoji, size, baseUrl) => { - if (emoji.isCustom) { - return ; + if (emoji && emoji.isCustom) { + return ; } return ( @@ -33,44 +31,41 @@ class EmojiCategory extends React.Component { width: PropTypes.number } - constructor(props) { - super(props); - const { window, width, emojisPerRow } = this.props; - const { width: widthWidth, height: windowHeight } = window; - - this.size = Math.min(width || widthWidth, windowHeight) / (emojisPerRow || EMOJIS_PER_ROW); - this.emojis = props.emojis; - } - - shouldComponentUpdate() { - return false; - } - - renderItem(emoji, size) { + renderItem(emoji) { const { baseUrl, onEmojiSelected } = this.props; return ( onEmojiSelected(emoji)} - testID={`reaction-picker-${ emoji.isCustom ? emoji.content : emoji }`} + testID={`reaction-picker-${ emoji && emoji.isCustom ? emoji.content : emoji }`} > - {renderEmoji(emoji, size, baseUrl)} + {renderEmoji(emoji, EMOJI_SIZE, baseUrl)} ); } render() { - const { emojis } = this.props; + const { emojis, width } = this.props; + + if (!width) { + return null; + } + + const numColumns = Math.trunc(width / EMOJI_SIZE); + const marginHorizontal = (width - (numColumns * EMOJI_SIZE)) / 2; return ( - (item.isCustom && item.content) || item} + (item && item.isCustom && item.content) || item} data={emojis} - renderItem={({ item }) => this.renderItem(item, this.size)} - numColumns={EMOJIS_PER_ROW} + extraData={this.props} + renderItem={({ item }) => this.renderItem(item)} + numColumns={numColumns} initialNumToRender={45} - getItemLayout={(data, index) => ({ length: this.size, offset: this.size * index, index })} removeClippedSubviews {...scrollPersistTaps} /> diff --git a/app/containers/EmojiPicker/index.js b/app/containers/EmojiPicker/index.js index 05ec129c1..8ec587304 100644 --- a/app/containers/EmojiPicker/index.js +++ b/app/containers/EmojiPicker/index.js @@ -1,6 +1,6 @@ import React, { Component } from 'react'; +import { View } from 'react-native'; import PropTypes from 'prop-types'; -import { ScrollView } from 'react-native'; import ScrollableTabView from 'react-native-scrollable-tab-view'; import { shortnameToUnicode } from 'emoji-toolkit'; import equal from 'deep-equal'; @@ -27,9 +27,7 @@ class EmojiPicker extends Component { baseUrl: PropTypes.string.isRequired, customEmojis: PropTypes.object, onEmojiSelected: PropTypes.func, - tabEmojiStyle: PropTypes.object, - emojisPerRow: PropTypes.number, - width: PropTypes.number + tabEmojiStyle: PropTypes.object }; constructor(props) { @@ -44,7 +42,8 @@ class EmojiPicker extends Component { this.state = { frequentlyUsed: [], customEmojis, - show: false + show: false, + width: null }; } @@ -54,12 +53,11 @@ class EmojiPicker extends Component { } shouldComponentUpdate(nextProps, nextState) { - const { frequentlyUsed, show } = this.state; - const { width } = this.props; + const { frequentlyUsed, show, width } = this.state; if (nextState.show !== show) { return true; } - if (nextProps.width !== width) { + if (nextState.width !== width) { return true; } if (!equal(nextState.frequentlyUsed, frequentlyUsed)) { @@ -126,11 +124,11 @@ class EmojiPicker extends Component { this.setState({ frequentlyUsed }); } - renderCategory(category, i) { - const { frequentlyUsed, customEmojis } = this.state; - const { - emojisPerRow, width, baseUrl - } = this.props; + onLayout = ({ nativeEvent: { layout: { width } } }) => this.setState({ width }); + + renderCategory(category, i, label) { + const { frequentlyUsed, customEmojis, width } = this.state; + const { baseUrl } = this.props; let emojis = []; if (i === 0) { @@ -145,9 +143,9 @@ class EmojiPicker extends Component { emojis={emojis} onEmojiSelected={emoji => this.onEmojiSelected(emoji)} style={styles.categoryContainer} - size={emojisPerRow} width={width} baseUrl={baseUrl} + tabLabel={label} /> ); } @@ -160,26 +158,21 @@ class EmojiPicker extends Component { return null; } return ( - } - contentProps={scrollProps} - style={styles.background} - > - { - categories.tabs.map((tab, i) => ( - (i === 0 && frequentlyUsed.length === 0) ? null // when no frequentlyUsed don't show the tab - : ( - - {this.renderCategory(tab.category, i)} - - ))) - } - + + } + contentProps={scrollProps} + style={styles.background} + > + { + categories.tabs.map((tab, i) => ( + (i === 0 && frequentlyUsed.length === 0) ? null // when no frequentlyUsed don't show the tab + : ( + this.renderCategory(tab.category, i, tab.tabLabel) + ))) + } + + ); } } diff --git a/app/containers/EmojiPicker/styles.js b/app/containers/EmojiPicker/styles.js index d4e993f50..2efe1e0ec 100644 --- a/app/containers/EmojiPicker/styles.js +++ b/app/containers/EmojiPicker/styles.js @@ -56,6 +56,6 @@ export default StyleSheet.create({ textAlign: 'center' }, customCategoryEmoji: { - margin: 4 + margin: 8 } }); diff --git a/app/containers/ListItem.js b/app/containers/ListItem.js index 069329343..0149fe95b 100644 --- a/app/containers/ListItem.js +++ b/app/containers/ListItem.js @@ -35,11 +35,11 @@ const styles = StyleSheet.create({ }); const Content = React.memo(({ - title, subtitle, disabled, testID, right + title, subtitle, disabled, testID, right, color }) => ( - {title} + {title} {subtitle ? {subtitle} : null @@ -78,6 +78,7 @@ Content.propTypes = { subtitle: PropTypes.string, right: PropTypes.func, disabled: PropTypes.bool, + color: PropTypes.string, testID: PropTypes.string }; diff --git a/app/containers/MessageBox/EmojiKeyboard.js b/app/containers/MessageBox/EmojiKeyboard.js index 28e75c97c..a7943a850 100644 --- a/app/containers/MessageBox/EmojiKeyboard.js +++ b/app/containers/MessageBox/EmojiKeyboard.js @@ -20,7 +20,7 @@ export default class EmojiKeyboard extends React.PureComponent { render() { return ( - this.onEmojiSelected(emoji)} baseUrl={this.baseUrl} /> + ); } diff --git a/app/containers/MessageBox/UploadModal.js b/app/containers/MessageBox/UploadModal.js index 3773391b7..18a43e113 100644 --- a/app/containers/MessageBox/UploadModal.js +++ b/app/containers/MessageBox/UploadModal.js @@ -11,17 +11,20 @@ import TextInput from '../TextInput'; import Button from '../Button'; import I18n from '../../i18n'; import sharedStyles from '../../views/Styles'; -import { isIOS } from '../../utils/deviceInfo'; +import { isIOS, isTablet } from '../../utils/deviceInfo'; import { COLOR_PRIMARY, COLOR_BACKGROUND_CONTAINER, COLOR_WHITE } from '../../constants/colors'; import { CustomIcon } from '../../lib/Icons'; +import { withSplit } from '../../split'; const cancelButtonColor = COLOR_BACKGROUND_CONTAINER; const styles = StyleSheet.create({ modal: { - alignItems: 'center' + width: '100%', + alignItems: 'center', + margin: 0 }, titleContainer: { flexDirection: 'row', @@ -48,6 +51,9 @@ const styles = StyleSheet.create({ marginBottom: 16, resizeMode: 'contain' }, + bigPreview: { + height: 250 + }, buttonContainer: { flexDirection: 'row', justifyContent: 'space-between', @@ -91,7 +97,8 @@ class UploadModal extends Component { file: PropTypes.object, close: PropTypes.func, submit: PropTypes.func, - window: PropTypes.object + window: PropTypes.object, + split: PropTypes.bool } state = { @@ -113,11 +120,14 @@ class UploadModal extends Component { shouldComponentUpdate(nextProps, nextState) { const { name, description, file } = this.state; - const { window, isVisible } = this.props; + const { window, isVisible, split } = this.props; if (nextState.name !== name) { return true; } + if (nextProps.split !== split) { + return true; + } if (nextState.description !== description) { return true; } @@ -184,9 +194,9 @@ class UploadModal extends Component { } renderPreview() { - const { file } = this.props; + const { file, split } = this.props; if (file.mime && file.mime.match(/image/)) { - return (); + return (); } if (file.mime && file.mime.match(/video/)) { return ( @@ -200,7 +210,7 @@ class UploadModal extends Component { render() { const { - window: { width }, isVisible, close + window: { width }, isVisible, close, split } = this.props; const { name, description } = this.state; return ( @@ -215,7 +225,7 @@ class UploadModal extends Component { hideModalContentWhileAnimating avoidKeyboard > - + {I18n.t('Upload_file_question_mark')} @@ -240,4 +250,4 @@ class UploadModal extends Component { } } -export default responsive(UploadModal); +export default responsive(withSplit(UploadModal)); diff --git a/app/containers/MessageBox/index.js b/app/containers/MessageBox/index.js index cb7feb555..a4e9ef9f6 100644 --- a/app/containers/MessageBox/index.js +++ b/app/containers/MessageBox/index.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { - View, TextInput, Alert + View, TextInput, Alert, Keyboard } from 'react-native'; import { connect } from 'react-redux'; import { KeyboardAccessoryView } from 'react-native-keyboard-input'; @@ -25,8 +25,15 @@ import debounce from '../../utils/debounce'; import { COLOR_TEXT_DESCRIPTION } from '../../constants/colors'; import LeftButtons from './LeftButtons'; import RightButtons from './RightButtons'; -import { isAndroid } from '../../utils/deviceInfo'; +import { isAndroid, isTablet } from '../../utils/deviceInfo'; import { canUploadFile } from '../../utils/media'; +import EventEmiter from '../../utils/events'; +import { + KEY_COMMAND, + handleCommandTyping, + handleCommandSubmit, + handleCommandShowUpload +} from '../../commands'; import Mentions from './Mentions'; import MessageboxContext from './Context'; import { @@ -98,8 +105,8 @@ class MessageBox extends Component { commandPreview: [], showCommandPreview: false }; - this.onEmojiSelected = this.onEmojiSelected.bind(this); this.text = ''; + this.focused = false; this.fileOptions = [ I18n.t('Cancel'), I18n.t('Take_a_photo'), @@ -162,6 +169,10 @@ class MessageBox extends Component { if (isAndroid) { require('./EmojiKeyboard'); } + + if (isTablet) { + EventEmiter.addEventListener(KEY_COMMAND, this.handleCommands); + } } componentWillReceiveProps(nextProps) { @@ -239,12 +250,16 @@ class MessageBox extends Component { if (this.getSlashCommands && this.getSlashCommands.stop) { this.getSlashCommands.stop(); } + if (isTablet) { + EventEmiter.removeListener(KEY_COMMAND, this.handleCommands); + } } onChangeText = (text) => { const isTextEmpty = text.length === 0; this.setShowSend(!isTextEmpty); this.debouncedOnChangeText(text); + this.setInput(text); } // eslint-disable-next-line react/sort-comp @@ -253,7 +268,6 @@ class MessageBox extends Component { const isTextEmpty = text.length === 0; // this.setShowSend(!isTextEmpty); this.handleTyping(!isTextEmpty); - this.setInput(text); // matches if their is text that stats with '/' and group the command and params so we can use it "/command params" const slashCommand = text.match(/^\/([a-z0-9._-]+) (.+)/im); if (slashCommand) { @@ -453,7 +467,10 @@ class MessageBox extends Component { } setShowSend = (showSend) => { - this.setState({ showSend }); + const { showSend: prevShowSend } = this.state; + if (prevShowSend !== showSend) { + this.setState({ showSend }); + } } clearInput = () => { @@ -624,6 +641,7 @@ class MessageBox extends Component { const message = this.text; this.clearInput(); + this.debouncedOnChangeText.stop(); this.closeEmoji(); this.stopTrackingMention(); this.handleTyping(false); @@ -725,6 +743,21 @@ class MessageBox extends Component { }); } + handleCommands = ({ event }) => { + if (handleCommandTyping(event)) { + if (this.focused) { + Keyboard.dismiss(); + } else { + this.component.focus(); + } + this.focused = !this.focused; + } else if (handleCommandSubmit(event)) { + this.submit(); + } else if (handleCommandShowUpload(event)) { + this.showFileActions(); + } + } + renderContent = () => { const { recording, showEmojiKeyboard, showSend, mentions, trackingType, commandPreview, showCommandPreview @@ -733,6 +766,12 @@ class MessageBox extends Component { editing, message, replying, replyCancel, user, getCustomEmoji } = this.props; + const isAndroidTablet = isTablet && isAndroid ? { + multiline: false, + onSubmitEditing: this.submit, + returnKeyType: 'send' + } : {}; + if (recording) { return ; } @@ -773,6 +812,7 @@ class MessageBox extends Component { multiline placeholderTextColor={COLOR_TEXT_DESCRIPTION} testID='messagebox-input' + {...isAndroidTablet} /> ( ); const SearchBox = ({ - onChangeText, onSubmitEditing, testID, hasCancel, onCancelPress, ...props + onChangeText, onSubmitEditing, testID, hasCancel, onCancelPress, inputRef, ...props }) => ( - +