diff --git a/js/models/messages.js b/js/models/messages.js index 37992e95ae3..63b5a5b2095 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -162,17 +162,8 @@ isUnread() { return !!this.get('unread'); }, - // Important to allow for this.unset('unread'), save to db, then fetch() - // to propagate. We don't want the unset key in the db so our unread index - // stays small. merge(model) { const attributes = model.attributes || model; - - const { unread } = attributes; - if (typeof unread === 'undefined') { - this.unset('unread'); - } - this.set(attributes); }, getNameForNumber(number) { @@ -311,13 +302,12 @@ const conversation = this.getConversation(); const isGroup = conversation && !conversation.isPrivate(); const phoneNumber = this.get('key_changed'); - const onVerify = () => - this.trigger('show-identity', this.findContact(phoneNumber)); + const showIdentity = id => this.trigger('show-identity', id); return { isGroup, contact: this.findAndFormatContact(phoneNumber), - onVerify, + showIdentity, }; }, getPropsForVerificationNotification() { @@ -339,21 +329,17 @@ return ConversationController.get(phoneNumber); }, findAndFormatContact(phoneNumber) { + const contactModel = this.findContact(phoneNumber); + if (contactModel) { + return contactModel.getProps(); + } + const { format } = PhoneNumber; const regionCode = storage.get('regionCode'); - - const contactModel = this.findContact(phoneNumber); - const color = contactModel ? contactModel.getColor() : null; - return { phoneNumber: format(phoneNumber, { ourRegionCode: regionCode, }), - color, - avatarPath: contactModel ? contactModel.getAvatarPath() : null, - name: contactModel ? contactModel.getName() : null, - profileName: contactModel ? contactModel.getProfileName() : null, - title: contactModel ? contactModel.getTitle() : null, }; }, getPropsForGroupNotification() { @@ -460,7 +446,7 @@ snippet: this.get('snippet'), }; }, - getPropsForMessage(options) { + getPropsForMessage() { const phoneNumber = this.getSource(); const contact = this.findAndFormatContact(phoneNumber); const contactModel = this.findContact(phoneNumber); @@ -479,9 +465,7 @@ const conversation = this.getConversation(); const isGroup = conversation && !conversation.isPrivate(); - const attachments = this.get('attachments') || []; - const firstAttachment = attachments[0]; return { text: this.createNonBreakingLastSeparator(this.get('body')), @@ -500,28 +484,30 @@ .filter(attachment => !attachment.error) .map(attachment => this.getPropsForAttachment(attachment)), previews: this.getPropsForPreview(), - quote: this.getPropsForQuote(options), + quote: this.getPropsForQuote(), authorAvatarPath, isExpired: this.hasExpired, expirationLength, expirationTimestamp, - onReply: () => this.trigger('reply', this), - onRetrySend: () => this.retrySend(), - onShowDetail: () => this.trigger('show-message-detail', this), - onDelete: () => this.trigger('delete', this), - onClickLinkPreview: url => this.trigger('navigate-to', url), - onClickAttachment: attachment => - this.trigger('show-lightbox', { - attachment, - message: this, - }), - onDownload: isDangerous => - this.trigger('download', { - attachment: firstAttachment, - message: this, - isDangerous, - }), + replyToMessage: id => this.trigger('reply', id), + retrySend: id => this.trigger('retry', id), + deleteMessage: id => this.trigger('delete', id), + showMessageDetail: id => this.trigger('show-message-detail', id), + + openConversation: conversationId => + this.trigger('open-conversation', conversationId), + showContactDetail: contactOptions => + this.trigger('show-contact-detail', contactOptions), + + showVisualAttachment: lightboxOptions => + this.trigger('show-lightbox', lightboxOptions), + downloadAttachment: downloadOptions => + this.trigger('download', downloadOptions), + + openLink: url => this.trigger('navigate-to', url), + scrollToMessage: scrollOptions => + this.trigger('scroll-to-message', scrollOptions), }; }, createNonBreakingLastSeparator(text) { @@ -551,20 +537,6 @@ const contact = contacts[0]; const firstNumber = contact.number && contact.number[0] && contact.number[0].value; - const onSendMessage = firstNumber - ? () => { - this.trigger('open-conversation', firstNumber); - } - : null; - const onClick = async () => { - // First let's be sure that the signal account check is complete. - await window.checkForSignalAccount(firstNumber); - - this.trigger('show-contact-detail', { - contact, - hasSignalAccount: window.hasSignalAccount(firstNumber), - }); - }; // Would be nice to do this before render, on initial load of message if (!window.isSignalAccountCheckComplete(firstNumber)) { @@ -576,9 +548,9 @@ return contactSelector(contact, { regionCode, getAbsoluteAttachmentPath, - onSendMessage, - onClick, - hasSignalAccount: window.hasSignalAccount(firstNumber), + signalAccount: window.hasSignalAccount(firstNumber) + ? firstNumber + : null, }); }, processQuoteAttachment(attachment) { @@ -610,8 +582,7 @@ image: preview.image ? this.getPropsForAttachment(preview.image) : null, })); }, - getPropsForQuote(options = {}) { - const { noClick } = options; + getPropsForQuote() { const quote = this.get('quote'); if (!quote) { return null; @@ -620,7 +591,7 @@ const { format } = PhoneNumber; const regionCode = storage.get('regionCode'); - const { author, id, referencedMessageNotFound } = quote; + const { author, id: sentAt, referencedMessageNotFound } = quote; const contact = author && ConversationController.get(author); const authorColor = contact ? contact.getColor() : 'grey'; @@ -630,16 +601,6 @@ const authorProfileName = contact ? contact.getProfileName() : null; const authorName = contact ? contact.getName() : null; const isFromMe = contact ? contact.id === this.OUR_NUMBER : false; - const onClick = noClick - ? null - : () => { - this.trigger('scroll-to-message', { - author, - id, - referencedMessageNotFound, - }); - }; - const firstAttachment = quote.attachments && quote.attachments[0]; return { @@ -648,11 +609,12 @@ ? this.processQuoteAttachment(firstAttachment) : null, isFromMe, + sentAt, + authorId: author, authorPhoneNumber, authorProfileName, authorName, authorColor, - onClick, referencedMessageNotFound, }; }, @@ -740,6 +702,7 @@ return { ...this.findAndFormatContact(id), + status: this.getStatus(id), errors: errorsForContact, isOutgoingKeyError, @@ -765,8 +728,9 @@ sentAt: this.get('sent_at'), receivedAt: this.get('received_at'), message: { - ...this.getPropsForMessage({ noClick: true }), + ...this.getPropsForMessage(), disableMenu: true, + disableScroll: true, // To ensure that group avatar doesn't show up conversationType: 'direct', }, diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index 6a0b8ad212f..67bad4c4182 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -114,6 +114,7 @@ 'reply', this.setQuoteMessage ); + this.listenTo(this.model.messageCollection, 'retry', this.retrySend); this.listenTo( this.model.messageCollection, 'show-contact-detail', @@ -705,8 +706,16 @@ } }, + async retrySend(messageId) { + const message = this.model.messageCollection.get(messageId); + if (!message) { + throw new Error(`retrySend: Did not find message for id ${messageId}`); + } + await message.retrySend(); + }, + async scrollToMessage(options = {}) { - const { author, id, referencedMessageNotFound } = options; + const { author, sentAt, 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. @@ -724,7 +733,7 @@ if (!messageAuthor || author !== messageAuthor.id) { return false; } - if (id !== item.get('sent_at')) { + if (sentAt !== item.get('sent_at')) { return false; } @@ -734,13 +743,16 @@ // 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 collection = await window.Signal.Data.getMessagesBySentAt( + sentAt, + { + MessageCollection: Whisper.MessageCollection, + } + ); + const found = Boolean( collection.find(item => { const messageAuthor = item.getContact(); - return messageAuthor && author === messageAuthor.id; }) ); @@ -765,7 +777,7 @@ toast.render(); window.log.info( - `Error: had target message ${id} in messageCollection, but it was not in DOM` + `Error: had target message ${targetMessage.idForLogging()} in messageCollection, but it was not in DOM` ); return; } @@ -1202,23 +1214,25 @@ dialog.focusCancel(); }, - showSafetyNumber(providedModel) { - let model = providedModel; + showSafetyNumber(id) { + let conversation; - if (!model && this.model.isPrivate()) { + if (!id && this.model.isPrivate()) { // eslint-disable-next-line prefer-destructuring - model = this.model; + conversation = this.model; + } else { + conversation = ConversationController.get(id); } - if (model) { + if (conversation) { const view = new Whisper.KeyVerificationPanelView({ - model, + model: conversation, }); this.listenBack(view); this.updateHeader(); } }, - downloadAttachment({ attachment, message, isDangerous }) { + downloadAttachment({ attachment, timestamp, isDangerous }) { if (isDangerous) { const toast = new Whisper.DangerousFileTypeToast(); toast.$el.appendTo(this.$el); @@ -1230,11 +1244,18 @@ attachment, document, getAbsolutePath: getAbsoluteAttachmentPath, - timestamp: message.get('sent_at'), + timestamp, }); }, - deleteMessage(message) { + deleteMessage(messageId) { + const message = this.model.messageCollection.get(messageId); + if (!message) { + throw new Error( + `deleteMessage: Did not find message for id ${messageId}` + ); + } + const dialog = new Whisper.ConfirmationDialogView({ message: i18n('deleteWarning'), okText: i18n('delete'), @@ -1253,7 +1274,13 @@ dialog.focusCancel(); }, - showLightbox({ attachment, message }) { + showLightbox({ attachment, messageId }) { + const message = this.model.messageCollection.get(messageId); + if (!message) { + throw new Error( + `showLightbox: did not find message for id ${messageId}` + ); + } const { contentType, path } = attachment; if ( @@ -1333,7 +1360,14 @@ Signal.Backbone.Views.Lightbox.show(this.lightboxGalleryView.el); }, - showMessageDetail(message) { + showMessageDetail(messageId) { + const message = this.model.messageCollection.get(messageId); + if (!message) { + throw new Error( + `showMessageDetail: Did not find message for id ${messageId}` + ); + } + const onClose = () => { this.stopListening(message, 'change', update); this.resetPanel(); @@ -1358,24 +1392,16 @@ view.render(); }, - showContactDetail({ contact, hasSignalAccount }) { - const regionCode = storage.get('regionCode'); - const { contactSelector } = Signal.Types.Contact; - + showContactDetail({ contact, signalAccount }) { const view = new Whisper.ReactWrapperView({ Component: Signal.Components.ContactDetail, className: 'contact-detail-pane panel', props: { - contact: contactSelector(contact, { - regionCode, - getAbsoluteAttachmentPath, - }), - hasSignalAccount, + contact, + signalAccount, onSendMessage: () => { - const number = - contact.number && contact.number[0] && contact.number[0].value; - if (number) { - this.openConversation(number); + if (signalAccount) { + this.openConversation(signalAccount); } }, }, @@ -1592,20 +1618,25 @@ this.focusMessageField(); }, - async setQuoteMessage(message) { + async setQuoteMessage(messageId) { this.quote = null; - this.quotedMessage = message; + this.quotedMessage = null; if (this.quoteHolder) { this.quoteHolder.unload(); this.quoteHolder = null; } + const message = this.model.messageCollection.get(messageId); if (message) { - const quote = await this.model.makeQuote(this.quotedMessage); - this.quote = quote; + this.quotedMessage = message; - this.focusMessageFieldAndClearDisabled(); + if (message) { + const quote = await this.model.makeQuote(this.quotedMessage); + this.quote = quote; + + this.focusMessageFieldAndClearDisabled(); + } } this.renderQuotedMessage(); diff --git a/ts/components/conversation/ContactDetail.tsx b/ts/components/conversation/ContactDetail.tsx index 2c209b62432..175bd9ca3d6 100644 --- a/ts/components/conversation/ContactDetail.tsx +++ b/ts/components/conversation/ContactDetail.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { AddressType, - Contact, + ContactFormType, ContactType, Email, Phone, @@ -19,7 +19,7 @@ import { import { LocalizerType } from '../../types/Util'; interface Props { - contact: Contact; + contact: ContactType; hasSignalAccount: boolean; i18n: LocalizerType; onSendMessage: () => void; @@ -27,13 +27,13 @@ interface Props { function getLabelForEmail(method: Email, i18n: LocalizerType): string { switch (method.type) { - case ContactType.CUSTOM: + case ContactFormType.CUSTOM: return method.label || i18n('email'); - case ContactType.HOME: + case ContactFormType.HOME: return i18n('home'); - case ContactType.MOBILE: + case ContactFormType.MOBILE: return i18n('mobile'); - case ContactType.WORK: + case ContactFormType.WORK: return i18n('work'); default: throw missingCaseError(method.type); @@ -42,13 +42,13 @@ function getLabelForEmail(method: Email, i18n: LocalizerType): string { function getLabelForPhone(method: Phone, i18n: LocalizerType): string { switch (method.type) { - case ContactType.CUSTOM: + case ContactFormType.CUSTOM: return method.label || i18n('phone'); - case ContactType.HOME: + case ContactFormType.HOME: return i18n('home'); - case ContactType.MOBILE: + case ContactFormType.MOBILE: return i18n('mobile'); - case ContactType.WORK: + case ContactFormType.WORK: return i18n('work'); default: throw missingCaseError(method.type); diff --git a/ts/components/conversation/EmbeddedContact.md b/ts/components/conversation/EmbeddedContact.md index 1f22d3ab9a4..0a113364cc7 100644 --- a/ts/components/conversation/EmbeddedContact.md +++ b/ts/components/conversation/EmbeddedContact.md @@ -20,7 +20,7 @@ const contact = { }, onClick: () => console.log('onClick'), onSendMessage: () => console.log('onSendMessage'), - hasSignalAccount: true, + signalAccount: '+12025550000', };
  • @@ -86,7 +86,7 @@ const contact = { }, onClick: () => console.log('onClick'), onSendMessage: () => console.log('onSendMessage'), - hasSignalAccount: true, + signalAccount: '+12025550000', };
  • @@ -129,7 +129,6 @@ const contact = { path: util.gifObjectUrl, }, }, - hasSignalAccount: true, };
  • @@ -170,7 +169,7 @@ const contact = { path: util.gifObjectUrl, }, }, - hasSignalAccount: true, + signalAccount: '+12025550000', };
  • @@ -230,7 +229,6 @@ const contact = { path: util.gifObjectUrl, }, }, - hasSignalAccount: false, };
  • @@ -292,7 +290,6 @@ const contact = { path: util.gifObjectUrl, }, }, - hasSignalAccount: false, };
  • @@ -356,7 +353,7 @@ const contact = { path: util.gifObjectUrl, }, }, - hasSignalAccount: false, + signalAccount: '+12025551000', };
  • @@ -415,7 +412,6 @@ const contact = { type: 1, }, ], - hasSignalAccount: true, };
  • @@ -527,7 +523,7 @@ const contactWithAccount = { path: util.gifObjectUrl, }, }, - hasSignalAccount: true, + signalAccount: '+12025550000', }; const contactWithoutAccount = { name: { @@ -544,7 +540,6 @@ const contactWithoutAccount = { path: util.gifObjectUrl, }, }, - hasSignalAccount: false, };
  • diff --git a/ts/components/conversation/EmbeddedContact.tsx b/ts/components/conversation/EmbeddedContact.tsx index bcc4fe80c83..5b88c133470 100644 --- a/ts/components/conversation/EmbeddedContact.tsx +++ b/ts/components/conversation/EmbeddedContact.tsx @@ -1,7 +1,7 @@ import React from 'react'; import classNames from 'classnames'; -import { Contact } from '../../types/Contact'; +import { ContactType } from '../../types/Contact'; import { LocalizerType } from '../../types/Util'; import { @@ -11,8 +11,7 @@ import { } from './_contactUtil'; interface Props { - contact: Contact; - hasSignalAccount: boolean; + contact: ContactType; i18n: LocalizerType; isIncoming: boolean; withContentAbove: boolean; diff --git a/ts/components/conversation/ImageGrid.tsx b/ts/components/conversation/ImageGrid.tsx index d79a510dcc4..41ad9e62e05 100644 --- a/ts/components/conversation/ImageGrid.tsx +++ b/ts/components/conversation/ImageGrid.tsx @@ -24,7 +24,7 @@ interface Props { i18n: LocalizerType; onError: () => void; - onClickAttachment?: (attachment: AttachmentType) => void; + onClick?: (attachment: AttachmentType) => void; } export class ImageGrid extends React.Component { @@ -35,7 +35,7 @@ export class ImageGrid extends React.Component { bottomOverlay, i18n, onError, - onClickAttachment, + onClick, withContentAbove, withContentBelow, } = this.props; @@ -76,7 +76,7 @@ export class ImageGrid extends React.Component { height={height} width={width} url={getUrl(attachments[0])} - onClick={onClickAttachment} + onClick={onClick} onError={onError} /> @@ -97,7 +97,7 @@ export class ImageGrid extends React.Component { height={149} width={149} url={getThumbnailUrl(attachments[0])} - onClick={onClickAttachment} + onClick={onClick} onError={onError} /> { width={149} attachment={attachments[1]} url={getThumbnailUrl(attachments[1])} - onClick={onClickAttachment} + onClick={onClick} onError={onError} /> @@ -132,7 +132,7 @@ export class ImageGrid extends React.Component { height={200} width={199} url={getUrl(attachments[0])} - onClick={onClickAttachment} + onClick={onClick} onError={onError} />
    @@ -145,7 +145,7 @@ export class ImageGrid extends React.Component { attachment={attachments[1]} playIconOverlay={isVideoAttachment(attachments[1])} url={getThumbnailUrl(attachments[1])} - onClick={onClickAttachment} + onClick={onClick} onError={onError} /> { attachment={attachments[2]} playIconOverlay={isVideoAttachment(attachments[2])} url={getThumbnailUrl(attachments[2])} - onClick={onClickAttachment} + onClick={onClick} onError={onError} />
    @@ -180,7 +180,7 @@ export class ImageGrid extends React.Component { height={149} width={149} url={getThumbnailUrl(attachments[0])} - onClick={onClickAttachment} + onClick={onClick} onError={onError} /> { width={149} attachment={attachments[1]} url={getThumbnailUrl(attachments[1])} - onClick={onClickAttachment} + onClick={onClick} onError={onError} /> @@ -207,7 +207,7 @@ export class ImageGrid extends React.Component { width={149} attachment={attachments[2]} url={getThumbnailUrl(attachments[2])} - onClick={onClickAttachment} + onClick={onClick} onError={onError} /> { width={149} attachment={attachments[3]} url={getThumbnailUrl(attachments[3])} - onClick={onClickAttachment} + onClick={onClick} onError={onError} /> @@ -247,7 +247,7 @@ export class ImageGrid extends React.Component { height={149} width={149} url={getThumbnailUrl(attachments[0])} - onClick={onClickAttachment} + onClick={onClick} onError={onError} /> { width={149} attachment={attachments[1]} url={getThumbnailUrl(attachments[1])} - onClick={onClickAttachment} + onClick={onClick} onError={onError} /> @@ -274,7 +274,7 @@ export class ImageGrid extends React.Component { width={99} attachment={attachments[2]} url={getThumbnailUrl(attachments[2])} - onClick={onClickAttachment} + onClick={onClick} onError={onError} /> { width={98} attachment={attachments[3]} url={getThumbnailUrl(attachments[3])} - onClick={onClickAttachment} + onClick={onClick} onError={onError} /> { overlayText={moreMessagesOverlayText} attachment={attachments[4]} url={getThumbnailUrl(attachments[4])} - onClick={onClickAttachment} + onClick={onClick} onError={onError} /> diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 9520444da1d..9cab3336ef4 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -25,7 +25,7 @@ import { isVideo, } from '../../../ts/types/Attachment'; import { AttachmentType } from '../../types/Attachment'; -import { Contact } from '../../types/Contact'; +import { ContactType } from '../../types/Contact'; import { getIncrement } from '../../util/timer'; import { isFileDangerous } from '../../util/isFileDangerous'; @@ -46,22 +46,14 @@ interface LinkPreviewType { image?: AttachmentType; } -export interface Props { - disableMenu?: boolean; +type PropsData = { + id: string; text?: string; textPending?: boolean; - id?: string; - collapseMetadata?: boolean; direction: 'incoming' | 'outgoing'; timestamp: number; status?: 'sending' | 'sent' | 'delivered' | 'read' | 'error'; - // What if changed this over to a single contact like quote, and put the events on it? - contact?: Contact & { - hasSignalAccount: boolean; - onSendMessage?: () => void; - onClick?: () => void; - }; - i18n: LocalizerType; + contact?: ContactType; authorName?: string; authorProfileName?: string; /** Note: this should be formatted for display */ @@ -73,11 +65,12 @@ export interface Props { text: string; attachment?: QuotedAttachmentType; isFromMe: boolean; + sentAt: number; + authorId: string; authorPhoneNumber: string; authorProfileName?: string; authorName?: string; authorColor?: ColorType; - onClick?: () => void; referencedMessageNotFound: boolean; }; previews: Array; @@ -85,14 +78,48 @@ export interface Props { isExpired: boolean; expirationLength?: number; expirationTimestamp?: number; - onClickAttachment?: (attachment: AttachmentType) => void; - onClickLinkPreview?: (url: string) => void; - onReply?: () => void; - onRetrySend?: () => void; - onDownload?: (isDangerous: boolean) => void; - onDelete?: () => void; - onShowDetail: () => void; -} +}; + +type PropsHousekeeping = { + i18n: LocalizerType; + disableMenu?: boolean; + disableScroll?: boolean; + collapseMetadata?: boolean; +}; + +export type PropsActions = { + replyToMessage: (id: string) => void; + retrySend: (id: string) => void; + deleteMessage: (id: string) => void; + showMessageDetail: (id: string) => void; + + openConversation: (conversationId: string, messageId?: string) => void; + showContactDetail: ( + options: { contact: ContactType; signalAccount?: string } + ) => void; + + showVisualAttachment: ( + options: { attachment: AttachmentType; messageId: string } + ) => void; + downloadAttachment: ( + options: { + attachment: AttachmentType; + timestamp: number; + isDangerous: boolean; + } + ) => void; + + openLink: (url: string) => void; + scrollToMessage: ( + options: { + author: string; + sentAt: number; + referencedMessageNotFound: boolean; + } + ) => void; +}; + +export type Props = PropsData & PropsHousekeeping & PropsActions; interface State { expiring: boolean; @@ -301,6 +328,7 @@ export class Message extends React.PureComponent { // tslint:disable-next-line max-func-body-length cyclomatic-complexity public renderAttachment() { const { + id, attachments, text, collapseMetadata, @@ -308,7 +336,7 @@ export class Message extends React.PureComponent { direction, i18n, quote, - onClickAttachment, + showVisualAttachment, } = this.props; const { imageBroken } = this.state; @@ -349,7 +377,9 @@ export class Message extends React.PureComponent { bottomOverlay={!collapseMetadata} i18n={i18n} onError={this.handleImageErrorBound} - onClickAttachment={onClickAttachment} + onClick={attachment => { + showVisualAttachment({ attachment, messageId: id }); + }} /> ); @@ -438,7 +468,7 @@ export class Message extends React.PureComponent { conversationType, direction, i18n, - onClickLinkPreview, + openLink, previews, quote, } = this.props; @@ -475,9 +505,7 @@ export class Message extends React.PureComponent { : null )} onClick={() => { - if (onClickLinkPreview) { - onClickLinkPreview(first.url); - } + openLink(first.url); }} > {first.image && previewHasImage && isFullSizeImage ? ( @@ -537,8 +565,10 @@ export class Message extends React.PureComponent { conversationType, authorColor, direction, + disableScroll, i18n, quote, + scrollToMessage, } = this.props; if (!quote) { @@ -550,10 +580,21 @@ export class Message extends React.PureComponent { const quoteColor = direction === 'incoming' ? authorColor : quote.authorColor; + const { referencedMessageNotFound } = quote; + const clickHandler = disableScroll + ? undefined + : () => { + scrollToMessage({ + author: quote.authorId, + sentAt: quote.sentAt, + referencedMessageNotFound, + }); + }; + return ( { authorProfileName={quote.authorProfileName} authorName={quote.authorName} authorColor={quoteColor} - referencedMessageNotFound={quote.referencedMessageNotFound} + referencedMessageNotFound={referencedMessageNotFound} isFromMe={quote.isFromMe} withContentAbove={withContentAbove} /> @@ -575,6 +616,7 @@ export class Message extends React.PureComponent { conversationType, direction, i18n, + showContactDetail, text, } = this.props; if (!contact) { @@ -589,10 +631,11 @@ export class Message extends React.PureComponent { return ( { + showContactDetail({ contact, signalAccount: contact.signalAccount }); + }} withContentAbove={withContentAbove} withContentBelow={withContentBelow} /> @@ -600,15 +643,19 @@ export class Message extends React.PureComponent { } public renderSendMessageButton() { - const { contact, i18n } = this.props; - if (!contact || !contact.hasSignalAccount) { + const { contact, openConversation, i18n } = this.props; + if (!contact || !contact.signalAccount) { return null; } return (
    { + if (contact.signalAccount) { + openConversation(contact.signalAccount); + } + }} className="module-message__send-message-button" > {i18n('sendMessageToContact')} @@ -718,8 +765,10 @@ export class Message extends React.PureComponent { attachments, direction, disableMenu, - onDownload, - onReply, + downloadAttachment, + id, + replyToMessage, + timestamp, } = this.props; if (!isCorrectSide || disableMenu) { @@ -736,9 +785,11 @@ export class Message extends React.PureComponent { !multipleAttachments && firstAttachment && !firstAttachment.pending ? (
    { - if (onDownload) { - onDownload(isDangerous); - } + downloadAttachment({ + isDangerous, + attachment: firstAttachment, + timestamp, + }); }} role="button" className={classNames( @@ -750,7 +801,9 @@ export class Message extends React.PureComponent { const replyButton = (
    { + replyToMessage(id); + }} role="button" className={classNames( 'module-message__buttons__reply', @@ -793,13 +846,15 @@ export class Message extends React.PureComponent { const { attachments, direction, - status, - onDelete, - onDownload, - onReply, - onRetrySend, - onShowDetail, + downloadAttachment, i18n, + id, + deleteMessage, + showMessageDetail, + replyToMessage, + retrySend, + status, + timestamp, } = this.props; const showRetry = status === 'error' && direction === 'outgoing'; @@ -816,9 +871,11 @@ export class Message extends React.PureComponent { className: 'module-message__context__download', }} onClick={() => { - if (onDownload) { - onDownload(isDangerous); - } + downloadAttachment({ + attachment: attachments[0], + timestamp, + isDangerous, + }); }} > {i18n('downloadAttachment')} @@ -828,7 +885,9 @@ export class Message extends React.PureComponent { attributes={{ className: 'module-message__context__reply', }} - onClick={onReply} + onClick={() => { + replyToMessage(id); + }} > {i18n('replyToMessage')} @@ -836,7 +895,9 @@ export class Message extends React.PureComponent { attributes={{ className: 'module-message__context__more-info', }} - onClick={onShowDetail} + onClick={() => { + showMessageDetail(id); + }} > {i18n('moreInfo')} @@ -845,7 +906,9 @@ export class Message extends React.PureComponent { attributes={{ className: 'module-message__context__retry-send', }} - onClick={onRetrySend} + onClick={() => { + retrySend(id); + }} > {i18n('retrySend')} @@ -854,7 +917,9 @@ export class Message extends React.PureComponent { attributes={{ className: 'module-message__context__delete-message', }} - onClick={onDelete} + onClick={() => { + deleteMessage(id); + }} > {i18n('deleteMessage')} diff --git a/ts/components/conversation/MessageDetail.tsx b/ts/components/conversation/MessageDetail.tsx index 91baec0ce30..238b776003c 100644 --- a/ts/components/conversation/MessageDetail.tsx +++ b/ts/components/conversation/MessageDetail.tsx @@ -59,7 +59,9 @@ export class MessageDetail extends React.Component { return (
    { + showIdentity(contact.id); + }} className="module-verification-notification__button" > {i18n('verifyNewNumber')} diff --git a/ts/components/conversation/_contactUtil.tsx b/ts/components/conversation/_contactUtil.tsx index 496aba756da..621efddb8f1 100644 --- a/ts/components/conversation/_contactUtil.tsx +++ b/ts/components/conversation/_contactUtil.tsx @@ -5,7 +5,7 @@ import { Avatar } from '../Avatar'; import { Spinner } from '../Spinner'; import { LocalizerType } from '../../types/Util'; -import { Contact, getName } from '../../types/Contact'; +import { ContactType, getName } from '../../types/Contact'; // This file starts with _ to keep it from showing up in the StyleGuide. @@ -15,7 +15,7 @@ export function renderAvatar({ size, direction, }: { - contact: Contact; + contact: ContactType; i18n: LocalizerType; size: number; direction?: string; @@ -52,7 +52,7 @@ export function renderName({ isIncoming, module, }: { - contact: Contact; + contact: ContactType; isIncoming: boolean; module: string; }) { @@ -73,7 +73,7 @@ export function renderContactShorthand({ isIncoming, module, }: { - contact: Contact; + contact: ContactType; isIncoming: boolean; module: string; }) { diff --git a/ts/test/types/Contact_test.ts b/ts/test/types/Contact_test.ts index c35eeee4d68..13ba9438c04 100644 --- a/ts/test/types/Contact_test.ts +++ b/ts/test/types/Contact_test.ts @@ -63,10 +63,8 @@ describe('Contact', () => { }); describe('contactSelector', () => { const regionCode = '1'; - const hasSignalAccount = true; + const signalAccount = '+1202555000'; const getAbsoluteAttachmentPath = (path: string) => `absolute:${path}`; - const onSendMessage = () => null; - const onClick = () => null; it('eliminates avatar if it has had an attachment download error', () => { const contact = { @@ -91,17 +89,13 @@ describe('Contact', () => { }, organization: 'Somewhere, Inc.', avatar: undefined, - hasSignalAccount, - onSendMessage, - onClick, + signalAccount, number: undefined, }; const actual = contactSelector(contact, { regionCode, - hasSignalAccount, + signalAccount, getAbsoluteAttachmentPath, - onSendMessage, - onClick, }); assert.deepEqual(actual, expected); }); @@ -135,17 +129,13 @@ describe('Contact', () => { path: undefined, }, }, - hasSignalAccount, - onSendMessage, - onClick, + signalAccount, number: undefined, }; const actual = contactSelector(contact, { regionCode, - hasSignalAccount, + signalAccount, getAbsoluteAttachmentPath, - onSendMessage, - onClick, }); assert.deepEqual(actual, expected); }); @@ -178,17 +168,13 @@ describe('Contact', () => { path: 'absolute:somewhere', }, }, - hasSignalAccount, - onSendMessage, - onClick, + signalAccount, number: undefined, }; const actual = contactSelector(contact, { regionCode, - hasSignalAccount, + signalAccount, getAbsoluteAttachmentPath, - onSendMessage, - onClick, }); assert.deepEqual(actual, expected); }); diff --git a/ts/types/Contact.tsx b/ts/types/Contact.tsx index 1307a0054fa..349c85027fb 100644 --- a/ts/types/Contact.tsx +++ b/ts/types/Contact.tsx @@ -2,13 +2,14 @@ import Attachments from '../../app/attachments'; import { format as formatPhoneNumber } from '../types/PhoneNumber'; -export interface Contact { +export interface ContactType { name?: Name; number?: Array; email?: Array; address?: Array; avatar?: Avatar; organization?: string; + signalAccount?: string; } interface Name { @@ -20,7 +21,7 @@ interface Name { displayName?: string; } -export enum ContactType { +export enum ContactFormType { HOME = 1, MOBILE = 2, WORK = 3, @@ -35,13 +36,13 @@ export enum AddressType { export interface Phone { value: string; - type: ContactType; + type: ContactFormType; label?: string; } export interface Email { value: string; - type: ContactType; + type: ContactFormType; label?: string; } @@ -69,22 +70,14 @@ interface Attachment { } export function contactSelector( - contact: Contact, + contact: ContactType, options: { regionCode: string; - hasSignalAccount: boolean; + signalAccount?: string; getAbsoluteAttachmentPath: (path: string) => string; - onSendMessage: () => void; - onClick: () => void; } ) { - const { - getAbsoluteAttachmentPath, - hasSignalAccount, - onClick, - onSendMessage, - regionCode, - } = options; + const { getAbsoluteAttachmentPath, signalAccount, regionCode } = options; let { avatar } = contact; if (avatar && avatar.avatar) { @@ -105,9 +98,7 @@ export function contactSelector( return { ...contact, - hasSignalAccount, - onSendMessage, - onClick, + signalAccount, avatar, number: contact.number && @@ -120,7 +111,7 @@ export function contactSelector( }; } -export function getName(contact: Contact): string | undefined { +export function getName(contact: ContactType): string | undefined { const { name, organization } = contact; const displayName = (name && name.displayName) || undefined; const givenName = (name && name.givenName) || undefined; diff --git a/ts/types/Message.ts b/ts/types/Message.ts index 1a2a6baad2f..6df08145083 100644 --- a/ts/types/Message.ts +++ b/ts/types/Message.ts @@ -1,5 +1,5 @@ import { Attachment } from './Attachment'; -import { Contact } from './Contact'; +import { ContactType } from './Contact'; import { IndexableBoolean, IndexablePresence } from './IndexedDB'; export type Message = UserMessage | VerifiedChangeMessage; @@ -87,7 +87,7 @@ type MessageSchemaVersion5 = Partial< type MessageSchemaVersion6 = Partial< Readonly<{ - contact: Array; + contact: Array; }> >;