signal-desktop/ts/services/MessageUpdater.ts
2025-01-10 08:18:32 +10:00

127 lines
3.8 KiB
TypeScript

// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { MessageAttributesType } from '../model-types.d';
import type { MessageModel } from '../models/messages';
import { ReadStatus, maxReadStatus } from '../messages/MessageReadStatus';
import { notificationService } from './notifications';
import { SeenStatus } from '../MessageSeenStatus';
import { queueUpdateMessage } from '../util/messageBatcher';
import * as Errors from '../types/errors';
import * as log from '../logging/log';
import { isValidTapToView } from '../util/isValidTapToView';
import { getMessageIdForLogging } from '../util/idForLogging';
import { eraseMessageContents } from '../util/cleanup';
import { getSource, getSourceServiceId } from '../messages/helpers';
import { isAciString } from '../util/isAciString';
import { viewOnceOpenJobQueue } from '../jobs/viewOnceOpenJobQueue';
function markReadOrViewed(
messageAttrs: Readonly<MessageAttributesType>,
readStatus: ReadStatus.Read | ReadStatus.Viewed,
timestamp: undefined | number,
skipSave: boolean
): MessageAttributesType {
const oldReadStatus = messageAttrs.readStatus ?? ReadStatus.Read;
const newReadStatus = maxReadStatus(oldReadStatus, readStatus);
const nextMessageAttributes: MessageAttributesType = {
...messageAttrs,
readAt: timestamp,
readStatus: newReadStatus,
seenStatus: SeenStatus.Seen,
};
const { id: messageId, expireTimer, expirationStartTimestamp } = messageAttrs;
if (expireTimer && !expirationStartTimestamp) {
nextMessageAttributes.expirationStartTimestamp = Math.min(
Date.now(),
timestamp || Date.now()
);
}
notificationService.removeBy({ messageId });
if (!skipSave) {
queueUpdateMessage(nextMessageAttributes);
}
return nextMessageAttributes;
}
export const markRead = (
messageAttrs: Readonly<MessageAttributesType>,
readAt?: number,
{ skipSave = false } = {}
): MessageAttributesType =>
markReadOrViewed(messageAttrs, ReadStatus.Read, readAt, skipSave);
export const markViewed = (
messageAttrs: Readonly<MessageAttributesType>,
viewedAt?: number,
{ skipSave = false } = {}
): MessageAttributesType =>
markReadOrViewed(messageAttrs, ReadStatus.Viewed, viewedAt, skipSave);
export async function markViewOnceMessageViewed(
message: MessageModel,
options?: {
fromSync?: boolean;
}
): Promise<void> {
const { fromSync } = options || {};
if (!isValidTapToView(message.attributes)) {
log.warn(
`markViewOnceMessageViewed: Message ${getMessageIdForLogging(message.attributes)} is not a valid tap to view message!`
);
return;
}
if (message.attributes.isErased) {
log.warn(
`markViewOnceMessageViewed: Message ${getMessageIdForLogging(message.attributes)} is already erased!`
);
return;
}
if (message.get('readStatus') !== ReadStatus.Viewed) {
message.set(markViewed(message.attributes));
}
await eraseMessageContents(message);
if (!fromSync) {
const senderE164 = getSource(message.attributes);
const senderAci = getSourceServiceId(message.attributes);
const timestamp = message.get('sent_at');
if (senderAci === undefined || !isAciString(senderAci)) {
throw new Error('markViewOnceMessageViewed: senderAci is undefined');
}
if (window.ConversationController.areWePrimaryDevice()) {
log.warn(
'markViewOnceMessageViewed: We are primary device; not sending view once open sync'
);
return;
}
try {
await viewOnceOpenJobQueue.add({
viewOnceOpens: [
{
senderE164,
senderAci,
timestamp,
},
],
});
} catch (error) {
log.error(
'markViewOnceMessageViewed: Failed to queue view once open sync',
Errors.toLogFormat(error)
);
}
}
}