Limit unnecessary thumbnail generation

This commit is contained in:
trevor-signal 2025-09-24 10:55:08 -04:00 committed by GitHub
commit 74e327a6c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 132 additions and 81 deletions

View file

@ -7,7 +7,7 @@ import * as durations from '../util/durations/index.js';
import { createLogger } from '../logging/log.js'; import { createLogger } from '../logging/log.js';
import type { AttachmentBackfillResponseSyncEvent } from '../textsecure/messageReceiverEvents.js'; import type { AttachmentBackfillResponseSyncEvent } from '../textsecure/messageReceiverEvents.js';
import { import {
type AttachmentDownloadJobTypeType, type MessageAttachmentType,
type AttachmentDownloadJobType, type AttachmentDownloadJobType,
type CoreAttachmentDownloadJobType, type CoreAttachmentDownloadJobType,
AttachmentDownloadUrgency, AttachmentDownloadUrgency,
@ -82,7 +82,7 @@ export { isPermanentlyUndownloadable };
// Type for adding a new job // Type for adding a new job
export type NewAttachmentDownloadJobType = { export type NewAttachmentDownloadJobType = {
attachment: AttachmentType; attachment: AttachmentType;
attachmentType: AttachmentDownloadJobTypeType; attachmentType: MessageAttachmentType;
isManualDownload: boolean; isManualDownload: boolean;
messageId: string; messageId: string;
receivedAt: number; receivedAt: number;
@ -808,10 +808,13 @@ export async function runDownloadAttachmentJobInner({
}, },
}); });
const upgradedAttachment = await dependencies.processNewAttachment({ const upgradedAttachment = await dependencies.processNewAttachment(
...omit(attachment, ['error', 'pending']), {
...downloadedAttachment, ...omit(attachment, ['error', 'pending']),
}); ...downloadedAttachment,
},
attachmentType
);
const isShowingLightbox = (): boolean => { const isShowingLightbox = (): boolean => {
const lightboxState = window.reduxStore.getState().lightbox; const lightboxState = window.reduxStore.getState().lightbox;

View file

@ -13,7 +13,7 @@ import {
getUndownloadedAttachmentSignature, getUndownloadedAttachmentSignature,
} from '../../types/Attachment.js'; } from '../../types/Attachment.js';
import { import {
type AttachmentDownloadJobTypeType, type MessageAttachmentType,
AttachmentDownloadUrgency, AttachmentDownloadUrgency,
} from '../../types/AttachmentDownload.js'; } from '../../types/AttachmentDownload.js';
import { AttachmentDownloadSource } from '../../sql/Interface.js'; import { AttachmentDownloadSource } from '../../sql/Interface.js';
@ -353,7 +353,7 @@ export class AttachmentBackfill {
} }
public static isEnabledForJob( public static isEnabledForJob(
jobType: AttachmentDownloadJobTypeType, jobType: MessageAttachmentType,
message: Pick<ReadonlyMessageAttributesType, 'type'> message: Pick<ReadonlyMessageAttributesType, 'type'>
): boolean { ): boolean {
if (message.type === 'story') { if (message.type === 'story') {
@ -456,7 +456,7 @@ export class AttachmentBackfill {
export function isPermanentlyUndownloadable( export function isPermanentlyUndownloadable(
attachment: AttachmentType, attachment: AttachmentType,
disposition: AttachmentDownloadJobTypeType, disposition: MessageAttachmentType,
message: Pick<ReadonlyMessageAttributesType, 'type'> message: Pick<ReadonlyMessageAttributesType, 'type'>
): boolean { ): boolean {
// Attachment is downloadable or user have not failed to download it yet // Attachment is downloadable or user have not failed to download it yet

View file

@ -3,7 +3,7 @@
import lodash from 'lodash'; import lodash from 'lodash';
import { createLogger } from '../logging/log.js'; import { createLogger } from '../logging/log.js';
import * as Bytes from '../Bytes.js'; import * as Bytes from '../Bytes.js';
import type { AttachmentDownloadJobTypeType } from '../types/AttachmentDownload.js'; import type { MessageAttachmentType } from '../types/AttachmentDownload.js';
import type { AttachmentType } from '../types/Attachment.js'; import type { AttachmentType } from '../types/Attachment.js';
import { import {
@ -69,7 +69,7 @@ export async function addAttachmentToMessage(
messageId: string, messageId: string,
attachment: AttachmentType, attachment: AttachmentType,
jobLogId: string, jobLogId: string,
{ type }: { type: AttachmentDownloadJobTypeType } { type }: { type: MessageAttachmentType }
): Promise<void> { ): Promise<void> {
const logPrefix = `${jobLogId}/addAttachmentToMessage`; const logPrefix = `${jobLogId}/addAttachmentToMessage`;
const message = await getMessageById(messageId); const message = await getMessageById(messageId);

View file

@ -245,10 +245,13 @@ export class ReleaseNotesFetcher {
); );
const processedAttachment = const processedAttachment =
await window.Signal.Migrations.processNewAttachment({ await window.Signal.Migrations.processNewAttachment(
...localAttachment, {
contentType: stringToMIMEType(contentType), ...localAttachment,
}); contentType: stringToMIMEType(contentType),
},
'attachment'
);
return { hydratedNote, processedAttachment }; return { hydratedNote, processedAttachment };
} }

View file

@ -58,6 +58,7 @@ import type {
} from './types/message/LinkPreviews.js'; } from './types/message/LinkPreviews.js';
import type { StickerType, StickerWithHydratedData } from './types/Stickers.js'; import type { StickerType, StickerWithHydratedData } from './types/Stickers.js';
import { beforeNavigateService } from './services/BeforeNavigate.js'; import { beforeNavigateService } from './services/BeforeNavigate.js';
import type { MessageAttachmentType } from './types/AttachmentDownload.js';
type EncryptedReader = ( type EncryptedReader = (
attachment: Partial<AddressableAttachmentType> attachment: Partial<AddressableAttachmentType>
@ -122,7 +123,10 @@ type MigrationsModuleType = {
name: string; name: string;
baseDir?: string; baseDir?: string;
}) => Promise<null | { fullPath: string; name: string }>; }) => Promise<null | { fullPath: string; name: string }>;
processNewAttachment: (attachment: AttachmentType) => Promise<AttachmentType>; processNewAttachment: (
attachment: AttachmentType,
attachmentType: MessageAttachmentType
) => Promise<AttachmentType>;
processNewSticker: (stickerData: Uint8Array) => Promise< processNewSticker: (stickerData: Uint8Array) => Promise<
LocalAttachmentV2Type & { LocalAttachmentV2Type & {
width: number; width: number;
@ -327,8 +331,11 @@ export function initializeMigrations({
readStickerData, readStickerData,
readTempData, readTempData,
saveAttachmentToDisk, saveAttachmentToDisk,
processNewAttachment: (attachment: AttachmentType) => processNewAttachment: (
MessageType.processNewAttachment(attachment, { attachment: AttachmentType,
attachmentType: MessageAttachmentType
) =>
MessageType.processNewAttachment(attachment, attachmentType, {
writeNewAttachmentData, writeNewAttachmentData,
makeObjectUrl, makeObjectUrl,
revokeObjectUrl, revokeObjectUrl,

View file

@ -49,7 +49,7 @@ import type {
} from '../types/CallLink.js'; } from '../types/CallLink.js';
import type { import type {
AttachmentDownloadJobType, AttachmentDownloadJobType,
AttachmentDownloadJobTypeType, MessageAttachmentType,
} from '../types/AttachmentDownload.js'; } from '../types/AttachmentDownload.js';
import type { import type {
GroupSendEndorsementsData, GroupSendEndorsementsData,
@ -651,7 +651,7 @@ export const MESSAGE_ATTACHMENT_COLUMNS = [
export type MessageAttachmentDBType = { export type MessageAttachmentDBType = {
messageId: string; messageId: string;
attachmentType: AttachmentDownloadJobTypeType; attachmentType: MessageAttachmentType;
orderInMessage: number; orderInMessage: number;
editHistoryIndex: number | null; editHistoryIndex: number | null;
conversationId: string; conversationId: string;

View file

@ -86,7 +86,7 @@ import {
} from '../types/AttachmentBackup.js'; } from '../types/AttachmentBackup.js';
import { import {
attachmentDownloadJobSchema, attachmentDownloadJobSchema,
type AttachmentDownloadJobTypeType, type MessageAttachmentType,
type AttachmentDownloadJobType, type AttachmentDownloadJobType,
} from '../types/AttachmentDownload.js'; } from '../types/AttachmentDownload.js';
import type { import type {
@ -2700,7 +2700,7 @@ function saveMessageAttachment({
sentAt: number; sentAt: number;
receivedAt: number; receivedAt: number;
receivedAtMs: number | undefined; receivedAtMs: number | undefined;
attachmentType: AttachmentDownloadJobTypeType; attachmentType: MessageAttachmentType;
attachment: AttachmentType; attachment: AttachmentType;
orderInMessage: number; orderInMessage: number;
editHistoryIndex: number | null; editHistoryIndex: number | null;

View file

@ -6,9 +6,9 @@ import * as z from 'zod';
import type { LoggerType } from '../../types/Logging.js'; import type { LoggerType } from '../../types/Logging.js';
import { import {
attachmentDownloadTypeSchema, messageAttachmentTypeSchema,
type AttachmentDownloadJobType, type AttachmentDownloadJobType,
type AttachmentDownloadJobTypeType, type MessageAttachmentType,
} from '../../types/AttachmentDownload.js'; } from '../../types/AttachmentDownload.js';
import type { AttachmentType } from '../../types/Attachment.js'; import type { AttachmentType } from '../../types/Attachment.js';
import { jsonToObject, objectToJSON, sql } from '../util.js'; import { jsonToObject, objectToJSON, sql } from '../util.js';
@ -28,7 +28,7 @@ export type _AttachmentDownloadJobTypeV1030 = {
messageId: string; messageId: string;
pending: number; pending: number;
timestamp: number; timestamp: number;
type: AttachmentDownloadJobTypeType; type: MessageAttachmentType;
}; };
const attachmentDownloadJobSchemaV1040 = z const attachmentDownloadJobSchemaV1040 = z
@ -36,7 +36,7 @@ const attachmentDownloadJobSchemaV1040 = z
attachment: z attachment: z
.object({ size: z.number(), contentType: MIMETypeSchema }) .object({ size: z.number(), contentType: MIMETypeSchema })
.passthrough(), .passthrough(),
attachmentType: attachmentDownloadTypeSchema, attachmentType: messageAttachmentTypeSchema,
ciphertextSize: z.number(), ciphertextSize: z.number(),
contentType: MIMETypeSchema, contentType: MIMETypeSchema,
digest: z.string(), digest: z.string(),

View file

@ -3,7 +3,7 @@
import { z } from 'zod'; import { z } from 'zod';
import { convertUndefinedToNull } from '../../util/dropNull.js'; import { convertUndefinedToNull } from '../../util/dropNull.js';
import { attachmentDownloadTypeSchema } from '../../types/AttachmentDownload.js'; import { messageAttachmentTypeSchema } from '../../types/AttachmentDownload.js';
import { APPLICATION_OCTET_STREAM } from '../../types/MIME.js'; import { APPLICATION_OCTET_STREAM } from '../../types/MIME.js';
import type { MessageAttachmentDBType } from '../Interface.js'; import type { MessageAttachmentDBType } from '../Interface.js';
@ -35,7 +35,7 @@ export const permissiveMessageAttachmentSchema = z.object({
messageId: z.string(), messageId: z.string(),
messageType: z.string(), messageType: z.string(),
editHistoryIndex: z.number(), editHistoryIndex: z.number(),
attachmentType: attachmentDownloadTypeSchema, attachmentType: messageAttachmentTypeSchema,
orderInMessage: z.number(), orderInMessage: z.number(),
conversationId: z.string(), conversationId: z.string(),
sentAt: z.number().catch(0), sentAt: z.number().catch(0),

View file

@ -71,7 +71,7 @@ import {
isIncremental, isIncremental,
defaultBlurHash, defaultBlurHash,
} from '../../types/Attachment.js'; } from '../../types/Attachment.js';
import type { AttachmentDownloadJobTypeType } from '../../types/AttachmentDownload.js'; import type { MessageAttachmentType } from '../../types/AttachmentDownload.js';
import { type DefaultConversationColorType } from '../../types/Colors.js'; import { type DefaultConversationColorType } from '../../types/Colors.js';
import { ReadStatus } from '../../messages/MessageReadStatus.js'; import { ReadStatus } from '../../messages/MessageReadStatus.js';
@ -1852,7 +1852,7 @@ export function getPropsForEmbeddedContact(
export function getPropsForAttachment( export function getPropsForAttachment(
attachment: AttachmentType, attachment: AttachmentType,
disposition: AttachmentDownloadJobTypeType, disposition: MessageAttachmentType,
message: Pick<ReadonlyMessageAttributesType, 'type'> message: Pick<ReadonlyMessageAttributesType, 'type'>
): AttachmentForUIType { ): AttachmentForUIType {
const { path, pending, screenshot, thumbnail, thumbnailFromBackup } = const { path, pending, screenshot, thumbnail, thumbnailFromBackup } =

View file

@ -37,6 +37,7 @@ import {
} from './Crypto.js'; } from './Crypto.js';
import { missingCaseError } from '../util/missingCaseError.js'; import { missingCaseError } from '../util/missingCaseError.js';
import type { MakeVideoScreenshotResultType } from './VisualAttachment.js'; import type { MakeVideoScreenshotResultType } from './VisualAttachment.js';
import type { MessageAttachmentType } from './AttachmentDownload.js';
const { const {
isNumber, isNumber,
@ -482,6 +483,7 @@ const THUMBNAIL_CONTENT_TYPE = MIME.IMAGE_PNG;
export async function captureDimensionsAndScreenshot( export async function captureDimensionsAndScreenshot(
attachment: AttachmentType, attachment: AttachmentType,
options: { generateThumbnail: boolean },
params: { params: {
writeNewAttachmentData: ( writeNewAttachmentData: (
data: Uint8Array data: Uint8Array
@ -544,28 +546,35 @@ export async function captureDimensionsAndScreenshot(
objectUrl: localUrl, objectUrl: localUrl,
logger, logger,
}); });
const thumbnailBuffer = await blobToArrayBuffer( let thumbnail: LocalAttachmentV2Type | undefined;
await makeImageThumbnail({
size: THUMBNAIL_SIZE, if (options.generateThumbnail) {
objectUrl: localUrl, const thumbnailBuffer = await blobToArrayBuffer(
contentType: THUMBNAIL_CONTENT_TYPE, await makeImageThumbnail({
logger, size: THUMBNAIL_SIZE,
}) objectUrl: localUrl,
); contentType: THUMBNAIL_CONTENT_TYPE,
logger,
})
);
thumbnail = await writeNewAttachmentData(
new Uint8Array(thumbnailBuffer)
);
}
const thumbnail = await writeNewAttachmentData(
new Uint8Array(thumbnailBuffer)
);
return { return {
...attachment, ...attachment,
width, width,
height, height,
thumbnail: { thumbnail: thumbnail
...thumbnail, ? {
contentType: THUMBNAIL_CONTENT_TYPE, ...thumbnail,
width: THUMBNAIL_SIZE, contentType: THUMBNAIL_CONTENT_TYPE,
height: THUMBNAIL_SIZE, width: THUMBNAIL_SIZE,
}, height: THUMBNAIL_SIZE,
}
: undefined,
}; };
} catch (error) { } catch (error) {
logger.error( logger.error(
@ -597,18 +606,19 @@ export async function captureDimensionsAndScreenshot(
new Uint8Array(screenshotBuffer) new Uint8Array(screenshotBuffer)
); );
const thumbnailBuffer = await blobToArrayBuffer( let thumbnail: LocalAttachmentV2Type | undefined;
await makeImageThumbnail({ if (options.generateThumbnail) {
size: THUMBNAIL_SIZE, const thumbnailBuffer = await blobToArrayBuffer(
objectUrl: screenshotObjectUrl, await makeImageThumbnail({
contentType: THUMBNAIL_CONTENT_TYPE, size: THUMBNAIL_SIZE,
logger, objectUrl: screenshotObjectUrl,
}) contentType: THUMBNAIL_CONTENT_TYPE,
); logger,
})
);
const thumbnail = await writeNewAttachmentData( thumbnail = await writeNewAttachmentData(new Uint8Array(thumbnailBuffer));
new Uint8Array(thumbnailBuffer) }
);
return { return {
...attachment, ...attachment,
@ -619,12 +629,14 @@ export async function captureDimensionsAndScreenshot(
width, width,
height, height,
}, },
thumbnail: { thumbnail: thumbnail
...thumbnail, ? {
contentType: THUMBNAIL_CONTENT_TYPE, ...thumbnail,
width: THUMBNAIL_SIZE, contentType: THUMBNAIL_CONTENT_TYPE,
height: THUMBNAIL_SIZE, width: THUMBNAIL_SIZE,
}, height: THUMBNAIL_SIZE,
}
: undefined,
width, width,
height, height,
}; };
@ -1421,3 +1433,12 @@ export function partitionBodyAndNormalAttachments<
attachments: normalAttachments, attachments: normalAttachments,
}; };
} }
const MESSAGE_ATTACHMENT_TYPES_NEEDING_THUMBNAILS: Set<MessageAttachmentType> =
new Set(['attachment', 'sticker']);
export function shouldGenerateThumbnailForAttachmentType(
type: MessageAttachmentType
): boolean {
return MESSAGE_ATTACHMENT_TYPES_NEEDING_THUMBNAILS.has(type);
}

View file

@ -14,7 +14,7 @@ export enum MediaTier {
BACKUP = 'backup', BACKUP = 'backup',
} }
export const attachmentDownloadTypeSchema = z.enum([ export const messageAttachmentTypeSchema = z.enum([
'long-message', 'long-message',
'attachment', 'attachment',
'preview', 'preview',
@ -23,13 +23,11 @@ export const attachmentDownloadTypeSchema = z.enum([
'sticker', 'sticker',
]); ]);
export type AttachmentDownloadJobTypeType = z.infer< export type MessageAttachmentType = z.infer<typeof messageAttachmentTypeSchema>;
typeof attachmentDownloadTypeSchema
>;
export type CoreAttachmentDownloadJobType = { export type CoreAttachmentDownloadJobType = {
attachment: AttachmentType; attachment: AttachmentType;
attachmentType: AttachmentDownloadJobTypeType; attachmentType: MessageAttachmentType;
ciphertextSize: number; ciphertextSize: number;
contentType: MIMEType; contentType: MIMEType;
attachmentSignature: string; attachmentSignature: string;
@ -49,7 +47,7 @@ export const coreAttachmentDownloadJobSchema = z.object({
attachment: z attachment: z
.object({ size: z.number(), contentType: MIMETypeSchema }) .object({ size: z.number(), contentType: MIMETypeSchema })
.passthrough(), .passthrough(),
attachmentType: attachmentDownloadTypeSchema, attachmentType: messageAttachmentTypeSchema,
ciphertextSize: z.number(), ciphertextSize: z.number(),
contentType: MIMETypeSchema, contentType: MIMETypeSchema,
attachmentSignature: z.string(), attachmentSignature: z.string(),

View file

@ -16,6 +16,7 @@ import {
removeSchemaVersion, removeSchemaVersion,
replaceUnicodeOrderOverrides, replaceUnicodeOrderOverrides,
replaceUnicodeV2, replaceUnicodeV2,
shouldGenerateThumbnailForAttachmentType,
} from './Attachment.js'; } from './Attachment.js';
import type { MakeVideoScreenshotResultType } from './VisualAttachment.js'; import type { MakeVideoScreenshotResultType } from './VisualAttachment.js';
import * as Errors from './errors.js'; import * as Errors from './errors.js';
@ -48,6 +49,7 @@ import { encryptLegacyAttachment } from '../util/encryptLegacyAttachment.js';
import { deepClone } from '../util/deepClone.js'; import { deepClone } from '../util/deepClone.js';
import * as Bytes from '../Bytes.js'; import * as Bytes from '../Bytes.js';
import { isBodyTooLong } from '../util/longAttachment.js'; import { isBodyTooLong } from '../util/longAttachment.js';
import type { MessageAttachmentType } from './AttachmentDownload.js';
const { isFunction, isObject, identity } = lodash; const { isFunction, isObject, identity } = lodash;
@ -493,7 +495,13 @@ const toVersion7 = _withSchemaVersion({
const toVersion8 = _withSchemaVersion({ const toVersion8 = _withSchemaVersion({
schemaVersion: 8, schemaVersion: 8,
upgrade: _mapAttachments(captureDimensionsAndScreenshot), upgrade: _mapAttachments((attachment, context) =>
captureDimensionsAndScreenshot(
attachment,
{ generateThumbnail: true },
context
)
),
}); });
const toVersion9 = _withSchemaVersion({ const toVersion9 = _withSchemaVersion({
@ -768,6 +776,7 @@ export const upgradeSchema = async (
// downloaded out of band. // downloaded out of band.
export const processNewAttachment = async ( export const processNewAttachment = async (
attachment: AttachmentType, attachment: AttachmentType,
attachmentType: MessageAttachmentType,
{ {
writeNewAttachmentData, writeNewAttachmentData,
makeObjectUrl, makeObjectUrl,
@ -810,15 +819,22 @@ export const processNewAttachment = async (
throw new TypeError('context.logger is required'); throw new TypeError('context.logger is required');
} }
const finalAttachment = await captureDimensionsAndScreenshot(attachment, { const finalAttachment = await captureDimensionsAndScreenshot(
writeNewAttachmentData, attachment,
makeObjectUrl, {
revokeObjectUrl, generateThumbnail:
getImageDimensions, shouldGenerateThumbnailForAttachmentType(attachmentType),
makeImageThumbnail, },
makeVideoScreenshot, {
logger, writeNewAttachmentData,
}); makeObjectUrl,
revokeObjectUrl,
getImageDimensions,
makeImageThumbnail,
makeVideoScreenshot,
logger,
}
);
return finalAttachment; return finalAttachment;
}; };

View file

@ -73,7 +73,10 @@ export async function downloadOnboardingStory(): Promise<void> {
...local, ...local,
}; };
return window.Signal.Migrations.processNewAttachment(attachment); return window.Signal.Migrations.processNewAttachment(
attachment,
'attachment'
);
}) })
); );