From 1ddb81e053160d63aeacb83f7cb03a9598c36838 Mon Sep 17 00:00:00 2001 From: yash-signal Date: Tue, 21 Oct 2025 16:40:10 -0500 Subject: [PATCH] Add ability to send poll create messages --- ts/RemoteConfig.dom.ts | 3 ++ ts/jobs/helpers/sendNormalMessage.preload.ts | 6 ++++ ts/models/conversations.preload.ts | 4 +++ ts/textsecure/SendMessage.preload.ts | 16 +++++++++ ts/types/Polls.dom.ts | 35 ++++++++++++++++++++ ts/util/enqueuePollCreateForSend.dom.ts | 32 ++++++++++++++++++ ts/windows/main/start.preload.ts | 17 ++++++++++ 7 files changed, 113 insertions(+) create mode 100644 ts/util/enqueuePollCreateForSend.dom.ts diff --git a/ts/RemoteConfig.dom.ts b/ts/RemoteConfig.dom.ts index 7f146f9ee1..51fda57945 100644 --- a/ts/RemoteConfig.dom.ts +++ b/ts/RemoteConfig.dom.ts @@ -41,6 +41,9 @@ const KnownConfigKeys = [ 'desktop.pollReceive.alpha', 'desktop.pollReceive.beta', 'desktop.pollReceive.prod', + 'desktop.pollSend.alpha', + 'desktop.pollSend.beta', + 'desktop.pollSend.prod', 'global.attachments.maxBytes', 'global.attachments.maxReceiveBytes', 'global.backups.mediaTierFallbackCdnNumber', diff --git a/ts/jobs/helpers/sendNormalMessage.preload.ts b/ts/jobs/helpers/sendNormalMessage.preload.ts index 648073339d..872457ff42 100644 --- a/ts/jobs/helpers/sendNormalMessage.preload.ts +++ b/ts/jobs/helpers/sendNormalMessage.preload.ts @@ -46,6 +46,7 @@ import { copyCdnFields } from '../../util/attachments.preload.js'; import type { RawBodyRange } from '../../types/BodyRange.std.js'; import type { EmbeddedContactWithUploadedAvatar } from '../../types/EmbeddedContact.std.js'; import type { StoryContextType } from '../../types/Util.std.js'; +import type { PollCreateType } from '../../types/Polls.dom.js'; import type { LoggerType } from '../../types/Logging.std.js'; import { GROUP } from '../../types/Message2.preload.js'; import type { @@ -231,6 +232,7 @@ export async function sendNormalMessage( sticker, storyMessage, storyContext, + poll, } = await getMessageSendData({ log, message, @@ -317,6 +319,7 @@ export async function sendNormalMessage( : undefined, timestamp: targetTimestamp, reaction, + pollCreate: poll, }); messageSendPromise = sendSyncMessageOnly(message, { dataMessage, @@ -362,6 +365,7 @@ export async function sendNormalMessage( sticker, storyContext, reaction, + pollCreate: poll, targetTimestampForEdit: editedMessageTimestamp ? targetOfThisEditTimestamp : undefined, @@ -619,6 +623,7 @@ async function getMessageSendData({ reaction: ReactionType | undefined; storyMessage?: MessageModel; storyContext?: StoryContextType; + poll?: PollCreateType; }> { const storyId = message.get('storyId'); @@ -761,6 +766,7 @@ async function getMessageSendData({ timestamp: storyMessage.get('sent_at'), } : undefined, + poll: message.get('poll'), }; } diff --git a/ts/models/conversations.preload.ts b/ts/models/conversations.preload.ts index 7cc58e315b..c4ae45b758 100644 --- a/ts/models/conversations.preload.ts +++ b/ts/models/conversations.preload.ts @@ -140,6 +140,7 @@ import type { LinkPreviewType, LinkPreviewWithHydratedData, } from '../types/message/LinkPreviews.std.js'; +import type { PollCreateType } from '../types/Polls.dom.js'; import { MINUTE, SECOND, @@ -4022,6 +4023,7 @@ export class ConversationModel { preview, quote, sticker, + poll, }: { attachments: Array; body: string | undefined; @@ -4030,6 +4032,7 @@ export class ConversationModel { preview?: Array; quote?: QuotedMessageType; sticker?: StickerWithHydratedData; + poll?: PollCreateType; }, { dontClearDraft = false, @@ -4154,6 +4157,7 @@ export class ConversationModel { }) ), storyId, + poll, }); const model = window.MessageCache.register(new MessageModel(attributes)); diff --git a/ts/textsecure/SendMessage.preload.ts b/ts/textsecure/SendMessage.preload.ts index 64a2adbf2a..d970a6ef82 100644 --- a/ts/textsecure/SendMessage.preload.ts +++ b/ts/textsecure/SendMessage.preload.ts @@ -101,6 +101,7 @@ import { import { MAX_MESSAGE_COUNT } from '../util/deleteForMe.types.std.js'; import { isProtoBinaryEncodingEnabled } from '../util/isProtoBinaryEncodingEnabled.std.js'; import type { GroupSendToken } from '../types/GroupSendEndorsements.std.js'; +import type { PollCreateType } from '../types/Polls.dom.js'; import { itemStorage } from './Storage.preload.js'; import { accountManager } from './AccountManager.preload.js'; @@ -214,6 +215,7 @@ export type MessageOptionsType = { recipients: ReadonlyArray; sticker?: OutgoingStickerType; reaction?: ReactionType; + pollCreate?: PollCreateType; deletedForEveryoneTimestamp?: number; targetTimestampForEdit?: number; timestamp: number; @@ -238,6 +240,7 @@ export type GroupSendOptionsType = { sticker?: OutgoingStickerType; storyContext?: StoryContextType; timestamp: number; + pollCreate?: PollCreateType; }; class Message { @@ -276,6 +279,8 @@ class Message { reaction?: ReactionType; + pollCreate?: PollCreateType; + timestamp: number; dataMessage?: Proto.DataMessage; @@ -303,6 +308,7 @@ class Message { this.recipients = options.recipients; this.sticker = options.sticker; this.reaction = options.reaction; + this.pollCreate = options.pollCreate; this.timestamp = options.timestamp; this.deletedForEveryoneTimestamp = options.deletedForEveryoneTimestamp; this.groupCallUpdate = options.groupCallUpdate; @@ -627,6 +633,14 @@ class Message { proto.storyContext = storyContext; } + if (this.pollCreate) { + const create = new Proto.DataMessage.PollCreate(); + create.question = this.pollCreate.question; + create.allowMultiple = Boolean(this.pollCreate.allowMultiple); + create.options = this.pollCreate.options.slice(); + proto.pollCreate = create; + } + this.dataMessage = proto; return proto; } @@ -938,6 +952,7 @@ export class MessageSender { storyContext, targetTimestampForEdit, timestamp, + pollCreate, } = options; if (!groupV2) { @@ -981,6 +996,7 @@ export class MessageSender { storyContext, targetTimestampForEdit, timestamp, + pollCreate, }; } diff --git a/ts/types/Polls.dom.ts b/ts/types/Polls.dom.ts index 0209525e99..7566e54c41 100644 --- a/ts/types/Polls.dom.ts +++ b/ts/types/Polls.dom.ts @@ -80,12 +80,18 @@ export type PollMessageAttribute = { terminatedAt?: number; }; +export type PollCreateType = Pick< + PollMessageAttribute, + 'question' | 'options' | 'allowMultiple' +>; + export function isPollReceiveEnabled(): boolean { const env = getEnvironment(); if ( env === Environment.Development || env === Environment.Test || + env === Environment.Staging || isMockEnvironment() ) { return true; @@ -107,3 +113,32 @@ export function isPollReceiveEnabled(): boolean { return false; } + +export function isPollSendEnabled(): boolean { + const env = getEnvironment(); + + if ( + env === Environment.Development || + env === Environment.Test || + env === Environment.Staging || + isMockEnvironment() + ) { + return true; + } + + const version = window.getVersion?.(); + + if (version != null) { + if (isProduction(version)) { + return RemoteConfig.isEnabled('desktop.pollSend.prod'); + } + if (isBeta(version)) { + return RemoteConfig.isEnabled('desktop.pollSend.beta'); + } + if (isAlpha(version)) { + return RemoteConfig.isEnabled('desktop.pollSend.alpha'); + } + } + + return false; +} diff --git a/ts/util/enqueuePollCreateForSend.dom.ts b/ts/util/enqueuePollCreateForSend.dom.ts new file mode 100644 index 0000000000..6b3a5db973 --- /dev/null +++ b/ts/util/enqueuePollCreateForSend.dom.ts @@ -0,0 +1,32 @@ +// Copyright 2025 Signal Messenger, LLC +// 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'; + +export async function enqueuePollCreateForSend( + conversation: ConversationModel, + poll: PollCreateType +): Promise { + if (!isPollSendEnabled()) { + throw new Error('enqueuePollCreateForSend: poll sending is not enabled'); + } + + if (!isGroupV2(conversation.attributes)) { + throw new Error( + 'enqueuePollCreateForSend: polls are group-only. Conversation is not GroupV2.' + ); + } + + await conversation.enqueueMessageForSend( + { + attachments: [], + body: undefined, + poll, + }, + { + timestamp: Date.now(), + } + ); +} diff --git a/ts/windows/main/start.preload.ts b/ts/windows/main/start.preload.ts index c26ff961a8..f6a632eb06 100644 --- a/ts/windows/main/start.preload.ts +++ b/ts/windows/main/start.preload.ts @@ -31,6 +31,11 @@ import { Environment, getEnvironment } from '../../environment.std.js'; import { isProduction } from '../../util/version.std.js'; import { benchmarkConversationOpen } from '../../CI/benchmarkConversationOpen.preload.js'; import { itemStorage } from '../../textsecure/Storage.preload.js'; +import { enqueuePollCreateForSend } from '../../util/enqueuePollCreateForSend.dom.js'; +import { + isPollSendEnabled, + type PollCreateType, +} from '../../types/Polls.dom.js'; const { has } = lodash; @@ -117,6 +122,18 @@ if ( }, setRtcStatsInterval: (intervalMillis: number) => calling.setAllRtcStatsInterval(intervalMillis), + sendPollInSelectedConversation: async (poll: PollCreateType) => { + if (!isPollSendEnabled()) { + throw new Error('Poll sending is not enabled'); + } + const conversationId = + window.reduxStore.getState().conversations.selectedConversationId; + const conversation = window.ConversationController.get(conversationId); + if (!conversation) { + throw new Error('No conversation selected'); + } + await enqueuePollCreateForSend(conversation, poll); + }, ...(window.SignalContext.config.ciMode === 'benchmark' ? { benchmarkConversationOpen,