Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions Signal.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,8 @@
50F946102AD768AF002EF293 /* MockIdentityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50F9460F2AD768AF002EF293 /* MockIdentityManager.swift */; };
5AA002E62CA24566002D1CC2 /* SessionStoreTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AA002E52CA2455F002D1CC2 /* SessionStoreTest.swift */; };
616577F953D77424E32C7438 /* Pods_SignalUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 675486AB8F0612FF2C717BAE /* Pods_SignalUI.framework */; };
63BA1AD22EBA643E00B2AD82 /* ImageQualitySettingStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63BA1AD12EBA643E00B2AD82 /* ImageQualitySettingStore.swift */; };
63BA1AD42EBA651700B2AD82 /* ImageQualitySettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63BA1AD32EBA651700B2AD82 /* ImageQualitySettingsViewController.swift */; };
6600BB182BA3A04C0005A035 /* LinkPreviewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6600BB172BA3A04C0005A035 /* LinkPreviewManager.swift */; };
6600BB1A2BA3A0930005A035 /* LinkPreviewManagerImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6600BB192BA3A0930005A035 /* LinkPreviewManagerImpl.swift */; };
6600BB1D2BA3ABDD0005A035 /* MockLinkPreviewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6600BB1C2BA3ABDD0005A035 /* MockLinkPreviewManager.swift */; };
Expand Down Expand Up @@ -4741,6 +4743,8 @@
5AA002E52CA2455F002D1CC2 /* SessionStoreTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionStoreTest.swift; sourceTree = "<group>"; };
5D6C4583F668E9D733E59B9B /* Pods-SignalServiceKitTests.testable release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalServiceKitTests.testable release.xcconfig"; path = "Target Support Files/Pods-SignalServiceKitTests/Pods-SignalServiceKitTests.testable release.xcconfig"; sourceTree = "<group>"; };
5F85041386A219C9710EAB41 /* Pods-Signal.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Signal.debug.xcconfig"; path = "Target Support Files/Pods-Signal/Pods-Signal.debug.xcconfig"; sourceTree = "<group>"; };
63BA1AD12EBA643E00B2AD82 /* ImageQualitySettingStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageQualitySettingStore.swift; sourceTree = "<group>"; };
63BA1AD32EBA651700B2AD82 /* ImageQualitySettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageQualitySettingsViewController.swift; sourceTree = "<group>"; };
65703441A3D2C7FE670E65ED /* Pods-SignalServiceKit.profiling.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalServiceKit.profiling.xcconfig"; path = "Target Support Files/Pods-SignalServiceKit/Pods-SignalServiceKit.profiling.xcconfig"; sourceTree = "<group>"; };
6600BB172BA3A04C0005A035 /* LinkPreviewManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewManager.swift; sourceTree = "<group>"; };
6600BB192BA3A0930005A035 /* LinkPreviewManagerImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewManagerImpl.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -8062,9 +8066,9 @@
B9B2AA932BC598B60060B56C /* ContactNoteSheet.swift */,
34E20D4B24256563002C011E /* ConversationHeaderBuilder.swift */,
34EB0DF42628D3B200B62DC3 /* ConversationInternalViewController.swift */,
34235F3724213550008C74CB /* ConversationSettingsViewController.swift */,
34E20D4D2425672A002C011E /* ConversationSettingsViewController+Contents.swift */,
34A17D80253F7236009F8C02 /* ConversationSettingsViewController+LegacyGroups.swift */,
34235F3724213550008C74CB /* ConversationSettingsViewController.swift */,
D900E6FE2DE143E9004D01A1 /* DisappearingMessagesCustomTimePickerViewController.swift */,
D900E6F72DE1048A004D01A1 /* DisappearingMessagesTimerSettingsViewController.swift */,
346594812434D5E000E5C510 /* GroupAttributesEditorHelper.swift */,
Expand All @@ -8075,8 +8079,9 @@
347B83FC24378DDE0019A52C /* GroupMemberRequestsAndInvitesViewController.swift */,
889DFA0F264EE76F00D03921 /* GroupNameViewController.swift */,
88BE44A72615451A00AE8E33 /* GroupPermissionsSettingsViewController.swift */,
347B83F624367EC00019A52C /* GroupViewHelper+MemberActionSheet.swift */,
347B83F82436820C0019A52C /* GroupViewHelper.swift */,
347B83F624367EC00019A52C /* GroupViewHelper+MemberActionSheet.swift */,
63BA1AD32EBA651700B2AD82 /* ImageQualitySettingsViewController.swift */,
88A357B823639384009D6B9A /* MemberActionSheet.swift */,
32CBF07C258C939800D56903 /* NameCollisionResolutionViewController.swift */,
B9D65E522BAE1DA70067322A /* NicknameEditorViewController.swift */,
Expand Down Expand Up @@ -8961,6 +8966,7 @@
children = (
66F6D6A12C7D0CA100EFAF75 /* Models */,
500AEE062A4DF48700371F05 /* ChatColorSettingStore.swift */,
63BA1AD12EBA643E00B2AD82 /* ImageQualitySettingStore.swift */,
66144B372BF8155F00E2C9CD /* MockWallpaperImageStore.swift */,
66144B2E2BF7FB5200E2C9CD /* WallpaperImageStore.swift */,
66144B302BF7FB7B00E2C9CD /* WallpaperImageStoreImpl.swift */,
Expand Down Expand Up @@ -17494,6 +17500,7 @@
34B6A905218B4C91007C4606 /* TypingIndicatorInteraction.swift in Sources */,
34B6A903218B3F63007C4606 /* TypingIndicatorView.swift in Sources */,
342FFE77271EF581000AC89F /* UIApplication+OWS.swift in Sources */,
63BA1AD42EBA651700B2AD82 /* ImageQualitySettingsViewController.swift in Sources */,
F93BCB9A29EDE86400E3C6A0 /* UIDevice+CanUpgradeOperatingSystem.swift in Sources */,
342FFE7A271EF581000AC89F /* UIResponder+OWS.swift in Sources */,
342FFE7B271EF581000AC89F /* UIStoryboard+OWS.swift in Sources */,
Expand Down Expand Up @@ -17763,6 +17770,7 @@
66CD256A2B06963200139E17 /* BackupArchiveStickerPackArchiver.swift in Sources */,
66232ADB2CB9E33600AE6A76 /* BackupArchiveStoryStore.swift in Sources */,
D9BFB8BF2C4EE33C00D67881 /* BackupArchiveThreadMergeChatUpdateArchiver.swift in Sources */,
63BA1AD22EBA643E00B2AD82 /* ImageQualitySettingStore.swift in Sources */,
66232AD92CB9D00400AE6A76 /* BackupArchiveThreadStore.swift in Sources */,
66CD258F2B0EB4AC00139E17 /* BackupArchiveTSIncomingMessageArchiver.swift in Sources */,
66CD25932B0EC55100139E17 /* BackupArchiveTSMessageContentsArchiver.swift in Sources */,
Expand Down Expand Up @@ -18064,6 +18072,7 @@
D9F399AD2A95798A001599EC /* IdentityKeyChecker.swift in Sources */,
D9F399B22A96D65D001599EC /* IdentityKeyMismatchManager.swift in Sources */,
661BFE0A2C07FB950065435B /* ImageMetadata.swift in Sources */,
63BA1AD22EBA643E00B2AD82 /* ImageQualitySettingStore.swift in Sources */,
F9C5CDDF289453B400548EEE /* ImageQuality.swift in Sources */,
D9C0AE662BD7103100FCB05E /* InactiveLinkedDeviceFinder.swift in Sources */,
04BBBE922E26C92D00E914B1 /* InactivePrimaryDeviceStore.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -579,7 +579,8 @@ public extension ConversationViewController {
hasQuotedReplyDraft: inputToolbar.quotedReplyDraft != nil,
approvalDelegate: self,
approvalDataSource: self,
stickerSheetDelegate: self
stickerSheetDelegate: self,
thread: thread
)
modal.modalPresentationStyle = .overCurrentContext
let presenter = self.splitViewController ?? self
Expand Down Expand Up @@ -982,6 +983,10 @@ extension ConversationViewController: SendMediaNavDataSource {
func sendMediaNavMentionCacheInvalidationKey() -> String {
return thread.uniqueId
}

var sendMediaNavThread: TSThread? {
return thread
}
}

// MARK: - StickerPickerSheetDelegate
Expand Down
5 changes: 5 additions & 0 deletions Signal/src/ViewControllers/CameraFirstCaptureSendFlow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ extension CameraFirstCaptureSendFlow: SendMediaNavDataSource {
func sendMediaNavMentionCacheInvalidationKey() -> String {
return "\(mentionCandidates.hashValue)"
}

var sendMediaNavThread: TSThread? {
// Camera first flow supports multiple recipients, so no single thread
return nil
}
}

extension CameraFirstCaptureSendFlow: ConversationPickerDelegate {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,14 @@ class MediaItemViewController: OWSViewController, VideoPlaybackStatusProvider {

super.init()

image = attachmentStream.thumbnailImageSync(quality: .large)
// Load full-quality image to preserve metadata when shared from preview
// Use decryptedImage() which loads the original file, not a processed thumbnail
image = try? attachmentStream.decryptedImage()

// Fall back to large thumbnail if decryptedImage fails (e.g., for very large images)
if image == nil {
image = attachmentStream.thumbnailImageSync(quality: .large)
}
}

deinit {
Expand Down
27 changes: 14 additions & 13 deletions Signal/src/ViewControllers/Photos/PhotoLibrary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,21 +97,22 @@ class PhotoAlbumContents {

private func requestImageDataSource(for asset: PHAsset) -> Promise<(dataSource: DataSource, dataUTI: String)> {
return Promise { future in
// Use PHAssetResourceManager to get original file bytes with metadata intact
guard let resource = PHAssetResource.assetResources(for: asset).first else {
future.reject(PhotoLibraryError.assertionError(description: "No asset resource found"))
return
}

let options: PHImageRequestOptions = PHImageRequestOptions()
let dataUTI = resource.uniformTypeIdentifier
var imageData = Data()
let options = PHAssetResourceRequestOptions()
options.isNetworkAccessAllowed = true
options.version = .current
options.deliveryMode = .highQualityFormat

_ = imageManager.requestImageDataAndOrientation(for: asset, options: options) { imageData, dataUTI, _, _ in

guard let imageData = imageData else {
future.reject(PhotoLibraryError.assertionError(description: "imageData was unexpectedly nil"))
return
}

guard let dataUTI = dataUTI else {
future.reject(PhotoLibraryError.assertionError(description: "dataUTI was unexpectedly nil"))
PHAssetResourceManager.default().requestData(for: resource, options: options, dataReceivedHandler: { data in
imageData.append(data)
}, completionHandler: { error in
if let error = error {
future.reject(error)
return
}

Expand All @@ -121,7 +122,7 @@ class PhotoAlbumContents {
}

future.resolve((dataSource: dataSource, dataUTI: dataUTI))
}
})
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ protocol SendMediaNavDataSource: AnyObject {
func sendMediaNavMentionableAcis(tx: DBReadTransaction) -> [Aci]

func sendMediaNavMentionCacheInvalidationKey() -> String

var sendMediaNavThread: TSThread? { get }
}

class CameraFirstCaptureNavigationController: SendMediaNavigationController {
Expand Down Expand Up @@ -214,7 +216,8 @@ class SendMediaNavigationController: OWSNavigationController {
if hasQuotedReplyDraft {
options.insert(.disallowViewOnce)
}
let approvalViewController = AttachmentApprovalViewController(options: options, attachmentApprovalItems: attachmentApprovalItems)
let thread = sendMediaNavDataSource.sendMediaNavThread
let approvalViewController = AttachmentApprovalViewController(options: options, attachmentApprovalItems: attachmentApprovalItems, thread: thread)
approvalViewController.approvalDelegate = self
approvalViewController.approvalDataSource = self
approvalViewController.stickerSheetDelegate = self
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ extension ConversationSettingsViewController {
addDisappearingMessagesItem(to: mainSection)
addNicknameItemIfNecessary(to: mainSection)
addColorAndWallpaperSettingsItem(to: mainSection)
addImageQualitySettingsItem(to: mainSection)
if !isNoteToSelf { addSoundAndNotificationSettingsItem(to: mainSection) }
addSafetyNumberItemIfNecessary(to: mainSection)

Expand Down Expand Up @@ -436,6 +437,46 @@ extension ConversationSettingsViewController {
}))
}

private func addImageQualitySettingsItem(to section: OWSTableSection) {
section.add(OWSTableItem(customCellBlock: { [weak self] in
guard let self = self else {
owsFailDebug("Missing self")
return OWSTableItem.newCell()
}

let imageQualityStore = ImageQualitySettingStore()
let settingText = SSKEnvironment.shared.databaseStorageRef.read { tx in
let setting = imageQualityStore.fetchSetting(for: self.thread, tx: tx)
if setting == .original {
return OWSLocalizedString(
"IMAGE_QUALITY_ORIGINAL",
comment: "Option for original image quality"
)
} else {
return OWSLocalizedString(
"IMAGE_QUALITY_DEFAULT",
comment: "Option to use default image quality setting"
)
}
}

let cell = OWSTableItem.buildCell(
icon: .buttonPhotoLibrary,
itemName: OWSLocalizedString(
"IMAGE_QUALITY_SETTING_TITLE",
comment: "The title for the image quality settings screen"
),
accessoryText: settingText,
accessoryType: .disclosureIndicator,
accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "image_quality")
)
return cell
},
actionBlock: { [weak self] in
self?.showImageQualitySettingsView()
}))
}

private func addSoundAndNotificationSettingsItem(to section: OWSTableSection) {
section.add(OWSTableItem(customCellBlock: { [weak self] in
guard let self = self else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,13 @@ class ConversationSettingsViewController: OWSTableViewController2, BadgeCollecti
navigationController?.pushViewController(vc, animated: true)
}

func showImageQualitySettingsView() {
let vc = ImageQualitySettingsViewController(thread: thread) { [weak self] in
self?.updateTableContents()
}
navigationController?.pushViewController(vc, animated: true)
}

func showSoundAndNotificationsSettingsView() {
let vc = SoundAndNotificationsSettingsViewController(threadViewModel: threadViewModel)
navigationController?.pushViewController(vc, animated: true)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import SignalServiceKit
import SignalUI

class ImageQualitySettingsViewController: OWSTableViewController2 {
private let thread: TSThread
private let imageQualitySettingStore: ImageQualitySettingStore
private let completion: () -> Void

private var initialSetting: ImageQualitySetting!
private var currentSetting: ImageQualitySetting!

init(thread: TSThread,
imageQualitySettingStore: ImageQualitySettingStore = ImageQualitySettingStore(),
completion: @escaping () -> Void) {
self.thread = thread
self.imageQualitySettingStore = imageQualitySettingStore
self.completion = completion
super.init()
}

override func viewDidLoad() {
super.viewDidLoad()

title = OWSLocalizedString(
"IMAGE_QUALITY_SETTING_TITLE",
comment: "The title for the image quality settings screen"
)
OWSTableViewController2.removeBackButtonText(viewController: self)

SSKEnvironment.shared.databaseStorageRef.read { tx in
let setting = imageQualitySettingStore.fetchSetting(for: thread, tx: tx)
initialSetting = setting
currentSetting = setting
}

updateTableContents()
}

func updateTableContents() {
let contents = OWSTableContents()
defer { self.contents = contents }

let section = OWSTableSection()

// Add toggle switch for Full Size
section.add(OWSTableItem(
customCellBlock: { [weak self] in
guard let self = self else {
return OWSTableItem.newCell()
}

let cell = OWSTableItem.newCell()
cell.selectionStyle = .none

let label = UILabel()
label.text = OWSLocalizedString(
"IMAGE_QUALITY_ORIGINAL_TOGGLE",
comment: "Toggle label for sending original quality images"
)
label.font = .dynamicTypeBody
label.textColor = Theme.primaryTextColor

let switchControl = UISwitch()
switchControl.isOn = (self.currentSetting == .original)
switchControl.addTarget(self, action: #selector(self.didToggleOriginal(_:)), for: .valueChanged)

let stackView = UIStackView(arrangedSubviews: [label, switchControl])
stackView.axis = .horizontal
stackView.alignment = .center
stackView.spacing = 12

cell.contentView.addSubview(stackView)
stackView.autoPinEdgesToSuperviewMargins()

return cell
}
))

section.footerTitle = OWSLocalizedString(
"IMAGE_QUALITY_ORIGINAL_WARNING",
comment: "Warning message about original quality images preserving metadata and location"
)

contents.add(section)
}

@objc
private func didToggleOriginal(_ sender: UISwitch) {
let newSetting: ImageQualitySetting = sender.isOn ? .original : .default
if newSetting != currentSetting {
currentSetting = newSetting
SSKEnvironment.shared.databaseStorageRef.write { tx in
self.imageQualitySettingStore.setSetting(newSetting, for: self.thread, tx: tx)
}
completion()
}
}
}

21 changes: 21 additions & 0 deletions Signal/translations/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,9 @@
/* toast alert shown after user taps the 'save' button */
"ATTACHMENT_APPROVAL_MEDIA_DID_SAVE" = "Saved";

/* Subtitle for the 'original' option for media quality. */
"ATTACHMENT_APPROVAL_MEDIA_QUALITY_ORIGINAL_OPTION_SUBTITLE" = "With all metadata";

/* Subtitle for the 'high' option for media quality. */
"ATTACHMENT_APPROVAL_MEDIA_QUALITY_HIGH_OPTION_SUBTITLE" = "Slower, more data";

Expand Down Expand Up @@ -7540,9 +7543,27 @@
/* String describing high quality sent media */
"SENT_MEDIA_QUALITY_HIGH" = "High";

/* String describing original quality sent media */
"SENT_MEDIA_QUALITY_ORIGINAL" = "Original";

/* String describing standard quality sent media */
"SENT_MEDIA_QUALITY_STANDARD" = "Standard";

/* The title for the image quality settings screen */
"IMAGE_QUALITY_SETTING_TITLE" = "Image Quality";

/* Warning message about original quality images preserving metadata */
"IMAGE_QUALITY_ORIGINAL_WARNING" = "Sending images with Original Quality preserves locations and other metadata. Be aware that your location can be inadvertently shared through the photos you send.";

/* Option for original image quality */
"IMAGE_QUALITY_ORIGINAL" = "Original Quality";

/* Toggle label for sending original quality images */
"IMAGE_QUALITY_ORIGINAL_TOGGLE" = "Send Original Quality";

/* Option to use default image quality setting */
"IMAGE_QUALITY_DEFAULT" = "Default";

/* Users can donate to Signal with a bank account. This is the label for email field on that screen. */
"SEPA_DONATION_EMAIL_LABEL" = "Email";

Expand Down
Loading