diff --git a/js/modules/signal.js b/js/modules/signal.js index 164af85ae..ebddb2500 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -188,6 +188,7 @@ function initializeMigrations({ const attachmentsPath = getPath(userDataPath); const readAttachmentData = createReader(attachmentsPath); const loadAttachmentData = Type.loadData(readAttachmentData); + const loadContactData = MessageType.loadContactData(loadAttachmentData); const loadPreviewData = MessageType.loadPreviewData(loadAttachmentData); const loadQuoteData = MessageType.loadQuoteData(loadAttachmentData); const loadStickerData = MessageType.loadStickerData(loadAttachmentData); @@ -248,6 +249,7 @@ function initializeMigrations({ getAbsoluteStickerPath, getAbsoluteTempPath, loadAttachmentData, + loadContactData, loadMessage: MessageType.createAttachmentLoader(loadAttachmentData), loadPreviewData, loadQuoteData, diff --git a/js/modules/types/message.js b/js/modules/types/message.js index 7247129b3..5bb5dd783 100644 --- a/js/modules/types/message.js +++ b/js/modules/types/message.js @@ -562,6 +562,42 @@ exports.loadQuoteData = loadAttachmentData => { }; }; +exports.loadContactData = loadAttachmentData => { + if (!isFunction(loadAttachmentData)) { + throw new TypeError('loadContactData: loadAttachmentData is required'); + } + + return async contact => { + if (!contact) { + return null; + } + + return Promise.all( + contact.map(async item => { + if ( + !item || + !item.avatar || + !item.avatar.avatar || + !item.avatar.avatar.path + ) { + return item; + } + + return { + ...item, + avatar: { + ...item.avatar, + avatar: { + ...item.avatar.avatar, + ...(await loadAttachmentData(item.avatar.avatar)), + }, + }, + }; + }) + ); + }; +}; + exports.loadPreviewData = loadAttachmentData => { if (!isFunction(loadAttachmentData)) { throw new TypeError('loadPreviewData: loadAttachmentData is required'); diff --git a/ts/components/ForwardMessageModal.stories.tsx b/ts/components/ForwardMessageModal.stories.tsx index 48d165fc8..05df2a187 100644 --- a/ts/components/ForwardMessageModal.stories.tsx +++ b/ts/components/ForwardMessageModal.stories.tsx @@ -48,6 +48,7 @@ const useProps = (overrideProps: Partial = {}): PropsType => ({ doForwardMessage: action('doForwardMessage'), getPreferredBadge: () => undefined, i18n, + hasContact: Boolean(overrideProps.hasContact), isSticker: Boolean(overrideProps.isSticker), linkPreview: overrideProps.linkPreview, messageBody: text('messageBody', overrideProps.messageBody || ''), @@ -75,6 +76,10 @@ story.add('a sticker', () => { return ; }); +story.add('with a contact', () => { + return ; +}); + story.add('link preview', () => { return ( void; getPreferredBadge: PreferredBadgeSelectorType; + hasContact: boolean; i18n: LocalizerType; isSticker: boolean; linkPreview?: LinkPreviewType; @@ -79,6 +80,7 @@ export const ForwardMessageModal: FunctionComponent = ({ candidateConversations, doForwardMessage, getPreferredBadge, + hasContact, i18n, isSticker, linkPreview, @@ -110,7 +112,7 @@ export const ForwardMessageModal: FunctionComponent = ({ const [messageBodyText, setMessageBodyText] = useState(messageBody || ''); const [cannotMessage, setCannotMessage] = useState(false); - const isMessageEditable = !isSticker; + const isMessageEditable = !isSticker && !hasContact; const hasSelectedMaximumNumberOfContacts = selectedContacts.length >= MAX_FORWARD; @@ -142,6 +144,7 @@ export const ForwardMessageModal: FunctionComponent = ({ hasContactsSelected && (Boolean(messageBodyText) || isSticker || + hasContact || (attachmentsToForward && attachmentsToForward.length)); const forwardMessage = React.useCallback(() => { diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index dabe7d710..2a9acb743 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -1710,6 +1710,7 @@ export class Message extends React.PureComponent { const { attachments, canDownload, + contact, canReact, canReply, canRetry, @@ -1729,7 +1730,7 @@ export class Message extends React.PureComponent { text, } = this.props; - const canForward = !isTapToView && !deletedForEveryone; + const canForward = !isTapToView && !deletedForEveryone && !contact; const multipleAttachments = attachments && attachments.length > 1; const shouldShowAdditional = diff --git a/ts/jobs/helpers/sendNormalMessage.ts b/ts/jobs/helpers/sendNormalMessage.ts index 9cf90818d..5d50d84ff 100644 --- a/ts/jobs/helpers/sendNormalMessage.ts +++ b/ts/jobs/helpers/sendNormalMessage.ts @@ -14,7 +14,10 @@ import { handleMessageSend } from '../../util/handleMessageSend'; import type { CallbackResultType } from '../../textsecure/Types.d'; import { isSent } from '../../messages/MessageSendState'; import { isOutgoing } from '../../state/selectors/message'; -import type { AttachmentType } from '../../textsecure/SendMessage'; +import type { + AttachmentType, + ContactWithHydratedAvatar, +} from '../../textsecure/SendMessage'; import type { LinkPreviewType } from '../../types/message/LinkPreviews'; import type { BodyRangesType, StoryContextType } from '../../types/Util'; import type { WhatIsThis } from '../../window.d'; @@ -131,6 +134,7 @@ export async function sendNormalMessage( const { attachments, body, + contact, deletedForEveryoneTimestamp, expireTimer, mentions, @@ -148,11 +152,12 @@ export async function sendNormalMessage( const dataMessage = await window.textsecure.messaging.getDataMessage({ attachments, body, + contact, + deletedForEveryoneTimestamp, + expireTimer, groupV2: conversation.getGroupV2Info({ members: recipientIdentifiersWithoutMe, }), - deletedForEveryoneTimestamp, - expireTimer, preview, profileKey, quote, @@ -188,6 +193,7 @@ export async function sendNormalMessage( contentHint: ContentHint.RESENDABLE, groupSendOptions: { attachments, + contact, deletedForEveryoneTimestamp, expireTimer, groupV1: conversation.getGroupV1Info( @@ -237,21 +243,22 @@ export async function sendNormalMessage( log.info('sending direct message'); innerPromise = window.textsecure.messaging.sendMessageToIdentifier({ + attachments, + contact, + contentHint: ContentHint.RESENDABLE, + deletedForEveryoneTimestamp, + expireTimer, + groupId: undefined, identifier: recipientIdentifiersWithoutMe[0], messageText: body, - attachments, - quote, - preview, - sticker, - reaction: undefined, - deletedForEveryoneTimestamp, - timestamp: messageTimestamp, - expireTimer, - contentHint: ContentHint.RESENDABLE, - groupId: undefined, - profileKey, options: sendOptions, + preview, + profileKey, + quote, + reaction: undefined, + sticker, storyContext, + timestamp: messageTimestamp, }); } @@ -380,6 +387,7 @@ async function getMessageSendData({ }>): Promise<{ attachments: Array; body: undefined | string; + contact?: Array; deletedForEveryoneTimestamp: undefined | number; expireTimer: undefined | number; mentions: undefined | BodyRangesType; @@ -391,6 +399,7 @@ async function getMessageSendData({ }> { const { loadAttachmentData, + loadContactData, loadPreviewData, loadQuoteData, loadStickerData, @@ -413,13 +422,15 @@ async function getMessageSendData({ const storyId = message.get('storyId'); - const [attachmentsWithData, preview, quote, sticker, storyMessage] = + const [attachmentsWithData, contact, preview, quote, sticker, storyMessage] = await Promise.all([ // We don't update the caches here because (1) we expect the caches to be populated // on initial send, so they should be there in the 99% case (2) if you're retrying // a failed message across restarts, we don't touch the cache for simplicity. If // sends are failing, let's not add the complication of a cache. Promise.all((message.get('attachments') ?? []).map(loadAttachmentData)), + message.cachedOutgoingContactData || + loadContactData(message.get('contact')), message.cachedOutgoingPreviewData || loadPreviewData(message.get('preview')), message.cachedOutgoingQuoteData || loadQuoteData(message.get('quote')), @@ -439,6 +450,7 @@ async function getMessageSendData({ return { attachments, body, + contact, deletedForEveryoneTimestamp: message.get('deletedForEveryoneTimestamp'), expireTimer: message.get('expireTimer'), mentions: message.get('bodyRanges'), diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts index 533f4695a..aec4f78b2 100644 --- a/ts/models/conversations.ts +++ b/ts/models/conversations.ts @@ -29,6 +29,7 @@ import * as Conversation from '../types/Conversation'; import * as Stickers from '../types/Stickers'; import { CapabilityError } from '../types/errors'; import type { + ContactWithHydratedAvatar, GroupV1InfoType, GroupV2InfoType, StickerType, @@ -3834,7 +3835,11 @@ export class ConversationModel extends window.Backbone }, }; - this.enqueueMessageForSend(undefined, [], undefined, [], sticker); + this.enqueueMessageForSend({ + body: undefined, + attachments: [], + sticker, + }); window.reduxActions.stickers.useSticker(packId, stickerId); } @@ -3925,12 +3930,23 @@ export class ConversationModel extends window.Backbone } async enqueueMessageForSend( - body: string | undefined, - attachments: Array, - quote?: QuotedMessageType, - preview?: Array, - sticker?: StickerType, - mentions?: BodyRangesType, + { + attachments, + body, + contact, + mentions, + preview, + quote, + sticker, + }: { + attachments: Array; + body: string | undefined; + contact?: Array; + mentions?: BodyRangesType; + preview?: Array; + quote?: QuotedMessageType; + sticker?: StickerType; + }, { dontClearDraft, sendHQImages, @@ -4000,6 +4016,7 @@ export class ConversationModel extends window.Backbone type: 'outgoing', body, conversationId: this.id, + contact, quote, preview, attachments: attachmentsToSend, @@ -4031,6 +4048,7 @@ export class ConversationModel extends window.Backbone const model = new window.Whisper.Message(attributes); const message = window.MessageController.register(model.id, model); + message.cachedOutgoingContactData = contact; message.cachedOutgoingPreviewData = preview; message.cachedOutgoingQuoteData = quote; message.cachedOutgoingStickerData = sticker; diff --git a/ts/models/messages.ts b/ts/models/messages.ts index fe2248505..fb3690dfa 100644 --- a/ts/models/messages.ts +++ b/ts/models/messages.ts @@ -151,6 +151,7 @@ import type { ConversationQueueJobData } from '../jobs/conversationJobQueue'; import { getMessageById } from '../messages/getMessageById'; import { shouldDownloadStory } from '../util/shouldDownloadStory'; import { shouldShowStoriesView } from '../state/selectors/stories'; +import type { ContactWithHydratedAvatar } from '../textsecure/SendMessage'; /* eslint-disable camelcase */ /* eslint-disable more/no-then */ @@ -188,6 +189,8 @@ export class MessageModel extends window.Backbone.Model { syncPromise?: Promise; + cachedOutgoingContactData?: Array; + cachedOutgoingPreviewData?: Array; cachedOutgoingQuoteData?: WhatIsThis; diff --git a/ts/state/ducks/stories.ts b/ts/state/ducks/stories.ts index 5668dd4c5..8bd7c5bc7 100644 --- a/ts/state/ducks/stories.ts +++ b/ts/state/ducks/stories.ts @@ -230,12 +230,11 @@ function replyToStory( if (conversation) { conversation.enqueueMessageForSend( - messageBody, - [], - undefined, - undefined, - undefined, - mentions, + { + body: messageBody, + attachments: [], + mentions, + }, { storyId: story.messageId, timestamp, diff --git a/ts/state/smart/ForwardMessageModal.tsx b/ts/state/smart/ForwardMessageModal.tsx index de2fd2abb..b2d1d4123 100644 --- a/ts/state/smart/ForwardMessageModal.tsx +++ b/ts/state/smart/ForwardMessageModal.tsx @@ -24,6 +24,7 @@ export type SmartForwardMessageModalProps = { attachments?: Array, linkPreview?: LinkPreviewType ) => void; + hasContact: boolean; isSticker: boolean; messageBody?: string; onClose: () => void; @@ -42,6 +43,7 @@ const mapStateToProps = ( const { attachments, doForwardMessage, + hasContact, isSticker, messageBody, onClose, @@ -59,6 +61,7 @@ const mapStateToProps = ( candidateConversations, doForwardMessage, getPreferredBadge: getPreferredBadgeSelector(state), + hasContact, i18n: getIntl(state), isSticker, linkPreview, diff --git a/ts/textsecure/SendMessage.ts b/ts/textsecure/SendMessage.ts index 02031ee47..4fb0f8e04 100644 --- a/ts/textsecure/SendMessage.ts +++ b/ts/textsecure/SendMessage.ts @@ -2,7 +2,6 @@ // SPDX-License-Identifier: AGPL-3.0-only /* eslint-disable no-nested-ternary */ -/* eslint-disable more/no-then */ /* eslint-disable no-bitwise */ /* eslint-disable max-classes-per-file */ @@ -69,6 +68,12 @@ import type { SendTypesType } from '../util/handleMessageSend'; import { shouldSaveProto, sendTypesEnum } from '../util/handleMessageSend'; import { SignalService as Proto } from '../protobuf'; import * as log from '../logging/log'; +import type { Avatar, EmbeddedContactType } from '../types/EmbeddedContact'; +import { + numberToPhoneType, + numberToEmailType, + numberToAddressType, +} from '../types/EmbeddedContact'; export type SendMetadataType = { [identifier: string]: { @@ -172,9 +177,16 @@ function makeAttachmentSendReady( }; } +export type ContactWithHydratedAvatar = EmbeddedContactType & { + avatar?: Avatar & { + attachmentPointer?: Proto.IAttachmentPointer; + }; +}; + export type MessageOptionsType = { attachments?: ReadonlyArray | null; body?: string; + contact?: Array; expireTimer?: number; flags?: number; group?: { @@ -197,21 +209,22 @@ export type MessageOptionsType = { }; export type GroupSendOptionsType = { attachments?: Array; + contact?: Array; + deletedForEveryoneTimestamp?: number; expireTimer?: number; flags?: number; - groupV2?: GroupV2InfoType; + groupCallUpdate?: GroupCallUpdateType; groupV1?: GroupV1InfoType; + groupV2?: GroupV2InfoType; + mentions?: BodyRangesType; messageText?: string; preview?: ReadonlyArray; profileKey?: Uint8Array; quote?: QuoteType; reaction?: ReactionType; sticker?: StickerType; - deletedForEveryoneTimestamp?: number; - timestamp: number; - mentions?: BodyRangesType; - groupCallUpdate?: GroupCallUpdateType; storyContext?: StoryContextType; + timestamp: number; }; class Message { @@ -219,6 +232,8 @@ class Message { body?: string; + contact?: Array; + expireTimer?: number; flags?: number; @@ -261,6 +276,7 @@ class Message { constructor(options: MessageOptionsType) { this.attachments = options.attachments || []; this.body = options.body; + this.contact = options.contact; this.expireTimer = options.expireTimer; this.flags = options.flags; this.group = options.group; @@ -403,6 +419,74 @@ class Message { return item; }); } + if (Array.isArray(this.contact)) { + proto.contact = this.contact.map(contact => { + const contactProto = new Proto.DataMessage.Contact(); + if (contact.name) { + const nameProto: Proto.DataMessage.Contact.IName = { + givenName: contact.name.givenName, + familyName: contact.name.familyName, + prefix: contact.name.prefix, + suffix: contact.name.suffix, + middleName: contact.name.middleName, + displayName: contact.name.displayName, + }; + contactProto.name = new Proto.DataMessage.Contact.Name(nameProto); + } + if (Array.isArray(contact.number)) { + contactProto.number = contact.number.map(number => { + const numberProto: Proto.DataMessage.Contact.IPhone = { + value: number.value, + type: numberToPhoneType(number.type), + label: number.label, + }; + + return new Proto.DataMessage.Contact.Phone(numberProto); + }); + } + if (Array.isArray(contact.email)) { + contactProto.email = contact.email.map(email => { + const emailProto: Proto.DataMessage.Contact.IEmail = { + value: email.value, + type: numberToEmailType(email.type), + label: email.label, + }; + + return new Proto.DataMessage.Contact.Email(emailProto); + }); + } + if (Array.isArray(contact.address)) { + contactProto.address = contact.address.map(address => { + const addressProto: Proto.DataMessage.Contact.IPostalAddress = { + type: numberToAddressType(address.type), + label: address.label, + street: address.street, + pobox: address.pobox, + neighborhood: address.neighborhood, + city: address.city, + region: address.region, + postcode: address.postcode, + country: address.country, + }; + + return new Proto.DataMessage.Contact.PostalAddress(addressProto); + }); + } + if (contact.avatar && contact.avatar.attachmentPointer) { + const avatarProto = new Proto.DataMessage.Contact.Avatar(); + avatarProto.avatar = contact.avatar.attachmentPointer; + avatarProto.isProfile = Boolean(contact.avatar.isProfile); + contactProto.avatar = avatarProto; + } + + if (contact.organization) { + contactProto.organization = contact.organization; + } + + return contactProto; + }); + } + if (this.quote) { const { QuotedAttachment } = Proto.DataMessage.Quote; const { BodyRange, Quote } = Proto.DataMessage; @@ -559,14 +643,17 @@ export default class MessageSender { } async makeAttachmentPointer( - attachment: Readonly + attachment: Readonly< + Partial & + Pick + > ): Promise { assert( typeof attachment === 'object' && attachment !== null, 'Got null attachment in `makeAttachmentPointer`' ); - const { data, size } = attachment; + const { data, size, contentType } = attachment; if (!(data instanceof Uint8Array)) { throw new Error( `makeAttachmentPointer: data was a '${typeof data}' instead of Uint8Array` @@ -577,6 +664,11 @@ export default class MessageSender { `makeAttachmentPointer: Size ${size} did not match data.byteLength ${data.byteLength}` ); } + if (typeof contentType !== 'string') { + throw new Error( + `makeAttachmentPointer: contentType ${contentType} was not a string` + ); + } const padded = this.getPaddedAttachment(data); const key = getRandomBytes(64); @@ -589,7 +681,7 @@ export default class MessageSender { proto.cdnId = Long.fromString(id); proto.contentType = attachment.contentType; proto.key = key; - proto.size = attachment.size; + proto.size = data.byteLength; proto.digest = result.digest; if (attachment.fileName) { @@ -615,22 +707,20 @@ export default class MessageSender { } async uploadAttachments(message: Message): Promise { - await Promise.all( - message.attachments.map(attachment => - this.makeAttachmentPointer(attachment) - ) - ) - .then(attachmentPointers => { - // eslint-disable-next-line no-param-reassign - message.attachmentPointers = attachmentPointers; - }) - .catch(error => { - if (error instanceof HTTPError) { - throw new MessageError(message, error); - } else { - throw error; - } - }); + try { + // eslint-disable-next-line no-param-reassign + message.attachmentPointers = await Promise.all( + message.attachments.map(attachment => + this.makeAttachmentPointer(attachment) + ) + ); + } catch (error) { + if (error instanceof HTTPError) { + throw new MessageError(message, error); + } else { + throw error; + } + } } async uploadLinkPreviews(message: Message): Promise { @@ -687,32 +777,68 @@ export default class MessageSender { } } - async uploadThumbnails(message: Message): Promise { - const makePointer = this.makeAttachmentPointer.bind(this); - const { quote } = message; - - if (!quote || !quote.attachments || quote.attachments.length === 0) { + async uploadContactAvatar(message: Message): Promise { + const { contact } = message; + if (!contact || contact.length === 0) { return; } - await Promise.all( - quote.attachments.map((attachment: QuoteAttachmentType) => { - if (!attachment.thumbnail) { - return null; - } + try { + await Promise.all( + contact.map(async (item: ContactWithHydratedAvatar) => { + const itemAvatar = item?.avatar; + const avatar = itemAvatar?.avatar; + + if (!itemAvatar || !avatar || !avatar.data) { + return; + } + + const attachment = makeAttachmentSendReady(avatar); + if (!attachment) { + return; + } - return makePointer(attachment.thumbnail).then(pointer => { // eslint-disable-next-line no-param-reassign - attachment.attachmentPointer = pointer; - }); - }) - ).catch(error => { + itemAvatar.attachmentPointer = await this.makeAttachmentPointer( + attachment + ); + }) + ); + } catch (error) { if (error instanceof HTTPError) { throw new MessageError(message, error); } else { throw error; } - }); + } + } + + async uploadThumbnails(message: Message): Promise { + const { quote } = message; + if (!quote || !quote.attachments || quote.attachments.length === 0) { + return; + } + + try { + await Promise.all( + quote.attachments.map(async (attachment: QuoteAttachmentType) => { + if (!attachment.thumbnail) { + return; + } + + // eslint-disable-next-line no-param-reassign + attachment.attachmentPointer = await this.makeAttachmentPointer( + attachment.thumbnail + ); + }) + ); + } catch (error) { + if (error instanceof HTTPError) { + throw new MessageError(message, error); + } else { + throw error; + } + } } // Proto assembly @@ -742,6 +868,7 @@ export default class MessageSender { const message = new Message(attributes); await Promise.all([ this.uploadAttachments(message), + this.uploadContactAvatar(message), this.uploadThumbnails(message), this.uploadLinkPreviews(message), this.uploadSticker(message), @@ -789,6 +916,7 @@ export default class MessageSender { ): MessageOptionsType { const { attachments, + contact, deletedForEveryoneTimestamp, expireTimer, flags, @@ -839,6 +967,7 @@ export default class MessageSender { return { attachments, body: messageText, + contact, deletedForEveryoneTimestamp, expireTimer, flags, @@ -883,33 +1012,25 @@ export default class MessageSender { groupId: string | undefined; options?: SendOptionsType; }>): Promise { - const message = new Message(messageOptions); + const message = await this.getHydratedMessage(messageOptions); - return Promise.all([ - this.uploadAttachments(message), - this.uploadThumbnails(message), - this.uploadLinkPreviews(message), - this.uploadSticker(message), - ]).then( - async (): Promise => - new Promise((resolve, reject) => { - this.sendMessageProto({ - callback: (res: CallbackResultType) => { - if (res.errors && res.errors.length > 0) { - reject(new SendMessageProtoError(res)); - } else { - resolve(res); - } - }, - contentHint, - groupId, - options, - proto: message.toProto(), - recipients: message.recipients || [], - timestamp: message.timestamp, - }); - }) - ); + return new Promise((resolve, reject) => { + this.sendMessageProto({ + callback: (res: CallbackResultType) => { + if (res.errors && res.errors.length > 0) { + reject(new SendMessageProtoError(res)); + } else { + resolve(res); + } + }, + contentHint, + groupId, + options, + proto: message.toProto(), + recipients: message.recipients || [], + timestamp: message.timestamp, + }); + }); } sendMessageProto({ @@ -1033,52 +1154,55 @@ export default class MessageSender { // You might wonder why this takes a groupId. models/messages.resend() can send a group // message to just one person. async sendMessageToIdentifier({ + attachments, + contact, + contentHint, + deletedForEveryoneTimestamp, + expireTimer, + groupId, identifier, messageText, - attachments, - quote, - preview, - sticker, - reaction, - deletedForEveryoneTimestamp, - timestamp, - expireTimer, - contentHint, - groupId, - profileKey, options, + preview, + profileKey, + quote, + reaction, + sticker, storyContext, + timestamp, }: Readonly<{ + attachments: ReadonlyArray | undefined; + contact?: Array; + contentHint: number; + deletedForEveryoneTimestamp: number | undefined; + expireTimer: number | undefined; + groupId: string | undefined; identifier: string; messageText: string | undefined; - attachments: ReadonlyArray | undefined; - quote?: QuoteType; - preview?: ReadonlyArray | undefined; - sticker?: StickerType; - reaction?: ReactionType; - deletedForEveryoneTimestamp: number | undefined; - timestamp: number; - expireTimer: number | undefined; - contentHint: number; - groupId: string | undefined; - profileKey?: Uint8Array; - storyContext?: StoryContextType; options?: SendOptionsType; + preview?: ReadonlyArray | undefined; + profileKey?: Uint8Array; + quote?: QuoteType; + reaction?: ReactionType; + sticker?: StickerType; + storyContext?: StoryContextType; + timestamp: number; }>): Promise { return this.sendMessage({ messageOptions: { - recipients: [identifier], - body: messageText, - timestamp, attachments, - quote, - preview, - sticker, - reaction, + body: messageText, + contact, deletedForEveryoneTimestamp, expireTimer, + preview, profileKey, + quote, + reaction, + recipients: [identifier], + sticker, storyContext, + timestamp, }, contentHint, groupId, diff --git a/ts/types/EmbeddedContact.ts b/ts/types/EmbeddedContact.ts index 94e1893a8..09398fd67 100644 --- a/ts/types/EmbeddedContact.ts +++ b/ts/types/EmbeddedContact.ts @@ -83,6 +83,51 @@ const DEFAULT_PHONE_TYPE = Proto.DataMessage.Contact.Phone.Type.HOME; const DEFAULT_EMAIL_TYPE = Proto.DataMessage.Contact.Email.Type.HOME; const DEFAULT_ADDRESS_TYPE = Proto.DataMessage.Contact.PostalAddress.Type.HOME; +export function numberToPhoneType( + type: number +): Proto.DataMessage.Contact.Phone.Type { + if (type === Proto.DataMessage.Contact.Phone.Type.MOBILE) { + return type; + } + if (type === Proto.DataMessage.Contact.Phone.Type.WORK) { + return type; + } + if (type === Proto.DataMessage.Contact.Phone.Type.CUSTOM) { + return type; + } + + return DEFAULT_PHONE_TYPE; +} + +export function numberToEmailType( + type: number +): Proto.DataMessage.Contact.Email.Type { + if (type === Proto.DataMessage.Contact.Email.Type.MOBILE) { + return type; + } + if (type === Proto.DataMessage.Contact.Email.Type.WORK) { + return type; + } + if (type === Proto.DataMessage.Contact.Email.Type.CUSTOM) { + return type; + } + + return DEFAULT_EMAIL_TYPE; +} + +export function numberToAddressType( + type: number +): Proto.DataMessage.Contact.PostalAddress.Type { + if (type === Proto.DataMessage.Contact.PostalAddress.Type.WORK) { + return type; + } + if (type === Proto.DataMessage.Contact.PostalAddress.Type.CUSTOM) { + return type; + } + + return DEFAULT_ADDRESS_TYPE; +} + export function embeddedContactSelector( contact: EmbeddedContactType, options: { diff --git a/ts/views/conversation_view.ts b/ts/views/conversation_view.ts index 1cc831d4b..c4c6e1a52 100644 --- a/ts/views/conversation_view.ts +++ b/ts/views/conversation_view.ts @@ -136,6 +136,7 @@ const { getAbsoluteAttachmentPath, getAbsoluteTempPath, loadAttachmentData, + loadContactData, loadPreviewData, loadStickerData, openFileInFolder, @@ -1284,34 +1285,37 @@ export class ConversationView extends window.Backbone.View { } const attachments = getAttachmentsForMessage(message.attributes); + const doForwardMessage = async ( + conversationIds: Array, + messageBody?: string, + includedAttachments?: Array, + linkPreview?: LinkPreviewType + ) => { + try { + const didForwardSuccessfully = await this.maybeForwardMessage( + message, + conversationIds, + messageBody, + includedAttachments, + linkPreview + ); + + if (didForwardSuccessfully && this.forwardMessageModal) { + this.forwardMessageModal.remove(); + this.forwardMessageModal = undefined; + } + } catch (err) { + log.warn('doForwardMessage', err && err.stack ? err.stack : err); + } + }; + this.forwardMessageModal = new Whisper.ReactWrapperView({ JSX: window.Signal.State.Roots.createForwardMessageModal( window.reduxStore, { attachments, - doForwardMessage: async ( - conversationIds: Array, - messageBody?: string, - includedAttachments?: Array, - linkPreview?: LinkPreviewType - ) => { - try { - const didForwardSuccessfully = await this.maybeForwardMessage( - message, - conversationIds, - messageBody, - includedAttachments, - linkPreview - ); - - if (didForwardSuccessfully && this.forwardMessageModal) { - this.forwardMessageModal.remove(); - this.forwardMessageModal = undefined; - } - } catch (err) { - log.warn('doForwardMessage', err && err.stack ? err.stack : err); - } - }, + doForwardMessage, + hasContact: Boolean(message.get('contact')), isSticker: Boolean(message.get('sticker')), messageBody: message.getRawText(), onClose: () => { @@ -1433,6 +1437,8 @@ export class ConversationView extends window.Backbone.View { const timestamp = baseTimestamp + offset; if (conversation) { const sticker = message.get('sticker'); + const contact = message.get('contact'); + if (sticker) { const stickerWithData = await loadStickerData(sticker); const stickerNoPath = stickerWithData @@ -1446,12 +1452,21 @@ export class ConversationView extends window.Backbone.View { : undefined; conversation.enqueueMessageForSend( - undefined, // body - [], - undefined, // quote - [], - stickerNoPath, - undefined, // BodyRanges + { + body: undefined, + attachments: [], + sticker: stickerNoPath, + }, + { ...sendMessageOptions, timestamp } + ); + } else if (contact) { + const contactWithHydratedAvatar = await loadContactData(contact); + conversation.enqueueMessageForSend( + { + body: undefined, + attachments: [], + contact: contactWithHydratedAvatar, + }, { ...sendMessageOptions, timestamp } ); } else { @@ -1472,12 +1487,11 @@ export class ConversationView extends window.Backbone.View { ); conversation.enqueueMessageForSend( - messageBody || undefined, - attachmentsToSend, - undefined, // quote - preview, - undefined, // sticker - undefined, // BodyRanges + { + body: messageBody || undefined, + attachments: attachmentsToSend, + preview, + }, { ...sendMessageOptions, timestamp } ); } @@ -2885,12 +2899,13 @@ export class ConversationView extends window.Backbone.View { log.info('Send pre-checks took', sendDelta, 'milliseconds'); model.enqueueMessageForSend( - message, - attachments, - this.quote, - this.getLinkPreviewForSend(message), - undefined, // sticker - mentions, + { + body: message, + attachments, + quote: this.quote, + preview: this.getLinkPreviewForSend(message), + mentions, + }, { sendHQImages, timestamp, diff --git a/ts/window.d.ts b/ts/window.d.ts index bf75f40ed..15fa26664 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -3,20 +3,17 @@ // Captures the globals put in place by preload.js, background.js and others -import { DeepPartial, Store } from 'redux'; +import { Store } from 'redux'; import * as Backbone from 'backbone'; import * as Underscore from 'underscore'; import moment from 'moment'; import PQueue from 'p-queue/dist'; import { Ref } from 'react'; import { imageToBlurHash } from './util/imageToBlurHash'; -import type { ParsedUrlQuery } from 'querystring'; import * as Util from './util'; import { ConversationModelCollectionType, MessageModelCollectionType, - MessageAttributesType, - ReactionAttributesType, } from './model-types.d'; import { TextSecureType } from './textsecure.d'; import { Storage } from './textsecure/Storage'; @@ -32,11 +29,8 @@ import * as Curve from './Curve'; import * as RemoteConfig from './RemoteConfig'; import * as OS from './OS'; import { getEnvironment } from './environment'; -import * as zkgroup from './util/zkgroup'; -import { LocalizerType, BodyRangesType, BodyRangeType } from './types/Util'; -import * as EmbeddedContact from './types/EmbeddedContact'; +import { LocalizerType } from './types/Util'; import type { Receipt } from './types/Receipt'; -import * as Errors from './types/errors'; import { ConversationController } from './ConversationController'; import { ReduxActions } from './state/types'; import { createStore } from './state/createStore'; @@ -71,13 +65,11 @@ import * as stickersDuck from './state/ducks/stickers'; import * as conversationsSelectors from './state/selectors/conversations'; import * as searchSelectors from './state/selectors/search'; import AccountManager from './textsecure/AccountManager'; -import { SendOptionsType } from './textsecure/SendMessage'; +import { ContactWithHydratedAvatar } from './textsecure/SendMessage'; import Data from './sql/Client'; -import { UserMessage } from './types/Message'; import { PhoneNumberFormat } from 'google-libphonenumber'; import { MessageModel } from './models/messages'; import { ConversationModel } from './models/conversations'; -import { combineNames } from './util'; import { BatcherType } from './util/batcher'; import { AttachmentList } from './components/conversation/AttachmentList'; import { ChatColorPicker } from './components/ChatColorPicker'; @@ -93,14 +85,12 @@ import { Quote } from './components/conversation/Quote'; import { StagedLinkPreview } from './components/conversation/StagedLinkPreview'; import { DisappearingTimeDialog } from './components/DisappearingTimeDialog'; import { WhatsNewLink } from './components/WhatsNewLink'; -import { MIMEType } from './types/MIME'; import { DownloadedAttachmentType } from './types/Attachment'; import { ElectronLocaleType } from './util/mapToSupportLocale'; import { SignalProtocolStore } from './SignalProtocolStore'; import { StartupQueue } from './util/StartupQueue'; import { SocketStatus } from './types/SocketStatus'; import SyncRequest from './textsecure/SyncRequest'; -import { ConversationColorType, CustomColorType } from './types/Colors'; import { MessageController } from './util/MessageController'; import { StateType } from './state/reducer'; import { SystemTraySetting } from './types/SystemTraySetting'; @@ -108,15 +98,13 @@ import { UUID } from './types/UUID'; import { Address } from './types/Address'; import { QualifiedAddress } from './types/QualifiedAddress'; import { CI } from './CI'; -import { IPCEventsType, IPCEventsValuesType } from './util/createIPCEvents'; +import { IPCEventsType } from './util/createIPCEvents'; import { ConversationView } from './views/conversation_view'; import type { SignalContextType } from './windows/context'; -import { GroupV2Change } from './components/conversation/GroupV2Change'; +import type { EmbeddedContactType } from './types/EmbeddedContact'; export { Long } from 'long'; -type TaskResultType = any; - export type WhatIsThis = any; // Synced with the type in ts/shims/showConfirmationDialog @@ -311,6 +299,9 @@ declare global { } >; loadQuoteData: (quote: unknown) => WhatIsThis; + loadContactData: ( + contact?: Array + ) => Promise | undefined>; loadPreviewData: (preview: unknown) => WhatIsThis; loadStickerData: (sticker: unknown) => WhatIsThis; readStickerData: (path: string) => Promise;