Send viewed receipt for view-once opens

This commit is contained in:
yash-signal
2026-02-18 12:28:16 -06:00
committed by GitHub
parent 6aca6a278a
commit c8619bc42b
4 changed files with 65 additions and 27 deletions

View File

@@ -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'

View File

@@ -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<Awaited<ReturnType<PrimaryDevice['waitForMessage']>>> {
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,

View File

@@ -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);

View File

@@ -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');