diff --git a/js/models/conversations.js b/js/models/conversations.js index dc074c6183..69066f0872 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -610,6 +610,59 @@ return _.without(this.get('members'), me); }, + blobToArrayBuffer(blob) { + return new Promise((resolve, reject) => { + const fileReader = new FileReader(); + + fileReader.onload = e => resolve(e.target.result); + fileReader.onerror = reject; + fileReader.onabort = reject; + + fileReader.readAsArrayBuffer(blob); + }); + }, + + async makeThumbnailAttachment(attachment) { + const attachmentWithData = await loadAttachmentData(attachment); + const { data, contentType } = attachmentWithData; + const objectUrl = this.makeObjectUrl(data, contentType); + const thumbnail = await Whisper.FileInputView.makeThumbnail(128, objectUrl); + URL.revokeObjectURL(objectUrl); + + const arrayBuffer = await this.blobToArrayBuffer(thumbnail); + const finalContentType = 'image/png'; + const finalObjectUrl = this.makeObjectUrl(arrayBuffer, finalContentType); + + return { + data: arrayBuffer, + objectUrl: finalObjectUrl, + contentType: finalContentType, + }; + }, + + async makeQuote(quotedMessage) { + const contact = quotedMessage.getContact(); + const attachments = quotedMessage.get('attachments'); + + return { + author: contact.id, + id: quotedMessage.get('sent_at'), + text: quotedMessage.get('body'), + attachments: await Promise.all((attachments || []).map(async (attachment) => { + const { contentType } = attachment; + const willMakeThumbnail = MIME.isImage(contentType); + + return { + contentType, + fileName: attachment.fileName, + thumbnail: willMakeThumbnail + ? await this.makeThumbnailAttachment(attachment) + : null, + }; + })), + }; + }, + sendMessage(body, attachments) { this.queueJob(async () => { const now = Date.now(); @@ -1113,18 +1166,8 @@ const queryFirst = queryAttachments[0]; try { - queryMessage.attachments[0] = await loadAttachmentData(queryFirst); - - // Note: it would be nice to take the full-size image and downsample it into - // a true thumbnail here. - queryMessage.updateImageUrl(); - - // We need to differentiate between messages we load from database and those - // already in memory. More cleanup needs to happen on messages from the database - // because they aren't tracked any other way. // eslint-disable-next-line no-param-reassign - message.quotedMessageFromDatabase = queryMessage; - + message.quoteThumbnail = await this.makeThumbnailAttachment(queryFirst); return true; } catch (error) { console.log( @@ -1155,12 +1198,9 @@ try { const queryFirst = quotedAttachments[0]; - // eslint-disable-next-line no-param-reassign - quotedMessage.attributes.attachments[0] = await loadAttachmentData(queryFirst); - // Note: it would be nice to take the full-size image and downsample it into - // a true thumbnail here. - quotedMessage.updateImageUrl(); + // eslint-disable-next-line no-param-reassign + message.quoteThumbnail = await this.makeThumbnailAttachment(queryFirst); } catch (error) { console.log( 'Problem loading attachment data for quoted message', diff --git a/js/models/messages.js b/js/models/messages.js index fd4ce25981..76b5bce692 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -188,6 +188,16 @@ if (this.quotedMessage) { this.quotedMessage = null; } + const quote = this.get('quote'); + const attachments = (quote && quote.attachments) || []; + attachments.forEach((attachment) => { + if (attachment.thumbnail && attachment.thumbnail.objectUrl) { + URL.revokeObjectURL(attachment.thumbnail.objectUrl); + // eslint-disable-next-line no-param-reassign + attachment.thumbnail.objectUrl = null; + } + }); + this.revokeImageUrl(); }, revokeImageUrl() { @@ -203,16 +213,6 @@ return this.imageUrl; }, getQuoteObjectUrl() { - const fromDB = this.quotedMessageFromDatabase; - if (fromDB && fromDB.imageUrl) { - return fromDB.imageUrl; - } - - const inMemory = this.quotedMessage; - if (inMemory && inMemory.imageUrl) { - return inMemory.imageUrl; - } - const thumbnail = this.quoteThumbnail; if (thumbnail && thumbnail.objectUrl) { return thumbnail.objectUrl; @@ -232,8 +232,11 @@ return ConversationController.get(author); }, - processAttachment(attachment, objectUrl) { - const thumbnail = !objectUrl + processAttachment(attachment, externalObjectUrl) { + const { thumbnail } = attachment; + const objectUrl = (thumbnail && thumbnail.objectUrl) || externalObjectUrl; + + const thumbnailWithObjectUrl = !objectUrl ? null : Object.assign({}, attachment.thumbnail || {}, { objectUrl, @@ -242,7 +245,7 @@ return Object.assign({}, attachment, { // eslint-disable-next-line no-bitwise isVoiceMessage: Boolean(attachment.flags & this.VOICE_FLAG), - thumbnail, + thumbnail: thumbnailWithObjectUrl, }); }, getPropsForQuote() { diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index 808d5c863e..7081551ef3 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -1065,27 +1065,24 @@ this.focusMessageField(); }, - setQuoteMessage(message) { + async setQuoteMessage(message) { + this.quote = null; this.quotedMessage = message; + + if (this.quoteHolder) { + this.quoteHolder.unload(); + this.quoteHolder = null; + } + + if (message) { + const quote = await this.model.makeQuote(this.quotedMessage); + console.log({ quote }); + this.quote = quote; + } + this.renderQuotedMessage(); }, - makeQuote(quotedMessage) { - const contact = quotedMessage.getContact(); - const attachments = quotedMessage.get('attachments'); - const first = attachments ? attachments[0] : null; - - return { - author: contact.id, - id: quotedMessage.get('sent_at'), - text: quotedMessage.get('body'), - attachments: !first ? [] : [{ - contentType: first.contentType, - fileName: first.fileName, - }], - }; - }, - renderQuotedMessage() { if (this.quoteView) { this.quoteView.remove(); @@ -1097,9 +1094,11 @@ } const message = new Whisper.Message({ - quote: this.makeQuote(this.quotedMessage), + quote: this.quote, }); message.quotedMessage = this.quotedMessage; + this.quoteHolder = message; + const props = Object.assign({}, message.getPropsForQuote(), { onClose: () => { this.setQuoteMessage(null); diff --git a/js/views/file_input_view.js b/js/views/file_input_view.js index 9e5eec9218..c3d53349a0 100644 --- a/js/views/file_input_view.js +++ b/js/views/file_input_view.js @@ -22,6 +22,41 @@ template: i18n('unsupportedFileType') }); + function makeThumbnail(size, objectUrl) { + return new Promise(function(resolve, reject) { + var img = document.createElement('img'); + img.onerror = reject; + img.onload = function () { + // using components/blueimp-load-image + + // first, make the correct size + var canvas = loadImage.scale(img, { + canvas: true, + cover: true, + maxWidth: size, + maxHeight: size, + minWidth: size, + minHeight: size, + }); + + // then crop + canvas = loadImage.scale(canvas, { + canvas: true, + crop: true, + maxWidth: size, + maxHeight: size, + minWidth: size, + minHeight: size, + }); + + var blob = window.dataURLToBlobSync(canvas.toDataURL('image/png')); + + resolve(blob); + }; + img.src = objectUrl; + }); + } + Whisper.FileInputView = Backbone.View.extend({ tagName: 'span', className: 'file-input', @@ -239,29 +274,11 @@ return Promise.resolve(); } - return new Promise(function(resolve, reject) { - var url = URL.createObjectURL(file); - var img = document.createElement('img'); - img.onerror = reject; - img.onload = function () { - URL.revokeObjectURL(url); - // loadImage.scale -> components/blueimp-load-image - // scale, then crop. - var canvas = loadImage.scale(img, { - canvas: true, maxWidth: size, maxHeight: size, - cover: true, minWidth: size, minHeight: size - }); - canvas = loadImage.scale(canvas, { - canvas: true, maxWidth: size, maxHeight: size, - crop: true, minWidth: size, minHeight: size - }); - - var blob = window.dataURLToBlobSync(canvas.toDataURL('image/png')); - - resolve(blob); - }; - img.src = url; - }).then(this.readFile); + const objectUrl = URL.createObjectURL(file); + return makeThumbnail(256, file).then(function(arrayBuffer) { + URL.revokeObjectURL(url); + return this.readFile(arrayBuffer); + }); }, // File -> Promise Attachment @@ -348,4 +365,6 @@ } } }); + + Whisper.FileInputView.makeThumbnail = makeThumbnail; })();