Copy quoted message contents into quote on receipt
Also: - visually distinguish any reference we couldn't verify on receipt - show toast on quote click if we can't scroll to message - toast visuals redesigned to match rest of app
This commit is contained in:
parent
a247ffe5cf
commit
fedfbed304
15 changed files with 468 additions and 336 deletions
|
@ -973,7 +973,9 @@
|
|||
return event.confirm();
|
||||
}
|
||||
|
||||
const upgradedMessage = await upgradeMessageSchema(data.message);
|
||||
const withQuoteReference = await copyFromQuotedMessage(data.message);
|
||||
const upgradedMessage = await upgradeMessageSchema(withQuoteReference);
|
||||
|
||||
await ConversationController.getOrCreateAndWait(
|
||||
messageDescriptor.id,
|
||||
messageDescriptor.type
|
||||
|
@ -984,6 +986,80 @@
|
|||
};
|
||||
}
|
||||
|
||||
async function copyFromQuotedMessage(message) {
|
||||
const { quote } = message;
|
||||
if (!quote) {
|
||||
return message;
|
||||
}
|
||||
|
||||
const { attachments, id, author } = quote;
|
||||
const firstAttachment = attachments[0];
|
||||
|
||||
const collection = await window.Signal.Data.getMessagesBySentAt(id, {
|
||||
MessageCollection: Whisper.MessageCollection,
|
||||
});
|
||||
const queryMessage = collection.find(item => {
|
||||
const messageAuthor = item.getContact();
|
||||
|
||||
return messageAuthor && author === messageAuthor.id;
|
||||
});
|
||||
|
||||
if (!queryMessage) {
|
||||
quote.referencedMessageNotFound = true;
|
||||
return message;
|
||||
}
|
||||
|
||||
quote.text = queryMessage.get('body');
|
||||
if (firstAttachment) {
|
||||
firstAttachment.thumbnail = null;
|
||||
}
|
||||
|
||||
if (
|
||||
!firstAttachment ||
|
||||
(!window.Signal.Util.GoogleChrome.isImageTypeSupported(
|
||||
firstAttachment.contentType
|
||||
) &&
|
||||
!window.Signal.Util.GoogleChrome.isVideoTypeSupported(
|
||||
firstAttachment.contentType
|
||||
))
|
||||
) {
|
||||
return message;
|
||||
}
|
||||
|
||||
try {
|
||||
if (queryMessage.get('schemaVersion') < Message.CURRENT_SCHEMA_VERSION) {
|
||||
const upgradedMessage = await upgradeMessageSchema(
|
||||
queryMessage.attributes
|
||||
);
|
||||
queryMessage.set(upgradedMessage);
|
||||
await window.Signal.Data.saveMessage(upgradedMessage, {
|
||||
Message: Whisper.Message,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'Problem upgrading message quoted message from database',
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
return message;
|
||||
}
|
||||
|
||||
const queryAttachments = queryMessage.get('attachments') || [];
|
||||
|
||||
if (queryAttachments.length === 0) {
|
||||
return message;
|
||||
}
|
||||
|
||||
const queryFirst = queryAttachments[0];
|
||||
const { thumbnail } = queryFirst;
|
||||
|
||||
if (thumbnail && thumbnail.path) {
|
||||
firstAttachment.thumbnail = thumbnail;
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
// Received:
|
||||
async function handleMessageReceivedProfileUpdate({
|
||||
data,
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
window.Whisper = window.Whisper || {};
|
||||
|
||||
const { Util } = window.Signal;
|
||||
const { GoogleChrome } = Util;
|
||||
const {
|
||||
Conversation,
|
||||
Contact,
|
||||
|
@ -189,7 +188,6 @@
|
|||
addSingleMessage(message) {
|
||||
const model = this.messageCollection.add(message, { merge: true });
|
||||
model.setToExpire();
|
||||
this.processQuotes(this.messageCollection);
|
||||
return model;
|
||||
},
|
||||
|
||||
|
@ -1272,244 +1270,6 @@
|
|||
});
|
||||
},
|
||||
|
||||
makeKey(author, id) {
|
||||
return `${author}-${id}`;
|
||||
},
|
||||
doesMessageMatch(id, author, message) {
|
||||
const messageAuthor = message.getContact().id;
|
||||
|
||||
if (author !== messageAuthor) {
|
||||
return false;
|
||||
}
|
||||
if (id !== message.get('sent_at')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
needData(attachments) {
|
||||
if (!attachments || attachments.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const first = attachments[0];
|
||||
const { thumbnail, contentType } = first;
|
||||
|
||||
return (
|
||||
thumbnail ||
|
||||
GoogleChrome.isImageTypeSupported(contentType) ||
|
||||
GoogleChrome.isVideoTypeSupported(contentType)
|
||||
);
|
||||
},
|
||||
forceRender(message) {
|
||||
message.trigger('change', message);
|
||||
},
|
||||
makeMessagesLookup(messages) {
|
||||
return messages.reduce((acc, message) => {
|
||||
const { source, sent_at: sentAt } = message.attributes;
|
||||
|
||||
// Checking for notification messages (safety number change, timer change)
|
||||
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;
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
},
|
||||
async loadQuotedMessageFromDatabase(message) {
|
||||
const { quote } = message.attributes;
|
||||
const { attachments, id, author } = quote;
|
||||
const first = attachments[0];
|
||||
|
||||
if (!first || message.quoteThumbnail) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
!GoogleChrome.isImageTypeSupported(first.contentType) &&
|
||||
!GoogleChrome.isVideoTypeSupported(first.contentType)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const collection = await window.Signal.Data.getMessagesBySentAt(id, {
|
||||
MessageCollection: Whisper.MessageCollection,
|
||||
});
|
||||
const queryMessage = collection.find(m =>
|
||||
this.doesMessageMatch(id, author, m)
|
||||
);
|
||||
|
||||
if (!queryMessage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (
|
||||
queryMessage.get('schemaVersion') < Message.CURRENT_SCHEMA_VERSION
|
||||
) {
|
||||
const upgradedMessage = await upgradeMessageSchema(
|
||||
queryMessage.attributes
|
||||
);
|
||||
queryMessage.set(upgradedMessage);
|
||||
await window.Signal.Data.saveMessage(upgradedMessage, {
|
||||
Message: Whisper.Message,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'Problem upgrading message quoted message from database',
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const queryAttachments = queryMessage.attachments || [];
|
||||
if (queryAttachments.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const queryFirst = queryAttachments[0];
|
||||
const { thumbnail } = queryFirst;
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
message.quoteThumbnail = {
|
||||
...thumbnail,
|
||||
objectUrl: getAbsoluteAttachmentPath(thumbnail.path),
|
||||
};
|
||||
|
||||
return true;
|
||||
},
|
||||
loadQuotedMessage(message, quotedMessage) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
message.quotedMessage = quotedMessage;
|
||||
|
||||
const { quote } = message.attributes;
|
||||
const { attachments } = quote;
|
||||
const first = attachments[0];
|
||||
|
||||
if (!first || message.quoteThumbnail) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!GoogleChrome.isImageTypeSupported(first.contentType) &&
|
||||
!GoogleChrome.isVideoTypeSupported(first.contentType)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const quotedAttachments = quotedMessage.get('attachments') || [];
|
||||
if (quotedAttachments.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const queryFirst = quotedAttachments[0];
|
||||
const { thumbnail } = queryFirst;
|
||||
|
||||
if (!thumbnail) {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
message.quoteThumbnail = {
|
||||
...thumbnail,
|
||||
objectUrl: getAbsoluteAttachmentPath(thumbnail.path),
|
||||
};
|
||||
},
|
||||
loadQuoteThumbnail(message) {
|
||||
const { quote } = message.attributes;
|
||||
const { attachments } = quote;
|
||||
const first = attachments[0];
|
||||
|
||||
if (!first || message.quoteThumbnail) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { thumbnail } = first;
|
||||
|
||||
if (!thumbnail) {
|
||||
return false;
|
||||
}
|
||||
// If we update this data in place, there's the risk that this data could be
|
||||
// saved back to the database
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
message.quoteThumbnail = {
|
||||
...thumbnail,
|
||||
objectUrl: getAbsoluteAttachmentPath(thumbnail.path),
|
||||
};
|
||||
|
||||
return true;
|
||||
},
|
||||
async processQuotes(messages) {
|
||||
const lookup = this.makeMessagesLookup(messages);
|
||||
|
||||
const promises = messages.map(async message => {
|
||||
const { quote } = message.attributes;
|
||||
if (!quote) {
|
||||
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.
|
||||
if (message.quotedMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Load provided thumbnail
|
||||
const gotThumbnail = this.loadQuoteThumbnail(message, quote);
|
||||
|
||||
// 2. Check to see if we've already loaded the target message into memory
|
||||
const { author, id } = quote;
|
||||
const key = this.makeKey(author, id);
|
||||
const quotedMessage = lookup[key];
|
||||
|
||||
if (quotedMessage) {
|
||||
this.loadQuotedMessage(message, quotedMessage);
|
||||
this.forceRender(message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Even if we got the thumbnail locall, we wanted to populate the referenced
|
||||
// message so a click can navigate to it.
|
||||
if (gotThumbnail) {
|
||||
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;
|
||||
}
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
message.quoteIsProcessed = true;
|
||||
|
||||
// 3. As a last resort, go to the database to generate a thumbnail on-demand
|
||||
const loaded = await this.loadQuotedMessageFromDatabase(message, id);
|
||||
if (loaded) {
|
||||
this.forceRender(message);
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
},
|
||||
|
||||
async upgradeMessages(messages) {
|
||||
for (let max = messages.length, i = 0; i < max; i += 1) {
|
||||
const message = messages.at(i);
|
||||
|
@ -1558,10 +1318,6 @@
|
|||
);
|
||||
}
|
||||
|
||||
// We kick this process off, but don't wait for it. If async updates happen on a
|
||||
// given Message, 'change' will be triggered
|
||||
this.processQuotes(this.messageCollection);
|
||||
|
||||
this.inProgressFetch = null;
|
||||
},
|
||||
|
||||
|
|
|
@ -233,41 +233,7 @@
|
|||
this.quotedMessage = null;
|
||||
}
|
||||
},
|
||||
getQuoteObjectUrl() {
|
||||
const thumbnail = this.quoteThumbnail;
|
||||
if (!thumbnail || !thumbnail.objectUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return thumbnail.objectUrl;
|
||||
},
|
||||
getQuoteContact() {
|
||||
const quote = this.get('quote');
|
||||
if (!quote) {
|
||||
return null;
|
||||
}
|
||||
const { author } = quote;
|
||||
if (!author) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ConversationController.get(author);
|
||||
},
|
||||
processQuoteAttachment(attachment, externalObjectUrl) {
|
||||
const { thumbnail } = attachment;
|
||||
const objectUrl = (thumbnail && thumbnail.objectUrl) || externalObjectUrl;
|
||||
|
||||
const thumbnailWithObjectUrl = !objectUrl
|
||||
? null
|
||||
: Object.assign({}, attachment.thumbnail || {}, {
|
||||
objectUrl,
|
||||
});
|
||||
|
||||
return Object.assign({}, attachment, {
|
||||
isVoiceMessage: Signal.Types.Attachment.isVoiceMessage(attachment),
|
||||
thumbnail: thumbnailWithObjectUrl,
|
||||
});
|
||||
},
|
||||
getPropsForTimerNotification() {
|
||||
const { expireTimer, fromSync, source } = this.get(
|
||||
'expirationTimerUpdate'
|
||||
|
@ -535,15 +501,34 @@
|
|||
hasSignalAccount: window.hasSignalAccount(firstNumber),
|
||||
});
|
||||
},
|
||||
processQuoteAttachment(attachment) {
|
||||
const { thumbnail } = attachment;
|
||||
const path =
|
||||
thumbnail &&
|
||||
thumbnail.path &&
|
||||
getAbsoluteAttachmentPath(thumbnail.path);
|
||||
const objectUrl = thumbnail && thumbnail.objectUrl;
|
||||
|
||||
const thumbnailWithObjectUrl =
|
||||
!path && !objectUrl
|
||||
? null
|
||||
: Object.assign({}, attachment.thumbnail || {}, {
|
||||
objectUrl: path || objectUrl,
|
||||
});
|
||||
|
||||
return Object.assign({}, attachment, {
|
||||
isVoiceMessage: Signal.Types.Attachment.isVoiceMessage(attachment),
|
||||
thumbnail: thumbnailWithObjectUrl,
|
||||
});
|
||||
},
|
||||
getPropsForQuote() {
|
||||
const quote = this.get('quote');
|
||||
if (!quote) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const objectUrl = this.getQuoteObjectUrl();
|
||||
const { author } = quote;
|
||||
const contact = this.getQuoteContact();
|
||||
const { author, id, referencedMessageNotFound } = quote;
|
||||
const contact = author && ConversationController.get(author);
|
||||
|
||||
const authorPhoneNumber = author;
|
||||
const authorProfileName = contact ? contact.getProfileName() : null;
|
||||
|
@ -551,10 +536,11 @@
|
|||
const authorColor = contact ? contact.getColor() : 'grey';
|
||||
const isFromMe = contact ? contact.id === this.OUR_NUMBER : false;
|
||||
const onClick = () => {
|
||||
const { quotedMessage } = this;
|
||||
if (quotedMessage) {
|
||||
this.trigger('scroll-to-message', { id: quotedMessage.id });
|
||||
}
|
||||
this.trigger('scroll-to-message', {
|
||||
author,
|
||||
id,
|
||||
referencedMessageNotFound,
|
||||
});
|
||||
};
|
||||
|
||||
const firstAttachment = quote.attachments && quote.attachments[0];
|
||||
|
@ -562,14 +548,15 @@
|
|||
return {
|
||||
text: this.createNonBreakingLastSeparator(quote.text),
|
||||
attachment: firstAttachment
|
||||
? this.processQuoteAttachment(firstAttachment, objectUrl)
|
||||
? this.processQuoteAttachment(firstAttachment)
|
||||
: null,
|
||||
isFromMe,
|
||||
authorPhoneNumber,
|
||||
authorProfileName,
|
||||
authorName,
|
||||
authorColor,
|
||||
onClick: this.quotedMessage ? onClick : null,
|
||||
onClick,
|
||||
referencedMessageNotFound,
|
||||
};
|
||||
},
|
||||
getPropsForAttachment(attachment) {
|
||||
|
@ -799,6 +786,19 @@
|
|||
|
||||
return ConversationController.getOrCreate(source, 'private');
|
||||
},
|
||||
getQuoteContact() {
|
||||
const quote = this.get('quote');
|
||||
if (!quote) {
|
||||
return null;
|
||||
}
|
||||
const { author } = quote;
|
||||
if (!author) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ConversationController.get(author);
|
||||
},
|
||||
|
||||
getSource() {
|
||||
if (this.isIncoming()) {
|
||||
return this.get('source');
|
||||
|
|
|
@ -206,7 +206,7 @@ exports._mapQuotedAttachments = upgradeAttachment => async (
|
|||
return attachment;
|
||||
}
|
||||
|
||||
if (!thumbnail.data) {
|
||||
if (!thumbnail.data && !thumbnail.path) {
|
||||
logger.warn('Quoted attachment did not have thumbnail data; removing it');
|
||||
return omit(attachment, ['thumbnail']);
|
||||
}
|
||||
|
|
|
@ -34,6 +34,21 @@
|
|||
return { toastMessage: i18n('youLeftTheGroup') };
|
||||
},
|
||||
});
|
||||
Whisper.OriginalNotFoundToast = Whisper.ToastView.extend({
|
||||
render_attributes() {
|
||||
return { toastMessage: i18n('originalMessageNotFound') };
|
||||
},
|
||||
});
|
||||
Whisper.OriginalNoLongerAvailableToast = Whisper.ToastView.extend({
|
||||
render_attributes() {
|
||||
return { toastMessage: i18n('originalMessageNotAvailable') };
|
||||
},
|
||||
});
|
||||
Whisper.FoundButNotLoadedToast = Whisper.ToastView.extend({
|
||||
render_attributes() {
|
||||
return { toastMessage: i18n('messageFoundButNotLoaded') };
|
||||
},
|
||||
});
|
||||
|
||||
Whisper.ConversationLoadingScreen = Whisper.View.extend({
|
||||
templateName: 'conversation-loading-screen',
|
||||
|
@ -566,15 +581,66 @@
|
|||
}
|
||||
},
|
||||
|
||||
scrollToMessage(options = {}) {
|
||||
const { id } = options;
|
||||
async scrollToMessage(options = {}) {
|
||||
const { author, id, referencedMessageNotFound } = options;
|
||||
|
||||
if (!id) {
|
||||
// For simplicity's sake, we show the 'not found' toast no matter what if we were
|
||||
// not able to find the referenced message when the quote was received.
|
||||
if (referencedMessageNotFound) {
|
||||
const toast = new Whisper.OriginalNotFoundToast();
|
||||
toast.$el.appendTo(this.$el);
|
||||
toast.render();
|
||||
return;
|
||||
}
|
||||
|
||||
const el = this.$(`#${id}`);
|
||||
// Look for message in memory first, which would tell us if we could scroll to it
|
||||
const targetMessage = this.model.messageCollection.find(item => {
|
||||
const messageAuthor = item.getContact().id;
|
||||
|
||||
if (author !== messageAuthor) {
|
||||
return false;
|
||||
}
|
||||
if (id !== item.get('sent_at')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// If there's no message already in memory, we won't be scrolling. So we'll gather
|
||||
// some more information then show an informative toast to the user.
|
||||
if (!targetMessage) {
|
||||
const collection = await window.Signal.Data.getMessagesBySentAt(id, {
|
||||
MessageCollection: Whisper.MessageCollection,
|
||||
});
|
||||
const messageFromDatabase = collection.find(item => {
|
||||
const messageAuthor = item.getContact();
|
||||
|
||||
return messageAuthor && author === messageAuthor.id;
|
||||
});
|
||||
|
||||
if (messageFromDatabase) {
|
||||
const toast = new Whisper.FoundButNotLoadedToast();
|
||||
toast.$el.appendTo(this.$el);
|
||||
toast.render();
|
||||
} else {
|
||||
const toast = new Whisper.OriginalNoLongerAvailableToast();
|
||||
toast.$el.appendTo(this.$el);
|
||||
toast.render();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const databaseId = targetMessage.id;
|
||||
const el = this.$(`#${databaseId}`);
|
||||
if (!el || el.length === 0) {
|
||||
const toast = new Whisper.OriginalNoLongerAvailableToast();
|
||||
toast.$el.appendTo(this.$el);
|
||||
toast.render();
|
||||
|
||||
window.log.info(
|
||||
`Error: had target message ${id} in messageCollection, but it was not in DOM`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1375,7 +1441,7 @@
|
|||
}
|
||||
|
||||
if (toast) {
|
||||
toast.$el.insertAfter(this.$el);
|
||||
toast.$el.appendTo(this.$el);
|
||||
toast.render();
|
||||
this.focusMessageFieldAndClearDisabled();
|
||||
return;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue