Retry File Upload (#515)

* rename searchMoreChannels to searchChannels

* Handle send post when a file failed to upload
This commit is contained in:
enahum 2017-05-02 12:06:08 -03:00 committed by GitHub
parent 136a8fede8
commit a7dc43aee9
13 changed files with 220 additions and 28 deletions

View file

@ -4,7 +4,7 @@
import FormData from 'form-data';
import {Platform} from 'react-native';
import {uploadFile} from 'mattermost-redux/actions/files';
import {lookupMimeType} from 'mattermost-redux/utils/file_utils';
import {lookupMimeType, parseClientIdsFromFormData} from 'mattermost-redux/utils/file_utils';
import {generateId} from 'app/utils/file';
import {ViewTypes} from 'app/constants';
@ -23,7 +23,9 @@ export function handleUploadFiles(files, rootId) {
clientIds.push({
clientId,
localPath: file.path
localPath: file.uri,
name: file.fileName,
type: mimeType
});
const fileData = {
@ -49,7 +51,40 @@ export function handleUploadFiles(files, rootId) {
rootId
});
await uploadFile(channelId, rootId, formData, formBoundary)(dispatch, getState);
await uploadFile(channelId, rootId, parseClientIdsFromFormData(formData), formData, formBoundary)(dispatch, getState);
};
}
export function retryFileUpload(file, rootId) {
return async (dispatch, getState) => {
const state = getState();
const channelId = state.entities.channels.currentChannelId;
const formData = new FormData();
const fileData = {
uri: file.localPath,
name: file.name,
type: file.type
};
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,
clientId: file.clientId,
channelId,
rootId
});
await uploadFile(channelId, rootId, [file.clientId], formData, formBoundary)(dispatch, getState);
};
}

View file

@ -28,7 +28,7 @@ export default class ChannelMention extends Component {
theme: PropTypes.object.isRequired,
onChangeText: PropTypes.func.isRequired,
actions: PropTypes.shape({
searchMoreChannels: PropTypes.func.isRequired
searchChannels: PropTypes.func.isRequired
})
};
@ -96,7 +96,7 @@ export default class ChannelMention extends Component {
});
const {currentTeamId} = this.props;
this.props.actions.searchMoreChannels(currentTeamId, matchTerm);
this.props.actions.searchChannels(currentTeamId, matchTerm);
return;
}

View file

@ -4,7 +4,7 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {searchMoreChannels} from 'mattermost-redux/actions/channels';
import {searchChannels} from 'mattermost-redux/actions/channels';
import {General} from 'mattermost-redux/constants';
import {getMyChannels, getOtherChannels} from 'mattermost-redux/selectors/entities/channels';
@ -41,7 +41,7 @@ function mapStateToProps(state, ownProps) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
searchMoreChannels
searchChannels
}, dispatch)
};
}

View file

@ -136,6 +136,8 @@ export default class FileAttachmentImage extends PureComponent {
source = imageIcon;
} else if (file.id) {
source = {uri: this.handleGetImageURL()};
} else if (file.failed) {
source = {uri: file.localPath};
}
const isInFetchCache = fetchCache[source.uri];
@ -158,10 +160,10 @@ export default class FileAttachmentImage extends PureComponent {
{...imageComponentLoaders}
/>
</AnimatedView>
{(!isInFetchCache && (file.loading || this.state.requesting)) &&
<View style={[style.loaderContainer, {backgroundColor: loadingBackgroundColor}]}>
<ActivityIndicator size='small'/>
</View>
{(!isInFetchCache && !file.failed && (file.loading || this.state.requesting)) &&
<View style={[style.loaderContainer, {backgroundColor: loadingBackgroundColor}]}>
<ActivityIndicator size='small'/>
</View>
}
</View>
);

View file

@ -10,7 +10,7 @@ import {
TouchableOpacity,
View
} from 'react-native';
import Font from 'react-native-vector-icons/Ionicons';
import Icon from 'react-native-vector-icons/Ionicons';
import {RequestStatus} from 'mattermost-redux/constants';
import FileAttachmentImage from 'app/components/file_attachment_list/file_attachment_image';
@ -22,7 +22,8 @@ export default class FileUploadPreview extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
addFileToFetchCache: PropTypes.func.isRequired,
handleRemoveFile: PropTypes.func.isRequired
handleRemoveFile: PropTypes.func.isRequired,
retryFileUpload: PropTypes.func.isRequired
}).isRequired,
channelId: PropTypes.string.isRequired,
channelIsLoading: PropTypes.bool,
@ -34,6 +35,14 @@ export default class FileUploadPreview extends PureComponent {
uploadFileRequestStatus: PropTypes.string.isRequired
};
handleRetryFileUpload = (file) => {
if (!file.failed) {
return;
}
this.props.actions.retryFileUpload(file, this.props.rootId);
};
buildFilePreviews = () => {
return this.props.files.map((file) => {
return (
@ -47,12 +56,24 @@ export default class FileUploadPreview extends PureComponent {
fetchCache={this.props.fetchCache}
file={file}
/>
{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)}
>
<Font
<Icon
name='md-close'
color='#fff'
size={18}
@ -94,12 +115,21 @@ 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({

View file

@ -4,7 +4,7 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {handleRemoveFile} from 'app/actions/views/file_upload';
import {handleRemoveFile, retryFileUpload} from 'app/actions/views/file_upload';
import {addFileToFetchCache} from 'app/actions/views/file_preview';
import {getTheme} from 'app/selectors/preferences';
@ -25,7 +25,8 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
addFileToFetchCache,
handleRemoveFile
handleRemoveFile,
retryFileUpload
}, dispatch)
};
}

View file

@ -98,7 +98,10 @@ class PostTextbox extends PureComponent {
);
}
const canSend = ((valueLength > 0 && valueLength <= MAX_MESSAGE_LENGTH) || files.length > 0) && uploadFileRequestStatus !== RequestStatus.STARTED;
const canSend = (
(valueLength > 0 && valueLength <= MAX_MESSAGE_LENGTH) ||
files.filter((f) => !f.failed).length > 0
) && uploadFileRequestStatus !== RequestStatus.STARTED;
this.setState({
canSend
});
@ -134,11 +137,38 @@ class PostTextbox extends PureComponent {
return false;
};
sendMessage = () => {
handleSendMessage = () => {
if (!this.state.canSend) {
return;
}
const hasFailedImages = this.props.files.some((f) => f.failed);
if (hasFailedImages) {
const {intl} = this.props;
Alert.alert(
intl.formatMessage({
id: 'mobile.post_textbox.uploadFailedTitle',
defaultMessage: 'Attachment failure'
}),
intl.formatMessage({
id: 'mobile.post_textbox.uploadFailedDesc',
defaultMessage: 'Some attachments failed to upload to the server, Are you sure you want to post the message?'
}),
[{
text: intl.formatMessage({id: 'mobile.channel_info.alertNo', defaultMessage: 'No'})
}, {
text: intl.formatMessage({id: 'mobile.channel_info.alertYes', defaultMessage: 'Yes'}),
onPress: this.sendMessage
}],
);
} else {
this.sendMessage();
}
};
sendMessage = () => {
const files = this.props.files.filter((f) => !f.failed);
const post = {
user_id: this.props.currentUserId,
channel_id: this.props.channelId,
@ -147,7 +177,7 @@ class PostTextbox extends PureComponent {
message: this.props.value
};
this.props.actions.createPost(post, this.props.files);
this.props.actions.createPost(post, files);
this.handleTextChange('');
if (this.props.files.length) {
this.props.actions.handleClearFiles(this.props.channelId, this.props.rootId);
@ -335,14 +365,14 @@ class PostTextbox extends PureComponent {
onContentSizeChange={this.handleContentSizeChange}
placeholder={placeholder}
placeholderTextColor={changeOpacity('#000', 0.5)}
onSubmitEditing={this.sendMessage}
onSubmitEditing={this.handleSendMessage}
multiline={true}
underlineColorAndroid='transparent'
style={[style.input, {height: Math.min(this.state.contentHeight, MAX_CONTENT_HEIGHT)}]}
/>
{this.state.canSend &&
<TouchableOpacity
onPress={this.sendMessage}
onPress={this.handleSendMessage}
style={style.sendButton}
>
<PaperPlane

View file

@ -24,6 +24,7 @@ const ViewTypes = keyMirror({
SET_COMMENT_DRAFT: null,
SET_TEMP_UPLOAD_FILES_FOR_POST_DRAFT: null,
RETRY_UPLOAD_FILE_FOR_POST: null,
CLEAR_FILES_FOR_POST_DRAFT: null,

View file

@ -53,6 +53,28 @@ function drafts(state = {}, action) {
[action.channelId]: Object.assign({}, state[action.channelId], {files})
};
}
case ViewTypes.RETRY_UPLOAD_FILE_FOR_POST: {
if (action.rootId) {
return state;
}
const files = state[action.channelId].files.map((f) => {
if (f.clientId === action.clientId) {
return {
...f,
loading: true,
failed: false
};
}
return f;
});
return {
...state,
[action.channelId]: Object.assign({}, state[action.channelId], {files})
};
}
case FileTypes.RECEIVED_UPLOAD_FILES: {
if (action.rootId || !state[action.channelId].files) {
return state;
@ -76,6 +98,29 @@ function drafts(state = {}, action) {
[action.channelId]: Object.assign({}, state[action.channelId], {files})
};
}
case FileTypes.UPLOAD_FILES_FAILURE: {
if (action.rootId) {
return state;
}
const clientIds = action.clientIds;
const files = state[action.channelId].files.map((tempFile) => {
if (clientIds.includes(tempFile.clientId)) {
return {
...tempFile,
loading: false,
failed: true
};
}
return tempFile;
});
return {
...state,
[action.channelId]: Object.assign({}, state[action.channelId], {files})
};
}
case ViewTypes.CLEAR_FILES_FOR_POST_DRAFT: {
if (action.rootId) {
return state;

View file

@ -41,7 +41,7 @@ function drafts(state = {}, action) {
return state;
}
const tempFiles = action.clientIds.map((id) => ({clientId: id, loading: true}));
const tempFiles = action.clientIds.map((temp) => ({...temp, loading: true}));
const files = [
...state[action.rootId].files,
...tempFiles
@ -52,6 +52,28 @@ function drafts(state = {}, action) {
[action.rootId]: Object.assign({}, state[action.rootId], {files})
};
}
case ViewTypes.RETRY_UPLOAD_FILE_FOR_POST: {
if (!action.rootId) {
return state;
}
const files = state[action.rootId].files.map((f) => {
if (f.clientId === action.clientId) {
return {
...f,
loading: true,
failed: false
};
}
return f;
});
return {
...state,
[action.rootId]: Object.assign({}, state[action.rootId], {files})
};
}
case FileTypes.RECEIVED_UPLOAD_FILES: {
if (!action.rootId) {
return state;
@ -61,7 +83,33 @@ function drafts(state = {}, action) {
const files = state[action.rootId].files.map((tempFile) => {
const file = action.data.find((f) => f.clientId === tempFile.clientId);
if (file) {
return file;
return {
...file,
localPath: tempFile.localPath
};
}
return tempFile;
});
return {
...state,
[action.rootId]: Object.assign({}, state[action.rootId], {files})
};
}
case FileTypes.UPLOAD_FILES_FAILURE: {
if (!action.rootId) {
return state;
}
const clientIds = action.clientIds;
const files = state[action.rootId].files.map((tempFile) => {
if (clientIds.includes(tempFile.clientId)) {
return {
...tempFile,
loading: false,
failed: true
};
}
return tempFile;

View file

@ -9,7 +9,7 @@ import {closeDrawers, goBack, goToCreateChannel} from 'app/actions/navigation';
import {getTheme} from 'app/selectors/preferences';
import {getOtherChannels} from 'mattermost-redux/selectors/entities/channels';
import {handleSelectChannel} from 'app/actions/views/channel';
import {getChannels, joinChannel, searchMoreChannels} from 'mattermost-redux/actions/channels';
import {getChannels, joinChannel, searchChannels} from 'mattermost-redux/actions/channels';
import MoreChannels from './more_channels';
@ -37,7 +37,7 @@ function mapDispatchToProps(dispatch) {
goToCreateChannel,
joinChannel,
getChannels,
searchMoreChannels
searchChannels
}, dispatch)
};
}

View file

@ -41,7 +41,7 @@ class MoreChannels extends PureComponent {
goToCreateChannel: PropTypes.func.isRequired,
joinChannel: PropTypes.func.isRequired,
getChannels: PropTypes.func.isRequired,
searchMoreChannels: PropTypes.func.isRequired
searchChannels: PropTypes.func.isRequired
}).isRequired
};
@ -151,7 +151,7 @@ class MoreChannels extends PureComponent {
clearTimeout(this.searchTimeoutId);
this.searchTimeoutId = setTimeout(() => {
this.props.actions.searchMoreChannels(this.props.currentTeamId, term);
this.props.actions.searchChannels(this.props.currentTeamId, term);
}, General.SEARCH_TIMEOUT_MILLISECONDS);
} else {
this.cancelSearch();

View file

@ -3518,7 +3518,7 @@ makeerror@1.0.x:
mattermost-redux@mattermost/mattermost-redux#master:
version "0.0.1"
resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/e75da057d1b37551ed4c4a4d8edf4f301c434162"
resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/e24060513e9f95c68c55bd0a1bd432259c79cf18"
dependencies:
deep-equal "1.0.1"
harmony-reflect "1.5.1"