Retry File Upload (#515)
* rename searchMoreChannels to searchChannels * Handle send post when a file failed to upload
This commit is contained in:
parent
136a8fede8
commit
a7dc43aee9
13 changed files with 220 additions and 28 deletions
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue