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
This commit is contained in:
Elias Nahum 2018-04-04 16:14:26 +03:00 committed by Harrison Healey
parent 1d72f46031
commit e20b84c6d8
18 changed files with 516 additions and 226 deletions

View file

@ -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

View file

@ -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);
};
}

View file

@ -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);

View file

@ -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};
}

View file

@ -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 (
<View style={styles.progressContent}>
<View style={styles.progressCirclePercentage}>
<Text style={styles.progressText}>
{`${realFill}%`}
</Text>
</View>
</View>
);
};
render() {
const {
actions,
channelId,
fetchCache,
file,
rootId,
theme,
} = this.props;
const {addFileToFetchCache} = actions;
const {progress} = this.state;
let filePreviewComponent;
if (this.isImageType()) {
filePreviewComponent = (
<FileAttachmentImage
addFileToFetchCache={addFileToFetchCache}
fetchCache={fetchCache}
file={file}
imageHeight={100}
imageWidth={100}
wrapperHeight={100}
wrapperWidth={100}
/>
);
} else {
filePreviewComponent = (
<FileAttachmentIcon
file={file}
theme={theme}
imageHeight={100}
imageWidth={100}
wrapperHeight={100}
wrapperWidth={100}
/>
);
}
return (
<View
key={file.clientId}
style={styles.preview}
>
<View style={styles.previewShadow}>
{filePreviewComponent}
{file.failed &&
<FileUploadRetry
file={file}
onPress={this.handleRetryFileUpload}
/>
}
{file.loading && !file.failed &&
<View style={styles.progressCircleContent}>
<AnimatedCircularProgress
size={100}
fill={progress}
width={4}
backgroundColor='rgba(255, 255, 255, 0.5)'
tintColor='white'
rotation={0}
style={styles.progressCircle}
>
{this.renderProgress}
</AnimatedCircularProgress>
</View>
}
</View>
<FileUploadRemove
channelId={channelId}
clientId={file.clientId}
onPress={this.handleRemoveFile}
rootId={rootId}
/>
</View>
);
}
}
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,
},
});

View file

@ -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);

View file

@ -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 = (
<FileAttachmentImage
addFileToFetchCache={this.props.actions.addFileToFetchCache}
fetchCache={this.props.fetchCache}
file={file}
imageHeight={100}
imageWidth={100}
wrapperHeight={100}
wrapperWidth={100}
/>
);
} else {
filePreviewComponent = (
<FileAttachmentIcon
file={file}
theme={this.props.theme}
imageHeight={100}
imageWidth={100}
wrapperHeight={100}
wrapperWidth={100}
/>
);
}
return (
<View
<FileUploadItem
key={file.clientId}
style={style.preview}
>
<View style={style.previewShadow}>
{filePreviewComponent}
{file.failed &&
<TouchableOpacity
style={style.failed}
onPress={() => this.handleRetryFileUpload(file)}
>
<Icon
name='md-refresh'
size={50}
color='#fff'
/>
</TouchableOpacity>
}
</View>
<TouchableOpacity
style={style.removeButtonWrapper}
onPress={() => this.props.actions.handleRemoveFile(file.clientId, this.props.channelId, this.props.rootId)}
>
<Icon
name='md-close'
color='#fff'
size={18}
style={style.removeButtonIcon}
/>
</TouchableOpacity>
</View>
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,

View file

@ -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 (
<TouchableOpacity
style={style.removeButtonWrapper}
onPress={this.handleOnPress}
>
<Icon
name='md-close'
color='#fff'
size={18}
style={style.removeButtonIcon}
/>
</TouchableOpacity>
);
}
}
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',
},
});

View file

@ -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 (
<TouchableOpacity
style={style.failed}
onPress={this.handleOnPress}
>
<Icon
name='md-refresh'
size={50}
color='#fff'
/>
</TouchableOpacity>
);
}
}
const style = StyleSheet.create({
failed: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
position: 'absolute',
height: '100%',
width: '100%',
alignItems: 'center',
justifyContent: 'center',
},
});

View file

@ -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);

View file

@ -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,

View file

@ -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 = () => {

View file

@ -114,6 +114,7 @@ function handleReceivedUploadFiles(state, action) {
return {
...file,
localPath: tempFile.localPath,
loading: false,
};
}

View file

@ -118,6 +118,7 @@ function handleReceiveUploadFiles(state, action) {
return {
...file,
localPath: tempFile.localPath,
loading: false,
};
}

21
app/selectors/file.js Normal file
View file

@ -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);
}
);

View file

@ -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)) {

View file

@ -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",

View file

@ -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"