diff --git a/_locales/en/messages.json b/_locales/en/messages.json index c89d62f92..8d9d0bdba 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -695,19 +695,20 @@ "description": "When rendering an address, used to provide context to a post office box" }, - "replyToMessage": { - "message": "Reply to Message", + "originalMessageNotFound": { + "message": "Original message not found", "description": - "Shown in triple-dot menu next to message to allow user to start crafting a message with a quotation" + "Shown in quote if reference message was not found as message was initially downloaded and processed" }, - "replyingToYourself": { - "message": "Replying to Yourself", - "description": "Shown in iOS theme when you quote yourself" + "originalMessageNotAvailable": { + "message": "Original message no longer available", + "description": + "Shown in toast if user clicks on quote that references message no longer in database" }, - "replyingToYou": { - "message": "Replying to You", + "messageFoundButNotLoaded": { + "message": "Original message found, but not loaded. Scroll up to load it.", "description": - "Shown in iOS theme when someone else quotes a message from you" + "Shown in toast if user clicks on quote references messages not loaded in view, but in database" }, "you": { "message": "You", diff --git a/images/broken-link.svg b/images/broken-link.svg new file mode 100644 index 000000000..73f3fd05d --- /dev/null +++ b/images/broken-link.svg @@ -0,0 +1,12 @@ + + + + Link/broken-link-16 + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/js/background.js b/js/background.js index ea2d0627f..b9f6277d9 100644 --- a/js/background.js +++ b/js/background.js @@ -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, diff --git a/js/models/conversations.js b/js/models/conversations.js index 4238ecfa3..d1af016a3 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -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; }, diff --git a/js/models/messages.js b/js/models/messages.js index d0744b7da..1a77e6bd7 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -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'); diff --git a/js/modules/types/message.js b/js/modules/types/message.js index b757e1708..d44925223 100644 --- a/js/modules/types/message.js +++ b/js/modules/types/message.js @@ -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']); } diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index 5c3792d5d..6f5eb040b 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -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; + + // 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; + } + + // 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 (!id) { + // 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 el = this.$(`#${id}`); + 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; diff --git a/stylesheets/_conversation.scss b/stylesheets/_conversation.scss index f19697942..69f0a0178 100644 --- a/stylesheets/_conversation.scss +++ b/stylesheets/_conversation.scss @@ -299,15 +299,25 @@ .toast { position: absolute; - bottom: 0; - margin: 0 2em 3em; - padding: 0.5em 1.5em; - background: rgba(0, 0, 0, 0.75); - color: $color-white; - box-shadow: 0 0 5px 0 black; - border-radius: $border-radius; - font-size: $font-size-small; + left: 50%; + transform: translate(-50%, 0); + bottom: 62px; + + text-align: center; + padding-left: 16px; + padding-right: 16px; + padding-top: 8px; + padding-bottom: 8px; + border-radius: 4px; z-index: 100; + + font-size: 13px; + line-height: 18px; + letter-spacing: 0; + + background-color: $color-light-60; + color: $color-white; + box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.12), 0 0 0 0.5px rgba(0, 0, 0, 0.08); } .confirmation-dialog { diff --git a/stylesheets/_ios.scss b/stylesheets/_ios.scss index 659f807b9..d87b25017 100644 --- a/stylesheets/_ios.scss +++ b/stylesheets/_ios.scss @@ -206,6 +206,10 @@ color: $color-light-90; } + .module-quote__reference-warning--incoming { + background-color: $color-signal-blue-mix; + } + // When you're composing a new quote .bottom-bar { .module-quote { @@ -351,6 +355,30 @@ color: $color-white; } + .module-quote__reference-warning { + background-color: $color-white-04; + } + + .module-quote__reference-warning--incoming { + background-color: $color-signal-blue-050; + } + + .module-quote__reference-warning__text { + color: $color-light-90; + } + + .module-quote__reference-warning__text--incoming { + color: $color-white; + } + + .module-quote__reference-warning__icon { + @include color-svg('../images/broken-link.svg', $color-light-60); + } + + .module-quote__reference-warning__icon--incoming { + @include color-svg('../images/broken-link.svg', $color-white-085); + } + // When you're composing a new quote .bottom-bar { .module-quote__primary__author { diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index e0890dcaf..a0c60f3ec 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -697,17 +697,23 @@ // Module: Quoted Reply +.module-quote-container { + margin-left: -6px; + margin-right: -6px; + margin-top: -4px; + margin-bottom: 5px; +} + +.module-quote-container--with-content-above { + margin-top: 3px; +} + .module-quote { position: relative; border-radius: 4px; border-top-left-radius: 10px; border-top-right-radius: 10px; - margin-left: -6px; - margin-right: -6px; - margin-top: -4px; - margin-bottom: 5px; - cursor: pointer; display: flex; flex-direction: row; @@ -723,11 +729,15 @@ } .module-quote--with-content-above { - margin-top: 3px; border-top-left-radius: 4px; border-top-right-radius: 4px; } +.module-quote--with-reference-warning { + border-bottom-left-radius: 0px; + border-bottom-right-radius: 0px; +} + .module-quote--incoming { background-color: $color-white-075; border-left-color: $color-white; @@ -973,6 +983,32 @@ text-overflow: ellipsis; } +.module-quote__reference-warning { + height: 26px; + display: flex; + flex-direction: row; + align-items: center; + + background-color: $color-white-085; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + padding-left: 8px; + padding-right: 8px; +} + +.module-quote__reference-warning__icon { + height: 16px; + width: 16px; + @include color-svg('../images/broken-link.svg', $color-light-60); +} + +.module-quote__reference-warning__text { + margin-left: 6px; + color: $color-light-90; + font-size: 13px; + line-height: 18px; +} + // Module: Embedded Contact .module-embedded-contact { diff --git a/stylesheets/_theme_dark.scss b/stylesheets/_theme_dark.scss index 987983753..75147420f 100644 --- a/stylesheets/_theme_dark.scss +++ b/stylesheets/_theme_dark.scss @@ -62,9 +62,10 @@ body.dark-theme { } .toast { - background: rgba(0, 0, 0, 0.75); + background-color: $color-light-60; color: $color-white; - box-shadow: 0 0 5px 0 black; + box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.12), + 0 0 0 0.5px rgba(0, 0, 0, 0.08); } .confirmation-dialog { @@ -1015,6 +1016,18 @@ body.dark-theme { color: $color-dark-05; } + .module-quote__reference-warning { + background-color: $color-black-06; + } + + .module-quote__reference-warning__icon { + @include color-svg('../images/broken-link.svg', $color-dark-30); + } + + .module-quote__reference-warning__text { + color: $color-dark-05; + } + // Module: Embedded Contact .module-embedded-contact__image-container__default-avatar { diff --git a/stylesheets/_variables.scss b/stylesheets/_variables.scss index 2692e2f76..7274dc362 100644 --- a/stylesheets/_variables.scss +++ b/stylesheets/_variables.scss @@ -32,12 +32,15 @@ $color-core-green: #4caf50; $color-core-red: #f44336; $color-signal-blue-025: rgba($color-signal-blue, 0.25); +$color-signal-blue-050: rgba($color-signal-blue, 0.5); $color-white: #ffffff; $color-white-02: rgba($color-white, 0.2); +$color-white-04: rgba($color-white, 0.4); $color-white-06: rgba($color-white, 0.6); $color-white-07: rgba($color-white, 0.7); $color-white-075: rgba($color-white, 0.75); +$color-white-085: rgba($color-white, 0.85); $color-light-02: #f9fafa; $color-light-10: #eeefef; $color-light-35: #a4a6a9; @@ -58,6 +61,9 @@ $color-black-016-no-tranparency: #d9d9d9; $color-black-012: rgba($color-black, 0.12); $color-black-02: rgba($color-black, 0.2); $color-black-04: rgba($color-black, 0.4); +$color-black-06: rgba($color-black, 0.6); + +$color-signal-blue-mix: mix($color-signal-blue-025, $color-white-04); $color-conversation-grey: #757575; $color-conversation-blue: #1976d2; diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 619fb8474..4ea220d22 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -78,6 +78,7 @@ export interface Props { authorName?: string; authorColor: Color; onClick?: () => void; + referencedMessageNotFound: boolean; }; authorAvatarPath?: string; expirationLength?: number; @@ -572,6 +573,7 @@ export class Message extends React.Component { authorProfileName={quote.authorProfileName} authorName={quote.authorName} authorColor={quote.authorColor} + referencedMessageNotFound={quote.referencedMessageNotFound} isFromMe={quote.isFromMe} withContentAbove={withContentAbove} /> diff --git a/ts/components/conversation/Quote.md b/ts/components/conversation/Quote.md index b399aa13c..d4ba4d0fc 100644 --- a/ts/components/conversation/Quote.md +++ b/ts/components/conversation/Quote.md @@ -225,6 +225,83 @@ ``` +#### Referenced message not found + +```jsx + +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
    +``` + #### Long names and context ```jsx diff --git a/ts/components/conversation/Quote.tsx b/ts/components/conversation/Quote.tsx index 8e6dc925a..b79e3ce3c 100644 --- a/ts/components/conversation/Quote.tsx +++ b/ts/components/conversation/Quote.tsx @@ -23,6 +23,7 @@ interface Props { onClick?: () => void; onClose?: () => void; text: string; + referencedMessageNotFound: boolean; } export interface QuotedAttachment { @@ -281,12 +282,49 @@ export class Quote extends React.Component { ); } + public renderReferenceWarning() { + const { i18n, isIncoming, referencedMessageNotFound } = this.props; + + if (!referencedMessageNotFound) { + return null; + } + + return ( +
    +
    +
    + {i18n('originalMessageNotFound')} +
    +
    + ); + } + public render() { const { authorColor, isFromMe, isIncoming, onClick, + referencedMessageNotFound, withContentAbove, } = this.props; @@ -296,26 +334,37 @@ export class Quote extends React.Component { return (
    -
    - {this.renderAuthor()} - {this.renderGenericFile()} - {this.renderText()} +
    +
    + {this.renderAuthor()} + {this.renderGenericFile()} + {this.renderText()} +
    + {this.renderIconContainer()} + {this.renderClose()}
    - {this.renderIconContainer()} - {this.renderClose()} + {this.renderReferenceWarning()}
    ); }