Improve performance on post textbox (#1138)

This commit is contained in:
enahum 2017-11-13 18:36:24 -03:00
parent 20168d90ca
commit aa5f0c9557
10 changed files with 219 additions and 245 deletions

View file

@ -27,16 +27,16 @@ export default class AtMention extends PureComponent {
matchTerm: PropTypes.string,
onChangeText: PropTypes.func.isRequired,
outChannel: PropTypes.array,
postDraft: PropTypes.string,
requestStatus: PropTypes.string.isRequired,
teamMembers: PropTypes.array,
theme: PropTypes.object.isRequired
theme: PropTypes.object.isRequired,
value: PropTypes.string
};
static defaultProps = {
defaultChannel: {},
postDraft: '',
isSearch: false
isSearch: false,
value: ''
};
constructor(props) {
@ -144,8 +144,8 @@ export default class AtMention extends PureComponent {
};
completeMention = (mention) => {
const {cursorPosition, isSearch, onChangeText, postDraft} = this.props;
const mentionPart = postDraft.substring(0, cursorPosition);
const {cursorPosition, isSearch, onChangeText, value} = this.props;
const mentionPart = value.substring(0, cursorPosition);
let completedDraft;
if (isSearch) {
@ -154,8 +154,8 @@ export default class AtMention extends PureComponent {
completedDraft = mentionPart.replace(AT_MENTION_REGEX, `@${mention} `);
}
if (postDraft.length > cursorPosition) {
completedDraft += postDraft.substring(cursorPosition);
if (value.length > cursorPosition) {
completedDraft += value.substring(cursorPosition);
}
onChangeText(completedDraft, true);

View file

@ -19,25 +19,10 @@ import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import AtMention from './at_mention';
function mapStateToProps(state, ownProps) {
const {cursorPosition, isSearch, rootId} = ownProps;
const {cursorPosition, isSearch} = ownProps;
const currentChannelId = getCurrentChannelId(state);
let postDraft = '';
if (isSearch) {
postDraft = state.views.search;
} else if (ownProps.rootId) {
const threadDraft = state.views.thread.drafts[rootId];
if (threadDraft) {
postDraft = threadDraft.draft;
}
} else if (currentChannelId) {
const channelDraft = state.views.channel.drafts[currentChannelId];
if (channelDraft) {
postDraft = channelDraft.draft;
}
}
const value = postDraft.substring(0, cursorPosition);
const value = ownProps.value.substring(0, cursorPosition);
const matchTerm = getMatchTermForAtMention(value, isSearch);
let teamMembers;
@ -54,14 +39,12 @@ function mapStateToProps(state, ownProps) {
currentChannelId,
currentTeamId: getCurrentTeamId(state),
defaultChannel: getDefaultChannel(state),
postDraft,
matchTerm,
teamMembers,
inChannel,
outChannel,
requestStatus: state.requests.users.autocompleteUsers.status,
theme: getTheme(state),
...ownProps
theme: getTheme(state)
};
}

View file

@ -24,16 +24,16 @@ export default class ChannelMention extends PureComponent {
myChannels: PropTypes.array,
otherChannels: PropTypes.array,
onChangeText: PropTypes.func.isRequired,
postDraft: PropTypes.string,
privateChannels: PropTypes.array,
publicChannels: PropTypes.array,
requestStatus: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired
theme: PropTypes.object.isRequired,
value: PropTypes.string
};
static defaultProps = {
postDraft: '',
isSearch: false
isSearch: false,
value: ''
};
constructor(props) {
@ -116,8 +116,8 @@ export default class ChannelMention extends PureComponent {
}
completeMention = (mention) => {
const {cursorPosition, isSearch, onChangeText, postDraft} = this.props;
const mentionPart = postDraft.substring(0, cursorPosition);
const {cursorPosition, isSearch, onChangeText, value} = this.props;
const mentionPart = value.substring(0, cursorPosition);
let completedDraft;
if (isSearch) {
@ -127,8 +127,8 @@ export default class ChannelMention extends PureComponent {
completedDraft = mentionPart.replace(CHANNEL_MENTION_REGEX, `~${mention} `);
}
if (postDraft.length > cursorPosition) {
completedDraft += postDraft.substring(cursorPosition);
if (value.length > cursorPosition) {
completedDraft += value.substring(cursorPosition);
}
onChangeText(completedDraft, true);

View file

@ -5,7 +5,6 @@ import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {searchChannels} from 'mattermost-redux/actions/channels';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {
@ -20,25 +19,9 @@ import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import ChannelMention from './channel_mention';
function mapStateToProps(state, ownProps) {
const {cursorPosition, isSearch, rootId} = ownProps;
const currentChannelId = getCurrentChannelId(state);
const {cursorPosition, isSearch} = ownProps;
let postDraft = '';
if (isSearch) {
postDraft = state.views.search;
} else if (rootId) {
const threadDraft = state.views.thread.drafts[rootId];
if (threadDraft) {
postDraft = threadDraft.draft;
}
} else if (currentChannelId) {
const channelDraft = state.views.channel.drafts[currentChannelId];
if (channelDraft) {
postDraft = channelDraft.draft;
}
}
const value = postDraft.substring(0, cursorPosition);
const value = ownProps.value.substring(0, cursorPosition);
const matchTerm = getMatchTermForChannelMention(value, isSearch);
let myChannels;
@ -61,7 +44,6 @@ function mapStateToProps(state, ownProps) {
privateChannels,
currentTeamId: getCurrentTeamId(state),
matchTerm,
postDraft,
requestStatus: state.requests.channels.getChannels.status,
theme: getTheme(state)
};

View file

@ -22,25 +22,25 @@ export default class EmojiSuggestion extends Component {
}).isRequired,
cursorPosition: PropTypes.number,
emojis: PropTypes.array.isRequired,
postDraft: PropTypes.string,
theme: PropTypes.object.isRequired,
onChangeText: PropTypes.func.isRequired,
rootId: PropTypes.string
rootId: PropTypes.string,
value: PropTypes.string
};
static defaultProps = {
defaultChannel: {},
postDraft: ''
value: ''
};
state = {
active: false,
dataSource: []
}
};
componentWillReceiveProps(nextProps) {
const regex = EMOJI_REGEX;
const match = nextProps.postDraft.substring(0, nextProps.cursorPosition).match(regex);
const match = nextProps.value.substring(0, nextProps.cursorPosition).match(regex);
if (!match || this.state.emojiComplete) {
this.setState({
@ -74,8 +74,8 @@ export default class EmojiSuggestion extends Component {
}
completeSuggestion = (emoji) => {
const {actions, cursorPosition, onChangeText, postDraft, rootId} = this.props;
const emojiPart = postDraft.substring(0, cursorPosition);
const {actions, cursorPosition, onChangeText, value, rootId} = this.props;
const emojiPart = value.substring(0, cursorPosition);
if (emojiPart.startsWith('+:')) {
actions.addReactionToLatestPost(emoji, rootId);
@ -83,8 +83,8 @@ export default class EmojiSuggestion extends Component {
} else {
let completedDraft = emojiPart.replace(EMOJI_REGEX, `:${emoji}: `);
if (postDraft.length > cursorPosition) {
completedDraft += postDraft.substring(cursorPosition);
if (value.length > cursorPosition) {
completedDraft += value.substring(cursorPosition);
}
onChangeText(completedDraft);

View file

@ -25,26 +25,11 @@ const getEmojisByName = createSelector(
}
);
function mapStateToProps(state, ownProps) {
const {currentChannelId} = state.entities.channels;
function mapStateToProps(state) {
const emojis = getEmojisByName(state);
let postDraft;
if (ownProps.rootId) {
const threadDraft = state.views.thread.drafts[ownProps.rootId];
if (threadDraft) {
postDraft = threadDraft.draft;
}
} else if (currentChannelId) {
const channelDraft = state.views.channel.drafts[currentChannelId];
if (channelDraft) {
postDraft = channelDraft.draft;
}
}
return {
emojis,
postDraft,
theme: getTheme(state)
};
}

View file

@ -17,7 +17,8 @@ export default class Autocomplete extends PureComponent {
static propTypes = {
onChangeText: PropTypes.func.isRequired,
rootId: PropTypes.string,
isSearch: PropTypes.bool
isSearch: PropTypes.bool,
value: PropTypes.string
};
static defaultProps = {

View file

@ -16,7 +16,7 @@ export default class PaperClipIcon extends Component {
if (this.root) {
this.root.setNativeProps(nativeProps);
}
}
};
render() {
return (

View file

@ -3,16 +3,7 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Alert,
BackHandler,
Keyboard,
Platform,
Text,
TextInput,
TouchableOpacity,
View
} from 'react-native';
import {Alert, BackHandler, Keyboard, Platform, Text, TextInput, TouchableOpacity, View} from 'react-native';
import {injectIntl, intlShape} from 'react-intl';
import {RequestStatus} from 'mattermost-redux/constants';
@ -62,11 +53,16 @@ class PostTextbox extends PureComponent {
value: ''
};
state = {
contentHeight: INITIAL_HEIGHT,
inputWidth: null,
keyboardType: 'default'
};
constructor(props) {
super(props);
this.state = {
contentHeight: INITIAL_HEIGHT,
inputWidth: null,
keyboardType: 'default',
value: props.value
};
}
componentDidMount() {
if (Platform.OS === 'android') {
@ -76,8 +72,54 @@ class PostTextbox extends PureComponent {
}
componentWillReceiveProps(nextProps) {
if (nextProps.channelId !== this.props.channelId || nextProps.rootId !== this.props.rootId) {
this.setState({value: nextProps.value});
}
}
componentWillUnmount() {
if (Platform.OS === 'android') {
Keyboard.removeListener('keyboardDidHide', this.handleAndroidKeyboard);
BackHandler.removeEventListener('hardwareBackPress', this.handleAndroidBack);
}
}
attachAutocomplete = (c) => {
this.autocomplete = c;
};
blur = () => {
this.refs.input.blur();
};
canSend = () => {
const {files, uploadFileRequestStatus} = this.props;
const {value} = this.state;
const valueLength = value.trim().length;
if (files.length) {
return valueLength <= MAX_MESSAGE_LENGTH && uploadFileRequestStatus !== RequestStatus.STARTED && files.filter((f) => !f.failed).length > 0;
}
return valueLength > 0 && valueLength <= MAX_MESSAGE_LENGTH;
};
changeDraft = (text) => {
const {
actions,
channelId,
rootId
} = this.props;
if (rootId) {
actions.handleCommentDraftChanged(rootId, text);
} else {
actions.handlePostDraftChanged(channelId, text);
}
};
checkMessageLength = (value) => {
const {intl} = this.props;
const {value} = nextProps;
const valueLength = value.trim().length;
if (valueLength > MAX_MESSAGE_LENGTH) {
@ -95,28 +137,6 @@ class PostTextbox extends PureComponent {
})
);
}
}
componentWillUnmount() {
if (Platform.OS === 'android') {
Keyboard.removeListener('keyboardDidHide', this.handleAndroidKeyboard);
BackHandler.removeEventListener('hardwareBackPress', this.handleAndroidBack);
}
}
blur = () => {
this.refs.input.blur();
};
canSend = () => {
const {files, uploadFileRequestStatus, value} = this.props;
const valueLength = value.trim().length;
if (files.length) {
return valueLength <= MAX_MESSAGE_LENGTH && uploadFileRequestStatus !== RequestStatus.STARTED && files.filter((f) => !f.failed).length > 0;
}
return valueLength > 0 && valueLength <= MAX_MESSAGE_LENGTH;
};
handleAndroidKeyboard = () => {
@ -132,12 +152,47 @@ class PostTextbox extends PureComponent {
return false;
};
handleContentSizeChange = (event) => {
let contentHeight = event.nativeEvent.layout.height;
if (contentHeight < INITIAL_HEIGHT) {
contentHeight = INITIAL_HEIGHT;
}
this.setState({
contentHeight
});
};
handleEndEditing = (e) => {
if (e && e.nativeEvent) {
this.changeDraft(e.nativeEvent.text || '');
}
};
handleInputSizeChange = (event) => {
this.setState({
inputWidth: event.nativeEvent.layout.width
});
};
handlePostDraftSelectionChanged = (event) => {
const cursorPosition = event.nativeEvent.selection.end;
if (this.props.rootId) {
this.props.actions.handleCommentDraftSelectionChanged(this.props.rootId, cursorPosition);
} else {
this.props.actions.handlePostDraftSelectionChanged(this.props.channelId, cursorPosition);
}
this.autocomplete.handleSelectionChange(event);
};
handleSendMessage = () => {
if (!this.canSend()) {
return;
}
const {files, value} = this.props;
const {files} = this.props;
const {value} = this.state;
const isReactionMatch = value.match(IS_REACTION_REGEX);
if (isReactionMatch) {
@ -171,8 +226,85 @@ class PostTextbox extends PureComponent {
}
};
handleSubmit = () => {
// Workaround for android as the multiline is not working
if (Platform.OS === 'android') {
if (this.timeout) {
clearTimeout(this.timeout);
}
this.timeout = setTimeout(() => {
let {value: msg} = this.props;
msg += '\n';
this.handleTextChange(msg);
}, 10);
}
};
handleTextChange = (value) => {
const {
actions,
channelId,
rootId
} = this.props;
this.checkMessageLength(value);
this.setState({value});
if (value) {
actions.userTyping(channelId, rootId);
}
};
handleUploadFiles = (images) => {
this.props.actions.handleUploadFiles(images, this.props.rootId);
};
renderDisabledSendButton = () => {
const {theme} = this.props;
const style = getStyleSheet(theme);
return (
<View style={style.sendButtonContainer}>
<View style={[style.sendButton, style.disableButton]}>
<PaperPlane
height={13}
width={15}
color={theme.buttonColor}
/>
</View>
</View>
);
};
renderSendButton = () => {
const {theme, uploadFileRequestStatus} = this.props;
const style = getStyleSheet(theme);
if (uploadFileRequestStatus === RequestStatus.STARTED) {
return this.renderDisabledSendButton();
} else if (this.canSend()) {
return (
<TouchableOpacity
onPress={this.handleSendMessage}
style={style.sendButtonContainer}
>
<View style={style.sendButton}>
<PaperPlane
height={13}
width={15}
color={theme.buttonColor}
/>
</View>
</TouchableOpacity>
);
}
return null;
};
sendMessage = () => {
const {actions, currentUserId, channelId, files, rootId, value} = this.props;
const {actions, currentUserId, channelId, files, rootId} = this.props;
const {value} = this.state;
const postFiles = files.filter((f) => !f.failed);
const post = {
@ -185,6 +317,7 @@ class PostTextbox extends PureComponent {
actions.createPost(post, postFiles);
this.handleTextChange('');
this.changeDraft('');
if (postFiles.length) {
actions.handleClearFiles(channelId, rootId);
}
@ -205,139 +338,25 @@ class PostTextbox extends PureComponent {
this.setState(nextState, callback);
};
handleUploadFiles = (images) => {
this.props.actions.handleUploadFiles(images, this.props.rootId);
};
changeDraft = (text) => {
const {
actions,
channelId,
rootId
} = this.props;
if (rootId) {
actions.handleCommentDraftChanged(rootId, text);
} else {
actions.handlePostDraftChanged(channelId, text);
}
}
sendReaction = (emoji) => {
const {actions, rootId} = this.props;
actions.addReactionToLatestPost(emoji, rootId);
this.handleTextChange('');
}
handleTextChange = (text) => {
const {
actions,
channelId,
rootId
} = this.props;
this.changeDraft(text);
actions.userTyping(channelId, rootId);
this.changeDraft('');
};
handleContentSizeChange = (event) => {
let contentHeight = event.nativeEvent.layout.height;
if (contentHeight < INITIAL_HEIGHT) {
contentHeight = INITIAL_HEIGHT;
}
this.setState({
contentHeight
});
};
handleInputSizeChange = (event) => {
this.setState({
inputWidth: event.nativeEvent.layout.width
});
};
handleSubmit = () => {
// Workaround for android as the multiline is not working
if (Platform.OS === 'android') {
if (this.timeout) {
clearTimeout(this.timeout);
}
this.timeout = setTimeout(() => {
let {value: msg} = this.props;
msg += '\n';
this.handleTextChange(msg);
}, 10);
}
};
attachAutocomplete = (c) => {
this.autocomplete = c;
};
renderDisabledSendButton = () => {
const {theme} = this.props;
const style = getStyleSheet(theme);
return (
<View style={[style.sendButton, style.disableButton]}>
<PaperPlane
height={13}
width={15}
color={theme.buttonColor}
/>
</View>
);
}
renderSendButton = () => {
const {theme, uploadFileRequestStatus} = this.props;
const style = getStyleSheet(theme);
if (uploadFileRequestStatus === RequestStatus.STARTED) {
return this.renderDisabledSendButton();
} else if (this.canSend()) {
return (
<TouchableOpacity
onPress={this.handleSendMessage}
style={style.sendButton}
>
<PaperPlane
height={13}
width={15}
color={theme.buttonColor}
/>
</TouchableOpacity>
);
}
return null;
}
handlePostDraftSelectionChanged = (event) => {
const cursorPosition = event.nativeEvent.selection.end;
if (this.props.rootId) {
this.props.actions.handleCommentDraftSelectionChanged(this.props.rootId, cursorPosition);
} else {
this.props.actions.handlePostDraftSelectionChanged(this.props.channelId, cursorPosition);
}
this.autocomplete.handleSelectionChange(event);
}
render() {
const {
canUploadFiles,
channelIsLoading,
intl,
theme,
value
theme
} = this.props;
const style = getStyleSheet(theme);
const textInputHeight = Math.min(this.state.contentHeight, MAX_CONTENT_HEIGHT);
const textValue = channelIsLoading ? '' : value;
const textValue = channelIsLoading ? '' : this.state.value;
let placeholder;
if (this.props.rootId) {
@ -378,8 +397,8 @@ class PostTextbox extends PureComponent {
/>
<Autocomplete
ref={this.attachAutocomplete}
onChangeText={this.changeDraft}
rootId={this.props.rootId}
onChangeText={this.handleTextChange}
value={this.state.value}
/>
<View style={style.inputWrapper}>
{attachmentButton}
@ -392,13 +411,14 @@ class PostTextbox extends PureComponent {
placeholder={intl.formatMessage(placeholder)}
placeholderTextColor={changeOpacity('#000', 0.5)}
multiline={true}
numberOfLines={10}
numberOfLines={5}
blurOnSubmit={false}
underlineColorAndroid='transparent'
style={[style.input, {height: textInputHeight}]}
onSubmitEditing={this.handleSubmit}
onLayout={this.handleInputSizeChange}
keyboardType={this.state.keyboardType}
onEndEditing={this.handleEndEditing}
/>
{this.renderSendButton()}
</View>
@ -435,8 +455,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
flex: 1,
flexDirection: 'row',
backgroundColor: '#fff',
alignItems: 'flex-end',
marginRight: 10
alignItems: 'flex-end'
},
inputContainerWithoutFileUpload: {
marginLeft: 10
@ -449,6 +468,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.20)
},
sendButtonContainer: {
paddingRight: 10
},
sendButton: {
backgroundColor: theme.buttonBg,
borderRadius: 18,

View file

@ -603,6 +603,7 @@ class Search extends PureComponent {
ref={this.attachAutocomplete}
onChangeText={this.handleTextChanged}
isSearch={true}
value={value}
/>
{previewComponent}
</View>