Receive support for editing messages
This commit is contained in:
parent
2781e621ad
commit
36e21c0134
46 changed files with 2053 additions and 405 deletions
|
@ -1,7 +1,6 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { omit } from 'lodash';
|
||||
import { blobToArrayBuffer } from 'blob-util';
|
||||
|
||||
import { scaleImageToLevel } from './scaleImageToLevel';
|
||||
|
@ -59,8 +58,7 @@ export async function autoOrientJPEG(
|
|||
// by potentially doubling stored image data.
|
||||
// See: https://github.com/signalapp/Signal-Desktop/issues/1589
|
||||
const xcodedAttachment = {
|
||||
// `digest` is no longer valid for auto-oriented image data, so we discard it:
|
||||
...omit(attachment, 'digest'),
|
||||
...attachment,
|
||||
data: new Uint8Array(xcodedDataArrayBuffer),
|
||||
size: xcodedDataArrayBuffer.byteLength,
|
||||
};
|
||||
|
|
28
ts/util/getQuoteBodyText.ts
Normal file
28
ts/util/getQuoteBodyText.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { MessageAttributesType } from '../model-types.d';
|
||||
import * as EmbeddedContact from '../types/EmbeddedContact';
|
||||
|
||||
export function getQuoteBodyText(
|
||||
messageAttributes: MessageAttributesType,
|
||||
id: number
|
||||
): string | undefined {
|
||||
const storyReactionEmoji = messageAttributes.storyReaction?.emoji;
|
||||
|
||||
const { editHistory } = messageAttributes;
|
||||
const editedMessage =
|
||||
editHistory && editHistory.find(edit => edit.timestamp === id);
|
||||
|
||||
if (editedMessage && editedMessage.body) {
|
||||
return editedMessage.body;
|
||||
}
|
||||
|
||||
const { body, contact: embeddedContact } = messageAttributes;
|
||||
const embeddedContactName =
|
||||
embeddedContact && embeddedContact.length > 0
|
||||
? EmbeddedContact.getName(embeddedContact[0])
|
||||
: '';
|
||||
|
||||
return body || embeddedContactName || storyReactionEmoji;
|
||||
}
|
191
ts/util/handleEditMessage.ts
Normal file
191
ts/util/handleEditMessage.ts
Normal file
|
@ -0,0 +1,191 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { AttachmentType } from '../types/Attachment';
|
||||
import type { EditAttributesType } from '../messageModifiers/Edits';
|
||||
import type { EditHistoryType, MessageAttributesType } from '../model-types.d';
|
||||
import type { LinkPreviewType } from '../types/message/LinkPreviews';
|
||||
import * as log from '../logging/log';
|
||||
import { ReadStatus } from '../messages/MessageReadStatus';
|
||||
import dataInterface from '../sql/Client';
|
||||
import { drop } from './drop';
|
||||
import {
|
||||
getAttachmentSignature,
|
||||
isDownloaded,
|
||||
isVoiceMessage,
|
||||
} from '../types/Attachment';
|
||||
import { getMessageIdForLogging } from './idForLogging';
|
||||
import { isOutgoing } from '../messages/helpers';
|
||||
import { queueAttachmentDownloads } from './queueAttachmentDownloads';
|
||||
import { shouldReplyNotifyUser } from './shouldReplyNotifyUser';
|
||||
|
||||
export async function handleEditMessage(
|
||||
mainMessage: MessageAttributesType,
|
||||
editAttributes: EditAttributesType
|
||||
): Promise<void> {
|
||||
const idLog = `handleEditMessage(${getMessageIdForLogging(mainMessage)})`;
|
||||
|
||||
// Verify that we can safely apply an edit to this type of message
|
||||
if (mainMessage.deletedForEveryone) {
|
||||
log.warn(`${idLog}: Cannot edit a DOE message`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mainMessage.isViewOnce) {
|
||||
log.warn(`${idLog}: Cannot edit an isViewOnce message`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mainMessage.contact && mainMessage.contact.length > 0) {
|
||||
log.warn(`${idLog}: Cannot edit a contact share`);
|
||||
return;
|
||||
}
|
||||
|
||||
const hasVoiceMessage = mainMessage.attachments?.some(isVoiceMessage);
|
||||
if (hasVoiceMessage) {
|
||||
log.warn(`${idLog}: Cannot edit a voice message`);
|
||||
return;
|
||||
}
|
||||
|
||||
const mainMessageModel = window.MessageController.register(
|
||||
mainMessage.id,
|
||||
mainMessage
|
||||
);
|
||||
|
||||
// Pull out the edit history from the main message. If this is the first edit
|
||||
// then the original message becomes the first item in the edit history.
|
||||
const editHistory: Array<EditHistoryType> = mainMessage.editHistory || [
|
||||
{
|
||||
attachments: mainMessage.attachments,
|
||||
body: mainMessage.body,
|
||||
bodyRanges: mainMessage.bodyRanges,
|
||||
preview: mainMessage.preview,
|
||||
timestamp: mainMessage.timestamp,
|
||||
},
|
||||
];
|
||||
|
||||
// Race condition prevention check here. If we already have the timestamp
|
||||
// recorded as an edit we can safely drop handling this edit.
|
||||
const editedMessageExists = editHistory.some(
|
||||
edit => edit.timestamp === editAttributes.message.timestamp
|
||||
);
|
||||
if (editedMessageExists) {
|
||||
log.warn(`${idLog}: edited message is duplicate. Dropping.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const messageAttributesForUpgrade: MessageAttributesType = {
|
||||
...editAttributes.message,
|
||||
...editAttributes.dataMessage,
|
||||
// There are type conflicts between MessageAttributesType and protos passed in here
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any as MessageAttributesType;
|
||||
|
||||
const upgradedEditedMessageData =
|
||||
await window.Signal.Migrations.upgradeMessageSchema(
|
||||
messageAttributesForUpgrade
|
||||
);
|
||||
|
||||
// Copies over the attachments from the main message if they're the same
|
||||
// and they have already been downloaded.
|
||||
const attachmentSignatures: Map<string, AttachmentType> = new Map();
|
||||
const previewSignatures: Map<string, LinkPreviewType> = new Map();
|
||||
|
||||
mainMessage.attachments?.forEach(attachment => {
|
||||
if (!isDownloaded(attachment)) {
|
||||
return;
|
||||
}
|
||||
const signature = getAttachmentSignature(attachment);
|
||||
attachmentSignatures.set(signature, attachment);
|
||||
});
|
||||
mainMessage.preview?.forEach(preview => {
|
||||
if (!preview.image || !isDownloaded(preview.image)) {
|
||||
return;
|
||||
}
|
||||
const signature = getAttachmentSignature(preview.image);
|
||||
previewSignatures.set(signature, preview);
|
||||
});
|
||||
|
||||
const nextEditedMessageAttachments =
|
||||
upgradedEditedMessageData.attachments?.map(attachment => {
|
||||
const signature = getAttachmentSignature(attachment);
|
||||
const existingAttachment = attachmentSignatures.get(signature);
|
||||
|
||||
return existingAttachment || attachment;
|
||||
});
|
||||
|
||||
const nextEditedMessagePreview = upgradedEditedMessageData.preview?.map(
|
||||
preview => {
|
||||
if (!preview.image) {
|
||||
return preview;
|
||||
}
|
||||
|
||||
const signature = getAttachmentSignature(preview.image);
|
||||
const existingPreview = previewSignatures.get(signature);
|
||||
return existingPreview || preview;
|
||||
}
|
||||
);
|
||||
|
||||
const editedMessage: EditHistoryType = {
|
||||
attachments: nextEditedMessageAttachments,
|
||||
body: upgradedEditedMessageData.body,
|
||||
bodyRanges: upgradedEditedMessageData.bodyRanges,
|
||||
preview: nextEditedMessagePreview,
|
||||
timestamp: upgradedEditedMessageData.timestamp,
|
||||
};
|
||||
|
||||
// The edit history works like a queue where the newest edits are at the top.
|
||||
// Here we unshift the latest edit onto the edit history.
|
||||
editHistory.unshift(editedMessage);
|
||||
|
||||
// Update all the editable attributes on the main message also updating the
|
||||
// edit history.
|
||||
mainMessageModel.set({
|
||||
attachments: editedMessage.attachments,
|
||||
body: editedMessage.body,
|
||||
bodyRanges: editedMessage.bodyRanges,
|
||||
editHistory,
|
||||
editMessageTimestamp: upgradedEditedMessageData.timestamp,
|
||||
preview: editedMessage.preview,
|
||||
});
|
||||
|
||||
// Queue up any downloads in case they're different, update the fields if so.
|
||||
const updatedFields = await queueAttachmentDownloads(
|
||||
mainMessageModel.attributes
|
||||
);
|
||||
if (updatedFields) {
|
||||
mainMessageModel.set(updatedFields);
|
||||
}
|
||||
|
||||
// For incoming edits, we mark the message as unread so that we're able to
|
||||
// send a read receipt for the message. In case we had already sent one for
|
||||
// the original message.
|
||||
const readStatus = isOutgoing(mainMessageModel.attributes)
|
||||
? ReadStatus.Read
|
||||
: ReadStatus.Unread;
|
||||
|
||||
// Save both the main message and the edited message for fast lookups
|
||||
drop(
|
||||
dataInterface.saveEditedMessage(
|
||||
mainMessageModel.attributes,
|
||||
window.textsecure.storage.user.getCheckedUuid().toString(),
|
||||
{
|
||||
fromId: editAttributes.fromId,
|
||||
messageId: mainMessage.id,
|
||||
readStatus,
|
||||
sentAt: upgradedEditedMessageData.timestamp,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
drop(mainMessageModel.getConversation()?.updateLastMessage());
|
||||
|
||||
// Update notifications
|
||||
const conversation = mainMessageModel.getConversation();
|
||||
if (!conversation) {
|
||||
return;
|
||||
}
|
||||
if (await shouldReplyNotifyUser(mainMessageModel, conversation)) {
|
||||
await conversation.notify(mainMessageModel);
|
||||
}
|
||||
}
|
|
@ -21,31 +21,12 @@ export function hasAttachmentDownloads(
|
|||
return true;
|
||||
}
|
||||
|
||||
const hasNormalAttachments = normalAttachments.some(attachment => {
|
||||
if (!attachment) {
|
||||
return false;
|
||||
}
|
||||
// We've already downloaded this!
|
||||
if (attachment.path) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const hasNormalAttachments = hasNormalAttachmentDownloads(normalAttachments);
|
||||
if (hasNormalAttachments) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const previews = message.preview || [];
|
||||
const hasPreviews = previews.some(item => {
|
||||
if (!item.image) {
|
||||
return false;
|
||||
}
|
||||
// We've already downloaded this!
|
||||
if (item.image.path) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const hasPreviews = hasPreviewDownloads(message.preview);
|
||||
if (hasPreviews) {
|
||||
return true;
|
||||
}
|
||||
|
@ -85,5 +66,48 @@ export function hasAttachmentDownloads(
|
|||
return !sticker.data || (sticker.data && !sticker.data.path);
|
||||
}
|
||||
|
||||
const { editHistory } = message;
|
||||
if (editHistory) {
|
||||
const hasAttachmentsWithinEditHistory = editHistory.some(
|
||||
edit =>
|
||||
hasNormalAttachmentDownloads(edit.attachments) ||
|
||||
hasPreviewDownloads(edit.preview)
|
||||
);
|
||||
|
||||
if (hasAttachmentsWithinEditHistory) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasPreviewDownloads(
|
||||
previews: MessageAttributesType['preview']
|
||||
): boolean {
|
||||
return (previews || []).some(item => {
|
||||
if (!item.image) {
|
||||
return false;
|
||||
}
|
||||
// We've already downloaded this!
|
||||
if (item.image.path) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function hasNormalAttachmentDownloads(
|
||||
attachments: MessageAttributesType['attachments']
|
||||
): boolean {
|
||||
return (attachments || []).some(attachment => {
|
||||
if (!attachment) {
|
||||
return false;
|
||||
}
|
||||
// We've already downloaded this!
|
||||
if (attachment.path) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2011,6 +2011,13 @@
|
|||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-07-30T16:57:33.618Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/EditHistoryMessagesModal.tsx",
|
||||
"line": " const containerElementRef = useRef<HTMLDivElement | null>(null);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2023-03-25T01:59:04.590Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/ForwardMessagesModal.tsx",
|
||||
|
@ -2399,6 +2406,13 @@
|
|||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2022-11-03T14:21:47.456Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/conversation/WaveformScrubber.tsx",
|
||||
"line": " const waveformRef = useRef<HTMLDivElement | null>(null);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2023-02-26T23:20:28.848Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/conversation/conversation-details/AddGroupMembersModal/ChooseGroupMembersModal.tsx",
|
||||
|
@ -2435,13 +2449,6 @@
|
|||
"updated": "2019-11-01T22:46:33.013Z",
|
||||
"reasonDetail": "Used for setting focus only"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/conversation/WaveformScrubber.tsx",
|
||||
"line": " const waveformRef = useRef<HTMLDivElement | null>(null);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2023-02-26T23:20:28.848Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/emoji/EmojiButton.tsx",
|
||||
|
|
149
ts/util/makeQuote.ts
Normal file
149
ts/util/makeQuote.ts
Normal file
|
@ -0,0 +1,149 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { AttachmentType, ThumbnailType } from '../types/Attachment';
|
||||
import type {
|
||||
MessageAttributesType,
|
||||
QuotedMessageType,
|
||||
} from '../model-types.d';
|
||||
import type { MIMEType } from '../types/MIME';
|
||||
import type { LinkPreviewType } from '../types/message/LinkPreviews';
|
||||
import type { StickerType } from '../types/Stickers';
|
||||
import { IMAGE_JPEG, IMAGE_GIF } from '../types/MIME';
|
||||
import { getContact } from '../messages/helpers';
|
||||
import { getQuoteBodyText } from './getQuoteBodyText';
|
||||
import { isGIF } from '../types/Attachment';
|
||||
import { isGiftBadge, isTapToView } from '../state/selectors/message';
|
||||
import { map, take, collect } from './iterables';
|
||||
import { strictAssert } from './assert';
|
||||
|
||||
export async function makeQuote(
|
||||
quotedMessage: MessageAttributesType
|
||||
): Promise<QuotedMessageType> {
|
||||
const contact = getContact(quotedMessage);
|
||||
|
||||
strictAssert(contact, 'makeQuote: no contact');
|
||||
|
||||
const {
|
||||
attachments,
|
||||
bodyRanges,
|
||||
editMessageTimestamp,
|
||||
id: messageId,
|
||||
payment,
|
||||
preview,
|
||||
sticker,
|
||||
} = quotedMessage;
|
||||
|
||||
const quoteId = editMessageTimestamp || quotedMessage.sent_at;
|
||||
|
||||
return {
|
||||
authorUuid: contact.get('uuid'),
|
||||
attachments: isTapToView(quotedMessage)
|
||||
? [{ contentType: IMAGE_JPEG, fileName: null }]
|
||||
: await getQuoteAttachment(attachments, preview, sticker),
|
||||
payment,
|
||||
bodyRanges,
|
||||
id: quoteId,
|
||||
isViewOnce: isTapToView(quotedMessage),
|
||||
isGiftBadge: isGiftBadge(quotedMessage),
|
||||
messageId,
|
||||
referencedMessageNotFound: false,
|
||||
text: getQuoteBodyText(quotedMessage, quoteId),
|
||||
};
|
||||
}
|
||||
|
||||
export async function getQuoteAttachment(
|
||||
attachments?: Array<AttachmentType>,
|
||||
preview?: Array<LinkPreviewType>,
|
||||
sticker?: StickerType
|
||||
): Promise<
|
||||
Array<{
|
||||
contentType: MIMEType;
|
||||
fileName: string | null;
|
||||
thumbnail: ThumbnailType | null;
|
||||
}>
|
||||
> {
|
||||
const { getAbsoluteAttachmentPath, loadAttachmentData } =
|
||||
window.Signal.Migrations;
|
||||
|
||||
if (attachments && attachments.length) {
|
||||
const attachmentsToUse = Array.from(take(attachments, 1));
|
||||
const isGIFQuote = isGIF(attachmentsToUse);
|
||||
|
||||
return Promise.all(
|
||||
map(attachmentsToUse, async attachment => {
|
||||
const { path, fileName, thumbnail, contentType } = attachment;
|
||||
|
||||
if (!path) {
|
||||
return {
|
||||
contentType: isGIFQuote ? IMAGE_GIF : contentType,
|
||||
// Our protos library complains about this field being undefined, so we
|
||||
// force it to null
|
||||
fileName: fileName || null,
|
||||
thumbnail: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
contentType: isGIFQuote ? IMAGE_GIF : contentType,
|
||||
// Our protos library complains about this field being undefined, so we force
|
||||
// it to null
|
||||
fileName: fileName || null,
|
||||
thumbnail: thumbnail
|
||||
? {
|
||||
...(await loadAttachmentData(thumbnail)),
|
||||
objectUrl: thumbnail.path
|
||||
? getAbsoluteAttachmentPath(thumbnail.path)
|
||||
: undefined,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (preview && preview.length) {
|
||||
const previewImages = collect(preview, prev => prev.image);
|
||||
const previewImagesToUse = take(previewImages, 1);
|
||||
|
||||
return Promise.all(
|
||||
map(previewImagesToUse, async image => {
|
||||
const { contentType } = image;
|
||||
|
||||
return {
|
||||
contentType,
|
||||
// Our protos library complains about this field being undefined, so we
|
||||
// force it to null
|
||||
fileName: null,
|
||||
thumbnail: image
|
||||
? {
|
||||
...(await loadAttachmentData(image)),
|
||||
objectUrl: image.path
|
||||
? getAbsoluteAttachmentPath(image.path)
|
||||
: undefined,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (sticker && sticker.data && sticker.data.path) {
|
||||
const { path, contentType } = sticker.data;
|
||||
|
||||
return [
|
||||
{
|
||||
contentType,
|
||||
// Our protos library complains about this field being undefined, so we
|
||||
// force it to null
|
||||
fileName: null,
|
||||
thumbnail: {
|
||||
...(await loadAttachmentData(sticker.data)),
|
||||
objectUrl: path ? getAbsoluteAttachmentPath(path) : undefined,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
|
@ -34,18 +34,23 @@ export async function markConversationRead(
|
|||
): Promise<boolean> {
|
||||
const { id: conversationId } = conversationAttrs;
|
||||
|
||||
const [unreadMessages, unreadReactions] = await Promise.all([
|
||||
window.Signal.Data.getUnreadByConversationAndMarkRead({
|
||||
conversationId,
|
||||
newestUnreadAt,
|
||||
readAt: options.readAt,
|
||||
includeStoryReplies: !isGroup(conversationAttrs),
|
||||
}),
|
||||
window.Signal.Data.getUnreadReactionsAndMarkRead({
|
||||
conversationId,
|
||||
newestUnreadAt,
|
||||
}),
|
||||
]);
|
||||
const [unreadMessages, unreadEditedMessages, unreadReactions] =
|
||||
await Promise.all([
|
||||
window.Signal.Data.getUnreadByConversationAndMarkRead({
|
||||
conversationId,
|
||||
newestUnreadAt,
|
||||
readAt: options.readAt,
|
||||
includeStoryReplies: !isGroup(conversationAttrs),
|
||||
}),
|
||||
window.Signal.Data.getUnreadEditedMessagesAndMarkRead({
|
||||
fromId: conversationId,
|
||||
newestUnreadAt,
|
||||
}),
|
||||
window.Signal.Data.getUnreadReactionsAndMarkRead({
|
||||
conversationId,
|
||||
newestUnreadAt,
|
||||
}),
|
||||
]);
|
||||
|
||||
log.info('markConversationRead', {
|
||||
conversationId: getConversationIdForLogging(conversationAttrs),
|
||||
|
@ -55,7 +60,11 @@ export async function markConversationRead(
|
|||
unreadReactions: unreadReactions.length,
|
||||
});
|
||||
|
||||
if (!unreadMessages.length && !unreadReactions.length) {
|
||||
if (
|
||||
!unreadMessages.length &&
|
||||
!unreadEditedMessages.length &&
|
||||
!unreadReactions.length
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -83,7 +92,9 @@ export async function markConversationRead(
|
|||
});
|
||||
});
|
||||
|
||||
const allReadMessagesSync = unreadMessages.map(messageSyncData => {
|
||||
const allUnreadMessages = [...unreadMessages, ...unreadEditedMessages];
|
||||
|
||||
const allReadMessagesSync = allUnreadMessages.map(messageSyncData => {
|
||||
const message = window.MessageController.getById(messageSyncData.id);
|
||||
// we update the in-memory MessageModel with the fresh database call data
|
||||
if (message) {
|
||||
|
|
|
@ -16,16 +16,23 @@ import dataInterface from '../sql/Client';
|
|||
import type { AttachmentType } from '../types/Attachment';
|
||||
import type { EmbeddedContactType } from '../types/EmbeddedContact';
|
||||
import type {
|
||||
EditHistoryType,
|
||||
MessageAttributesType,
|
||||
QuotedMessageType,
|
||||
} from '../model-types.d';
|
||||
import * as Errors from '../types/errors';
|
||||
import {
|
||||
getAttachmentSignature,
|
||||
isDownloading,
|
||||
isDownloaded,
|
||||
} from '../types/Attachment';
|
||||
import type { StickerType } from '../types/Stickers';
|
||||
import type { LinkPreviewType } from '../types/message/LinkPreviews';
|
||||
|
||||
type ReturnType = {
|
||||
bodyAttachment?: AttachmentType;
|
||||
attachments: Array<AttachmentType>;
|
||||
editHistory?: Array<EditHistoryType>;
|
||||
preview: Array<LinkPreviewType>;
|
||||
contact: Array<EmbeddedContactType>;
|
||||
quote?: QuotedMessageType;
|
||||
|
@ -45,8 +52,10 @@ export async function queueAttachmentDownloads(
|
|||
let count = 0;
|
||||
let bodyAttachment;
|
||||
|
||||
const idLog = `queueAttachmentDownloads(${idForLogging}})`;
|
||||
|
||||
log.info(
|
||||
`Queueing ${attachmentsToQueue.length} attachment downloads for message ${idForLogging}`
|
||||
`${idLog}: Queueing ${attachmentsToQueue.length} attachment downloads`
|
||||
);
|
||||
|
||||
const [longMessageAttachments, normalAttachments] = partition(
|
||||
|
@ -55,13 +64,11 @@ export async function queueAttachmentDownloads(
|
|||
);
|
||||
|
||||
if (longMessageAttachments.length > 1) {
|
||||
log.error(
|
||||
`Received more than one long message attachment in message ${idForLogging}`
|
||||
);
|
||||
log.error(`${idLog}: Received more than one long message attachment`);
|
||||
}
|
||||
|
||||
log.info(
|
||||
`Queueing ${longMessageAttachments.length} long message attachment downloads for message ${idForLogging}`
|
||||
`${idLog}: Queueing ${longMessageAttachments.length} long message attachment downloads`
|
||||
);
|
||||
|
||||
if (longMessageAttachments.length > 0) {
|
||||
|
@ -82,63 +89,31 @@ export async function queueAttachmentDownloads(
|
|||
}
|
||||
|
||||
log.info(
|
||||
`Queueing ${normalAttachments.length} normal attachment downloads for message ${idForLogging}`
|
||||
`${idLog}: Queueing ${normalAttachments.length} normal attachment downloads`
|
||||
);
|
||||
const attachments = await Promise.all(
|
||||
normalAttachments.map((attachment, index) => {
|
||||
if (!attachment) {
|
||||
return attachment;
|
||||
}
|
||||
// We've already downloaded this!
|
||||
if (attachment.path || attachment.textAttachment) {
|
||||
log.info(
|
||||
`Normal attachment already downloaded for message ${idForLogging}`
|
||||
);
|
||||
return attachment;
|
||||
}
|
||||
|
||||
count += 1;
|
||||
|
||||
return AttachmentDownloads.addJob(attachment, {
|
||||
messageId,
|
||||
type: 'attachment',
|
||||
index,
|
||||
});
|
||||
})
|
||||
const { attachments, count: attachmentsCount } = await queueNormalAttachments(
|
||||
idLog,
|
||||
messageId,
|
||||
normalAttachments,
|
||||
message.editHistory?.flatMap(x => x.attachments ?? [])
|
||||
);
|
||||
count += attachmentsCount;
|
||||
|
||||
const previewsToQueue = message.preview || [];
|
||||
log.info(
|
||||
`Queueing ${previewsToQueue.length} preview attachment downloads for message ${idForLogging}`
|
||||
`${idLog}: Queueing ${previewsToQueue.length} preview attachment downloads`
|
||||
);
|
||||
const preview = await Promise.all(
|
||||
previewsToQueue.map(async (item, index) => {
|
||||
if (!item.image) {
|
||||
return item;
|
||||
}
|
||||
// We've already downloaded this!
|
||||
if (item.image.path) {
|
||||
log.info(
|
||||
`Preview attachment already downloaded for message ${idForLogging}`
|
||||
);
|
||||
return item;
|
||||
}
|
||||
|
||||
count += 1;
|
||||
return {
|
||||
...item,
|
||||
image: await AttachmentDownloads.addJob(item.image, {
|
||||
messageId,
|
||||
type: 'preview',
|
||||
index,
|
||||
}),
|
||||
};
|
||||
})
|
||||
const { preview, count: previewCount } = await queuePreviews(
|
||||
idLog,
|
||||
messageId,
|
||||
previewsToQueue,
|
||||
message.editHistory?.flatMap(x => x.preview ?? [])
|
||||
);
|
||||
count += previewCount;
|
||||
|
||||
const contactsToQueue = message.contact || [];
|
||||
log.info(
|
||||
`Queueing ${contactsToQueue.length} contact attachment downloads for message ${idForLogging}`
|
||||
`${idLog}: Queueing ${contactsToQueue.length} contact attachment downloads`
|
||||
);
|
||||
const contact = await Promise.all(
|
||||
contactsToQueue.map(async (item, index) => {
|
||||
|
@ -147,9 +122,7 @@ export async function queueAttachmentDownloads(
|
|||
}
|
||||
// We've already downloaded this!
|
||||
if (item.avatar.avatar.path) {
|
||||
log.info(
|
||||
`Contact attachment already downloaded for message ${idForLogging}`
|
||||
);
|
||||
log.info(`${idLog}: Contact attachment already downloaded`);
|
||||
return item;
|
||||
}
|
||||
|
||||
|
@ -172,7 +145,7 @@ export async function queueAttachmentDownloads(
|
|||
const quoteAttachmentsToQueue =
|
||||
quote && quote.attachments ? quote.attachments : [];
|
||||
log.info(
|
||||
`Queueing ${quoteAttachmentsToQueue.length} quote attachment downloads for message ${idForLogging}`
|
||||
`${idLog}: Queueing ${quoteAttachmentsToQueue.length} quote attachment downloads`
|
||||
);
|
||||
if (quote && quoteAttachmentsToQueue.length > 0) {
|
||||
quote = {
|
||||
|
@ -184,9 +157,7 @@ export async function queueAttachmentDownloads(
|
|||
}
|
||||
// We've already downloaded this!
|
||||
if (item.thumbnail.path) {
|
||||
log.info(
|
||||
`Quote attachment already downloaded for message ${idForLogging}`
|
||||
);
|
||||
log.info(`${idLog}: Quote attachment already downloaded`);
|
||||
return item;
|
||||
}
|
||||
|
||||
|
@ -206,11 +177,9 @@ export async function queueAttachmentDownloads(
|
|||
|
||||
let { sticker } = message;
|
||||
if (sticker && sticker.data && sticker.data.path) {
|
||||
log.info(
|
||||
`Sticker attachment already downloaded for message ${idForLogging}`
|
||||
);
|
||||
log.info(`${idLog}: Sticker attachment already downloaded`);
|
||||
} else if (sticker) {
|
||||
log.info(`Queueing sticker download for message ${idForLogging}`);
|
||||
log.info(`${idLog}: Queueing sticker download`);
|
||||
count += 1;
|
||||
const { packId, stickerId, packKey } = sticker;
|
||||
|
||||
|
@ -222,7 +191,7 @@ export async function queueAttachmentDownloads(
|
|||
data = await copyStickerToAttachments(packId, stickerId);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Problem copying sticker (${packId}, ${stickerId}) to attachments:`,
|
||||
`${idLog}: Problem copying sticker (${packId}, ${stickerId}) to attachments:`,
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
}
|
||||
|
@ -252,20 +221,197 @@ export async function queueAttachmentDownloads(
|
|||
};
|
||||
}
|
||||
|
||||
log.info(
|
||||
`Queued ${count} total attachment downloads for message ${idForLogging}`
|
||||
);
|
||||
let { editHistory } = message;
|
||||
if (editHistory) {
|
||||
log.info(`${idLog}: Looping through ${editHistory.length} edits`);
|
||||
editHistory = await Promise.all(
|
||||
editHistory.map(async edit => {
|
||||
const editAttachmentsToQueue = edit.attachments || [];
|
||||
log.info(
|
||||
`${idLog}: Queueing ${editAttachmentsToQueue.length} normal attachment downloads (edited:${edit.timestamp})`
|
||||
);
|
||||
|
||||
const { attachments: editAttachments, count: editAttachmentsCount } =
|
||||
await queueNormalAttachments(
|
||||
idLog,
|
||||
messageId,
|
||||
edit.attachments,
|
||||
attachments
|
||||
);
|
||||
count += editAttachmentsCount;
|
||||
|
||||
log.info(
|
||||
`${idLog}: Queueing ${
|
||||
(edit.preview || []).length
|
||||
} preview attachment downloads (edited:${edit.timestamp})`
|
||||
);
|
||||
const { preview: editPreview, count: editPreviewCount } =
|
||||
await queuePreviews(idLog, messageId, edit.preview, preview);
|
||||
count += editPreviewCount;
|
||||
|
||||
return {
|
||||
...edit,
|
||||
attachments: editAttachments,
|
||||
preview: editPreview,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
log.info(`${idLog}: Queued ${count} total attachment downloads`);
|
||||
|
||||
if (count <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
bodyAttachment,
|
||||
attachments,
|
||||
preview,
|
||||
bodyAttachment,
|
||||
contact,
|
||||
editHistory,
|
||||
preview,
|
||||
quote,
|
||||
sticker,
|
||||
};
|
||||
}
|
||||
|
||||
async function queueNormalAttachments(
|
||||
idLog: string,
|
||||
messageId: string,
|
||||
attachments: MessageAttributesType['attachments'] = [],
|
||||
otherAttachments: MessageAttributesType['attachments']
|
||||
): Promise<{
|
||||
attachments: Array<AttachmentType>;
|
||||
count: number;
|
||||
}> {
|
||||
// Look through "otherAttachments" which can either be attachments in the
|
||||
// edit history or the message's attachments and see if any of the attachments
|
||||
// are the same. If they are let's replace it so that we don't download more
|
||||
// than once.
|
||||
// We don't also register the signatures for "attachments" because they would
|
||||
// then not be added to the AttachmentDownloads job.
|
||||
const attachmentSignatures: Map<string, AttachmentType> = new Map();
|
||||
otherAttachments?.forEach(attachment => {
|
||||
const signature = getAttachmentSignature(attachment);
|
||||
attachmentSignatures.set(signature, attachment);
|
||||
});
|
||||
|
||||
let count = 0;
|
||||
const nextAttachments = await Promise.all(
|
||||
attachments.map((attachment, index) => {
|
||||
if (!attachment) {
|
||||
return attachment;
|
||||
}
|
||||
// We've already downloaded this!
|
||||
if (isDownloaded(attachment)) {
|
||||
log.info(`${idLog}: Normal attachment already downloaded`);
|
||||
return attachment;
|
||||
}
|
||||
|
||||
const signature = getAttachmentSignature(attachment);
|
||||
const existingAttachment = signature
|
||||
? attachmentSignatures.get(signature)
|
||||
: undefined;
|
||||
|
||||
// We've already downloaded this elsewhere!
|
||||
if (
|
||||
existingAttachment &&
|
||||
(isDownloading(existingAttachment) || isDownloaded(existingAttachment))
|
||||
) {
|
||||
log.info(
|
||||
`${idLog}: Normal attachment already downloaded in other attachments. Replacing`
|
||||
);
|
||||
// Incrementing count so that we update the message's fields downstream
|
||||
count += 1;
|
||||
return existingAttachment;
|
||||
}
|
||||
|
||||
count += 1;
|
||||
|
||||
return AttachmentDownloads.addJob(attachment, {
|
||||
messageId,
|
||||
type: 'attachment',
|
||||
index,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
attachments: nextAttachments,
|
||||
count,
|
||||
};
|
||||
}
|
||||
|
||||
function getLinkPreviewSignature(preview: LinkPreviewType): string | undefined {
|
||||
const { image, url } = preview;
|
||||
|
||||
if (!image) {
|
||||
return;
|
||||
}
|
||||
|
||||
return `<${url}>${getAttachmentSignature(image)}`;
|
||||
}
|
||||
|
||||
async function queuePreviews(
|
||||
idLog: string,
|
||||
messageId: string,
|
||||
previews: MessageAttributesType['preview'] = [],
|
||||
otherPreviews: MessageAttributesType['preview']
|
||||
): Promise<{ preview: Array<LinkPreviewType>; count: number }> {
|
||||
// Similar to queueNormalAttachments' logic for detecting same attachments
|
||||
// except here we also pick by link preview URL.
|
||||
const previewSignatures: Map<string, LinkPreviewType> = new Map();
|
||||
otherPreviews?.forEach(preview => {
|
||||
const signature = getLinkPreviewSignature(preview);
|
||||
if (!signature) {
|
||||
return;
|
||||
}
|
||||
previewSignatures.set(signature, preview);
|
||||
});
|
||||
|
||||
let count = 0;
|
||||
|
||||
const preview = await Promise.all(
|
||||
previews.map(async (item, index) => {
|
||||
if (!item.image) {
|
||||
return item;
|
||||
}
|
||||
// We've already downloaded this!
|
||||
if (isDownloaded(item.image)) {
|
||||
log.info(`${idLog}: Preview attachment already downloaded`);
|
||||
return item;
|
||||
}
|
||||
const signature = getLinkPreviewSignature(item);
|
||||
const existingPreview = signature
|
||||
? previewSignatures.get(signature)
|
||||
: undefined;
|
||||
|
||||
// We've already downloaded this elsewhere!
|
||||
if (
|
||||
existingPreview &&
|
||||
(isDownloading(existingPreview.image) ||
|
||||
isDownloaded(existingPreview.image))
|
||||
) {
|
||||
log.info(`${idLog}: Preview already downloaded elsewhere. Replacing`);
|
||||
// Incrementing count so that we update the message's fields downstream
|
||||
count += 1;
|
||||
return existingPreview;
|
||||
}
|
||||
|
||||
count += 1;
|
||||
return {
|
||||
...item,
|
||||
image: await AttachmentDownloads.addJob(item.image, {
|
||||
messageId,
|
||||
type: 'preview',
|
||||
index,
|
||||
}),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
preview,
|
||||
count,
|
||||
};
|
||||
}
|
||||
|
|
82
ts/util/shouldReplyNotifyUser.ts
Normal file
82
ts/util/shouldReplyNotifyUser.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ConversationModel } from '../models/conversations';
|
||||
import type { UUID } from '../types/UUID';
|
||||
import type { MessageModel } from '../models/messages';
|
||||
import * as log from '../logging/log';
|
||||
import dataInterface from '../sql/Client';
|
||||
import { isGroup } from './whatTypeOfConversation';
|
||||
import { isMessageUnread } from './isMessageUnread';
|
||||
|
||||
function isSameUuid(
|
||||
a: UUID | string | null | undefined,
|
||||
b: UUID | string | null | undefined
|
||||
): boolean {
|
||||
return a != null && b != null && String(a) === String(b);
|
||||
}
|
||||
|
||||
export async function shouldReplyNotifyUser(
|
||||
message: MessageModel,
|
||||
conversation: ConversationModel
|
||||
): Promise<boolean> {
|
||||
// Don't notify if the message has already been read
|
||||
if (!isMessageUnread(message.attributes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const storyId = message.get('storyId');
|
||||
|
||||
// If this is not a reply to a story, always notify.
|
||||
if (storyId == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Always notify if this is not a group
|
||||
if (!isGroup(conversation.attributes)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const matchedStory = window.reduxStore
|
||||
.getState()
|
||||
.stories.stories.find(story => {
|
||||
return story.messageId === storyId;
|
||||
});
|
||||
|
||||
// If we can't find the story, don't notify
|
||||
if (matchedStory == null) {
|
||||
log.warn("Couldn't find story for reply");
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentUserId = window.textsecure.storage.user.getUuid();
|
||||
const storySourceId = matchedStory.sourceUuid;
|
||||
|
||||
const currentUserIdSource = isSameUuid(storySourceId, currentUserId);
|
||||
|
||||
// If the story is from the current user, always notify
|
||||
if (currentUserIdSource) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the story is from a different user, only notify if the user has
|
||||
// replied or reacted to the story
|
||||
|
||||
const replies = await dataInterface.getOlderMessagesByConversation({
|
||||
conversationId: conversation.id,
|
||||
limit: 9000,
|
||||
storyId,
|
||||
includeStoryReplies: true,
|
||||
});
|
||||
|
||||
const prevCurrentUserReply = replies.find(replyMessage => {
|
||||
return replyMessage.type === 'outgoing';
|
||||
});
|
||||
|
||||
if (prevCurrentUserReply != null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise don't notify
|
||||
return false;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue