View-once media: backend send support

This commit is contained in:
yash-signal
2026-01-08 12:49:46 -06:00
committed by GitHub
parent c9281f7f95
commit c36c329645
7 changed files with 56 additions and 11 deletions

View File

@@ -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<RawBodyRange>;
isViewOnce?: boolean;
preview: Array<OutgoingLinkPreviewType> | 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,

View File

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

View File

@@ -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<EmbeddedContactWithHydratedAvatar>;
bodyRanges?: DraftBodyRanges;
isViewOnce?: boolean;
preview?: Array<LinkPreviewWithHydratedData>;
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,

View File

@@ -613,6 +613,7 @@ function sendMultiMediaMessage(
options: WithPreSendChecksOptions & {
bodyRanges?: DraftBodyRanges;
draftAttachments?: ReadonlyArray<AttachmentDraftType>;
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,

View File

@@ -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<OutgoingLinkPreviewType>;
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,

View File

@@ -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 = {}

View File

@@ -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'
? {