Skip to content

Commit

Permalink
Merge pull request #11256 from nextcloud/backport/11107/stable28
Browse files Browse the repository at this point in the history
[stable28] fix(shares): allow to retry failed uploads
  • Loading branch information
DorraJaouad committed Dec 18, 2023
2 parents bcf0c2f + 02c266f commit e4315c4
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 23 deletions.
14 changes: 10 additions & 4 deletions src/components/MessagesList/MessagesGroup/Message/Message.vue
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ the main body of the message as well as a quote.
tabindex="0"
@mouseover="showReloadButton = true"
@focus="showReloadButton = true"
@mouseleave="showReloadButton = true"
@blur="showReloadButton = true">
@mouseleave="showReloadButton = false"
@blur="showReloadButton = false">
<NcButton v-if="sendingErrorCanRetry && showReloadButton"
:aria-label="sendingErrorIconTooltip"
@click="handleRetry">
Expand Down Expand Up @@ -687,6 +687,7 @@ export default {
sendingErrorCanRetry() {
return this.sendingFailure === 'timeout' || this.sendingFailure === 'other'
|| this.sendingFailure === 'failed-upload'
},
sendingErrorIconTooltip() {
Expand Down Expand Up @@ -802,8 +803,13 @@ export default {
handleRetry() {
if (this.sendingErrorCanRetry) {
EventBus.$emit('retry-message', this.id)
EventBus.$emit('focus-chat-input')
if (this.sendingFailure === 'failed-upload') {
const caption = this.renderedMessage !== this.message ? this.message : undefined
this.$store.dispatch('retryUploadFiles', { uploadId: this.messageObject.uploadId, caption })
} else {
EventBus.$emit('retry-message', this.id)
EventBus.$emit('focus-chat-input')
}
}
},
Expand Down
71 changes: 55 additions & 16 deletions src/store/fileUploadStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
shareFile,
} from '../services/filesSharingServices.js'
import { setAttachmentFolder } from '../services/settingsService.js'
import { useChatExtrasStore } from '../stores/chatExtras.js'
import {
hasDuplicateUploadNames,
findUniquePath,
Expand Down Expand Up @@ -66,6 +67,16 @@ const getters = {
.filter(([_index, uploadedFile]) => uploadedFile.status === 'initialised')
},

getFailedUploads: (state, getters) => (uploadId) => {
return getters.getUploadsArray(uploadId)
.filter(([_index, uploadedFile]) => uploadedFile.status === 'failedUpload')
},

getUploadingFiles: (state, getters) => (uploadId) => {
return getters.getUploadsArray(uploadId)
.filter(([_index, uploadedFile]) => uploadedFile.status === 'uploading')
},

// Returns all the files that have been successfully uploaded provided an
// upload id
getShareableFiles: (state, getters) => (uploadId) => {
Expand All @@ -89,7 +100,7 @@ const getters = {
},

uploadProgress: (state) => (uploadId, index) => {
if (state.uploads[uploadId].files[index]) {
if (state.uploads[uploadId]?.files[index]) {
return state.uploads[uploadId].files[index].uploadedSize / state.uploads[uploadId].files[index].totalSize * 100
} else {
return 0
Expand All @@ -112,9 +123,8 @@ const getters = {
const mutations = {

// Adds a "file to be shared to the store"
addFileToBeUploaded(state, { file, temporaryMessage, localUrl }) {
addFileToBeUploaded(state, { file, temporaryMessage, localUrl, token }) {
const uploadId = temporaryMessage.messageParameters.file.uploadId
const token = temporaryMessage.messageParameters.file.token
const index = temporaryMessage.messageParameters.file.index
// Create upload id if not present
if (!state.uploads[uploadId]) {
Expand All @@ -133,6 +143,11 @@ const mutations = {
Vue.set(state.localUrls, temporaryMessage.referenceId, localUrl)
},

// Marks a given file as initialized (for retry)
markFileAsInitializedUpload(state, { uploadId, index }) {
state.uploads[uploadId].files[index].status = 'initialised'
},

// Marks a given file as failed upload
markFileAsFailedUpload(state, { uploadId, index, status }) {
state.uploads[uploadId].files[index].status = 'failedUpload'
Expand Down Expand Up @@ -253,7 +268,7 @@ const actions = {
text: '{file}', token, uploadId, index, file, localUrl, isVoiceMessage,
})
console.debug('temporarymessage: ', temporaryMessage, 'uploadId', uploadId)
commit('addFileToBeUploaded', { file, temporaryMessage, localUrl })
commit('addFileToBeUploaded', { file, temporaryMessage, localUrl, token })
}
},

Expand Down Expand Up @@ -296,8 +311,8 @@ const actions = {

// Tag previously indexed files and add temporary messages to the MessagesList
// If caption is provided, attach to the last temporary message
const lastIndex = getters.getUploadsArray(uploadId).at(-1).at(0)
for (const [index, uploadedFile] of getters.getUploadsArray(uploadId)) {
const lastIndex = getters.getInitialisedUploads(uploadId).at(-1).at(0)
for (const [index, uploadedFile] of getters.getInitialisedUploads(uploadId)) {
// mark all files as uploading
commit('markFileAsUploading', { uploadId, index })
// Store the previously created temporary message
Expand All @@ -322,14 +337,14 @@ const actions = {
// Candidate rest of the path
const path = getters.getAttachmentFolder() + '/' + fileName

// Check if previous propfind attempt was stored
const promptPath = getFileNamePrompt(path)
const knownSuffix = knownPaths[promptPath]
// Get a unique relative path based on the previous path variable
const { uniquePath, suffix } = await findUniquePath(client, userRoot, path, knownSuffix)
knownPaths[promptPath] = suffix

try {
// Check if previous propfind attempt was stored
const promptPath = getFileNamePrompt(path)
const knownSuffix = knownPaths[promptPath]
// Get a unique relative path based on the previous path variable
const { uniquePath, suffix } = await findUniquePath(client, userRoot, path, knownSuffix)
knownPaths[promptPath] = suffix

// Upload the file
const currentFileBuffer = await new Blob([currentFile]).arrayBuffer()
await client.putFileContents(userRoot + uniquePath, currentFileBuffer, {
Expand Down Expand Up @@ -360,7 +375,7 @@ const actions = {

// Mark the upload as failed in the store
commit('markFileAsFailedUpload', { uploadId, index })
dispatch('markTemporaryMessageAsFailed', { message: uploadedFile.temporaryMessage, reason })
dispatch('markTemporaryMessageAsFailed', { message: uploadedFile.temporaryMessage, uploadId, reason })
}
}

Expand Down Expand Up @@ -391,15 +406,15 @@ const actions = {
} else {
showError(t('spreed', 'An error happened when trying to share your file'))
}
dispatch('markTemporaryMessageAsFailed', { message: temporaryMessage, reason: 'failed-share' })
dispatch('markTemporaryMessageAsFailed', { message: temporaryMessage, uploadId, reason: 'failed-share' })
console.error('An error happened when trying to share your file: ', error)
}
}

const client = getDavClient()
const userRoot = '/files/' + getters.getUserId()

const uploads = getters.getUploadsArray(uploadId)
const uploads = getters.getUploadingFiles(uploadId)
// Check for duplicate names in the uploads array
if (hasDuplicateUploadNames(uploads)) {
const { uniques, duplicates } = separateDuplicateUploads(uploads)
Expand Down Expand Up @@ -431,6 +446,30 @@ const actions = {

EventBus.$emit('upload-finished')
},

/**
* Re-initialize failed uploads and open UploadEditor dialog
* Insert caption if was provided
*
* @param {object} context default store context;
* @param {object} data payload;
* @param {string} data.uploadId the internal id of the upload;
* @param {string} [data.caption] the message caption;
*/
retryUploadFiles(context, { uploadId, caption }) {
context.getters.getFailedUploads(uploadId).forEach(([index, file]) => {
context.dispatch('removeTemporaryMessageFromStore', file.temporaryMessage)
context.commit('markFileAsInitializedUpload', { uploadId, index })
})

if (caption) {
const chatExtrasStore = useChatExtrasStore()
chatExtrasStore.setChatInput({ token: context.getters.getToken(), text: caption })
}

context.commit('setCurrentUploadId', uploadId)
},

/**
* Set the folder to store new attachments in
*
Expand Down
5 changes: 5 additions & 0 deletions src/store/fileUploadStore.spec.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { createLocalVue } from '@vue/test-utils'
import mockConsole from 'jest-mock-console'
import { cloneDeep } from 'lodash'
import { createPinia, setActivePinia } from 'pinia'
import Vuex from 'vuex'

import { showError } from '@nextcloud/dialogs'

// eslint-disable-next-line no-unused-vars -- required for testing
import storeConfig from './storeConfig.js'
// eslint-disable-next-line import/order -- required for testing
import fileUploadStore from './fileUploadStore.js'
import { getDavClient } from '../services/DavClient.js'
import { shareFile } from '../services/filesSharingServices.js'
Expand Down Expand Up @@ -39,6 +43,7 @@ describe('fileUploadStore', () => {

localVue = createLocalVue()
localVue.use(Vuex)
setActivePinia(createPinia())

mockedActions = {
createTemporaryMessage: jest.fn()
Expand Down
11 changes: 8 additions & 3 deletions src/store/messagesStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -336,11 +336,15 @@ const mutations = {
* @param {object} state current store state;
* @param {object} data the wrapping object;
* @param {object} data.message the temporary message;
* @param {string|undefined} data.uploadId the internal id of the upload;
* @param {string} data.reason the reason the temporary message failed;
*/
markTemporaryMessageAsFailed(state, { message, reason }) {
markTemporaryMessageAsFailed(state, { message, uploadId = undefined, reason }) {
if (state.messages[message.token][message.id]) {
Vue.set(state.messages[message.token][message.id], 'sendingFailure', reason)
if (uploadId) {
Vue.set(state.messages[message.token][message.id], 'uploadId', uploadId)
}
}
},

Expand Down Expand Up @@ -676,10 +680,11 @@ const actions = {
* @param {object} context default store context;
* @param {object} data the wrapping object;
* @param {object} data.message the temporary message;
* @param {string} data.uploadId the internal id of the upload;
* @param {string} data.reason the reason the temporary message failed;
*/
markTemporaryMessageAsFailed(context, { message, reason }) {
context.commit('markTemporaryMessageAsFailed', { message, reason })
markTemporaryMessageAsFailed(context, { message, uploadId, reason }) {
context.commit('markTemporaryMessageAsFailed', { message, uploadId, reason })
},

/**
Expand Down

0 comments on commit e4315c4

Please sign in to comment.