Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Optimize Images setting and enable it by default #21981

Merged
merged 31 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
624c1f4
Add optimize image popup
fluiddot Nov 3, 2023
574f2d1
Add image quality setting to App settings screen
fluiddot Nov 3, 2023
454ed20
Add image optimization setting to App Settings creen
fluiddot Nov 3, 2023
92aa560
Disable max image setting when image optimization is off
fluiddot Nov 3, 2023
9df1f57
Set optimized value as default for max image size
fluiddot Nov 3, 2023
39b5c63
Use image quality setting when compressing images before upload
fluiddot Nov 3, 2023
b9f0892
Add new event for tracking max image size setting changes
fluiddot Nov 3, 2023
351b740
Display optimize images popup when uploading from device media
fluiddot Nov 3, 2023
c0bd61d
Display optimize images popup when uploading after taking a photo
fluiddot Nov 3, 2023
a3c5fe8
Fix typos in `MediaCoordinator`
fluiddot Nov 3, 2023
1040b88
Add inline comment to image size for upload calculation
fluiddot Nov 6, 2023
bf760b9
Move media settings default values to constants
fluiddot Nov 6, 2023
eaba7b1
Update media setting unit test
fluiddot Nov 6, 2023
637a196
Remove unneeded self reference in `advertiseImageOptimization` handlers
fluiddot Nov 6, 2023
5b2c7a6
Add `MediaSettings` unit tests
fluiddot Nov 7, 2023
2a4dd6b
Update release notes
fluiddot Nov 7, 2023
b6f0127
Update image quality values
fluiddot Nov 7, 2023
3bc4737
Merge branch 'trunk' into add/image-optimization-setting
fluiddot Nov 7, 2023
2ce126f
Use multiplication sign to display dimenstions in upload settings
kean Nov 9, 2023
a51fb8f
Merge pull request #22019 from wordpress-mobile/task/use-multiplicati…
fluiddot Nov 10, 2023
d64232c
Use Leave On option as preferred in image optimization alert
fluiddot Nov 10, 2023
a85f4b1
Remove unused function from `ImageQuality` enum
fluiddot Nov 10, 2023
f29f4d2
Remove casting from event properties passed in `AppSettingsViewContro…
fluiddot Nov 10, 2023
b968b02
Use unique reverse-DNS naming style in updated strings
fluiddot Nov 13, 2023
477777c
Rename constant `defaultMaxImageDimension`
fluiddot Nov 13, 2023
92d4d13
Rename `imageOptimizationSetting`
fluiddot Nov 13, 2023
294a62f
Merge branch 'trunk' into add/image-optimization-setting
fluiddot Nov 13, 2023
63d6250
Update release notes
fluiddot Nov 13, 2023
b71b7f8
Apply code improvements in `AppSettingsViewController`
fluiddot Nov 22, 2023
132bfbc
Improve show/hide animation of image optimization settings
fluiddot Nov 22, 2023
119b325
Merge branch 'trunk' into add/image-optimization-setting
fluiddot Nov 22, 2023
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
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
23.8
-----
* [**] Add Optimize Images setting for image uploads and enable it by default [#21981]
* [*] Fix the media item details screen layout on iPad [#22042]
* [*] Integrate native photos picker (`PHPickerViewControlle`) in Story Editor [#22059]
* [*] [internal] Fix an issue with scheduling of posts not working on iOS 17 with Xcode 15 [#22012]
Expand Down
4 changes: 2 additions & 2 deletions WordPress/Classes/Services/MediaCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ class MediaCoordinator: NSObject {
addMedia(from: asset, post: post, coordinator: coordinator(for: post), analyticsInfo: analyticsInfo)
}

/// Create a `Media` instance from the main context and upload the asset to the Meida Library.
/// Create a `Media` instance from the main context and upload the asset to the Media Library.
///
/// - Warning: This function must be called from the main thread.
///
Expand Down Expand Up @@ -186,7 +186,7 @@ class MediaCoordinator: NSObject {
return media
}

/// Create a `Media` instance and upload the asset to the Meida Library.
/// Create a `Media` instance and upload the asset to the Media Library.
///
/// - SeeAlso: `MediaImportService.createMedia(with:blog:post:receiveUpdate:thumbnailCallback:completion:)`
private func addMedia(from asset: ExportableAsset, blog: Blog, post: AbstractPost?, coordinator: MediaProgressCoordinator, analyticsInfo: MediaAnalyticsInfo? = nil) {
Expand Down
32 changes: 32 additions & 0 deletions WordPress/Classes/Services/MediaHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,38 @@ class MediaHelper: NSObject {
}

}

static func advertiseImageOptimization(completion: @escaping (() -> Void)) {
guard MediaSettings().advertiseImageOptimization else {
completion()
return
}

let title = NSLocalizedString("appSettings.optimizeImagesPopup.title", value: "Keep optimizing images?",
comment: "Title of an alert informing users to enable image optimization in uploads.")
let message = NSLocalizedString("appSettings.optimizeImagesPopup.message", value: "Image optimization shrinks images for faster uploading.\n\nThis option is enabled by default, but you can change it in the app settings at any time.",
comment: "Message of an alert informing users to enable image optimization in uploads.")
let turnOffTitle = NSLocalizedString("appSettings.optimizeImagesPopup.turnOff", value: "No, turn off", comment: "Title of button for turning off image optimization, displayed in the alert informing users to enable image optimization in uploads.")
let leaveOnTitle = NSLocalizedString("appSettings.optimizeImagesPopup.turnOn", value: "Yes, leave on", comment: "Title of button for leaving on image optimization, displayed in the alert informing users to enable image optimization in uploads.")

let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
twstokes marked this conversation as resolved.
Show resolved Hide resolved
let turnOffAction = UIAlertAction(title: turnOffTitle, style: .default) { _ in
MediaSettings().imageOptimizationEnabled = false
WPAnalytics.track(.appSettingsOptimizeImagesPopupTapped, properties: ["option": "off"])
completion()
}
let leaveOnAction = UIAlertAction(title: leaveOnTitle, style: .default) { _ in
MediaSettings().imageOptimizationEnabled = true
WPAnalytics.track(.appSettingsOptimizeImagesPopupTapped, properties: ["option": "on"])
completion()
}
alert.addAction(turnOffAction)
alert.addAction(leaveOnAction)
alert.preferredAction = leaveOnAction
alert.presentFromRootViewController()

MediaSettings().advertiseImageOptimization = false
}
}

extension Media {
Expand Down
2 changes: 1 addition & 1 deletion WordPress/Classes/Services/MediaImportService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ class MediaImportService: NSObject {
var options = MediaImageExporter.Options()
options.maximumImageSize = self.exporterMaximumImageSize()
options.stripsGeoLocationIfNeeded = MediaSettings().removeLocationSetting
options.imageCompressionQuality = MediaImportService.preferredImageCompressionQuality
options.imageCompressionQuality = MediaSettings().imageQualityForUpload.doubleValue
return options
}

Expand Down
99 changes: 95 additions & 4 deletions WordPress/Classes/Services/MediaSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,55 @@ import AVFoundation

class MediaSettings: NSObject {
// MARK: - Constants
fileprivate let imageOptimizationKey = "SavedImageOptimizationSetting"
fileprivate let maxImageSizeKey = "SavedMaxImageSizeSetting"
fileprivate let imageQualityKey = "SavedImageQualitySetting"
fileprivate let removeLocationKey = "SavedRemoveLocationSetting"
fileprivate let maxVideoSizeKey = "SavedMaxVideoSizeSetting"
fileprivate let advertiseImageOptimizationKey = "SavedAdvertiseImageOptimization"

fileprivate let defaultImageOptimization = true
fileprivate let defaultMaxImageDimension = 2000
fileprivate let defaultImageQuality: ImageQuality = .medium
fileprivate let defaultMaxVideoSize: VideoResolution = .sizeOriginal
fileprivate let defaultRemoveLocation = true

fileprivate let minImageDimension = 150
fileprivate let maxImageDimension = 3000

enum ImageQuality: String {
case maximum = "MaximumQuality100"
case high = "HighQuality90"
case medium = "MediumQuality80"
case low = "LowQuality70"

var doubleValue: Double {
switch self {
case .maximum:
return 1.0
case .high:
return 0.9
case .medium:
return 0.8
case .low:
return 0.7
}
}

var description: String {
switch self {
case .maximum:
return NSLocalizedString("appSettings.media.imageQuality.maximum", value: "Maximum", comment: "Indicates an image will use maximum quality when uploaded.")
case .high:
return NSLocalizedString("appSettings.media.imageQuality.high", value: "High", comment: "Indicates an image will use high quality when uploaded.")
case .medium:
return NSLocalizedString("appSettings.media.imageQuality.medium", value: "Medium", comment: "Indicates an image will use medium quality when uploaded.")
case(.low):
return NSLocalizedString("appSettings.media.imageQuality.low", value: "Low", comment: "Indicates an image will use low quality when uploaded.")
}
}
}

enum VideoResolution: String {
case size640x480 = "AVAssetExportPreset640x480"
case size1280x720 = "AVAssetExportPreset1280x720"
Expand Down Expand Up @@ -110,13 +151,19 @@ class MediaSettings: NSObject {
/// - Note: if the image doesn't need to be resized, it returns `Int.max`
///
@objc var imageSizeForUpload: Int {
if maxImageSizeSetting >= maxImageDimension {
// When image optimization is enabled, setting the max image size setting to
// the maximum value will be considered as to using the original size.
if !imageOptimizationEnabled || maxImageSizeSetting >= maxImageDimension {
return Int.max
} else {
return maxImageSizeSetting
}
}

var imageQualityForUpload: ImageQuality {
return imageOptimizationEnabled ? imageQualitySetting : .high
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When image optimization is disabled, the quality used is High not Maximum. This behavior matches the previous implementation where preferredImageCompressionQuality was used:

@objc static let preferredImageCompressionQuality = 0.9

https://github.com/wordpress-mobile/WordPress-iOS/pull/21981/files#diff-c162ac46983f01e3b35e008aa8b553cc0cb2b010a84926da308ee9b875e35638L381

}

/// The stored value for the maximum size images can have before uploading.
/// If you set this to `maxImageDimension` or higher, it means images won't
/// be resized on upload.
Expand All @@ -134,7 +181,7 @@ class MediaSettings: NSObject {
database.set(newSize, forKey: maxImageSizeKey)
return Int(newSize)
} else {
return maxImageDimension
return defaultMaxImageDimension
}
}
set {
Expand All @@ -148,7 +195,7 @@ class MediaSettings: NSObject {
if let savedRemoveLocation = database.object(forKey: removeLocationKey) as? Bool {
return savedRemoveLocation
} else {
return true
return defaultRemoveLocation
}
}
set {
Expand All @@ -160,12 +207,56 @@ class MediaSettings: NSObject {
get {
guard let savedSize = database.object(forKey: maxVideoSizeKey) as? String,
let videoSize = VideoResolution(rawValue: savedSize) else {
return .sizeOriginal
return defaultMaxVideoSize
}
return videoSize
}
set {
database.set(newValue.rawValue, forKey: maxVideoSizeKey)
}
}

var imageOptimizationEnabled: Bool {
get {
if let savedImageOptimization = database.object(forKey: imageOptimizationKey) as? Bool {
return savedImageOptimization
} else {
return defaultImageOptimization
}
}
set {
database.set(newValue, forKey: imageOptimizationKey)

// If the user changes this setting manually, we disable the image optimization popup.
if advertiseImageOptimization {
advertiseImageOptimization = false
}
}
}

var imageQualitySetting: ImageQuality {
get {
guard let savedQuality = database.object(forKey: imageQualityKey) as? String,
let imageQuality = ImageQuality(rawValue: savedQuality) else {
return defaultImageQuality
}
return imageQuality
}
set {
database.set(newValue.rawValue, forKey: imageQualityKey)
}
}

var advertiseImageOptimization: Bool {
get {
if let savedAdvertiseImageOptimization = database.object(forKey: advertiseImageOptimizationKey) as? Bool {
return savedAdvertiseImageOptimization
} else {
return true
}
}
set {
database.set(newValue, forKey: advertiseImageOptimizationKey)
}
}
}
12 changes: 12 additions & 0 deletions WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -286,10 +286,14 @@ import Foundation
case accountCloseCompleted

// App Settings
case appSettingsOptimizeImagesChanged
case appSettingsMaxImageSizeChanged
case appSettingsImageQualityChanged
case appSettingsClearMediaCacheTapped
case appSettingsClearSpotlightIndexTapped
case appSettingsClearSiriSuggestionsTapped
case appSettingsOpenDeviceSettingsTapped
case appSettingsOptimizeImagesPopupTapped

// Notifications
case notificationsPreviousTapped
Expand Down Expand Up @@ -1013,6 +1017,14 @@ import Foundation
return "app_settings_clear_siri_suggestions_tapped"
case .appSettingsOpenDeviceSettingsTapped:
return "app_settings_open_device_settings_tapped"
case .appSettingsOptimizeImagesChanged:
return "app_settings_optimize_images_changed"
case .appSettingsMaxImageSizeChanged:
return "app_settings_max_image_size_changed"
case .appSettingsImageQualityChanged:
return "app_settings_image_quality_changed"
case .appSettingsOptimizeImagesPopupTapped:
return "app_settings_optimize_images_popup_tapped"

// Account Close
case .accountCloseTapped:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ struct ImageSizeModel: MediaSizeModel {
if value == maxValue {
return NSLocalizedString("Original", comment: "Indicates an image will use its original size when uploaded.")
}
let format = NSLocalizedString("%dx%dpx", comment: "Max image size in pixels (e.g. 300x300px)")
let format = NSLocalizedString("mediaSizeSlider.valueFormat", value: "%d × %d px", comment: "Max image size in pixels (e.g. 300x300px)")
return String(format: format, value, value)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,10 @@ extension GutenbergMediaPickerHelper: ImagePickerControllerDelegate {
switch mediaType {
case UTType.image.identifier:
if let image = info[.originalImage] as? UIImage {
self.didPickMediaCallback?([image])
self.didPickMediaCallback = nil
MediaHelper.advertiseImageOptimization() { [self] in
self.didPickMediaCallback?([image])
self.didPickMediaCallback = nil
}
}

case UTType.movie.identifier:
Expand Down Expand Up @@ -108,7 +110,20 @@ extension GutenbergMediaPickerHelper: PHPickerViewControllerDelegate {
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
context.dismiss(animated: true)

didPickMediaCallback?(results.map(\.itemProvider))
didPickMediaCallback = nil
guard results.count > 0 else {
return
}

let mediaFilter = picker.configuration.filter
if mediaFilter == PHPickerFilter(.all) || mediaFilter == PHPickerFilter(.image) {
MediaHelper.advertiseImageOptimization() { [self] in
didPickMediaCallback?(results.map(\.itemProvider))
didPickMediaCallback = nil
}
}
else {
didPickMediaCallback?(results.map(\.itemProvider))
didPickMediaCallback = nil
}
}
}
Loading