Skip to content

Commit

Permalink
Feature Branch 25.7 (#23923)
Browse files Browse the repository at this point in the history
* Rename ImageLoadingController

* Add LightboxViewController to replace WPImageViewController

* Integrate LightboxViewController in Reader

* Add Media support in LightboxViewController

* Add convenience init to LightboxViewController

* Integrate LightboxViewController in SiteMedia

* Integrate LightboxViewController in ReaderDetailsCoordinator (cover image)

* INtegrate in DefaultContentCoordinator

* Integrate LightboxViewController in Guteberg

* Integrate LightboxViewController in ExternalMediaPickerViewController

* Integrate LightboxViewController in PostSettingsViewController (featured image)

* Remove FeaturedImageViewController (ObjC)

* Rewrite PostFeaturedImageCell

* Integrate LightboxViewController in ReaderCommentsViewController

* Update WPRichTextImage to use AsyncImageView

* Automatically pick thumbnail when available

* Remove WPImageViewController

* Update release notes

* Remove ImageLoader

* Remove ImageDimensionParser

* Update MediaItemHeaderView to use AsyncImageView instead of CachedAnimatedImageView

* Fix code formatting in RichTextView

* Update AnimatedGifAttachmentViewProvider to use GIFImageView directly

* Remove SolidColorActivityIndicator

* Remove CachedAnimatedImageView

* Remove GIFPlaybackStrategy

* Update EditorMediaUtility to use ImageDownloader directly (without AuthenticatedImageDownload redirect)

* Remove AuthenticatedImageDownload

* Update MediaExternalExporter to use ImageDownloader for downloading GIF data

* Remove AnimatedImageCache

* Remove remaining AlamofireImage usages from the anouncement cells

* Remove AlamofireImageCacheAdapter

* Remove AlamofireImage

* Add ImagePrefetcher

* Update releaes notes

* Add ImageRequest support in AsyncImageView

* Add ImageSize

* Fix an issue with blogging reminders flow not being shown after publishing a new post

* Remove unused LightNavigationController

* Remove BottomSheetViewController usage from BloggingReminders flow

* Simplify BloggingRemindersFlowIntroViewController

* Add SpacerView

* Add BottomToolbarView

* Fix notice covering the blogging reminders fow

* Replace FancyButton

* Add close button to BloggingRemindersFlowSettingsViewController

* Fix BloggingRemindersTimeSelectionViewController presentation

* Remove FancyButton from BloggingRemindersPushPromptViewController

* Remove dismiss button (it now shows back)

* Update BloggingRemindersPushPromptViewController layout

* Remove FancyButton from BloggingRemindersFlowCompletionViewController

* Update BloggingRemindersFlowCompletionViewController layout

* Update releaes notes

* Fix typo in release notes

* Fix compliance popover accessibility settings

* Fix an issue with compliance popover not dismissing

* Update release notes

* Remove unused CircularProgressView extensions

* Remove BottomSheetViewController usage from JetpackBrandingCoordinator

* Remove ottomSheetViewControllerTests

* Remove BottomSheetViewController

* Remove DrawerPresentationController

* Update release notes

* Add Share action to the site link on dashboard

* Remove duplicated Share actions

* Remove duplicated Strings.ok

* Update release notes

* Fix layout issues in Privacy Settings

* Add assertion

* Update release notes

* Rename WordPressMedia to AsyncImageKit

* Remove MediaHost from AsyncImageKit

* Move ImageDownloader.shared to AsyncImageKit

* Move AsyncImageView and other related types to AsyncImageKit

* Fix unit tests

* Cleanup MediaHost initializers

* Optimize account lookup

* Fix MediaHostTests

* Fix crash in ReaderDetailFeaturedImageView

* Fix RTL support in WebKitViewController

* Use semantic back/forward chevrons in other places

* Update StatsBaseCell

* Update SiteStatsTableHeaderView

* Replace disclosure-chevron and editor-chevron-left

* Fix remainig incorrect chevron usages

* Remove remainig chevron images

* Update release notes

* Fix separator insets on homepage

* Fix an issue with clear navigation bar background in revision browser

* Fix an issue with clear navigation bar background in revision browser

* Fix toolbar inset to safe area in revision browser

* Modernize menus and stuff

* Fix MediaRequestAuthenticatorTests

* Remove preflight connection check when sending replies (can be lagging behind)

* Fix an issue with comments disppearing if request fails

* Update other screens using TextView

* Update release notes

* Fix formatting

* Fix an issue with referrers showing invalid icons

* Update release notes

* Remove some of the scenarios where isInternetConnected used

* Update site menu style on iPhone

* Update release notes

* Integrate zoom transitions in Theme browser

* Update release notes

* Fix tint colors in wpios

* Remove UIAppColor.brand

* Enable zoom transitions in Reader (iPad)

* Update release notes

* Remove unused isVisibleInScrollView

* Enable toolbar hiding on iPad

* Fix ReaderDetailFeaturedImageView gradienet showing up when no image is present

* Fix an issue with Publisize options appearing in the prepublishing sheet for XMLRPC sites

* Fix code formatting and remove unused imports

* Move SiteIconView to WordPressUI

* Update Share extension to use SiteIconView

* Remove UIImageView+Blavatar

* Use firstLetter

* Update release notes

* Disable universal links support for QR code login

* Fix an issue with the confirmation screen shown more than once

* Update release notes

* Enable fast deceleration for filters on the Discover tab

* Update release notes

* Show selected filter in the Discover navigation bar

* Update release notes

* Remove unused makeCreateButtonAnnouncementAlertController

* Add scroll-to-top button to Reader

* Cleanup

* Update design for iPad

* Add analytics

* Update releaes notes

* Flatten a nested localized string to avoid `genstrings` failure

* Import `WordPressUI` in `SiteIconViewModelTests`

Otherwise, it won't build

* Add initial MediaPicker implementation

* Add initial PostSettingsFeaturedImageCell implementation

* Add configurable MediaPicker content

* Add ViewModel to PostSettingsFeaturedImageCell

* Add reuseIdentifier for featured image cells

* Pass selection from MediaPicker to PostSettingsFeaturedImageViewModel

* Show upload status using PostMediaUploadItemView

* Rename MediaUploadItemViewModel

* Add PostSettingsFeaturedImageUploadView to show upload progress

* Simlify how the app shows media upload status

* Handle upload failure

* Implement featured image save

* Add support for showing a selected featured image

* Add support for removing featured image

* Simplify lightbox

* Add support for camera as a source

* Add .siteMedia(blog:) source

* Add ImagePlayground source support

* Add ImagePlayground support in MediaPicker

* Add free photos and GIFs support to MediaPicker

* Remove unused media upload code from PostSettingsViewController

* Remove WPTableViewActivityCell

* Remove WPProgressTableViewCell

* Remvove unused featured image size

* Remove more unused code

* Remove unused code

* Add SiteMediaImageView

* Remove unused code

* Integrate FeaturedImageDelegate

* Fix SiteMediaImage background when loading with spinner

* Fix animations

* Add zoom transition

* Add shadow to more menu

* Make the entire cell tappable

* Add View action

* Add replace action

* Show spinner when replacing an image

* Remove unused reloadFeaturedImageCell

* Update release notse

* Revert "Update site menu style on iPhone"

This reverts commit 565a34b.

* Fix an issue with wrong cover images appearing in Reader (#23914)

* Fix an issue with wrong cover images appearing in Reader

* Update release notes

* Update release notes

* Point back to wpios-edition

* Fix an issue with non-stable order in Posts and Pages in stats (#23915)

* Fix an issue with non-stable order in Posts and Pages in stats

* Update release notes

* Update release notes

* Fix an issue with a missing "Mark as Unread" button in the More menu (#23917)

* Add missing toggle read/unread button

* Show read status in the list

* Update release notes

* Update release notes

* Add missing social sharing icons (#23918)

* Add missing social sharing icons

* Update release notes

* Update release notes

* Update release notes

* Fix build

* Update UI tests

* Use medium font for main navigation area in Reader to align with Home

* Remove commented-out code

* Add context menus and previews for sites in Reader (#23964)

* Fix l10n typo

* Add Unsubscribe context menu to Reader sidebar sites

* Extract ReaderSiteFavoriteButton

* Move actions to ReaderSidebarSubscriptionCell

* Extract ReaderSubscriptionContextMenu and add Share

* Add Notification Settings and Copy Link buttons

* Add context menu for sites in Subscriptions view

* Add previews

* Fix notification settings sometimes being clipped on iPad

* Fix layout in ReaderSubscriptionCel actions

* Update release notes

* Fix more button color in dark mode

* Fix an issue with fullscreen button in reply view clipped by the notch (#23965)

* Fix an issue with fullscreen button in reply view clipped by the notch

* Update release notes

* Fix display of certain topics in Discover recommendations

* Remove "Lazy Images" Jetpack option (#23966)

* Remove lazy loading:

* Add error handling

* Update release notes

* Update WordPressKit (has lazy-load fix)

* Update release notes

* Reoder site actions

* Update release notes

* Add missing imports

---------

Co-authored-by: Gio Lodi <[email protected]>
  • Loading branch information
kean and mokagio authored Jan 13, 2025
1 parent 388dbb1 commit 1e120e3
Show file tree
Hide file tree
Showing 342 changed files with 3,941 additions and 7,308 deletions.
31 changes: 21 additions & 10 deletions Modules/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,18 @@ let package = Package(
.iOS(.v16),
],
products: XcodeSupport.products + [
.library(name: "JetpackStatsWidgetsCore", targets: ["JetpackStatsWidgetsCore"]),
.library(name: "AsyncImageKit", targets: ["AsyncImageKit"]),
.library(name: "DesignSystem", targets: ["DesignSystem"]),
.library(name: "JetpackStatsWidgetsCore", targets: ["JetpackStatsWidgetsCore"]),
.library(name: "WordPressFlux", targets: ["WordPressFlux"]),
.library(name: "WordPressMedia", targets: ["WordPressMedia"]),
.library(name: "WordPressShared", targets: ["WordPressShared"]),
.library(name: "WordPressUI", targets: ["WordPressUI"]),
],
dependencies: [
.package(url: "https://github.com/airbnb/lottie-ios", from: "4.4.0"),
.package(url: "https://github.com/Alamofire/Alamofire", from: "5.9.1"),
.package(url: "https://github.com/Alamofire/AlamofireImage", from: "4.3.0"),
.package(url: "https://github.com/AliSoftware/OHHTTPStubs", from: "9.1.0"),
.package(url: "https://github.com/apple/swift-collections", from: "1.0.0"),
.package(url: "https://github.com/Automattic/Automattic-Tracks-iOS", from: "3.4.2"),
.package(url: "https://github.com/Automattic/AutomatticAbout-swift", from: "1.1.4"),
.package(url: "https://github.com/Automattic/Gravatar-SDK-iOS", from: "3.1.1"),
Expand Down Expand Up @@ -52,23 +52,34 @@ let package = Package(
.package(url: "https://github.com/Automattic/color-studio", branch: "trunk"),
],
targets: XcodeSupport.targets + [
.target(name: "JetpackStatsWidgetsCore", swiftSettings: [.swiftLanguageMode(.v5)]),
.target(name: "AsyncImageKit", dependencies: [
.product(name: "Collections", package: "swift-collections"),
.product(name: "Gifu", package: "Gifu"),
]),
.target(name: "DesignSystem", swiftSettings: [.swiftLanguageMode(.v5)]),
.target(name: "JetpackStatsWidgetsCore", swiftSettings: [.swiftLanguageMode(.v5)]),
.target(name: "UITestsFoundation", dependencies: [
.product(name: "ScreenObject", package: "ScreenObject"),
.product(name: "XCUITestHelpers", package: "XCUITestHelpers"),
], swiftSettings: [.swiftLanguageMode(.v5)]),
.target(name: "WordPressFlux", swiftSettings: [.swiftLanguageMode(.v5)]),
.target(name: "WordPressMedia"),
.target(name: "WordPressSharedObjC", resources: [.process("Resources")], swiftSettings: [.swiftLanguageMode(.v5)]),
.target(name: "WordPressShared", dependencies: [.target(name: "WordPressSharedObjC")], resources: [.process("Resources")], swiftSettings: [.swiftLanguageMode(.v5)]),
.target(name: "WordPressTesting", resources: [.process("Resources")]),
.target(name: "WordPressUI", dependencies: [.target(name: "WordPressShared")], resources: [.process("Resources")], swiftSettings: [.swiftLanguageMode(.v5)]),
.target(
name: "WordPressUI",
dependencies: [
"AsyncImageKit",
.target(name: "WordPressShared")
],
resources: [.process("Resources")],
swiftSettings: [.swiftLanguageMode(.v5)]
),
.testTarget(name: "JetpackStatsWidgetsCoreTests", dependencies: [.target(name: "JetpackStatsWidgetsCore")], swiftSettings: [.swiftLanguageMode(.v5)]),
.testTarget(name: "DesignSystemTests", dependencies: [.target(name: "DesignSystem")], swiftSettings: [.swiftLanguageMode(.v5)]),
.testTarget(name: "WordPressFluxTests", dependencies: ["WordPressFlux"], swiftSettings: [.swiftLanguageMode(.v5)]),
.testTarget(name: "WordPressMediaTests", dependencies: [
.target(name: "WordPressMedia"),
.testTarget(name: "AsyncImageKitTests", dependencies: [
.target(name: "AsyncImageKit"),
.target(name: "WordPressTesting"),
.product(name: "OHHTTPStubsSwift", package: "OHHTTPStubs")
]),
Expand Down Expand Up @@ -143,10 +154,9 @@ enum XcodeSupport {
"JetpackStatsWidgetsCore",
"WordPressFlux",
"WordPressShared",
"WordPressMedia",
"AsyncImageKit",
"WordPressUI",
.product(name: "Alamofire", package: "Alamofire"),
.product(name: "AlamofireImage", package: "AlamofireImage"),
.product(name: "AutomatticAbout", package: "AutomatticAbout-swift"),
.product(name: "AutomatticTracks", package: "Automattic-Tracks-iOS"),
.product(name: "CocoaLumberjack", package: "CocoaLumberjack"),
Expand Down Expand Up @@ -191,6 +201,7 @@ enum XcodeSupport {
.xcodeTarget("XcodeTarget_StatsWidget", dependencies: [
"JetpackStatsWidgetsCore",
"WordPressShared",
"WordPressUI",
.product(name: "CocoaLumberjackSwift", package: "CocoaLumberjack"),
.product(name: "WordPressAPI", package: "wordpress-rs"),
.product(name: "ColorStudio", package: "color-studio"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ private extension Data {
}
}

private extension CGSize {
extension CGSize {
func scaled(by scale: CGFloat) -> CGSize {
CGSize(width: width * scale, height: height * scale)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import UIKit
/// The system that downloads and caches images, and prepares them for display.
@ImageDownloaderActor
public final class ImageDownloader {
public nonisolated static let shared = ImageDownloader()

private nonisolated let cache: MemoryCacheProtocol
private let authenticator: MediaRequestAuthenticatorProtocol?

private let urlSession = URLSession {
$0.urlCache = nil
Expand All @@ -21,14 +22,12 @@ public final class ImageDownloader {
private var tasks: [String: ImageDataTask] = [:]

public nonisolated init(
cache: MemoryCacheProtocol = MemoryCache.shared,
authenticator: MediaRequestAuthenticatorProtocol?
cache: MemoryCacheProtocol = MemoryCache.shared
) {
self.cache = cache
self.authenticator = authenticator
}

public func image(from url: URL, host: MediaHost? = nil, options: ImageRequestOptions = .init()) async throws -> UIImage {
public func image(from url: URL, host: MediaHostProtocol? = nil, options: ImageRequestOptions = .init()) async throws -> UIImage {
try await image(for: ImageRequest(url: url, host: host, options: options))
}

Expand All @@ -39,7 +38,7 @@ public final class ImageDownloader {
return image
}
let data = try await data(for: request)
let image = try await ImageDecoder.makeImage(from: data, size: options.size)
let image = try await ImageDecoder.makeImage(from: data, size: options.size.map(CGSize.init))
if options.isMemoryCacheEnabled {
cache[key] = image
}
Expand All @@ -55,8 +54,8 @@ public final class ImageDownloader {
switch request.source {
case .url(let url, let host):
var request: URLRequest
if let host, let authenticator {
request = try await authenticator.authenticatedRequest(for: url, host: host)
if let host {
request = try await host.authenticatedRequest(for: url)
} else {
request = URLRequest(url: url)
}
Expand All @@ -69,24 +68,30 @@ public final class ImageDownloader {

// MARK: - Caching

/// Returns an image from the memory cache.
nonisolated public func cachedImage(for request: ImageRequest) -> UIImage? {
guard let imageURL = request.source.url else { return nil }
return cachedImage(for: imageURL, size: request.options.size)
}

/// Returns an image from the memory cache.
///
/// - note: Use it to retrieve the image synchronously, which is no not possible
/// with the async functions.
nonisolated public func cachedImage(for imageURL: URL, size: CGSize? = nil) -> UIImage? {
nonisolated public func cachedImage(for imageURL: URL, size: ImageSize? = nil) -> UIImage? {
cache[makeKey(for: imageURL, size: size)]
}

nonisolated public func setCachedImage(_ image: UIImage?, for imageURL: URL, size: CGSize? = nil) {
nonisolated public func setCachedImage(_ image: UIImage?, for imageURL: URL, size: ImageSize? = nil) {
cache[makeKey(for: imageURL, size: size)] = image
}

private nonisolated func makeKey(for imageURL: URL?, size: CGSize?) -> String {
private nonisolated func makeKey(for imageURL: URL?, size: ImageSize?) -> String {
guard let imageURL else {
assertionFailure("The request.url was nil") // This should never happen
return ""
}
return imageURL.absoluteString + (size.map { "?size=\($0)" } ?? "")
return imageURL.absoluteString + (size.map { "?w=\($0.width),h=\($0.height)" } ?? "")
}

public func clearURLSessionCache() {
Expand Down Expand Up @@ -189,6 +194,6 @@ private extension URLSession {
}
}

public protocol MediaRequestAuthenticatorProtocol: Sendable {
@MainActor func authenticatedRequest(for url: URL, host: MediaHost) async throws -> URLRequest
public protocol MediaHostProtocol: Sendable {
@MainActor func authenticatedRequest(for url: URL) async throws -> URLRequest
}
111 changes: 111 additions & 0 deletions Modules/Sources/AsyncImageKit/ImagePrefetcher.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import UIKit
import Collections

@ImageDownloaderActor
public final class ImagePrefetcher {
private let downloader: ImageDownloader
private let maxConcurrentTasks: Int
private var queue = OrderedDictionary<PrefetchKey, PrefetchTask>()
private var numberOfActiveTasks = 0

deinit {
let tasks = queue.values.compactMap(\.task)
for task in tasks {
task.cancel()
}
}

public nonisolated init(
downloader: ImageDownloader = .shared,
maxConcurrentTasks: Int = 2
) {
self.downloader = downloader
self.maxConcurrentTasks = maxConcurrentTasks
}

public nonisolated func startPrefetching(for requests: [ImageRequest]) {
Task { @ImageDownloaderActor in
for request in requests {
startPrefetching(for: request)
}
performPendingTasks()
}
}

private func startPrefetching(for request: ImageRequest) {
let key = PrefetchKey(request: request)
guard queue[key] == nil else {
return
}
queue[key] = PrefetchTask()
}

private func performPendingTasks() {
var index = 0
func nextPendingTask() -> (PrefetchKey, PrefetchTask)? {
while index < queue.count {
if queue.elements[index].value.task == nil {
return queue.elements[index]
}
index += 1
}
return nil
}
while numberOfActiveTasks < maxConcurrentTasks, let (key, task) = nextPendingTask() {
task.task = Task {
await self.actuallyPrefetchImage(for: key.request)
}
numberOfActiveTasks += 1
}
}

private func actuallyPrefetchImage(for request: ImageRequest) async {
_ = try? await downloader.image(for: request)

numberOfActiveTasks -= 1
queue[PrefetchKey(request: request)] = nil
performPendingTasks()
}

public nonisolated func stopPrefetching(for requests: [ImageRequest]) {
Task { @ImageDownloaderActor in
for request in requests {
stopPrefetching(for: request)
}
performPendingTasks()
}
}

private func stopPrefetching(for request: ImageRequest) {
let key = PrefetchKey(request: request)
if let task = queue.removeValue(forKey: key) {
task.task?.cancel()
}
}

public nonisolated func stopAll() {
Task { @ImageDownloaderActor in
for (_, value) in queue {
value.task?.cancel()
}
queue.removeAll()
}
}

private struct PrefetchKey: Hashable, Sendable {
let request: ImageRequest

func hash(into hasher: inout Hasher) {
request.source.url?.hash(into: &hasher)
}

static func == (lhs: PrefetchKey, rhs: PrefetchKey) -> Bool {
let (lhs, rhs) = (lhs.request, rhs.request)
return (lhs.source.url, lhs.options) == (rhs.source.url, rhs.options)
}
}

private final class PrefetchTask: @unchecked Sendable {
var task: Task<Void, Error>?
}
}
84 changes: 84 additions & 0 deletions Modules/Sources/AsyncImageKit/ImageRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import UIKit

public final class ImageRequest: Sendable {
public enum Source: Sendable {
case url(URL, MediaHostProtocol?)
case urlRequest(URLRequest)

var url: URL? {
switch self {
case .url(let url, _): url
case .urlRequest(let request): request.url
}
}
}

let source: Source
let options: ImageRequestOptions

public init(url: URL, host: MediaHostProtocol? = nil, options: ImageRequestOptions = .init()) {
self.source = .url(url, host)
self.options = options
}

public init(urlRequest: URLRequest, options: ImageRequestOptions = .init()) {
self.source = .urlRequest(urlRequest)
self.options = options
}
}

public struct ImageRequestOptions: Hashable, Sendable {
/// Resize the thumbnail to the given size. By default, `nil`.
public var size: ImageSize?

/// If enabled, uses ``MemoryCache`` for caching decompressed images.
public var isMemoryCacheEnabled = true

/// If enabled, uses `URLSession` preconfigured with a custom `URLCache`
/// with a relatively high disk capacity. By default, `true`.
public var isDiskCacheEnabled = true

public init(
size: ImageSize? = nil,
isMemoryCacheEnabled: Bool = true,
isDiskCacheEnabled: Bool = true
) {
self.size = size
self.isMemoryCacheEnabled = isMemoryCacheEnabled
self.isDiskCacheEnabled = isDiskCacheEnabled
}
}

/// Image size in **pixels**.
public struct ImageSize: Hashable, Sendable {
public let width: CGFloat
public let height: CGFloat

public init(width: CGFloat, height: CGFloat) {
self.width = width
self.height = height
}

public init(_ size: CGSize) {
self.width = size.width
self.height = size.height
}

/// Initializes `ImageSize` with the given size scaled for the given view.
@MainActor
public init(scaling size: CGSize, in view: UIView) {
self.init(size.scaled(by: view.traitCollection.displayScale))
}

/// Initializes `ImageSize` with the given size scaled for the current trait
/// collection display scale.
public init(scaling size: CGSize) {
self.init(size.scaled(by: UITraitCollection.current.displayScale))
}
}

extension CGSize {
init(_ size: ImageSize) {
self.init(width: size.width, height: size.height)
}
}
Loading

0 comments on commit 1e120e3

Please sign in to comment.