diff --git a/ts/RemoteConfig.dom.ts b/ts/RemoteConfig.dom.ts index 1280b7a080..7fc52f25c9 100644 --- a/ts/RemoteConfig.dom.ts +++ b/ts/RemoteConfig.dom.ts @@ -50,6 +50,8 @@ const SemverKeys = [ 'desktop.keyTransparency.prod', 'desktop.binaryServiceId.beta', 'desktop.binaryServiceId.prod', + 'desktop.pollSend1to1.beta', + 'desktop.pollSend1to1.prod', ] as const; export type SemverKeyType = ArrayValues; diff --git a/ts/components/CompositionArea.dom.stories.tsx b/ts/components/CompositionArea.dom.stories.tsx index 374e8e5185..424c382900 100644 --- a/ts/components/CompositionArea.dom.stories.tsx +++ b/ts/components/CompositionArea.dom.stories.tsx @@ -79,6 +79,7 @@ export default { i18n, isDisabled: false, isFormattingEnabled: true, + isPollSend1to1Enabled: true, messageCompositionId: '456', sendEditedMessage: action('sendEditedMessage'), sendMultiMediaMessage: action('sendMultiMediaMessage'), diff --git a/ts/components/CompositionArea.dom.tsx b/ts/components/CompositionArea.dom.tsx index 988a8768c3..7f18c52c85 100644 --- a/ts/components/CompositionArea.dom.tsx +++ b/ts/components/CompositionArea.dom.tsx @@ -125,6 +125,7 @@ export type OwnProps = Readonly<{ isFormattingEnabled: boolean; isGroupV1AndDisabled: boolean | null; isMissingMandatoryProfileSharing: boolean | null; + isPollSend1to1Enabled: boolean; isSignalConversation: boolean | null; isActive: boolean; lastEditableMessageId: string | null; @@ -246,6 +247,7 @@ export const CompositionArea = memo(function CompositionArea({ i18n, imageToBlurHash, isDisabled, + isPollSend1to1Enabled, isSignalConversation, isMuted, isActive, @@ -776,7 +778,7 @@ export const CompositionArea = memo(function CompositionArea({ {i18n('icu:CompositionArea__AttachMenu__File')} - {conversationType === 'group' && ( + {(conversationType === 'group' || isPollSend1to1Enabled) && ( { + const profileKey = conversation.get('profileSharing') + ? await ourProfileKeyService.get() + : undefined; + + const sendOptions = await getSendOptions(conversation.attributes); + const timestamp = Date.now(); + const expireTimer = conversation.get('expireTimer'); + + try { + const isGroupV2Conversation = isGroupV2(conversation.attributes); + const shouldSendSyncOnly = + (isDirectConversation(conversation.attributes) && + isMe(conversation.attributes)) || + (isGroupV2Conversation && recipients.length === 0); + + if (shouldSendSyncOnly) { jobLog.info( - `${logId}: Sending poll terminate for poll timestamp ${targetTimestamp}` + `${logId}: Sending poll terminate for poll timestamp ${targetTimestamp} (sync only)` ); - const profileKey = conversation.get('profileSharing') - ? await ourProfileKeyService.get() + const groupV2Info = isGroupV2Conversation + ? conversation.getGroupV2Info({ + members: recipients, + }) : undefined; - - const sendOptions = await getSendOptions(conversation.attributes); - - try { - if (isGroupV2(conversation.attributes) && !isNumber(revision)) { - jobLog.error('No revision provided, but conversation is GroupV2'); - } - - const groupV2Info = conversation.getGroupV2Info({ - members: recipients, - }); - if (groupV2Info && isNumber(revision)) { - groupV2Info.revision = revision; - } - - strictAssert(groupV2Info, 'could not get group info from conversation'); - - const timestamp = Date.now(); - const expireTimer = conversation.get('expireTimer'); - - const contentMessage = await messaging.getPollTerminateContentMessage({ - groupV2: groupV2Info, - timestamp, - profileKey, - expireTimer, - expireTimerVersion: conversation.getExpireTimerVersion(), - pollTerminate: { - targetTimestamp, - }, - }); - - if (abortSignal?.aborted) { - throw new Error('sendPollTerminate was aborted'); - } - - await wrapWithSyncMessageSend({ - conversation, - logId, - messageIds: [pollMessageId], - send: async () => - sendContentMessageToGroup({ - contentHint: ContentHint.Resendable, - contentMessage, - messageId: pollMessageId, - recipients, - sendOptions, - sendTarget: conversation.toSenderKeyTarget(), - sendType: 'pollTerminate', - timestamp, - urgent: true, - }), - sendType: 'pollTerminate', - timestamp, - expirationStartTimestamp: null, - }); - - await markTerminateSuccess(pollMessage, jobLog); - } catch (error: unknown) { - const errors = maybeExpandErrors(error); - await handleMultipleSendErrors({ - errors, - isFinalAttempt, - log: jobLog, - markFailed: () => markTerminateFailed(pollMessage, jobLog), - timeRemaining, - toThrow: error, - }); + if (isGroupV2Conversation) { + strictAssert( + groupV2Info, + `${logId}: missing groupV2 info for sync-only poll terminate` + ); } + + const dataMessage = messaging.createDataMessageProtoForPollTerminate({ + groupV2: groupV2Info, + timestamp, + profileKey, + expireTimer, + expireTimerVersion: conversation.getExpireTimerVersion(), + pollTerminate: { + targetTimestamp, + }, + }); + + await handleMessageSend( + messaging.sendSyncMessage({ + encodedDataMessage: Proto.DataMessage.encode(dataMessage).finish(), + destinationE164: conversation.get('e164'), + destinationServiceId: conversation.getServiceId(), + expirationStartTimestamp: null, + options: sendOptions, + timestamp, + urgent: false, + }), + { messageIds: [pollMessageId], sendType: 'pollTerminate' } + ); + + await markTerminateSuccess(pollMessage, jobLog); + } else if (isDirectConversation(conversation.attributes)) { + const [ok, refusal] = shouldSendToDirectConversation(conversation); + if (!ok) { + jobLog.info(`${logId}: ${refusal.logLine}`); + return; + } + + const recipientServiceId = recipients[0]; + + jobLog.info( + `${logId}: Sending direct poll terminate for poll timestamp ${targetTimestamp}` + ); + + const contentMessage = await messaging.getPollTerminateContentMessage({ + groupV2: undefined, + timestamp, + profileKey, + expireTimer, + expireTimerVersion: conversation.getExpireTimerVersion(), + pollTerminate: { + targetTimestamp, + }, + }); + + addPniSignatureMessageToProto({ + conversation, + proto: contentMessage, + reason: `sendPollTerminate(${timestamp})`, + }); + + await wrapWithSyncMessageSend({ + conversation, + logId, + messageIds: [pollMessageId], + send: async () => + messaging.sendMessageProtoAndWait({ + timestamp, + recipients: [recipientServiceId], + proto: contentMessage, + contentHint: ContentHint.Resendable, + groupId: undefined, + options: sendOptions, + urgent: true, + }), + sendType: 'pollTerminate', + timestamp, + expirationStartTimestamp: null, + }); + + await markTerminateSuccess(pollMessage, jobLog); + } else { + strictAssert( + isGroupV2Conversation, + `${logId}: expected GroupV2 conversation when not direct` + ); + + await conversation.queueJob( + 'conversationQueue/sendPollTerminate', + async abortSignal => { + jobLog.info( + `${logId}: Sending group poll terminate for poll timestamp ${targetTimestamp}` + ); + + if (!isNumber(revision)) { + jobLog.error('No revision provided, but conversation is GroupV2'); + } + + const groupV2Info = conversation.getGroupV2Info({ + members: recipients, + }); + if (groupV2Info && isNumber(revision)) { + groupV2Info.revision = revision; + } + + strictAssert( + groupV2Info, + 'could not get group info from conversation' + ); + + const contentMessage = await messaging.getPollTerminateContentMessage( + { + groupV2: groupV2Info, + timestamp, + profileKey, + expireTimer, + expireTimerVersion: conversation.getExpireTimerVersion(), + pollTerminate: { + targetTimestamp, + }, + } + ); + + if (abortSignal?.aborted) { + throw new Error('sendPollTerminate was aborted'); + } + + await wrapWithSyncMessageSend({ + conversation, + logId, + messageIds: [pollMessageId], + send: async () => + sendContentMessageToGroup({ + contentHint: ContentHint.Resendable, + contentMessage, + messageId: pollMessageId, + recipients, + sendOptions, + sendTarget: conversation.toSenderKeyTarget(), + sendType: 'pollTerminate', + timestamp, + urgent: true, + }), + sendType: 'pollTerminate', + timestamp, + expirationStartTimestamp: null, + }); + + await markTerminateSuccess(pollMessage, jobLog); + } + ); } - ); + } catch (error: unknown) { + const errors = maybeExpandErrors(error); + await handleMultipleSendErrors({ + errors, + isFinalAttempt, + log: jobLog, + markFailed: () => markTerminateFailed(pollMessage, jobLog), + timeRemaining, + toThrow: error, + }); + } } async function markTerminateSuccess( diff --git a/ts/jobs/helpers/sendPollVote.preload.ts b/ts/jobs/helpers/sendPollVote.preload.ts index c5048d1f4e..b920530b2b 100644 --- a/ts/jobs/helpers/sendPollVote.preload.ts +++ b/ts/jobs/helpers/sendPollVote.preload.ts @@ -28,10 +28,9 @@ import * as pollVoteUtil from '../../polls/util.std.js'; import { strictAssert } from '../../util/assert.std.js'; import { getSendRecipientLists } from './getSendRecipientLists.dom.js'; import { isDirectConversation } from '../../util/whatTypeOfConversation.dom.js'; -import { isConversationAccepted } from '../../util/isConversationAccepted.preload.js'; -import { isConversationUnregistered } from '../../util/isConversationUnregistered.dom.js'; import type { CallbackResultType } from '../../textsecure/Types.d.ts'; import { addPniSignatureMessageToProto } from '../../textsecure/SendMessage.preload.js'; +import { shouldSendToDirectConversation } from './shouldSendToConversation.preload.js'; export async function sendPollVote( conversation: ConversationModel, @@ -214,22 +213,9 @@ export async function sendPollVote( let promise: Promise; if (isDirectConversation(conversation.attributes)) { - if (!isConversationAccepted(conversation.attributes)) { - jobLog.info( - `conversation ${conversation.idForLogging()} is not accepted; refusing to send` - ); - return; - } - if (isConversationUnregistered(conversation.attributes)) { - jobLog.info( - `conversation ${conversation.idForLogging()} is unregistered; refusing to send` - ); - return; - } - if (conversation.isBlocked()) { - jobLog.info( - `conversation ${conversation.idForLogging()} is blocked; refusing to send` - ); + const [ok, refusal] = shouldSendToDirectConversation(conversation); + if (!ok) { + jobLog.info(refusal.logLine); return; } diff --git a/ts/jobs/helpers/sendReaction.preload.ts b/ts/jobs/helpers/sendReaction.preload.ts index fa70237185..d210250d36 100644 --- a/ts/jobs/helpers/sendReaction.preload.ts +++ b/ts/jobs/helpers/sendReaction.preload.ts @@ -35,13 +35,12 @@ import type { ConversationQueueJobBundle, ReactionJobData, } from '../conversationJobQueue.preload.js'; -import { isConversationAccepted } from '../../util/isConversationAccepted.preload.js'; -import { isConversationUnregistered } from '../../util/isConversationUnregistered.dom.js'; import { sendToGroup } from '../../util/sendToGroup.preload.js'; import { hydrateStoryContext } from '../../util/hydrateStoryContext.preload.js'; import { send, sendSyncMessageOnly } from '../../messages/send.preload.js'; import { itemStorage } from '../../textsecure/Storage.preload.js'; import { getSendRecipientLists } from './getSendRecipientLists.dom.js'; +import { shouldSendToDirectConversation } from './shouldSendToConversation.preload.js'; const { isNumber } = lodash; @@ -223,24 +222,9 @@ export async function sendReaction( let promise: Promise; if (isDirectConversation(conversation.attributes)) { - if (!isConversationAccepted(conversation.attributes)) { - log.info( - `conversation ${conversation.idForLogging()} is not accepted; refusing to send` - ); - markReactionFailed(message, pendingReaction); - return; - } - if (isConversationUnregistered(conversation.attributes)) { - log.info( - `conversation ${conversation.idForLogging()} is unregistered; refusing to send` - ); - markReactionFailed(message, pendingReaction); - return; - } - if (conversation.isBlocked()) { - log.info( - `conversation ${conversation.idForLogging()} is blocked; refusing to send` - ); + const [ok, refusal] = shouldSendToDirectConversation(conversation); + if (!ok) { + log.info(refusal.logLine); markReactionFailed(message, pendingReaction); return; } diff --git a/ts/jobs/helpers/shouldSendToConversation.preload.ts b/ts/jobs/helpers/shouldSendToConversation.preload.ts index 5d83497779..cffef3b091 100644 --- a/ts/jobs/helpers/shouldSendToConversation.preload.ts +++ b/ts/jobs/helpers/shouldSendToConversation.preload.ts @@ -5,9 +5,15 @@ import type { ConversationModel } from '../../models/conversations.preload.js'; import type { LoggerType } from '../../types/Logging.std.js'; import { getRecipients } from '../../util/getRecipients.dom.js'; import { isConversationAccepted } from '../../util/isConversationAccepted.preload.js'; +import { isConversationUnregistered } from '../../util/isConversationUnregistered.dom.js'; import { isSignalConversation } from '../../util/isSignalConversation.dom.js'; import { getUntrustedConversationServiceIds } from './getUntrustedConversationServiceIds.dom.js'; +type ConversationForDirectSendType = Pick< + ConversationModel, + 'attributes' | 'isBlocked' | 'idForLogging' +>; + export function shouldSendToConversation( conversation: ConversationModel, log: LoggerType @@ -45,3 +51,48 @@ export function shouldSendToConversation( return true; } + +export type DirectConversationSendRefusalType = Readonly<{ + logLine: string; + error: Error; +}>; + +export type ShouldSendToDirectConversationResult = + | readonly [ok: true, refusal: undefined] + | readonly [ok: false, refusal: DirectConversationSendRefusalType]; + +export function shouldSendToDirectConversation( + conversation: ConversationForDirectSendType +): ShouldSendToDirectConversationResult { + if (!isConversationAccepted(conversation.attributes)) { + return [ + false, + { + logLine: `conversation ${conversation.idForLogging()} is not accepted; refusing to send`, + error: new Error('Message request was not accepted'), + }, + ]; + } + + if (isConversationUnregistered(conversation.attributes)) { + return [ + false, + { + logLine: `conversation ${conversation.idForLogging()} is unregistered; refusing to send`, + error: new Error('Contact no longer has a Signal account'), + }, + ]; + } + + if (conversation.isBlocked()) { + return [ + false, + { + logLine: `conversation ${conversation.idForLogging()} is blocked; refusing to send`, + error: new Error('Contact is blocked'), + }, + ]; + } + + return [true, undefined]; +} diff --git a/ts/jobs/singleProtoJobQueue.preload.ts b/ts/jobs/singleProtoJobQueue.preload.ts index b37b9f841d..b9a22a86a9 100644 --- a/ts/jobs/singleProtoJobQueue.preload.ts +++ b/ts/jobs/singleProtoJobQueue.preload.ts @@ -25,9 +25,8 @@ import { handleMultipleSendErrors, maybeExpandErrors, } from './helpers/handleMultipleSendErrors.std.js'; -import { isConversationUnregistered } from '../util/isConversationUnregistered.dom.js'; -import { isConversationAccepted } from '../util/isConversationAccepted.preload.js'; import { parseUnknown } from '../util/schemas.std.js'; +import { shouldSendToDirectConversation } from './helpers/shouldSendToConversation.preload.js'; const { isBoolean } = lodash; @@ -90,22 +89,9 @@ export class SingleProtoJobQueue extends JobQueue { throw new Error(`Failed to get conversation for serviceId ${serviceId}`); } - if (!isConversationAccepted(conversation.attributes)) { - log.info( - `conversation ${conversation.idForLogging()} is not accepted; refusing to send` - ); - return undefined; - } - if (isConversationUnregistered(conversation.attributes)) { - log.info( - `conversation ${conversation.idForLogging()} is unregistered; refusing to send` - ); - return undefined; - } - if (conversation.isBlocked()) { - log.info( - `conversation ${conversation.idForLogging()} is blocked; refusing to send` - ); + const [ok, refusal] = shouldSendToDirectConversation(conversation); + if (!ok) { + log.info(refusal.logLine); return undefined; } diff --git a/ts/polls/enqueuePollTerminateForSend.preload.ts b/ts/polls/enqueuePollTerminateForSend.preload.ts index caeff82855..11e5a96336 100644 --- a/ts/polls/enqueuePollTerminateForSend.preload.ts +++ b/ts/polls/enqueuePollTerminateForSend.preload.ts @@ -12,7 +12,7 @@ import { PollSource, type PollTerminateAttributesType, } from '../messageModifiers/Polls.preload.js'; -import { isGroup } from '../util/whatTypeOfConversation.dom.js'; +import { isGroupV1 } from '../util/whatTypeOfConversation.dom.js'; import { strictAssert } from '../util/assert.std.js'; import { createLogger } from '../logging/log.std.js'; @@ -33,10 +33,12 @@ export async function enqueuePollTerminateForSend({ conversation, 'enqueuePollTerminateForSend: No conversation extracted from target message' ); - strictAssert( - isGroup(conversation.attributes), - 'enqueuePollTerminateForSend: conversation must be a group' - ); + if (isGroupV1(conversation.attributes)) { + log.info( + 'enqueuePollTerminateForSend: refusing to send poll terminate to GroupV1' + ); + return; + } const ourId = window.ConversationController.getOurConversationIdOrThrow(); const timestamp = Date.now(); diff --git a/ts/state/smart/CompositionArea.preload.tsx b/ts/state/smart/CompositionArea.preload.tsx index b85ac7b46b..46381852a8 100644 --- a/ts/state/smart/CompositionArea.preload.tsx +++ b/ts/state/smart/CompositionArea.preload.tsx @@ -36,6 +36,7 @@ import { getSharedGroupNames } from '../../util/sharedGroupNames.dom.js'; import { getDefaultConversationColor, getEmojiSkinToneDefault, + getItems, getTextFormattingEnabled, } from '../selectors/items.dom.js'; import { canForward, getPropsForQuote } from '../selectors/message.preload.js'; @@ -44,6 +45,7 @@ import { getPlatform, getTheme, getUserConversationId, + getVersion, } from '../selectors/user.std.js'; import { SmartCompositionRecording } from './CompositionRecording.preload.js'; import type { SmartCompositionRecordingDraftProps } from './CompositionRecordingDraft.preload.js'; @@ -60,6 +62,7 @@ import { isDirectConversation } from '../../util/whatTypeOfConversation.dom.js'; import { isConversationMuted } from '../../util/isConversationMuted.std.js'; import { itemStorage } from '../../textsecure/Storage.preload.js'; import { useNavActions } from '../ducks/nav.std.js'; +import { isFeaturedEnabledSelector } from '../../util/isFeatureEnabled.dom.js'; function renderSmartCompositionRecording() { return ; @@ -86,6 +89,8 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({ const selectedMessageIds = useSelector(getSelectedMessageIds); const messageLookup = useSelector(getMessages); const isFormattingEnabled = useSelector(getTextFormattingEnabled); + const items = useSelector(getItems); + const version = useSelector(getVersion); const lastEditableMessageId = useSelector(getLastEditableMessageId); const platform = useSelector(getPlatform); const shouldHidePopovers = useSelector(getHasPanelOpen); @@ -243,6 +248,12 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({ i18n={i18n} isDisabled={isDisabled} isFormattingEnabled={isFormattingEnabled} + isPollSend1to1Enabled={isFeaturedEnabledSelector({ + betaKey: 'desktop.pollSend1to1.beta', + prodKey: 'desktop.pollSend1to1.prod', + currentVersion: version, + remoteConfig: items.remoteConfig, + })} isActive={isActive} lastEditableMessageId={lastEditableMessageId ?? null} messageCompositionId={messageCompositionId} diff --git a/ts/textsecure/SendMessage.preload.ts b/ts/textsecure/SendMessage.preload.ts index 724d16c34f..013827649f 100644 --- a/ts/textsecure/SendMessage.preload.ts +++ b/ts/textsecure/SendMessage.preload.ts @@ -255,9 +255,12 @@ export type PollVoteBuildOptions = Required< >; export type PollTerminateBuildOptions = Required< - Pick + Pick > & - Pick; + Pick< + MessageOptionsType, + 'groupV2' | 'profileKey' | 'expireTimer' | 'expireTimerVersion' + >; class Message { attachments: ReadonlyArray; @@ -943,10 +946,12 @@ export class MessageSender { const dataMessage = new Proto.DataMessage(); dataMessage.timestamp = Long.fromNumber(timestamp); - const groupContext = new Proto.GroupContextV2(); - groupContext.masterKey = groupV2.masterKey; - groupContext.revision = groupV2.revision; - dataMessage.groupV2 = groupContext; + if (groupV2) { + const groupContext = new Proto.GroupContextV2(); + groupContext.masterKey = groupV2.masterKey; + groupContext.revision = groupV2.revision; + dataMessage.groupV2 = groupContext; + } if (typeof expireTimer !== 'undefined') { dataMessage.expireTimer = expireTimer; diff --git a/ts/types/Polls.dom.ts b/ts/types/Polls.dom.ts index 514e496831..285e4894d2 100644 --- a/ts/types/Polls.dom.ts +++ b/ts/types/Polls.dom.ts @@ -10,6 +10,7 @@ import { } from '../environment.std.js'; import * as RemoteConfig from '../RemoteConfig.dom.js'; import { isAlpha, isBeta, isProduction } from '../util/version.std.js'; +import { isFeaturedEnabledNoRedux } from '../util/isFeatureEnabled.dom.js'; import type { SendStateByConversationId } from '../messages/MessageSendState.std.js'; import { aciSchema } from './ServiceId.std.js'; import { MAX_MESSAGE_BODY_BYTE_LENGTH } from '../util/longAttachment.std.js'; @@ -173,3 +174,10 @@ export function isPollSendEnabled(): boolean { return false; } + +export function isPollSend1to1Enabled(): boolean { + return isFeaturedEnabledNoRedux({ + betaKey: 'desktop.pollSend1to1.beta', + prodKey: 'desktop.pollSend1to1.prod', + }); +} diff --git a/ts/util/enqueuePollCreateForSend.dom.ts b/ts/util/enqueuePollCreateForSend.dom.ts index 6b3a5db973..e6d64972c2 100644 --- a/ts/util/enqueuePollCreateForSend.dom.ts +++ b/ts/util/enqueuePollCreateForSend.dom.ts @@ -2,8 +2,12 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { ConversationModel } from '../models/conversations.preload.js'; -import { isGroupV2 } from './whatTypeOfConversation.dom.js'; -import { isPollSendEnabled, type PollCreateType } from '../types/Polls.dom.js'; +import { isDirectConversation } from './whatTypeOfConversation.dom.js'; +import { + isPollSend1to1Enabled, + isPollSendEnabled, + type PollCreateType, +} from '../types/Polls.dom.js'; export async function enqueuePollCreateForSend( conversation: ConversationModel, @@ -13,9 +17,12 @@ export async function enqueuePollCreateForSend( throw new Error('enqueuePollCreateForSend: poll sending is not enabled'); } - if (!isGroupV2(conversation.attributes)) { + if ( + isDirectConversation(conversation.attributes) && + !isPollSend1to1Enabled() + ) { throw new Error( - 'enqueuePollCreateForSend: polls are group-only. Conversation is not GroupV2.' + 'enqueuePollCreateForSend: 1:1 poll sending is not enabled' ); } diff --git a/ts/util/sendReceipts.preload.ts b/ts/util/sendReceipts.preload.ts index cfb8e2500e..4b37933358 100644 --- a/ts/util/sendReceipts.preload.ts +++ b/ts/util/sendReceipts.preload.ts @@ -7,14 +7,13 @@ import type { Receipt } from '../types/Receipt.std.js'; import { ReceiptType } from '../types/Receipt.std.js'; import { getSendOptions } from './getSendOptions.preload.js'; import { handleMessageSend } from './handleMessageSend.preload.js'; -import { isConversationAccepted } from './isConversationAccepted.preload.js'; -import { isConversationUnregistered } from './isConversationUnregistered.dom.js'; import { missingCaseError } from './missingCaseError.std.js'; import type { ConversationModel } from '../models/conversations.preload.js'; import { mapEmplace } from './mapEmplace.std.js'; import { isSignalConversation } from './isSignalConversation.dom.js'; import { messageSender } from '../textsecure/SendMessage.preload.js'; import { itemStorage } from '../textsecure/Storage.preload.js'; +import { shouldSendToDirectConversation } from '../jobs/helpers/shouldSendToConversation.preload.js'; const { chunk, map } = lodash; @@ -106,22 +105,9 @@ export async function sendReceipts({ Array.from(allGroups.values(), async group => { const { conversationId, sender, receipts: receiptsForSender } = group; - if (!isConversationAccepted(sender.attributes)) { - log.info( - `conversation ${sender.idForLogging()} is not accepted; refusing to send` - ); - return; - } - if (isConversationUnregistered(sender.attributes)) { - log.info( - `conversation ${sender.idForLogging()} is unregistered; refusing to send` - ); - return; - } - if (sender.isBlocked()) { - log.info( - `conversation ${sender.idForLogging()} is blocked; refusing to send` - ); + const [ok, refusal] = shouldSendToDirectConversation(sender); + if (!ok) { + log.info(refusal.logLine); return; } if (isSignalConversation(sender.attributes)) {