Fix autocomplete showing behind the keyboard on iOS and not working on Android (#2830)

* Fix autocomplete showing behind the keyboard on iOS and not working on Android

* Unbundle config for Android

* Dismiss keyboard on post long press, fix scroll to bottom on new message and update tests

* Add a timeout before scrolling to give time to render the last post

* Fix crash on Android
This commit is contained in:
Elias Nahum 2019-05-27 19:18:43 -04:00
parent 03c3b91c1c
commit 2c7116bc7f
No known key found for this signature in database
GPG key ID: E038DB71E0B61702
19 changed files with 644 additions and 326 deletions

View file

@ -4,6 +4,7 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Keyboard,
ScrollView,
TouchableOpacity,
View,
@ -204,7 +205,10 @@ export default class PostBody extends PureComponent {
},
};
navigator.showModal(options);
Keyboard.dismiss();
requestAnimationFrame(() => {
navigator.showModal(options);
});
};
renderAddChannelMember = (style, messageStyle, textStyles) => {

View file

@ -83,6 +83,7 @@ export default class PostList extends PureComponent {
this.hasDoneInitialScroll = false;
this.contentOffsetY = 0;
this.shouldScrollToBottom = false;
this.makeExtraData = makeExtraData();
this.flatListRef = React.createRef();
@ -92,7 +93,7 @@ export default class PostList extends PureComponent {
}
componentDidMount() {
EventEmitter.on('scroll-to-bottom', this.scrollToBottom);
EventEmitter.on('scroll-to-bottom', this.handleSetScrollToBottom);
}
componentWillReceiveProps(nextProps) {
@ -102,15 +103,22 @@ export default class PostList extends PureComponent {
}
}
componentDidUpdate() {
if (this.props.deepLinkURL) {
this.handleDeepLink(this.props.deepLinkURL);
this.props.actions.setDeepLinkURL('');
componentDidUpdate(prevProps) {
const {actions, channelId, deepLinkURL, postIds} = this.props;
if (deepLinkURL && deepLinkURL !== prevProps.deepLinkURL) {
this.handleDeepLink(deepLinkURL);
actions.setDeepLinkURL('');
}
if (this.shouldScrollToBottom && prevProps.channelId === channelId && prevProps.postIds.length === postIds.length) {
this.scrollToBottom();
this.shouldScrollToBottom = false;
}
}
componentWillUnmount() {
EventEmitter.off('scroll-to-bottom', this.scrollToBottom);
EventEmitter.off('scroll-to-bottom', this.handleSetScrollToBottom);
}
handleClosePermalink = () => {
@ -199,6 +207,10 @@ export default class PostList extends PureComponent {
});
};
handleSetScrollToBottom = () => {
this.shouldScrollToBottom = true;
}
keyExtractor = (item) => {
// All keys are strings (either post IDs or special keys)
return item;
@ -268,7 +280,9 @@ export default class PostList extends PureComponent {
};
scrollToBottom = () => {
this.flatListRef.current.scrollToOffset({offset: 0, animated: true});
setTimeout(() => {
this.flatListRef.current.scrollToOffset({offset: 0, animated: true});
}, 250);
};
scrollToInitialIndexIfNeeded = (width, height) => {

View file

@ -0,0 +1,49 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import Autocomplete from 'app/components/autocomplete';
import FileUploadPreview from 'app/components/file_upload_preview';
import Typing from './components/typing';
import PostTextBoxBase from './post_textbox_base';
const AUTOCOMPLETE_MARGIN = 20;
const AUTOCOMPLETE_MAX_HEIGHT = 200;
export default class PostTextBoxAndroid extends PostTextBoxBase {
render() {
const {
channelId,
deactivatedChannel,
files,
rootId,
} = this.props;
if (deactivatedChannel) {
return this.renderDeactivatedChannel();
}
const {cursorPosition, top} = this.state;
return (
<React.Fragment>
<Typing/>
<FileUploadPreview
channelId={channelId}
files={files}
rootId={rootId}
/>
<Autocomplete
cursorPosition={cursorPosition}
maxHeight={Math.min(top - AUTOCOMPLETE_MARGIN, AUTOCOMPLETE_MAX_HEIGHT)}
onChangeText={this.handleTextChange}
value={this.state.value}
rootId={rootId}
/>
{this.renderTextBox()}
</React.Fragment>
);
}
}

View file

@ -0,0 +1,24 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import Typing from './components/typing';
import PostTextBoxBase from './post_textbox_base';
export default class PostTextBoxIOS extends PostTextBoxBase {
render() {
const {deactivatedChannel} = this.props;
if (deactivatedChannel) {
return this.renderDeactivatedChannel();
}
return (
<React.Fragment>
<Typing/>
{this.renderTextBox()}
</React.Fragment>
);
}
}

View file

@ -3,7 +3,17 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Alert, BackHandler, findNodeHandle, Keyboard, NativeModules, Platform, Text, TextInput, View} from 'react-native';
import {
Alert,
BackHandler,
findNodeHandle,
Keyboard,
NativeModules,
Platform,
Text,
TextInput,
View,
} from 'react-native';
import {intlShape} from 'react-intl';
import Button from 'react-native-button';
import {General, RequestStatus} from 'mattermost-redux/constants';
@ -12,20 +22,19 @@ import {getFormattedFileSize} from 'mattermost-redux/utils/file_utils';
import AttachmentButton from 'app/components/attachment_button';
import Fade from 'app/components/fade';
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT, IS_REACTION_REGEX, MAX_CONTENT_HEIGHT, MAX_FILE_COUNT} from 'app/constants/post_textbox';
import {confirmOutOfOfficeDisabled} from 'app/utils/status';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
import FormattedMarkdownText from 'app/components/formatted_markdown_text';
import FormattedText from 'app/components/formatted_text';
import SendButton from 'app/components/send_button';
import Typing from './components/typing';
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT, IS_REACTION_REGEX, MAX_CONTENT_HEIGHT, MAX_FILE_COUNT} from 'app/constants/post_textbox';
import {t} from 'app/utils/i18n';
import {confirmOutOfOfficeDisabled} from 'app/utils/status';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
const PLACEHOLDER_COLOR = changeOpacity('#000', 0.5);
const {RNTextInputReset} = NativeModules;
export default class PostTextbox extends PureComponent {
export default class PostTextBoxBase extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
addReactionToLatestPost: PropTypes.func.isRequired,
@ -172,6 +181,60 @@ export default class PostTextbox extends PureComponent {
}
};
getAttachmentButton = () => {
const {canUploadFiles, channelIsReadOnly, files, maxFileSize, navigator, theme} = this.props;
let attachmentButton = null;
if (canUploadFiles && !channelIsReadOnly) {
attachmentButton = (
<AttachmentButton
blurTextBox={this.blur}
theme={theme}
navigator={navigator}
fileCount={files.length}
maxFileSize={maxFileSize}
maxFileCount={MAX_FILE_COUNT}
onShowFileMaxWarning={this.onShowFileMaxWarning}
onShowFileSizeWarning={this.onShowFileSizeWarning}
uploadFiles={this.handleUploadFiles}
/>
);
}
return attachmentButton;
};
getInputContainerStyle = () => {
const {canUploadFiles, channelIsReadOnly, theme} = this.props;
const style = getStyleSheet(theme);
const inputContainerStyle = [style.inputContainer];
if (!canUploadFiles) {
inputContainerStyle.push(style.inputContainerWithoutFileUpload);
}
if (channelIsReadOnly) {
inputContainerStyle.push(style.readonlyContainer);
}
return inputContainerStyle;
};
getPlaceHolder = () => {
const {channelIsReadOnly, rootId} = this.props;
let placeholder;
if (channelIsReadOnly) {
placeholder = {id: t('mobile.create_post.read_only'), defaultMessage: 'This channel is read-only.'};
} else if (rootId) {
placeholder = {id: t('create_comment.addComment'), defaultMessage: 'Add a comment...'};
} else {
placeholder = {id: t('create_post.write'), defaultMessage: 'Write to {channelDisplayName}'};
}
return placeholder;
};
handleAndroidKeyboard = () => {
this.blur();
};
@ -270,20 +333,31 @@ export default class PostTextbox extends PureComponent {
actions,
channelId,
rootId,
cursorPositionEvent,
valueEvent,
} = this.props;
// Workaround for some Android keyboards that don't play well with cursors (e.g. Samsung keyboards)
if (autocomplete && Platform.OS === 'android' && this.input?.current) {
RNTextInputReset.resetKeyboardInput(findNodeHandle(this.input.current));
}
this.checkMessageLength(value);
if (!autocomplete && valueEvent) {
if (valueEvent) {
EventEmitter.emit(valueEvent, value);
}
this.setState({value});
const nextState = {value};
// Workaround for some Android keyboards that don't play well with cursors (e.g. Samsung keyboards)
if (autocomplete && this.input?.current) {
if (Platform.OS === 'android') {
RNTextInputReset.resetKeyboardInput(findNodeHandle(this.input.current));
} else {
nextState.cursorPosition = value.length;
if (cursorPositionEvent) {
EventEmitter.emit(cursorPositionEvent, nextState.cursorPosition);
}
}
}
this.checkMessageLength(value);
this.setState(nextState);
if (value) {
actions.userTyping(channelId, rootId);
@ -464,24 +538,26 @@ export default class PostTextbox extends PureComponent {
};
archivedView = (theme, style) => {
return (<View style={style.archivedWrapper}>
<FormattedMarkdownText
id='archivedChannelMessage'
defaultMessage='You are viewing an **archived channel**. New messages cannot be posted.'
theme={theme}
style={style.archivedText}
/>
<Button
containerStyle={style.closeButton}
onPress={this.onCloseChannelPress}
>
<FormattedText
id='center_panel.archived.closeChannel'
defaultMessage='Close Channel'
style={style.closeButtonText}
return (
<View style={style.archivedWrapper}>
<FormattedMarkdownText
id='archivedChannelMessage'
defaultMessage='You are viewing an **archived channel**. New messages cannot be posted.'
theme={theme}
style={style.archivedText}
/>
</Button>
</View>);
<Button
containerStyle={style.closeButton}
onPress={this.onCloseChannelPress}
>
<FormattedText
id='center_panel.archived.closeChannel'
defaultMessage='Close Channel'
style={style.closeButtonText}
/>
</Button>
</View>
);
};
handleLayout = (e) => {
@ -490,110 +566,67 @@ export default class PostTextbox extends PureComponent {
});
};
render() {
renderDeactivatedChannel = () => {
const {intl} = this.context;
const {
canUploadFiles,
channelDisplayName,
channelIsLoading,
channelIsReadOnly,
deactivatedChannel,
files,
maxFileSize,
navigator,
rootId,
theme,
channelIsArchived,
} = this.props;
const style = getStyleSheet(this.props.theme);
return (
<Text style={style.deactivatedMessage}>
{intl.formatMessage({
id: 'create_post.deactivated',
defaultMessage: 'You are viewing an archived channel with a deactivated user.',
})}
</Text>
);
}
renderTextBox = () => {
const {intl} = this.context;
const {channelDisplayName, channelIsArchived, channelIsLoading, channelIsReadOnly, theme} = this.props;
const style = getStyleSheet(theme);
if (deactivatedChannel) {
return (
<Text style={style.deactivatedMessage}>
{intl.formatMessage({
id: 'create_post.deactivated',
defaultMessage: 'You are viewing an archived channel with a deactivated user.',
})}
</Text>
);
if (channelIsArchived) {
return this.archivedView(theme, style);
}
const {value} = this.state;
const textValue = channelIsLoading ? '' : value;
let placeholder;
if (channelIsReadOnly) {
placeholder = {id: t('mobile.create_post.read_only'), defaultMessage: 'This channel is read-only.'};
} else if (rootId) {
placeholder = {id: t('create_comment.addComment'), defaultMessage: 'Add a comment...'};
} else {
placeholder = {id: t('create_post.write'), defaultMessage: 'Write to {channelDisplayName}'};
}
let attachmentButton = null;
const inputContainerStyle = [style.inputContainer];
if (canUploadFiles) {
attachmentButton = (
<AttachmentButton
blurTextBox={this.blur}
theme={theme}
navigator={navigator}
fileCount={files.length}
maxFileSize={maxFileSize}
maxFileCount={MAX_FILE_COUNT}
onShowFileMaxWarning={this.onShowFileMaxWarning}
onShowFileSizeWarning={this.onShowFileSizeWarning}
uploadFiles={this.handleUploadFiles}
/>
);
} else {
inputContainerStyle.push(style.inputContainerWithoutFileUpload);
}
if (channelIsReadOnly) {
inputContainerStyle.push(style.readonlyContainer);
}
const placeholder = this.getPlaceHolder();
return (
<React.Fragment>
<Typing/>
{!channelIsArchived && (
<View
style={style.inputWrapper}
onLayout={this.handleLayout}
>
{!channelIsReadOnly && attachmentButton}
<View style={inputContainerStyle}>
<TextInput
ref={this.input}
value={textValue}
onChangeText={this.handleTextChange}
onSelectionChange={this.handlePostDraftSelectionChanged}
placeholder={intl.formatMessage(placeholder, {channelDisplayName})}
placeholderTextColor={PLACEHOLDER_COLOR}
multiline={true}
blurOnSubmit={false}
underlineColorAndroid='transparent'
style={style.input}
keyboardType={this.state.keyboardType}
onEndEditing={this.handleEndEditing}
disableFullscreenUI={true}
editable={!channelIsReadOnly}
/>
<Fade visible={this.isSendButtonVisible()}>
<SendButton
disabled={this.isFileLoading()}
handleSendMessage={this.handleSendMessage}
theme={theme}
/>
</Fade>
</View>
</View>
)}
{channelIsArchived && this.archivedView(theme, style)}
</React.Fragment>
<View
style={style.inputWrapper}
onLayout={this.handleLayout}
>
{this.getAttachmentButton()}
<View style={this.getInputContainerStyle()}>
<TextInput
ref={this.input}
value={textValue}
onChangeText={this.handleTextChange}
onSelectionChange={this.handlePostDraftSelectionChanged}
placeholder={intl.formatMessage(placeholder, {channelDisplayName})}
placeholderTextColor={PLACEHOLDER_COLOR}
multiline={true}
blurOnSubmit={false}
underlineColorAndroid='transparent'
style={style.input}
keyboardType={this.state.keyboardType}
onEndEditing={this.handleEndEditing}
disableFullscreenUI={true}
editable={!channelIsReadOnly}
/>
<Fade visible={this.isSendButtonVisible()}>
<SendButton
disabled={this.isFileLoading()}
handleSendMessage={this.handleSendMessage}
theme={theme}
/>
</Fade>
</View>
</View>
);
}
};
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {

View file

@ -0,0 +1,57 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Dimensions, View} from 'react-native';
import ChannelLoader from 'app/components/channel_loader';
import KeyboardLayout from 'app/components/layout/keyboard_layout';
import NetworkIndicator from 'app/components/network_indicator';
import SafeAreaView from 'app/components/safe_area_view';
import StatusBar from 'app/components/status_bar';
import PostTextbox from 'app/components/post_textbox';
import LocalConfig from 'assets/config';
import ChannelNavBar from './channel_nav_bar';
import ChannelPostList from './channel_post_list';
import ChannelBase, {ClientUpgradeListener, style} from './channel_base';
export default class ChannelAndroid extends ChannelBase {
render() {
const {height} = Dimensions.get('window');
const {
navigator,
} = this.props;
const channelLoaderStyle = [style.channelLoader, {height}];
const drawerContent = (
<SafeAreaView navigator={navigator}>
<StatusBar/>
<NetworkIndicator/>
<ChannelNavBar
navigator={navigator}
openChannelDrawer={this.openChannelSidebar}
openSettingsDrawer={this.openSettingsSidebar}
onPress={this.goToChannelInfo}
/>
<KeyboardLayout>
<View style={style.flex}>
<ChannelPostList navigator={navigator}/>
</View>
<PostTextbox
ref={this.postTextbox}
navigator={navigator}
/>
</KeyboardLayout>
<ChannelLoader
height={height}
style={channelLoaderStyle}
/>
{LocalConfig.EnableMobileClientUpgrade && <ClientUpgradeListener navigator={navigator}/>}
</SafeAreaView>
);
return this.renderChannel(drawerContent);
}
}

View file

@ -0,0 +1,87 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Dimensions, View} from 'react-native';
import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view';
import Autocomplete, {AUTOCOMPLETE_MAX_HEIGHT} from 'app/components/autocomplete';
import ChannelLoader from 'app/components/channel_loader';
import FileUploadPreview from 'app/components/file_upload_preview';
import NetworkIndicator from 'app/components/network_indicator';
import PostTextbox from 'app/components/post_textbox';
import SafeAreaView from 'app/components/safe_area_view';
import StatusBar from 'app/components/status_bar';
import {DeviceTypes} from 'app/constants';
import LocalConfig from 'assets/config';
import ChannelBase, {ClientUpgradeListener, style} from './channel_base';
import ChannelNavBar from './channel_nav_bar';
import ChannelPostList from './channel_post_list';
const ACCESSORIES_CONTAINER_NATIVE_ID = 'channelAccessoriesContainer';
const CHANNEL_POST_TEXTBOX_CURSOR_CHANGE = 'onChannelTextBoxCursorChange';
const CHANNEL_POST_TEXTBOX_VALUE_CHANGE = 'onChannelTextBoxValueChange';
export default class ChannelIOS extends ChannelBase {
render() {
const {height} = Dimensions.get('window');
const {
currentChannelId,
navigator,
} = this.props;
const channelLoaderStyle = [style.channelLoader, {height}];
if ((DeviceTypes.IS_IPHONE_X || DeviceTypes.IS_TABLET)) {
channelLoaderStyle.push(style.iOSHomeIndicator);
}
const drawerContent = (
<React.Fragment>
<SafeAreaView navigator={navigator}>
<StatusBar/>
<NetworkIndicator/>
<ChannelNavBar
navigator={navigator}
openChannelDrawer={this.openChannelSidebar}
openSettingsDrawer={this.openSettingsSidebar}
onPress={this.goToChannelInfo}
/>
<ChannelPostList
navigator={navigator}
updateNativeScrollView={this.updateNativeScrollView}
/>
<View nativeID={ACCESSORIES_CONTAINER_NATIVE_ID}>
<FileUploadPreview channelId={currentChannelId}/>
<Autocomplete
maxHeight={AUTOCOMPLETE_MAX_HEIGHT}
onChangeText={this.handleAutoComplete}
cursorPositionEvent={CHANNEL_POST_TEXTBOX_CURSOR_CHANGE}
valueEvent={CHANNEL_POST_TEXTBOX_VALUE_CHANGE}
/>
</View>
<ChannelLoader
height={height}
style={channelLoaderStyle}
/>
{LocalConfig.EnableMobileClientUpgrade && <ClientUpgradeListener navigator={navigator}/>}
</SafeAreaView>
<KeyboardTrackingView
ref={this.keyboardTracker}
scrollViewNativeID={currentChannelId}
accessoriesContainerID={ACCESSORIES_CONTAINER_NATIVE_ID}
>
<PostTextbox
cursorPositionEvent={CHANNEL_POST_TEXTBOX_CURSOR_CHANGE}
valueEvent={CHANNEL_POST_TEXTBOX_VALUE_CHANGE}
ref={this.postTextbox}
navigator={navigator}
/>
</KeyboardTrackingView>
</React.Fragment>
);
return this.renderChannel(drawerContent);
}
}

View file

@ -5,47 +5,33 @@ import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {intlShape} from 'react-intl';
import {
Dimensions,
Keyboard,
Platform,
StyleSheet,
View,
} from 'react-native';
import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {app} from 'app/mattermost';
import Autocomplete, {AUTOCOMPLETE_MAX_HEIGHT} from 'app/components/autocomplete';
import InteractiveDialogController from 'app/components/interactive_dialog_controller';
import ChannelLoader from 'app/components/channel_loader';
import EmptyToolbar from 'app/components/start/empty_toolbar';
import FileUploadPreview from 'app/components/file_upload_preview';
import InteractiveDialogController from 'app/components/interactive_dialog_controller';
import MainSidebar from 'app/components/sidebars/main';
import SettingsSidebar from 'app/components/sidebars/settings';
import NetworkIndicator from 'app/components/network_indicator';
import SafeAreaView from 'app/components/safe_area_view';
import StatusBar from 'app/components/status_bar';
import {DeviceTypes} from 'app/constants';
import SettingsSidebar from 'app/components/sidebars/settings';
import {preventDoubleTap} from 'app/utils/tap';
import PostTextbox from 'app/components/post_textbox';
import PushNotifications from 'app/push_notifications';
import tracker from 'app/utils/time_tracker';
import LocalConfig from 'assets/config';
import telemetry from 'app/telemetry';
import ChannelNavBar from './channel_nav_bar';
import ChannelPostList from './channel_post_list';
import LocalConfig from 'assets/config';
const CHANNEL_POST_TEXTBOX_CURSOR_CHANGE = 'onChannelTextBoxCursorChange';
const CHANNEL_POST_TEXTBOX_VALUE_CHANGE = 'onChannelTextBoxValueChange';
export let ClientUpgradeListener;
let ClientUpgradeListener;
export default class Channel extends PureComponent {
export default class ChannelBase extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
loadChannelsIfNecessary: PropTypes.func.isRequired,
@ -273,7 +259,7 @@ export default class Channel extends PureComponent {
}
};
render() {
renderChannel(drawerContent) {
const {
channelsRequestFailed,
currentChannelId,
@ -282,8 +268,6 @@ export default class Channel extends PureComponent {
theme,
} = this.props;
const {height} = Dimensions.get('window');
if (!currentChannelId) {
if (channelsRequestFailed) {
const PostListRetry = require('app/components/post_list_retry').default;
@ -309,11 +293,6 @@ export default class Channel extends PureComponent {
);
}
const channelLoaderStyle = [style.channelLoader, {height}];
if (Platform.OS === 'ios' && (DeviceTypes.IS_IPHONE_X || DeviceTypes.IS_TABLET)) {
channelLoaderStyle.push(style.iOSHomeIndicator);
}
return (
<MainSidebar
ref={this.channelSidebarRef}
@ -325,43 +304,7 @@ export default class Channel extends PureComponent {
blurPostTextBox={this.blurPostTextBox}
navigator={navigator}
>
<SafeAreaView navigator={navigator}>
<StatusBar/>
<NetworkIndicator/>
<ChannelNavBar
navigator={navigator}
openChannelDrawer={this.openChannelSidebar}
openSettingsDrawer={this.openSettingsSidebar}
onPress={this.goToChannelInfo}
/>
<ChannelPostList
navigator={navigator}
updateNativeScrollView={this.updateNativeScrollView}
/>
<FileUploadPreview channelId={currentChannelId}/>
<Autocomplete
maxHeight={AUTOCOMPLETE_MAX_HEIGHT}
onChangeText={this.handleAutoComplete}
cursorPositionEvent={CHANNEL_POST_TEXTBOX_CURSOR_CHANGE}
valueEvent={CHANNEL_POST_TEXTBOX_VALUE_CHANGE}
/>
<ChannelLoader
height={height}
style={channelLoaderStyle}
/>
{LocalConfig.EnableMobileClientUpgrade && <ClientUpgradeListener navigator={navigator}/>}
</SafeAreaView>
<KeyboardTrackingView
ref={this.keyboardTracker}
scrollViewNativeID={currentChannelId}
>
<PostTextbox
cursorPositionEvent={CHANNEL_POST_TEXTBOX_CURSOR_CHANGE}
valueEvent={CHANNEL_POST_TEXTBOX_VALUE_CHANGE}
ref={this.postTextbox}
navigator={navigator}
/>
</KeyboardTrackingView>
{drawerContent}
</SettingsSidebar>
<InteractiveDialogController
navigator={navigator}
@ -372,7 +315,7 @@ export default class Channel extends PureComponent {
}
}
const style = StyleSheet.create({
export const style = StyleSheet.create({
flex: {
flex: 1,
},

View file

@ -89,7 +89,7 @@ export default class ChannelPostList extends PureComponent {
this.props.actions.recordLoadTime('Switch Channel', 'channelSwitch');
}
if (!prevProps.postIds?.length && this.props.postIds?.length > 0) {
if (!prevProps.postIds?.length && this.props.postIds?.length > 0 && this.props.updateNativeScrollView) {
// This is needed to re-bind the scrollview natively when getting the first posts
this.props.updateNativeScrollView();
}

View file

@ -270,7 +270,7 @@ export default class PostOptions extends PureComponent {
const {navigator, theme} = this.props;
this.close();
requestAnimationFrame(() => {
setTimeout(() => {
MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor).then((source) => {
navigator.showModal({
screen: 'AddReaction',
@ -288,7 +288,7 @@ export default class PostOptions extends PureComponent {
},
});
});
});
}, 300);
};
handleReply = () => {
@ -368,7 +368,7 @@ export default class PostOptions extends PureComponent {
const {navigator, post, theme} = this.props;
this.close();
requestAnimationFrame(() => {
setTimeout(() => {
MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor).then((source) => {
navigator.showModal({
screen: 'EditPost',
@ -386,7 +386,7 @@ export default class PostOptions extends PureComponent {
},
});
});
});
}, 300);
};
handleUnflagPost = () => {

View file

@ -4,14 +4,14 @@ exports[`thread should match snapshot, has root post 1`] = `
<React.Fragment>
<Connect(SafeAreaIos)
excludeHeader={true}
keyboardOffset={20}
>
<Connect(StatusBar) />
<React.Fragment>
<Connect(PostList)
currentUserId="member_user_id"
indicateNewMessages={false}
lastPostIndex={-1}
lastPostIndex={2}
lastViewedAt={0}
location="thread"
navigator={
Object {
@ -52,20 +52,25 @@ exports[`thread should match snapshot, has root post 1`] = `
}
scrollViewNativeID="threadPostList"
/>
<Connect(FileUploadPreview)
channelId="channel_id"
rootId="root_id"
/>
<ForwardRef(forwardConnectRef)
cursorPositionEvent="onThreadTextBoxCursorChange"
maxHeight={200}
onChangeText={[Function]}
rootId="root_id"
valueEvent="onThreadTextBoxValueChange"
/>
<View
nativeID="threadAccessoriesContainer"
>
<Connect(FileUploadPreview)
channelId="channel_id"
rootId="root_id"
/>
<ForwardRef(forwardConnectRef)
cursorPositionEvent="onThreadTextBoxCursorChange"
maxHeight={200}
onChangeText={[Function]}
rootId="root_id"
valueEvent="onThreadTextBoxValueChange"
/>
</View>
</React.Fragment>
</Connect(SafeAreaIos)>
<KeyboardTrackingView
accessoriesContainerID="threadAccessoriesContainer"
scrollViewNativeID="threadPostList"
>
<ForwardRef(forwardConnectRef)
@ -106,7 +111,6 @@ exports[`thread should match snapshot, no root post, loading 1`] = `
<React.Fragment>
<Connect(SafeAreaIos)
excludeHeader={true}
keyboardOffset={20}
>
<Connect(StatusBar) />
<Loading
@ -122,7 +126,8 @@ exports[`thread should match snapshot, render footer 1`] = `
<Connect(PostList)
currentUserId="member_user_id"
indicateNewMessages={false}
lastPostIndex={-1}
lastPostIndex={2}
lastViewedAt={0}
location="thread"
navigator={
Object {
@ -169,7 +174,7 @@ exports[`thread should match snapshot, render footer 2`] = `
<Connect(PostList)
currentUserId="member_user_id"
indicateNewMessages={false}
lastPostIndex={-1}
lastPostIndex={2}
lastViewedAt={0}
location="thread"
navigator={
@ -211,7 +216,6 @@ exports[`thread should match snapshot, render footer 3`] = `
<React.Fragment>
<Connect(SafeAreaIos)
excludeHeader={true}
keyboardOffset={20}
>
<Connect(StatusBar) />
<Loading

View file

@ -0,0 +1,70 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {THREAD} from 'app/constants/screen';
import Loading from 'app/components/loading';
import KeyboardLayout from 'app/components/layout/keyboard_layout';
import PostList from 'app/components/post_list';
import PostTextbox from 'app/components/post_textbox';
import SafeAreaView from 'app/components/safe_area_view';
import StatusBar from 'app/components/status_bar';
import ThreadBase from './thread_base';
export default class ThreadAndroid extends ThreadBase {
render() {
const {
channelId,
myMember,
navigator,
postIds,
rootId,
channelIsArchived,
} = this.props;
let content;
let postTextBox;
if (this.hasRootPost()) {
content = (
<PostList
renderFooter={this.renderFooter()}
indicateNewMessages={false}
postIds={postIds}
currentUserId={myMember && myMember.user_id}
lastViewedAt={this.state.lastViewedAt}
lastPostIndex={-1}
navigator={navigator}
onPostPress={this.hideKeyboard}
location={THREAD}
/>
);
postTextBox = (
<PostTextbox
channelIsArchived={channelIsArchived}
rootId={rootId}
channelId={channelId}
navigator={navigator}
onCloseChannel={this.onCloseChannel}
/>
);
} else {
content = (
<Loading/>
);
}
return (
<SafeAreaView>
<StatusBar/>
<KeyboardLayout>
{content}
{postTextBox}
</KeyboardLayout>
</SafeAreaView>
);
}
}

View file

@ -0,0 +1,104 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {View} from 'react-native';
import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view';
import {getLastPostIndex} from 'mattermost-redux/utils/post_list';
import Autocomplete, {AUTOCOMPLETE_MAX_HEIGHT} from 'app/components/autocomplete';
import FileUploadPreview from 'app/components/file_upload_preview';
import Loading from 'app/components/loading';
import PostList from 'app/components/post_list';
import PostTextbox from 'app/components/post_textbox';
import SafeAreaView from 'app/components/safe_area_view';
import StatusBar from 'app/components/status_bar';
import {THREAD} from 'app/constants/screen';
import ThreadBase from './thread_base';
const ACCESSORIES_CONTAINER_NATIVE_ID = 'threadAccessoriesContainer';
const THREAD_POST_TEXTBOX_CURSOR_CHANGE = 'onThreadTextBoxCursorChange';
const THREAD_POST_TEXTBOX_VALUE_CHANGE = 'onThreadTextBoxValueChange';
const SCROLLVIEW_NATIVE_ID = 'threadPostList';
export default class ThreadIOS extends ThreadBase {
render() {
const {
channelId,
myMember,
navigator,
postIds,
rootId,
channelIsArchived,
} = this.props;
let content;
let postTextBox;
if (this.hasRootPost()) {
content = (
<React.Fragment>
<PostList
renderFooter={this.renderFooter()}
indicateNewMessages={false}
postIds={postIds}
lastPostIndex={getLastPostIndex(postIds)}
currentUserId={myMember && myMember.user_id}
lastViewedAt={this.state.lastViewedAt}
navigator={navigator}
onPostPress={this.hideKeyboard}
location={THREAD}
scrollViewNativeID={SCROLLVIEW_NATIVE_ID}
/>
<View nativeID={ACCESSORIES_CONTAINER_NATIVE_ID}>
<FileUploadPreview
channelId={channelId}
rootId={rootId}
/>
<Autocomplete
maxHeight={AUTOCOMPLETE_MAX_HEIGHT}
onChangeText={this.handleAutoComplete}
cursorPositionEvent={THREAD_POST_TEXTBOX_CURSOR_CHANGE}
valueEvent={THREAD_POST_TEXTBOX_VALUE_CHANGE}
rootId={rootId}
/>
</View>
</React.Fragment>
);
postTextBox = (
<KeyboardTrackingView
scrollViewNativeID={SCROLLVIEW_NATIVE_ID}
accessoriesContainerID={ACCESSORIES_CONTAINER_NATIVE_ID}
>
<PostTextbox
ref={this.postTextbox}
channelIsArchived={channelIsArchived}
rootId={rootId}
channelId={channelId}
navigator={navigator}
onCloseChannel={this.onCloseChannel}
cursorPositionEvent={THREAD_POST_TEXTBOX_CURSOR_CHANGE}
valueEvent={THREAD_POST_TEXTBOX_VALUE_CHANGE}
/>
</KeyboardTrackingView>
);
} else {
content = (
<Loading/>
);
}
return (
<React.Fragment>
<SafeAreaView excludeHeader={true}>
<StatusBar/>
{content}
</SafeAreaView>
{postTextBox}
</React.Fragment>
);
}
}

View file

@ -8,7 +8,7 @@ import Preferences from 'mattermost-redux/constants/preferences';
import {General, RequestStatus} from 'mattermost-redux/constants';
import PostList from 'app/components/post_list';
import Thread from './thread.js';
import ThreadIOS from './thread.ios';
jest.mock('react-intl');
@ -37,7 +37,7 @@ describe('thread', () => {
test('should match snapshot, has root post', () => {
const wrapper = shallow(
<Thread {...baseProps}/>,
<ThreadIOS {...baseProps}/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
expect(wrapper.getElement()).toMatchSnapshot();
@ -46,7 +46,7 @@ describe('thread', () => {
test('should match snapshot, no root post, loading', () => {
const newPostIds = ['post_id_1', 'post_id_2'];
const wrapper = shallow(
<Thread
<ThreadIOS
{...baseProps}
postIds={newPostIds}
/>,
@ -75,7 +75,7 @@ describe('thread', () => {
};
const newNavigator = {...navigator};
const wrapper = shallow(
<Thread
<ThreadIOS
{...baseProps}
navigator={newNavigator}
/>,
@ -88,7 +88,7 @@ describe('thread', () => {
test('should match snapshot, render footer', () => {
const wrapper = shallow(
<Thread {...baseProps}/>,
<ThreadIOS {...baseProps}/>,
{context: {intl: {formatMessage: jest.fn()}}},
);

View file

@ -5,28 +5,14 @@ import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Keyboard, Platform} from 'react-native';
import {intlShape} from 'react-intl';
import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view';
import {General, RequestStatus} from 'mattermost-redux/constants';
import {getLastPostIndex} from 'mattermost-redux/utils/post_list';
import {THREAD} from 'app/constants/screen';
import Autocomplete, {AUTOCOMPLETE_MAX_HEIGHT} from 'app/components/autocomplete';
import FileUploadPreview from 'app/components/file_upload_preview';
import Loading from 'app/components/loading';
import PostList from 'app/components/post_list';
import PostTextbox from 'app/components/post_textbox';
import SafeAreaView from 'app/components/safe_area_view';
import StatusBar from 'app/components/status_bar';
import {setNavigatorStyles} from 'app/utils/theme';
import DeletedPost from 'app/components/deleted_post';
const THREAD_POST_TEXTBOX_CURSOR_CHANGE = 'onThreadTextBoxCursorChange';
const THREAD_POST_TEXTBOX_VALUE_CHANGE = 'onThreadTextBoxValueChange';
const SCROLLVIEW_NATIVE_ID = 'threadPostList';
export default class Thread extends PureComponent {
export default class ThreadBase extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
selectPost: PropTypes.func.isRequired,
@ -47,8 +33,6 @@ export default class Thread extends PureComponent {
postIds: [],
};
state = {};
static contextTypes = {
intl: intlShape,
};
@ -71,6 +55,10 @@ export default class Thread extends PureComponent {
this.props.navigator.setTitle({
title,
});
this.state = {
lastViewedAt: props.myMember && props.myMember.last_viewed_at,
};
}
componentWillReceiveProps(nextProps) {
@ -153,79 +141,4 @@ export default class Thread extends PureComponent {
},
});
};
render() {
const {
channelId,
myMember,
navigator,
postIds,
rootId,
channelIsArchived,
} = this.props;
let content;
let postTextBox;
if (this.hasRootPost()) {
content = (
<React.Fragment>
<PostList
renderFooter={this.renderFooter()}
indicateNewMessages={false}
postIds={postIds}
lastPostIndex={Platform.OS === 'android' ? getLastPostIndex(postIds) : -1}
currentUserId={myMember && myMember.user_id}
lastViewedAt={this.state.lastViewedAt}
navigator={navigator}
onPostPress={this.hideKeyboard}
location={THREAD}
scrollViewNativeID={SCROLLVIEW_NATIVE_ID}
/>
<FileUploadPreview
channelId={channelId}
rootId={rootId}
/>
<Autocomplete
maxHeight={AUTOCOMPLETE_MAX_HEIGHT}
onChangeText={this.handleAutoComplete}
cursorPositionEvent={THREAD_POST_TEXTBOX_CURSOR_CHANGE}
valueEvent={THREAD_POST_TEXTBOX_VALUE_CHANGE}
rootId={rootId}
/>
</React.Fragment>
);
postTextBox = (
<KeyboardTrackingView scrollViewNativeID={SCROLLVIEW_NATIVE_ID}>
<PostTextbox
ref={this.postTextbox}
channelIsArchived={channelIsArchived}
rootId={rootId}
channelId={channelId}
navigator={navigator}
onCloseChannel={this.onCloseChannel}
cursorPositionEvent={THREAD_POST_TEXTBOX_CURSOR_CHANGE}
valueEvent={THREAD_POST_TEXTBOX_VALUE_CHANGE}
/>
</KeyboardTrackingView>
);
} else {
content = (
<Loading/>
);
}
return (
<React.Fragment>
<SafeAreaView
excludeHeader={true}
keyboardOffset={20}
>
<StatusBar/>
{content}
</SafeAreaView>
{postTextBox}
</React.Fragment>
);
}
}

4
package-lock.json generated
View file

@ -15552,8 +15552,8 @@
}
},
"react-native-keyboard-tracking-view": {
"version": "github:enahum/react-native-keyboard-tracking-view#60c8a81e3ff2ad5a17d969f235e5990c88d465d6",
"from": "github:enahum/react-native-keyboard-tracking-view#60c8a81e3ff2ad5a17d969f235e5990c88d465d6"
"version": "github:enahum/react-native-keyboard-tracking-view#66979a750e42fefe06366f0c924b29368bb586f2",
"from": "github:enahum/react-native-keyboard-tracking-view#66979a750e42fefe06366f0c924b29368bb586f2"
},
"react-native-keychain": {
"version": "3.1.3",

View file

@ -40,7 +40,7 @@
"react-native-image-gallery": "github:mattermost/react-native-image-gallery#c1a9f7118e90cc87d47620bc0584c9cac4b0cf38",
"react-native-image-picker": "0.28.1",
"react-native-keyboard-aware-scroll-view": "0.8.0",
"react-native-keyboard-tracking-view": "github:enahum/react-native-keyboard-tracking-view#60c8a81e3ff2ad5a17d969f235e5990c88d465d6",
"react-native-keyboard-tracking-view": "github:enahum/react-native-keyboard-tracking-view#66979a750e42fefe06366f0c924b29368bb586f2",
"react-native-keychain": "3.1.3",
"react-native-linear-gradient": "2.5.4",
"react-native-local-auth": "github:mattermost/react-native-local-auth#cc9ce2f468fbf7b431dfad3191a31aaa9227a6ab",

View file

@ -17,6 +17,8 @@ module.exports = [
'app/components/formatted_markdown_text.js',
'app/components/formatted_text.js',
'app/components/formatted_time.js',
'app/components/interactive_dialog_controller/index.js',
'app/components/interactive_dialog_controller/interactive_dialog_controller.js',
'app/components/layout/keyboard_layout/index.js',
'app/components/layout/keyboard_layout/keyboard_layout.js',
'app/components/loading.js',
@ -45,6 +47,7 @@ module.exports = [
'app/components/markdown/markdown_table_row/markdown_table_row.js',
'app/components/network_indicator/index.js',
'app/components/network_indicator/network_indicator.js',
'app/components/paper_plane.js',
'app/components/post/index.js',
'app/components/post/post.js',
'app/components/post_body/index.js',
@ -60,8 +63,11 @@ module.exports = [
'app/components/post_list_retry.js',
'app/components/post_profile_picture/index.js',
'app/components/post_profile_picture/post_profile_picture.js',
'app/components/post_textbox/components/typing/index.js',
'app/components/post_textbox/components/typing/typing.js',
'app/components/post_textbox/index.js',
'app/components/post_textbox/post_textbox.js',
'app/components/post_textbox/post_textbox.android.js',
'app/components/post_textbox/post_textbox_base.js',
'app/components/profile_picture/index.js',
'app/components/profile_picture/profile_picture.js',
'app/components/progressive_image/index.js',
@ -69,6 +75,7 @@ module.exports = [
'app/components/reply_icon.js',
'app/components/safe_area_view/index.js',
'app/components/safe_area_view/safe_area_view.android.js',
'app/components/send_button.js',
'app/components/show_more_button/index.js',
'app/components/show_more_button/show_more_button.js',
'app/components/start/empty_toolbar.js',
@ -121,7 +128,8 @@ module.exports = [
'app/reducers/views/team.js',
'app/reducers/views/thread.js',
'app/reducers/views/user.js',
'app/screens/channel/channel.js',
'app/screens/channel/channel.android.js',
'app/screens/channel/channel_base.js',
'app/screens/channel/channel_nav_bar/channel_drawer_button.js',
'app/screens/channel/channel_nav_bar/channel_nav_bar.js',
'app/screens/channel/channel_nav_bar/channel_search_button/channel_search_button.js',

View file

@ -15,6 +15,8 @@ module.exports = ['./node_modules/app/app.js',
'./node_modules/app/components/formatted_markdown_text.js',
'./node_modules/app/components/formatted_text.js',
'./node_modules/app/components/formatted_time.js',
'./node_modules/app/components/interactive_dialog_controller/index.js',
'./node_modules/app/components/interactive_dialog_controller/interactive_dialog_controller.js',
'./node_modules/app/components/layout/keyboard_layout/index.js',
'./node_modules/app/components/layout/keyboard_layout/keyboard_layout.js',
'./node_modules/app/components/loading.js',
@ -43,6 +45,7 @@ module.exports = ['./node_modules/app/app.js',
'./node_modules/app/components/markdown/markdown_table_row/markdown_table_row.js',
'./node_modules/app/components/network_indicator/index.js',
'./node_modules/app/components/network_indicator/network_indicator.js',
'./node_modules/app/components/paper_plane.js',
'./node_modules/app/components/post/index.js',
'./node_modules/app/components/post/post.js',
'./node_modules/app/components/post_body/index.js',
@ -58,8 +61,11 @@ module.exports = ['./node_modules/app/app.js',
'./node_modules/app/components/post_list_retry.js',
'./node_modules/app/components/post_profile_picture/index.js',
'./node_modules/app/components/post_profile_picture/post_profile_picture.js',
'./node_modules/app/components/post_textbox/components/typing/index.js',
'./node_modules/app/components/post_textbox/components/typing/typing.js',
'./node_modules/app/components/post_textbox/index.js',
'./node_modules/app/components/post_textbox/post_textbox.js',
'./node_modules/app/components/post_textbox/post_textbox.android.js',
'./node_modules/app/components/post_textbox/post_textbox_base.js',
'./node_modules/app/components/profile_picture/index.js',
'./node_modules/app/components/profile_picture/profile_picture.js',
'./node_modules/app/components/progressive_image/index.js',
@ -67,6 +73,7 @@ module.exports = ['./node_modules/app/app.js',
'./node_modules/app/components/reply_icon.js',
'./node_modules/app/components/safe_area_view/index.js',
'./node_modules/app/components/safe_area_view/safe_area_view.android.js',
'./node_modules/app/components/send_button.js',
'./node_modules/app/components/show_more_button/index.js',
'./node_modules/app/components/show_more_button/show_more_button.js',
'./node_modules/app/components/start/empty_toolbar.js',
@ -119,7 +126,8 @@ module.exports = ['./node_modules/app/app.js',
'./node_modules/app/reducers/views/team.js',
'./node_modules/app/reducers/views/thread.js',
'./node_modules/app/reducers/views/user.js',
'./node_modules/app/screens/channel/channel.js',
'./node_modules/app/screens/channel/channel.android.js',
'./node_modules/app/screens/channel/channel_base.js',
'./node_modules/app/screens/channel/channel_nav_bar/channel_drawer_button.js',
'./node_modules/app/screens/channel/channel_nav_bar/channel_nav_bar.js',
'./node_modules/app/screens/channel/channel_nav_bar/channel_search_button/channel_search_button.js',