Polls: Support sending polls in 1:1 conversations

Co-authored-by: yash-signal <yash@signal.org>
This commit is contained in:
automated-signal
2026-02-24 16:18:18 -06:00
committed by GitHub
parent 3a579ece7c
commit 7e47676b90
18 changed files with 333 additions and 286 deletions

View File

@@ -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<typeof SemverKeys>;

View File

@@ -79,6 +79,7 @@ export default {
i18n,
isDisabled: false,
isFormattingEnabled: true,
isPollSend1to1Enabled: true,
messageCompositionId: '456',
sendEditedMessage: action('sendEditedMessage'),
sendMultiMediaMessage: action('sendMultiMediaMessage'),

View File

@@ -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({
<AxoDropdownMenu.Item symbol="file" onSelect={launchFilePicker}>
{i18n('icu:CompositionArea__AttachMenu__File')}
</AxoDropdownMenu.Item>
{conversationType === 'group' && (
{(conversationType === 'group' || isPollSend1to1Enabled) && (
<AxoDropdownMenu.Item
symbol="poll"
onSelect={handleOpenPollModal}

View File

@@ -26,8 +26,6 @@ import type {
} from '../conversationJobQueue.preload.js';
import { getUntrustedConversationServiceIds } from './getUntrustedConversationServiceIds.dom.js';
import { handleMessageSend } from '../../util/handleMessageSend.preload.js';
import { isConversationAccepted } from '../../util/isConversationAccepted.preload.js';
import { isConversationUnregistered } from '../../util/isConversationUnregistered.dom.js';
import { getMessageById } from '../../messages/getMessageById.preload.js';
import { isNotNil } from '../../util/isNotNil.std.js';
import type { CallbackResultType } from '../../textsecure/Types.d.ts';
@@ -38,6 +36,7 @@ import type { LoggerType } from '../../types/Logging.std.js';
import type { ServiceIdString } from '../../types/ServiceId.std.js';
import { isStory } from '../../messages/helpers.std.js';
import { sendToGroup } from '../../util/sendToGroup.preload.js';
import { shouldSendToDirectConversation } from './shouldSendToConversation.preload.js';
const { isNumber } = lodash;
@@ -157,37 +156,10 @@ export async function sendDeleteForEveryone(
);
await updateMessageWithSuccessfulSends(message);
} else if (isDirectConversation(conversation.attributes)) {
if (!isConversationAccepted(conversation.attributes)) {
log.info(
`conversation ${conversation.idForLogging()} is not accepted; refusing to send`
);
void updateMessageWithFailure(
message,
[new Error('Message request was not accepted')],
log
);
return;
}
if (isConversationUnregistered(conversation.attributes)) {
log.info(
`conversation ${conversation.idForLogging()} is unregistered; refusing to send`
);
void updateMessageWithFailure(
message,
[new Error('Contact no longer has a Signal account')],
log
);
return;
}
if (conversation.isBlocked()) {
log.info(
`conversation ${conversation.idForLogging()} is blocked; refusing to send`
);
void updateMessageWithFailure(
message,
[new Error('Contact is blocked')],
log
);
const [ok, refusal] = shouldSendToDirectConversation(conversation);
if (!ok) {
log.info(refusal.logLine);
void updateMessageWithFailure(message, [refusal.error], log);
return;
}

View File

@@ -22,8 +22,6 @@ import type {
} from '../conversationJobQueue.preload.js';
import { getUntrustedConversationServiceIds } from './getUntrustedConversationServiceIds.dom.js';
import { handleMessageSend } from '../../util/handleMessageSend.preload.js';
import { isConversationAccepted } from '../../util/isConversationAccepted.preload.js';
import { isConversationUnregistered } from '../../util/isConversationUnregistered.dom.js';
import { getMessageById } from '../../messages/getMessageById.preload.js';
import { isNotNil } from '../../util/isNotNil.std.js';
import type { CallbackResultType } from '../../textsecure/Types.d.ts';
@@ -32,6 +30,7 @@ import { SendMessageProtoError } from '../../textsecure/Errors.std.js';
import { strictAssert } from '../../util/assert.std.js';
import type { LoggerType } from '../../types/Logging.std.js';
import { isStory } from '../../messages/helpers.std.js';
import { shouldSendToDirectConversation } from './shouldSendToConversation.preload.js';
export async function sendDeleteStoryForEveryone(
ourConversation: ConversationModel,
@@ -109,40 +108,10 @@ export async function sendDeleteStoryForEveryone(
return undefined;
}
if (!isConversationAccepted(conversation.attributes)) {
log.info(
`${logId}: conversation ${conversation.idForLogging()} ` +
'is not accepted; refusing to send'
);
void updateMessageWithFailure(
message,
[new Error('Message request was not accepted')],
log
);
return undefined;
}
if (isConversationUnregistered(conversation.attributes)) {
log.info(
`${logId}: conversation ${conversation.idForLogging()} ` +
'is unregistered; refusing to send'
);
void updateMessageWithFailure(
message,
[new Error('Contact no longer has a Signal account')],
log
);
return undefined;
}
if (conversation.isBlocked()) {
log.info(
`${logId}: conversation ${conversation.idForLogging()} ` +
'is blocked; refusing to send'
);
void updateMessageWithFailure(
message,
[new Error('Contact is blocked')],
log
);
const [ok, refusal] = shouldSendToDirectConversation(conversation);
if (!ok) {
log.info(`${logId}: ${refusal.logLine}`);
void updateMessageWithFailure(message, [refusal.error], log);
return undefined;
}

View File

@@ -22,9 +22,8 @@ import type {
ConversationQueueJobBundle,
} from '../conversationJobQueue.preload.js';
import { handleMessageSend } from '../../util/handleMessageSend.preload.js';
import { isConversationAccepted } from '../../util/isConversationAccepted.preload.js';
import { isConversationUnregistered } from '../../util/isConversationUnregistered.dom.js';
import { DurationInSeconds } from '../../util/durations/index.std.js';
import { shouldSendToDirectConversation } from './shouldSendToConversation.preload.js';
export async function sendDirectExpirationTimerUpdate(
conversation: ConversationModel,
@@ -119,22 +118,9 @@ export async function sendDirectExpirationTimerUpdate(
{ messageIds: [], sendType }
);
} else if (isDirectConversation(conversation.attributes)) {
if (!isConversationAccepted(conversation.attributes)) {
log.info(
`conversation ${conversation.idForLogging()} is not accepted; refusing to send`
);
return;
}
if (isConversationUnregistered(conversation.attributes)) {
log.info(
`conversation ${conversation.idForLogging()} is unregistered; refusing to send`
);
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);
return;
}

View File

@@ -55,8 +55,6 @@ import type { QuotedMessageType } from '../../model-types.d.ts';
import { handleMultipleSendErrors } from './handleMultipleSendErrors.std.js';
import { ourProfileKeyService } from '../../services/ourProfileKey.std.js';
import { isConversationUnregistered } from '../../util/isConversationUnregistered.dom.js';
import { isConversationAccepted } from '../../util/isConversationAccepted.preload.js';
import { sendToGroup } from '../../util/sendToGroup.preload.js';
import type { DurationInSeconds } from '../../util/durations/index.std.js';
import type { ServiceIdString } from '../../types/ServiceId.std.js';
@@ -81,6 +79,7 @@ import { getMessageIdForLogging } from '../../util/idForLogging.preload.js';
import { send, sendSyncMessageOnly } from '../../messages/send.preload.js';
import type { SignalService } from '../../protobuf/index.std.js';
import { eraseMessageContents } from '../../util/cleanup.preload.js';
import { shouldSendToDirectConversation } from './shouldSendToConversation.preload.js';
const { isNumber } = lodash;
@@ -383,35 +382,12 @@ export async function sendNormalMessage(
})
);
} else {
if (!isConversationAccepted(conversation.attributes)) {
log.info(
`conversation ${conversation.idForLogging()} is not accepted; refusing to send`
);
const [ok, refusal] = shouldSendToDirectConversation(conversation);
if (!ok) {
log.info(refusal.logLine);
void markMessageFailed({
message,
errors: [new Error('Message request was not accepted')],
targetTimestamp,
});
return;
}
if (isConversationUnregistered(conversation.attributes)) {
log.info(
`conversation ${conversation.idForLogging()} is unregistered; refusing to send`
);
void markMessageFailed({
message,
errors: [new Error('Contact no longer has a Signal account')],
targetTimestamp,
});
return;
}
if (conversation.isBlocked()) {
log.info(
`conversation ${conversation.idForLogging()} is blocked; refusing to send`
);
void markMessageFailed({
message,
errors: [new Error('Contact is blocked')],
errors: [refusal.error],
targetTimestamp,
});
return;
@@ -438,6 +414,7 @@ export async function sendNormalMessage(
targetTimestampForEdit: editedMessageTimestamp
? targetOfThisEditTimestamp
: undefined,
pollCreate: poll,
timestamp: targetTimestamp,
},
contentHint: ContentHint.Resendable,

View File

@@ -15,7 +15,12 @@ import { sendContentMessageToGroup } from '../../util/sendToGroup.preload.js';
import { getMessageById } from '../../messages/getMessageById.preload.js';
import { ourProfileKeyService } from '../../services/ourProfileKey.std.js';
import { getSendOptions } from '../../util/getSendOptions.preload.js';
import { isGroupV2 } from '../../util/whatTypeOfConversation.dom.js';
import {
isGroupV2,
isDirectConversation,
isMe,
} from '../../util/whatTypeOfConversation.dom.js';
import { SignalService as Proto } from '../../protobuf/index.std.js';
import {
handleMultipleSendErrors,
maybeExpandErrors,
@@ -26,6 +31,9 @@ import type { LoggerType } from '../../types/Logging.std.js';
import { strictAssert } from '../../util/assert.std.js';
import { DataWriter } from '../../sql/Client.preload.js';
import { cleanupMessages } from '../../util/cleanup.preload.js';
import { addPniSignatureMessageToProto } from '../../textsecure/SendMessage.preload.js';
import { shouldSendToDirectConversation } from './shouldSendToConversation.preload.js';
import { handleMessageSend } from '../../util/handleMessageSend.preload.js';
const { isNumber } = lodash;
@@ -57,11 +65,6 @@ export async function sendPollTerminate(
return;
}
if (!isGroupV2(conversation.attributes)) {
jobLog.error(`${logId}: Non-GroupV2 conversation. Failing job.`);
return;
}
if (!shouldContinue) {
jobLog.info(`${logId}: Ran out of time. Giving up on sending`);
await markTerminateFailed(pollMessage, jobLog);
@@ -70,86 +73,195 @@ export async function sendPollTerminate(
const recipients = getRecipients(conversation.attributes);
await conversation.queueJob(
'conversationQueue/sendPollTerminate',
async abortSignal => {
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(

View File

@@ -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<CallbackResultType>;
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;
}

View File

@@ -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<CallbackResultType>;
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;
}

View File

@@ -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];
}

View File

@@ -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<SingleProtoJobData> {
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;
}

View File

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

View File

@@ -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 <SmartCompositionRecording />;
@@ -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}

View File

@@ -255,9 +255,12 @@ export type PollVoteBuildOptions = Required<
>;
export type PollTerminateBuildOptions = Required<
Pick<MessageOptionsType, 'groupV2' | 'timestamp' | 'pollTerminate'>
Pick<MessageOptionsType, 'timestamp' | 'pollTerminate'>
> &
Pick<MessageOptionsType, 'profileKey' | 'expireTimer' | 'expireTimerVersion'>;
Pick<
MessageOptionsType,
'groupV2' | 'profileKey' | 'expireTimer' | 'expireTimerVersion'
>;
class Message {
attachments: ReadonlyArray<Proto.IAttachmentPointer>;
@@ -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;

View File

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

View File

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

View File

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