Require ImageQuality when sending all attachments
This commit is contained in:
parent
4f541fc61b
commit
e4ceece74b
@ -397,8 +397,9 @@ extension ConversationViewController: ConversationInputToolbarDelegate {
|
||||
}
|
||||
|
||||
let imageQuality = approvedAttachments.imageQuality
|
||||
let imageQualityLevel = ImageQualityLevel.resolvedValue(imageQuality: imageQuality)
|
||||
let sendableAttachments = try await approvedAttachments.attachments.mapAsync {
|
||||
return try await SendableAttachment.forPreviewableAttachment($0, imageQuality: imageQuality)
|
||||
return try await SendableAttachment.forPreviewableAttachment($0, imageQualityLevel: imageQualityLevel)
|
||||
}
|
||||
|
||||
if self.isBlockedConversation() {
|
||||
|
||||
@ -253,10 +253,9 @@ extension ConversationViewController: ConversationInputTextViewDelegate {
|
||||
// If the thing we pasted is sticker-like, send it immediately
|
||||
// and render it borderless.
|
||||
if attachments.count == 1, let a = attachments.first, a.rawValue.isBorderless {
|
||||
// [15M] TODO: We should compress images here to ensure they're valid.
|
||||
Task {
|
||||
await self.sendAttachments(
|
||||
ApprovedAttachments(nonViewOnceAttachments: [a], imageQuality: nil),
|
||||
ApprovedAttachments(nonViewOnceAttachments: [a], imageQuality: .standard),
|
||||
messageBody: nil,
|
||||
from: self,
|
||||
)
|
||||
|
||||
@ -109,7 +109,7 @@ extension ConversationViewController {
|
||||
let attachment = try voiceMemoDraft.prepareAttachment()
|
||||
Task { @MainActor in
|
||||
await self.sendAttachments(
|
||||
ApprovedAttachments(nonViewOnceAttachments: [attachment], imageQuality: nil),
|
||||
ApprovedAttachments(nonViewOnceAttachments: [attachment], imageQuality: .standard),
|
||||
messageBody: nil,
|
||||
from: self,
|
||||
)
|
||||
|
||||
@ -106,7 +106,7 @@ class DataSettingsTableViewController: OWSTableViewController2 {
|
||||
"SETTINGS_DATA_SENT_MEDIA_QUALITY_ITEM_TITLE",
|
||||
comment: "Item title for the sent media quality setting"
|
||||
),
|
||||
accessoryText: SSKEnvironment.shared.databaseStorageRef.read(block: ImageQualityLevel.resolvedQuality(tx:)).localizedString,
|
||||
accessoryText: SSKEnvironment.shared.databaseStorageRef.read(block: ImageQuality.fetchValue(tx:)).localizedString,
|
||||
actionBlock: { [weak self] in
|
||||
self?.showSentMediaQualityPreferences()
|
||||
}
|
||||
@ -170,11 +170,10 @@ class DataSettingsTableViewController: OWSTableViewController2 {
|
||||
}
|
||||
|
||||
private func showSentMediaQualityPreferences() {
|
||||
let vc = SentMediaQualitySettingsViewController { [weak self] isHighQuality in
|
||||
let vc = SentMediaQualitySettingsViewController.loadWithSneakyTransaction { [weak self] imageQuality in
|
||||
guard let self else { return }
|
||||
SSKEnvironment.shared.databaseStorageRef.write { tx in
|
||||
ImageQualityLevel.setUserSelectedHighQuality(isHighQuality, tx: tx)
|
||||
}
|
||||
let databaseStorage = SSKEnvironment.shared.databaseStorageRef
|
||||
databaseStorage.write { tx in ImageQuality.setValue(imageQuality, tx: tx) }
|
||||
self.updateTableContents()
|
||||
}
|
||||
navigationController?.pushViewController(vc, animated: true)
|
||||
|
||||
@ -6,16 +6,27 @@
|
||||
import SignalServiceKit
|
||||
import SignalUI
|
||||
|
||||
class SentMediaQualitySettingsViewController: OWSTableViewController2 {
|
||||
private let updateHandler: (_ isHighQuality: Bool) -> Void
|
||||
init(updateHandler: @escaping (_ isHighQuality: Bool) -> Void) {
|
||||
final class SentMediaQualitySettingsViewController: OWSTableViewController2 {
|
||||
private var imageQuality: ImageQuality
|
||||
private let updateHandler: (_ imageQuality: ImageQuality) -> Void
|
||||
|
||||
static func loadWithSneakyTransaction(updateHandler: @escaping (_ imageQuality: ImageQuality) -> Void) -> Self {
|
||||
let databaseStorage = SSKEnvironment.shared.databaseStorageRef
|
||||
return Self(
|
||||
imageQuality: databaseStorage.read(block: ImageQuality.fetchValue(tx:)),
|
||||
updateHandler: updateHandler,
|
||||
)
|
||||
}
|
||||
|
||||
init(
|
||||
imageQuality: ImageQuality,
|
||||
updateHandler: @escaping (_ imageQuality: ImageQuality) -> Void,
|
||||
) {
|
||||
self.imageQuality = imageQuality
|
||||
self.updateHandler = updateHandler
|
||||
super.init()
|
||||
}
|
||||
|
||||
private var remoteDefaultLevel: ImageQualityLevel!
|
||||
private var currentQualityLevel: ImageQualityLevel!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
@ -24,13 +35,6 @@ class SentMediaQualitySettingsViewController: OWSTableViewController2 {
|
||||
comment: "Item title for the sent media quality setting"
|
||||
)
|
||||
|
||||
SSKEnvironment.shared.databaseStorageRef.read { tx in
|
||||
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
|
||||
let localPhoneNumber = tsAccountManager.localIdentifiers(tx: tx)?.phoneNumber
|
||||
remoteDefaultLevel = ImageQualityLevel.remoteDefault(localPhoneNumber: localPhoneNumber)
|
||||
currentQualityLevel = ImageQualityLevel.resolvedQuality(tx: tx)
|
||||
}
|
||||
|
||||
updateTableContents()
|
||||
}
|
||||
|
||||
@ -48,24 +52,24 @@ class SentMediaQualitySettingsViewController: OWSTableViewController2 {
|
||||
comment: "The footer for the photos and videos section in the sent media quality settings."
|
||||
)
|
||||
|
||||
section.add(qualityItem(remoteDefaultLevel))
|
||||
section.add(qualityItem(.standard))
|
||||
section.add(qualityItem(.high))
|
||||
|
||||
contents.add(section)
|
||||
}
|
||||
|
||||
func qualityItem(_ level: ImageQualityLevel) -> OWSTableItem {
|
||||
func qualityItem(_ imageQuality: ImageQuality) -> OWSTableItem {
|
||||
return OWSTableItem(
|
||||
text: level.localizedString,
|
||||
text: imageQuality.localizedString,
|
||||
actionBlock: { [weak self] in
|
||||
self?.changeLevel(isHighQuality: level == .high)
|
||||
self?.changeImageQuality(imageQuality)
|
||||
},
|
||||
accessoryType: currentQualityLevel == level ? .checkmark : .none
|
||||
accessoryType: self.imageQuality == imageQuality ? .checkmark : .none
|
||||
)
|
||||
}
|
||||
|
||||
func changeLevel(isHighQuality: Bool) {
|
||||
updateHandler(isHighQuality)
|
||||
func changeImageQuality(_ imageQuality: ImageQuality) {
|
||||
updateHandler(imageQuality)
|
||||
navigationController?.popViewController(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
@ -345,7 +345,7 @@ extension ForwardMessageViewController {
|
||||
_ = try await AttachmentMultisend.enqueueApprovedMedia(
|
||||
conversations: conversations,
|
||||
approvedMessageBody: item.messageBody,
|
||||
approvedAttachments: ApprovedAttachments(nonViewOnceAttachments: item.attachments, imageQuality: nil),
|
||||
approvedAttachments: ApprovedAttachments(nonViewOnceAttachments: item.attachments, imageQuality: .high),
|
||||
)
|
||||
} else if let textAttachment = item.textAttachment {
|
||||
// TODO: we want to reuse the uploaded link preview image attachment instead of re-uploading
|
||||
|
||||
@ -35,9 +35,9 @@ extension GifPickerNavigationViewController: GifPickerViewControllerDelegate {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
let attachmentApprovalItem = AttachmentApprovalItem(attachment: attachment, canSave: false)
|
||||
let attachmentApproval = AttachmentApprovalViewController(
|
||||
options: self.hasQuotedReplyDraft ? [.disallowViewOnce] : [],
|
||||
let attachmentApproval = AttachmentApprovalViewController.loadWithSneakyTransaction(
|
||||
attachmentApprovalItems: [attachmentApprovalItem],
|
||||
options: self.hasQuotedReplyDraft ? [.disallowViewOnce] : [],
|
||||
)
|
||||
attachmentApproval.approvalDataSource = self
|
||||
attachmentApproval.setMessageBody(initialMessageBody, txProvider: DependenciesBridge.shared.db.readTxProvider)
|
||||
|
||||
@ -484,7 +484,10 @@ public class Location: NSObject {
|
||||
guard let dataSource else {
|
||||
throw SignalAttachmentError.missingData
|
||||
}
|
||||
return try await SendableAttachment.forPreviewableAttachment(PreviewableAttachment(rawValue: SignalAttachment.imageAttachment(dataSource: dataSource, dataUTI: UTType.jpeg.identifier)))
|
||||
return try await SendableAttachment.forPreviewableAttachment(
|
||||
PreviewableAttachment(rawValue: SignalAttachment.imageAttachment(dataSource: dataSource, dataUTI: UTType.jpeg.identifier)),
|
||||
imageQualityLevel: .one,
|
||||
)
|
||||
}
|
||||
|
||||
public var messageText: String {
|
||||
|
||||
@ -206,7 +206,10 @@ class SendMediaNavigationController: OWSNavigationController {
|
||||
if hasQuotedReplyDraft {
|
||||
options.insert(.disallowViewOnce)
|
||||
}
|
||||
let approvalViewController = AttachmentApprovalViewController(options: options, attachmentApprovalItems: pendingAttachments.map(\.approvalItem))
|
||||
let approvalViewController = AttachmentApprovalViewController.loadWithSneakyTransaction(
|
||||
attachmentApprovalItems: pendingAttachments.map(\.approvalItem),
|
||||
options: options,
|
||||
)
|
||||
approvalViewController.approvalDelegate = self
|
||||
approvalViewController.approvalDataSource = self
|
||||
approvalViewController.stickerSheetDelegate = self
|
||||
|
||||
@ -44,23 +44,23 @@ public struct SendableAttachment {
|
||||
@concurrent
|
||||
public static func forPreviewableAttachment(
|
||||
_ previewableAttachment: PreviewableAttachment,
|
||||
imageQuality: ImageQualityLevel? = nil,
|
||||
imageQualityLevel: ImageQualityLevel,
|
||||
) async throws(SignalAttachmentError) -> Self {
|
||||
// We only bother converting/compressing non-animated images
|
||||
if let imageQuality, previewableAttachment.isImage, !previewableAttachment.isAnimatedImage {
|
||||
if previewableAttachment.isImage, !previewableAttachment.isAnimatedImage {
|
||||
let dataSource = previewableAttachment.dataSource
|
||||
guard let imageMetadata = try? dataSource.imageSource().imageMetadata(ignorePerTypeFileSizeLimits: true) else {
|
||||
throw .invalidData
|
||||
}
|
||||
let isValidOriginal = SignalAttachment.isOriginalImageValid(
|
||||
forImageQuality: imageQuality,
|
||||
forImageQuality: imageQualityLevel,
|
||||
fileSize: UInt64(safeCast: dataSource.dataLength),
|
||||
dataUTI: previewableAttachment.dataUTI,
|
||||
imageMetadata: imageMetadata,
|
||||
)
|
||||
if !isValidOriginal {
|
||||
let (dataSource, containerType) = try SignalAttachment.convertAndCompressImage(
|
||||
toImageQuality: imageQuality,
|
||||
toImageQuality: imageQualityLevel,
|
||||
dataSource: dataSource,
|
||||
imageMetadata: imageMetadata,
|
||||
)
|
||||
|
||||
@ -676,7 +676,7 @@ public class SignalAttachment: CustomDebugStringConvertible {
|
||||
// standard or high quality. We will do the final convert and compress before uploading.
|
||||
|
||||
let isOriginalValid = self.isOriginalImageValid(
|
||||
forImageQuality: .maximumForCurrentAppContext,
|
||||
forImageQuality: .maximumForCurrentAppContext(),
|
||||
fileSize: UInt64(safeCast: dataSource.dataLength),
|
||||
dataUTI: dataUTI,
|
||||
imageMetadata: imageMetadata,
|
||||
@ -693,7 +693,7 @@ public class SignalAttachment: CustomDebugStringConvertible {
|
||||
// Otherwise, resize & convert to a PNG or JPG before previewing it.
|
||||
let containerType: ContainerType
|
||||
(newDataSource, containerType) = try convertAndCompressImage(
|
||||
toImageQuality: .maximumForCurrentAppContext,
|
||||
toImageQuality: .maximumForCurrentAppContext(),
|
||||
dataSource: dataSource,
|
||||
imageMetadata: imageMetadata,
|
||||
)
|
||||
|
||||
@ -314,7 +314,7 @@ public class BackupArchiveAccountDataArchiver: BackupArchiveProtoStreamWriter {
|
||||
}
|
||||
|
||||
accountSettings.allowSealedSenderFromAnyone = udManager.shouldAllowUnrestrictedAccessLocal(tx: context.tx)
|
||||
accountSettings.defaultSentMediaQuality = (imageQuality.resolvedQuality(tx: context.tx) == .high ? .high : .standard)
|
||||
accountSettings.defaultSentMediaQuality = imageQuality.fetchValue(tx: context.tx) == .high ? .high : .standard
|
||||
|
||||
var downloadSettings = BackupProto_AccountData.AutoDownloadSettings()
|
||||
for type in MediaBandwidthPreferences.MediaType.allCases {
|
||||
@ -568,10 +568,10 @@ public class BackupArchiveAccountDataArchiver: BackupArchiveProtoStreamWriter {
|
||||
udManager.setShouldAllowUnrestrictedAccessLocal(settings.allowSealedSenderFromAnyone, tx: context.tx)
|
||||
|
||||
switch settings.defaultSentMediaQuality {
|
||||
case .unknownQuality, .UNRECOGNIZED, .standard:
|
||||
imageQuality.setUserSelectedHighQuality(false, tx: context.tx)
|
||||
case .high:
|
||||
imageQuality.setUserSelectedHighQuality(true, tx: context.tx)
|
||||
imageQuality.setValue(.high, tx: context.tx)
|
||||
case .unknownQuality, .UNRECOGNIZED, .standard:
|
||||
imageQuality.setValue(.standard, tx: context.tx)
|
||||
}
|
||||
|
||||
if settings.hasAutoDownloadSettings {
|
||||
|
||||
@ -14,7 +14,7 @@ extension BackupArchive {
|
||||
public typealias BlockingManager = _MessageBackup_BlockingManagerShim
|
||||
public typealias ContactManager = _MessageBackup_ContactManagerShim
|
||||
public typealias DonationSubscriptionManager = _MessageBackup_DonationSubscriptionManagerShim
|
||||
public typealias ImageQuality = _MessageBackup_ImageQualityLevelShim
|
||||
public typealias ImageQuality = _MessageBackup_ImageQualityShim
|
||||
public typealias OWS2FAManager = _MessageBackup_OWS2FAManagerShim
|
||||
public typealias Preferences = _MessageBackup_PreferencesShim
|
||||
public typealias ProfileManager = _MessageBackup_ProfileManagerShim
|
||||
@ -32,7 +32,7 @@ extension BackupArchive {
|
||||
public typealias BlockingManager = _MessageBackup_BlockingManagerWrapper
|
||||
public typealias ContactManager = _MessageBackup_ContactManagerWrapper
|
||||
public typealias DonationSubscriptionManager = _MessageBackup_DonationSubscriptionManagerWrapper
|
||||
public typealias ImageQuality = _MessageBackup_ImageQualityLevelWrapper
|
||||
public typealias ImageQuality = _MessageBackup_ImageQualityWrapper
|
||||
public typealias OWS2FAManager = _MessageBackup_OWS2FAManagerWrapper
|
||||
public typealias Preferences = _MessageBackup_PreferencesWrapper
|
||||
public typealias ProfileManager = _MessageBackup_ProfileManagerWrapper
|
||||
@ -159,18 +159,18 @@ public class _MessageBackup_DonationSubscriptionManagerWrapper: _MessageBackup_D
|
||||
|
||||
// MARK: - ImageQuality
|
||||
|
||||
public protocol _MessageBackup_ImageQualityLevelShim {
|
||||
func setUserSelectedHighQuality(_ isHighQuality: Bool, tx: DBWriteTransaction)
|
||||
func resolvedQuality(tx: DBReadTransaction) -> ImageQualityLevel
|
||||
public protocol _MessageBackup_ImageQualityShim {
|
||||
func setValue(_ imageQuality: ImageQuality, tx: DBWriteTransaction)
|
||||
func fetchValue(tx: DBReadTransaction) -> ImageQuality
|
||||
}
|
||||
|
||||
public class _MessageBackup_ImageQualityLevelWrapper: _MessageBackup_ImageQualityLevelShim {
|
||||
public func setUserSelectedHighQuality(_ isHighQuality: Bool, tx: DBWriteTransaction) {
|
||||
ImageQualityLevel.setUserSelectedHighQuality(isHighQuality, tx: tx)
|
||||
public class _MessageBackup_ImageQualityWrapper: _MessageBackup_ImageQualityShim {
|
||||
public func setValue(_ imageQuality: ImageQuality, tx: DBWriteTransaction) {
|
||||
ImageQuality.setValue(imageQuality, tx: tx)
|
||||
}
|
||||
|
||||
public func resolvedQuality(tx: DBReadTransaction) -> ImageQualityLevel {
|
||||
ImageQualityLevel.resolvedQuality(tx: tx)
|
||||
public func fetchValue(tx: DBReadTransaction) -> ImageQuality {
|
||||
return ImageQuality.fetchValue(tx: tx)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -165,11 +165,10 @@ public class RemoteConfig {
|
||||
!isEnabled(.paypalMonthlyDonationKillSwitch)
|
||||
}
|
||||
|
||||
public func standardMediaQualityLevel(localPhoneNumber: String?) -> ImageQualityLevel? {
|
||||
let rawValue: String = ValueFlag.standardMediaQualityLevel.rawValue
|
||||
public func standardMediaQualityLevel(callingCode: Int?) -> ImageQualityLevel? {
|
||||
guard
|
||||
let csvString = valueFlags[rawValue],
|
||||
let stringValue = Self.countryCodeValue(csvString: csvString, csvDescription: rawValue, localPhoneNumber: localPhoneNumber),
|
||||
let csvString = self.value(.standardMediaQualityLevel),
|
||||
let stringValue = Self.countryCodeValue(csvString: csvString, callingCode: callingCode),
|
||||
let uintValue = UInt(stringValue),
|
||||
let defaultMediaQuality = ImageQualityLevel(rawValue: uintValue)
|
||||
else {
|
||||
@ -404,9 +403,11 @@ public class RemoteConfig {
|
||||
///
|
||||
/// - Parameter csvString: a CSV containing `<country-code>:<parts-per-million>` pairs
|
||||
/// - Parameter key: a key to use as part of bucketing
|
||||
static func isCountryCodeBucketEnabled(csvString: String, key: String, csvDescription: String, localIdentifiers: LocalIdentifiers) -> Bool {
|
||||
static func isCountryCodeBucketEnabled(csvString: String, key: String, localIdentifiers: LocalIdentifiers) -> Bool {
|
||||
let phoneNumberUtil = SSKEnvironment.shared.phoneNumberUtilRef
|
||||
let callingCode = phoneNumberUtil.parseE164(localIdentifiers.phoneNumber)?.getCallingCode()
|
||||
guard
|
||||
let countryCodeValue = countryCodeValue(csvString: csvString, csvDescription: csvDescription, localPhoneNumber: localIdentifiers.phoneNumber),
|
||||
let countryCodeValue = countryCodeValue(csvString: csvString, callingCode: callingCode),
|
||||
let countEnabled = UInt64(countryCodeValue)
|
||||
else {
|
||||
return false
|
||||
@ -415,44 +416,27 @@ public class RemoteConfig {
|
||||
return isBucketEnabled(key: key, countEnabled: countEnabled, bucketSize: 1_000_000, localAci: localIdentifiers.aci)
|
||||
}
|
||||
|
||||
private static func isCountryCodeBucketEnabled(flag: ValueFlag, valueFlags: [String: String], localIdentifiers: LocalIdentifiers) -> Bool {
|
||||
let rawValue = flag.rawValue
|
||||
guard let csvString = valueFlags[rawValue] else { return false }
|
||||
|
||||
return isCountryCodeBucketEnabled(csvString: csvString, key: rawValue, csvDescription: rawValue, localIdentifiers: localIdentifiers)
|
||||
}
|
||||
|
||||
/// Given a CSV of `<country-code>:<value>` pairs, extract the `<value>`
|
||||
/// corresponding to the current user's country.
|
||||
private static func countryCodeValue(csvString: String, csvDescription: String, localPhoneNumber: String?) -> String? {
|
||||
guard !csvString.isEmpty else { return nil }
|
||||
|
||||
// The value should always be a comma-separated list of country codes
|
||||
// colon-separated from a value. There all may be an optional be a wildcard
|
||||
// "*" country code that any unspecified country codes should use. If
|
||||
// neither the local country code or the wildcard is specified, we assume
|
||||
// the value is not set.
|
||||
/// corresponding to the current user's country. The value should always be
|
||||
/// a comma-separated list of country codes colon-separated from a value.
|
||||
/// There may be an optional "*" wildcard country code that any unspecified
|
||||
/// country codes should use. If we can't parse the country code from our
|
||||
/// own phone number, we fall back to this wildcard value.
|
||||
private static func countryCodeValue(csvString: String, callingCode: Int?) -> String? {
|
||||
let callingCodeToValueMap = csvString
|
||||
.components(separatedBy: ",")
|
||||
.reduce(into: [String: String]()) { result, value in
|
||||
let components = value.components(separatedBy: ":")
|
||||
guard components.count == 2 else { return owsFailDebug("Invalid \(csvDescription) value \(value)") }
|
||||
guard components.count == 2 else {
|
||||
owsFailDebug("malformed country-code:value remote config value")
|
||||
return
|
||||
}
|
||||
let callingCode = components[0]
|
||||
let countryValue = components[1]
|
||||
result[callingCode] = countryValue
|
||||
}
|
||||
|
||||
guard !callingCodeToValueMap.isEmpty else { return nil }
|
||||
|
||||
guard
|
||||
let localPhoneNumber,
|
||||
let localCallingCode = SSKEnvironment.shared.phoneNumberUtilRef.parseE164(localPhoneNumber)?.getCallingCode()
|
||||
else {
|
||||
owsFailDebug("Invalid local number")
|
||||
return nil
|
||||
}
|
||||
|
||||
return callingCodeToValueMap[String(localCallingCode)] ?? callingCodeToValueMap["*"]
|
||||
return callingCode.flatMap({ callingCodeToValueMap[String($0)] }) ?? callingCodeToValueMap["*"]
|
||||
}
|
||||
|
||||
private static func isBucketEnabled(key: String, countEnabled: UInt64, bucketSize: UInt64, localAci: Aci) -> Bool {
|
||||
|
||||
@ -786,7 +786,6 @@ extension ExperienceUpgradeManifest {
|
||||
guard RemoteConfig.isCountryCodeBucketEnabled(
|
||||
csvString: megaphone.manifest.countries,
|
||||
key: megaphone.manifest.id,
|
||||
csvDescription: "remoteMegaphoneCountries_\(megaphone.manifest.id)",
|
||||
localIdentifiers: localIdentifiers
|
||||
) else {
|
||||
return false
|
||||
|
||||
@ -5,20 +5,65 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/// The user-selected quality for images. Users are offered a choice between
|
||||
/// "standard" and "high" quality. The former may correspond to level "one"
|
||||
/// or "two" (depending on a remote config); the latter always corresponds
|
||||
/// to level "three". Most callers should use ImageQuality; typically only
|
||||
/// the compression logic needs access to ImageQualityLevel.
|
||||
public enum ImageQuality {
|
||||
/// Indirectly translates to ImageQualityLevel.one or ImageQualityLevel.two.
|
||||
case standard
|
||||
|
||||
/// Always translates to ImageQualityLevel.three.
|
||||
case high
|
||||
|
||||
private static let keyValueStore = KeyValueStore(collection: "ImageQualityLevel")
|
||||
private static let userSelectedHighQualityKey = "defaultQuality"
|
||||
private static let userSelectedHighQualityValue = 3 as UInt
|
||||
|
||||
public static func fetchValue(tx: DBReadTransaction) -> Self {
|
||||
let highQualityValue = keyValueStore.getUInt(userSelectedHighQualityKey, transaction: tx)
|
||||
return highQualityValue == userSelectedHighQualityValue ? .high : .standard
|
||||
}
|
||||
|
||||
public static func setValue(_ imageQuality: Self, tx: DBWriteTransaction) {
|
||||
switch imageQuality {
|
||||
case .high:
|
||||
keyValueStore.setUInt(userSelectedHighQualityValue, key: userSelectedHighQualityKey, transaction: tx)
|
||||
case .standard:
|
||||
keyValueStore.removeValue(forKey: userSelectedHighQualityKey, transaction: tx)
|
||||
}
|
||||
}
|
||||
|
||||
public var localizedString: String {
|
||||
switch self {
|
||||
case .standard:
|
||||
return OWSLocalizedString("SENT_MEDIA_QUALITY_STANDARD", comment: "String describing standard quality sent media")
|
||||
case .high:
|
||||
return OWSLocalizedString("SENT_MEDIA_QUALITY_HIGH", comment: "String describing high quality sent media")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum ImageQualityLevel: UInt, Comparable {
|
||||
case one = 1
|
||||
case two = 2
|
||||
case three = 3
|
||||
|
||||
public static let high: ImageQualityLevel = .three
|
||||
|
||||
// We calculate the "standard" media quality remotely
|
||||
// based on country code. For some regions, we use
|
||||
// a lower "standard" quality than others. High quality
|
||||
// is always level three. If not remotely specified,
|
||||
// standard uses quality level two.
|
||||
public static func remoteDefault(localPhoneNumber: String?) -> ImageQualityLevel {
|
||||
return RemoteConfig.current.standardMediaQualityLevel(localPhoneNumber: localPhoneNumber) ?? .two
|
||||
// We calculate the "standard" media quality remotely based on country
|
||||
// code. For some regions, we use a lower "standard" quality than others.
|
||||
// High quality is always level three. If not remotely specified, standard
|
||||
// uses quality level two.
|
||||
public static func standardQualityLevel(
|
||||
remoteConfig: RemoteConfig = .current,
|
||||
callingCode: Int? = { () -> Int? in
|
||||
let phoneNumberUtil = SSKEnvironment.shared.phoneNumberUtilRef
|
||||
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
|
||||
let localIdentifiers = tsAccountManager.localIdentifiersWithMaybeSneakyTransaction
|
||||
return localIdentifiers.flatMap({ phoneNumberUtil.parseE164($0.phoneNumber) })?.getCallingCode()
|
||||
}(),
|
||||
) -> ImageQualityLevel {
|
||||
return remoteConfig.standardMediaQualityLevel(callingCode: callingCode) ?? .two
|
||||
}
|
||||
|
||||
public var startingTier: ImageQualityTier {
|
||||
@ -51,8 +96,8 @@ public enum ImageQualityLevel: UInt, Comparable {
|
||||
}
|
||||
}
|
||||
|
||||
public static var maximumForCurrentAppContext: ImageQualityLevel {
|
||||
if CurrentAppContext().isMainApp {
|
||||
public static func maximumForCurrentAppContext(_ currentAppContext: any AppContext = CurrentAppContext()) -> Self {
|
||||
if currentAppContext.isMainApp {
|
||||
return .three
|
||||
} else {
|
||||
// Outside of the main app (like in the share extension)
|
||||
@ -62,49 +107,21 @@ public enum ImageQualityLevel: UInt, Comparable {
|
||||
}
|
||||
}
|
||||
|
||||
private static let keyValueStore = KeyValueStore(collection: "ImageQualityLevel")
|
||||
private static var userSelectedHighQualityKey: String { "defaultQuality" }
|
||||
|
||||
public static func resolvedQuality(tx: DBReadTransaction) -> ImageQualityLevel {
|
||||
public static func resolvedValue(
|
||||
imageQuality: ImageQuality,
|
||||
standardQualityLevel: @autoclosure () -> Self = .standardQualityLevel(),
|
||||
maximumForCurrentAppContext: Self = .maximumForCurrentAppContext(),
|
||||
) -> ImageQualityLevel {
|
||||
let targetQualityLevel: Self
|
||||
switch imageQuality {
|
||||
case .high:
|
||||
targetQualityLevel = .three
|
||||
case .standard:
|
||||
targetQualityLevel = standardQualityLevel()
|
||||
}
|
||||
// If the max quality we allow is less than the stored preference,
|
||||
// we have to restrict ourselves to the max allowed.
|
||||
return min(_resolvedQuality(tx: tx), maximumForCurrentAppContext)
|
||||
}
|
||||
|
||||
private static func _resolvedQuality(tx: DBReadTransaction) -> ImageQualityLevel {
|
||||
let isHighQuality: Bool = {
|
||||
// All that matters is "did the user choose high quality explicity?". If
|
||||
// they didn't, we always fall back to the current server-provided value
|
||||
// for standard quality. In the past, we stored low/medium values
|
||||
// explicitly, but this was wrong.
|
||||
guard let rawValue = keyValueStore.getUInt(userSelectedHighQualityKey, transaction: tx) else {
|
||||
return false
|
||||
}
|
||||
return ImageQualityLevel(rawValue: rawValue) == .high
|
||||
}()
|
||||
if isHighQuality {
|
||||
return .high
|
||||
}
|
||||
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
|
||||
let localPhoneNumber = tsAccountManager.localIdentifiers(tx: tx)?.phoneNumber
|
||||
return remoteDefault(localPhoneNumber: localPhoneNumber)
|
||||
}
|
||||
|
||||
public static func setUserSelectedHighQuality(_ isHighQuality: Bool, tx: DBWriteTransaction) {
|
||||
if isHighQuality {
|
||||
keyValueStore.setUInt(ImageQualityLevel.three.rawValue, key: userSelectedHighQualityKey, transaction: tx)
|
||||
} else {
|
||||
keyValueStore.removeValue(forKey: userSelectedHighQualityKey, transaction: tx)
|
||||
}
|
||||
}
|
||||
|
||||
public var localizedString: String {
|
||||
switch self {
|
||||
case .one, .two:
|
||||
return OWSLocalizedString("SENT_MEDIA_QUALITY_STANDARD", comment: "String describing standard quality sent media")
|
||||
case .three:
|
||||
return OWSLocalizedString("SENT_MEDIA_QUALITY_HIGH", comment: "String describing high quality sent media")
|
||||
}
|
||||
return min(targetQualityLevel, maximumForCurrentAppContext)
|
||||
}
|
||||
|
||||
public static func < (lhs: ImageQualityLevel, rhs: ImageQualityLevel) -> Bool {
|
||||
|
||||
@ -165,7 +165,7 @@ class SharingThreadPickerViewController: ConversationPickerViewController {
|
||||
if self.selection.conversations.contains(where: \.isStory) {
|
||||
approvalVCOptions.insert(.disallowViewOnce)
|
||||
}
|
||||
let approvalView = AttachmentApprovalViewController(options: approvalVCOptions, attachmentApprovalItems: approvalItems)
|
||||
let approvalView = AttachmentApprovalViewController.loadWithSneakyTransaction(attachmentApprovalItems: approvalItems, options: approvalVCOptions)
|
||||
approvalVC = approvalView
|
||||
approvalView.approvalDelegate = self
|
||||
approvalView.approvalDataSource = self
|
||||
|
||||
@ -13,28 +13,23 @@ public import SignalServiceKit
|
||||
|
||||
public struct ApprovedAttachments {
|
||||
public let isViewOnce: Bool
|
||||
// [15M] TODO: Make this non-optional and always downsample in the same location.
|
||||
public let imageQuality: ImageQualityLevel?
|
||||
public let imageQuality: ImageQuality
|
||||
public let attachments: [PreviewableAttachment]
|
||||
|
||||
private init(isViewOnce: Bool, imageQuality: ImageQualityLevel?, attachments: [PreviewableAttachment]) {
|
||||
private init(isViewOnce: Bool, imageQuality: ImageQuality, attachments: [PreviewableAttachment]) {
|
||||
owsPrecondition(!isViewOnce || attachments.count <= 1)
|
||||
self.isViewOnce = isViewOnce
|
||||
self.imageQuality = imageQuality
|
||||
self.attachments = attachments
|
||||
}
|
||||
|
||||
public init(viewOnceAttachment: PreviewableAttachment, imageQuality: ImageQualityLevel?) {
|
||||
public init(viewOnceAttachment: PreviewableAttachment, imageQuality: ImageQuality) {
|
||||
self.init(isViewOnce: true, imageQuality: imageQuality, attachments: [viewOnceAttachment])
|
||||
}
|
||||
|
||||
public init(nonViewOnceAttachments: [PreviewableAttachment], imageQuality: ImageQualityLevel?) {
|
||||
public init(nonViewOnceAttachments: [PreviewableAttachment], imageQuality: ImageQuality) {
|
||||
self.init(isViewOnce: false, imageQuality: imageQuality, attachments: nonViewOnceAttachments)
|
||||
}
|
||||
|
||||
public static func empty() -> Self {
|
||||
return Self(isViewOnce: false, imageQuality: nil, attachments: [])
|
||||
}
|
||||
}
|
||||
|
||||
public protocol AttachmentApprovalViewControllerDelegate: AnyObject {
|
||||
@ -92,7 +87,7 @@ public struct AttachmentApprovalViewControllerOptions: OptionSet {
|
||||
|
||||
// MARK: -
|
||||
|
||||
public class AttachmentApprovalViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, OWSNavigationChildController {
|
||||
public final class AttachmentApprovalViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, OWSNavigationChildController {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
@ -111,8 +106,9 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
|
||||
}
|
||||
|
||||
if
|
||||
ImageQualityLevel.maximumForCurrentAppContext == .high,
|
||||
attachmentApprovalItemCollection.attachmentApprovalItems.contains(where: { $0.attachment.isImage }) {
|
||||
ImageQualityLevel.maximumForCurrentAppContext() == .three,
|
||||
attachmentApprovalItemCollection.attachmentApprovalItems.contains(where: { $0.attachment.isImage })
|
||||
{
|
||||
options.insert(.canChangeQualityLevel)
|
||||
}
|
||||
|
||||
@ -129,7 +125,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
|
||||
}
|
||||
}
|
||||
|
||||
lazy var outputQualityLevel: ImageQualityLevel = SSKEnvironment.shared.databaseStorageRef.read { .resolvedQuality(tx: $0) }
|
||||
private var outputImageQuality: ImageQuality
|
||||
|
||||
public weak var approvalDelegate: AttachmentApprovalViewControllerDelegate?
|
||||
public weak var approvalDataSource: AttachmentApprovalViewControllerDataSource?
|
||||
@ -155,9 +151,26 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
|
||||
}
|
||||
}
|
||||
|
||||
public init(options: AttachmentApprovalViewControllerOptions, attachmentApprovalItems: [AttachmentApprovalItem]) {
|
||||
public static func loadWithSneakyTransaction(
|
||||
attachmentApprovalItems: [AttachmentApprovalItem],
|
||||
options: AttachmentApprovalViewControllerOptions,
|
||||
) -> Self {
|
||||
let databaseStorage = SSKEnvironment.shared.databaseStorageRef
|
||||
return Self(
|
||||
attachmentApprovalItems: attachmentApprovalItems,
|
||||
defaultImageQuality: databaseStorage.read(block: ImageQuality.fetchValue(tx:)),
|
||||
options: options,
|
||||
)
|
||||
}
|
||||
|
||||
private init(
|
||||
attachmentApprovalItems: [AttachmentApprovalItem],
|
||||
defaultImageQuality: ImageQuality,
|
||||
options: AttachmentApprovalViewControllerOptions,
|
||||
) {
|
||||
assert(attachmentApprovalItems.count > 0)
|
||||
|
||||
self.outputImageQuality = defaultImageQuality
|
||||
self.receivedOptions = options
|
||||
|
||||
let pageOptions: [UIPageViewController.OptionsKey: Any] = [.interPageSpacing: kSpacingBetweenItems]
|
||||
@ -166,7 +179,10 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
|
||||
let isAddMoreVisibleBlock = { [weak self] in
|
||||
return self?.isAddMoreVisible ?? false
|
||||
}
|
||||
self.attachmentApprovalItemCollection = AttachmentApprovalItemCollection(attachmentApprovalItems: attachmentApprovalItems, isAddMoreVisible: isAddMoreVisibleBlock)
|
||||
self.attachmentApprovalItemCollection = AttachmentApprovalItemCollection(
|
||||
attachmentApprovalItems: attachmentApprovalItems,
|
||||
isAddMoreVisible: isAddMoreVisibleBlock,
|
||||
)
|
||||
self.dataSource = self
|
||||
self.delegate = self
|
||||
|
||||
@ -205,7 +221,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
|
||||
if hasQuotedReplyDraft {
|
||||
options.insert(.disallowViewOnce)
|
||||
}
|
||||
let vc = AttachmentApprovalViewController(options: options, attachmentApprovalItems: attachmentApprovalItems)
|
||||
let vc = AttachmentApprovalViewController.loadWithSneakyTransaction(attachmentApprovalItems: attachmentApprovalItems, options: options)
|
||||
// The data source needs to be set before the message body because it is needed to hydrate mentions.
|
||||
vc.approvalDataSource = approvalDataSource
|
||||
vc.setMessageBody(initialMessageBody, txProvider: DependenciesBridge.shared.db.readTxProvider)
|
||||
@ -423,7 +439,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
|
||||
let configuration = AttachmentApprovalToolbar.Configuration(
|
||||
isAddMoreVisible: isAddMoreVisible,
|
||||
isMediaStripVisible: attachmentApprovalItems.count > 1,
|
||||
isMediaHighQualityEnabled: outputQualityLevel == .high,
|
||||
isMediaHighQualityEnabled: outputImageQuality == .high,
|
||||
isViewOnceOn: isViewOnceEnabled,
|
||||
canToggleViewOnce: options.contains(.canToggleViewOnce),
|
||||
canChangeMediaQuality: options.contains(.canChangeQualityLevel),
|
||||
@ -873,7 +889,7 @@ extension AttachmentApprovalViewController {
|
||||
// make below are reflected afterwards.
|
||||
ModalActivityIndicatorViewController.present(fromViewController: self, canCancel: false, asyncBlock: { modalVC in
|
||||
do {
|
||||
let imageQuality = self.outputQualityLevel
|
||||
let imageQuality = self.outputImageQuality
|
||||
let attachments = try await self.prepareAttachments()
|
||||
modalVC.dismiss {
|
||||
let isViewOnce = self.options.contains(.canToggleViewOnce) && self.isViewOnceEnabled
|
||||
@ -1068,16 +1084,9 @@ extension AttachmentApprovalViewController {
|
||||
actionSheet.overrideUserInterfaceStyle = .dark
|
||||
actionSheet.isCancelable = true
|
||||
|
||||
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
|
||||
let localPhoneNumber = tsAccountManager.localIdentifiersWithMaybeSneakyTransaction?.phoneNumber
|
||||
let standardQualityLevel = ImageQualityLevel.remoteDefault(localPhoneNumber: localPhoneNumber)
|
||||
|
||||
let selectionControl = MediaQualitySelectionControl(
|
||||
standardQualityLevel: standardQualityLevel,
|
||||
currentQualityLevel: outputQualityLevel
|
||||
)
|
||||
let selectionControl = MediaQualitySelectionControl(currentQuality: outputImageQuality)
|
||||
selectionControl.callback = { [weak self, weak actionSheet] qualityLevel in
|
||||
self?.outputQualityLevel = qualityLevel
|
||||
self?.outputImageQuality = qualityLevel
|
||||
self?.updateBottomToolView(animated: false)
|
||||
|
||||
if UIAccessibility.isVoiceOverRunning {
|
||||
@ -1119,24 +1128,22 @@ extension AttachmentApprovalViewController {
|
||||
private let buttonQualityStandard: MediaQualityButton
|
||||
|
||||
private let buttonQualityHigh = MediaQualityButton(
|
||||
title: ImageQualityLevel.high.localizedString,
|
||||
title: ImageQuality.high.localizedString,
|
||||
subtitle: OWSLocalizedString(
|
||||
"ATTACHMENT_APPROVAL_MEDIA_QUALITY_HIGH_OPTION_SUBTITLE",
|
||||
comment: "Subtitle for the 'high' option for media quality."
|
||||
)
|
||||
)
|
||||
|
||||
private let standardQualityLevel: ImageQualityLevel
|
||||
private(set) var qualityLevel: ImageQualityLevel
|
||||
private(set) var imageQuality: ImageQuality
|
||||
|
||||
var callback: ((ImageQualityLevel) -> Void)?
|
||||
var callback: ((ImageQuality) -> Void)?
|
||||
|
||||
init(standardQualityLevel: ImageQualityLevel, currentQualityLevel: ImageQualityLevel) {
|
||||
self.standardQualityLevel = standardQualityLevel
|
||||
self.qualityLevel = currentQualityLevel
|
||||
init(currentQuality: ImageQuality) {
|
||||
self.imageQuality = currentQuality
|
||||
|
||||
self.buttonQualityStandard = MediaQualityButton(
|
||||
title: standardQualityLevel.localizedString,
|
||||
title: ImageQuality.standard.localizedString,
|
||||
subtitle: OWSLocalizedString(
|
||||
"ATTACHMENT_APPROVAL_MEDIA_QUALITY_STANDARD_OPTION_SUBTITLE",
|
||||
comment: "Subtitle for the 'standard' option for media quality."
|
||||
@ -1146,7 +1153,7 @@ extension AttachmentApprovalViewController {
|
||||
super.init(frame: .zero)
|
||||
|
||||
buttonQualityStandard.block = { [weak self] in
|
||||
self?.didSelectQualityLevel(standardQualityLevel)
|
||||
self?.didSelectQualityLevel(.standard)
|
||||
}
|
||||
addSubview(buttonQualityStandard)
|
||||
buttonQualityStandard.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .trailing)
|
||||
@ -1167,15 +1174,15 @@ extension AttachmentApprovalViewController {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func didSelectQualityLevel(_ qualityLevel: ImageQualityLevel) {
|
||||
self.qualityLevel = qualityLevel
|
||||
private func didSelectQualityLevel(_ imageQuality: ImageQuality) {
|
||||
self.imageQuality = imageQuality
|
||||
updateButtonAppearance()
|
||||
callback?(qualityLevel)
|
||||
callback?(imageQuality)
|
||||
}
|
||||
|
||||
private func updateButtonAppearance() {
|
||||
buttonQualityStandard.isSelected = qualityLevel == standardQualityLevel
|
||||
buttonQualityHigh.isSelected = qualityLevel == .high
|
||||
buttonQualityStandard.isSelected = imageQuality == .standard
|
||||
buttonQualityHigh.isSelected = imageQuality == .high
|
||||
}
|
||||
|
||||
private class MediaQualityButton: OWSButton {
|
||||
@ -1256,7 +1263,7 @@ extension AttachmentApprovalViewController {
|
||||
|
||||
override var accessibilityValue: String? {
|
||||
get {
|
||||
let selectedButton = qualityLevel == .high ? buttonQualityHigh : buttonQualityStandard
|
||||
let selectedButton = imageQuality == .high ? buttonQualityHigh : buttonQualityStandard
|
||||
return [ selectedButton.topLabel, selectedButton.bottomLabel ].compactMap { $0.text }.joined(separator: ",")
|
||||
}
|
||||
set { super.accessibilityValue = newValue }
|
||||
@ -1268,20 +1275,20 @@ extension AttachmentApprovalViewController {
|
||||
}
|
||||
|
||||
override func accessibilityActivate() -> Bool {
|
||||
callback?(qualityLevel)
|
||||
callback?(imageQuality)
|
||||
return true
|
||||
}
|
||||
|
||||
override func accessibilityIncrement() {
|
||||
if qualityLevel == standardQualityLevel {
|
||||
qualityLevel = .high
|
||||
if imageQuality == .standard {
|
||||
imageQuality = .high
|
||||
updateButtonAppearance()
|
||||
}
|
||||
}
|
||||
|
||||
override func accessibilityDecrement() {
|
||||
if qualityLevel == .high {
|
||||
qualityLevel = standardQualityLevel
|
||||
if imageQuality == .high {
|
||||
imageQuality = .standard
|
||||
updateButtonAppearance()
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,8 +40,9 @@ public class AttachmentMultisend {
|
||||
}
|
||||
|
||||
let imageQuality = approvedAttachments.imageQuality
|
||||
let imageQualityLevel = ImageQualityLevel.resolvedValue(imageQuality: imageQuality)
|
||||
let sendableAttachments = try await approvedAttachments.attachments.mapAsync {
|
||||
return try await SendableAttachment.forPreviewableAttachment($0, imageQuality: imageQuality)
|
||||
return try await SendableAttachment.forPreviewableAttachment($0, imageQualityLevel: imageQualityLevel)
|
||||
}
|
||||
|
||||
let segmentedAttachments = try await segmentAttachmentsIfNecessary(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user