mattermost-mobile/app/components/post_textbox/post_textbox.js
enahum 7cda6be8d7
Edit profile (#1322)
* Add set status feature

Small code refactoring

Add Do Not Disturb option

* Add Account Settings screen

Add username, nicname, and position fields

Add email field

get deviceHeight from redux

Clean up onPress handler in Settings

Refactor each account settings item to it's own component

Clean up email field and updateUser action method

Use mobile localization format in en.json

Update profile image

Clean up ProfilePicture for editing

Add loading and error components in AccountSettings

Refactor updateRequest state into a function

Follow design specs

Remove unnecessary this.state.emailUpdated

Polish AccountSettings and secondary components

Refactor AttachmentButton into it's own componentg

Add Account Settings icon in General Settings

Add TODO comments

* Update user status

* Edit Profile screen refactored

* Fix edit profile item text color
2018-01-08 14:18:57 -03:00

550 lines
18 KiB
JavaScript

// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Alert, BackHandler, Keyboard, Platform, TextInput, TouchableOpacity, View} from 'react-native';
import {intlShape} from 'react-intl';
import {RequestStatus} from 'mattermost-redux/constants';
import AttachmentButton from 'app/components/attachment_button';
import Autocomplete from 'app/components/autocomplete';
import FileUploadPreview from 'app/components/file_upload_preview';
import PaperPlane from 'app/components/paper_plane';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import Typing from './components/typing';
const INITIAL_HEIGHT = Platform.OS === 'ios' ? 34 : 36;
const MAX_CONTENT_HEIGHT = 100;
const MAX_MESSAGE_LENGTH = 4000;
const MAX_FILE_COUNT = 5;
const IS_REACTION_REGEX = /(^\+:([^:\s]*):)$/i;
export default class PostTextbox extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
addReactionToLatestPost: PropTypes.func.isRequired,
createPost: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired,
handleCommentDraftChanged: PropTypes.func.isRequired,
handlePostDraftChanged: PropTypes.func.isRequired,
handleClearFiles: PropTypes.func.isRequired,
handleClearFailedFiles: PropTypes.func.isRequired,
handleRemoveLastFile: PropTypes.func.isRequired,
handleUploadFiles: PropTypes.func.isRequired,
userTyping: PropTypes.func.isRequired,
handlePostDraftSelectionChanged: PropTypes.func.isRequired,
handleCommentDraftSelectionChanged: PropTypes.func.isRequired
}).isRequired,
canUploadFiles: PropTypes.bool.isRequired,
channelId: PropTypes.string.isRequired,
channelIsLoading: PropTypes.bool.isRequired,
currentUserId: PropTypes.string.isRequired,
files: PropTypes.array,
navigator: PropTypes.object,
rootId: PropTypes.string,
theme: PropTypes.object.isRequired,
uploadFileRequestStatus: PropTypes.string.isRequired,
value: PropTypes.string.isRequired
};
static defaultProps = {
files: [],
rootId: '',
value: ''
};
static contextTypes = {
intl: intlShape
};
constructor(props) {
super(props);
this.state = {
contentHeight: INITIAL_HEIGHT,
keyboardType: 'default',
value: props.value,
showFileMaxWarning: false
};
}
componentDidMount() {
if (Platform.OS === 'android') {
Keyboard.addListener('keyboardDidHide', this.handleAndroidKeyboard);
BackHandler.addEventListener('hardwareBackPress', this.handleAndroidBack);
}
}
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) {
const filesLoading = [];
files.forEach((file) => {
if (file.loading) {
filesLoading.push(file);
}
});
const loadingComplete = filesLoading.length === 0;
return valueLength <= MAX_MESSAGE_LENGTH && uploadFileRequestStatus !== RequestStatus.STARTED && loadingComplete;
}
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.context;
const valueLength = value.trim().length;
if (valueLength > MAX_MESSAGE_LENGTH) {
Alert.alert(
intl.formatMessage({
id: 'mobile.message_length.title',
defaultMessage: 'Message Length'
}),
intl.formatMessage({
id: 'mobile.message_length.message',
defaultMessage: 'Your current message is too long. Current character count: {max}/{count}'
}, {
max: MAX_MESSAGE_LENGTH,
count: valueLength
})
);
}
};
handleAndroidKeyboard = () => {
this.blur();
};
handleAndroidBack = () => {
const {channelId, files, rootId} = this.props;
if (files.length) {
this.props.actions.handleRemoveLastFile(channelId, rootId);
return true;
}
return false;
};
handleBlur = () => {
if (this.refs.input && Platform.OS === 'android') {
this.refs.input.setNativeProps({
autoScroll: false
});
}
};
handleContentSizeChange = (event) => {
let contentHeight = event.nativeEvent.contentSize.height;
if (contentHeight < INITIAL_HEIGHT) {
contentHeight = INITIAL_HEIGHT;
} else if (Platform.OS === 'ios') {
contentHeight += 5;
}
this.setState({
contentHeight
});
};
handleEndEditing = (e) => {
if (e && e.nativeEvent) {
this.changeDraft(e.nativeEvent.text || '');
}
};
handleFocus = () => {
if (this.refs.input && Platform.OS === 'android') {
this.refs.input.setNativeProps({
autoScroll: true
});
}
};
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.getWrappedInstance().handleSelectionChange(event);
};
handleSendMessage = () => {
if (!this.canSend()) {
return;
}
const {actions, channelId, files, rootId} = this.props;
const {value} = this.state;
const isReactionMatch = value.match(IS_REACTION_REGEX);
if (isReactionMatch) {
const emoji = isReactionMatch[2];
this.sendReaction(emoji);
return;
}
const hasFailedAttachments = files.some((f) => f.failed);
if (hasFailedAttachments) {
const {intl} = this.context;
Alert.alert(
intl.formatMessage({
id: 'mobile.post_textbox.uploadFailedTitle',
defaultMessage: 'Attachment failure'
}),
intl.formatMessage({
id: 'mobile.post_textbox.uploadFailedDesc',
defaultMessage: 'Some attachments failed to upload to the server, Are you sure you want to post the message?'
}),
[{
text: intl.formatMessage({id: 'mobile.channel_info.alertNo', defaultMessage: 'No'})
}, {
text: intl.formatMessage({id: 'mobile.channel_info.alertYes', defaultMessage: 'Yes'}),
onPress: () => {
// Remove only failed files
actions.handleClearFailedFiles(channelId, rootId);
this.sendMessage();
}
}],
);
} else {
this.sendMessage();
}
};
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);
};
renderSendButton = () => {
const {files, theme} = this.props;
const style = getStyleSheet(theme);
const icon = (
<PaperPlane
height={13}
width={15}
color={theme.buttonColor}
/>
);
let button = null;
const imagesLoading = files.filter((f) => f.loading).length > 0;
if (imagesLoading) {
button = (
<View style={style.sendButtonContainer}>
<View style={[style.sendButton, style.disableButton]}>
{icon}
</View>
</View>
);
} else if (this.canSend()) {
button = (
<TouchableOpacity
onPress={this.handleSendMessage}
style={style.sendButtonContainer}
>
<View style={style.sendButton}>
{icon}
</View>
</TouchableOpacity>
);
}
return button;
};
sendMessage = () => {
const {actions, currentUserId, channelId, files, rootId} = this.props;
const {intl} = this.context;
const {value} = this.state;
if (files.length === 0 && !value) {
Alert.alert(
intl.formatMessage({
id: 'mobile.post_textbox.empty.title',
defaultMessage: 'Empty Message'
}),
intl.formatMessage({
id: 'mobile.post_textbox.empty.message',
defaultMessage: 'You are trying to send an empty message.\nPlease make sure you have a message or at least one attached file.'
}),
[{
text: intl.formatMessage({id: 'mobile.post_textbox.empty.ok', defaultMessage: 'OK'})
}],
);
} else {
if (value.indexOf('/') === 0) {
this.sendCommand(value);
} else {
const postFiles = files.filter((f) => !f.failed);
const post = {
user_id: currentUserId,
channel_id: channelId,
root_id: rootId,
parent_id: rootId,
message: value
};
actions.createPost(post, postFiles);
if (postFiles.length) {
actions.handleClearFiles(channelId, rootId);
}
}
this.handleTextChange('');
this.changeDraft('');
// Shrink the input textbox since the layout events lag slightly
const nextState = {
contentHeight: INITIAL_HEIGHT
};
// Fixes the issue where Android predictive text would prepend suggestions to the post draft when messages
// are typed successively without blurring the input
let callback;
if (Platform.OS === 'android') {
nextState.keyboardType = 'email-address';
callback = () => this.setState({keyboardType: 'default'});
}
this.setState(nextState, callback);
}
};
sendCommand = async (msg) => {
const {intl} = this.context;
const {actions, channelId, rootId} = this.props;
const {error} = await actions.executeCommand(msg, channelId, rootId);
if (error) {
Alert.alert(
intl.formatMessage({
id: 'mobile.commands.error_title',
defaultMessage: 'Error Executing Command'
}),
error.message
);
}
};
sendReaction = (emoji) => {
const {actions, rootId} = this.props;
actions.addReactionToLatestPost(emoji, rootId);
this.handleTextChange('');
this.changeDraft('');
};
onShowFileMaxWarning = () => {
this.setState({showFileMaxWarning: true}, () => {
setTimeout(() => {
this.setState({showFileMaxWarning: false});
}, 3000);
});
};
render() {
const {intl} = this.context;
const {
canUploadFiles,
channelId,
channelIsLoading,
files,
navigator,
rootId,
theme
} = this.props;
const {showFileMaxWarning} = this.state;
const style = getStyleSheet(theme);
const textInputHeight = Math.min(this.state.contentHeight, MAX_CONTENT_HEIGHT);
const textValue = channelIsLoading ? '' : this.state.value;
let placeholder;
if (rootId) {
placeholder = {id: 'create_comment.addComment', defaultMessage: 'Add a comment...'};
} else {
placeholder = {id: 'create_post.write', defaultMessage: 'Write a message...'};
}
let attachmentButton = null;
const inputContainerStyle = [style.inputContainer];
if (canUploadFiles) {
attachmentButton = (
<AttachmentButton
blurTextBox={this.blur}
theme={theme}
navigator={navigator}
fileCount={files.length}
maxFileCount={MAX_FILE_COUNT}
onShowFileMaxWarning={this.onShowFileMaxWarning}
uploadFiles={this.handleUploadFiles}
/>
);
} else {
inputContainerStyle.push(style.inputContainerWithoutFileUpload);
}
return (
<View>
<Typing/>
<FileUploadPreview
channelId={channelId}
files={files}
inputHeight={textInputHeight}
rootId={rootId}
showFileMaxWarning={showFileMaxWarning}
/>
<Autocomplete
ref={this.attachAutocomplete}
onChangeText={this.handleTextChange}
value={this.state.value}
rootId={rootId}
/>
<View style={style.inputWrapper}>
{attachmentButton}
<View style={inputContainerStyle}>
<TextInput
ref='input'
value={textValue}
onChangeText={this.handleTextChange}
onSelectionChange={this.handlePostDraftSelectionChanged}
placeholder={intl.formatMessage(placeholder)}
placeholderTextColor={changeOpacity('#000', 0.5)}
multiline={true}
numberOfLines={5}
blurOnSubmit={false}
underlineColorAndroid='transparent'
style={[style.input, {height: textInputHeight}]}
onContentSizeChange={this.handleContentSizeChange}
keyboardType={this.state.keyboardType}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
onEndEditing={this.handleEndEditing}
disableFullscreenUI={true}
/>
{this.renderSendButton()}
</View>
</View>
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
disableButton: {
backgroundColor: changeOpacity(theme.buttonBg, 0.3)
},
input: {
color: '#000',
flex: 1,
fontSize: 14,
paddingBottom: 8,
paddingLeft: 12,
paddingRight: 12,
paddingTop: 8,
textAlignVertical: 'top'
},
hidden: {
position: 'absolute',
top: 10000, // way off screen
left: 10000, // way off screen
backgroundColor: 'transparent',
borderColor: 'transparent',
color: 'transparent'
},
inputContainer: {
flex: 1,
flexDirection: 'row',
backgroundColor: '#fff',
alignItems: 'stretch',
marginRight: 10
},
inputContainerWithoutFileUpload: {
marginLeft: 10
},
inputWrapper: {
alignItems: 'flex-end',
flexDirection: 'row',
paddingVertical: 4,
backgroundColor: theme.centerChannelBg,
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.20)
},
sendButtonContainer: {
justifyContent: 'flex-end',
paddingHorizontal: 5,
paddingVertical: 3
},
sendButton: {
backgroundColor: theme.buttonBg,
borderRadius: 18,
height: 28,
width: 28,
alignItems: 'center',
justifyContent: 'center'
}
};
});