Disable forward for messages with embedded contact
This commit is contained in:
parent
6d816d01ad
commit
7f89f6162f
14 changed files with 442 additions and 185 deletions
|
@ -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,
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -48,6 +48,7 @@ const useProps = (overrideProps: Partial<PropsType> = {}): 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 <ForwardMessageModal {...useProps({ isSticker: true })} />;
|
||||
});
|
||||
|
||||
story.add('with a contact', () => {
|
||||
return <ForwardMessageModal {...useProps({ hasContact: true })} />;
|
||||
});
|
||||
|
||||
story.add('link preview', () => {
|
||||
return (
|
||||
<ForwardMessageModal
|
||||
|
|
|
@ -48,6 +48,7 @@ export type DataPropsType = {
|
|||
linkPreview?: LinkPreviewType
|
||||
) => void;
|
||||
getPreferredBadge: PreferredBadgeSelectorType;
|
||||
hasContact: boolean;
|
||||
i18n: LocalizerType;
|
||||
isSticker: boolean;
|
||||
linkPreview?: LinkPreviewType;
|
||||
|
@ -79,6 +80,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
|||
candidateConversations,
|
||||
doForwardMessage,
|
||||
getPreferredBadge,
|
||||
hasContact,
|
||||
i18n,
|
||||
isSticker,
|
||||
linkPreview,
|
||||
|
@ -110,7 +112,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
|||
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<PropsType> = ({
|
|||
hasContactsSelected &&
|
||||
(Boolean(messageBodyText) ||
|
||||
isSticker ||
|
||||
hasContact ||
|
||||
(attachmentsToForward && attachmentsToForward.length));
|
||||
|
||||
const forwardMessage = React.useCallback(() => {
|
||||
|
|
|
@ -1710,6 +1710,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
const {
|
||||
attachments,
|
||||
canDownload,
|
||||
contact,
|
||||
canReact,
|
||||
canReply,
|
||||
canRetry,
|
||||
|
@ -1729,7 +1730,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
text,
|
||||
} = this.props;
|
||||
|
||||
const canForward = !isTapToView && !deletedForEveryone;
|
||||
const canForward = !isTapToView && !deletedForEveryone && !contact;
|
||||
const multipleAttachments = attachments && attachments.length > 1;
|
||||
|
||||
const shouldShowAdditional =
|
||||
|
|
|
@ -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<AttachmentType>;
|
||||
body: undefined | string;
|
||||
contact?: Array<ContactWithHydratedAvatar>;
|
||||
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'),
|
||||
|
|
|
@ -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<AttachmentType>,
|
||||
quote?: QuotedMessageType,
|
||||
preview?: Array<LinkPreviewType>,
|
||||
sticker?: StickerType,
|
||||
mentions?: BodyRangesType,
|
||||
{
|
||||
attachments,
|
||||
body,
|
||||
contact,
|
||||
mentions,
|
||||
preview,
|
||||
quote,
|
||||
sticker,
|
||||
}: {
|
||||
attachments: Array<AttachmentType>;
|
||||
body: string | undefined;
|
||||
contact?: Array<ContactWithHydratedAvatar>;
|
||||
mentions?: BodyRangesType;
|
||||
preview?: Array<LinkPreviewType>;
|
||||
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;
|
||||
|
|
|
@ -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<MessageAttributesType> {
|
|||
|
||||
syncPromise?: Promise<CallbackResultType | void>;
|
||||
|
||||
cachedOutgoingContactData?: Array<ContactWithHydratedAvatar>;
|
||||
|
||||
cachedOutgoingPreviewData?: Array<LinkPreviewType>;
|
||||
|
||||
cachedOutgoingQuoteData?: WhatIsThis;
|
||||
|
|
|
@ -230,12 +230,11 @@ function replyToStory(
|
|||
|
||||
if (conversation) {
|
||||
conversation.enqueueMessageForSend(
|
||||
messageBody,
|
||||
[],
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
mentions,
|
||||
{
|
||||
body: messageBody,
|
||||
attachments: [],
|
||||
mentions,
|
||||
},
|
||||
{
|
||||
storyId: story.messageId,
|
||||
timestamp,
|
||||
|
|
|
@ -24,6 +24,7 @@ export type SmartForwardMessageModalProps = {
|
|||
attachments?: Array<AttachmentType>,
|
||||
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,
|
||||
|
|
|
@ -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<AttachmentType> | null;
|
||||
body?: string;
|
||||
contact?: Array<ContactWithHydratedAvatar>;
|
||||
expireTimer?: number;
|
||||
flags?: number;
|
||||
group?: {
|
||||
|
@ -197,21 +209,22 @@ export type MessageOptionsType = {
|
|||
};
|
||||
export type GroupSendOptionsType = {
|
||||
attachments?: Array<AttachmentType>;
|
||||
contact?: Array<ContactWithHydratedAvatar>;
|
||||
deletedForEveryoneTimestamp?: number;
|
||||
expireTimer?: number;
|
||||
flags?: number;
|
||||
groupV2?: GroupV2InfoType;
|
||||
groupCallUpdate?: GroupCallUpdateType;
|
||||
groupV1?: GroupV1InfoType;
|
||||
groupV2?: GroupV2InfoType;
|
||||
mentions?: BodyRangesType;
|
||||
messageText?: string;
|
||||
preview?: ReadonlyArray<LinkPreviewType>;
|
||||
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<ContactWithHydratedAvatar>;
|
||||
|
||||
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<AttachmentType>
|
||||
attachment: Readonly<
|
||||
Partial<AttachmentType> &
|
||||
Pick<AttachmentType, 'data' | 'size' | 'contentType'>
|
||||
>
|
||||
): Promise<Proto.IAttachmentPointer> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
|
@ -687,32 +777,68 @@ export default class MessageSender {
|
|||
}
|
||||
}
|
||||
|
||||
async uploadThumbnails(message: Message): Promise<void> {
|
||||
const makePointer = this.makeAttachmentPointer.bind(this);
|
||||
const { quote } = message;
|
||||
|
||||
if (!quote || !quote.attachments || quote.attachments.length === 0) {
|
||||
async uploadContactAvatar(message: Message): Promise<void> {
|
||||
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<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
|
||||
|
@ -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<CallbackResultType> {
|
||||
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<CallbackResultType> =>
|
||||
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<AttachmentType> | undefined;
|
||||
contact?: Array<ContactWithHydratedAvatar>;
|
||||
contentHint: number;
|
||||
deletedForEveryoneTimestamp: number | undefined;
|
||||
expireTimer: number | undefined;
|
||||
groupId: string | undefined;
|
||||
identifier: string;
|
||||
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;
|
||||
preview?: ReadonlyArray<LinkPreviewType> | undefined;
|
||||
profileKey?: Uint8Array;
|
||||
quote?: QuoteType;
|
||||
reaction?: ReactionType;
|
||||
sticker?: StickerType;
|
||||
storyContext?: StoryContextType;
|
||||
timestamp: number;
|
||||
}>): Promise<CallbackResultType> {
|
||||
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,
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -136,6 +136,7 @@ const {
|
|||
getAbsoluteAttachmentPath,
|
||||
getAbsoluteTempPath,
|
||||
loadAttachmentData,
|
||||
loadContactData,
|
||||
loadPreviewData,
|
||||
loadStickerData,
|
||||
openFileInFolder,
|
||||
|
@ -1284,34 +1285,37 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
}
|
||||
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({
|
||||
JSX: window.Signal.State.Roots.createForwardMessageModal(
|
||||
window.reduxStore,
|
||||
{
|
||||
attachments,
|
||||
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);
|
||||
}
|
||||
},
|
||||
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<ConversationModel> {
|
|||
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<ConversationModel> {
|
|||
: 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<ConversationModel> {
|
|||
);
|
||||
|
||||
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<ConversationModel> {
|
|||
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,
|
||||
|
|
25
ts/window.d.ts
vendored
25
ts/window.d.ts
vendored
|
@ -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<EmbeddedContactType>
|
||||
) => Promise<Array<ContactWithHydratedAvatar> | undefined>;
|
||||
loadPreviewData: (preview: unknown) => WhatIsThis;
|
||||
loadStickerData: (sticker: unknown) => WhatIsThis;
|
||||
readStickerData: (path: string) => Promise<Uint8Array>;
|
||||
|
|
Loading…
Reference in a new issue