// 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 { createLogger } 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'; const log = createLogger('MessageUpdater'); function markReadOrViewed( messageAttrs: Readonly, 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, readAt?: number, { skipSave = false } = {} ): MessageAttributesType => markReadOrViewed(messageAttrs, ReadStatus.Read, readAt, skipSave); export const markViewed = ( messageAttrs: Readonly, viewedAt?: number, { skipSave = false } = {} ): MessageAttributesType => markReadOrViewed(messageAttrs, ReadStatus.Viewed, viewedAt, skipSave); export async function markViewOnceMessageViewed( message: MessageModel, options?: { fromSync?: boolean; } ): Promise { const { fromSync } = options || {}; if (message.attributes.isErased) { log.warn( `markViewOnceMessageViewed: Message ${getMessageIdForLogging(message.attributes)} is already erased!` ); return; } if (!isValidTapToView(message.attributes)) { log.warn( `markViewOnceMessageViewed: Message ${getMessageIdForLogging(message.attributes)} is not a valid tap to view message!` ); } 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) ); } } }