RN-342 Refactor post textbox component (#915)
* RN-342 Refactor post textbox component * Rebase * Review feedback
This commit is contained in:
parent
4995a76f2c
commit
e4b86518a9
13 changed files with 400 additions and 316 deletions
|
|
@ -38,6 +38,7 @@ export default class Emoji extends React.PureComponent {
|
|||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.mounted = true;
|
||||
if (this.state.imageUrl && this.state.isCustomEmoji) {
|
||||
this.updateImageHeight(this.state.imageUrl);
|
||||
}
|
||||
|
|
@ -59,6 +60,10 @@ export default class Emoji extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
getImageUrl = (props = this.props) => {
|
||||
const emojiName = props.emojiName;
|
||||
|
||||
|
|
@ -83,10 +88,12 @@ export default class Emoji extends React.PureComponent {
|
|||
|
||||
updateImageHeight = (imageUrl) => {
|
||||
Image.getSize(imageUrl, (originalWidth, originalHeight) => {
|
||||
this.setState({
|
||||
originalWidth,
|
||||
originalHeight
|
||||
});
|
||||
if (this.mounted) {
|
||||
this.setState({
|
||||
originalWidth,
|
||||
originalHeight
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
StyleSheet,
|
||||
View
|
||||
} from 'react-native';
|
||||
import FlatList from 'app/components/inverted_flat_list';
|
||||
|
|
@ -183,7 +184,14 @@ export default class PostList extends PureComponent {
|
|||
theme={theme}
|
||||
getItem={this.getItem}
|
||||
getItemCount={this.getItemCount}
|
||||
contentContainerStyle={styles.postListContent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
postListContent: {
|
||||
paddingTop: 5
|
||||
}
|
||||
});
|
||||
|
|
|
|||
177
app/components/post_textbox/components/attachment_button.js
Normal file
177
app/components/post_textbox/components/attachment_button.js
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Platform,
|
||||
StyleSheet,
|
||||
TouchableOpacity
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
import ImagePicker from 'react-native-image-picker';
|
||||
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
|
||||
export default class AttachmentButton extends PureComponent {
|
||||
static propTypes = {
|
||||
blurTextBox: PropTypes.func.isRequired,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
uploadFiles: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
attachFileFromCamera = () => {
|
||||
const options = {
|
||||
quality: 0.7,
|
||||
noData: true,
|
||||
storageOptions: {
|
||||
cameraRoll: true,
|
||||
waitUntilSaved: true
|
||||
}
|
||||
};
|
||||
|
||||
ImagePicker.launchCamera(options, (response) => {
|
||||
if (response.error || response.didCancel) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.uploadFiles([response]);
|
||||
});
|
||||
};
|
||||
|
||||
attachFileFromLibrary = () => {
|
||||
const options = {
|
||||
quality: 0.7,
|
||||
noData: true
|
||||
};
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
options.mediaType = 'mixed';
|
||||
}
|
||||
|
||||
ImagePicker.launchImageLibrary(options, (response) => {
|
||||
if (response.error || response.didCancel) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.uploadFiles([response]);
|
||||
});
|
||||
};
|
||||
|
||||
attachVideoFromLibraryAndroid = () => {
|
||||
const options = {
|
||||
quality: 0.7,
|
||||
mediaType: 'video',
|
||||
noData: true
|
||||
};
|
||||
|
||||
ImagePicker.launchImageLibrary(options, (response) => {
|
||||
if (response.error || response.didCancel) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.uploadFiles([response]);
|
||||
});
|
||||
}
|
||||
|
||||
uploadFiles = (images) => {
|
||||
this.props.uploadFiles(images);
|
||||
};
|
||||
|
||||
handleFileAttachmentOption = (action) => {
|
||||
this.props.navigator.dismissModal({
|
||||
animationType: 'none'
|
||||
});
|
||||
|
||||
// Have to wait to launch the library attachment action.
|
||||
// If we call the action after dismissModal with no delay then the
|
||||
// Wix navigator will dismiss the library attachment modal as well.
|
||||
setTimeout(() => {
|
||||
if (typeof action === 'function') {
|
||||
action();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
showFileAttachmentOptions = () => {
|
||||
this.props.blurTextBox();
|
||||
const options = {
|
||||
items: [{
|
||||
action: () => this.handleFileAttachmentOption(this.attachFileFromCamera),
|
||||
text: {
|
||||
id: 'mobile.file_upload.camera',
|
||||
defaultMessage: 'Take Photo or Video'
|
||||
},
|
||||
icon: 'camera'
|
||||
}, {
|
||||
action: () => this.handleFileAttachmentOption(this.attachFileFromLibrary),
|
||||
text: {
|
||||
id: 'mobile.file_upload.library',
|
||||
defaultMessage: 'Photo Library'
|
||||
},
|
||||
icon: 'photo'
|
||||
}]
|
||||
};
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
options.items.push({
|
||||
action: () => this.handleFileAttachmentOption(this.attachVideoFromLibraryAndroid),
|
||||
text: {
|
||||
id: 'mobile.file_upload.video',
|
||||
defaultMessage: 'Video Library'
|
||||
},
|
||||
icon: 'file-video-o'
|
||||
});
|
||||
}
|
||||
|
||||
this.props.navigator.showModal({
|
||||
screen: 'OptionsModal',
|
||||
title: '',
|
||||
animationType: 'none',
|
||||
passProps: {
|
||||
items: options.items
|
||||
},
|
||||
navigatorStyle: {
|
||||
navBarHidden: true,
|
||||
statusBarHidden: false,
|
||||
statusBarHideWithNavBar: false,
|
||||
screenBackgroundColor: 'transparent',
|
||||
modalPresentationStyle: 'overCurrentContext'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {theme} = this.props;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={this.showFileAttachmentOptions}
|
||||
style={style.buttonContainer}
|
||||
>
|
||||
<Icon
|
||||
size={30}
|
||||
style={style.attachIcon}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.9)}
|
||||
name='md-add'
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
attachIcon: {
|
||||
marginTop: Platform.select({
|
||||
ios: 2,
|
||||
android: 0
|
||||
})
|
||||
},
|
||||
buttonContainer: {
|
||||
height: Platform.select({
|
||||
ios: 34,
|
||||
android: 36
|
||||
}),
|
||||
width: 45,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}
|
||||
});
|
||||
19
app/components/post_textbox/components/typing/index.js
Normal file
19
app/components/post_textbox/components/typing/index.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getUsersTyping} from 'mattermost-redux/selectors/entities/typing';
|
||||
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
|
||||
import Typing from './typing';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
typing: getUsersTyping(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(Typing);
|
||||
102
app/components/post_textbox/components/typing/typing.js
Normal file
102
app/components/post_textbox/components/typing/typing.js
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Animated,
|
||||
Text
|
||||
} from 'react-native';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
const {View: AnimatedView} = Animated;
|
||||
|
||||
export default class Typing extends PureComponent {
|
||||
static propTypes = {
|
||||
theme: PropTypes.object.isRequired,
|
||||
typing: PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
state = {
|
||||
typingHeight: new Animated.Value(0)
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.typing.length && !this.props.typing.length) {
|
||||
this.animateTyping(true);
|
||||
} else if (!nextProps.typing.length) {
|
||||
this.animateTyping();
|
||||
}
|
||||
}
|
||||
|
||||
animateTyping = (show = false) => {
|
||||
const height = show ? 20 : 0;
|
||||
|
||||
Animated.timing(this.state.typingHeight, {
|
||||
toValue: height,
|
||||
duration: 200
|
||||
}).start();
|
||||
}
|
||||
|
||||
renderTyping = () => {
|
||||
const {typing} = this.props;
|
||||
const nextTyping = [...typing];
|
||||
const numUsers = nextTyping.length;
|
||||
|
||||
switch (numUsers) {
|
||||
case 0:
|
||||
return null;
|
||||
case 1:
|
||||
return (
|
||||
<FormattedText
|
||||
id='msg_typing.isTyping'
|
||||
defaultMessage='{user} is typing...'
|
||||
values={{
|
||||
user: nextTyping[0]
|
||||
}}
|
||||
/>
|
||||
);
|
||||
default: {
|
||||
const last = nextTyping.pop();
|
||||
return (
|
||||
<FormattedText
|
||||
id='msg_typing.areTyping'
|
||||
defaultMessage='{users} and {last} are typing...'
|
||||
values={{
|
||||
users: (nextTyping.join(', ')),
|
||||
last
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const style = getStyleSheet(this.props.theme);
|
||||
|
||||
return (
|
||||
<AnimatedView style={{height: this.state.typingHeight}}>
|
||||
<Text
|
||||
style={style.typing}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{this.renderTyping()}
|
||||
</Text>
|
||||
</AnimatedView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
typing: {
|
||||
paddingLeft: 10,
|
||||
paddingTop: 3,
|
||||
fontSize: 11,
|
||||
marginBottom: 5,
|
||||
color: theme.centerChannelColor,
|
||||
backgroundColor: 'transparent'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
@ -3,27 +3,32 @@
|
|||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {createPost} from 'mattermost-redux/actions/posts';
|
||||
import {userTyping} from 'mattermost-redux/actions/websocket';
|
||||
|
||||
import {addReactionToLatestPost} from 'app/actions/views/emoji';
|
||||
import {handleClearFiles, handleRemoveLastFile, handleUploadFiles} from 'app/actions/views/file_upload';
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
import {canUploadFilesOnMobile} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getUsersTyping} from 'mattermost-redux/selectors/entities/typing';
|
||||
|
||||
import {addReactionToLatestPost} from 'app/actions/views/emoji';
|
||||
import {handlePostDraftChanged} from 'app/actions/views/channel';
|
||||
import {handleClearFiles, handleRemoveLastFile, handleUploadFiles} from 'app/actions/views/file_upload';
|
||||
import {handleCommentDraftChanged} from 'app/actions/views/thread';
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
import {getCurrentChannelDraft, getThreadDraft} from 'app/selectors/views';
|
||||
|
||||
import PostTextbox from './post_textbox';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const currentDraft = ownProps.rootId ? getThreadDraft(state, ownProps.rootId) : getCurrentChannelDraft(state);
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
canUploadFiles: canUploadFilesOnMobile(state),
|
||||
channelIsLoading: state.views.channel.loading,
|
||||
currentUserId: getCurrentUserId(state),
|
||||
typing: getUsersTyping(state),
|
||||
files: currentDraft.files,
|
||||
theme: getTheme(state),
|
||||
uploadFileRequestStatus: state.requests.files.uploadFiles.status
|
||||
uploadFileRequestStatus: state.requests.files.uploadFiles.status,
|
||||
value: currentDraft.draft
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -33,6 +38,8 @@ function mapDispatchToProps(dispatch) {
|
|||
addReactionToLatestPost,
|
||||
createPost,
|
||||
handleClearFiles,
|
||||
handleCommentDraftChanged,
|
||||
handlePostDraftChanged,
|
||||
handleRemoveLastFile,
|
||||
handleUploadFiles,
|
||||
userTyping
|
||||
|
|
|
|||
|
|
@ -13,17 +13,17 @@ import {
|
|||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
import ImagePicker from 'react-native-image-picker';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
|
||||
import Autocomplete from 'app/components/autocomplete';
|
||||
import FileUploadPreview from 'app/components/file_upload_preview';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import PaperPlane from 'app/components/paper_plane';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import AttachmentButton from './components/attachment_button';
|
||||
import Typing from './components/typing';
|
||||
|
||||
const INITIAL_HEIGHT = Platform.OS === 'ios' ? 34 : 36;
|
||||
const MAX_CONTENT_HEIGHT = 100;
|
||||
const MAX_MESSAGE_LENGTH = 4000;
|
||||
|
|
@ -34,6 +34,8 @@ class PostTextbox extends PureComponent {
|
|||
actions: PropTypes.shape({
|
||||
addReactionToLatestPost: PropTypes.func.isRequired,
|
||||
createPost: PropTypes.func.isRequired,
|
||||
handleCommentDraftChanged: PropTypes.func.isRequired,
|
||||
handlePostDraftChanged: PropTypes.func.isRequired,
|
||||
handleClearFiles: PropTypes.func.isRequired,
|
||||
handleRemoveLastFile: PropTypes.func.isRequired,
|
||||
handleUploadFiles: PropTypes.func.isRequired,
|
||||
|
|
@ -46,10 +48,8 @@ class PostTextbox extends PureComponent {
|
|||
files: PropTypes.array,
|
||||
intl: intlShape.isRequired,
|
||||
navigator: PropTypes.object,
|
||||
onChangeText: PropTypes.func.isRequired,
|
||||
rootId: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
typing: PropTypes.array.isRequired,
|
||||
uploadFileRequestStatus: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired
|
||||
};
|
||||
|
|
@ -61,14 +61,10 @@ class PostTextbox extends PureComponent {
|
|||
value: ''
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
contentHeight: INITIAL_HEIGHT,
|
||||
inputWidth: null
|
||||
};
|
||||
}
|
||||
state = {
|
||||
contentHeight: INITIAL_HEIGHT,
|
||||
inputWidth: null
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if (Platform.OS === 'android') {
|
||||
|
|
@ -197,6 +193,24 @@ class PostTextbox extends PureComponent {
|
|||
});
|
||||
};
|
||||
|
||||
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);
|
||||
|
|
@ -205,13 +219,12 @@ class PostTextbox extends PureComponent {
|
|||
|
||||
handleTextChange = (text) => {
|
||||
const {
|
||||
onChangeText,
|
||||
actions,
|
||||
channelId,
|
||||
rootId,
|
||||
actions
|
||||
rootId
|
||||
} = this.props;
|
||||
|
||||
onChangeText(text);
|
||||
this.changeDraft(text);
|
||||
actions.userTyping(channelId, rootId);
|
||||
};
|
||||
|
||||
|
|
@ -256,160 +269,6 @@ class PostTextbox extends PureComponent {
|
|||
this.autocomplete = c;
|
||||
};
|
||||
|
||||
attachFileFromCamera = () => {
|
||||
const options = {
|
||||
quality: 0.7,
|
||||
noData: true,
|
||||
storageOptions: {
|
||||
cameraRoll: true,
|
||||
waitUntilSaved: true
|
||||
}
|
||||
};
|
||||
|
||||
ImagePicker.launchCamera(options, (response) => {
|
||||
if (response.error || response.didCancel) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.uploadFiles([response]);
|
||||
});
|
||||
};
|
||||
|
||||
attachFileFromLibrary = () => {
|
||||
const options = {
|
||||
quality: 0.7,
|
||||
noData: true
|
||||
};
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
options.mediaType = 'mixed';
|
||||
}
|
||||
|
||||
ImagePicker.launchImageLibrary(options, (response) => {
|
||||
if (response.error || response.didCancel) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.uploadFiles([response]);
|
||||
});
|
||||
};
|
||||
|
||||
attachVideoFromLibraryAndroid = () => {
|
||||
const options = {
|
||||
quality: 0.7,
|
||||
mediaType: 'video',
|
||||
noData: true
|
||||
};
|
||||
|
||||
ImagePicker.launchImageLibrary(options, (response) => {
|
||||
if (response.error || response.didCancel) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.uploadFiles([response]);
|
||||
});
|
||||
}
|
||||
|
||||
uploadFiles = (images) => {
|
||||
this.props.actions.handleUploadFiles(images, this.props.rootId);
|
||||
};
|
||||
|
||||
handleFileAttachmentOption = (action) => {
|
||||
this.props.navigator.dismissModal({
|
||||
animationType: 'none'
|
||||
});
|
||||
|
||||
// Have to wait to launch the library attachment action.
|
||||
// If we call the action after dismissModal with no delay then the
|
||||
// Wix navigator will dismiss the library attachment modal as well.
|
||||
setTimeout(() => {
|
||||
if (typeof action === 'function') {
|
||||
action();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
showFileAttachmentOptions = () => {
|
||||
this.blur();
|
||||
const options = {
|
||||
items: [{
|
||||
action: () => this.handleFileAttachmentOption(this.attachFileFromCamera),
|
||||
text: {
|
||||
id: 'mobile.file_upload.camera',
|
||||
defaultMessage: 'Take Photo or Video'
|
||||
},
|
||||
icon: 'camera'
|
||||
}, {
|
||||
action: () => this.handleFileAttachmentOption(this.attachFileFromLibrary),
|
||||
text: {
|
||||
id: 'mobile.file_upload.library',
|
||||
defaultMessage: 'Photo Library'
|
||||
},
|
||||
icon: 'photo'
|
||||
}]
|
||||
};
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
options.items.push({
|
||||
action: () => this.handleFileAttachmentOption(this.attachVideoFromLibraryAndroid),
|
||||
text: {
|
||||
id: 'mobile.file_upload.video',
|
||||
defaultMessage: 'Video Library'
|
||||
},
|
||||
icon: 'file-video-o'
|
||||
});
|
||||
}
|
||||
|
||||
this.props.navigator.showModal({
|
||||
screen: 'OptionsModal',
|
||||
title: '',
|
||||
animationType: 'none',
|
||||
passProps: {
|
||||
items: options.items
|
||||
},
|
||||
navigatorStyle: {
|
||||
navBarHidden: true,
|
||||
statusBarHidden: false,
|
||||
statusBarHideWithNavBar: false,
|
||||
screenBackgroundColor: 'transparent',
|
||||
modalPresentationStyle: 'overCurrentContext'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
renderTyping = () => {
|
||||
const {typing} = this.props;
|
||||
const numUsers = typing.length;
|
||||
|
||||
switch (numUsers) {
|
||||
case 0:
|
||||
return null;
|
||||
case 1:
|
||||
return (
|
||||
<FormattedText
|
||||
id='msg_typing.isTyping'
|
||||
defaultMessage='{user} is typing...'
|
||||
values={{
|
||||
user: typing[0]
|
||||
}}
|
||||
/>
|
||||
);
|
||||
default: {
|
||||
const last = typing.pop();
|
||||
return (
|
||||
<FormattedText
|
||||
id='msg_typing.areTyping'
|
||||
defaultMessage='{users} and {last} are typing...'
|
||||
values={{
|
||||
users: (typing.join(', ')),
|
||||
last
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
renderDisabledSendButton = () => {
|
||||
const {theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
|
@ -470,21 +329,16 @@ class PostTextbox extends PureComponent {
|
|||
placeholder = {id: 'create_post.write', defaultMessage: 'Write a message...'};
|
||||
}
|
||||
|
||||
let fileUpload = null;
|
||||
let attachmentButton = null;
|
||||
const inputContainerStyle = [style.inputContainer];
|
||||
if (canUploadFiles) {
|
||||
fileUpload = (
|
||||
<TouchableOpacity
|
||||
onPress={this.showFileAttachmentOptions}
|
||||
style={style.buttonContainer}
|
||||
>
|
||||
<Icon
|
||||
size={30}
|
||||
style={style.attachIcon}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.9)}
|
||||
name='md-add'
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
attachmentButton = (
|
||||
<AttachmentButton
|
||||
blurTextBox={this.blur}
|
||||
theme={theme}
|
||||
navigator={this.props.navigator}
|
||||
uploadFiles={this.handleUploadFiles}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
inputContainerStyle.push(style.inputContainerWithoutFileUpload);
|
||||
|
|
@ -498,15 +352,7 @@ class PostTextbox extends PureComponent {
|
|||
>
|
||||
{textValue + ' '}
|
||||
</Text>
|
||||
<View>
|
||||
<Text
|
||||
style={[style.typing]}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{this.renderTyping()}
|
||||
</Text>
|
||||
</View>
|
||||
<Typing/>
|
||||
<FileUploadPreview
|
||||
channelId={this.props.channelId}
|
||||
files={this.props.files}
|
||||
|
|
@ -515,11 +361,11 @@ class PostTextbox extends PureComponent {
|
|||
/>
|
||||
<Autocomplete
|
||||
ref={this.attachAutocomplete}
|
||||
onChangeText={this.props.onChangeText}
|
||||
onChangeText={this.changeDraft}
|
||||
rootId={this.props.rootId}
|
||||
/>
|
||||
<View style={style.inputWrapper}>
|
||||
{fileUpload}
|
||||
{attachmentButton}
|
||||
<View style={inputContainerStyle}>
|
||||
<TextInput
|
||||
ref='input'
|
||||
|
|
@ -546,15 +392,6 @@ class PostTextbox extends PureComponent {
|
|||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
buttonContainer: {
|
||||
height: Platform.select({
|
||||
ios: 34,
|
||||
android: 36
|
||||
}),
|
||||
width: 45,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
disableButton: {
|
||||
backgroundColor: changeOpacity(theme.buttonBg, 0.3)
|
||||
},
|
||||
|
|
@ -594,12 +431,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
|||
borderTopWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.20)
|
||||
},
|
||||
attachIcon: {
|
||||
marginTop: Platform.select({
|
||||
ios: 2,
|
||||
android: 0
|
||||
})
|
||||
},
|
||||
sendButton: {
|
||||
backgroundColor: theme.buttonBg,
|
||||
borderRadius: 18,
|
||||
|
|
@ -619,13 +450,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
|||
width: 29
|
||||
}
|
||||
})
|
||||
},
|
||||
typing: {
|
||||
paddingLeft: 10,
|
||||
fontSize: 11,
|
||||
marginBottom: 5,
|
||||
color: theme.centerChannelColor,
|
||||
backgroundColor: 'transparent'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@ import PostListRetry from 'app/components/post_list_retry';
|
|||
import StatusBar from 'app/components/status_bar';
|
||||
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import PostTextbox from 'app/components/post_textbox';
|
||||
|
||||
import ChannelDrawerButton from './channel_drawer_button';
|
||||
import ChannelPostList from './channel_post_list';
|
||||
import ChannelPostTextbox from './channel_post_textbox';
|
||||
import ChannelSearchButton from './channel_search_button';
|
||||
import ChannelTitle from './channel_title';
|
||||
|
||||
|
|
@ -92,7 +92,7 @@ class Channel extends PureComponent {
|
|||
};
|
||||
|
||||
blurPostTextBox = () => {
|
||||
this.postTextbox.getWrappedInstance().blur();
|
||||
this.postTextbox.getWrappedInstance().getWrappedInstance().blur();
|
||||
};
|
||||
|
||||
goToChannelInfo = wrapWithPreventDoubleTap(() => {
|
||||
|
|
@ -219,8 +219,9 @@ class Channel extends PureComponent {
|
|||
navigator={navigator}
|
||||
/>
|
||||
</View>
|
||||
<ChannelPostTextbox
|
||||
<PostTextbox
|
||||
ref={this.attachPostTextbox}
|
||||
onChangeText={this.handleDraftChanged}
|
||||
channelId={currentChannelId}
|
||||
navigator={navigator}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import PostTextbox from 'app/components/post_textbox';
|
||||
|
||||
export default class ChannelPostTextbox extends React.PureComponent {
|
||||
static propTypes = {
|
||||
channelId: PropTypes.string.isRequired,
|
||||
draft: PropTypes.object.isRequired,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
actions: PropTypes.shape({
|
||||
handlePostDraftChanged: PropTypes.func.isRequired
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
handleDraftChanged = (value) => {
|
||||
this.props.actions.handlePostDraftChanged(this.props.channelId, value);
|
||||
};
|
||||
|
||||
blur = () => {
|
||||
this.refs.postTextbox.getWrappedInstance().getWrappedInstance().blur();
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<PostTextbox
|
||||
ref='postTextbox'
|
||||
files={this.props.draft.files}
|
||||
value={this.props.draft.draft}
|
||||
channelId={this.props.channelId}
|
||||
onChangeText={this.handleDraftChanged}
|
||||
navigator={this.props.navigator}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {
|
||||
handlePostDraftChanged
|
||||
} from 'app/actions/views/channel';
|
||||
|
||||
import ChannelPostTextbox from './channel_post_textbox';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
draft: state.views.channel.drafts[ownProps.channelId] || {},
|
||||
...ownProps
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
handlePostDraftChanged
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps, null, {withRef: true})(ChannelPostTextbox);
|
||||
|
|
@ -4,7 +4,6 @@
|
|||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {handleCommentDraftChanged} from 'app/actions/views/thread';
|
||||
import {getStatusBarHeight} from 'app/selectors/device';
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
|
||||
|
|
@ -21,15 +20,12 @@ function makeMapStateToProps() {
|
|||
|
||||
return function mapStateToProps(state, ownProps) {
|
||||
const posts = getPostsForThread(state, ownProps);
|
||||
const threadDraft = state.views.thread.drafts[ownProps.rootId];
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
channelId: ownProps.channelId,
|
||||
myMember: getMyCurrentChannelMembership(state),
|
||||
rootId: ownProps.rootId,
|
||||
draft: threadDraft.draft,
|
||||
files: threadDraft.files,
|
||||
posts,
|
||||
statusBarHeight: getStatusBarHeight(state),
|
||||
theme: getTheme(state)
|
||||
|
|
@ -40,7 +36,6 @@ function makeMapStateToProps() {
|
|||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
handleCommentDraftChanged,
|
||||
selectPost
|
||||
}, dispatch)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,15 +13,12 @@ import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
|||
export default class Thread extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
handleCommentDraftChanged: PropTypes.func.isRequired,
|
||||
selectPost: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
channelId: PropTypes.string.isRequired,
|
||||
navigator: PropTypes.object,
|
||||
myMember: PropTypes.object.isRequired,
|
||||
files: PropTypes.array,
|
||||
rootId: PropTypes.string.isRequired,
|
||||
draft: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
posts: PropTypes.array.isRequired,
|
||||
statusBarHeight: PropTypes.number
|
||||
|
|
@ -39,15 +36,9 @@ export default class Thread extends PureComponent {
|
|||
this.props.actions.selectPost('');
|
||||
}
|
||||
|
||||
handleDraftChanged = (value) => {
|
||||
this.props.actions.handleCommentDraftChanged(this.props.rootId, value);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
channelId,
|
||||
draft,
|
||||
files,
|
||||
myMember,
|
||||
navigator,
|
||||
posts,
|
||||
|
|
@ -78,10 +69,7 @@ export default class Thread extends PureComponent {
|
|||
/>
|
||||
<PostTextbox
|
||||
rootId={rootId}
|
||||
value={draft}
|
||||
files={files}
|
||||
channelId={channelId}
|
||||
onChangeText={this.handleDraftChanged}
|
||||
navigator={navigator}
|
||||
/>
|
||||
</KeyboardLayout>
|
||||
|
|
|
|||
23
app/selectors/views.js
Normal file
23
app/selectors/views.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import {createSelector} from 'reselect';
|
||||
|
||||
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
|
||||
function getChannelDrafts(state) {
|
||||
return state.views.channel.drafts;
|
||||
}
|
||||
|
||||
function getThreadDrafts(state) {
|
||||
return state.views.thread.drafts;
|
||||
}
|
||||
|
||||
export const getCurrentChannelDraft = createSelector(
|
||||
getChannelDrafts,
|
||||
getCurrentChannel,
|
||||
(drafts, currentChannel) => drafts[currentChannel.id]
|
||||
);
|
||||
|
||||
export const getThreadDraft = createSelector(
|
||||
getThreadDrafts,
|
||||
(state, rootId) => rootId,
|
||||
(drafts, rootId) => drafts[rootId]
|
||||
);
|
||||
Loading…
Reference in a new issue