diff --git a/app/actions/views/file_upload.js b/app/actions/views/file_upload.js
index 0017a9cbc..efface9cf 100644
--- a/app/actions/views/file_upload.js
+++ b/app/actions/views/file_upload.js
@@ -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);
};
}
diff --git a/app/components/autocomplete/channel_mention/channel_mention.js b/app/components/autocomplete/channel_mention/channel_mention.js
index f8612bbff..07e6b5612 100644
--- a/app/components/autocomplete/channel_mention/channel_mention.js
+++ b/app/components/autocomplete/channel_mention/channel_mention.js
@@ -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;
}
diff --git a/app/components/autocomplete/channel_mention/index.js b/app/components/autocomplete/channel_mention/index.js
index 025590766..09ee79742 100644
--- a/app/components/autocomplete/channel_mention/index.js
+++ b/app/components/autocomplete/channel_mention/index.js
@@ -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)
};
}
diff --git a/app/components/file_attachment_list/file_attachment_image.js b/app/components/file_attachment_list/file_attachment_image.js
index 9456e1c58..2184973fe 100644
--- a/app/components/file_attachment_list/file_attachment_image.js
+++ b/app/components/file_attachment_list/file_attachment_image.js
@@ -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}
/>
- {(!isInFetchCache && (file.loading || this.state.requesting)) &&
-
-
-
+ {(!isInFetchCache && !file.failed && (file.loading || this.state.requesting)) &&
+
+
+
}
);
diff --git a/app/components/file_upload_preview/file_upload_preview.js b/app/components/file_upload_preview/file_upload_preview.js
index 3ac1e11e4..73508753f 100644
--- a/app/components/file_upload_preview/file_upload_preview.js
+++ b/app/components/file_upload_preview/file_upload_preview.js
@@ -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 &&
+ this.handleRetryFileUpload(file)}
+ >
+
+
+ }
this.props.actions.handleRemoveFile(file.clientId, this.props.channelId, this.props.rootId)}
>
- 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 &&
{
+ 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;
diff --git a/app/reducers/views/thread.js b/app/reducers/views/thread.js
index f894a093d..10ca589e6 100644
--- a/app/reducers/views/thread.js
+++ b/app/reducers/views/thread.js
@@ -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;
diff --git a/app/scenes/more_channels/index.js b/app/scenes/more_channels/index.js
index 10e6a5b07..884427ae8 100644
--- a/app/scenes/more_channels/index.js
+++ b/app/scenes/more_channels/index.js
@@ -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)
};
}
diff --git a/app/scenes/more_channels/more_channels.js b/app/scenes/more_channels/more_channels.js
index 7628d3e2b..b203a23a5 100644
--- a/app/scenes/more_channels/more_channels.js
+++ b/app/scenes/more_channels/more_channels.js
@@ -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();
diff --git a/yarn.lock b/yarn.lock
index cf4965fac..1ca7ed6ef 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"