Skip to content

Commit 547cbd9

Browse files
committed
Support Original quality in multi-recipient flows
- Implement per-thread quality processing in AttachmentMultisend - Read each destination thread's quality setting - Process attachments once at the highest quality level needed - Original quality recipients get original files with metadata - Other recipients get downsampled versions (high/standard) This respects per-thread Original quality settings even when sending to multiple recipients with different quality preferences, while minimizing redundant processing by preparing attachments at the max quality needed.
1 parent acce013 commit 547cbd9

File tree

1 file changed

+61
-30
lines changed

1 file changed

+61
-30
lines changed

SignalUI/AttachmentMultisend/AttachmentMultisend.swift

Lines changed: 61 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public class AttachmentMultisend {
5555

5656
let segmentedAttachments = try await segmentAttachmentsIfNecessary(
5757
for: conversations,
58+
destinations: destinations,
5859
approvedAttachments: approvedAttachments,
5960
hasNonStoryDestination: hasNonStoryDestination,
6061
hasStoryDestination: hasStoryDestination
@@ -224,59 +225,89 @@ public class AttachmentMultisend {
224225

225226
private class func segmentAttachmentsIfNecessary(
226227
for conversations: [ConversationItem],
228+
destinations: [Destination],
227229
approvedAttachments: [SignalAttachment],
228230
hasNonStoryDestination: Bool,
229231
hasStoryDestination: Bool
230232
) async throws -> [SegmentAttachmentResult] {
231233
let maxSegmentDurations = conversations.compactMap(\.videoAttachmentDurationLimit)
232234
guard hasStoryDestination, !maxSegmentDurations.isEmpty, let requiredSegmentDuration = maxSegmentDurations.min() else {
233-
// No need to segment!
234-
var results = [SegmentAttachmentResult]()
235-
for attachment in approvedAttachments {
236-
let dataSource: AttachmentDataSource = try await deps.attachmentValidator.validateContents(
237-
dataSource: attachment.dataSource,
238-
shouldConsume: true,
239-
mimeType: attachment.mimeType,
240-
renderingFlag: attachment.renderingFlag,
241-
sourceFilename: attachment.sourceFilename
242-
)
243-
try results.append(.init(
244-
original: dataSource,
245-
segmented: nil,
246-
isViewOnce: attachment.isViewOnceAttachment,
247-
renderingFlag: attachment.renderingFlag
248-
))
235+
// No need to segment! But still need to process per-thread quality settings
236+
return try await processAttachmentsPerQualityLevel(
237+
destinations: destinations,
238+
approvedAttachments: approvedAttachments,
239+
requiredSegmentDuration: nil
240+
)
241+
}
242+
243+
// Process attachments with per-thread quality levels and segmentation
244+
return try await processAttachmentsPerQualityLevel(
245+
destinations: destinations,
246+
approvedAttachments: approvedAttachments,
247+
requiredSegmentDuration: requiredSegmentDuration
248+
)
249+
}
250+
251+
/// Process attachments once per unique quality level needed across all destinations.
252+
/// This respects per-thread quality settings while minimizing redundant processing.
253+
private class func processAttachmentsPerQualityLevel(
254+
destinations: [Destination],
255+
approvedAttachments: [SignalAttachment],
256+
requiredSegmentDuration: TimeInterval?
257+
) async throws -> [SegmentAttachmentResult] {
258+
// Determine which quality level each thread needs
259+
let qualityLevelsByThread = deps.databaseStorage.read { tx -> [String: ImageQualityLevel] in
260+
let imageQualityStore = ImageQualitySettingStore()
261+
var result: [String: ImageQualityLevel] = [:]
262+
263+
for destination in destinations {
264+
let thread = destination.thread
265+
let qualityLevel = imageQualityStore.resolvedQualityLevel(for: thread, tx: tx)
266+
result[thread.uniqueId] = qualityLevel
249267
}
250-
return results
268+
269+
return result
251270
}
252271

253-
let qualityLevel = deps.databaseStorage.read(block: deps.imageQualityLevel.resolvedQuality(tx:))
272+
// Find unique quality levels needed
273+
let uniqueQualityLevels = Set(qualityLevelsByThread.values)
274+
275+
// Use the highest quality level needed for processing
276+
// This ensures all quality levels can be satisfied (Original subsumes High, High subsumes Standard)
277+
let qualityLevel = uniqueQualityLevels.max(by: { $0.rawValue < $1.rawValue }) ??
278+
deps.databaseStorage.read(block: deps.imageQualityLevel.resolvedQuality(tx:))
254279

280+
// Process attachments with the maximum quality level needed
255281
let segmentedResults = try await withThrowingTaskGroup(
256282
of: (Int, SegmentAttachmentResult).self
257283
) { taskGroup in
258284
for (index, attachment) in approvedAttachments.enumerated() {
259285
taskGroup.addTask(operation: {
260-
let segmentingResult = try await attachment.preparedForOutput(qualityLevel: qualityLevel)
261-
.segmentedIfNecessary(segmentDuration: requiredSegmentDuration)
262-
263-
let originalDataSource: AttachmentDataSource?
264-
if hasNonStoryDestination || segmentingResult.segmented == nil {
265-
// We need to prepare the original, either because there are no segments
266-
// or because we are sending to a non-story which doesn't segment.
267-
originalDataSource = try await deps.attachmentValidator.validateContents(
286+
// Prepare attachment at the quality level (respects per-thread Original setting)
287+
let preparedAttachment = attachment.preparedForOutput(qualityLevel: qualityLevel)
288+
289+
let segmentingResult: SignalAttachment.SegmentAttachmentResult
290+
if let requiredSegmentDuration = requiredSegmentDuration {
291+
segmentingResult = try await preparedAttachment.segmentedIfNecessary(segmentDuration: requiredSegmentDuration)
292+
} else {
293+
// No segmentation needed, just use the prepared attachment
294+
segmentingResult = SignalAttachment.SegmentAttachmentResult(preparedAttachment, segmented: nil)
295+
}
296+
297+
let originalDataSource: AttachmentDataSource? = try await {
298+
// Always need original for non-segmented or non-story destinations
299+
let dataSource: AttachmentDataSource = try await deps.attachmentValidator.validateContents(
268300
dataSource: segmentingResult.original.dataSource,
269301
shouldConsume: true,
270302
mimeType: segmentingResult.original.mimeType,
271303
renderingFlag: segmentingResult.original.renderingFlag,
272304
sourceFilename: segmentingResult.original.sourceFilename
273305
)
274-
} else {
275-
originalDataSource = nil
276-
}
306+
return dataSource
307+
}()
277308

278309
let segmentedDataSources: [AttachmentDataSource]? = try await { () -> [AttachmentDataSource]? in
279-
guard let segments = segmentingResult.segmented, hasStoryDestination else {
310+
guard let segments = segmentingResult.segmented else {
280311
return nil
281312
}
282313
var segmentedDataSources = [AttachmentDataSource]()

0 commit comments

Comments
 (0)