Disable forward for messages with embedded contact

This commit is contained in:
Scott Nonnenberg 2022-04-11 13:57:44 -07:00 committed by GitHub
parent 6d816d01ad
commit 7f89f6162f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 442 additions and 185 deletions

View File

@ -188,6 +188,7 @@ function initializeMigrations({
const attachmentsPath = getPath(userDataPath); const attachmentsPath = getPath(userDataPath);
const readAttachmentData = createReader(attachmentsPath); const readAttachmentData = createReader(attachmentsPath);
const loadAttachmentData = Type.loadData(readAttachmentData); const loadAttachmentData = Type.loadData(readAttachmentData);
const loadContactData = MessageType.loadContactData(loadAttachmentData);
const loadPreviewData = MessageType.loadPreviewData(loadAttachmentData); const loadPreviewData = MessageType.loadPreviewData(loadAttachmentData);
const loadQuoteData = MessageType.loadQuoteData(loadAttachmentData); const loadQuoteData = MessageType.loadQuoteData(loadAttachmentData);
const loadStickerData = MessageType.loadStickerData(loadAttachmentData); const loadStickerData = MessageType.loadStickerData(loadAttachmentData);
@ -248,6 +249,7 @@ function initializeMigrations({
getAbsoluteStickerPath, getAbsoluteStickerPath,
getAbsoluteTempPath, getAbsoluteTempPath,
loadAttachmentData, loadAttachmentData,
loadContactData,
loadMessage: MessageType.createAttachmentLoader(loadAttachmentData), loadMessage: MessageType.createAttachmentLoader(loadAttachmentData),
loadPreviewData, loadPreviewData,
loadQuoteData, loadQuoteData,

View File

@ -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 => { exports.loadPreviewData = loadAttachmentData => {
if (!isFunction(loadAttachmentData)) { if (!isFunction(loadAttachmentData)) {
throw new TypeError('loadPreviewData: loadAttachmentData is required'); throw new TypeError('loadPreviewData: loadAttachmentData is required');

View File

@ -48,6 +48,7 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
doForwardMessage: action('doForwardMessage'), doForwardMessage: action('doForwardMessage'),
getPreferredBadge: () => undefined, getPreferredBadge: () => undefined,
i18n, i18n,
hasContact: Boolean(overrideProps.hasContact),
isSticker: Boolean(overrideProps.isSticker), isSticker: Boolean(overrideProps.isSticker),
linkPreview: overrideProps.linkPreview, linkPreview: overrideProps.linkPreview,
messageBody: text('messageBody', overrideProps.messageBody || ''), messageBody: text('messageBody', overrideProps.messageBody || ''),
@ -75,6 +76,10 @@ story.add('a sticker', () => {
return <ForwardMessageModal {...useProps({ isSticker: true })} />; return <ForwardMessageModal {...useProps({ isSticker: true })} />;
}); });
story.add('with a contact', () => {
return <ForwardMessageModal {...useProps({ hasContact: true })} />;
});
story.add('link preview', () => { story.add('link preview', () => {
return ( return (
<ForwardMessageModal <ForwardMessageModal

View File

@ -48,6 +48,7 @@ export type DataPropsType = {
linkPreview?: LinkPreviewType linkPreview?: LinkPreviewType
) => void; ) => void;
getPreferredBadge: PreferredBadgeSelectorType; getPreferredBadge: PreferredBadgeSelectorType;
hasContact: boolean;
i18n: LocalizerType; i18n: LocalizerType;
isSticker: boolean; isSticker: boolean;
linkPreview?: LinkPreviewType; linkPreview?: LinkPreviewType;
@ -79,6 +80,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
candidateConversations, candidateConversations,
doForwardMessage, doForwardMessage,
getPreferredBadge, getPreferredBadge,
hasContact,
i18n, i18n,
isSticker, isSticker,
linkPreview, linkPreview,
@ -110,7 +112,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
const [messageBodyText, setMessageBodyText] = useState(messageBody || ''); const [messageBodyText, setMessageBodyText] = useState(messageBody || '');
const [cannotMessage, setCannotMessage] = useState(false); const [cannotMessage, setCannotMessage] = useState(false);
const isMessageEditable = !isSticker; const isMessageEditable = !isSticker && !hasContact;
const hasSelectedMaximumNumberOfContacts = const hasSelectedMaximumNumberOfContacts =
selectedContacts.length >= MAX_FORWARD; selectedContacts.length >= MAX_FORWARD;
@ -142,6 +144,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
hasContactsSelected && hasContactsSelected &&
(Boolean(messageBodyText) || (Boolean(messageBodyText) ||
isSticker || isSticker ||
hasContact ||
(attachmentsToForward && attachmentsToForward.length)); (attachmentsToForward && attachmentsToForward.length));
const forwardMessage = React.useCallback(() => { const forwardMessage = React.useCallback(() => {

View File

@ -1710,6 +1710,7 @@ export class Message extends React.PureComponent<Props, State> {
const { const {
attachments, attachments,
canDownload, canDownload,
contact,
canReact, canReact,
canReply, canReply,
canRetry, canRetry,
@ -1729,7 +1730,7 @@ export class Message extends React.PureComponent<Props, State> {
text, text,
} = this.props; } = this.props;
const canForward = !isTapToView && !deletedForEveryone; const canForward = !isTapToView && !deletedForEveryone && !contact;
const multipleAttachments = attachments && attachments.length > 1; const multipleAttachments = attachments && attachments.length > 1;
const shouldShowAdditional = const shouldShowAdditional =

View File

@ -14,7 +14,10 @@ import { handleMessageSend } from '../../util/handleMessageSend';
import type { CallbackResultType } from '../../textsecure/Types.d'; import type { CallbackResultType } from '../../textsecure/Types.d';
import { isSent } from '../../messages/MessageSendState'; import { isSent } from '../../messages/MessageSendState';
import { isOutgoing } from '../../state/selectors/message'; 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 { LinkPreviewType } from '../../types/message/LinkPreviews';
import type { BodyRangesType, StoryContextType } from '../../types/Util'; import type { BodyRangesType, StoryContextType } from '../../types/Util';
import type { WhatIsThis } from '../../window.d'; import type { WhatIsThis } from '../../window.d';
@ -131,6 +134,7 @@ export async function sendNormalMessage(
const { const {
attachments, attachments,
body, body,
contact,
deletedForEveryoneTimestamp, deletedForEveryoneTimestamp,
expireTimer, expireTimer,
mentions, mentions,
@ -148,11 +152,12 @@ export async function sendNormalMessage(
const dataMessage = await window.textsecure.messaging.getDataMessage({ const dataMessage = await window.textsecure.messaging.getDataMessage({
attachments, attachments,
body, body,
contact,
deletedForEveryoneTimestamp,
expireTimer,
groupV2: conversation.getGroupV2Info({ groupV2: conversation.getGroupV2Info({
members: recipientIdentifiersWithoutMe, members: recipientIdentifiersWithoutMe,
}), }),
deletedForEveryoneTimestamp,
expireTimer,
preview, preview,
profileKey, profileKey,
quote, quote,
@ -188,6 +193,7 @@ export async function sendNormalMessage(
contentHint: ContentHint.RESENDABLE, contentHint: ContentHint.RESENDABLE,
groupSendOptions: { groupSendOptions: {
attachments, attachments,
contact,
deletedForEveryoneTimestamp, deletedForEveryoneTimestamp,
expireTimer, expireTimer,
groupV1: conversation.getGroupV1Info( groupV1: conversation.getGroupV1Info(
@ -237,21 +243,22 @@ export async function sendNormalMessage(
log.info('sending direct message'); log.info('sending direct message');
innerPromise = window.textsecure.messaging.sendMessageToIdentifier({ innerPromise = window.textsecure.messaging.sendMessageToIdentifier({
attachments,
contact,
contentHint: ContentHint.RESENDABLE,
deletedForEveryoneTimestamp,
expireTimer,
groupId: undefined,
identifier: recipientIdentifiersWithoutMe[0], identifier: recipientIdentifiersWithoutMe[0],
messageText: body, messageText: body,
attachments,
quote,
preview,
sticker,
reaction: undefined,
deletedForEveryoneTimestamp,
timestamp: messageTimestamp,
expireTimer,
contentHint: ContentHint.RESENDABLE,
groupId: undefined,
profileKey,
options: sendOptions, options: sendOptions,
preview,
profileKey,
quote,
reaction: undefined,
sticker,
storyContext, storyContext,
timestamp: messageTimestamp,
}); });
} }
@ -380,6 +387,7 @@ async function getMessageSendData({
}>): Promise<{ }>): Promise<{
attachments: Array<AttachmentType>; attachments: Array<AttachmentType>;
body: undefined | string; body: undefined | string;
contact?: Array<ContactWithHydratedAvatar>;
deletedForEveryoneTimestamp: undefined | number; deletedForEveryoneTimestamp: undefined | number;
expireTimer: undefined | number; expireTimer: undefined | number;
mentions: undefined | BodyRangesType; mentions: undefined | BodyRangesType;
@ -391,6 +399,7 @@ async function getMessageSendData({
}> { }> {
const { const {
loadAttachmentData, loadAttachmentData,
loadContactData,
loadPreviewData, loadPreviewData,
loadQuoteData, loadQuoteData,
loadStickerData, loadStickerData,
@ -413,13 +422,15 @@ async function getMessageSendData({
const storyId = message.get('storyId'); const storyId = message.get('storyId');
const [attachmentsWithData, preview, quote, sticker, storyMessage] = const [attachmentsWithData, contact, preview, quote, sticker, storyMessage] =
await Promise.all([ await Promise.all([
// We don't update the caches here because (1) we expect the caches to be populated // 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 // 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 // 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. // sends are failing, let's not add the complication of a cache.
Promise.all((message.get('attachments') ?? []).map(loadAttachmentData)), Promise.all((message.get('attachments') ?? []).map(loadAttachmentData)),
message.cachedOutgoingContactData ||
loadContactData(message.get('contact')),
message.cachedOutgoingPreviewData || message.cachedOutgoingPreviewData ||
loadPreviewData(message.get('preview')), loadPreviewData(message.get('preview')),
message.cachedOutgoingQuoteData || loadQuoteData(message.get('quote')), message.cachedOutgoingQuoteData || loadQuoteData(message.get('quote')),
@ -439,6 +450,7 @@ async function getMessageSendData({
return { return {
attachments, attachments,
body, body,
contact,
deletedForEveryoneTimestamp: message.get('deletedForEveryoneTimestamp'), deletedForEveryoneTimestamp: message.get('deletedForEveryoneTimestamp'),
expireTimer: message.get('expireTimer'), expireTimer: message.get('expireTimer'),
mentions: message.get('bodyRanges'), mentions: message.get('bodyRanges'),

View File

@ -29,6 +29,7 @@ import * as Conversation from '../types/Conversation';
import * as Stickers from '../types/Stickers'; import * as Stickers from '../types/Stickers';
import { CapabilityError } from '../types/errors'; import { CapabilityError } from '../types/errors';
import type { import type {
ContactWithHydratedAvatar,
GroupV1InfoType, GroupV1InfoType,
GroupV2InfoType, GroupV2InfoType,
StickerType, 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); window.reduxActions.stickers.useSticker(packId, stickerId);
} }
@ -3925,12 +3930,23 @@ export class ConversationModel extends window.Backbone
} }
async enqueueMessageForSend( async enqueueMessageForSend(
body: string | undefined, {
attachments: Array<AttachmentType>, attachments,
quote?: QuotedMessageType, body,
preview?: Array<LinkPreviewType>, contact,
sticker?: StickerType, mentions,
mentions?: BodyRangesType, preview,
quote,
sticker,
}: {
attachments: Array<AttachmentType>;
body: string | undefined;
contact?: Array<ContactWithHydratedAvatar>;
mentions?: BodyRangesType;
preview?: Array<LinkPreviewType>;
quote?: QuotedMessageType;
sticker?: StickerType;
},
{ {
dontClearDraft, dontClearDraft,
sendHQImages, sendHQImages,
@ -4000,6 +4016,7 @@ export class ConversationModel extends window.Backbone
type: 'outgoing', type: 'outgoing',
body, body,
conversationId: this.id, conversationId: this.id,
contact,
quote, quote,
preview, preview,
attachments: attachmentsToSend, attachments: attachmentsToSend,
@ -4031,6 +4048,7 @@ export class ConversationModel extends window.Backbone
const model = new window.Whisper.Message(attributes); const model = new window.Whisper.Message(attributes);
const message = window.MessageController.register(model.id, model); const message = window.MessageController.register(model.id, model);
message.cachedOutgoingContactData = contact;
message.cachedOutgoingPreviewData = preview; message.cachedOutgoingPreviewData = preview;
message.cachedOutgoingQuoteData = quote; message.cachedOutgoingQuoteData = quote;
message.cachedOutgoingStickerData = sticker; message.cachedOutgoingStickerData = sticker;

View File

@ -151,6 +151,7 @@ import type { ConversationQueueJobData } from '../jobs/conversationJobQueue';
import { getMessageById } from '../messages/getMessageById'; import { getMessageById } from '../messages/getMessageById';
import { shouldDownloadStory } from '../util/shouldDownloadStory'; import { shouldDownloadStory } from '../util/shouldDownloadStory';
import { shouldShowStoriesView } from '../state/selectors/stories'; import { shouldShowStoriesView } from '../state/selectors/stories';
import type { ContactWithHydratedAvatar } from '../textsecure/SendMessage';
/* eslint-disable camelcase */ /* eslint-disable camelcase */
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
@ -188,6 +189,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
syncPromise?: Promise<CallbackResultType | void>; syncPromise?: Promise<CallbackResultType | void>;
cachedOutgoingContactData?: Array<ContactWithHydratedAvatar>;
cachedOutgoingPreviewData?: Array<LinkPreviewType>; cachedOutgoingPreviewData?: Array<LinkPreviewType>;
cachedOutgoingQuoteData?: WhatIsThis; cachedOutgoingQuoteData?: WhatIsThis;

View File

@ -230,12 +230,11 @@ function replyToStory(
if (conversation) { if (conversation) {
conversation.enqueueMessageForSend( conversation.enqueueMessageForSend(
messageBody, {
[], body: messageBody,
undefined, attachments: [],
undefined, mentions,
undefined, },
mentions,
{ {
storyId: story.messageId, storyId: story.messageId,
timestamp, timestamp,

View File

@ -24,6 +24,7 @@ export type SmartForwardMessageModalProps = {
attachments?: Array<AttachmentType>, attachments?: Array<AttachmentType>,
linkPreview?: LinkPreviewType linkPreview?: LinkPreviewType
) => void; ) => void;
hasContact: boolean;
isSticker: boolean; isSticker: boolean;
messageBody?: string; messageBody?: string;
onClose: () => void; onClose: () => void;
@ -42,6 +43,7 @@ const mapStateToProps = (
const { const {
attachments, attachments,
doForwardMessage, doForwardMessage,
hasContact,
isSticker, isSticker,
messageBody, messageBody,
onClose, onClose,
@ -59,6 +61,7 @@ const mapStateToProps = (
candidateConversations, candidateConversations,
doForwardMessage, doForwardMessage,
getPreferredBadge: getPreferredBadgeSelector(state), getPreferredBadge: getPreferredBadgeSelector(state),
hasContact,
i18n: getIntl(state), i18n: getIntl(state),
isSticker, isSticker,
linkPreview, linkPreview,

View File

@ -2,7 +2,6 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable no-nested-ternary */ /* eslint-disable no-nested-ternary */
/* eslint-disable more/no-then */
/* eslint-disable no-bitwise */ /* eslint-disable no-bitwise */
/* eslint-disable max-classes-per-file */ /* eslint-disable max-classes-per-file */
@ -69,6 +68,12 @@ import type { SendTypesType } from '../util/handleMessageSend';
import { shouldSaveProto, sendTypesEnum } from '../util/handleMessageSend'; import { shouldSaveProto, sendTypesEnum } from '../util/handleMessageSend';
import { SignalService as Proto } from '../protobuf'; import { SignalService as Proto } from '../protobuf';
import * as log from '../logging/log'; import * as log from '../logging/log';
import type { Avatar, EmbeddedContactType } from '../types/EmbeddedContact';
import {
numberToPhoneType,
numberToEmailType,
numberToAddressType,
} from '../types/EmbeddedContact';
export type SendMetadataType = { export type SendMetadataType = {
[identifier: string]: { [identifier: string]: {
@ -172,9 +177,16 @@ function makeAttachmentSendReady(
}; };
} }
export type ContactWithHydratedAvatar = EmbeddedContactType & {
avatar?: Avatar & {
attachmentPointer?: Proto.IAttachmentPointer;
};
};
export type MessageOptionsType = { export type MessageOptionsType = {
attachments?: ReadonlyArray<AttachmentType> | null; attachments?: ReadonlyArray<AttachmentType> | null;
body?: string; body?: string;
contact?: Array<ContactWithHydratedAvatar>;
expireTimer?: number; expireTimer?: number;
flags?: number; flags?: number;
group?: { group?: {
@ -197,21 +209,22 @@ export type MessageOptionsType = {
}; };
export type GroupSendOptionsType = { export type GroupSendOptionsType = {
attachments?: Array<AttachmentType>; attachments?: Array<AttachmentType>;
contact?: Array<ContactWithHydratedAvatar>;
deletedForEveryoneTimestamp?: number;
expireTimer?: number; expireTimer?: number;
flags?: number; flags?: number;
groupV2?: GroupV2InfoType; groupCallUpdate?: GroupCallUpdateType;
groupV1?: GroupV1InfoType; groupV1?: GroupV1InfoType;
groupV2?: GroupV2InfoType;
mentions?: BodyRangesType;
messageText?: string; messageText?: string;
preview?: ReadonlyArray<LinkPreviewType>; preview?: ReadonlyArray<LinkPreviewType>;
profileKey?: Uint8Array; profileKey?: Uint8Array;
quote?: QuoteType; quote?: QuoteType;
reaction?: ReactionType; reaction?: ReactionType;
sticker?: StickerType; sticker?: StickerType;
deletedForEveryoneTimestamp?: number;
timestamp: number;
mentions?: BodyRangesType;
groupCallUpdate?: GroupCallUpdateType;
storyContext?: StoryContextType; storyContext?: StoryContextType;
timestamp: number;
}; };
class Message { class Message {
@ -219,6 +232,8 @@ class Message {
body?: string; body?: string;
contact?: Array<ContactWithHydratedAvatar>;
expireTimer?: number; expireTimer?: number;
flags?: number; flags?: number;
@ -261,6 +276,7 @@ class Message {
constructor(options: MessageOptionsType) { constructor(options: MessageOptionsType) {
this.attachments = options.attachments || []; this.attachments = options.attachments || [];
this.body = options.body; this.body = options.body;
this.contact = options.contact;
this.expireTimer = options.expireTimer; this.expireTimer = options.expireTimer;
this.flags = options.flags; this.flags = options.flags;
this.group = options.group; this.group = options.group;
@ -403,6 +419,74 @@ class Message {
return item; 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) { if (this.quote) {
const { QuotedAttachment } = Proto.DataMessage.Quote; const { QuotedAttachment } = Proto.DataMessage.Quote;
const { BodyRange, Quote } = Proto.DataMessage; const { BodyRange, Quote } = Proto.DataMessage;
@ -559,14 +643,17 @@ export default class MessageSender {
} }
async makeAttachmentPointer( async makeAttachmentPointer(
attachment: Readonly<AttachmentType> attachment: Readonly<
Partial<AttachmentType> &
Pick<AttachmentType, 'data' | 'size' | 'contentType'>
>
): Promise<Proto.IAttachmentPointer> { ): Promise<Proto.IAttachmentPointer> {
assert( assert(
typeof attachment === 'object' && attachment !== null, typeof attachment === 'object' && attachment !== null,
'Got null attachment in `makeAttachmentPointer`' 'Got null attachment in `makeAttachmentPointer`'
); );
const { data, size } = attachment; const { data, size, contentType } = attachment;
if (!(data instanceof Uint8Array)) { if (!(data instanceof Uint8Array)) {
throw new Error( throw new Error(
`makeAttachmentPointer: data was a '${typeof data}' instead of Uint8Array` `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}` `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 padded = this.getPaddedAttachment(data);
const key = getRandomBytes(64); const key = getRandomBytes(64);
@ -589,7 +681,7 @@ export default class MessageSender {
proto.cdnId = Long.fromString(id); proto.cdnId = Long.fromString(id);
proto.contentType = attachment.contentType; proto.contentType = attachment.contentType;
proto.key = key; proto.key = key;
proto.size = attachment.size; proto.size = data.byteLength;
proto.digest = result.digest; proto.digest = result.digest;
if (attachment.fileName) { if (attachment.fileName) {
@ -615,22 +707,20 @@ export default class MessageSender {
} }
async uploadAttachments(message: Message): Promise<void> { async uploadAttachments(message: Message): Promise<void> {
await Promise.all( try {
message.attachments.map(attachment => // eslint-disable-next-line no-param-reassign
this.makeAttachmentPointer(attachment) message.attachmentPointers = 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) {
.catch(error => { throw new MessageError(message, error);
if (error instanceof HTTPError) { } else {
throw new MessageError(message, error); throw error;
} else { }
throw error; }
}
});
} }
async uploadLinkPreviews(message: Message): Promise<void> { async uploadLinkPreviews(message: Message): Promise<void> {
@ -687,32 +777,68 @@ export default class MessageSender {
} }
} }
async uploadThumbnails(message: Message): Promise<void> { async uploadContactAvatar(message: Message): Promise<void> {
const makePointer = this.makeAttachmentPointer.bind(this); const { contact } = message;
const { quote } = message; if (!contact || contact.length === 0) {
if (!quote || !quote.attachments || quote.attachments.length === 0) {
return; return;
} }
await Promise.all( try {
quote.attachments.map((attachment: QuoteAttachmentType) => { await Promise.all(
if (!attachment.thumbnail) { contact.map(async (item: ContactWithHydratedAvatar) => {
return null; 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 // eslint-disable-next-line no-param-reassign
attachment.attachmentPointer = pointer; itemAvatar.attachmentPointer = await this.makeAttachmentPointer(
}); attachment
}) );
).catch(error => { })
);
} catch (error) {
if (error instanceof HTTPError) { if (error instanceof HTTPError) {
throw new MessageError(message, error); throw new MessageError(message, error);
} else { } else {
throw error; throw error;
} }
}); }
}
async uploadThumbnails(message: Message): Promise<void> {
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 // Proto assembly
@ -742,6 +868,7 @@ export default class MessageSender {
const message = new Message(attributes); const message = new Message(attributes);
await Promise.all([ await Promise.all([
this.uploadAttachments(message), this.uploadAttachments(message),
this.uploadContactAvatar(message),
this.uploadThumbnails(message), this.uploadThumbnails(message),
this.uploadLinkPreviews(message), this.uploadLinkPreviews(message),
this.uploadSticker(message), this.uploadSticker(message),
@ -789,6 +916,7 @@ export default class MessageSender {
): MessageOptionsType { ): MessageOptionsType {
const { const {
attachments, attachments,
contact,
deletedForEveryoneTimestamp, deletedForEveryoneTimestamp,
expireTimer, expireTimer,
flags, flags,
@ -839,6 +967,7 @@ export default class MessageSender {
return { return {
attachments, attachments,
body: messageText, body: messageText,
contact,
deletedForEveryoneTimestamp, deletedForEveryoneTimestamp,
expireTimer, expireTimer,
flags, flags,
@ -883,33 +1012,25 @@ export default class MessageSender {
groupId: string | undefined; groupId: string | undefined;
options?: SendOptionsType; options?: SendOptionsType;
}>): Promise<CallbackResultType> { }>): Promise<CallbackResultType> {
const message = new Message(messageOptions); const message = await this.getHydratedMessage(messageOptions);
return Promise.all([ return new Promise((resolve, reject) => {
this.uploadAttachments(message), this.sendMessageProto({
this.uploadThumbnails(message), callback: (res: CallbackResultType) => {
this.uploadLinkPreviews(message), if (res.errors && res.errors.length > 0) {
this.uploadSticker(message), reject(new SendMessageProtoError(res));
]).then( } else {
async (): Promise<CallbackResultType> => resolve(res);
new Promise((resolve, reject) => { }
this.sendMessageProto({ },
callback: (res: CallbackResultType) => { contentHint,
if (res.errors && res.errors.length > 0) { groupId,
reject(new SendMessageProtoError(res)); options,
} else { proto: message.toProto(),
resolve(res); recipients: message.recipients || [],
} timestamp: message.timestamp,
}, });
contentHint, });
groupId,
options,
proto: message.toProto(),
recipients: message.recipients || [],
timestamp: message.timestamp,
});
})
);
} }
sendMessageProto({ sendMessageProto({
@ -1033,52 +1154,55 @@ export default class MessageSender {
// You might wonder why this takes a groupId. models/messages.resend() can send a group // You might wonder why this takes a groupId. models/messages.resend() can send a group
// message to just one person. // message to just one person.
async sendMessageToIdentifier({ async sendMessageToIdentifier({
attachments,
contact,
contentHint,
deletedForEveryoneTimestamp,
expireTimer,
groupId,
identifier, identifier,
messageText, messageText,
attachments,
quote,
preview,
sticker,
reaction,
deletedForEveryoneTimestamp,
timestamp,
expireTimer,
contentHint,
groupId,
profileKey,
options, options,
preview,
profileKey,
quote,
reaction,
sticker,
storyContext, storyContext,
timestamp,
}: Readonly<{ }: Readonly<{
attachments: ReadonlyArray<AttachmentType> | undefined;
contact?: Array<ContactWithHydratedAvatar>;
contentHint: number;
deletedForEveryoneTimestamp: number | undefined;
expireTimer: number | undefined;
groupId: string | undefined;
identifier: string; identifier: string;
messageText: string | undefined; messageText: string | undefined;
attachments: ReadonlyArray<AttachmentType> | undefined;
quote?: QuoteType;
preview?: ReadonlyArray<LinkPreviewType> | undefined;
sticker?: StickerType;
reaction?: ReactionType;
deletedForEveryoneTimestamp: number | undefined;
timestamp: number;
expireTimer: number | undefined;
contentHint: number;
groupId: string | undefined;
profileKey?: Uint8Array;
storyContext?: StoryContextType;
options?: SendOptionsType; options?: SendOptionsType;
preview?: ReadonlyArray<LinkPreviewType> | undefined;
profileKey?: Uint8Array;
quote?: QuoteType;
reaction?: ReactionType;
sticker?: StickerType;
storyContext?: StoryContextType;
timestamp: number;
}>): Promise<CallbackResultType> { }>): Promise<CallbackResultType> {
return this.sendMessage({ return this.sendMessage({
messageOptions: { messageOptions: {
recipients: [identifier],
body: messageText,
timestamp,
attachments, attachments,
quote, body: messageText,
preview, contact,
sticker,
reaction,
deletedForEveryoneTimestamp, deletedForEveryoneTimestamp,
expireTimer, expireTimer,
preview,
profileKey, profileKey,
quote,
reaction,
recipients: [identifier],
sticker,
storyContext, storyContext,
timestamp,
}, },
contentHint, contentHint,
groupId, groupId,

View File

@ -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_EMAIL_TYPE = Proto.DataMessage.Contact.Email.Type.HOME;
const DEFAULT_ADDRESS_TYPE = Proto.DataMessage.Contact.PostalAddress.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( export function embeddedContactSelector(
contact: EmbeddedContactType, contact: EmbeddedContactType,
options: { options: {

View File

@ -136,6 +136,7 @@ const {
getAbsoluteAttachmentPath, getAbsoluteAttachmentPath,
getAbsoluteTempPath, getAbsoluteTempPath,
loadAttachmentData, loadAttachmentData,
loadContactData,
loadPreviewData, loadPreviewData,
loadStickerData, loadStickerData,
openFileInFolder, openFileInFolder,
@ -1284,34 +1285,37 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
} }
const attachments = getAttachmentsForMessage(message.attributes); const attachments = getAttachmentsForMessage(message.attributes);
const doForwardMessage = async (
conversationIds: Array<string>,
messageBody?: string,
includedAttachments?: Array<AttachmentType>,
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({ this.forwardMessageModal = new Whisper.ReactWrapperView({
JSX: window.Signal.State.Roots.createForwardMessageModal( JSX: window.Signal.State.Roots.createForwardMessageModal(
window.reduxStore, window.reduxStore,
{ {
attachments, attachments,
doForwardMessage: async ( doForwardMessage,
conversationIds: Array<string>, hasContact: Boolean(message.get('contact')),
messageBody?: string,
includedAttachments?: Array<AttachmentType>,
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);
}
},
isSticker: Boolean(message.get('sticker')), isSticker: Boolean(message.get('sticker')),
messageBody: message.getRawText(), messageBody: message.getRawText(),
onClose: () => { onClose: () => {
@ -1433,6 +1437,8 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
const timestamp = baseTimestamp + offset; const timestamp = baseTimestamp + offset;
if (conversation) { if (conversation) {
const sticker = message.get('sticker'); const sticker = message.get('sticker');
const contact = message.get('contact');
if (sticker) { if (sticker) {
const stickerWithData = await loadStickerData(sticker); const stickerWithData = await loadStickerData(sticker);
const stickerNoPath = stickerWithData const stickerNoPath = stickerWithData
@ -1446,12 +1452,21 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
: undefined; : undefined;
conversation.enqueueMessageForSend( conversation.enqueueMessageForSend(
undefined, // body {
[], body: undefined,
undefined, // quote attachments: [],
[], sticker: stickerNoPath,
stickerNoPath, },
undefined, // BodyRanges { ...sendMessageOptions, timestamp }
);
} else if (contact) {
const contactWithHydratedAvatar = await loadContactData(contact);
conversation.enqueueMessageForSend(
{
body: undefined,
attachments: [],
contact: contactWithHydratedAvatar,
},
{ ...sendMessageOptions, timestamp } { ...sendMessageOptions, timestamp }
); );
} else { } else {
@ -1472,12 +1487,11 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
); );
conversation.enqueueMessageForSend( conversation.enqueueMessageForSend(
messageBody || undefined, {
attachmentsToSend, body: messageBody || undefined,
undefined, // quote attachments: attachmentsToSend,
preview, preview,
undefined, // sticker },
undefined, // BodyRanges
{ ...sendMessageOptions, timestamp } { ...sendMessageOptions, timestamp }
); );
} }
@ -2885,12 +2899,13 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
log.info('Send pre-checks took', sendDelta, 'milliseconds'); log.info('Send pre-checks took', sendDelta, 'milliseconds');
model.enqueueMessageForSend( model.enqueueMessageForSend(
message, {
attachments, body: message,
this.quote, attachments,
this.getLinkPreviewForSend(message), quote: this.quote,
undefined, // sticker preview: this.getLinkPreviewForSend(message),
mentions, mentions,
},
{ {
sendHQImages, sendHQImages,
timestamp, timestamp,

25
ts/window.d.ts vendored
View File

@ -3,20 +3,17 @@
// Captures the globals put in place by preload.js, background.js and others // 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 Backbone from 'backbone';
import * as Underscore from 'underscore'; import * as Underscore from 'underscore';
import moment from 'moment'; import moment from 'moment';
import PQueue from 'p-queue/dist'; import PQueue from 'p-queue/dist';
import { Ref } from 'react'; import { Ref } from 'react';
import { imageToBlurHash } from './util/imageToBlurHash'; import { imageToBlurHash } from './util/imageToBlurHash';
import type { ParsedUrlQuery } from 'querystring';
import * as Util from './util'; import * as Util from './util';
import { import {
ConversationModelCollectionType, ConversationModelCollectionType,
MessageModelCollectionType, MessageModelCollectionType,
MessageAttributesType,
ReactionAttributesType,
} from './model-types.d'; } from './model-types.d';
import { TextSecureType } from './textsecure.d'; import { TextSecureType } from './textsecure.d';
import { Storage } from './textsecure/Storage'; import { Storage } from './textsecure/Storage';
@ -32,11 +29,8 @@ import * as Curve from './Curve';
import * as RemoteConfig from './RemoteConfig'; import * as RemoteConfig from './RemoteConfig';
import * as OS from './OS'; import * as OS from './OS';
import { getEnvironment } from './environment'; import { getEnvironment } from './environment';
import * as zkgroup from './util/zkgroup'; import { LocalizerType } from './types/Util';
import { LocalizerType, BodyRangesType, BodyRangeType } from './types/Util';
import * as EmbeddedContact from './types/EmbeddedContact';
import type { Receipt } from './types/Receipt'; import type { Receipt } from './types/Receipt';
import * as Errors from './types/errors';
import { ConversationController } from './ConversationController'; import { ConversationController } from './ConversationController';
import { ReduxActions } from './state/types'; import { ReduxActions } from './state/types';
import { createStore } from './state/createStore'; 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 conversationsSelectors from './state/selectors/conversations';
import * as searchSelectors from './state/selectors/search'; import * as searchSelectors from './state/selectors/search';
import AccountManager from './textsecure/AccountManager'; import AccountManager from './textsecure/AccountManager';
import { SendOptionsType } from './textsecure/SendMessage'; import { ContactWithHydratedAvatar } from './textsecure/SendMessage';
import Data from './sql/Client'; import Data from './sql/Client';
import { UserMessage } from './types/Message';
import { PhoneNumberFormat } from 'google-libphonenumber'; import { PhoneNumberFormat } from 'google-libphonenumber';
import { MessageModel } from './models/messages'; import { MessageModel } from './models/messages';
import { ConversationModel } from './models/conversations'; import { ConversationModel } from './models/conversations';
import { combineNames } from './util';
import { BatcherType } from './util/batcher'; import { BatcherType } from './util/batcher';
import { AttachmentList } from './components/conversation/AttachmentList'; import { AttachmentList } from './components/conversation/AttachmentList';
import { ChatColorPicker } from './components/ChatColorPicker'; import { ChatColorPicker } from './components/ChatColorPicker';
@ -93,14 +85,12 @@ import { Quote } from './components/conversation/Quote';
import { StagedLinkPreview } from './components/conversation/StagedLinkPreview'; import { StagedLinkPreview } from './components/conversation/StagedLinkPreview';
import { DisappearingTimeDialog } from './components/DisappearingTimeDialog'; import { DisappearingTimeDialog } from './components/DisappearingTimeDialog';
import { WhatsNewLink } from './components/WhatsNewLink'; import { WhatsNewLink } from './components/WhatsNewLink';
import { MIMEType } from './types/MIME';
import { DownloadedAttachmentType } from './types/Attachment'; import { DownloadedAttachmentType } from './types/Attachment';
import { ElectronLocaleType } from './util/mapToSupportLocale'; import { ElectronLocaleType } from './util/mapToSupportLocale';
import { SignalProtocolStore } from './SignalProtocolStore'; import { SignalProtocolStore } from './SignalProtocolStore';
import { StartupQueue } from './util/StartupQueue'; import { StartupQueue } from './util/StartupQueue';
import { SocketStatus } from './types/SocketStatus'; import { SocketStatus } from './types/SocketStatus';
import SyncRequest from './textsecure/SyncRequest'; import SyncRequest from './textsecure/SyncRequest';
import { ConversationColorType, CustomColorType } from './types/Colors';
import { MessageController } from './util/MessageController'; import { MessageController } from './util/MessageController';
import { StateType } from './state/reducer'; import { StateType } from './state/reducer';
import { SystemTraySetting } from './types/SystemTraySetting'; import { SystemTraySetting } from './types/SystemTraySetting';
@ -108,15 +98,13 @@ import { UUID } from './types/UUID';
import { Address } from './types/Address'; import { Address } from './types/Address';
import { QualifiedAddress } from './types/QualifiedAddress'; import { QualifiedAddress } from './types/QualifiedAddress';
import { CI } from './CI'; import { CI } from './CI';
import { IPCEventsType, IPCEventsValuesType } from './util/createIPCEvents'; import { IPCEventsType } from './util/createIPCEvents';
import { ConversationView } from './views/conversation_view'; import { ConversationView } from './views/conversation_view';
import type { SignalContextType } from './windows/context'; import type { SignalContextType } from './windows/context';
import { GroupV2Change } from './components/conversation/GroupV2Change'; import type { EmbeddedContactType } from './types/EmbeddedContact';
export { Long } from 'long'; export { Long } from 'long';
type TaskResultType = any;
export type WhatIsThis = any; export type WhatIsThis = any;
// Synced with the type in ts/shims/showConfirmationDialog // Synced with the type in ts/shims/showConfirmationDialog
@ -311,6 +299,9 @@ declare global {
} }
>; >;
loadQuoteData: (quote: unknown) => WhatIsThis; loadQuoteData: (quote: unknown) => WhatIsThis;
loadContactData: (
contact?: Array<EmbeddedContactType>
) => Promise<Array<ContactWithHydratedAvatar> | undefined>;
loadPreviewData: (preview: unknown) => WhatIsThis; loadPreviewData: (preview: unknown) => WhatIsThis;
loadStickerData: (sticker: unknown) => WhatIsThis; loadStickerData: (sticker: unknown) => WhatIsThis;
readStickerData: (path: string) => Promise<Uint8Array>; readStickerData: (path: string) => Promise<Uint8Array>;