diff --git a/ts/services/MessageUpdater.preload.ts b/ts/services/MessageUpdater.preload.ts index 91c4db1800..4742286881 100644 --- a/ts/services/MessageUpdater.preload.ts +++ b/ts/services/MessageUpdater.preload.ts @@ -14,11 +14,19 @@ import * as Errors from '../types/errors.std.js'; import { createLogger } from '../logging/log.std.js'; import { isValidTapToView } from '../util/isValidTapToView.std.js'; import { getMessageIdForLogging } from '../util/idForLogging.preload.js'; +import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp.std.js'; import { eraseMessageContents } from '../util/cleanup.preload.js'; import { getSource, getSourceServiceId } from '../messages/sources.preload.js'; import { isAciString } from '../util/isAciString.std.js'; import { viewOnceOpenJobQueue } from '../jobs/viewOnceOpenJobQueue.preload.js'; import { drop } from '../util/drop.std.js'; +import { isIncoming } from '../messages/helpers.std.js'; +import { + conversationJobQueue, + conversationQueueJobEnum, +} from '../jobs/conversationJobQueue.preload.js'; +import { ReceiptType } from '../types/Receipt.std.js'; +import { isDirectConversation } from '../util/whatTypeOfConversation.dom.js'; const log = createLogger('MessageUpdater'); @@ -99,12 +107,39 @@ export async function markViewOnceMessageViewed( if (!fromSync) { const senderE164 = getSource(message.attributes); const senderAci = getSourceServiceId(message.attributes); - const timestamp = message.get('sent_at'); + const timestamp = getMessageSentTimestamp(message.attributes, { log }); if (senderAci === undefined || !isAciString(senderAci)) { throw new Error('markViewOnceMessageViewed: senderAci is undefined'); } + // Send viewed receipt to sender for incoming view-once messages + if (isIncoming(message.attributes)) { + const conversationId = message.get('conversationId'); + const conversation = window.ConversationController.get(conversationId); + const isDirectConversationValue = conversation + ? isDirectConversation(conversation.attributes) + : true; + + drop( + conversationJobQueue.add({ + type: conversationQueueJobEnum.enum.Receipts, + conversationId, + receiptsType: ReceiptType.Viewed, + receipts: [ + { + messageId: message.id, + conversationId, + senderE164, + senderAci, + timestamp, + isDirectConversation: isDirectConversationValue, + }, + ], + }) + ); + } + if (window.ConversationController.areWePrimaryDevice()) { log.warn( 'markViewOnceMessageViewed: We are primary device; not sending view once open sync' diff --git a/ts/test-mock/helpers.node.ts b/ts/test-mock/helpers.node.ts index d7213593b4..808406b50c 100644 --- a/ts/test-mock/helpers.node.ts +++ b/ts/test-mock/helpers.node.ts @@ -6,7 +6,7 @@ import { type Device, type Group, PrimaryDevice, - type Proto, + Proto, StorageState, } from '@signalapp/mock-server'; import { assert } from 'chai'; @@ -30,6 +30,30 @@ export function bufferToUuid(buffer: Buffer): string { ].join('-'); } +function isProfileKeyUpdate(flags: number | null | undefined): boolean { + if (flags == null) { + return false; + } + // eslint-disable-next-line no-bitwise + return (flags & Proto.DataMessage.Flags.PROFILE_KEY_UPDATE) !== 0; +} + +export async function waitForNonProfileKeyUpdateMessage( + device: PrimaryDevice, + { maxAttempts = 5 }: { maxAttempts?: number } = {} +): Promise>> { + for (let i = 0; i < maxAttempts; i += 1) { + // eslint-disable-next-line no-await-in-loop + const message = await device.waitForMessage(); + if (isProfileKeyUpdate(message.dataMessage.flags)) { + debug('Skipping profile key update'); + continue; + } + return message; + } + throw new Error(`No message with body after ${maxAttempts} attempts`); +} + export async function typeIntoInput( input: Locator, additionalText: string, diff --git a/ts/test-mock/messaging/expire_timer_version_test.node.ts b/ts/test-mock/messaging/expire_timer_version_test.node.ts index fe67da52d2..924ff19a74 100644 --- a/ts/test-mock/messaging/expire_timer_version_test.node.ts +++ b/ts/test-mock/messaging/expire_timer_version_test.node.ts @@ -19,6 +19,7 @@ import { expectSystemMessages, typeIntoInput, waitForEnabledComposer, + waitForNonProfileKeyUpdateMessage, } from '../helpers.node.js'; export const debug = createDebug('mock:test:messaging'); @@ -27,30 +28,6 @@ const IdentifierType = Proto.ManifestRecord.Identifier.Type; const DAY = 24 * 3600; -function isProfileKeyUpdate(flags: number | null | undefined): boolean { - if (flags == null) { - return false; - } - // eslint-disable-next-line no-bitwise - return (flags & Proto.DataMessage.Flags.PROFILE_KEY_UPDATE) !== 0; -} - -async function waitForNonProfileKeyUpdateMessage( - device: PrimaryDevice -): Promise<{ body: string; dataMessage: Proto.IDataMessage }> { - const maxAttempts = 5; - for (let i = 0; i < maxAttempts; i += 1) { - // eslint-disable-next-line no-await-in-loop - const message = await device.waitForMessage(); - if (isProfileKeyUpdate(message.dataMessage.flags)) { - debug('Skipping profile key update'); - continue; - } - return message; - } - throw new Error(`No message with body after ${maxAttempts} attempts`); -} - describe('messaging/expireTimerVersion', function (this: Mocha.Suite) { this.timeout(durations.MINUTE); diff --git a/ts/test-mock/pnp/pni_signature_test.node.ts b/ts/test-mock/pnp/pni_signature_test.node.ts index 484477e3e2..3dd522ac7d 100644 --- a/ts/test-mock/pnp/pni_signature_test.node.ts +++ b/ts/test-mock/pnp/pni_signature_test.node.ts @@ -26,6 +26,7 @@ import { acceptConversation, expectSystemMessages, typeIntoInput, + waitForNonProfileKeyUpdateMessage, waitForEnabledComposer, } from '../helpers.node.js'; @@ -376,7 +377,8 @@ describe('pnp/PNI Signature', function (this: Mocha.Suite) { debug('Wait for a ACI message'); { - const { source, body, serviceIdKind } = await stranger.waitForMessage(); + const { source, body, serviceIdKind } = + await waitForNonProfileKeyUpdateMessage(stranger); assert.strictEqual(source, desktop, 'ACI message has valid source'); assert.strictEqual(body, 'Hello ACI', 'ACI message has valid body');