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:
parent
1d72f46031
commit
e20b84c6d8
18 changed files with 516 additions and 226 deletions
1
Makefile
1
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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
33
app/components/file_upload_preview/file_upload_item/index.js
Normal file
33
app/components/file_upload_preview/file_upload_item/index.js
Normal 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);
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
64
app/components/file_upload_preview/file_upload_remove.js
Normal file
64
app/components/file_upload_preview/file_upload_remove.js
Normal 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',
|
||||
},
|
||||
});
|
||||
46
app/components/file_upload_preview/file_upload_retry.js
Normal file
46
app/components/file_upload_preview/file_upload_retry.js
Normal 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',
|
||||
},
|
||||
});
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ function handleReceivedUploadFiles(state, action) {
|
|||
return {
|
||||
...file,
|
||||
localPath: tempFile.localPath,
|
||||
loading: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ function handleReceiveUploadFiles(state, action) {
|
|||
return {
|
||||
...file,
|
||||
localPath: tempFile.localPath,
|
||||
loading: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
21
app/selectors/file.js
Normal file
21
app/selectors/file.js
Normal 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);
|
||||
}
|
||||
);
|
||||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue