From c36c3296453b90e132b8faf1ad8e3eaf1ecd94a3 Mon Sep 17 00:00:00 2001 From: yash-signal Date: Thu, 8 Jan 2026 12:49:46 -0600 Subject: [PATCH] View-once media: backend send support --- ts/jobs/helpers/sendNormalMessage.preload.ts | 10 +++++ ts/messages/handleDataMessage.preload.ts | 2 +- ts/models/conversations.preload.ts | 3 ++ ts/state/ducks/composer.preload.ts | 3 ++ ts/textsecure/SendMessage.preload.ts | 9 +++++ ts/util/cleanup.preload.ts | 1 + ts/windows/main/start.preload.ts | 39 +++++++++++++++----- 7 files changed, 56 insertions(+), 11 deletions(-) diff --git a/ts/jobs/helpers/sendNormalMessage.preload.ts b/ts/jobs/helpers/sendNormalMessage.preload.ts index 53683e91c1..7de46cb538 100644 --- a/ts/jobs/helpers/sendNormalMessage.preload.ts +++ b/ts/jobs/helpers/sendNormalMessage.preload.ts @@ -86,6 +86,7 @@ import { uuidToBytes } from '../../util/uuidToBytes.std.js'; import { fromBase64 } from '../../Bytes.std.js'; import { MIMETypeToString } from '../../types/MIME.std.js'; import { canReuseExistingTransitCdnPointerForEditedMessage } from '../../util/Attachment.std.js'; +import { eraseMessageContents } from '../../util/cleanup.preload.js'; const { isNumber } = lodash; @@ -231,6 +232,7 @@ export async function sendNormalMessage( contact, deletedForEveryoneTimestamp, expireTimer, + isViewOnce, bodyRanges, preview, quote, @@ -365,6 +367,7 @@ export async function sendNormalMessage( deletedForEveryoneTimestamp, expireTimer, groupV2: groupV2Info, + isViewOnce, body, preview, profileKey, @@ -432,6 +435,7 @@ export async function sendNormalMessage( deletedForEveryoneTimestamp, expireTimer, expireTimerVersion: conversation.getExpireTimerVersion(), + isViewOnce, preview, profileKey, quote, @@ -495,6 +499,10 @@ export async function sendNormalMessage( } throw new Error('message did not fully send'); } + + if (isViewOnce) { + await eraseMessageContents(message, 'view-once-sent'); + } } catch (thrownError: unknown) { const errors = [thrownError, ...messageSendErrors]; await handleMultipleSendErrors({ @@ -626,6 +634,7 @@ async function getMessageSendData({ deletedForEveryoneTimestamp: undefined | number; expireTimer: undefined | DurationInSeconds; bodyRanges: undefined | ReadonlyArray; + isViewOnce?: boolean; preview: Array | undefined; quote: OutgoingQuoteType | undefined; sticker: OutgoingStickerType | undefined; @@ -753,6 +762,7 @@ async function getMessageSendData({ contact, deletedForEveryoneTimestamp: message.get('deletedForEveryoneTimestamp'), expireTimer: message.get('expireTimer'), + isViewOnce: message.get('isViewOnce'), bodyRanges: getPropForTimestamp({ log, message: message.attributes, diff --git a/ts/messages/handleDataMessage.preload.ts b/ts/messages/handleDataMessage.preload.ts index 12e2ff6914..1075fc48cc 100644 --- a/ts/messages/handleDataMessage.preload.ts +++ b/ts/messages/handleDataMessage.preload.ts @@ -728,7 +728,7 @@ export async function handleDataMessage( } if (isTapToView(message.attributes) && type === 'outgoing') { - await eraseMessageContents(message, 'view-once-viewed'); + await eraseMessageContents(message, 'view-once-sent'); } if ( diff --git a/ts/models/conversations.preload.ts b/ts/models/conversations.preload.ts index c64b53ff7b..4cab3357f4 100644 --- a/ts/models/conversations.preload.ts +++ b/ts/models/conversations.preload.ts @@ -4087,6 +4087,7 @@ export class ConversationModel { body, contact, bodyRanges, + isViewOnce, preview, quote, sticker, @@ -4096,6 +4097,7 @@ export class ConversationModel { body: string | undefined; contact?: Array; bodyRanges?: DraftBodyRanges; + isViewOnce?: boolean; preview?: Array; quote?: QuotedMessageType; sticker?: StickerWithHydratedData; @@ -4230,6 +4232,7 @@ export class ConversationModel { received_at_ms: now, expirationStartTimestamp, expireTimer, + isViewOnce, readStatus: ReadStatus.Read, seenStatus: SeenStatus.NotApplicable, sticker, diff --git a/ts/state/ducks/composer.preload.ts b/ts/state/ducks/composer.preload.ts index d5ff905f1f..b2acd42693 100644 --- a/ts/state/ducks/composer.preload.ts +++ b/ts/state/ducks/composer.preload.ts @@ -613,6 +613,7 @@ function sendMultiMediaMessage( options: WithPreSendChecksOptions & { bodyRanges?: DraftBodyRanges; draftAttachments?: ReadonlyArray; + isViewOnce?: boolean; timestamp?: number; } ): ThunkAction< @@ -635,6 +636,7 @@ function sendMultiMediaMessage( const { draftAttachments, bodyRanges, + isViewOnce, message = '', timestamp = Date.now(), voiceNoteAttachment, @@ -676,6 +678,7 @@ function sendMultiMediaMessage( quote, preview: getLinkPreviewForSend(message), bodyRanges, + isViewOnce, }, { sendHQImages, diff --git a/ts/textsecure/SendMessage.preload.ts b/ts/textsecure/SendMessage.preload.ts index 6302891196..2dd8007792 100644 --- a/ts/textsecure/SendMessage.preload.ts +++ b/ts/textsecure/SendMessage.preload.ts @@ -212,6 +212,7 @@ export type SharedMessageOptionsType = Readonly<{ flags?: number; groupCallUpdate?: GroupCallUpdateType; groupV2?: GroupV2InfoType; + isViewOnce?: boolean; pinMessage?: SendPinMessageType; pollVote?: OutgoingPollVote; pollCreate?: PollCreateType; @@ -272,6 +273,8 @@ class Message { groupV2?: GroupV2InfoType; + isViewOnce?: boolean; + preview?: ReadonlyArray; profileKey?: Uint8Array; @@ -314,6 +317,7 @@ class Message { this.expireTimerVersion = options.expireTimerVersion; this.flags = options.flags; this.groupV2 = options.groupV2; + this.isViewOnce = options.isViewOnce; this.preview = options.preview; this.profileKey = options.profileKey; this.quote = options.quote; @@ -584,6 +588,9 @@ class Message { if (this.profileKey) { proto.profileKey = this.profileKey; } + if (this.isViewOnce) { + proto.isViewOnce = true; + } if (this.deletedForEveryoneTimestamp) { proto.delete = { targetSentTimestamp: Long.fromNumber(this.deletedForEveryoneTimestamp), @@ -1125,6 +1132,7 @@ export class MessageSender { flags, groupCallUpdate, groupV2, + isViewOnce, body, preview, profileKey, @@ -1173,6 +1181,7 @@ export class MessageSender { flags, groupCallUpdate, groupV2, + isViewOnce, preview, profileKey, quote, diff --git a/ts/util/cleanup.preload.ts b/ts/util/cleanup.preload.ts index 4604552b21..ff26fc5ada 100644 --- a/ts/util/cleanup.preload.ts +++ b/ts/util/cleanup.preload.ts @@ -46,6 +46,7 @@ export async function eraseMessageContents( | 'view-once-viewed' | 'view-once-invalid' | 'view-once-expired' + | 'view-once-sent' | 'unsupported-message' | 'delete-for-everyone', additionalProperties = {} diff --git a/ts/windows/main/start.preload.ts b/ts/windows/main/start.preload.ts index fa2198ea67..777966abe4 100644 --- a/ts/windows/main/start.preload.ts +++ b/ts/windows/main/start.preload.ts @@ -1,6 +1,7 @@ // Copyright 2017 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import { fabric } from 'fabric'; import lodash from 'lodash'; import { contextBridge } from 'electron'; @@ -31,11 +32,7 @@ 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'; +import { IMAGE_PNG } from '../../types/MIME.std.js'; const { has } = lodash; @@ -120,17 +117,39 @@ if ( calling._iceServerOverride = override; }, - sendPollInSelectedConversation: async (poll: PollCreateType) => { - if (!isPollSendEnabled()) { - throw new Error('Poll sending is not enabled'); - } + sendViewOnceImageInSelectedConversation: async () => { 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); + + const canvas = new fabric.StaticCanvas(null, { + width: 100, + height: 100, + backgroundColor: '#3b82f6', + }); + const dataURL = canvas.toDataURL({ format: 'png' }); + const base64Data = dataURL.split(',')[1]; + const data = Uint8Array.from(atob(base64Data), c => c.charCodeAt(0)); + + await conversation.enqueueMessageForSend( + { + body: undefined, + attachments: [ + { + contentType: IMAGE_PNG, + size: data.byteLength, + data, + }, + ], + isViewOnce: true, + }, + {} + ); + + log.info('Sent view-once test image'); }, ...(window.SignalContext.config.ciMode === 'benchmark' ? {