mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-04-25 19:08:04 +01:00
Use minimal replacement class for MessageModel
This commit is contained in:
@@ -6,7 +6,55 @@ import type { AttachmentDownloadJobTypeType } from '../types/AttachmentDownload'
|
||||
|
||||
import type { AttachmentType } from '../types/Attachment';
|
||||
import { getAttachmentSignatureSafe, isDownloaded } from '../types/Attachment';
|
||||
import { __DEPRECATED$getMessageById } from '../messages/getMessageById';
|
||||
import { getMessageById } from '../messages/getMessageById';
|
||||
|
||||
export async function markAttachmentAsCorrupted(
|
||||
messageId: string,
|
||||
attachment: AttachmentType
|
||||
): Promise<void> {
|
||||
const message = await getMessageById(messageId);
|
||||
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!attachment.path) {
|
||||
throw new Error(
|
||||
"Attachment can't be marked as corrupted because it wasn't loaded"
|
||||
);
|
||||
}
|
||||
|
||||
// We intentionally don't check in quotes/stickers/contacts/... here,
|
||||
// because this function should be called only for something that can
|
||||
// be displayed as a generic attachment.
|
||||
const attachments: ReadonlyArray<AttachmentType> =
|
||||
message.get('attachments') || [];
|
||||
|
||||
let changed = false;
|
||||
const newAttachments = attachments.map(existing => {
|
||||
if (existing.path !== attachment.path) {
|
||||
return existing;
|
||||
}
|
||||
changed = true;
|
||||
|
||||
return {
|
||||
...existing,
|
||||
isCorrupted: true,
|
||||
};
|
||||
});
|
||||
|
||||
if (!changed) {
|
||||
throw new Error(
|
||||
"Attachment can't be marked as corrupted because it wasn't found"
|
||||
);
|
||||
}
|
||||
|
||||
log.info('markAttachmentAsCorrupted: marking an attachment as corrupted');
|
||||
|
||||
message.set({
|
||||
attachments: newAttachments,
|
||||
});
|
||||
}
|
||||
|
||||
export async function addAttachmentToMessage(
|
||||
messageId: string,
|
||||
@@ -15,7 +63,7 @@ export async function addAttachmentToMessage(
|
||||
{ type }: { type: AttachmentDownloadJobTypeType }
|
||||
): Promise<void> {
|
||||
const logPrefix = `${jobLogId}/addAttachmentToMessage`;
|
||||
const message = await __DEPRECATED$getMessageById(messageId, logPrefix);
|
||||
const message = await getMessageById(messageId);
|
||||
|
||||
if (!message) {
|
||||
return;
|
||||
|
||||
@@ -9,6 +9,7 @@ import * as Errors from '../types/errors';
|
||||
import { deleteForEveryone } from '../util/deleteForEveryone';
|
||||
import { drop } from '../util/drop';
|
||||
import { getMessageSentTimestampSet } from '../util/getMessageSentTimestampSet';
|
||||
import { MessageModel } from '../models/messages';
|
||||
|
||||
export type DeleteAttributesType = {
|
||||
envelopeId: string;
|
||||
@@ -86,10 +87,8 @@ export async function onDelete(del: DeleteAttributesType): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = window.MessageCache.__DEPRECATED$register(
|
||||
targetMessage.id,
|
||||
targetMessage,
|
||||
'Deletes.onDelete'
|
||||
const message = window.MessageCache.register(
|
||||
new MessageModel(targetMessage)
|
||||
);
|
||||
|
||||
await deleteForEveryone(message, del);
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
isAttachmentDownloadQueueEmpty,
|
||||
registerQueueEmptyCallback,
|
||||
} from '../util/attachmentDownloadQueue';
|
||||
import { MessageModel } from '../models/messages';
|
||||
|
||||
export type EditAttributesType = {
|
||||
conversationId: string;
|
||||
@@ -134,10 +135,8 @@ export async function onEdit(edit: EditAttributesType): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = window.MessageCache.__DEPRECATED$register(
|
||||
targetMessage.id,
|
||||
targetMessage,
|
||||
'Edits.onEdit'
|
||||
const message = window.MessageCache.register(
|
||||
new MessageModel(targetMessage)
|
||||
);
|
||||
|
||||
await handleEditMessage(message.attributes, edit);
|
||||
|
||||
@@ -32,6 +32,9 @@ import {
|
||||
RECEIPT_BATCHER_WAIT_MS,
|
||||
} from '../types/Receipt';
|
||||
import { drop } from '../util/drop';
|
||||
import { getMessageById } from '../messages/getMessageById';
|
||||
import { postSaveUpdates } from '../util/cleanup';
|
||||
import { MessageModel } from '../models/messages';
|
||||
|
||||
const { deleteSentProtoRecipient, removeSyncTaskById } = DataWriter;
|
||||
|
||||
@@ -78,12 +81,11 @@ const processReceiptBatcher = createWaitBatcher({
|
||||
> = new Map();
|
||||
|
||||
function addReceiptAndTargetMessage(
|
||||
message: MessageAttributesType,
|
||||
message: MessageModel,
|
||||
receipt: MessageReceiptAttributesType
|
||||
): void {
|
||||
const existing = receiptsByMessageId.get(message.id);
|
||||
if (!existing) {
|
||||
window.MessageCache.toMessageAttributes(message);
|
||||
receiptsByMessageId.set(message.id, [receipt]);
|
||||
} else {
|
||||
existing.push(receipt);
|
||||
@@ -151,9 +153,10 @@ const processReceiptBatcher = createWaitBatcher({
|
||||
);
|
||||
|
||||
if (targetMessages.length) {
|
||||
targetMessages.forEach(msg =>
|
||||
addReceiptAndTargetMessage(msg, receipt)
|
||||
);
|
||||
targetMessages.forEach(msg => {
|
||||
const model = window.MessageCache.register(new MessageModel(msg));
|
||||
addReceiptAndTargetMessage(model, receipt);
|
||||
});
|
||||
} else {
|
||||
// Nope, no target message was found
|
||||
const { receiptSync } = receipt;
|
||||
@@ -188,53 +191,43 @@ async function processReceiptsForMessage(
|
||||
}
|
||||
|
||||
// Get message from cache or DB
|
||||
const message = await window.MessageCache.resolveAttributes(
|
||||
'processReceiptsForMessage',
|
||||
messageId
|
||||
);
|
||||
const message = await getMessageById(messageId);
|
||||
if (!message) {
|
||||
throw new Error(
|
||||
`processReceiptsForMessage: Failed to find message ${messageId}`
|
||||
);
|
||||
}
|
||||
|
||||
// Note: it is important to have no `await` in between `resolveAttributes` and
|
||||
// `setAttributes` since it might overwrite other updates otherwise.
|
||||
const { updatedMessage, validReceipts, droppedReceipts } =
|
||||
updateMessageWithReceipts(message, receipts);
|
||||
const { validReceipts } = await updateMessageWithReceipts(message, receipts);
|
||||
|
||||
// Save it to cache & to DB, and remove dropped receipts
|
||||
await Promise.all([
|
||||
window.MessageCache.setAttributes({
|
||||
messageId,
|
||||
messageAttributes: updatedMessage,
|
||||
skipSaveToDatabase: false,
|
||||
}),
|
||||
Promise.all(droppedReceipts.map(remove)),
|
||||
]);
|
||||
const ourAci = window.textsecure.storage.user.getCheckedAci();
|
||||
await DataWriter.saveMessage(message.attributes, { ourAci, postSaveUpdates });
|
||||
|
||||
// Confirm/remove receipts, and delete sent protos
|
||||
for (const receipt of validReceipts) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await remove(receipt);
|
||||
drop(addToDeleteSentProtoBatcher(receipt, updatedMessage));
|
||||
drop(addToDeleteSentProtoBatcher(receipt, message.attributes));
|
||||
}
|
||||
|
||||
// notify frontend listeners
|
||||
const conversation = window.ConversationController.get(
|
||||
message.conversationId
|
||||
message.get('conversationId')
|
||||
);
|
||||
conversation?.debouncedUpdateLastMessage?.();
|
||||
}
|
||||
|
||||
function updateMessageWithReceipts(
|
||||
message: MessageAttributesType,
|
||||
async function updateMessageWithReceipts(
|
||||
message: MessageModel,
|
||||
receipts: Array<MessageReceiptAttributesType>
|
||||
): {
|
||||
updatedMessage: MessageAttributesType;
|
||||
): Promise<{
|
||||
validReceipts: Array<MessageReceiptAttributesType>;
|
||||
droppedReceipts: Array<MessageReceiptAttributesType>;
|
||||
} {
|
||||
const logId = `updateMessageWithReceipts(timestamp=${message.timestamp})`;
|
||||
}> {
|
||||
const logId = `updateMessageWithReceipts(timestamp=${message.get('timestamp')})`;
|
||||
|
||||
const droppedReceipts: Array<MessageReceiptAttributesType> = [];
|
||||
const receiptsToProcess = receipts.filter(receipt => {
|
||||
if (shouldDropReceipt(receipt, message)) {
|
||||
if (shouldDropReceipt(receipt, message.attributes)) {
|
||||
const { receiptSync } = receipt;
|
||||
log.info(
|
||||
`${logId}: Dropping a receipt ${receiptSync.type} for sentAt=${receiptSync.messageSentAt}`
|
||||
@@ -257,14 +250,16 @@ function updateMessageWithReceipts(
|
||||
);
|
||||
|
||||
// Generate the updated message synchronously
|
||||
let updatedMessage: MessageAttributesType = { ...message };
|
||||
let { attributes } = message;
|
||||
for (const receipt of receiptsToProcess) {
|
||||
updatedMessage = {
|
||||
...updatedMessage,
|
||||
...updateMessageSendStateWithReceipt(updatedMessage, receipt),
|
||||
attributes = {
|
||||
...attributes,
|
||||
...updateMessageSendStateWithReceipt(attributes, receipt),
|
||||
};
|
||||
}
|
||||
return { updatedMessage, validReceipts: receiptsToProcess, droppedReceipts };
|
||||
message.set(attributes);
|
||||
|
||||
return { validReceipts: receiptsToProcess };
|
||||
}
|
||||
|
||||
const deleteSentProtoBatcher = createWaitBatcher({
|
||||
@@ -310,7 +305,7 @@ function getTargetMessage({
|
||||
sourceConversationId: string;
|
||||
messagesMatchingTimestamp: ReadonlyArray<MessageAttributesType>;
|
||||
targetTimestamp: number;
|
||||
}): MessageAttributesType | null {
|
||||
}): MessageModel | null {
|
||||
if (messagesMatchingTimestamp.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -366,7 +361,7 @@ function getTargetMessage({
|
||||
}
|
||||
|
||||
const message = matchingMessages[0];
|
||||
return window.MessageCache.toMessageAttributes(message);
|
||||
return window.MessageCache.register(new MessageModel(message));
|
||||
}
|
||||
const wasDeliveredWithSealedSender = (
|
||||
conversationId: string,
|
||||
|
||||
@@ -1,23 +1,45 @@
|
||||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { maxBy } from 'lodash';
|
||||
|
||||
import type { AciString } from '../types/ServiceId';
|
||||
import type {
|
||||
MessageAttributesType,
|
||||
MessageReactionType,
|
||||
ReadonlyMessageAttributesType,
|
||||
} from '../model-types.d';
|
||||
import type { MessageModel } from '../models/messages';
|
||||
import type { ReactionSource } from '../reactions/ReactionSource';
|
||||
import { DataReader } from '../sql/Client';
|
||||
import { MessageModel } from '../models/messages';
|
||||
import { ReactionSource } from '../reactions/ReactionSource';
|
||||
import { DataReader, DataWriter } from '../sql/Client';
|
||||
import * as Errors from '../types/errors';
|
||||
import * as log from '../logging/log';
|
||||
import { getAuthor } from '../messages/helpers';
|
||||
import { getAuthor, isIncoming, isOutgoing } from '../messages/helpers';
|
||||
import { getMessageSentTimestampSet } from '../util/getMessageSentTimestampSet';
|
||||
import { isMe } from '../util/whatTypeOfConversation';
|
||||
import { isStory } from '../state/selectors/message';
|
||||
import { isDirectConversation, isMe } from '../util/whatTypeOfConversation';
|
||||
import {
|
||||
getMessagePropStatus,
|
||||
hasErrors,
|
||||
isStory,
|
||||
} from '../state/selectors/message';
|
||||
import { getPropForTimestamp } from '../util/editHelpers';
|
||||
import { isSent } from '../messages/MessageSendState';
|
||||
import { strictAssert } from '../util/assert';
|
||||
import { repeat, zipObject } from '../util/iterables';
|
||||
import { getMessageIdForLogging } from '../util/idForLogging';
|
||||
import { hydrateStoryContext } from '../util/hydrateStoryContext';
|
||||
import { shouldReplyNotifyUser } from '../util/shouldReplyNotifyUser';
|
||||
import { drop } from '../util/drop';
|
||||
import * as reactionUtil from '../reactions/util';
|
||||
import { isNewReactionReplacingPrevious } from '../reactions/util';
|
||||
import { notificationService } from '../services/notifications';
|
||||
import { ReactionReadStatus } from '../types/Reactions';
|
||||
import type { ConversationQueueJobData } from '../jobs/conversationJobQueue';
|
||||
import {
|
||||
conversationJobQueue,
|
||||
conversationQueueJobEnum,
|
||||
} from '../jobs/conversationJobQueue';
|
||||
import { postSaveUpdates } from '../util/cleanup';
|
||||
|
||||
export type ReactionAttributesType = {
|
||||
emoji: string;
|
||||
@@ -36,24 +58,26 @@ export type ReactionAttributesType = {
|
||||
receivedAtDate: number;
|
||||
};
|
||||
|
||||
const reactions = new Map<string, ReactionAttributesType>();
|
||||
const reactionCache = new Map<string, ReactionAttributesType>();
|
||||
|
||||
function remove(reaction: ReactionAttributesType): void {
|
||||
reactions.delete(reaction.envelopeId);
|
||||
reactionCache.delete(reaction.envelopeId);
|
||||
reaction.removeFromMessageReceiverCache();
|
||||
}
|
||||
|
||||
export function findReactionsForMessage(
|
||||
message: ReadonlyMessageAttributesType
|
||||
): Array<ReactionAttributesType> {
|
||||
const matchingReactions = Array.from(reactions.values()).filter(reaction => {
|
||||
return isMessageAMatchForReaction({
|
||||
message,
|
||||
targetTimestamp: reaction.targetTimestamp,
|
||||
targetAuthorAci: reaction.targetAuthorAci,
|
||||
reactionSenderConversationId: reaction.fromId,
|
||||
});
|
||||
});
|
||||
const matchingReactions = Array.from(reactionCache.values()).filter(
|
||||
reaction => {
|
||||
return isMessageAMatchForReaction({
|
||||
message,
|
||||
targetTimestamp: reaction.targetTimestamp,
|
||||
targetAuthorAci: reaction.targetAuthorAci,
|
||||
reactionSenderConversationId: reaction.fromId,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
matchingReactions.forEach(reaction => remove(reaction));
|
||||
return matchingReactions;
|
||||
@@ -173,7 +197,7 @@ function isMessageAMatchForReaction({
|
||||
export async function onReaction(
|
||||
reaction: ReactionAttributesType
|
||||
): Promise<void> {
|
||||
reactions.set(reaction.envelopeId, reaction);
|
||||
reactionCache.set(reaction.envelopeId, reaction);
|
||||
|
||||
const logId = `Reactions.onReaction(timestamp=${reaction.timestamp};target=${reaction.targetTimestamp})`;
|
||||
|
||||
@@ -231,23 +255,21 @@ export async function onReaction(
|
||||
return;
|
||||
}
|
||||
|
||||
const targetMessageModel = window.MessageCache.__DEPRECATED$register(
|
||||
targetMessage.id,
|
||||
targetMessage,
|
||||
'Reactions.onReaction'
|
||||
const targetMessageModel = window.MessageCache.register(
|
||||
new MessageModel(targetMessage)
|
||||
);
|
||||
|
||||
// Use the generated message in ts/background.ts to create a message
|
||||
// if the reaction is targeted at a story.
|
||||
if (!isStory(targetMessage)) {
|
||||
await targetMessageModel.handleReaction(reaction);
|
||||
await handleReaction(targetMessageModel, reaction);
|
||||
} else {
|
||||
const generatedMessage = reaction.generatedMessageForStoryReaction;
|
||||
strictAssert(
|
||||
generatedMessage,
|
||||
'Generated message must exist for story reaction'
|
||||
);
|
||||
await generatedMessage.handleReaction(reaction, {
|
||||
await handleReaction(generatedMessage, reaction, {
|
||||
storyMessage: targetMessage,
|
||||
});
|
||||
}
|
||||
@@ -260,3 +282,324 @@ export async function onReaction(
|
||||
log.error(`${logId} error:`, Errors.toLogFormat(error));
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleReaction(
|
||||
message: MessageModel,
|
||||
reaction: ReactionAttributesType,
|
||||
{
|
||||
storyMessage,
|
||||
shouldPersist = true,
|
||||
}: {
|
||||
storyMessage?: MessageAttributesType;
|
||||
shouldPersist?: boolean;
|
||||
} = {}
|
||||
): Promise<void> {
|
||||
const { attributes } = message;
|
||||
|
||||
if (message.get('deletedForEveryone')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We allow you to react to messages with outgoing errors only if it has sent
|
||||
// successfully to at least one person.
|
||||
if (
|
||||
hasErrors(attributes) &&
|
||||
(isIncoming(attributes) ||
|
||||
getMessagePropStatus(
|
||||
attributes,
|
||||
window.ConversationController.getOurConversationIdOrThrow()
|
||||
) !== 'partial-sent')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const conversation = window.ConversationController.get(
|
||||
message.attributes.conversationId
|
||||
);
|
||||
if (!conversation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isFromThisDevice = reaction.source === ReactionSource.FromThisDevice;
|
||||
const isFromSync = reaction.source === ReactionSource.FromSync;
|
||||
const isFromSomeoneElse = reaction.source === ReactionSource.FromSomeoneElse;
|
||||
strictAssert(
|
||||
isFromThisDevice || isFromSync || isFromSomeoneElse,
|
||||
'Reaction can only be from this device, from sync, or from someone else'
|
||||
);
|
||||
|
||||
const newReaction: MessageReactionType = {
|
||||
emoji: reaction.remove ? undefined : reaction.emoji,
|
||||
fromId: reaction.fromId,
|
||||
targetTimestamp: reaction.targetTimestamp,
|
||||
timestamp: reaction.timestamp,
|
||||
isSentByConversationId: isFromThisDevice
|
||||
? zipObject(conversation.getMemberConversationIds(), repeat(false))
|
||||
: undefined,
|
||||
};
|
||||
|
||||
// Reactions to stories are saved as separate messages, and so require a totally
|
||||
// different codepath.
|
||||
if (storyMessage) {
|
||||
if (isFromThisDevice) {
|
||||
log.info(
|
||||
'handleReaction: sending story reaction to ' +
|
||||
`${getMessageIdForLogging(storyMessage)} from this device`
|
||||
);
|
||||
} else {
|
||||
if (isFromSomeoneElse) {
|
||||
log.info(
|
||||
'handleReaction: receiving story reaction to ' +
|
||||
`${getMessageIdForLogging(storyMessage)} from someone else`
|
||||
);
|
||||
} else if (isFromSync) {
|
||||
log.info(
|
||||
'handleReaction: receiving story reaction to ' +
|
||||
`${getMessageIdForLogging(storyMessage)} from another device`
|
||||
);
|
||||
}
|
||||
|
||||
const generatedMessage = reaction.generatedMessageForStoryReaction;
|
||||
strictAssert(
|
||||
generatedMessage,
|
||||
'Story reactions must provide storyReactionMessage'
|
||||
);
|
||||
const targetConversation = window.ConversationController.get(
|
||||
generatedMessage.get('conversationId')
|
||||
);
|
||||
strictAssert(
|
||||
targetConversation,
|
||||
'handleReaction: targetConversation not found'
|
||||
);
|
||||
|
||||
window.MessageCache.register(generatedMessage);
|
||||
generatedMessage.set({
|
||||
expireTimer: isDirectConversation(targetConversation.attributes)
|
||||
? targetConversation.get('expireTimer')
|
||||
: undefined,
|
||||
storyId: storyMessage.id,
|
||||
storyReaction: {
|
||||
emoji: reaction.emoji,
|
||||
targetAuthorAci: reaction.targetAuthorAci,
|
||||
targetTimestamp: reaction.targetTimestamp,
|
||||
},
|
||||
});
|
||||
|
||||
await hydrateStoryContext(generatedMessage.id, storyMessage, {
|
||||
shouldSave: false,
|
||||
});
|
||||
// Note: generatedMessage comes with an id, so we have to force this save
|
||||
await DataWriter.saveMessage(generatedMessage.attributes, {
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
forceSave: true,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
log.info('Reactions.onReaction adding reaction to story', {
|
||||
reactionMessageId: getMessageIdForLogging(generatedMessage.attributes),
|
||||
storyId: getMessageIdForLogging(storyMessage),
|
||||
targetTimestamp: reaction.targetTimestamp,
|
||||
timestamp: reaction.timestamp,
|
||||
});
|
||||
|
||||
window.MessageCache.register(generatedMessage);
|
||||
if (isDirectConversation(targetConversation.attributes)) {
|
||||
await targetConversation.addSingleMessage(generatedMessage.attributes);
|
||||
if (!targetConversation.get('active_at')) {
|
||||
targetConversation.set({
|
||||
active_at: generatedMessage.attributes.timestamp,
|
||||
});
|
||||
await DataWriter.updateConversation(targetConversation.attributes);
|
||||
}
|
||||
}
|
||||
|
||||
if (isFromSomeoneElse) {
|
||||
log.info(
|
||||
'handleReaction: notifying for story reaction to ' +
|
||||
`${getMessageIdForLogging(storyMessage)} from someone else`
|
||||
);
|
||||
if (
|
||||
await shouldReplyNotifyUser(
|
||||
generatedMessage.attributes,
|
||||
targetConversation
|
||||
)
|
||||
) {
|
||||
drop(targetConversation.notify(generatedMessage.attributes));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Reactions to all messages other than stories will update the target message
|
||||
const previousLength = (message.get('reactions') || []).length;
|
||||
|
||||
if (isFromThisDevice) {
|
||||
log.info(
|
||||
`handleReaction: sending reaction to ${getMessageIdForLogging(message.attributes)} ` +
|
||||
'from this device'
|
||||
);
|
||||
|
||||
const reactions = reactionUtil.addOutgoingReaction(
|
||||
message.get('reactions') || [],
|
||||
newReaction
|
||||
);
|
||||
message.set({ reactions });
|
||||
} else {
|
||||
const oldReactions = message.get('reactions') || [];
|
||||
let reactions: Array<MessageReactionType>;
|
||||
const oldReaction = oldReactions.find(re =>
|
||||
isNewReactionReplacingPrevious(re, newReaction)
|
||||
);
|
||||
if (oldReaction) {
|
||||
notificationService.removeBy({
|
||||
...oldReaction,
|
||||
messageId: message.id,
|
||||
});
|
||||
}
|
||||
|
||||
if (reaction.remove) {
|
||||
log.info(
|
||||
'handleReaction: removing reaction for message',
|
||||
getMessageIdForLogging(message.attributes)
|
||||
);
|
||||
|
||||
if (isFromSync) {
|
||||
reactions = oldReactions.filter(
|
||||
re =>
|
||||
!isNewReactionReplacingPrevious(re, newReaction) ||
|
||||
re.timestamp > reaction.timestamp
|
||||
);
|
||||
} else {
|
||||
reactions = oldReactions.filter(
|
||||
re => !isNewReactionReplacingPrevious(re, newReaction)
|
||||
);
|
||||
}
|
||||
message.set({ reactions });
|
||||
} else {
|
||||
log.info(
|
||||
'handleReaction: adding reaction for message',
|
||||
getMessageIdForLogging(message.attributes)
|
||||
);
|
||||
|
||||
let reactionToAdd: MessageReactionType;
|
||||
if (isFromSync) {
|
||||
const ourReactions = [
|
||||
newReaction,
|
||||
...oldReactions.filter(re => re.fromId === reaction.fromId),
|
||||
];
|
||||
reactionToAdd = maxBy(ourReactions, 'timestamp') || newReaction;
|
||||
} else {
|
||||
reactionToAdd = newReaction;
|
||||
}
|
||||
|
||||
reactions = oldReactions.filter(
|
||||
re => !isNewReactionReplacingPrevious(re, reaction)
|
||||
);
|
||||
reactions.push(reactionToAdd);
|
||||
message.set({ reactions });
|
||||
|
||||
if (isOutgoing(message.attributes) && isFromSomeoneElse) {
|
||||
void conversation.notify(message.attributes, reaction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (reaction.remove) {
|
||||
await DataWriter.removeReactionFromConversation({
|
||||
emoji: reaction.emoji,
|
||||
fromId: reaction.fromId,
|
||||
targetAuthorServiceId: reaction.targetAuthorAci,
|
||||
targetTimestamp: reaction.targetTimestamp,
|
||||
});
|
||||
} else {
|
||||
await DataWriter.addReaction(
|
||||
{
|
||||
conversationId: message.get('conversationId'),
|
||||
emoji: reaction.emoji,
|
||||
fromId: reaction.fromId,
|
||||
messageId: message.id,
|
||||
messageReceivedAt: message.get('received_at'),
|
||||
targetAuthorAci: reaction.targetAuthorAci,
|
||||
targetTimestamp: reaction.targetTimestamp,
|
||||
timestamp: reaction.timestamp,
|
||||
},
|
||||
{
|
||||
readStatus: isFromThisDevice
|
||||
? ReactionReadStatus.Read
|
||||
: ReactionReadStatus.Unread,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const currentLength = (message.get('reactions') || []).length;
|
||||
log.info(
|
||||
'handleReaction:',
|
||||
`Done processing reaction for message ${getMessageIdForLogging(message.attributes)}.`,
|
||||
`Went from ${previousLength} to ${currentLength} reactions.`
|
||||
);
|
||||
}
|
||||
|
||||
if (isFromThisDevice) {
|
||||
let jobData: ConversationQueueJobData;
|
||||
if (storyMessage) {
|
||||
strictAssert(
|
||||
newReaction.emoji !== undefined,
|
||||
'New story reaction must have an emoji'
|
||||
);
|
||||
|
||||
const generatedMessage = reaction.generatedMessageForStoryReaction;
|
||||
strictAssert(
|
||||
generatedMessage,
|
||||
'Story reactions must provide storyReactionmessage'
|
||||
);
|
||||
|
||||
await hydrateStoryContext(generatedMessage.id, message.attributes, {
|
||||
shouldSave: false,
|
||||
});
|
||||
await DataWriter.saveMessage(generatedMessage.attributes, {
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
forceSave: true,
|
||||
postSaveUpdates,
|
||||
});
|
||||
|
||||
window.MessageCache.register(generatedMessage);
|
||||
|
||||
void conversation.addSingleMessage(generatedMessage.attributes);
|
||||
|
||||
jobData = {
|
||||
type: conversationQueueJobEnum.enum.NormalMessage,
|
||||
conversationId: conversation.id,
|
||||
messageId: generatedMessage.id,
|
||||
revision: conversation.get('revision'),
|
||||
};
|
||||
} else {
|
||||
jobData = {
|
||||
type: conversationQueueJobEnum.enum.Reaction,
|
||||
conversationId: conversation.id,
|
||||
messageId: message.id,
|
||||
revision: conversation.get('revision'),
|
||||
};
|
||||
}
|
||||
if (shouldPersist) {
|
||||
await conversationJobQueue.add(jobData, async jobToInsert => {
|
||||
log.info(
|
||||
`enqueueReactionForSend: saving message ${getMessageIdForLogging(message.attributes)} and job ${
|
||||
jobToInsert.id
|
||||
}`
|
||||
);
|
||||
await DataWriter.saveMessage(message.attributes, {
|
||||
jobToInsert,
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
postSaveUpdates,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
await conversationJobQueue.add(jobData);
|
||||
}
|
||||
} else if (shouldPersist && !isStory(message.attributes)) {
|
||||
await DataWriter.saveMessage(message.attributes, {
|
||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||
postSaveUpdates,
|
||||
});
|
||||
window.reduxActions.conversations.markOpenConversationRead(conversation.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ import { queueUpdateMessage } from '../util/messageBatcher';
|
||||
import { strictAssert } from '../util/assert';
|
||||
import { isAciString } from '../util/isAciString';
|
||||
import { DataReader, DataWriter } from '../sql/Client';
|
||||
import { markRead } from '../services/MessageUpdater';
|
||||
import { MessageModel } from '../models/messages';
|
||||
|
||||
const { removeSyncTaskById } = DataWriter;
|
||||
|
||||
@@ -146,11 +148,7 @@ export async function onSync(sync: ReadSyncAttributesType): Promise<void> {
|
||||
|
||||
notificationService.removeBy({ messageId: found.id });
|
||||
|
||||
const message = window.MessageCache.__DEPRECATED$register(
|
||||
found.id,
|
||||
found,
|
||||
'ReadSyncs.onSync'
|
||||
);
|
||||
const message = window.MessageCache.register(new MessageModel(found));
|
||||
const readAt = Math.min(readSync.readAt, Date.now());
|
||||
const newestSentAt = readSync.timestamp;
|
||||
|
||||
@@ -158,11 +156,12 @@ export async function onSync(sync: ReadSyncAttributesType): Promise<void> {
|
||||
// timer to the time specified by the read sync if it's earlier than
|
||||
// the previous read time.
|
||||
if (isMessageUnread(message.attributes)) {
|
||||
// TODO DESKTOP-1509: use MessageUpdater.markRead once this is TS
|
||||
message.markRead(readAt, { skipSave: true });
|
||||
message.set(markRead(message.attributes, readAt, { skipSave: true }));
|
||||
|
||||
const updateConversation = async () => {
|
||||
const conversation = message.getConversation();
|
||||
const conversation = window.ConversationController.get(
|
||||
message.get('conversationId')
|
||||
);
|
||||
strictAssert(conversation, `${logId}: conversation not found`);
|
||||
// onReadMessage may result in messages older than this one being
|
||||
// marked read. We want those messages to have the same expire timer
|
||||
@@ -174,7 +173,9 @@ export async function onSync(sync: ReadSyncAttributesType): Promise<void> {
|
||||
|
||||
// only available during initialization
|
||||
if (StartupQueue.isAvailable()) {
|
||||
const conversation = message.getConversation();
|
||||
const conversation = window.ConversationController.get(
|
||||
message.get('conversationId')
|
||||
);
|
||||
strictAssert(
|
||||
conversation,
|
||||
`${logId}: conversation not found (StartupQueue)`
|
||||
|
||||
@@ -7,6 +7,8 @@ import { DataReader } from '../sql/Client';
|
||||
import * as Errors from '../types/errors';
|
||||
import * as log from '../logging/log';
|
||||
import { getMessageIdForLogging } from '../util/idForLogging';
|
||||
import { markViewOnceMessageViewed } from '../services/MessageUpdater';
|
||||
import { MessageModel } from '../models/messages';
|
||||
|
||||
export type ViewOnceOpenSyncAttributesType = {
|
||||
removeFromMessageReceiverCache: () => unknown;
|
||||
@@ -93,12 +95,8 @@ export async function onSync(
|
||||
return;
|
||||
}
|
||||
|
||||
const message = window.MessageCache.__DEPRECATED$register(
|
||||
found.id,
|
||||
found,
|
||||
'ViewOnceOpenSyncs.onSync'
|
||||
);
|
||||
await message.markViewOnceMessageViewed({ fromSync: true });
|
||||
const message = window.MessageCache.register(new MessageModel(found));
|
||||
await markViewOnceMessageViewed(message, { fromSync: true });
|
||||
|
||||
viewOnceSyncs.delete(sync.timestamp);
|
||||
sync.removeFromMessageReceiverCache();
|
||||
|
||||
@@ -19,6 +19,7 @@ import { queueUpdateMessage } from '../util/messageBatcher';
|
||||
import { AttachmentDownloadUrgency } from '../jobs/AttachmentDownloadManager';
|
||||
import { isAciString } from '../util/isAciString';
|
||||
import { DataReader, DataWriter } from '../sql/Client';
|
||||
import { MessageModel } from '../models/messages';
|
||||
|
||||
export const viewSyncTaskSchema = z.object({
|
||||
type: z.literal('ViewSync').readonly(),
|
||||
@@ -114,11 +115,7 @@ export async function onSync(sync: ViewSyncAttributesType): Promise<void> {
|
||||
|
||||
notificationService.removeBy({ messageId: found.id });
|
||||
|
||||
const message = window.MessageCache.__DEPRECATED$register(
|
||||
found.id,
|
||||
found,
|
||||
'ViewSyncs.onSync'
|
||||
);
|
||||
const message = window.MessageCache.register(new MessageModel(found));
|
||||
let didChangeMessage = false;
|
||||
|
||||
if (message.get('readStatus') !== ReadStatus.Viewed) {
|
||||
|
||||
Reference in New Issue
Block a user