From e20b84c6d89790e29377175c06cb5d2c2e091126 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Wed, 4 Apr 2018 16:14:26 +0300 Subject: [PATCH] Upload progress indicator (#1562) * Long post support * Split file upload component * Use fetch blob fork * add progress to file upload * Remove SERVER.PID from Makefile * Preview only images * remove unused prop --- Makefile | 1 - app/actions/views/file_upload.js | 63 ++-- app/components/attachment_button.js | 18 +- .../file_attachment_image.js | 8 +- .../file_upload_item/file_upload_item.js | 290 ++++++++++++++++++ .../file_upload_item/index.js | 33 ++ .../file_upload_preview.js | 139 +-------- .../file_upload_preview/file_upload_remove.js | 64 ++++ .../file_upload_preview/file_upload_retry.js | 46 +++ app/components/file_upload_preview/index.js | 39 +-- app/components/post_textbox/index.js | 4 +- app/components/post_textbox/post_textbox.js | 4 +- app/reducers/views/channel.js | 1 + app/reducers/views/thread.js | 1 + app/selectors/file.js | 21 ++ app/utils/file.js | 4 +- package.json | 2 +- yarn.lock | 4 +- 18 files changed, 516 insertions(+), 226 deletions(-) create mode 100644 app/components/file_upload_preview/file_upload_item/file_upload_item.js create mode 100644 app/components/file_upload_preview/file_upload_item/index.js create mode 100644 app/components/file_upload_preview/file_upload_remove.js create mode 100644 app/components/file_upload_preview/file_upload_retry.js create mode 100644 app/selectors/file.js diff --git a/Makefile b/Makefile index f8fd16db6..4e1a49a1e 100644 --- a/Makefile +++ b/Makefile @@ -88,7 +88,6 @@ start: | pre-run ## Starts the React Native packager server node ./node_modules/react-native/local-cli/cli.js start; \ else \ echo React Native packager server already running; \ - ps -e | grep -i "cli.js start" | grep -iv grep | awk '{print $$1}' > server.PID; \ fi stop: ## Stops the React Native packager server diff --git a/app/actions/views/file_upload.js b/app/actions/views/file_upload.js index bf4f29d8b..b2309dbad 100644 --- a/app/actions/views/file_upload.js +++ b/app/actions/views/file_upload.js @@ -1,22 +1,15 @@ // Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import {Platform} from 'react-native'; -import {uploadFile} from 'mattermost-redux/actions/files'; +import {FileTypes} from 'mattermost-redux/action_types'; -import { - buildFileUploadData, - encodeHeaderURIStringToUTF8, - generateId, -} from 'app/utils/file'; import {ViewTypes} from 'app/constants'; +import {buildFileUploadData, generateId} from 'app/utils/file'; -export function handleUploadFiles(files, rootId) { - return async (dispatch, getState) => { +export function initUploadFiles(files, rootId) { + return (dispatch, getState) => { const state = getState(); - const channelId = state.entities.channels.currentChannelId; - const formData = new FormData(); const clientIds = []; files.forEach((file) => { @@ -27,30 +20,36 @@ export function handleUploadFiles(files, rootId) { clientId, localPath: fileData.uri, name: fileData.name, - type: fileData.mimeType, + type: fileData.type, extension: fileData.extension, }); - - fileData.name = encodeHeaderURIStringToUTF8(fileData.name); - formData.append('files', fileData); - formData.append('channel_id', channelId); - formData.append('client_ids', clientId); }); - let formBoundary; - if (Platform.os === 'ios') { - formBoundary = '--mobile.client.file.upload'; - } - dispatch({ type: ViewTypes.SET_TEMP_UPLOAD_FILES_FOR_POST_DRAFT, clientIds, channelId, rootId, }); + }; +} - const clientIdsArray = clientIds.map((c) => c.clientId); - await uploadFile(channelId, rootId, clientIdsArray, formData, formBoundary)(dispatch, getState); +export function uploadFailed(clientIds, channelId, rootId, error) { + return { + type: FileTypes.UPLOAD_FILES_FAILURE, + clientIds, + channelId, + rootId, + error, + }; +} + +export function uploadComplete(data, channelId, rootId) { + return { + type: FileTypes.RECEIVED_UPLOAD_FILES, + data, + channelId, + rootId, }; } @@ -59,20 +58,6 @@ export function retryFileUpload(file, rootId) { const state = getState(); const channelId = state.entities.channels.currentChannelId; - const formData = new FormData(); - const fileData = buildFileUploadData(file); - - fileData.uri = file.localPath; - - fileData.name = encodeHeaderURIStringToUTF8(fileData.name); - formData.append('files', fileData); - formData.append('channel_id', channelId); - formData.append('client_ids', file.clientId); - - let formBoundary; - if (Platform.os === 'ios') { - formBoundary = '--mobile.client.file.upload'; - } dispatch({ type: ViewTypes.RETRY_UPLOAD_FILE_FOR_POST, @@ -80,8 +65,6 @@ export function retryFileUpload(file, rootId) { channelId, rootId, }); - - await uploadFile(channelId, rootId, [file.clientId], formData, formBoundary)(dispatch, getState); }; } diff --git a/app/components/attachment_button.js b/app/components/attachment_button.js index bb068746b..7e6fe9bfd 100644 --- a/app/components/attachment_button.js +++ b/app/components/attachment_button.js @@ -1,6 +1,6 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; -import {injectIntl, intlShape} from 'react-intl'; +import {intlShape} from 'react-intl'; import { Alert, Platform, @@ -14,12 +14,11 @@ import Permissions from 'react-native-permissions'; import {PermissionTypes} from 'app/constants'; import {changeOpacity} from 'app/utils/theme'; -class AttachmentButton extends PureComponent { +export default class AttachmentButton extends PureComponent { static propTypes = { blurTextBox: PropTypes.func.isRequired, children: PropTypes.node, fileCount: PropTypes.number, - intl: intlShape.isRequired, maxFileCount: PropTypes.number.isRequired, navigator: PropTypes.object.isRequired, onShowFileMaxWarning: PropTypes.func, @@ -32,8 +31,12 @@ class AttachmentButton extends PureComponent { maxFileCount: 5, }; + static contextTypes = { + intl: intlShape.isRequired, + }; + attachFileFromCamera = async () => { - const {formatMessage} = this.props.intl; + const {formatMessage} = this.context.intl; const options = { quality: 1.0, noData: true, @@ -72,7 +75,7 @@ class AttachmentButton extends PureComponent { }; attachFileFromLibrary = () => { - const {formatMessage} = this.props.intl; + const {formatMessage} = this.context.intl; const options = { quality: 1.0, noData: true, @@ -107,7 +110,7 @@ class AttachmentButton extends PureComponent { }; attachVideoFromLibraryAndroid = () => { - const {formatMessage} = this.props.intl; + const {formatMessage} = this.context.intl; const options = { quality: 1.0, mediaType: 'video', @@ -140,7 +143,7 @@ class AttachmentButton extends PureComponent { hasPhotoPermission = async () => { if (Platform.OS === 'ios') { - const {formatMessage} = this.props.intl; + const {formatMessage} = this.context.intl; let permissionRequest; const hasPermissionToStorage = await Permissions.check('photo'); @@ -312,4 +315,3 @@ const style = StyleSheet.create({ }, }); -export default injectIntl(AttachmentButton); diff --git a/app/components/file_attachment_list/file_attachment_image.js b/app/components/file_attachment_list/file_attachment_image.js index 82f4d6847..8c4c45dcf 100644 --- a/app/components/file_attachment_list/file_attachment_image.js +++ b/app/components/file_attachment_list/file_attachment_image.js @@ -144,13 +144,13 @@ export default class FileAttachmentImage extends PureComponent { if (this.state.retry === 4) { source = imageIcon; + } else if (file.failed || file.localPath) { + source = {uri: file.localPath}; } else if (file.id) { source = {uri: this.handleGetImageURL()}; - } else if (file.failed) { - source = {uri: file.localPath}; } - const isInFetchCache = fetchCache[source.uri]; + const isInFetchCache = fetchCache[source.uri] || Boolean(file.localPath); const imageComponentLoaders = { onError: isInFetchCache ? null : this.handleLoadError, @@ -164,7 +164,7 @@ export default class FileAttachmentImage extends PureComponent { let imageStyle = {height, width}; if (imageSize === IMAGE_SIZE.Preview) { height = 100; - width = this.calculateNeededWidth(file.height, file.width, height); + width = this.calculateNeededWidth(file.height, file.width, height) || 100; imageStyle = {height, width, position: 'absolute', top: 0, left: 0, borderBottomLeftRadius: 2, borderTopLeftRadius: 2}; } diff --git a/app/components/file_upload_preview/file_upload_item/file_upload_item.js b/app/components/file_upload_preview/file_upload_item/file_upload_item.js new file mode 100644 index 000000000..0d4afec23 --- /dev/null +++ b/app/components/file_upload_preview/file_upload_item/file_upload_item.js @@ -0,0 +1,290 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React, {PureComponent} from 'react'; +import PropTypes from 'prop-types'; +import {Platform, StyleSheet, Text, View} from 'react-native'; +import RNFetchBlob from 'react-native-fetch-blob'; +import {AnimatedCircularProgress} from 'react-native-circular-progress'; + +import {Client4} from 'mattermost-redux/client'; + +import FileAttachmentImage from 'app/components/file_attachment_list/file_attachment_image'; +import FileAttachmentIcon from 'app/components/file_attachment_list/file_attachment_icon'; +import FileUploadRetry from 'app/components/file_upload_preview/file_upload_retry'; +import FileUploadRemove from 'app/components/file_upload_preview/file_upload_remove'; +import {buildFileUploadData, encodeHeaderURIStringToUTF8} from 'app/utils/file'; + +export default class FileUploadItem extends PureComponent { + static propTypes = { + actions: PropTypes.shape({ + addFileToFetchCache: PropTypes.func.isRequired, + handleRemoveFile: PropTypes.func.isRequired, + retryFileUpload: PropTypes.func.isRequired, + uploadComplete: PropTypes.func.isRequired, + uploadFailed: PropTypes.func.isRequired, + }).isRequired, + channelId: PropTypes.string.isRequired, + fetchCache: PropTypes.object.isRequired, + file: PropTypes.object.isRequired, + rootId: PropTypes.string, + theme: PropTypes.object.isRequired, + }; + + state = { + progress: 0, + }; + + componentDidMount() { + if (this.props.file.loading) { + this.uploadFile(); + } + } + + componentWillReceiveProps(nextProps) { + const {file} = this.props; + const {file: nextFile} = nextProps; + + if (file.failed !== nextFile.failed && nextFile.loading) { + this.uploadFile(); + } + } + + handleRetryFileUpload = (file) => { + if (!file.failed) { + return; + } + + this.props.actions.retryFileUpload(file, this.props.rootId); + }; + + handleRemoveFile = (clientId, channelId, rootId) => { + const {handleRemoveFile} = this.props.actions; + if (this.uploadPromise) { + this.uploadPromise.cancel(() => { + this.canceled = true; + handleRemoveFile(clientId, channelId, rootId); + }); + } else { + handleRemoveFile(clientId, channelId, rootId); + } + }; + + handleUploadCompleted = (res) => { + const {actions, channelId, file, rootId} = this.props; + const response = JSON.parse(res.data); + if (res.respInfo.status === 200 || res.respInfo.status === 201) { + this.setState({progress: 100}, () => { + const data = response.file_infos.map((f) => { + return { + ...f, + clientId: file.clientId, + }; + }); + actions.uploadComplete(data, channelId, rootId); + }); + } else { + actions.uploadFailed([file.clientId], channelId, rootId, response.message); + } + this.uploadPromise = null; + }; + + handleUploadError = (error) => { + const {actions, channelId, file, rootId} = this.props; + if (!this.canceled) { + actions.uploadFailed([file.clientId], channelId, rootId, error); + } + this.uploadPromise = null; + }; + + handleUploadProgress = (loaded, total) => { + this.setState({progress: Math.floor((loaded / total) * 100)}); + }; + + isImageType = () => { + const {file} = this.props; + + if (file.has_preview_image || file.mime_type === 'image/gif' || + (file.localPath && file.type && file.type.includes('image'))) { + return true; + } + + return false; + }; + + uploadFile = () => { + const {channelId, file} = this.props; + const fileData = buildFileUploadData(file); + + const headers = { + Authorization: `Bearer ${Client4.getToken()}`, + 'Content-Type': 'multipart/form-data', + }; + + const fileInfo = { + name: 'files', + filename: encodeHeaderURIStringToUTF8(fileData.name), + data: RNFetchBlob.wrap(file.localPath.replace('file://', '')), + type: fileData.type, + }; + + const data = [ + {name: 'channel_id', data: channelId}, + {name: 'client_ids', data: file.clientId}, + fileInfo, + ]; + + Client4.trackEvent('api', 'api_files_upload'); + + this.uploadPromise = RNFetchBlob.fetch('POST', Client4.getFilesRoute(), headers, data); + this.uploadPromise.uploadProgress(this.handleUploadProgress); + this.uploadPromise.then(this.handleUploadCompleted).catch(this.handleUploadError); + }; + + renderProgress = (fill) => { + const realFill = Number(fill.toFixed(0)); + + return ( + + + + {`${realFill}%`} + + + + ); + }; + + render() { + const { + actions, + channelId, + fetchCache, + file, + rootId, + theme, + } = this.props; + const {addFileToFetchCache} = actions; + const {progress} = this.state; + let filePreviewComponent; + + if (this.isImageType()) { + filePreviewComponent = ( + + ); + } else { + filePreviewComponent = ( + + ); + } + + return ( + + + {filePreviewComponent} + {file.failed && + + } + {file.loading && !file.failed && + + + {this.renderProgress} + + + } + + + + ); + } +} + +const styles = StyleSheet.create({ + preview: { + justifyContent: 'flex-end', + height: 115, + width: 115, + }, + previewShadow: { + height: 100, + width: 100, + elevation: 10, + ...Platform.select({ + ios: { + backgroundColor: '#fff', + shadowColor: '#000', + shadowOpacity: 0.5, + shadowRadius: 4, + shadowOffset: { + width: 0, + height: 0, + }, + }, + }), + }, + progressCircle: { + alignItems: 'center', + height: '100%', + justifyContent: 'center', + width: '100%', + }, + progressCircleContent: { + alignItems: 'center', + backgroundColor: 'rgba(0, 0, 0, 0.4)', + height: 100, + justifyContent: 'center', + position: 'absolute', + width: 100, + }, + progressCirclePercentage: { + alignItems: 'center', + flex: 1, + }, + progressContent: { + alignItems: 'center', + height: '100%', + justifyContent: 'center', + left: 0, + position: 'absolute', + top: 40, + width: '100%', + }, + progressText: { + color: 'white', + fontSize: 18, + }, +}); diff --git a/app/components/file_upload_preview/file_upload_item/index.js b/app/components/file_upload_preview/file_upload_item/index.js new file mode 100644 index 000000000..f90c0c4f2 --- /dev/null +++ b/app/components/file_upload_preview/file_upload_item/index.js @@ -0,0 +1,33 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {bindActionCreators} from 'redux'; +import {connect} from 'react-redux'; + +import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; + +import {handleRemoveFile, retryFileUpload, uploadComplete, uploadFailed} from 'app/actions/views/file_upload'; +import {addFileToFetchCache} from 'app/actions/views/file_preview'; + +import FileUploadItem from './file_upload_item'; + +function mapStateToProps(state) { + return { + fetchCache: state.views.fetchCache, + theme: getTheme(state), + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + addFileToFetchCache, + handleRemoveFile, + retryFileUpload, + uploadComplete, + uploadFailed, + }, dispatch), + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(FileUploadItem); diff --git a/app/components/file_upload_preview/file_upload_preview.js b/app/components/file_upload_preview/file_upload_preview.js index c63c4b8e9..d277f4a38 100644 --- a/app/components/file_upload_preview/file_upload_preview.js +++ b/app/components/file_upload_preview/file_upload_preview.js @@ -4,105 +4,39 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import { - Platform, ScrollView, StyleSheet, - TouchableOpacity, View, } from 'react-native'; -import Icon from 'react-native-vector-icons/Ionicons'; import FormattedText from 'app/components/formatted_text'; -import FileAttachmentImage from 'app/components/file_attachment_list/file_attachment_image'; -import FileAttachmentIcon from 'app/components/file_attachment_list/file_attachment_icon'; + +import FileUploadItem from './file_upload_item'; export default class FileUploadPreview extends PureComponent { static propTypes = { - actions: PropTypes.shape({ - addFileToFetchCache: PropTypes.func.isRequired, - handleRemoveFile: PropTypes.func.isRequired, - retryFileUpload: PropTypes.func.isRequired, - }).isRequired, channelId: PropTypes.string.isRequired, channelIsLoading: PropTypes.bool, createPostRequestStatus: PropTypes.string.isRequired, deviceHeight: PropTypes.number.isRequired, - fetchCache: PropTypes.object.isRequired, files: PropTypes.array.isRequired, + filesUploadingForCurrentChannel: PropTypes.bool.isRequired, inputHeight: PropTypes.number.isRequired, rootId: PropTypes.string, - theme: PropTypes.object.isRequired, - filesUploadingForCurrentChannel: PropTypes.bool.isRequired, showFileMaxWarning: PropTypes.bool.isRequired, - }; - - handleRetryFileUpload = (file) => { - if (!file.failed) { - return; - } - - this.props.actions.retryFileUpload(file, this.props.rootId); + theme: PropTypes.object.isRequired, }; buildFilePreviews = () => { return this.props.files.map((file) => { - let filePreviewComponent; - if (file.loading | (file.has_preview_image || file.mime_type === 'image/gif')) { - filePreviewComponent = ( - - ); - } else { - filePreviewComponent = ( - - ); - } return ( - - - {filePreviewComponent} - {file.failed && - this.handleRetryFileUpload(file)} - > - - - } - - this.props.actions.handleRemoveFile(file.clientId, this.props.channelId, this.props.rootId)} - > - - - + channelId={this.props.channelId} + file={file} + rootId={this.props.rootId} + theme={this.props.theme} + /> ); }); }; @@ -151,59 +85,6 @@ const style = StyleSheet.create({ position: 'absolute', width: '100%', }, - failed: { - backgroundColor: 'rgba(0, 0, 0, 0.8)', - position: 'absolute', - height: '100%', - width: '100%', - alignItems: 'center', - justifyContent: 'center', - }, - preview: { - justifyContent: 'flex-end', - height: 115, - width: 115, - }, - previewShadow: { - height: 100, - width: 100, - elevation: 10, - ...Platform.select({ - ios: { - backgroundColor: '#fff', - shadowColor: '#000', - shadowOpacity: 0.5, - shadowRadius: 4, - shadowOffset: { - width: 0, - height: 0, - }, - }, - }), - }, - removeButtonIcon: Platform.select({ - ios: { - marginTop: 2, - }, - android: { - marginLeft: 1, - }, - }), - removeButtonWrapper: { - alignItems: 'center', - justifyContent: 'center', - position: 'absolute', - overflow: 'hidden', - elevation: 11, - top: 7, - right: 7, - width: 24, - height: 24, - borderRadius: 12, - backgroundColor: '#000', - borderWidth: 1, - borderColor: '#fff', - }, scrollView: { flex: 1, marginBottom: 10, diff --git a/app/components/file_upload_preview/file_upload_remove.js b/app/components/file_upload_preview/file_upload_remove.js new file mode 100644 index 000000000..20c7c4ad7 --- /dev/null +++ b/app/components/file_upload_preview/file_upload_remove.js @@ -0,0 +1,64 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +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'; + +export default class FileUploadRemove extends PureComponent { + static propTypes = { + channelId: PropTypes.string, + clientId: PropTypes.string, + onPress: PropTypes.func.isRequired, + rootId: PropTypes.string, + }; + + handleOnPress = () => { + const {channelId, clientId, onPress, rootId} = this.props; + + onPress(clientId, channelId, rootId); + }; + + render() { + return ( + + + + ); + } +} + +const style = StyleSheet.create({ + removeButtonIcon: Platform.select({ + ios: { + marginTop: 2, + }, + android: { + marginLeft: 1, + }, + }), + removeButtonWrapper: { + alignItems: 'center', + justifyContent: 'center', + position: 'absolute', + overflow: 'hidden', + elevation: 11, + top: 7, + right: 7, + width: 24, + height: 24, + borderRadius: 12, + backgroundColor: '#000', + borderWidth: 1, + borderColor: '#fff', + }, +}); diff --git a/app/components/file_upload_preview/file_upload_retry.js b/app/components/file_upload_preview/file_upload_retry.js new file mode 100644 index 000000000..03ac44789 --- /dev/null +++ b/app/components/file_upload_preview/file_upload_retry.js @@ -0,0 +1,46 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React, {PureComponent} from 'react'; +import PropTypes from 'prop-types'; +import {StyleSheet, TouchableOpacity} from 'react-native'; +import Icon from 'react-native-vector-icons/Ionicons'; + +export default class FileUploadRetry extends PureComponent { + static propTypes = { + file: PropTypes.object.isRequired, + onPress: PropTypes.func.isRequired, + }; + + handleOnPress = () => { + const {file, onPress} = this.props; + + onPress(file); + }; + + render() { + return ( + + + + ); + } +} + +const style = StyleSheet.create({ + failed: { + backgroundColor: 'rgba(0, 0, 0, 0.8)', + position: 'absolute', + height: '100%', + width: '100%', + alignItems: 'center', + justifyContent: 'center', + }, +}); diff --git a/app/components/file_upload_preview/index.js b/app/components/file_upload_preview/index.js index 42448023e..3e0fd8201 100644 --- a/app/components/file_upload_preview/index.js +++ b/app/components/file_upload_preview/index.js @@ -1,34 +1,14 @@ // 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 {createSelector} from 'reselect'; - -import {handleRemoveFile, retryFileUpload} from 'app/actions/views/file_upload'; -import {addFileToFetchCache} from 'app/actions/views/file_preview'; -import {getDimensions} from 'app/selectors/device'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {getDimensions} from 'app/selectors/device'; +import {checkForFileUploadingInChannel} from 'app/selectors/file'; + import FileUploadPreview from './file_upload_preview'; -const checkForFileUploadingInChannel = createSelector( - (state, channelId, rootId) => { - if (rootId) { - return state.views.thread.drafts[rootId]; - } - - return state.views.channel.drafts[channelId]; - }, - (draft) => { - if (!draft || !draft.files) { - return false; - } - - return draft.files.some((f) => f.loading); - } -); - function mapStateToProps(state, ownProps) { const {deviceHeight} = getDimensions(state); @@ -36,20 +16,9 @@ function mapStateToProps(state, ownProps) { channelIsLoading: state.views.channel.loading, createPostRequestStatus: state.requests.posts.createPost.status, deviceHeight, - fetchCache: state.views.fetchCache, filesUploadingForCurrentChannel: checkForFileUploadingInChannel(state, ownProps.channelId, ownProps.rootId), theme: getTheme(state), }; } -function mapDispatchToProps(dispatch) { - return { - actions: bindActionCreators({ - addFileToFetchCache, - handleRemoveFile, - retryFileUpload, - }, dispatch), - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(FileUploadPreview); +export default connect(mapStateToProps)(FileUploadPreview); diff --git a/app/components/post_textbox/index.js b/app/components/post_textbox/index.js index d88b7aa96..7a8d75b40 100644 --- a/app/components/post_textbox/index.js +++ b/app/components/post_textbox/index.js @@ -14,7 +14,7 @@ import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import {executeCommand} from 'app/actions/views/command'; import {addReactionToLatestPost} from 'app/actions/views/emoji'; import {handlePostDraftChanged, handlePostDraftSelectionChanged} from 'app/actions/views/channel'; -import {handleClearFiles, handleClearFailedFiles, handleRemoveLastFile, handleUploadFiles} from 'app/actions/views/file_upload'; +import {handleClearFiles, handleClearFailedFiles, handleRemoveLastFile, initUploadFiles} from 'app/actions/views/file_upload'; import {handleCommentDraftChanged, handleCommentDraftSelectionChanged} from 'app/actions/views/thread'; import {userTyping} from 'app/actions/views/typing'; import {getCurrentChannelDraft, getThreadDraft} from 'app/selectors/views'; @@ -62,7 +62,7 @@ function mapDispatchToProps(dispatch) { handleCommentDraftChanged, handlePostDraftChanged, handleRemoveLastFile, - handleUploadFiles, + initUploadFiles, userTyping, handlePostDraftSelectionChanged, handleCommentDraftSelectionChanged, diff --git a/app/components/post_textbox/post_textbox.js b/app/components/post_textbox/post_textbox.js index adab21440..a0814ceab 100644 --- a/app/components/post_textbox/post_textbox.js +++ b/app/components/post_textbox/post_textbox.js @@ -31,7 +31,7 @@ export default class PostTextbox extends PureComponent { handleClearFiles: PropTypes.func.isRequired, handleClearFailedFiles: PropTypes.func.isRequired, handleRemoveLastFile: PropTypes.func.isRequired, - handleUploadFiles: PropTypes.func.isRequired, + initUploadFiles: PropTypes.func.isRequired, userTyping: PropTypes.func.isRequired, handlePostDraftSelectionChanged: PropTypes.func.isRequired, handleCommentDraftSelectionChanged: PropTypes.func.isRequired, @@ -260,7 +260,7 @@ export default class PostTextbox extends PureComponent { }; handleUploadFiles = (images) => { - this.props.actions.handleUploadFiles(images, this.props.rootId); + this.props.actions.initUploadFiles(images, this.props.rootId); }; renderSendButton = () => { diff --git a/app/reducers/views/channel.js b/app/reducers/views/channel.js index e465c99cd..44e86c13a 100644 --- a/app/reducers/views/channel.js +++ b/app/reducers/views/channel.js @@ -114,6 +114,7 @@ function handleReceivedUploadFiles(state, action) { return { ...file, localPath: tempFile.localPath, + loading: false, }; } diff --git a/app/reducers/views/thread.js b/app/reducers/views/thread.js index d71a9ad62..c9792a55c 100644 --- a/app/reducers/views/thread.js +++ b/app/reducers/views/thread.js @@ -118,6 +118,7 @@ function handleReceiveUploadFiles(state, action) { return { ...file, localPath: tempFile.localPath, + loading: false, }; } diff --git a/app/selectors/file.js b/app/selectors/file.js new file mode 100644 index 000000000..d28350933 --- /dev/null +++ b/app/selectors/file.js @@ -0,0 +1,21 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {createSelector} from 'reselect'; + +export const checkForFileUploadingInChannel = createSelector( + (state, channelId, rootId) => { + if (rootId) { + return state.views.thread.drafts[rootId]; + } + + return state.views.channel.drafts[channelId]; + }, + (draft) => { + if (!draft || !draft.files) { + return false; + } + + return draft.files.some((f) => f.loading); + } +); diff --git a/app/utils/file.js b/app/utils/file.js index 895844d75..03f4f633e 100644 --- a/app/utils/file.js +++ b/app/utils/file.js @@ -59,8 +59,8 @@ export async function deleteFileCache() { export function buildFileUploadData(file) { const re = /heic/i; const uri = file.uri; - let name = file.fileName || file.path || file.uri; - let mimeType = lookupMimeType(name); + let name = file.fileName || file.name || file.path || file.uri; + let mimeType = lookupMimeType(name.toLowerCase()); let extension = name.split('.').pop().replace('.', ''); if (re.test(extension)) { diff --git a/package.json b/package.json index 86ad3735d..49fb0be93 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "react-native-drawer": "2.5.0", "react-native-exception-handler": "2.7.1", "react-native-fast-image": "4.0.0", - "react-native-fetch-blob": "0.10.8", + "react-native-fetch-blob": "enahum/react-native-fetch-blob.git", "react-native-image-picker": "0.26.7", "react-native-keyboard-aware-scroll-view": "0.5.0", "react-native-linear-gradient": "2.4.0", diff --git a/yarn.lock b/yarn.lock index 90912f968..8689ae3cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5916,9 +5916,9 @@ react-native-fast-image@4.0.0: dependencies: prop-types "^15.5.10" -react-native-fetch-blob@0.10.8: +react-native-fetch-blob@enahum/react-native-fetch-blob.git: version "0.10.8" - resolved "https://registry.yarnpkg.com/react-native-fetch-blob/-/react-native-fetch-blob-0.10.8.tgz#4fc256abae0cb5f10e7c41f28c11b3ff330d72a9" + resolved "https://codeload.github.com/enahum/react-native-fetch-blob/tar.gz/75d5abfa1886665d7eaa947e70b9297b981f0983" dependencies: base-64 "0.1.0" glob "7.0.6"