diff --git a/frontend/controller/actions/identity.js b/frontend/controller/actions/identity.js index 1d4022722..723a4b256 100644 --- a/frontend/controller/actions/identity.js +++ b/frontend/controller/actions/identity.js @@ -9,7 +9,7 @@ import { import { cloneDeep, has, omit } from '@model/contracts/shared/giLodash.js' import { SETTING_CHELONIA_STATE } from '@model/database.js' import sbp from '@sbp/sbp' -import { imageUpload, objectURLtoBlob, compressImage } from '@utils/image.js' +import { imageUpload, objectURLtoBlob } from '@utils/image.js' import { SETTING_CURRENT_USER } from '~/frontend/model/database.js' import { JOINED_CHATROOM, KV_QUEUE, LOGIN, LOGOUT } from '~/frontend/utils/events.js' import { GIMessage } from '~/shared/domains/chelonia/GIMessage.js' @@ -596,28 +596,17 @@ export default (sbp('sbp/selectors/register', { const { identityContractID } = sbp('state/vuex/state').loggedIn try { const attachmentsData = await Promise.all(attachments.map(async (attachment) => { - const { url, needsImageCompression } = attachment + const { url, compressedBlob } = attachment // url here is an instance of URL.createObjectURL(), which needs to be converted to a 'Blob' - const attachmentBlob = needsImageCompression - ? await compressImage(url) - : await objectURLtoBlob(url) - - if (needsImageCompression) { - // Update the attachment details to reflect the compressed image. - const fileNameWithoutExtension = attachment.name.split('.').slice(0, -1).join('.') - const extension = attachmentBlob.type.split('/')[1] - - attachment.mimeType = attachmentBlob.type - attachment.name = `${fileNameWithoutExtension}.${extension}` - attachment.size = attachmentBlob.size - } + const attachmentBlob = compressedBlob || await objectURLtoBlob(url) + const response = await sbp('chelonia/fileUpload', attachmentBlob, { type: attachment.mimeType, cipher: 'aes256gcm' }, { billableContractID }) const { delete: token, download: downloadData } = response return { - attributes: omit(attachment, ['url', 'needsImageCompression']), + attributes: omit(attachment, ['url', 'compressedBlob', 'needsImageCompression']), downloadData, deleteData: { token } } diff --git a/frontend/utils/events.js b/frontend/utils/events.js index a27b18334..42ba6c4d2 100644 --- a/frontend/utils/events.js +++ b/frontend/utils/events.js @@ -25,6 +25,8 @@ export const LEFT_GROUP = 'left-group' export const JOINED_CHATROOM = 'joined-chatroom' export const LEFT_CHATROOM = 'left-chatroom' export const DELETED_CHATROOM = 'deleted-chatroom' +export const DELETE_ATTACHMENT = 'delete-attachment' +export const DELETE_ATTACHMENT_FEEDBACK = 'delete-attachment-complete' export const REPLACED_STATE = 'replaced-state' diff --git a/frontend/views/containers/chatroom/ChatMain.vue b/frontend/views/containers/chatroom/ChatMain.vue index 73c978bed..637cbca5f 100644 --- a/frontend/views/containers/chatroom/ChatMain.vue +++ b/frontend/views/containers/chatroom/ChatMain.vue @@ -138,11 +138,12 @@ import Emoticons from './Emoticons.vue' import TouchLinkHelper from './TouchLinkHelper.vue' import DragActiveOverlay from './file-attachment/DragActiveOverlay.vue' import { MESSAGE_TYPES, MESSAGE_VARIANTS, CHATROOM_ACTIONS_PER_PAGE } from '@model/contracts/shared/constants.js' -import { CHATROOM_EVENTS, NEW_CHATROOM_UNREAD_POSITION } from '@utils/events.js' +import { CHATROOM_EVENTS, NEW_CHATROOM_UNREAD_POSITION, DELETE_ATTACHMENT_FEEDBACK } from '@utils/events.js' import { findMessageIdx } from '@model/contracts/shared/functions.js' import { proximityDate, MINS_MILLIS } from '@model/contracts/shared/time.js' import { cloneDeep, debounce, throttle, delay } from '@model/contracts/shared/giLodash.js' import { EVENT_HANDLED } from '~/shared/domains/chelonia/events.js' +import { compressImage } from '@utils/image.js' const ignorableScrollDistanceInPixel = 500 @@ -455,6 +456,7 @@ export default ({ } const uploadAttachments = async () => { try { + attachments = await this.checkAndCompressImages(attachments) data.attachments = await sbp('gi.actions/identity/uploadFiles', { attachments, billableContractID: contractID @@ -509,6 +511,26 @@ export default ({ }) } }, + checkAndCompressImages (attachments) { + return Promise.all( + attachments.map(async attachment => { + if (attachment.needsImageCompression) { + const compressedImageBlob = await compressImage(attachment.url) + const fileNameWithoutExtension = attachment.name.split('.').slice(0, -1).join('.') + const extension = compressedImageBlob.type.split('/')[1] + + return { + ...attachment, + mimeType: compressedImageBlob.type, + name: `${fileNameWithoutExtension}.${extension}`, + size: compressedImageBlob.size, + url: URL.createObjectURL(compressedImageBlob), + compressedBlob: compressedImageBlob + } + } else { return attachment } + }) + ) + }, async scrollToMessage (messageHash, effect = true) { if (!messageHash || !this.messages.length) { return @@ -685,12 +707,21 @@ export default ({ } const primaryButtonSelected = await sbp('gi.ui/prompt', promptConfig) + const sendDeleteAttachmentFeedback = (action) => { + // Delete attachment action can lead to 'success', 'error' or can be cancelled by user. + sbp('okTurtles.events/emit', DELETE_ATTACHMENT_FEEDBACK, { action, manifestCid }) + } if (primaryButtonSelected) { const data = { hash, manifestCid, messageSender: from } - sbp('gi.actions/chatroom/deleteAttachment', { contractID, data }).catch((e) => { - console.error(`Error while deleting attachment(${manifestCid}) of message(${hash}) for chatroom(${contractID})`, e) - }) + sbp('gi.actions/chatroom/deleteAttachment', { contractID, data }) + .then(() => sendDeleteAttachmentFeedback('complete')) + .catch((e) => { + console.error(`Error while deleting attachment(${manifestCid}) of message(${hash}) for chatroom(${contractID})`, e) + sendDeleteAttachmentFeedback('error') + }) + } else { + sendDeleteAttachmentFeedback('cancel') } }, changeDay (index) { diff --git a/frontend/views/containers/chatroom/file-attachment/ChatAttachmentPreview.vue b/frontend/views/containers/chatroom/file-attachment/ChatAttachmentPreview.vue index 39af0e8b1..6004c2694 100644 --- a/frontend/views/containers/chatroom/file-attachment/ChatAttachmentPreview.vue +++ b/frontend/views/containers/chatroom/file-attachment/ChatAttachmentPreview.vue @@ -52,7 +52,7 @@ ) button.is-icon-small( :aria-label='L("Delete")' - @click='deleteAttachment(entryIndex)' + @click='deleteAttachment({ index: entryIndex })' ) i.icon-trash-alt @@ -95,9 +95,8 @@ import Tooltip from '@components/Tooltip.vue' import { MESSAGE_VARIANTS } from '@model/contracts/shared/constants.js' import { getFileExtension, getFileType, formatBytesDecimal } from '@view-utils/filters.js' import { Secret } from '~/shared/domains/chelonia/Secret.js' -import { OPEN_MODAL } from '@utils/events.js' +import { OPEN_MODAL, DELETE_ATTACHMENT } from '@utils/events.js' import { L } from '@common/common.js' -import { randomHexString } from '@model/contracts/shared/giLodash.js' export default { name: 'ChatAttachmentPreview', @@ -152,6 +151,13 @@ export default { return this.getStretchedDimension(attachment.dimension) }) } + + sbp('okTurtles.events/on', DELETE_ATTACHMENT, this.deleteAttachment) + } + }, + beforeDestroy () { + if (this.shouldPreviewImages) { + sbp('okTurtles.events/off', DELETE_ATTACHMENT, this.deleteAttachment) } }, methods: { @@ -169,10 +175,16 @@ export default { fileType ({ mimeType }) { return getFileType(mimeType) }, - deleteAttachment (index) { - const attachment = this.attachmentList[index] - if (attachment.downloadData) { - this.$emit('delete-attachment', attachment.downloadData.manifestCid) + deleteAttachment ({ index, url }) { + if (url) { + index = this.objectURLList.indexOf(url) + } + + if (index >= 0) { + const attachment = this.attachmentList[index] + if (attachment.downloadData) { + this.$emit('delete-attachment', attachment.downloadData.manifestCid) + } } }, async getAttachmentObjectURL (attachment) { @@ -229,12 +241,15 @@ export default { const allImageAttachments = this.attachmentList.filter(entry => this.fileType(entry) === 'image') .map((entry, index) => { + const imgUrl = entry.url || this.objectURLList[index] || '' return { name: entry.name, ownerID: this.ownerID, - imgUrl: entry.url || this.objectURLList[index] || '', createdAt: this.createdAt || new Date(), - id: randomHexString(12) + size: entry.size, + id: imgUrl, + imgUrl, + manifestCid: entry.downloadData?.manifestCid } }) const initialIndex = allImageAttachments.findIndex(attachment => attachment.imgUrl === objectURL) @@ -244,7 +259,8 @@ export default { null, { images: allImageAttachments, - initialIndex: initialIndex === -1 ? 0 : initialIndex + initialIndex: initialIndex === -1 ? 0 : initialIndex, + canDelete: this.isMsgSender || this.isGroupCreator // delete-attachment action can only be performed by the sender or the group creator } ) } diff --git a/frontend/views/containers/chatroom/image-viewer/ImageViewerModal.vue b/frontend/views/containers/chatroom/image-viewer/ImageViewerModal.vue index 461e7a83f..60078f517 100644 --- a/frontend/views/containers/chatroom/image-viewer/ImageViewerModal.vue +++ b/frontend/views/containers/chatroom/image-viewer/ImageViewerModal.vue @@ -7,6 +7,10 @@ :key='currentImage.id' :img-src='currentImage.imgUrl' :name='currentImage.name' + :can-delete='canDelete' + :deleting='deletingCurrentImage' + @download='downloadImage' + @delete-attachment='deleteAttachment' ) header.c-modal-header @@ -18,7 +22,9 @@ .c-img-data .c-name.has-ellipsis {{ displayName }} - .c-filename.has-ellipsis {{ currentImage.name }} + .c-filename-and-size + .c-filename.has-ellipsis {{ currentImage.name }} + .c-file-size {{ displayFilesize(currentImage.size) }} button.is-icon-small.c-close-btn( type='button' @@ -39,15 +45,21 @@ @click='selectNextImage' ) i.icon-chevron-right + + a.c-invisible-link( + ref='downloadHelper' + @click.stop='' + )