Skip to content

Commit acce013

Browse files
committed
Integrate Original quality in attachment approval UI
AttachmentApprovalViewController: - Add 2-button quality selector (Standard | Original) - Read per-thread setting and default to Original when enabled - Show Original option in multi-recipient flows with subtitle 'Applies where enabled' - Set preserveMetadata flag when Original is selected - Accessibility support for quality control (VoiceOver adjustable) MediaItemViewController: - Fix preview to use attachmentStream.decryptedImage() - Ensures 'Save from preview' preserves EXIF metadata Flow integration: - Pass thread context to AttachmentApprovalViewController where available - Update SendMediaNavigationController, ConversationViewController, CameraFirstCaptureSendFlow, and SharingThreadPickerViewController The approval UI now respects per-thread Original quality settings and defaults the quality selector appropriately.
1 parent a33b738 commit acce013

File tree

6 files changed

+114
-21
lines changed

6 files changed

+114
-21
lines changed

Signal/ConversationView/ConversationViewController+ConversationInputToolbarDelegate.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -585,7 +585,8 @@ public extension ConversationViewController {
585585
hasQuotedReplyDraft: inputToolbar.quotedReplyDraft != nil,
586586
approvalDelegate: self,
587587
approvalDataSource: self,
588-
stickerSheetDelegate: self
588+
stickerSheetDelegate: self,
589+
thread: thread
589590
)
590591
modal.modalPresentationStyle = .overCurrentContext
591592
let presenter = self.splitViewController ?? self
@@ -998,6 +999,10 @@ extension ConversationViewController: SendMediaNavDataSource {
998999
func sendMediaNavMentionCacheInvalidationKey() -> String {
9991000
return thread.uniqueId
10001001
}
1002+
1003+
var sendMediaNavThread: TSThread? {
1004+
return thread
1005+
}
10011006
}
10021007

10031008
// MARK: - StickerPickerSheetDelegate

Signal/src/ViewControllers/CameraFirstCaptureSendFlow.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@ extension CameraFirstCaptureSendFlow: SendMediaNavDataSource {
125125
func sendMediaNavMentionCacheInvalidationKey() -> String {
126126
return "\(mentionCandidates.hashValue)"
127127
}
128+
129+
var sendMediaNavThread: TSThread? {
130+
// Camera first flow supports multiple recipients, so no single thread
131+
return nil
132+
}
128133
}
129134

130135
extension CameraFirstCaptureSendFlow: ConversationPickerDelegate {

Signal/src/ViewControllers/MediaGallery/MediaItemViewController.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,14 @@ class MediaItemViewController: OWSViewController, VideoPlaybackStatusProvider {
3333

3434
super.init()
3535

36-
image = attachmentStream.thumbnailImageSync(quality: .large)
36+
// Load full-quality image to preserve metadata when shared from preview
37+
// Use decryptedImage() which loads the original file, not a processed thumbnail
38+
image = try? attachmentStream.decryptedImage()
39+
40+
// Fall back to large thumbnail if decryptedImage fails (e.g., for very large images)
41+
if image == nil {
42+
image = attachmentStream.thumbnailImageSync(quality: .large)
43+
}
3744
}
3845

3946
deinit {

Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ protocol SendMediaNavDataSource: AnyObject {
3232
func sendMediaNavMentionableAddresses(tx: DBReadTransaction) -> [SignalServiceAddress]
3333

3434
func sendMediaNavMentionCacheInvalidationKey() -> String
35+
36+
var sendMediaNavThread: TSThread? { get }
3537
}
3638

3739
class CameraFirstCaptureNavigationController: SendMediaNavigationController {
@@ -213,7 +215,8 @@ class SendMediaNavigationController: OWSNavigationController {
213215
if hasQuotedReplyDraft {
214216
options.insert(.disallowViewOnce)
215217
}
216-
let approvalViewController = AttachmentApprovalViewController(options: options, attachmentApprovalItems: attachmentApprovalItems)
218+
let thread = sendMediaNavDataSource.sendMediaNavThread
219+
let approvalViewController = AttachmentApprovalViewController(options: options, attachmentApprovalItems: attachmentApprovalItems, thread: thread)
217220
approvalViewController.approvalDelegate = self
218221
approvalViewController.approvalDataSource = self
219222
approvalViewController.stickerSheetDelegate = self

SignalShareExtension/SharingThreadPickerViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ extension SharingThreadPickerViewController {
171171
if self.selection.conversations.contains(where: \.isStory) {
172172
approvalVCOptions.insert(.disallowViewOnce)
173173
}
174-
let approvalView = AttachmentApprovalViewController(options: approvalVCOptions, attachmentApprovalItems: approvalItems)
174+
let approvalView = AttachmentApprovalViewController(options: approvalVCOptions, attachmentApprovalItems: approvalItems, thread: nil)
175175
approvalVC = approvalView
176176
approvalView.approvalDelegate = self
177177
approvalView.approvalDataSource = self

SignalUI/AttachmentApproval/AttachmentApprovalViewController.swift

Lines changed: 90 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,25 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
9999
}
100100
}
101101

102-
lazy var outputQualityLevel: ImageQualityLevel = SSKEnvironment.shared.databaseStorageRef.read { .resolvedQuality(tx: $0) }
102+
lazy var outputQualityLevel: ImageQualityLevel = {
103+
SSKEnvironment.shared.databaseStorageRef.read { tx in
104+
// Check if there's a thread-specific quality setting
105+
if let thread = self.thread {
106+
let imageQualityStore = ImageQualitySettingStore()
107+
let resolvedLevel = imageQualityStore.resolvedQualityLevel(for: thread, tx: tx)
108+
// Enable metadata preservation for original quality mode
109+
if resolvedLevel == .original {
110+
self.attachmentApprovalItemCollection.attachmentApprovalItems.forEach { item in
111+
item.attachment.preserveMetadata = true
112+
}
113+
}
114+
return resolvedLevel
115+
}
116+
return .resolvedQuality(tx: tx)
117+
}
118+
}()
119+
120+
private let thread: TSThread?
103121

104122
public weak var approvalDelegate: AttachmentApprovalViewControllerDelegate?
105123
public weak var approvalDataSource: AttachmentApprovalViewControllerDataSource?
@@ -125,10 +143,11 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
125143
}
126144
}
127145

128-
public init(options: AttachmentApprovalViewControllerOptions, attachmentApprovalItems: [AttachmentApprovalItem]) {
146+
public init(options: AttachmentApprovalViewControllerOptions, attachmentApprovalItems: [AttachmentApprovalItem], thread: TSThread? = nil) {
129147
assert(attachmentApprovalItems.count > 0)
130148

131149
self.receivedOptions = options
150+
self.thread = thread
132151

133152
let pageOptions: [UIPageViewController.OptionsKey: Any] = [.interPageSpacing: kSpacingBetweenItems]
134153
super.init(transitionStyle: .scroll,
@@ -168,7 +187,8 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
168187
hasQuotedReplyDraft: Bool,
169188
approvalDelegate: AttachmentApprovalViewControllerDelegate,
170189
approvalDataSource: AttachmentApprovalViewControllerDataSource,
171-
stickerSheetDelegate: StickerPickerSheetDelegate?
190+
stickerSheetDelegate: StickerPickerSheetDelegate?,
191+
thread: TSThread? = nil
172192
) -> OWSNavigationController {
173193

174194
let attachmentApprovalItems = attachments.map { AttachmentApprovalItem(attachment: $0, canSave: false) }
@@ -177,7 +197,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
177197
if hasQuotedReplyDraft {
178198
options.insert(.disallowViewOnce)
179199
}
180-
let vc = AttachmentApprovalViewController(options: options, attachmentApprovalItems: attachmentApprovalItems)
200+
let vc = AttachmentApprovalViewController(options: options, attachmentApprovalItems: attachmentApprovalItems, thread: thread)
181201
// The data source needs to be set before the message body because it is needed to hydrate mentions.
182202
vc.approvalDataSource = approvalDataSource
183203
vc.setMessageBody(initialMessageBody, txProvider: DependenciesBridge.shared.db.readTxProvider)
@@ -1064,9 +1084,25 @@ extension AttachmentApprovalViewController {
10641084
let localPhoneNumber = tsAccountManager.localIdentifiersWithMaybeSneakyTransaction?.phoneNumber
10651085
let standardQualityLevel = ImageQualityLevel.remoteDefault(localPhoneNumber: localPhoneNumber)
10661086

1087+
// Determine if Original quality option should be shown
1088+
let threadSupportsOriginal: Bool
1089+
if let thread = thread {
1090+
// Single-recipient: show Original only if enabled for this specific thread
1091+
threadSupportsOriginal = SSKEnvironment.shared.databaseStorageRef.read { tx in
1092+
let imageQualityStore = ImageQualitySettingStore()
1093+
let setting = imageQualityStore.fetchSetting(for: thread, tx: tx)
1094+
return setting == .original
1095+
}
1096+
} else {
1097+
// Multi-recipient: always show Original option
1098+
// It will apply to threads that have it enabled, others will get High quality
1099+
threadSupportsOriginal = true
1100+
}
1101+
10671102
let selectionControl = MediaQualitySelectionControl(
10681103
standardQualityLevel: standardQualityLevel,
1069-
currentQualityLevel: outputQualityLevel
1104+
currentQualityLevel: outputQualityLevel,
1105+
supportsOriginal: threadSupportsOriginal
10701106
)
10711107
selectionControl.callback = { [weak self, weak actionSheet] qualityLevel in
10721108
self?.outputQualityLevel = qualityLevel
@@ -1118,14 +1154,24 @@ extension AttachmentApprovalViewController {
11181154
)
11191155
)
11201156

1157+
private let buttonQualityOriginal = MediaQualityButton(
1158+
title: ImageQualityLevel.original.localizedString,
1159+
subtitle: OWSLocalizedString(
1160+
"ATTACHMENT_APPROVAL_MEDIA_QUALITY_ORIGINAL_OPTION_SUBTITLE",
1161+
comment: "Subtitle for the 'original' option for media quality."
1162+
)
1163+
)
1164+
11211165
private let standardQualityLevel: ImageQualityLevel
1166+
private let supportsOriginal: Bool
11221167
private(set) var qualityLevel: ImageQualityLevel
11231168

11241169
var callback: ((ImageQualityLevel) -> Void)?
11251170

1126-
init(standardQualityLevel: ImageQualityLevel, currentQualityLevel: ImageQualityLevel) {
1171+
init(standardQualityLevel: ImageQualityLevel, currentQualityLevel: ImageQualityLevel, supportsOriginal: Bool = false) {
11271172
self.standardQualityLevel = standardQualityLevel
11281173
self.qualityLevel = currentQualityLevel
1174+
self.supportsOriginal = supportsOriginal
11291175

11301176
self.buttonQualityStandard = MediaQualityButton(
11311177
title: standardQualityLevel.localizedString,
@@ -1143,14 +1189,26 @@ extension AttachmentApprovalViewController {
11431189
addSubview(buttonQualityStandard)
11441190
buttonQualityStandard.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .trailing)
11451191

1146-
buttonQualityHigh.block = { [weak self] in
1147-
self?.didSelectQualityLevel(.high)
1192+
// Two-button layout: Always show Standard + either Original or High
1193+
if supportsOriginal {
1194+
// Standard | Original
1195+
buttonQualityOriginal.block = { [weak self] in
1196+
self?.didSelectQualityLevel(.original)
1197+
}
1198+
addSubview(buttonQualityOriginal)
1199+
buttonQualityOriginal.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .leading)
1200+
buttonQualityOriginal.autoPinEdge(.leading, to: .trailing, of: buttonQualityStandard, withOffset: 20)
1201+
buttonQualityOriginal.autoPinWidth(toWidthOf: buttonQualityStandard)
1202+
} else {
1203+
// Standard | High
1204+
buttonQualityHigh.block = { [weak self] in
1205+
self?.didSelectQualityLevel(.high)
1206+
}
1207+
addSubview(buttonQualityHigh)
1208+
buttonQualityHigh.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .leading)
1209+
buttonQualityHigh.autoPinEdge(.leading, to: .trailing, of: buttonQualityStandard, withOffset: 20)
1210+
buttonQualityHigh.autoPinWidth(toWidthOf: buttonQualityStandard)
11481211
}
1149-
addSubview(buttonQualityHigh)
1150-
buttonQualityHigh.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .leading)
1151-
1152-
buttonQualityHigh.autoPinWidth(toWidthOf: buttonQualityStandard)
1153-
buttonQualityHigh.autoPinEdge(.leading, to: .trailing, of: buttonQualityStandard, withOffset: 20)
11541212

11551213
updateButtonAppearance()
11561214
}
@@ -1168,6 +1226,7 @@ extension AttachmentApprovalViewController {
11681226
private func updateButtonAppearance() {
11691227
buttonQualityStandard.isSelected = qualityLevel == standardQualityLevel
11701228
buttonQualityHigh.isSelected = qualityLevel == .high
1229+
buttonQualityOriginal.isSelected = qualityLevel == .original
11711230
}
11721231

11731232
private class MediaQualityButton: OWSButton {
@@ -1200,6 +1259,11 @@ extension AttachmentApprovalViewController {
12001259
topLabel.text = title
12011260
bottomLabel.text = subtitle
12021261

1262+
// Parent MediaQualitySelectionControl handles accessibility
1263+
isAccessibilityElement = false
1264+
topLabel.isAccessibilityElement = false
1265+
bottomLabel.isAccessibilityElement = false
1266+
12031267
let stackView = UIStackView(arrangedSubviews: [ topLabel, bottomLabel ])
12041268
stackView.alignment = .center
12051269
stackView.axis = .vertical
@@ -1248,8 +1312,15 @@ extension AttachmentApprovalViewController {
12481312

12491313
override var accessibilityValue: String? {
12501314
get {
1251-
let selectedButton = qualityLevel == .high ? buttonQualityHigh : buttonQualityStandard
1252-
return [ selectedButton.topLabel, selectedButton.bottomLabel ].compactMap { $0.text }.joined(separator: ",")
1315+
let selectedButton: MediaQualityButton
1316+
if qualityLevel == .original {
1317+
selectedButton = buttonQualityOriginal
1318+
} else if qualityLevel == .high {
1319+
selectedButton = buttonQualityHigh
1320+
} else {
1321+
selectedButton = buttonQualityStandard
1322+
}
1323+
return [ selectedButton.topLabel, selectedButton.bottomLabel ].compactMap { $0.text }.joined(separator: ", ")
12531324
}
12541325
set { super.accessibilityValue = newValue }
12551326
}
@@ -1266,13 +1337,15 @@ extension AttachmentApprovalViewController {
12661337

12671338
override func accessibilityIncrement() {
12681339
if qualityLevel == standardQualityLevel {
1269-
qualityLevel = .high
1340+
// Increment to either Original or High depending on what's available
1341+
qualityLevel = supportsOriginal ? .original : .high
12701342
updateButtonAppearance()
12711343
}
12721344
}
12731345

12741346
override func accessibilityDecrement() {
1275-
if qualityLevel == .high {
1347+
if qualityLevel == .high || qualityLevel == .original {
1348+
// Decrement back to standard
12761349
qualityLevel = standardQualityLevel
12771350
updateButtonAppearance()
12781351
}

0 commit comments

Comments
 (0)