diff --git a/js/models/conversations.js b/js/models/conversations.js index 8d093752d..6434e7628 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -111,6 +111,11 @@ return this.id === this.ourNumber; }, + addSingleMessage(message) { + this.messageCollection.add(message, { merge: true }); + this.processQuotes(this.messageCollection); + }, + onMessageError() { this.updateVerified(); }, @@ -1030,11 +1035,13 @@ makeKey(author, id) { return `${author}-${id}`; }, - doMessagesMatch(left, right) { - if (left.get('source') !== right.get('source')) { + doesMessageMatch(id, author, message) { + const messageAuthor = message.getContact().id; + + if (author !== messageAuthor) { return false; } - if (left.get('sent_at') !== right.get('sent_at')) { + if (id !== message.get('sent_at')) { return false; } return true; @@ -1061,7 +1068,19 @@ makeMessagesLookup(messages) { return messages.reduce((acc, message) => { const { source, sent_at: sentAt } = message.attributes; - const key = this.makeKey(source, sentAt); + + // Checking for notification messages without a sender + if (!source && message.isIncoming()) { + return acc; + } + + const contact = message.getContact(); + if (!contact) { + return acc; + } + + const author = contact.id; + const key = this.makeKey(author, sentAt); acc[key] = message; @@ -1070,7 +1089,7 @@ }, async loadQuotedMessageFromDatabase(message) { const { quote } = message.attributes; - const { attachments, id } = quote; + const { attachments, id, author } = quote; const first = attachments[0]; // Maybe in the future we could try to pull the thumbnail from a video ourselves, @@ -1081,7 +1100,7 @@ const collection = new Whisper.MessageCollection(); await collection.fetchSentAt(id); - const queryMessage = collection.find(m => this.doMessagesMatch(message, m)); + const queryMessage = collection.find(m => this.doesMessageMatch(id, author, m)); if (!queryMessage) { return false; @@ -1097,8 +1116,6 @@ // Note: it would be nice to take the full-size image and downsample it into // a true thumbnail here. - // Note: if the attachment is a video, then this object URL won't make any sense - // when we try to use it in an img tag. queryMessage.updateImageUrl(); // We need to differentiate between messages we load from database and those already @@ -1110,6 +1127,36 @@ this.forceRender(message); return true; }, + async loadQuotedMessage(message, quotedMessage) { + // eslint-disable-next-line no-param-reassign + message.quotedMessage = quotedMessage; + + const { quote } = message.attributes; + const { attachments } = quote; + const first = attachments[0]; + + // Maybe in the future we could try to pull thumbnails video ourselves, + // but for now we will rely on incoming thumbnails only. + console.log({ first, contentType: first ? first.contentType : null }); + if (!first || !MIME.isImage(first.contentType)) { + return; + } + + const quotedAttachments = quotedMessage.get('attachments') || []; + console.log({ quotedMessage, quotedAttachments }); + if (quotedAttachments.length === 0) { + return; + } + + 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(); + console.log({ quotedMessage }); + }, async loadQuoteThumbnail(message) { const { quote } = message.attributes; const { attachments } = quote; @@ -1133,7 +1180,6 @@ this.forceRender(message); return true; }, - async processQuotes(messages) { const lookup = this.makeMessagesLookup(messages); @@ -1143,11 +1189,6 @@ return; } - const { attachments } = quote; - if (!this.needData(attachments)) { - return; - } - // If we already have a quoted message, then we exit early. If we don't have it, // then we'll continue to look again for an in-memory message to use. Why? This // will enable us to scroll to it when the user clicks. @@ -1162,11 +1203,18 @@ if (quotedMessage) { // eslint-disable-next-line no-param-reassign - message.quotedMessage = quotedMessage; + await this.loadQuotedMessage(message, quotedMessage); this.forceRender(message); return; } + // We only go further if we need more data for this message. It's always important + // to grab the quoted message to allow for navigating to it by clicking. + const { attachments } = quote; + if (!this.needData(attachments)) { + return; + } + // We've don't want to go to the database or load thumbnails a second time. if (message.quoteIsProcessed) { return; diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index b69a7b407..bcc70e80b 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -530,11 +530,10 @@ } }, - scrollToMessage: function(providedOptions) { - const options = providedOptions || {}; + scrollToMessage: function(options = {}) { const { id } = options; - if (id) { + if (!id) { return; } @@ -543,7 +542,7 @@ return; } - el.scrollIntoView(); + el[0].scrollIntoView(); }, scrollToBottom: function() { @@ -686,7 +685,7 @@ // This is debounced, so it won't hit the database too often. this.lazyUpdateVerified(); - this.model.messageCollection.add(message, {merge: true}); + this.model.addSingleMessage(message); message.setToExpire(); if (message.isOutgoing()) { diff --git a/js/views/message_view.js b/js/views/message_view.js index 837df7fb7..95e346d93 100644 --- a/js/views/message_view.js +++ b/js/views/message_view.js @@ -194,7 +194,7 @@ this.listenTo(this.model, 'change:delivered', this.renderDelivered); this.listenTo(this.model, 'change:read_by', this.renderRead); this.listenTo(this.model, 'change:expirationStartTimestamp', this.renderExpiring); - this.listenTo(this.model, 'change', this.renderSent); + this.listenTo(this.model, 'change', this.onChange); this.listenTo(this.model, 'change:flags change:group_update', this.renderControl); this.listenTo(this.model, 'destroy', this.onDestroy); this.listenTo(this.model, 'unload', this.onUnload); @@ -274,6 +274,10 @@ } this.onUnload(); }, + onChange() { + this.renderRead(); + this.renderQuote(); + }, select(e) { this.$el.trigger('select', { message: this.model }); e.stopPropagation(); @@ -379,17 +383,19 @@ return null; }, renderQuote() { - const VOICE_FLAG = textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE; - const objectUrl = this.getQuoteObjectUrl(); const quote = this.model.get('quote'); if (!quote) { return; } + const VOICE_FLAG = textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE; + const objectUrl = this.getQuoteObjectUrl(); + + function processAttachment(attachment) { - const thumbnail = !attachment.thumbnail + const thumbnail = !objectUrl ? null - : Object.assign({}, attachment.thumbnail, { + : Object.assign({}, attachment.thumbnail || {}, { objectUrl, }); @@ -411,7 +417,7 @@ const isIncoming = this.model.isIncoming(); const props = { - attachments: quote.attachments && quote.attachments.map(processAttachment), + attachments: (quote.attachments || []).map(processAttachment), authorColor, authorProfileName, authorTitle, @@ -420,14 +426,13 @@ onClick: () => { const { quotedMessage } = this.model; if (quotedMessage) { - this.trigger('scroll-to-message', { id: quotedMessage.id }); + this.model.trigger('scroll-to-message', { id: quotedMessage.id }); } }, text: quote.text, }; if (this.replyView) { - this.replyView.remove(); this.replyView = null; } else if (contact) { this.listenTo(contact, 'change:color', this.renderQuote);