2023-01-03 19:55:46 +00:00
|
|
|
// Copyright 2021 Signal Messenger, LLC
|
2021-05-07 01:15:25 +00:00
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2023-05-11 16:04:17 +00:00
|
|
|
import { omit, isNumber } from 'lodash';
|
2022-04-29 23:42:47 +00:00
|
|
|
|
2021-10-26 19:15:33 +00:00
|
|
|
import type { ConversationAttributesType } from '../model-types.d';
|
2021-06-17 17:15:10 +00:00
|
|
|
import { hasErrors } from '../state/selectors/message';
|
2021-07-23 22:02:36 +00:00
|
|
|
import { readSyncJobQueue } from '../jobs/readSyncJobQueue';
|
2021-09-23 18:16:09 +00:00
|
|
|
import { notificationService } from '../services/notifications';
|
2022-05-31 23:53:14 +00:00
|
|
|
import { expiringMessagesDeletionService } from '../services/expiringMessagesDeletion';
|
|
|
|
import { tapToViewMessagesDeletionService } from '../services/tapToViewMessagesDeletionService';
|
2022-08-15 21:53:33 +00:00
|
|
|
import { isGroup, isDirectConversation } from './whatTypeOfConversation';
|
2021-09-17 18:27:53 +00:00
|
|
|
import * as log from '../logging/log';
|
2022-04-22 18:35:14 +00:00
|
|
|
import { getConversationIdForLogging } from './idForLogging';
|
2022-12-21 18:41:48 +00:00
|
|
|
import { drop } from './drop';
|
2023-05-11 16:04:17 +00:00
|
|
|
import { isNotNil } from './isNotNil';
|
|
|
|
import { assertDev } from './assert';
|
2022-11-09 01:33:25 +00:00
|
|
|
import { isConversationAccepted } from './isConversationAccepted';
|
2022-04-29 23:42:47 +00:00
|
|
|
import { ReadStatus } from '../messages/MessageReadStatus';
|
2023-02-06 17:24:34 +00:00
|
|
|
import {
|
|
|
|
conversationJobQueue,
|
|
|
|
conversationQueueJobEnum,
|
|
|
|
} from '../jobs/conversationJobQueue';
|
|
|
|
import { ReceiptType } from '../types/Receipt';
|
2023-08-16 20:54:39 +00:00
|
|
|
import type { AciString } from '../types/ServiceId';
|
|
|
|
import { isAciString } from '../types/ServiceId';
|
2021-05-07 01:15:25 +00:00
|
|
|
|
|
|
|
export async function markConversationRead(
|
|
|
|
conversationAttrs: ConversationAttributesType,
|
2021-12-08 19:52:46 +00:00
|
|
|
newestUnreadAt: number,
|
2022-04-22 18:35:14 +00:00
|
|
|
options: {
|
|
|
|
readAt?: number;
|
|
|
|
sendReadReceipts: boolean;
|
|
|
|
newestSentAt?: number;
|
|
|
|
} = {
|
2021-05-07 01:15:25 +00:00
|
|
|
sendReadReceipts: true,
|
|
|
|
}
|
2021-05-10 18:49:13 +00:00
|
|
|
): Promise<boolean> {
|
2021-05-07 01:15:25 +00:00
|
|
|
const { id: conversationId } = conversationAttrs;
|
|
|
|
|
2023-03-27 23:48:57 +00:00
|
|
|
const [unreadMessages, unreadEditedMessages, unreadReactions] =
|
|
|
|
await Promise.all([
|
|
|
|
window.Signal.Data.getUnreadByConversationAndMarkRead({
|
|
|
|
conversationId,
|
|
|
|
newestUnreadAt,
|
|
|
|
readAt: options.readAt,
|
|
|
|
includeStoryReplies: !isGroup(conversationAttrs),
|
|
|
|
}),
|
|
|
|
window.Signal.Data.getUnreadEditedMessagesAndMarkRead({
|
2023-05-16 17:37:12 +00:00
|
|
|
conversationId,
|
2023-03-27 23:48:57 +00:00
|
|
|
newestUnreadAt,
|
|
|
|
}),
|
|
|
|
window.Signal.Data.getUnreadReactionsAndMarkRead({
|
|
|
|
conversationId,
|
|
|
|
newestUnreadAt,
|
|
|
|
}),
|
|
|
|
]);
|
2021-05-07 01:15:25 +00:00
|
|
|
|
2023-05-11 16:04:17 +00:00
|
|
|
const convoId = getConversationIdForLogging(conversationAttrs);
|
|
|
|
const logId = `markConversationRead(${convoId})`;
|
|
|
|
|
|
|
|
log.info(logId, {
|
2022-04-22 18:35:14 +00:00
|
|
|
newestSentAt: options.newestSentAt,
|
2021-12-08 19:52:46 +00:00
|
|
|
newestUnreadAt,
|
2021-05-17 16:52:09 +00:00
|
|
|
unreadMessages: unreadMessages.length,
|
|
|
|
unreadReactions: unreadReactions.length,
|
|
|
|
});
|
|
|
|
|
2023-03-27 23:48:57 +00:00
|
|
|
if (
|
|
|
|
!unreadMessages.length &&
|
|
|
|
!unreadEditedMessages.length &&
|
|
|
|
!unreadReactions.length
|
|
|
|
) {
|
2021-05-10 18:49:13 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-09-23 18:16:09 +00:00
|
|
|
notificationService.removeBy({ conversationId });
|
2021-05-17 16:52:09 +00:00
|
|
|
|
2021-05-07 01:15:25 +00:00
|
|
|
const unreadReactionSyncData = new Map<
|
|
|
|
string,
|
|
|
|
{
|
2021-07-15 23:48:09 +00:00
|
|
|
messageId?: string;
|
2023-08-16 20:54:39 +00:00
|
|
|
senderAci?: AciString;
|
2021-05-07 01:15:25 +00:00
|
|
|
senderE164?: string;
|
|
|
|
timestamp: number;
|
|
|
|
}
|
|
|
|
>();
|
|
|
|
unreadReactions.forEach(reaction => {
|
2023-08-16 20:54:39 +00:00
|
|
|
const targetKey = `${reaction.targetAuthorAci}/${reaction.targetTimestamp}`;
|
2021-05-07 01:15:25 +00:00
|
|
|
if (unreadReactionSyncData.has(targetKey)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
unreadReactionSyncData.set(targetKey, {
|
2021-07-15 23:48:09 +00:00
|
|
|
messageId: reaction.messageId,
|
2021-05-07 01:15:25 +00:00
|
|
|
senderE164: undefined,
|
2023-08-16 20:54:39 +00:00
|
|
|
senderAci: reaction.targetAuthorAci,
|
2021-05-07 01:15:25 +00:00
|
|
|
timestamp: reaction.targetTimestamp,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2023-03-27 23:48:57 +00:00
|
|
|
const allUnreadMessages = [...unreadMessages, ...unreadEditedMessages];
|
|
|
|
|
2023-05-11 16:04:17 +00:00
|
|
|
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) {
|
|
|
|
message.set(omit(messageSyncData, 'originalReadStatus'));
|
|
|
|
}
|
2021-05-07 01:15:25 +00:00
|
|
|
|
2023-05-11 16:04:17 +00:00
|
|
|
const {
|
|
|
|
sent_at: timestamp,
|
|
|
|
source: senderE164,
|
2023-08-16 20:54:39 +00:00
|
|
|
sourceServiceId: senderAci,
|
2023-05-11 16:04:17 +00:00
|
|
|
} = messageSyncData;
|
|
|
|
|
|
|
|
if (!isNumber(timestamp)) {
|
|
|
|
assertDev(
|
|
|
|
false,
|
|
|
|
`${logId}: message sent_at timestamp is not number` +
|
|
|
|
`type=${messageSyncData.type}`
|
|
|
|
);
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
2023-08-16 20:54:39 +00:00
|
|
|
if (!isAciString(senderAci)) {
|
2023-08-17 18:49:45 +00:00
|
|
|
log.warn(
|
2023-08-16 20:54:39 +00:00
|
|
|
`${logId}: message sourceServiceId timestamp is not aci` +
|
|
|
|
`type=${messageSyncData.type}`
|
|
|
|
);
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
2023-05-11 16:04:17 +00:00
|
|
|
return {
|
|
|
|
messageId: messageSyncData.id,
|
|
|
|
conversationId: conversationAttrs.id,
|
|
|
|
originalReadStatus: messageSyncData.originalReadStatus,
|
|
|
|
senderE164,
|
2023-08-16 20:54:39 +00:00
|
|
|
senderAci,
|
2023-05-11 16:04:17 +00:00
|
|
|
senderId: window.ConversationController.lookupOrCreate({
|
|
|
|
e164: senderE164,
|
2023-08-16 20:54:39 +00:00
|
|
|
serviceId: senderAci,
|
2023-05-11 16:04:17 +00:00
|
|
|
reason: 'markConversationRead',
|
|
|
|
})?.id,
|
|
|
|
timestamp,
|
|
|
|
isDirectConversation: isDirectConversation(conversationAttrs),
|
|
|
|
hasErrors: message ? hasErrors(message.attributes) : false,
|
|
|
|
};
|
|
|
|
})
|
|
|
|
.filter(isNotNil);
|
2021-05-07 01:15:25 +00:00
|
|
|
|
2022-04-29 23:42:47 +00:00
|
|
|
// Some messages we're marking read are local notifications with no sender or were just
|
|
|
|
// unseen and not unread.
|
|
|
|
// Also, if a message has errors, we don't want to send anything out about it:
|
2021-05-07 01:15:25 +00:00
|
|
|
// read syncs - let's wait for a client that really understands the message
|
|
|
|
// to mark it read. we'll mark our local error read locally, though.
|
|
|
|
// read receipts - here we can run into infinite loops, where each time the
|
|
|
|
// conversation is viewed, another error message shows up for the contact
|
2021-05-10 18:49:13 +00:00
|
|
|
const unreadMessagesSyncData = allReadMessagesSync.filter(
|
2022-04-29 23:42:47 +00:00
|
|
|
item =>
|
|
|
|
Boolean(item.senderId) &&
|
|
|
|
item.originalReadStatus === ReadStatus.Unread &&
|
|
|
|
!item.hasErrors
|
2021-05-07 01:15:25 +00:00
|
|
|
);
|
|
|
|
|
2021-07-15 23:48:09 +00:00
|
|
|
const readSyncs: Array<{
|
|
|
|
messageId?: string;
|
|
|
|
senderE164?: string;
|
2023-08-16 20:54:39 +00:00
|
|
|
senderAci?: AciString;
|
2021-07-15 23:48:09 +00:00
|
|
|
senderId?: string;
|
|
|
|
timestamp: number;
|
|
|
|
hasErrors?: string;
|
2021-08-31 16:47:15 +00:00
|
|
|
}> = [...unreadMessagesSyncData, ...unreadReactionSyncData.values()];
|
2021-05-07 01:15:25 +00:00
|
|
|
|
|
|
|
if (readSyncs.length && options.sendReadReceipts) {
|
2021-09-17 18:27:53 +00:00
|
|
|
log.info(`Sending ${readSyncs.length} read syncs`);
|
2021-05-07 01:15:25 +00:00
|
|
|
// Because syncReadMessages sends to our other devices, and sendReadReceipts goes
|
|
|
|
// to a contact, we need accessKeys for both.
|
2021-07-15 23:48:09 +00:00
|
|
|
if (window.ConversationController.areWePrimaryDevice()) {
|
2021-09-17 18:27:53 +00:00
|
|
|
log.warn(
|
2021-07-15 23:48:09 +00:00
|
|
|
'markConversationRead: We are primary device; not sending read syncs'
|
|
|
|
);
|
|
|
|
} else {
|
2022-12-21 18:41:48 +00:00
|
|
|
drop(readSyncJobQueue.add({ readSyncs }));
|
2021-07-15 23:48:09 +00:00
|
|
|
}
|
2021-05-07 01:15:25 +00:00
|
|
|
|
2022-11-09 01:33:25 +00:00
|
|
|
if (isConversationAccepted(conversationAttrs)) {
|
2023-02-06 17:24:34 +00:00
|
|
|
await conversationJobQueue.add({
|
|
|
|
type: conversationQueueJobEnum.enum.Receipts,
|
|
|
|
conversationId,
|
|
|
|
receiptsType: ReceiptType.Read,
|
|
|
|
receipts: allReadMessagesSync,
|
|
|
|
});
|
2022-11-09 01:33:25 +00:00
|
|
|
}
|
2021-05-07 01:15:25 +00:00
|
|
|
}
|
|
|
|
|
2022-12-21 18:41:48 +00:00
|
|
|
void expiringMessagesDeletionService.update();
|
|
|
|
void tapToViewMessagesDeletionService.update();
|
2021-07-19 20:45:18 +00:00
|
|
|
|
2021-05-10 18:49:13 +00:00
|
|
|
return true;
|
2021-05-07 01:15:25 +00:00
|
|
|
}
|