Refactor: Prepare Message function props for conversation scope

This commit is contained in:
Scott Nonnenberg 2019-03-15 15:18:00 -07:00
parent 7e58594038
commit d342b23cbc
13 changed files with 300 additions and 256 deletions

View File

@ -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',
},

View File

@ -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();

View File

@ -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);

View File

@ -20,7 +20,7 @@ const contact = {
},
onClick: () => console.log('onClick'),
onSendMessage: () => console.log('onSendMessage'),
hasSignalAccount: true,
signalAccount: '+12025550000',
};
<util.ConversationContext theme={util.theme} ios={util.ios}>
<li>
@ -86,7 +86,7 @@ const contact = {
},
onClick: () => console.log('onClick'),
onSendMessage: () => console.log('onSendMessage'),
hasSignalAccount: true,
signalAccount: '+12025550000',
};
<util.ConversationContext theme={util.theme} ios={util.ios}>
<li>
@ -129,7 +129,6 @@ const contact = {
path: util.gifObjectUrl,
},
},
hasSignalAccount: true,
};
<util.ConversationContext theme={util.theme} ios={util.ios}>
<li>
@ -170,7 +169,7 @@ const contact = {
path: util.gifObjectUrl,
},
},
hasSignalAccount: true,
signalAccount: '+12025550000',
};
<util.ConversationContext theme={util.theme} type="group" ios={util.ios}>
<li>
@ -230,7 +229,6 @@ const contact = {
path: util.gifObjectUrl,
},
},
hasSignalAccount: false,
};
<util.ConversationContext theme={util.theme} ios={util.ios}>
<li>
@ -292,7 +290,6 @@ const contact = {
path: util.gifObjectUrl,
},
},
hasSignalAccount: false,
};
<util.ConversationContext theme={util.theme} ios={util.ios}>
<li>
@ -356,7 +353,7 @@ const contact = {
path: util.gifObjectUrl,
},
},
hasSignalAccount: false,
signalAccount: '+12025551000',
};
<util.ConversationContext theme={util.theme} ios={util.ios}>
<li>
@ -415,7 +412,6 @@ const contact = {
type: 1,
},
],
hasSignalAccount: true,
};
<util.ConversationContext theme={util.theme} ios={util.ios}>
<li>
@ -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,
};
<util.ConversationContext theme={util.theme} ios={util.ios}>
<li>

View File

@ -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;

View File

@ -24,7 +24,7 @@ interface Props {
i18n: LocalizerType;
onError: () => void;
onClickAttachment?: (attachment: AttachmentType) => void;
onClick?: (attachment: AttachmentType) => void;
}
export class ImageGrid extends React.Component<Props> {
@ -35,7 +35,7 @@ export class ImageGrid extends React.Component<Props> {
bottomOverlay,
i18n,
onError,
onClickAttachment,
onClick,
withContentAbove,
withContentBelow,
} = this.props;
@ -76,7 +76,7 @@ export class ImageGrid extends React.Component<Props> {
height={height}
width={width}
url={getUrl(attachments[0])}
onClick={onClickAttachment}
onClick={onClick}
onError={onError}
/>
</div>
@ -97,7 +97,7 @@ export class ImageGrid extends React.Component<Props> {
height={149}
width={149}
url={getThumbnailUrl(attachments[0])}
onClick={onClickAttachment}
onClick={onClick}
onError={onError}
/>
<Image
@ -111,7 +111,7 @@ export class ImageGrid extends React.Component<Props> {
width={149}
attachment={attachments[1]}
url={getThumbnailUrl(attachments[1])}
onClick={onClickAttachment}
onClick={onClick}
onError={onError}
/>
</div>
@ -132,7 +132,7 @@ export class ImageGrid extends React.Component<Props> {
height={200}
width={199}
url={getUrl(attachments[0])}
onClick={onClickAttachment}
onClick={onClick}
onError={onError}
/>
<div className="module-image-grid__column">
@ -145,7 +145,7 @@ export class ImageGrid extends React.Component<Props> {
attachment={attachments[1]}
playIconOverlay={isVideoAttachment(attachments[1])}
url={getThumbnailUrl(attachments[1])}
onClick={onClickAttachment}
onClick={onClick}
onError={onError}
/>
<Image
@ -158,7 +158,7 @@ export class ImageGrid extends React.Component<Props> {
attachment={attachments[2]}
playIconOverlay={isVideoAttachment(attachments[2])}
url={getThumbnailUrl(attachments[2])}
onClick={onClickAttachment}
onClick={onClick}
onError={onError}
/>
</div>
@ -180,7 +180,7 @@ export class ImageGrid extends React.Component<Props> {
height={149}
width={149}
url={getThumbnailUrl(attachments[0])}
onClick={onClickAttachment}
onClick={onClick}
onError={onError}
/>
<Image
@ -192,7 +192,7 @@ export class ImageGrid extends React.Component<Props> {
width={149}
attachment={attachments[1]}
url={getThumbnailUrl(attachments[1])}
onClick={onClickAttachment}
onClick={onClick}
onError={onError}
/>
</div>
@ -207,7 +207,7 @@ export class ImageGrid extends React.Component<Props> {
width={149}
attachment={attachments[2]}
url={getThumbnailUrl(attachments[2])}
onClick={onClickAttachment}
onClick={onClick}
onError={onError}
/>
<Image
@ -220,7 +220,7 @@ export class ImageGrid extends React.Component<Props> {
width={149}
attachment={attachments[3]}
url={getThumbnailUrl(attachments[3])}
onClick={onClickAttachment}
onClick={onClick}
onError={onError}
/>
</div>
@ -247,7 +247,7 @@ export class ImageGrid extends React.Component<Props> {
height={149}
width={149}
url={getThumbnailUrl(attachments[0])}
onClick={onClickAttachment}
onClick={onClick}
onError={onError}
/>
<Image
@ -259,7 +259,7 @@ export class ImageGrid extends React.Component<Props> {
width={149}
attachment={attachments[1]}
url={getThumbnailUrl(attachments[1])}
onClick={onClickAttachment}
onClick={onClick}
onError={onError}
/>
</div>
@ -274,7 +274,7 @@ export class ImageGrid extends React.Component<Props> {
width={99}
attachment={attachments[2]}
url={getThumbnailUrl(attachments[2])}
onClick={onClickAttachment}
onClick={onClick}
onError={onError}
/>
<Image
@ -286,7 +286,7 @@ export class ImageGrid extends React.Component<Props> {
width={98}
attachment={attachments[3]}
url={getThumbnailUrl(attachments[3])}
onClick={onClickAttachment}
onClick={onClick}
onError={onError}
/>
<Image
@ -301,7 +301,7 @@ export class ImageGrid extends React.Component<Props> {
overlayText={moreMessagesOverlayText}
attachment={attachments[4]}
url={getThumbnailUrl(attachments[4])}
onClick={onClickAttachment}
onClick={onClick}
onError={onError}
/>
</div>

View File

@ -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<LinkPreviewType>;
@ -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<Props, State> {
// 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<Props, State> {
direction,
i18n,
quote,
onClickAttachment,
showVisualAttachment,
} = this.props;
const { imageBroken } = this.state;
@ -349,7 +377,9 @@ export class Message extends React.PureComponent<Props, State> {
bottomOverlay={!collapseMetadata}
i18n={i18n}
onError={this.handleImageErrorBound}
onClickAttachment={onClickAttachment}
onClick={attachment => {
showVisualAttachment({ attachment, messageId: id });
}}
/>
</div>
);
@ -438,7 +468,7 @@ export class Message extends React.PureComponent<Props, State> {
conversationType,
direction,
i18n,
onClickLinkPreview,
openLink,
previews,
quote,
} = this.props;
@ -475,9 +505,7 @@ export class Message extends React.PureComponent<Props, State> {
: null
)}
onClick={() => {
if (onClickLinkPreview) {
onClickLinkPreview(first.url);
}
openLink(first.url);
}}
>
{first.image && previewHasImage && isFullSizeImage ? (
@ -537,8 +565,10 @@ export class Message extends React.PureComponent<Props, State> {
conversationType,
authorColor,
direction,
disableScroll,
i18n,
quote,
scrollToMessage,
} = this.props;
if (!quote) {
@ -550,10 +580,21 @@ export class Message extends React.PureComponent<Props, State> {
const quoteColor =
direction === 'incoming' ? authorColor : quote.authorColor;
const { referencedMessageNotFound } = quote;
const clickHandler = disableScroll
? undefined
: () => {
scrollToMessage({
author: quote.authorId,
sentAt: quote.sentAt,
referencedMessageNotFound,
});
};
return (
<Quote
i18n={i18n}
onClick={quote.onClick}
onClick={clickHandler}
text={quote.text}
attachment={quote.attachment}
isIncoming={direction === 'incoming'}
@ -561,7 +602,7 @@ export class Message extends React.PureComponent<Props, State> {
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<Props, State> {
conversationType,
direction,
i18n,
showContactDetail,
text,
} = this.props;
if (!contact) {
@ -589,10 +631,11 @@ export class Message extends React.PureComponent<Props, State> {
return (
<EmbeddedContact
contact={contact}
hasSignalAccount={contact.hasSignalAccount}
isIncoming={direction === 'incoming'}
i18n={i18n}
onClick={contact.onClick}
onClick={() => {
showContactDetail({ contact, signalAccount: contact.signalAccount });
}}
withContentAbove={withContentAbove}
withContentBelow={withContentBelow}
/>
@ -600,15 +643,19 @@ export class Message extends React.PureComponent<Props, State> {
}
public renderSendMessageButton() {
const { contact, i18n } = this.props;
if (!contact || !contact.hasSignalAccount) {
const { contact, openConversation, i18n } = this.props;
if (!contact || !contact.signalAccount) {
return null;
}
return (
<div
role="button"
onClick={contact.onSendMessage}
onClick={() => {
if (contact.signalAccount) {
openConversation(contact.signalAccount);
}
}}
className="module-message__send-message-button"
>
{i18n('sendMessageToContact')}
@ -718,8 +765,10 @@ export class Message extends React.PureComponent<Props, State> {
attachments,
direction,
disableMenu,
onDownload,
onReply,
downloadAttachment,
id,
replyToMessage,
timestamp,
} = this.props;
if (!isCorrectSide || disableMenu) {
@ -736,9 +785,11 @@ export class Message extends React.PureComponent<Props, State> {
!multipleAttachments && firstAttachment && !firstAttachment.pending ? (
<div
onClick={() => {
if (onDownload) {
onDownload(isDangerous);
}
downloadAttachment({
isDangerous,
attachment: firstAttachment,
timestamp,
});
}}
role="button"
className={classNames(
@ -750,7 +801,9 @@ export class Message extends React.PureComponent<Props, State> {
const replyButton = (
<div
onClick={onReply}
onClick={() => {
replyToMessage(id);
}}
role="button"
className={classNames(
'module-message__buttons__reply',
@ -793,13 +846,15 @@ export class Message extends React.PureComponent<Props, State> {
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<Props, State> {
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<Props, State> {
attributes={{
className: 'module-message__context__reply',
}}
onClick={onReply}
onClick={() => {
replyToMessage(id);
}}
>
{i18n('replyToMessage')}
</MenuItem>
@ -836,7 +895,9 @@ export class Message extends React.PureComponent<Props, State> {
attributes={{
className: 'module-message__context__more-info',
}}
onClick={onShowDetail}
onClick={() => {
showMessageDetail(id);
}}
>
{i18n('moreInfo')}
</MenuItem>
@ -845,7 +906,9 @@ export class Message extends React.PureComponent<Props, State> {
attributes={{
className: 'module-message__context__retry-send',
}}
onClick={onRetrySend}
onClick={() => {
retrySend(id);
}}
>
{i18n('retrySend')}
</MenuItem>
@ -854,7 +917,9 @@ export class Message extends React.PureComponent<Props, State> {
attributes={{
className: 'module-message__context__delete-message',
}}
onClick={onDelete}
onClick={() => {
deleteMessage(id);
}}
>
{i18n('deleteMessage')}
</MenuItem>

View File

@ -59,7 +59,9 @@ export class MessageDetail extends React.Component<Props> {
return (
<div className="module-message-detail__delete-button-container">
<button
onClick={message.onDelete}
onClick={() => {
message.deleteMessage(message.id);
}}
className="module-message-detail__delete-button"
>
{i18n('deleteThisMessage')}

View File

@ -5,22 +5,31 @@ import { ContactName } from './ContactName';
import { Intl } from '../Intl';
import { LocalizerType } from '../../types/Util';
interface Contact {
interface ContactType {
id: string;
phoneNumber: string;
profileName?: string;
name?: string;
}
interface Props {
type PropsData = {
isGroup: boolean;
contact: Contact;
contact: ContactType;
};
type PropsHousekeeping = {
i18n: LocalizerType;
onVerify: () => void;
}
};
export type PropsActions = {
showIdentity: (id: string) => void;
};
type Props = PropsData & PropsHousekeeping & PropsActions;
export class SafetyNumberNotification extends React.Component<Props> {
public render() {
const { contact, isGroup, i18n, onVerify } = this.props;
const { contact, isGroup, i18n, showIdentity } = this.props;
const changeKey = isGroup
? 'safetyNumberChangedGroup'
: 'safetyNumberChanged';
@ -50,7 +59,9 @@ export class SafetyNumberNotification extends React.Component<Props> {
</div>
<div
role="button"
onClick={onVerify}
onClick={() => {
showIdentity(contact.id);
}}
className="module-verification-notification__button"
>
{i18n('verifyNewNumber')}

View File

@ -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;
}) {

View File

@ -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);
});

View File

@ -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<Phone>;
email?: Array<Email>;
address?: Array<PostalAddress>;
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;

View File

@ -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>;
contact: Array<ContactType>;
}>
>;