diff --git a/ts/background.ts b/ts/background.ts index 6721778de7b3..77d758cbff1e 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -23,7 +23,8 @@ import type { } from './model-types.d'; import * as Bytes from './Bytes'; import * as Timers from './Timers'; -import type { WhatIsThis, DeliveryReceiptBatcherItemType } from './window.d'; +import type { WhatIsThis } from './window.d'; +import type { Receipt } from './types/Receipt'; import { getTitleBarVisibility, TitleBarVisibility } from './types/Settings'; import { SocketStatus } from './types/SocketStatus'; import { DEFAULT_CONVERSATION_COLOR } from './types/Colors'; @@ -131,6 +132,7 @@ import { ToastConversationArchived } from './components/ToastConversationArchive import { ToastConversationUnarchived } from './components/ToastConversationUnarchived'; import { showToast } from './util/showToast'; import { startInteractionMode } from './windows/startInteractionMode'; +import { deliveryReceiptsJobQueue } from './jobs/deliveryReceiptsJobQueue'; const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000; @@ -378,59 +380,12 @@ export async function startApp(): Promise { }); window.Whisper.deliveryReceiptQueue.pause(); window.Whisper.deliveryReceiptBatcher = - window.Signal.Util.createBatcher({ + window.Signal.Util.createBatcher({ name: 'Whisper.deliveryReceiptBatcher', wait: 500, maxSize: 100, - processBatch: async items => { - const byConversationId = window._.groupBy(items, item => - window.ConversationController.ensureContactIds({ - e164: item.source, - uuid: item.sourceUuid, - }) - ); - const ids = Object.keys(byConversationId); - - for (let i = 0, max = ids.length; i < max; i += 1) { - const conversationId = ids[i]; - const ourItems = byConversationId[conversationId]; - const timestamps = ourItems.map(item => item.timestamp); - const messageIds = ourItems.map(item => item.messageId); - - const c = window.ConversationController.get(conversationId); - if (!c) { - log.warn( - `deliveryReceiptBatcher: Conversation ${conversationId} does not exist! ` + - `Will not send delivery receipts for timestamps ${timestamps}` - ); - continue; - } - - const senderUuid = c.get('uuid'); - const senderE164 = c.get('e164'); - - c.queueJob('sendDeliveryReceipt', async () => { - try { - const sendOptions = await getSendOptions(c.attributes); - - // eslint-disable-next-line no-await-in-loop - await handleMessageSend( - window.textsecure.messaging.sendDeliveryReceipt({ - senderE164, - senderUuid, - timestamps, - options: sendOptions, - }), - { messageIds, sendType: 'deliveryReceipt' } - ); - } catch (error) { - log.error( - `Failed to send delivery receipt to ${senderE164}/${senderUuid} for timestamps ${timestamps}:`, - error && error.stack ? error.stack : error - ); - } - }); - } + processBatch: async deliveryReceipts => { + await deliveryReceiptsJobQueue.add({ deliveryReceipts }); }, }); diff --git a/ts/jobs/deliveryReceiptsJobQueue.ts b/ts/jobs/deliveryReceiptsJobQueue.ts new file mode 100644 index 000000000000..7f32160f8086 --- /dev/null +++ b/ts/jobs/deliveryReceiptsJobQueue.ts @@ -0,0 +1,45 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { z } from 'zod'; +import type { LoggerType } from '../types/Logging'; +import { exponentialBackoffMaxAttempts } from '../util/exponentialBackoff'; +import { receiptSchema, ReceiptType } from '../types/Receipt'; +import { MAX_RETRY_TIME, runReceiptJob } from './helpers/receiptHelpers'; + +import { JobQueue } from './JobQueue'; +import { jobQueueDatabaseStore } from './JobQueueDatabaseStore'; + +const deliveryReceiptsJobDataSchema = z.object({ + deliveryReceipts: receiptSchema.array(), +}); + +type DeliveryReceiptsJobData = z.infer; + +export class DeliveryReceiptsJobQueue extends JobQueue { + protected parseData(data: unknown): DeliveryReceiptsJobData { + return deliveryReceiptsJobDataSchema.parse(data); + } + + protected async run( + { + data, + timestamp, + }: Readonly<{ data: DeliveryReceiptsJobData; timestamp: number }>, + { attempt, log }: Readonly<{ attempt: number; log: LoggerType }> + ): Promise { + await runReceiptJob({ + attempt, + log, + timestamp, + receipts: data.deliveryReceipts, + type: ReceiptType.Delivery, + }); + } +} + +export const deliveryReceiptsJobQueue = new DeliveryReceiptsJobQueue({ + store: jobQueueDatabaseStore, + queueType: 'delivery receipts', + maxAttempts: exponentialBackoffMaxAttempts(MAX_RETRY_TIME), +}); diff --git a/ts/jobs/helpers/receiptHelpers.ts b/ts/jobs/helpers/receiptHelpers.ts new file mode 100644 index 000000000000..614b403c09c7 --- /dev/null +++ b/ts/jobs/helpers/receiptHelpers.ts @@ -0,0 +1,42 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import * as durations from '../../util/durations'; +import type { LoggerType } from '../../types/Logging'; +import type { Receipt, ReceiptType } from '../../types/Receipt'; +import { sendReceipts } from '../../util/sendReceipts'; +import { commonShouldJobContinue } from './commonShouldJobContinue'; +import { handleCommonJobRequestError } from './handleCommonJobRequestError'; + +export const MAX_RETRY_TIME = durations.DAY; + +export async function runReceiptJob({ + attempt, + log, + timestamp, + receipts, + type, +}: Readonly<{ + attempt: number; + log: LoggerType; + receipts: ReadonlyArray; + timestamp: number; + type: ReceiptType; +}>): Promise { + const timeRemaining = timestamp + MAX_RETRY_TIME - Date.now(); + + const shouldContinue = await commonShouldJobContinue({ + attempt, + log, + timeRemaining, + }); + if (!shouldContinue) { + return; + } + + try { + await sendReceipts({ log, receipts, type }); + } catch (err: unknown) { + await handleCommonJobRequestError({ err, log, timeRemaining }); + } +} diff --git a/ts/jobs/initializeAllJobQueues.ts b/ts/jobs/initializeAllJobQueues.ts index 863fcb5eec7b..cb48d5b76ee2 100644 --- a/ts/jobs/initializeAllJobQueues.ts +++ b/ts/jobs/initializeAllJobQueues.ts @@ -3,8 +3,10 @@ import type { WebAPIType } from '../textsecure/WebAPI'; +import { deliveryReceiptsJobQueue } from './deliveryReceiptsJobQueue'; import { normalMessageSendJobQueue } from './normalMessageSendJobQueue'; import { reactionJobQueue } from './reactionJobQueue'; +import { readReceiptsJobQueue } from './readReceiptsJobQueue'; import { readSyncJobQueue } from './readSyncJobQueue'; import { removeStorageKeyJobQueue } from './removeStorageKeyJobQueue'; import { reportSpamJobQueue } from './reportSpamJobQueue'; @@ -21,8 +23,10 @@ export function initializeAllJobQueues({ }): void { reportSpamJobQueue.initialize({ server }); + deliveryReceiptsJobQueue.streamJobs(); normalMessageSendJobQueue.streamJobs(); reactionJobQueue.streamJobs(); + readReceiptsJobQueue.streamJobs(); readSyncJobQueue.streamJobs(); removeStorageKeyJobQueue.streamJobs(); reportSpamJobQueue.streamJobs(); diff --git a/ts/jobs/readReceiptsJobQueue.ts b/ts/jobs/readReceiptsJobQueue.ts new file mode 100644 index 000000000000..ebda87901893 --- /dev/null +++ b/ts/jobs/readReceiptsJobQueue.ts @@ -0,0 +1,56 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { z } from 'zod'; +import type { LoggerType } from '../types/Logging'; +import { exponentialBackoffMaxAttempts } from '../util/exponentialBackoff'; +import type { StorageInterface } from '../types/Storage.d'; +import type { Receipt } from '../types/Receipt'; +import { receiptSchema, ReceiptType } from '../types/Receipt'; +import { MAX_RETRY_TIME, runReceiptJob } from './helpers/receiptHelpers'; + +import { JobQueue } from './JobQueue'; +import { jobQueueDatabaseStore } from './JobQueueDatabaseStore'; + +const readReceiptsJobDataSchema = z.object({ + readReceipts: receiptSchema.array(), +}); + +type ReadReceiptsJobData = z.infer; + +export class ReadReceiptsJobQueue extends JobQueue { + public async addIfAllowedByUser( + storage: Pick, + readReceipts: Array + ): Promise { + if (storage.get('read-receipt-setting')) { + await this.add({ readReceipts }); + } + } + + protected parseData(data: unknown): ReadReceiptsJobData { + return readReceiptsJobDataSchema.parse(data); + } + + protected async run( + { + data, + timestamp, + }: Readonly<{ data: ReadReceiptsJobData; timestamp: number }>, + { attempt, log }: Readonly<{ attempt: number; log: LoggerType }> + ): Promise { + await runReceiptJob({ + attempt, + log, + timestamp, + receipts: data.readReceipts, + type: ReceiptType.Read, + }); + } +} + +export const readReceiptsJobQueue = new ReadReceiptsJobQueue({ + store: jobQueueDatabaseStore, + queueType: 'read receipts', + maxAttempts: exponentialBackoffMaxAttempts(MAX_RETRY_TIME), +}); diff --git a/ts/jobs/viewedReceiptsJobQueue.ts b/ts/jobs/viewedReceiptsJobQueue.ts index e6b2a532aa11..8b0ccbc9d931 100644 --- a/ts/jobs/viewedReceiptsJobQueue.ts +++ b/ts/jobs/viewedReceiptsJobQueue.ts @@ -2,26 +2,15 @@ // SPDX-License-Identifier: AGPL-3.0-only import { z } from 'zod'; -import * as durations from '../util/durations'; import type { LoggerType } from '../types/Logging'; import { exponentialBackoffMaxAttempts } from '../util/exponentialBackoff'; -import { commonShouldJobContinue } from './helpers/commonShouldJobContinue'; -import { sendViewedReceipt } from '../util/sendViewedReceipt'; +import { receiptSchema, ReceiptType } from '../types/Receipt'; +import { MAX_RETRY_TIME, runReceiptJob } from './helpers/receiptHelpers'; import { JobQueue } from './JobQueue'; import { jobQueueDatabaseStore } from './JobQueueDatabaseStore'; -import { handleCommonJobRequestError } from './helpers/handleCommonJobRequestError'; -const MAX_RETRY_TIME = durations.DAY; - -const viewedReceiptsJobDataSchema = z.object({ - viewedReceipt: z.object({ - messageId: z.string(), - senderE164: z.string().optional(), - senderUuid: z.string().optional(), - timestamp: z.number(), - }), -}); +const viewedReceiptsJobDataSchema = z.object({ viewedReceipt: receiptSchema }); type ViewedReceiptsJobData = z.infer; @@ -37,22 +26,13 @@ export class ViewedReceiptsJobQueue extends JobQueue { }: Readonly<{ data: ViewedReceiptsJobData; timestamp: number }>, { attempt, log }: Readonly<{ attempt: number; log: LoggerType }> ): Promise { - const timeRemaining = timestamp + MAX_RETRY_TIME - Date.now(); - - const shouldContinue = await commonShouldJobContinue({ + await runReceiptJob({ attempt, log, - timeRemaining, + timestamp, + receipts: [data.viewedReceipt], + type: ReceiptType.Viewed, }); - if (!shouldContinue) { - return; - } - - try { - await sendViewedReceipt(data.viewedReceipt, log); - } catch (err: unknown) { - await handleCommonJobRequestError({ err, log, timeRemaining }); - } } } diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts index 53d2bcd85b35..c82491dbf093 100644 --- a/ts/models/conversations.ts +++ b/ts/models/conversations.ts @@ -62,7 +62,6 @@ import { isConversationAccepted } from '../util/isConversationAccepted'; import { markConversationRead } from '../util/markConversationRead'; import { handleMessageSend } from '../util/handleMessageSend'; import { getConversationMembers } from '../util/getConversationMembers'; -import { sendReadReceiptsFor } from '../util/sendReadReceiptsFor'; import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup'; import { ReadStatus } from '../messages/MessageReadStatus'; import { SendStatus } from '../messages/MessageSendState'; @@ -92,6 +91,7 @@ import { getMessagePropStatus, } from '../state/selectors/message'; import { normalMessageSendJobQueue } from '../jobs/normalMessageSendJobQueue'; +import { readReceiptsJobQueue } from '../jobs/readReceiptsJobQueue'; import { Deletes } from '../messageModifiers/Deletes'; import type { ReactionModel } from '../messageModifiers/Reactions'; import { isAnnouncementGroupReady } from '../util/isAnnouncementGroupReady'; @@ -1976,21 +1976,18 @@ export class ConversationModel extends window.Backbone const readMessages = messages.filter( m => !hasErrors(m.attributes) && isIncoming(m.attributes) ); - const receiptSpecs = readMessages.map(m => ({ - messageId: m.id, - senderE164: m.get('source'), - senderUuid: m.get('sourceUuid'), - senderId: window.ConversationController.ensureContactIds({ - e164: m.get('source'), - uuid: m.get('sourceUuid'), - }), - timestamp: m.get('sent_at'), - hasErrors: hasErrors(m.attributes), - })); if (isLocalAction) { // eslint-disable-next-line no-await-in-loop - await sendReadReceiptsFor(this.attributes, receiptSpecs); + await readReceiptsJobQueue.addIfAllowedByUser( + window.storage, + readMessages.map(m => ({ + messageId: m.id, + senderE164: m.get('source'), + senderUuid: m.get('sourceUuid'), + timestamp: m.get('sent_at'), + })) + ); } // eslint-disable-next-line no-await-in-loop diff --git a/ts/models/messages.ts b/ts/models/messages.ts index d84ec5efb1c9..835304e5eeb5 100644 --- a/ts/models/messages.ts +++ b/ts/models/messages.ts @@ -2524,8 +2524,8 @@ export class MessageModel extends window.Backbone.Model { window.Whisper.deliveryReceiptQueue.add(() => { window.Whisper.deliveryReceiptBatcher.add({ messageId, - source, - sourceUuid, + senderE164: source, + senderUuid: sourceUuid, timestamp: this.get('sent_at'), }); }); diff --git a/ts/textsecure/SendMessage.ts b/ts/textsecure/SendMessage.ts index a60601dd8bce..94419d02b458 100644 --- a/ts/textsecure/SendMessage.ts +++ b/ts/textsecure/SendMessage.ts @@ -1584,7 +1584,7 @@ export default class MessageSender { }); } - async sendReadReceipts( + async sendReadReceipt( options: Readonly<{ senderE164?: string; senderUuid?: string; @@ -1598,7 +1598,7 @@ export default class MessageSender { }); } - async sendViewedReceipts( + async sendViewedReceipt( options: Readonly<{ senderE164?: string; senderUuid?: string; diff --git a/ts/types/Receipt.ts b/ts/types/Receipt.ts new file mode 100644 index 000000000000..3bb8cb0f2005 --- /dev/null +++ b/ts/types/Receipt.ts @@ -0,0 +1,19 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { z } from 'zod'; + +export const receiptSchema = z.object({ + messageId: z.string(), + senderE164: z.string().optional(), + senderUuid: z.string().optional(), + timestamp: z.number(), +}); + +export enum ReceiptType { + Delivery = 'deliveryReceipt', + Read = 'readReceipt', + Viewed = 'viewedReceipt', +} + +export type Receipt = z.infer; diff --git a/ts/util/markConversationRead.ts b/ts/util/markConversationRead.ts index 7ca25b46add4..2f4d7d2188d2 100644 --- a/ts/util/markConversationRead.ts +++ b/ts/util/markConversationRead.ts @@ -2,8 +2,8 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { ConversationAttributesType } from '../model-types.d'; -import { sendReadReceiptsFor } from './sendReadReceiptsFor'; import { hasErrors } from '../state/selectors/message'; +import { readReceiptsJobQueue } from '../jobs/readReceiptsJobQueue'; import { readSyncJobQueue } from '../jobs/readSyncJobQueue'; import { notificationService } from '../services/notifications'; import * as log from '../logging/log'; @@ -115,7 +115,10 @@ export async function markConversationRead( readSyncJobQueue.add({ readSyncs }); } - await sendReadReceiptsFor(conversationAttrs, unreadMessagesSyncData); + await readReceiptsJobQueue.addIfAllowedByUser( + window.storage, + allReadMessagesSync + ); } window.Whisper.ExpiringMessagesListener.update(); diff --git a/ts/util/sendReadReceiptsFor.ts b/ts/util/sendReadReceiptsFor.ts deleted file mode 100644 index 4f497369392a..000000000000 --- a/ts/util/sendReadReceiptsFor.ts +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import { chunk, groupBy, map } from 'lodash'; -import type { ConversationAttributesType } from '../model-types.d'; -import { getSendOptions } from './getSendOptions'; -import { handleMessageSend } from './handleMessageSend'; -import { isConversationAccepted } from './isConversationAccepted'; -import * as log from '../logging/log'; - -type ReceiptSpecType = { - messageId: string; - senderE164?: string; - senderUuid?: string; - senderId?: string; - timestamp: number; - hasErrors: boolean; -}; - -const CHUNK_SIZE = 100; - -export async function sendReadReceiptsFor( - conversationAttrs: ConversationAttributesType, - items: Array -): Promise { - // Only send read receipts for accepted conversations - if ( - window.Events.getReadReceiptSetting() && - isConversationAccepted(conversationAttrs) - ) { - log.info(`Sending ${items.length} read receipts`); - const sendOptions = await getSendOptions(conversationAttrs); - const receiptsBySender = groupBy(items, 'senderId'); - - await Promise.all( - map(receiptsBySender, async (receipts, senderId) => { - const conversation = window.ConversationController.get(senderId); - - if (!conversation) { - return; - } - - const batches = chunk(receipts, CHUNK_SIZE); - await Promise.all( - batches.map(batch => { - const timestamps = map(batch, item => item.timestamp); - const messageIds = map(batch, item => item.messageId); - - return handleMessageSend( - window.textsecure.messaging.sendReadReceipts({ - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - senderE164: conversation.get('e164')!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - senderUuid: conversation.get('uuid')!, - timestamps, - options: sendOptions, - }), - { messageIds, sendType: 'readReceipt' } - ); - }) - ); - }) - ); - } -} diff --git a/ts/util/sendReceipts.ts b/ts/util/sendReceipts.ts new file mode 100644 index 000000000000..712a3d722bfb --- /dev/null +++ b/ts/util/sendReceipts.ts @@ -0,0 +1,116 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { chunk } from 'lodash'; +import type { LoggerType } from '../types/Logging'; +import type { Receipt } from '../types/Receipt'; +import { ReceiptType } from '../types/Receipt'; +import { getSendOptions } from './getSendOptions'; +import { handleMessageSend } from './handleMessageSend'; +import { isConversationAccepted } from './isConversationAccepted'; +import { map } from './iterables'; +import { missingCaseError } from './missingCaseError'; + +const CHUNK_SIZE = 100; + +export async function sendReceipts({ + log, + receipts, + type, +}: Readonly<{ + log: LoggerType; + receipts: ReadonlyArray; + type: ReceiptType; +}>): Promise { + let requiresUserSetting: boolean; + let methodName: + | 'sendDeliveryReceipt' + | 'sendReadReceipt' + | 'sendViewedReceipt'; + switch (type) { + case ReceiptType.Delivery: + requiresUserSetting = false; + methodName = 'sendDeliveryReceipt'; + break; + case ReceiptType.Read: + requiresUserSetting = true; + methodName = 'sendReadReceipt'; + break; + case ReceiptType.Viewed: + requiresUserSetting = true; + methodName = 'sendViewedReceipt'; + break; + default: + throw missingCaseError(type); + } + + if (requiresUserSetting && !window.storage.get('read-receipt-setting')) { + log.info('requires user setting. Not sending these receipts'); + return; + } + + const receiptsBySenderId: Map> = receipts.reduce( + (result, receipt) => { + const { senderE164, senderUuid } = receipt; + if (!senderE164 && !senderUuid) { + log.error('no sender E164 or UUID. Skipping this receipt'); + return result; + } + + const senderId = window.ConversationController.ensureContactIds({ + e164: senderE164, + uuid: senderUuid, + }); + if (!senderId) { + throw new Error( + 'no conversation found with that E164/UUID. Cannot send this receipt' + ); + } + + const existingGroup = result.get(senderId); + if (existingGroup) { + existingGroup.push(receipt); + } else { + result.set(senderId, [receipt]); + } + + return result; + }, + new Map() + ); + + await Promise.all( + map(receiptsBySenderId, async ([senderId, receiptsForSender]) => { + const sender = window.ConversationController.get(senderId); + if (!sender) { + throw new Error( + 'despite having a conversation ID, no conversation was found' + ); + } + + if (!isConversationAccepted(sender.attributes)) { + return; + } + + const sendOptions = await getSendOptions(sender.attributes); + + const batches = chunk(receiptsForSender, CHUNK_SIZE); + await Promise.all( + map(batches, async batch => { + const timestamps = batch.map(receipt => receipt.timestamp); + const messageIds = batch.map(receipt => receipt.messageId); + + await handleMessageSend( + window.textsecure.messaging[methodName]({ + senderE164: sender.get('e164'), + senderUuid: sender.get('uuid'), + timestamps, + options: sendOptions, + }), + { messageIds, sendType: type } + ); + }) + ); + }) + ); +} diff --git a/ts/util/sendViewedReceipt.ts b/ts/util/sendViewedReceipt.ts deleted file mode 100644 index 9283e6dfca45..000000000000 --- a/ts/util/sendViewedReceipt.ts +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import type { ConversationAttributesType } from '../model-types.d'; -import type { LoggerType } from '../types/Logging'; -import { getSendOptions } from './getSendOptions'; -import { handleMessageSend } from './handleMessageSend'; -import { isConversationAccepted } from './isConversationAccepted'; - -export async function sendViewedReceipt( - { - messageId, - senderE164, - senderUuid, - timestamp, - }: Readonly<{ - messageId: string; - senderE164?: string; - senderUuid?: string; - timestamp: number; - }>, - log: LoggerType -): Promise { - if (!window.storage.get('read-receipt-setting')) { - return; - } - - // We introduced a bug in `75f0cd50beff73885ebae92e4ac977de9f56d6c9` where we'd enqueue - // jobs that had no sender information. These jobs cannot possibly succeed. This - // removes them from the queue to avoid constantly retrying something. - // - // We should be able to safely remove this check after the fix has been present for - // awhile. Probably ~40 days from when this is first deployed (30 days to unlink + 10 - // days of buffer). - if (!senderE164 && !senderUuid) { - log.error( - 'sendViewedReceipt: no sender E164 or UUID. Cannot possibly complete this job. Giving up' - ); - return; - } - - const conversationId = window.ConversationController.ensureContactIds({ - e164: senderE164, - uuid: senderUuid, - }); - if (!conversationId) { - throw new Error( - 'sendViewedReceipt: no conversation found with that E164/UUID' - ); - } - - const conversation = window.ConversationController.get(conversationId); - if (!conversation) { - throw new Error( - 'sendViewedReceipt: no conversation found with that conversation ID, even though we found the ID with E164/UUID?' - ); - } - - const conversationAttrs: ConversationAttributesType = conversation.attributes; - if (!isConversationAccepted(conversationAttrs)) { - return; - } - - await handleMessageSend( - window.textsecure.messaging.sendViewedReceipts({ - senderE164, - senderUuid, - timestamps: [timestamp], - options: await getSendOptions(conversationAttrs), - }), - { messageIds: [messageId], sendType: 'viewedReceipt' } - ); -} diff --git a/ts/window.d.ts b/ts/window.d.ts index 59e771bee8bb..7d75d6205481 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -35,6 +35,7 @@ import { getEnvironment } from './environment'; import * as zkgroup from './util/zkgroup'; import { LocalizerType, BodyRangesType, BodyRangeType } from './types/Util'; import * as EmbeddedContact from './types/EmbeddedContact'; +import type { Receipt } from './types/Receipt'; import * as Errors from './types/errors'; import { ConversationController } from './ConversationController'; import { ReduxActions } from './state/types'; @@ -523,13 +524,6 @@ export class CanvasVideoRenderer { constructor(canvas: Ref); } -export type DeliveryReceiptBatcherItemType = { - messageId: string; - source?: string; - sourceUuid?: string; - timestamp: number; -}; - export class AnyViewClass extends window.Backbone.View { public headerTitle?: string; static show(view: typeof AnyViewClass, element: Element): void; @@ -551,7 +545,7 @@ export type WhisperType = { WallClockListener: WhatIsThis; deliveryReceiptQueue: PQueue; - deliveryReceiptBatcher: BatcherType; + deliveryReceiptBatcher: BatcherType; events: Backbone.Events; activeConfirmationView: WhatIsThis;