MM-9494 & MM-13888 Tapping execute actions & interactive keyboard dismissal (#2799)

* MM-9494 & MM-13888 Tapping with the keyboard opened executes the action & iOS iteractive keyboard

* Fix tests

* feedback review

* add new line at the end of file

* feedback review and added todo list

* Track interactive dismiss keyboard and set scrollview bounds natively

* Fix snapshots

* Fastlane default to current branch when no BRANCH_TO_BUILD is set

* Set NODE_OPTIONS in ios build script

* Rebind scrollview when channel gets first set of posts

* Keep scrolling momentum on keyboard close

* Update react-native-keyboard-tracking-view

* Fix ScrollView offset with keyboard-tracking

* Fix offset while dragging the keyboard

* Allow action on channel drawer on tablets

* Fix typo

Co-Authored-By: Saturnino Abril <saturnino.abril@gmail.com>

* Fix indentation
This commit is contained in:
Elias Nahum 2019-05-20 12:02:00 -04:00 committed by GitHub
parent ff4374f59f
commit f7f56e958b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 1268 additions and 979 deletions

View file

@ -1595,6 +1595,41 @@ SOFTWARE.
---
## react-native-keyboard-tracking-view
This product contains a modified version of 'react-native-keyboard-tracking-view' by Wix.com.
a strongly specified, highly compatible variant of Markdown
* HOMEPAGE:
* wix/react-native-keyboard-tracking-view
* LICENSE: MIT
The MIT License (MIT)
Copyright (c) 2016 Wix.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-keychain
This product contains 'react-native-keychain' by Joel Arvidsson.

View file

@ -9,6 +9,8 @@ import {
View,
} from 'react-native';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {DeviceTypes} from 'app/constants';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
@ -20,7 +22,7 @@ import DateSuggestion from './date_suggestion';
export default class Autocomplete extends PureComponent {
static propTypes = {
cursorPosition: PropTypes.number.isRequired,
cursorPosition: PropTypes.number,
deviceHeight: PropTypes.number,
onChangeText: PropTypes.func.isRequired,
maxHeight: PropTypes.number,
@ -29,6 +31,8 @@ export default class Autocomplete extends PureComponent {
theme: PropTypes.object.isRequired,
value: PropTypes.string,
enableDateSuggestion: PropTypes.bool.isRequired,
valueEvent: PropTypes.string,
cursorPositionEvent: PropTypes.string,
};
static defaultProps = {
@ -37,18 +41,67 @@ export default class Autocomplete extends PureComponent {
enableDateSuggestion: false,
};
state = {
atMentionCount: 0,
channelMentionCount: 0,
emojiCount: 0,
commandCount: 0,
dateCount: 0,
keyboardOffset: 0,
};
static getDerivedStateFromProps(props, state) {
const nextState = {};
let updated = false;
if (props.cursorPosition !== state.cursorPosition && !props.cursorPositionEvent) {
nextState.cursorPosition = props.cursorPosition;
updated = true;
}
if (props.value !== state.value && !props.valueEvent) {
nextState.value = props.value;
updated = true;
}
return updated ? nextState : null;
}
constructor(props) {
super(props);
this.state = {
atMentionCount: 0,
channelMentionCount: 0,
cursorPosition: props.cursorPosition,
emojiCount: 0,
commandCount: 0,
dateCount: 0,
keyboardOffset: 0,
value: props.value,
};
}
componentDidMount() {
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this.keyboardDidShow);
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this.keyboardDidHide);
if (this.props.valueEvent) {
EventEmitter.on(this.props.valueEvent, this.handleValueChange);
}
if (this.props.cursorPositionEvent) {
EventEmitter.on(this.props.cursorPositionEvent, this.handleCursorPositionChange);
}
}
componentWillUnmount() {
this.keyboardDidShowListener.remove();
this.keyboardDidHideListener.remove();
if (this.props.valueEvent) {
EventEmitter.off(this.props.valueEvent, this.handleValueChange);
}
if (this.props.cursorPositionEvent) {
EventEmitter.off(this.props.cursorPositionEvent, this.handleCursorPositionChange);
}
}
onChangeText = (value) => {
this.props.onChangeText(value, true);
}
};
handleAtMentionCountChange = (atMentionCount) => {
this.setState({atMentionCount});
@ -58,6 +111,10 @@ export default class Autocomplete extends PureComponent {
this.setState({channelMentionCount});
};
handleCursorPositionChange = (cursorPosition) => {
this.setState({cursorPosition});
};
handleEmojiCountChange = (emojiCount) => {
this.setState({emojiCount});
};
@ -70,15 +127,9 @@ export default class Autocomplete extends PureComponent {
this.setState({dateCount});
};
componentWillMount() {
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this.keyboardDidShow);
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this.keyboardDidHide);
}
componentWillUnmount() {
this.keyboardDidShowListener.remove();
this.keyboardDidHideListener.remove();
}
handleValueChange = (value) => {
this.setState({value});
};
keyboardDidShow = (e) => {
const {height} = e.endCoordinates;
@ -119,7 +170,7 @@ export default class Autocomplete extends PureComponent {
}
// We always need to render something, but we only draw the borders when we have results to show
const {atMentionCount, channelMentionCount, emojiCount, commandCount, dateCount} = this.state;
const {atMentionCount, channelMentionCount, emojiCount, commandCount, dateCount, cursorPosition, value} = this.state;
if (atMentionCount + channelMentionCount + emojiCount + commandCount + dateCount > 0) {
if (this.props.isSearch) {
wrapperStyle.push(style.bordersSearch);
@ -134,34 +185,43 @@ export default class Autocomplete extends PureComponent {
<View style={wrapperStyle}>
<View style={containerStyle}>
<AtMention
maxListHeight={maxListHeight}
onResultCountChange={this.handleAtMentionCountChange}
{...this.props}
cursorPosition={cursorPosition}
maxListHeight={maxListHeight}
onChangeText={this.onChangeText}
onResultCountChange={this.handleAtMentionCountChange}
value={value || ''}
/>
<ChannelMention
maxListHeight={maxListHeight}
onResultCountChange={this.handleChannelMentionCountChange}
{...this.props}
cursorPosition={cursorPosition}
maxListHeight={maxListHeight}
onChangeText={this.onChangeText}
onResultCountChange={this.handleChannelMentionCountChange}
value={value || ''}
/>
<EmojiSuggestion
maxListHeight={maxListHeight}
onResultCountChange={this.handleEmojiCountChange}
{...this.props}
cursorPosition={cursorPosition}
maxListHeight={maxListHeight}
onChangeText={this.onChangeText}
onResultCountChange={this.handleEmojiCountChange}
value={value || ''}
/>
<SlashSuggestion
maxListHeight={maxListHeight}
onResultCountChange={this.handleCommandCountChange}
{...this.props}
maxListHeight={maxListHeight}
onChangeText={this.onChangeText}
onResultCountChange={this.handleCommandCountChange}
value={value || ''}
/>
{(this.props.isSearch && this.props.enableDateSuggestion) &&
<DateSuggestion
onResultCountChange={this.handleIsDateFilterChange}
{...this.props}
cursorPosition={cursorPosition}
onChangeText={this.onChangeText}
onResultCountChange={this.handleIsDateFilterChange}
value={value || ''}
/>
}
</View>

View file

@ -17,4 +17,6 @@ function mapStateToProps(state) {
};
}
export const AUTOCOMPLETE_MAX_HEIGHT = 200;
export default connect(mapStateToProps, null, null, {forwardRef: true})(Autocomplete);

View file

@ -24,7 +24,7 @@ exports[`CustomList should match snapshot with FlatList 1`] = `
horizontal={false}
initialNumToRender={15}
keyExtractor={[Function]}
keyboardDismissMode="interactive"
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="always"
maxToRenderPerBatch={16}
numColumns={1}
@ -62,7 +62,7 @@ exports[`CustomList should match snapshot with SectionList 1`] = `
horizontal={false}
initialNumToRender={15}
keyExtractor={[Function]}
keyboardDismissMode="interactive"
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="always"
maxToRenderPerBatch={16}
onEndReachedThreshold={2}

View file

@ -3,7 +3,7 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {FlatList, Platform, SectionList, Text, View} from 'react-native';
import {FlatList, Keyboard, Platform, SectionList, Text, View} from 'react-native';
import {ListTypes} from 'app/constants';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
@ -26,10 +26,7 @@ export default class CustomList extends PureComponent {
onLoadMore: PropTypes.func,
onRowPress: PropTypes.func,
onRowSelect: PropTypes.func,
renderItem: PropTypes.oneOfType([
PropTypes.func,
PropTypes.element,
]).isRequired,
renderItem: PropTypes.func.isRequired,
selectable: PropTypes.bool,
theme: PropTypes.object.isRequired,
shouldRenderSeparator: PropTypes.bool,
@ -45,6 +42,14 @@ export default class CustomList extends PureComponent {
super(props);
this.contentOffsetY = 0;
this.keyboardDismissProp = Platform.select({
android: {
onScrollBeginDrag: Keyboard.dismiss,
},
ios: {
keyboardDismissMode: 'on-drag',
},
});
this.state = {};
}
@ -98,12 +103,6 @@ export default class CustomList extends PureComponent {
props.onPress = onRowPress;
}
// Allow passing in a component like UserListRow or ChannelListRow
if (this.props.renderItem.prototype.isReactComponent) {
const RowComponent = this.props.renderItem;
return <RowComponent {...props}/>;
}
return this.props.renderItem(props);
};
@ -117,7 +116,7 @@ export default class CustomList extends PureComponent {
data={data}
extraData={extraData}
keyboardShouldPersistTaps='always'
keyboardDismissMode='interactive'
{...this.keyboardDismissProp}
keyExtractor={this.keyExtractor}
initialNumToRender={INITIAL_BATCH_TO_RENDER}
ItemSeparatorComponent={this.renderSeparator}
@ -169,7 +168,7 @@ export default class CustomList extends PureComponent {
contentContainerStyle={style.container}
extraData={loading}
keyboardShouldPersistTaps='always'
keyboardDismissMode='interactive'
{...this.keyboardDismissProp}
keyExtractor={this.keyExtractor}
initialNumToRender={INITIAL_BATCH_TO_RENDER}
ItemSeparatorComponent={this.renderSeparator}

View file

@ -53,42 +53,39 @@ export default class EditChannelInfo extends PureComponent {
editing: false,
};
constructor(props) {
super(props);
this.nameInput = React.createRef();
this.urlInput = React.createRef();
this.purposeInput = React.createRef();
this.headerInput = React.createRef();
this.lastText = React.createRef();
this.scroll = React.createRef();
}
blur = () => {
if (this.nameInput) {
this.nameInput.blur();
if (this.nameInput?.current) {
this.nameInput.current.blur();
}
// TODO: uncomment below once the channel URL field is added
// if (this.urlInput) {
// this.urlInput.blur();
// if (this.urlInput?.current) {
// this.urlInput.current.blur();
// }
if (this.purposeInput) {
this.purposeInput.blur();
if (this.purposeInput?.current) {
this.purposeInput.current.blur();
}
if (this.headerInput) {
this.headerInput.blur();
if (this.headerInput?.current) {
this.headerInput.current.blur();
}
if (this.scroll) {
this.scroll.scrollToPosition(0, 0, true);
if (this.scroll?.current) {
this.scroll.current.scrollToPosition(0, 0, true);
}
};
channelNameRef = (ref) => {
this.nameInput = ref;
};
channelURLRef = (ref) => {
this.urlInput = ref;
};
channelPurposeRef = (ref) => {
this.purposeInput = ref;
};
channelHeaderRef = (ref) => {
this.headerInput = ref;
};
close = (goBack = false) => {
if (goBack) {
this.props.navigator.pop({animated: true});
@ -99,10 +96,6 @@ export default class EditChannelInfo extends PureComponent {
}
};
lastTextRef = (ref) => {
this.lastText = ref;
};
canUpdate = (displayName, channelURL, purpose, header) => {
const {
oldDisplayName,
@ -167,13 +160,9 @@ export default class EditChannelInfo extends PureComponent {
}
};
scrollRef = (ref) => {
this.scroll = ref;
};
scrollToEnd = () => {
if (this.scroll && this.lastText) {
this.scroll.scrollToFocusedInput(findNodeHandle(this.lastText));
if (this.scroll?.current && this.lastText?.current) {
this.scroll.current.scrollToFocusedInput(findNodeHandle(this.lastText.current));
}
};
@ -223,7 +212,7 @@ export default class EditChannelInfo extends PureComponent {
<View style={style.container}>
<StatusBar/>
<KeyboardAwareScrollView
ref={this.scrollRef}
ref={this.scroll}
style={style.container}
>
{displayError}
@ -240,7 +229,7 @@ export default class EditChannelInfo extends PureComponent {
</View>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
ref={this.channelNameRef}
ref={this.nameInput}
value={displayName}
onChangeText={this.onDisplayNameChangeText}
style={style.input}
@ -269,7 +258,7 @@ export default class EditChannelInfo extends PureComponent {
</View>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
ref={this.channelURLRef}
ref={this.urlInput}
value={channelURL}
onChangeText={this.onDisplayURLChangeText}
style={style.input}
@ -299,7 +288,7 @@ export default class EditChannelInfo extends PureComponent {
</View>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
ref={this.channelPurposeRef}
ref={this.purposeInput}
value={purpose}
onChangeText={this.onPurposeChangeText}
style={[style.input, {height: 110}]}
@ -337,7 +326,7 @@ export default class EditChannelInfo extends PureComponent {
</View>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
ref={this.channelHeaderRef}
ref={this.headerInput}
value={header}
onChangeText={this.onHeaderChangeText}
style={[style.input, {height: 110}]}
@ -353,7 +342,7 @@ export default class EditChannelInfo extends PureComponent {
disableFullscreenUI={true}
/>
</View>
<View ref={this.lastTextRef}>
<View ref={this.lastText}>
<FormattedText
style={style.helpText}
id='channel_modal.headerHelp'

View file

@ -65,7 +65,7 @@ export default class Emoji extends React.PureComponent {
componentWillReceiveProps(nextProps) {
const {displayTextOnly, emojiName, imageUrl} = nextProps;
if (emojiName !== this.props.emojiName) {
if (emojiName !== this.props.emojiName && this.mounted) {
this.setState({
imageUrl: null,
});
@ -82,9 +82,11 @@ export default class Emoji extends React.PureComponent {
}
setImageUrl = (imageUrl) => {
this.setState({
imageUrl,
});
if (this.mounted) {
this.setState({
imageUrl,
});
}
};
render() {

View file

@ -3,6 +3,7 @@
exports[`PostAttachmentOpenGraph should match snapshot with a single image file 1`] = `
<ScrollViewMock
horizontal={true}
keyboardShouldPersistTaps="always"
scrollEnabled={false}
style={
Array [

View file

@ -4,7 +4,6 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {
Keyboard,
ScrollView,
StyleSheet,
} from 'react-native';
@ -123,7 +122,6 @@ export default class FileAttachmentList extends Component {
};
handlePreviewPress = preventDoubleTap((idx) => {
Keyboard.dismiss();
previewImageAtIndex(this.props.navigator, this.items, idx, this.galleryFiles);
});
@ -176,6 +174,7 @@ export default class FileAttachmentList extends Component {
horizontal={true}
scrollEnabled={fileIds.length > 1}
style={[(isFailed && styles.failed)]}
keyboardShouldPersistTaps={'always'}
>
{this.renderItems()}
</ScrollView>

View file

@ -10,6 +10,8 @@ import {
View,
} from 'react-native';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import FormattedText from 'app/components/formatted_text';
import FileUploadItem from './file_upload_item';
@ -21,9 +23,7 @@ export default class FileUploadPreview extends PureComponent {
deviceHeight: PropTypes.number.isRequired,
files: PropTypes.array.isRequired,
filesUploadingForCurrentChannel: PropTypes.bool.isRequired,
fileSizeWarning: PropTypes.string,
rootId: PropTypes.string,
showFileMaxWarning: PropTypes.bool.isRequired,
theme: PropTypes.object.isRequired,
};
@ -31,6 +31,21 @@ export default class FileUploadPreview extends PureComponent {
files: [],
};
state = {
fileSizeWarning: null,
showFileMaxWarning: false,
};
componentDidMount() {
EventEmitter.on('fileMaxWarning', this.handleFileMaxWarning);
EventEmitter.on('fileSizeWarning', this.handleFileSizeWarning);
}
componentWillUnmount() {
EventEmitter.off('fileMaxWarning', this.handleFileMaxWarning);
EventEmitter.off('fileSizeWarning', this.handleFileSizeWarning);
}
buildFilePreviews = () => {
return this.props.files.map((file) => {
return (
@ -45,15 +60,25 @@ export default class FileUploadPreview extends PureComponent {
});
};
handleFileMaxWarning = () => {
this.setState({showFileMaxWarning: true});
setTimeout(() => {
this.setState({showFileMaxWarning: false});
}, 3000);
};
handleFileSizeWarning = (message) => {
this.setState({fileSizeWarning: message});
};
render() {
const {
showFileMaxWarning,
fileSizeWarning,
channelIsLoading,
filesUploadingForCurrentChannel,
deviceHeight,
files,
} = this.props;
const {fileSizeWarning, showFileMaxWarning} = this.state;
if (
!fileSizeWarning && !showFileMaxWarning &&
@ -69,6 +94,7 @@ export default class FileUploadPreview extends PureComponent {
horizontal={true}
style={style.scrollView}
contentContainerStyle={style.scrollViewContent}
keyboardShouldPersistTaps={'handled'}
>
{this.buildFilePreviews()}
</ScrollView>

View file

@ -6,15 +6,18 @@ import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getDimensions} from 'app/selectors/device';
import {checkForFileUploadingInChannel} from 'app/selectors/file';
import {getCurrentChannelDraft, getThreadDraft} from 'app/selectors/views';
import FileUploadPreview from './file_upload_preview';
function mapStateToProps(state, ownProps) {
const {deviceHeight} = getDimensions(state);
const currentDraft = ownProps.rootId ? getThreadDraft(state, ownProps.rootId) : getCurrentChannelDraft(state);
return {
channelIsLoading: state.views.channel.loading,
deviceHeight,
files: currentDraft.files,
filesUploadingForCurrentChannel: checkForFileUploadingInChannel(state, ownProps.channelId, ownProps.rootId),
theme: getTheme(state),
};

View file

@ -1,6 +1,16 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {isLandscape} from 'app/selectors/device';
import KeyboardLayout from './keyboard_layout';
export default KeyboardLayout;
function mapStateToProps(state) {
return {
isLandscape: isLandscape(state),
};
}
export default connect(mapStateToProps, null, null, {forwardRef: true})(KeyboardLayout);

View file

@ -4,6 +4,7 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Keyboard,
Platform,
TouchableHighlight,
View,
@ -104,11 +105,14 @@ export default class Post extends PureComponent {
},
};
if (Platform.OS === 'ios') {
navigator.push(options);
} else {
navigator.showModal(options);
}
Keyboard.dismiss();
requestAnimationFrame(() => {
if (Platform.OS === 'ios') {
navigator.push(options);
} else {
navigator.showModal(options);
}
});
};
autofillUserMention = (username) => {

View file

@ -439,6 +439,7 @@ export default class PostBody extends PureComponent {
scrollEnabled={false}
showsVerticalScrollIndicator={false}
showsHorizontalScrollIndicator={false}
keyboardShouldPersistTaps={'always'}
>
{messageComponent}
</ScrollView>

View file

@ -26,6 +26,8 @@ exports[`PostList setting channel deep link 1`] = `
initialNumToRender={15}
inverted={true}
keyExtractor={[Function]}
keyboardDismissMode="interactive"
keyboardShouldPersistTaps="handled"
maintainVisibleContentPosition={
Object {
"autoscrollToTopThreshold": 60,
@ -74,6 +76,8 @@ exports[`PostList setting permalink deep link 1`] = `
initialNumToRender={15}
inverted={true}
keyExtractor={[Function]}
keyboardDismissMode="interactive"
keyboardShouldPersistTaps="handled"
maintainVisibleContentPosition={
Object {
"autoscrollToTopThreshold": 60,
@ -122,6 +126,8 @@ exports[`PostList should match snapshot 1`] = `
initialNumToRender={15}
inverted={true}
keyExtractor={[Function]}
keyboardDismissMode="interactive"
keyboardShouldPersistTaps="handled"
maintainVisibleContentPosition={
Object {
"autoscrollToTopThreshold": 60,

View file

@ -66,6 +66,7 @@ export default class PostList extends PureComponent {
siteURL: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
location: PropTypes.string,
scrollViewNativeID: PropTypes.string,
};
static defaultProps = {
@ -320,6 +321,7 @@ export default class PostList extends PureComponent {
highlightPostId,
postIds,
refreshing,
scrollViewNativeID,
} = this.props;
const refreshControl = {refreshing};
@ -339,6 +341,8 @@ export default class PostList extends PureComponent {
extraData={this.makeExtraData(channelId, highlightPostId, this.props.extraData)}
initialNumToRender={INITIAL_BATCH_TO_RENDER}
inverted={true}
keyboardDismissMode={'interactive'}
keyboardShouldPersistTaps={'handled'}
keyExtractor={this.keyExtractor}
ListFooterComponent={this.props.renderFooter}
maintainVisibleContentPosition={SCROLL_POSITION_CONFIG}
@ -351,6 +355,7 @@ export default class PostList extends PureComponent {
renderItem={this.renderItem}
scrollEventThrottle={60}
{...refreshControl}
nativeID={scrollViewNativeID}
/>
);
}

View file

@ -11,9 +11,7 @@ import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {getFormattedFileSize} from 'mattermost-redux/utils/file_utils';
import AttachmentButton from 'app/components/attachment_button';
import Autocomplete from 'app/components/autocomplete';
import Fade from 'app/components/fade';
import FileUploadPreview from 'app/components/file_upload_preview';
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';
@ -27,9 +25,6 @@ import Typing from './components/typing';
const PLACEHOLDER_COLOR = changeOpacity('#000', 0.5);
const {RNTextInputReset} = NativeModules;
const AUTOCOMPLETE_MARGIN = 20;
const AUTOCOMPLETE_MAX_HEIGHT = 200;
export default class PostTextbox extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
@ -66,6 +61,8 @@ export default class PostTextbox extends PureComponent {
userIsOutOfOffice: PropTypes.bool.isRequired,
channelIsArchived: PropTypes.bool,
onCloseChannel: PropTypes.func,
cursorPositionEvent: PropTypes.string,
valueEvent: PropTypes.string,
};
static defaultProps = {
@ -86,10 +83,8 @@ export default class PostTextbox extends PureComponent {
this.state = {
cursorPosition: 0,
keyboardType: 'default',
fileSizeWarning: null,
top: 0,
value: props.value,
showFileMaxWarning: false,
};
}
@ -118,7 +113,7 @@ export default class PostTextbox extends PureComponent {
}
blur = () => {
if (this.input?.current) {
if (this.input.current) {
this.input.current.blur();
}
};
@ -198,6 +193,12 @@ export default class PostTextbox extends PureComponent {
handlePostDraftSelectionChanged = (event) => {
const cursorPosition = event.nativeEvent.selection.end;
const {cursorPositionEvent} = this.props;
if (cursorPositionEvent) {
EventEmitter.emit(cursorPositionEvent, cursorPosition);
}
this.setState({
cursorPosition,
});
@ -269,14 +270,19 @@ export default class PostTextbox extends PureComponent {
actions,
channelId,
rootId,
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) {
if (autocomplete && Platform.OS === 'android' && this.input?.current) {
RNTextInputReset.resetKeyboardInput(findNodeHandle(this.input.current));
}
this.checkMessageLength(value);
if (!autocomplete && valueEvent) {
EventEmitter.emit(valueEvent, value);
}
this.setState({value});
if (value) {
@ -430,11 +436,7 @@ export default class PostTextbox extends PureComponent {
};
onShowFileMaxWarning = () => {
this.setState({showFileMaxWarning: true}, () => {
setTimeout(() => {
this.setState({showFileMaxWarning: false});
}, 3000);
});
EventEmitter.emit('fileMaxWarning');
};
onShowFileSizeWarning = (filename) => {
@ -447,11 +449,10 @@ export default class PostTextbox extends PureComponent {
filename,
});
this.setState({fileSizeWarning}, () => {
setTimeout(() => {
this.setState({fileSizeWarning: null});
}, 3000);
});
EventEmitter.emit('fileSizeWarning', fileSizeWarning);
setTimeout(() => {
EventEmitter.emit('fileSizeWarning', null);
}, 3000);
};
onCloseChannelPress = () => {
@ -493,7 +494,6 @@ export default class PostTextbox extends PureComponent {
const {intl} = this.context;
const {
canUploadFiles,
channelId,
channelDisplayName,
channelIsLoading,
channelIsReadOnly,
@ -518,14 +518,7 @@ export default class PostTextbox extends PureComponent {
);
}
const {
cursorPosition,
fileSizeWarning,
showFileMaxWarning,
top,
value,
} = this.state;
const {value} = this.state;
const textValue = channelIsLoading ? '' : value;
let placeholder;
@ -564,20 +557,6 @@ export default class PostTextbox extends PureComponent {
return (
<React.Fragment>
<Typing/>
<FileUploadPreview
channelId={channelId}
files={files}
fileSizeWarning={fileSizeWarning}
rootId={rootId}
showFileMaxWarning={showFileMaxWarning}
/>
<Autocomplete
cursorPosition={cursorPosition}
maxHeight={Math.min(top - AUTOCOMPLETE_MARGIN, AUTOCOMPLETE_MAX_HEIGHT)}
onChangeText={this.handleTextChange}
value={this.state.value}
rootId={rootId}
/>
{!channelIsArchived && (
<View
style={style.inputWrapper}

View file

@ -186,6 +186,7 @@ export default class Reactions extends PureComponent {
alwaysBounceHorizontal={false}
horizontal={true}
overScrollMode='never'
keyboardShouldPersistTaps={'always'}
>
{reactionElements}
</ScrollView>

View file

@ -120,7 +120,7 @@ export default class DrawerLayout extends Component {
this.setState({ drawerShown, accessibilityViewIsModal });
}
if (this.props.keyboardDismissMode === 'on-drag') {
if (this.props.keyboardDismissMode === 'on-drag' || drawerShown) {
Keyboard.dismiss();
}

View file

@ -6,10 +6,11 @@ import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {
FlatList,
Keyboard,
Platform,
Text,
TouchableHighlight,
View,
Platform,
} from 'react-native';
import {injectIntl, intlShape} from 'react-intl';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
@ -61,6 +62,12 @@ class FilteredList extends Component {
constructor(props) {
super(props);
this.keyboardDismissProp = {
keyboardDismissMode: Platform.OS === 'ios' ? 'interactive' : 'none',
onScrollBeginDrag: Keyboard.dismiss,
};
this.state = {
dataSource: this.buildData(props),
};
@ -112,7 +119,6 @@ class FilteredList extends Component {
createChannelElement = (channel) => {
return (
<ChannelItem
ref={channel.id}
channelId={channel.id}
channel={channel}
isSearchResult={true}
@ -386,7 +392,6 @@ class FilteredList extends Component {
render() {
const {styles} = this.props;
const {dataSource} = this.state;
return (
@ -394,13 +399,12 @@ class FilteredList extends Component {
style={styles.container}
>
<FlatList
ref='list'
data={dataSource}
renderItem={this.renderItem}
keyExtractor={(item) => item.id}
onViewableItemsChanged={this.updateUnreadIndicators}
keyboardDismissMode={Platform.OS === 'ios' ? 'interactive' : 'on-drag'}
keyboardShouldPersistTaps='always'
{...this.keyboardDismissProp}
keyboardShouldPersistTaps={'always'}
maxToRenderPerBatch={10}
viewabilityConfig={VIEWABILITY_CONFIG}
/>

View file

@ -5,6 +5,8 @@ import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
InteractionManager,
Keyboard,
Platform,
SectionList,
Text,
TouchableHighlight,
@ -17,7 +19,7 @@ import {General} from 'mattermost-redux/constants';
import {debounce} from 'mattermost-redux/actions/helpers';
import ChannelItem from 'app/components/sidebars/main/channels_list/channel_item';
import {ListTypes} from 'app/constants';
import {DeviceTypes, ListTypes} from 'app/constants';
import {SidebarSectionTypes} from 'app/constants/view';
import {t} from 'app/utils/i18n';
import {preventDoubleTap} from 'app/utils/tap';
@ -44,7 +46,6 @@ export default class List extends PureComponent {
static contextTypes = {
intl: intlShape,
unreadChannelIds: [],
};
constructor(props) {
@ -56,6 +57,11 @@ export default class List extends PureComponent {
width: 0,
};
this.keyboardDismissProp = {
keyboardDismissMode: Platform.OS === 'ios' ? 'interactive' : 'none',
onScrollBeginDrag: this.scrollBeginDrag,
};
MaterialIcon.getImageSource('close', 20, this.props.theme.sidebarHeaderTextColor).then((source) => {
this.closeButton = source;
});
@ -135,7 +141,7 @@ export default class List extends PureComponent {
defaultMessage: 'CHANNELS',
};
}
}
};
buildSections = (props) => {
const {
@ -209,7 +215,7 @@ export default class List extends PureComponent {
modalPresentationStyle: 'overCurrentContext',
},
});
}
};
goToCreatePublicChannel = preventDoubleTap(() => {
const {navigator, theme} = this.props;
@ -308,6 +314,9 @@ export default class List extends PureComponent {
onSelectChannel = (channel, currentChannelId) => {
const {onSelectChannel} = this.props;
if (DeviceTypes.IS_TABLET) {
Keyboard.dismiss();
}
onSelectChannel(channel, currentChannelId);
};
@ -409,6 +418,12 @@ export default class List extends PureComponent {
});
};
scrollBeginDrag = () => {
if (DeviceTypes.IS_TABLET) {
Keyboard.dismiss();
}
};
render() {
const {styles, theme} = this.props;
const {sections, width, showIndicator} = this.state;
@ -425,10 +440,11 @@ export default class List extends PureComponent {
renderSectionHeader={this.renderSectionHeader}
keyExtractor={this.keyExtractor}
onViewableItemsChanged={this.updateUnreadIndicators}
keyboardDismissMode='on-drag'
maxToRenderPerBatch={10}
stickySectionHeadersEnabled={false}
viewabilityConfig={VIEWABILITY_CONFIG}
keyboardShouldPersistTaps={'always'}
{...this.keyboardDismissProp}
/>
{showIndicator &&
<UnreadIndicator

View file

@ -7,7 +7,6 @@ import {
AppState,
Dimensions,
InteractionManager,
Keyboard,
NativeModules,
Platform,
YellowBox,
@ -426,7 +425,6 @@ const handleAppActive = async () => {
}
app.setInBackgroundSince(null);
Keyboard.dismiss();
};
const handleAppInActive = () => {

View file

@ -6,22 +6,25 @@ 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 EmptyToolbar from 'app/components/start/empty_toolbar';
import ChannelLoader from 'app/components/channel_loader';
import EmptyToolbar from 'app/components/start/empty_toolbar';
import FileUploadPreview from 'app/components/file_upload_preview';
import MainSidebar from 'app/components/sidebars/main';
import SettingsSidebar from 'app/components/sidebars/settings';
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';
@ -45,6 +48,9 @@ const {
IOSX_TOP_PORTRAIT,
} = ViewTypes;
const CHANNEL_POST_TEXTBOX_CURSOR_CHANGE = 'onChannelTextBoxCursorChange';
const CHANNEL_POST_TEXTBOX_VALUE_CHANGE = 'onChannelTextBoxValueChange';
let ClientUpgradeListener;
export default class Channel extends PureComponent {
@ -77,6 +83,13 @@ export default class Channel extends PureComponent {
constructor(props) {
super(props);
this.postTextbox = React.createRef();
this.keyboardTracker = React.createRef();
props.navigator.setStyle({
screenBackgroundColor: props.theme.centerChannelBg,
});
if (LocalConfig.EnableMobileClientUpgrade && !ClientUpgradeListener) {
ClientUpgradeListener = require('app/components/client_upgrade_listener').default;
}
@ -140,19 +153,19 @@ export default class Channel extends PureComponent {
if (this.props.currentChannelId && !prevProps.currentChannelId) {
EventEmitter.emit('renderDrawer');
}
if (this.props.currentChannelId && this.props.currentChannelId !== prevProps.currentChannelId) {
this.updateNativeScrollView();
}
}
componentWillUnmount() {
EventEmitter.off('leave_team', this.handleLeaveTeam);
}
attachPostTextBox = (ref) => {
this.postTextbox = ref;
};
blurPostTextBox = () => {
if (this.postTextbox) {
this.postTextbox.blur();
if (this.postTextbox?.current) {
this.postTextbox.current.blur();
}
};
@ -233,13 +246,23 @@ export default class Channel extends PureComponent {
},
};
Keyboard.dismiss();
if (Platform.OS === 'android') {
navigator.showModal(options);
} else {
navigator.push(options);
requestAnimationFrame(() => {
navigator.push(options);
});
}
});
handleAutoComplete = (value) => {
if (this.postTextbox?.current) {
this.postTextbox.current.handleTextChange(value, true);
}
};
handleLeaveTeam = () => {
this.props.actions.selectDefaultTeam();
};
@ -278,6 +301,12 @@ export default class Channel extends PureComponent {
this.loadChannels(this.props.currentTeamId);
};
updateNativeScrollView = () => {
if (this.keyboardTracker?.current) {
this.keyboardTracker.current.resetScrollView(this.props.currentChannelId);
}
};
render() {
const {
channelsRequestFailed,
@ -334,21 +363,34 @@ export default class Channel extends PureComponent {
openSettingsDrawer={this.openSettingsSidebar}
onPress={this.goToChannelInfo}
/>
<KeyboardLayout>
<View style={style.flex}>
<ChannelPostList navigator={navigator}/>
</View>
<PostTextbox
ref={this.attachPostTextBox}
navigator={navigator}
/>
</KeyboardLayout>
<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
style={[style.channelLoader, loaderDimensions]}
maxRows={isLandscape ? 4 : 6}
/>
{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>
</SettingsSidebar>
<InteractiveDialogController
navigator={navigator}

View file

@ -4,6 +4,7 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Keyboard,
TouchableOpacity,
View,
} from 'react-native';
@ -25,6 +26,7 @@ export default class ChannelSearchButton extends PureComponent {
handlePress = preventDoubleTap(async () => {
const {actions, navigator} = this.props;
Keyboard.dismiss();
await actions.clearSearch();
await actions.showSearchModal(navigator);
});

View file

@ -4,6 +4,7 @@
import PropTypes from 'prop-types';
import React, {PureComponent} from 'react';
import {
Keyboard,
Platform,
StyleSheet,
View,
@ -43,6 +44,7 @@ export default class ChannelPostList extends PureComponent {
postVisibility: PropTypes.number,
refreshing: PropTypes.bool.isRequired,
theme: PropTypes.object.isRequired,
updateNativeScrollView: PropTypes.func,
};
static defaultProps = {
@ -86,6 +88,11 @@ export default class ChannelPostList extends PureComponent {
if (prevProps.channelId !== this.props.channelId && tracker.channelSwitch) {
this.props.actions.recordLoadTime('Switch Channel', 'channelSwitch');
}
if (!prevProps.postIds?.length && this.props.postIds?.length > 0) {
// This is needed to re-bind the scrollview natively when getting the first posts
this.props.updateNativeScrollView();
}
}
componentWillUnmount() {
@ -101,6 +108,7 @@ export default class ChannelPostList extends PureComponent {
const {actions, channelId, navigator, theme} = this.props;
const rootId = (post.root_id || post.id);
Keyboard.dismiss();
actions.loadThreadIfNecessary(rootId);
actions.selectPost(rootId);
@ -123,7 +131,9 @@ export default class ChannelPostList extends PureComponent {
if (Platform.OS === 'android') {
navigator.showModal(options);
} else {
navigator.push(options);
requestAnimationFrame(() => {
navigator.push(options);
});
}
};
@ -216,6 +226,7 @@ export default class ChannelPostList extends PureComponent {
navigator={navigator}
renderFooter={this.renderFooter}
refreshing={refreshing}
scrollViewNativeID={channelId}
/>
);
}

View file

@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MoreChannels should match snapshot 1`] = `
<KeyboardLayout>
<ForwardRef(forwardConnectRef)>
<Connect(StatusBar) />
<React.Fragment>
<View
@ -110,5 +110,5 @@ exports[`MoreChannels should match snapshot 1`] = `
}
/>
</React.Fragment>
</KeyboardLayout>
</ForwardRef(forwardConnectRef)>
`;

View file

@ -292,6 +292,12 @@ export default class MoreChannels extends PureComponent {
);
};
renderItem = (props) => {
return (
<ChannelListRow {...props}/>
);
}
searchChannels = (text) => {
const {actions, channels, currentTeamId} = this.props;
@ -363,7 +369,7 @@ export default class MoreChannels extends PureComponent {
noResults={this.renderNoResults()}
onLoadMore={more}
onRowPress={this.onSelectChannel}
renderItem={ChannelListRow}
renderItem={this.renderItem}
theme={theme}
/>
</React.Fragment>

View file

@ -275,6 +275,18 @@ export default class SelectorScreen extends PureComponent {
);
};
renderChannelItem = (props) => {
return <ChannelListRow {...props}/>;
};
renderOptionItem = (props) => {
return <OptionListRow {...props}/>;
};
renderUserItem = (props) => {
return <UserListRow {...props}/>;
};
render() {
const {formatMessage} = this.context.intl;
const {theme, dataSource} = this.props;
@ -294,11 +306,11 @@ export default class SelectorScreen extends PureComponent {
let rowComponent;
if (dataSource === ViewTypes.DATA_SOURCE_USERS) {
rowComponent = UserListRow;
rowComponent = this.renderUserItem;
} else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
rowComponent = ChannelListRow;
rowComponent = this.renderChannelItem;
} else {
rowComponent = OptionListRow;
rowComponent = this.renderOptionItem;
}
const {data, listType} = this.getDataResults();

View file

@ -1,64 +1,77 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`thread should match snapshot, has root post 1`] = `
<Connect(SafeAreaIos)
excludeHeader={true}
keyboardOffset={20}
>
<Connect(StatusBar) />
<KeyboardLayout
style={
Object {
"backgroundColor": "#ffffff",
"flex": 1,
}
}
<React.Fragment>
<Connect(SafeAreaIos)
excludeHeader={true}
keyboardOffset={20}
>
<Connect(PostList)
currentUserId="member_user_id"
indicateNewMessages={false}
lastPostIndex={-1}
location="thread"
navigator={
Object {
"dismissModal": [MockFunction],
"pop": [MockFunction],
"resetTo": [MockFunction],
"setTitle": [MockFunction] {
"calls": Array [
Array [
<Connect(StatusBar) />
<React.Fragment>
<Connect(PostList)
currentUserId="member_user_id"
indicateNewMessages={false}
lastPostIndex={-1}
location="thread"
navigator={
Object {
"dismissModal": [MockFunction],
"pop": [MockFunction],
"resetTo": [MockFunction],
"setTitle": [MockFunction] {
"calls": Array [
Array [
Object {
"title": undefined,
},
],
],
"results": Array [
Object {
"title": undefined,
"type": "return",
"value": undefined,
},
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
},
},
}
}
}
postIds={
Array [
"root_id",
"post_id_1",
"post_id_2",
]
}
renderFooter={
<Loading
color="grey"
size="large"
style={Object {}}
/>
}
/>
onPostPress={[Function]}
postIds={
Array [
"root_id",
"post_id_1",
"post_id_2",
]
}
renderFooter={
<Loading
color="grey"
size="large"
style={Object {}}
/>
}
scrollViewNativeID="threadPostList"
/>
<Connect(FileUploadPreview)
channelId="channel_id"
rootId="root_id"
/>
<ForwardRef(forwardConnectRef)
cursorPositionEvent="onThreadTextBoxCursorChange"
maxHeight={200}
onChangeText={[Function]}
rootId="root_id"
valueEvent="onThreadTextBoxValueChange"
/>
</React.Fragment>
</Connect(SafeAreaIos)>
<KeyboardTrackingView
scrollViewNativeID="threadPostList"
>
<ForwardRef(forwardConnectRef)
channelId="channel_id"
channelIsArchived={false}
cursorPositionEvent="onThreadTextBoxCursorChange"
navigator={
Object {
"dismissModal": [MockFunction],
@ -83,32 +96,26 @@ exports[`thread should match snapshot, has root post 1`] = `
}
onCloseChannel={[Function]}
rootId="root_id"
valueEvent="onThreadTextBoxValueChange"
/>
</KeyboardLayout>
</Connect(SafeAreaIos)>
</KeyboardTrackingView>
</React.Fragment>
`;
exports[`thread should match snapshot, no root post, loading 1`] = `
<Connect(SafeAreaIos)
excludeHeader={true}
keyboardOffset={20}
>
<Connect(StatusBar) />
<KeyboardLayout
style={
Object {
"backgroundColor": "#ffffff",
"flex": 1,
}
}
<React.Fragment>
<Connect(SafeAreaIos)
excludeHeader={true}
keyboardOffset={20}
>
<Connect(StatusBar) />
<Loading
color="grey"
size="large"
style={Object {}}
/>
</KeyboardLayout>
</Connect(SafeAreaIos)>
</Connect(SafeAreaIos)>
</React.Fragment>
`;
exports[`thread should match snapshot, render footer 1`] = `
@ -139,6 +146,7 @@ exports[`thread should match snapshot, render footer 1`] = `
},
}
}
onPostPress={[Function]}
postIds={
Array [
"root_id",
@ -153,6 +161,7 @@ exports[`thread should match snapshot, render footer 1`] = `
style={Object {}}
/>
}
scrollViewNativeID="threadPostList"
/>
`;
@ -185,6 +194,7 @@ exports[`thread should match snapshot, render footer 2`] = `
},
}
}
onPostPress={[Function]}
postIds={
Array [
"root_id",
@ -193,28 +203,22 @@ exports[`thread should match snapshot, render footer 2`] = `
]
}
renderFooter={null}
scrollViewNativeID="threadPostList"
/>
`;
exports[`thread should match snapshot, render footer 3`] = `
<Connect(SafeAreaIos)
excludeHeader={true}
keyboardOffset={20}
>
<Connect(StatusBar) />
<KeyboardLayout
style={
Object {
"backgroundColor": "#ffffff",
"flex": 1,
}
}
<React.Fragment>
<Connect(SafeAreaIos)
excludeHeader={true}
keyboardOffset={20}
>
<Connect(StatusBar) />
<Loading
color="grey"
size="large"
style={Object {}}
/>
</KeyboardLayout>
</Connect(SafeAreaIos)>
</Connect(SafeAreaIos)>
</React.Fragment>
`;

View file

@ -3,23 +3,29 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Platform} from 'react-native';
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 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 {makeStyleSheetFromTheme, setNavigatorStyles} from 'app/utils/theme';
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 {
static propTypes = {
actions: PropTypes.shape({
@ -47,17 +53,21 @@ export default class Thread extends PureComponent {
intl: intlShape,
};
componentWillMount() {
const {channelType, displayName} = this.props;
const {intl} = this.context;
constructor(props, context) {
super(props);
const {channelType, displayName} = props;
const {formatMessage} = context.intl;
let title;
if (channelType === General.DM_CHANNEL) {
title = intl.formatMessage({id: 'mobile.routes.thread_dm', defaultMessage: 'Direct Message Thread'});
title = formatMessage({id: 'mobile.routes.thread_dm', defaultMessage: 'Direct Message Thread'});
} else {
title = intl.formatMessage({id: 'mobile.routes.thread', defaultMessage: '{channelName} Thread'}, {channelName: displayName});
title = formatMessage({id: 'mobile.routes.thread', defaultMessage: '{channelName} Thread'}, {channelName: displayName});
}
this.postTextbox = React.createRef();
this.props.navigator.setTitle({
title,
});
@ -96,10 +106,20 @@ export default class Thread extends PureComponent {
}
};
handleAutoComplete = (value) => {
if (this.postTextbox?.current) {
this.postTextbox.current.handleTextChange(value, true);
}
};
hasRootPost = () => {
return this.props.postIds.includes(this.props.rootId);
};
hideKeyboard = () => {
Keyboard.dismiss();
};
renderFooter = () => {
if (!this.hasRootPost() && this.props.threadLoadingStatus.status !== RequestStatus.STARTED) {
return (
@ -141,34 +161,53 @@ export default class Thread extends PureComponent {
navigator,
postIds,
rootId,
theme,
channelIsArchived,
} = this.props;
const style = getStyle(theme);
let content;
let postTextBox;
if (this.hasRootPost()) {
content = (
<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}
location={THREAD}
/>
<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 = (
<PostTextbox
channelIsArchived={channelIsArchived}
rootId={rootId}
channelId={channelId}
navigator={navigator}
onCloseChannel={this.onCloseChannel}
/>
<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 = (
@ -177,25 +216,16 @@ export default class Thread extends PureComponent {
}
return (
<SafeAreaView
excludeHeader={true}
keyboardOffset={20}
>
<StatusBar/>
<KeyboardLayout style={style.container}>
<React.Fragment>
<SafeAreaView
excludeHeader={true}
keyboardOffset={20}
>
<StatusBar/>
{content}
{postTextBox}
</KeyboardLayout>
</SafeAreaView>
</SafeAreaView>
{postTextBox}
</React.Fragment>
);
}
}
const getStyle = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
backgroundColor: theme.centerChannelBg,
},
};
});

View file

@ -1,6 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Keyboard} from 'react-native';
import {
IMAGE_MAX_HEIGHT,
IMAGE_MIN_DIMENSION,
@ -75,18 +77,21 @@ export function previewImageAtIndex(navigator, components, index, files) {
}
function goToImagePreview(navigator, passProps) {
navigator.showModal({
screen: 'ImagePreview',
title: '',
animationType: 'none',
passProps,
navigatorStyle: {
navBarHidden: true,
statusBarHidden: false,
statusBarHideWithNavBar: false,
screenBackgroundColor: 'transparent',
modalPresentationStyle: 'overCurrentContext',
},
Keyboard.dismiss();
requestAnimationFrame(() => {
navigator.showModal({
screen: 'ImagePreview',
title: '',
animationType: 'none',
passProps,
navigatorStyle: {
navBarHidden: true,
statusBarHidden: false,
statusBarHideWithNavBar: false,
screenBackgroundColor: 'transparent',
modalPresentationStyle: 'overCurrentContext',
},
});
});
}

View file

@ -555,4 +555,4 @@
"user.settings.push_notification.offline": "Offline",
"user.settings.push_notification.online": "Online, away or offline",
"web.root.signup_info": "All team communication in one place, searchable and accessible anywhere"
}
}

View file

@ -22,7 +22,7 @@ before_all do |lane, options|
end
# Block to ensure we are on the right branch
branch = ENV['BRANCH_TO_BUILD'] || 'master'
branch = ENV['BRANCH_TO_BUILD'] || ENV['GIT_BRANCH']
begin
ensure_git_branch(
branch: branch
@ -693,7 +693,7 @@ lane :upload_file_to_s3 do |options|
filename_plist = "#{app_name}.plist"
end
end
build_folder_path = Dir[File.expand_path('..')].first
file_path = "#{build_folder_path}/#{filename}"

View file

@ -5,6 +5,7 @@
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; };
00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */; };
@ -51,6 +52,7 @@
7BD159C40A68467FB5A17141 /* FontAwesome5_Solid.ttf in Resources */ = {isa = PBXBuildFile; fileRef = DC1D660B55BE462A9C3B8028 /* FontAwesome5_Solid.ttf */; };
7F151D3E221B062700FAD8F3 /* RuntimeUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F151D3D221B062700FAD8F3 /* RuntimeUtils.swift */; };
7F151D41221B069200FAD8F3 /* 0155-keys.png in Resources */ = {isa = PBXBuildFile; fileRef = 7F151D40221B069200FAD8F3 /* 0155-keys.png */; };
7F1967FF228E379000D19270 /* libKeyboardTrackingView.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F11AA10228848D8001C9540 /* libKeyboardTrackingView.a */; };
7F1A56B4227E38B600EF7A90 /* libRNCAsyncStorage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F1A569B227E389600EF7A90 /* libRNCAsyncStorage.a */; };
7F240A1C220D3A2300637665 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F240A1B220D3A2300637665 /* ShareViewController.swift */; };
7F240A1F220D3A2300637665 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7F240A1D220D3A2300637665 /* MainInterface.storyboard */; };
@ -366,6 +368,13 @@
remoteGlobalIDString = 134814201AA4EA6300B7C361;
remoteInfo = RCTLinking;
};
7F11AA0F228848D8001C9540 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 7F11AA06228848D8001C9540 /* KeyboardTrackingView.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = D834CED81CC64F2400FA5668;
remoteInfo = KeyboardTrackingView;
};
7F1A569A227E389600EF7A90 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 7F1A5663227E389600EF7A90 /* RNCAsyncStorage.xcodeproj */;
@ -779,6 +788,7 @@
78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = "<group>"; };
79CB6EBA24FE4ABFB0C155F0 /* YTPlayerView-iframe-player.html */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "YTPlayerView-iframe-player.html"; path = "../node_modules/react-native-youtube/assets/YTPlayerView-iframe-player.html"; sourceTree = "<group>"; };
7DCC3D826CE640AF8F491692 /* BVLinearGradient.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = BVLinearGradient.xcodeproj; path = "../node_modules/react-native-linear-gradient/BVLinearGradient.xcodeproj"; sourceTree = "<group>"; };
7F11AA06228848D8001C9540 /* KeyboardTrackingView.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = KeyboardTrackingView.xcodeproj; path = "../node_modules/react-native-keyboard-tracking-view/lib/KeyboardTrackingView.xcodeproj"; sourceTree = "<group>"; };
7F151D3D221B062700FAD8F3 /* RuntimeUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RuntimeUtils.swift; path = Mattermost/RuntimeUtils.swift; sourceTree = "<group>"; };
7F151D40221B069200FAD8F3 /* 0155-keys.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "0155-keys.png"; path = "Mattermost/0155-keys.png"; sourceTree = "<group>"; };
7F151D42221B07F700FAD8F3 /* MattermostShare-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MattermostShare-Bridging-Header.h"; sourceTree = "<group>"; };
@ -889,6 +899,7 @@
files = (
7FABE04622137F5C00D0F595 /* libUploadAttachments.a in Frameworks */,
74D116AD208A8D5600CF8A79 /* libRNKeychain.a in Frameworks */,
7F1967FF228E379000D19270 /* libKeyboardTrackingView.a in Frameworks */,
37ABD3C81F4CE142001FDE6B /* libART.a in Frameworks */,
375218501F4B9EE70035444B /* libRCTCameraRoll.a in Frameworks */,
7F43D5E01F6BF994001FC614 /* libRNSVG.a in Frameworks */,
@ -1241,6 +1252,14 @@
name = Products;
sourceTree = "<group>";
};
7F11AA07228848D8001C9540 /* Products */ = {
isa = PBXGroup;
children = (
7F11AA10228848D8001C9540 /* libKeyboardTrackingView.a */,
);
name = Products;
sourceTree = "<group>";
};
7F1A5664227E389600EF7A90 /* Products */ = {
isa = PBXGroup;
children = (
@ -1484,6 +1503,7 @@
isa = PBXGroup;
children = (
7FABE04022137F2900D0F595 /* UploadAttachments.xcodeproj */,
7F11AA06228848D8001C9540 /* KeyboardTrackingView.xcodeproj */,
7F1A5663227E389600EF7A90 /* RNCAsyncStorage.xcodeproj */,
7F4C258F227E3AE9009144EF /* RNCNetInfo.xcodeproj */,
7F26C1C6219C463300FEB42D /* RNCWebView.xcodeproj */,
@ -1732,6 +1752,10 @@
ProductGroup = 7F8C4A5D1F3E21FB003A22BA /* Products */;
ProjectRef = 27A6EA89298440439DA9F98D /* JailMonkey.xcodeproj */;
},
{
ProductGroup = 7F11AA07228848D8001C9540 /* Products */;
ProjectRef = 7F11AA06228848D8001C9540 /* KeyboardTrackingView.xcodeproj */;
},
{
ProductGroup = 00C302A81ABCB8CE00DB3ED1 /* Products */;
ProjectRef = 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */;
@ -2130,6 +2154,13 @@
remoteRef = 78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
7F11AA10228848D8001C9540 /* libKeyboardTrackingView.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = libKeyboardTrackingView.a;
remoteRef = 7F11AA0F228848D8001C9540 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
7F1A569B227E389600EF7A90 /* libRNCAsyncStorage.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;

View file

@ -1,5 +1,6 @@
#!/bin/sh
export NODE_OPTIONS=--max_old_space_size=12000
export NODE_BINARY=node
if [[ "${SENTRY_ENABLED}" = "true" ]]; then

1316
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -40,6 +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-keychain": "3.1.3",
"react-native-linear-gradient": "2.5.4",
"react-native-local-auth": "github:mattermost/react-native-local-auth#cc9ce2f468fbf7b431dfad3191a31aaa9227a6ab",