Make thumbnails on quote load and on quote preview creation

This commit is contained in:
Scott Nonnenberg 2018-04-18 18:25:55 -07:00
parent 37cac717cb
commit 13ce056830
No known key found for this signature in database
GPG key ID: 5F82280C35134661
4 changed files with 131 additions and 70 deletions

View file

@ -610,6 +610,59 @@
return _.without(this.get('members'), me); 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) { sendMessage(body, attachments) {
this.queueJob(async () => { this.queueJob(async () => {
const now = Date.now(); const now = Date.now();
@ -1113,18 +1166,8 @@
const queryFirst = queryAttachments[0]; const queryFirst = queryAttachments[0];
try { 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 // eslint-disable-next-line no-param-reassign
message.quotedMessageFromDatabase = queryMessage; message.quoteThumbnail = await this.makeThumbnailAttachment(queryFirst);
return true; return true;
} catch (error) { } catch (error) {
console.log( console.log(
@ -1155,12 +1198,9 @@
try { try {
const queryFirst = quotedAttachments[0]; 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 // eslint-disable-next-line no-param-reassign
// a true thumbnail here. message.quoteThumbnail = await this.makeThumbnailAttachment(queryFirst);
quotedMessage.updateImageUrl();
} catch (error) { } catch (error) {
console.log( console.log(
'Problem loading attachment data for quoted message', 'Problem loading attachment data for quoted message',

View file

@ -188,6 +188,16 @@
if (this.quotedMessage) { if (this.quotedMessage) {
this.quotedMessage = null; 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(); this.revokeImageUrl();
}, },
revokeImageUrl() { revokeImageUrl() {
@ -203,16 +213,6 @@
return this.imageUrl; return this.imageUrl;
}, },
getQuoteObjectUrl() { 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; const thumbnail = this.quoteThumbnail;
if (thumbnail && thumbnail.objectUrl) { if (thumbnail && thumbnail.objectUrl) {
return thumbnail.objectUrl; return thumbnail.objectUrl;
@ -232,8 +232,11 @@
return ConversationController.get(author); return ConversationController.get(author);
}, },
processAttachment(attachment, objectUrl) { processAttachment(attachment, externalObjectUrl) {
const thumbnail = !objectUrl const { thumbnail } = attachment;
const objectUrl = (thumbnail && thumbnail.objectUrl) || externalObjectUrl;
const thumbnailWithObjectUrl = !objectUrl
? null ? null
: Object.assign({}, attachment.thumbnail || {}, { : Object.assign({}, attachment.thumbnail || {}, {
objectUrl, objectUrl,
@ -242,7 +245,7 @@
return Object.assign({}, attachment, { return Object.assign({}, attachment, {
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
isVoiceMessage: Boolean(attachment.flags & this.VOICE_FLAG), isVoiceMessage: Boolean(attachment.flags & this.VOICE_FLAG),
thumbnail, thumbnail: thumbnailWithObjectUrl,
}); });
}, },
getPropsForQuote() { getPropsForQuote() {

View file

@ -1065,27 +1065,24 @@
this.focusMessageField(); this.focusMessageField();
}, },
setQuoteMessage(message) { async setQuoteMessage(message) {
this.quote = null;
this.quotedMessage = message; 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(); 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() { renderQuotedMessage() {
if (this.quoteView) { if (this.quoteView) {
this.quoteView.remove(); this.quoteView.remove();
@ -1097,9 +1094,11 @@
} }
const message = new Whisper.Message({ const message = new Whisper.Message({
quote: this.makeQuote(this.quotedMessage), quote: this.quote,
}); });
message.quotedMessage = this.quotedMessage; message.quotedMessage = this.quotedMessage;
this.quoteHolder = message;
const props = Object.assign({}, message.getPropsForQuote(), { const props = Object.assign({}, message.getPropsForQuote(), {
onClose: () => { onClose: () => {
this.setQuoteMessage(null); this.setQuoteMessage(null);

View file

@ -22,6 +22,41 @@
template: i18n('unsupportedFileType') 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({ Whisper.FileInputView = Backbone.View.extend({
tagName: 'span', tagName: 'span',
className: 'file-input', className: 'file-input',
@ -239,29 +274,11 @@
return Promise.resolve(); return Promise.resolve();
} }
return new Promise(function(resolve, reject) { const objectUrl = URL.createObjectURL(file);
var url = URL.createObjectURL(file); return makeThumbnail(256, file).then(function(arrayBuffer) {
var img = document.createElement('img'); URL.revokeObjectURL(url);
img.onerror = reject; return this.readFile(arrayBuffer);
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);
}, },
// File -> Promise Attachment // File -> Promise Attachment
@ -348,4 +365,6 @@
} }
} }
}); });
Whisper.FileInputView.makeThumbnail = makeThumbnail;
})(); })();