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

Feature Branch 25.7 #23923

Open
wants to merge 109 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
109 commits
Select commit Hold shift + click to select a range
5812760
Rename ImageLoadingController
kean Dec 23, 2024
d807dfe
Add LightboxViewController to replace WPImageViewController
kean Dec 24, 2024
0d05ae8
Integrate LightboxViewController in Reader
kean Dec 24, 2024
1407116
Add Media support in LightboxViewController
kean Dec 24, 2024
79c49ae
Add convenience init to LightboxViewController
kean Dec 24, 2024
246b122
Integrate LightboxViewController in SiteMedia
kean Dec 24, 2024
6ed4454
Integrate LightboxViewController in ReaderDetailsCoordinator (cover i…
kean Dec 24, 2024
95399b0
INtegrate in DefaultContentCoordinator
kean Dec 24, 2024
b3a84ac
Integrate LightboxViewController in Guteberg
kean Dec 24, 2024
8fbeb66
Integrate LightboxViewController in ExternalMediaPickerViewController
kean Dec 24, 2024
8f7dd16
Integrate LightboxViewController in PostSettingsViewController (featu…
kean Dec 24, 2024
8096de9
Remove FeaturedImageViewController (ObjC)
kean Dec 24, 2024
88c7b13
Rewrite PostFeaturedImageCell
kean Dec 24, 2024
da7b29a
Integrate LightboxViewController in ReaderCommentsViewController
kean Dec 24, 2024
3354d85
Update WPRichTextImage to use AsyncImageView
kean Dec 24, 2024
c3993e0
Automatically pick thumbnail when available
kean Dec 24, 2024
36b392e
Remove WPImageViewController
kean Dec 24, 2024
342e63e
Update release notes
kean Dec 24, 2024
d613c05
Remove ImageLoader
kean Dec 24, 2024
2ac4c2e
Remove ImageDimensionParser
kean Dec 24, 2024
5141337
Update MediaItemHeaderView to use AsyncImageView instead of CachedAni…
kean Dec 24, 2024
a3adfee
Fix code formatting in RichTextView
kean Dec 24, 2024
61871d0
Update AnimatedGifAttachmentViewProvider to use GIFImageView directly
kean Dec 24, 2024
4b3f4ef
Remove SolidColorActivityIndicator
kean Dec 24, 2024
79b3a0a
Remove CachedAnimatedImageView
kean Dec 24, 2024
c96c844
Remove GIFPlaybackStrategy
kean Dec 24, 2024
3f270d2
Update EditorMediaUtility to use ImageDownloader directly (without Au…
kean Dec 24, 2024
dca2548
Remove AuthenticatedImageDownload
kean Dec 24, 2024
f9a2315
Update MediaExternalExporter to use ImageDownloader for downloading G…
kean Dec 24, 2024
8ad55b8
Remove AnimatedImageCache
kean Dec 24, 2024
f013a31
Remove remaining AlamofireImage usages from the anouncement cells
kean Dec 24, 2024
f27c41d
Remove AlamofireImageCacheAdapter
kean Dec 24, 2024
129bc3e
Remove AlamofireImage
kean Dec 24, 2024
eece5e9
Add ImagePrefetcher
kean Dec 30, 2024
49df350
Update releaes notes
kean Dec 30, 2024
1e5f0e8
Add ImageRequest support in AsyncImageView
kean Dec 30, 2024
023b3cd
Add ImageSize
kean Dec 30, 2024
f352225
Fix an issue with blogging reminders flow not being shown after publi…
kean Dec 30, 2024
afdfd05
Remove unused LightNavigationController
kean Dec 30, 2024
af4ba9c
Remove BottomSheetViewController usage from BloggingReminders flow
kean Dec 30, 2024
c9dca5f
Simplify BloggingRemindersFlowIntroViewController
kean Dec 31, 2024
956a3f0
Add SpacerView
kean Dec 31, 2024
a49d006
Add BottomToolbarView
kean Dec 31, 2024
6431474
Fix notice covering the blogging reminders fow
kean Dec 31, 2024
79334a2
Replace FancyButton
kean Dec 31, 2024
9c0bed9
Add close button to BloggingRemindersFlowSettingsViewController
kean Dec 31, 2024
882df62
Fix BloggingRemindersTimeSelectionViewController presentation
kean Dec 31, 2024
581a10a
Remove FancyButton from BloggingRemindersPushPromptViewController
kean Dec 31, 2024
9f81f7f
Remove dismiss button (it now shows back)
kean Dec 31, 2024
1d56a16
Update BloggingRemindersPushPromptViewController layout
kean Dec 31, 2024
a5d4fc2
Remove FancyButton from BloggingRemindersFlowCompletionViewController
kean Dec 31, 2024
94dd45f
Update BloggingRemindersFlowCompletionViewController layout
kean Dec 31, 2024
7bbd838
Update releaes notes
kean Dec 31, 2024
c1c70e1
Fix typo in release notes
kean Dec 31, 2024
60a9e23
Fix compliance popover accessibility settings
kean Dec 31, 2024
a1786a4
Fix an issue with compliance popover not dismissing
kean Dec 31, 2024
195e8e8
Update release notes
kean Dec 31, 2024
f91c0d7
Remove unused CircularProgressView extensions
kean Dec 31, 2024
ae33151
Remove BottomSheetViewController usage from JetpackBrandingCoordinator
kean Jan 2, 2025
c97bbdd
Remove ottomSheetViewControllerTests
kean Jan 2, 2025
927616a
Remove BottomSheetViewController
kean Jan 2, 2025
2aa3bff
Remove DrawerPresentationController
kean Jan 2, 2025
8b886a3
Update release notes
kean Jan 2, 2025
1782da1
Add Share action to the site link on dashboard
kean Jan 2, 2025
92d446f
Remove duplicated Share actions
kean Jan 2, 2025
8921783
Remove duplicated Strings.ok
kean Jan 2, 2025
57dc107
Update release notes
kean Jan 2, 2025
86ee87b
Fix layout issues in Privacy Settings
kean Jan 2, 2025
9038395
Add assertion
kean Jan 2, 2025
df6f938
Update release notes
kean Jan 2, 2025
b9de1da
Rename WordPressMedia to AsyncImageKit
kean Jan 2, 2025
cdfbd20
Rename WordPressMedia (#23937)
kean Jan 2, 2025
c7b0a49
Remove MediaHost from AsyncImageKit
kean Jan 2, 2025
4d8d512
Move ImageDownloader.shared to AsyncImageKit
kean Jan 2, 2025
7f45fec
Move AsyncImageView and other related types to AsyncImageKit
kean Jan 2, 2025
90b1166
Fix unit tests
kean Jan 2, 2025
2c17896
Move AsyncImageView to AsyncImageKit (#23938)
kean Jan 2, 2025
330ebaf
Cleanup MediaHost initializers
kean Jan 2, 2025
58ec973
Optimize account lookup
kean Jan 2, 2025
ebbeb8b
Fix MediaHostTests
kean Jan 2, 2025
221386f
Cleanup MediaHost initializers (#23939)
kean Jan 2, 2025
42aea77
Fix crash in ReaderDetailFeaturedImageView
kean Jan 3, 2025
14b5c56
Fix RTL support in WebKitViewController
kean Jan 3, 2025
eb7bed6
Use semantic back/forward chevrons in other places
kean Jan 3, 2025
2b4ff05
Update StatsBaseCell
kean Jan 3, 2025
c227ab4
Update SiteStatsTableHeaderView
kean Jan 3, 2025
e6883c5
Replace disclosure-chevron and editor-chevron-left
kean Jan 3, 2025
379b156
Fix remainig incorrect chevron usages
kean Jan 3, 2025
5e77775
Remove remainig chevron images
kean Jan 3, 2025
161fc2d
Update release notes
kean Jan 3, 2025
3a85553
Fix separator insets on homepage
kean Jan 3, 2025
cb3816d
Fix incorrect chevron icons direction in RTL languages (#23940)
kean Jan 3, 2025
eab7835
Fix an issue with clear navigation bar background in revision browser
kean Jan 3, 2025
bf01b8f
Fix an issue with clear navigation bar background in revision browser
kean Jan 3, 2025
a337327
Fix toolbar inset to safe area in revision browser
kean Jan 3, 2025
56ef3a7
Modernize menus and stuff
kean Jan 3, 2025
87ff4b6
Fix an issue with clear navigation bar background in revision browser…
kean Jan 3, 2025
7b36ced
Fix an issue with clear navigation bar background in revision browser…
kean Jan 3, 2025
8b02a8e
Fix MediaRequestAuthenticatorTests
kean Jan 3, 2025
c74a857
Remove preflight connection check when sending replies (can be laggin…
kean Jan 3, 2025
b1709b7
Fix an issue with comments disppearing if request fails
kean Jan 3, 2025
d847e8a
Update other screens using TextView
kean Jan 3, 2025
cb47b04
Update release notes
kean Jan 3, 2025
309ddbc
Fix an issue with comments being lost on request failure (#23942)
kean Jan 3, 2025
ccbdbfb
Fix formatting
kean Jan 3, 2025
13bcd39
Fix an issue with referrers showing invalid icons
kean Jan 3, 2025
2f22b2f
Update release notes
kean Jan 3, 2025
fe31a0d
Fix an issue with Referrers in Stats showing invalid icons (#23943)
kean Jan 3, 2025
4c2f229
Remove some of the scenarios where isInternetConnected used
kean Jan 4, 2025
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
20 changes: 11 additions & 9 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.0"),
Expand Down Expand Up @@ -52,23 +52,26 @@ 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)]),
.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 +146,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
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