Skip to content

Commit ac135bc

Browse files
committed
Implement new ReaderPostCell
1 parent 1d85989 commit ac135bc

15 files changed

+1106
-18
lines changed

WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ import Foundation
115115
case readerArticleTextCopied
116116
case readerCommentTextHighlighted
117117
case readerCommentTextCopied
118+
case readerPostContextMenuButtonTapped
118119

119120
// Stats - Empty Stats nudges
120121
case statsPublicizeNudgeShown
@@ -834,6 +835,8 @@ import Foundation
834835
return "reader_comment_text_highlighted"
835836
case .readerCommentTextCopied:
836837
return "reader_comment_text_copied"
838+
case .readerPostContextMenuButtonTapped:
839+
return "reader_post_context_menu_button_tapped"
837840

838841
// Stats - Empty Stats nudges
839842
case .statsPublicizeNudgeShown:
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import UIKit
2+
3+
// Fetches URLs for favicons for sites.
4+
actor FaviconService {
5+
static let shared = FaviconService()
6+
7+
private nonisolated let cache = FaviconCache()
8+
9+
private let session = URLSession(configuration: {
10+
let configuration = URLSessionConfiguration.default
11+
configuration.urlCache = nil
12+
return configuration
13+
}())
14+
15+
private var tasks: [URL: FaviconTask] = [:]
16+
17+
nonisolated func cachedFavicon(forURL siteURL: URL) -> URL? {
18+
cache.cachedFavicon(forURL: siteURL)
19+
}
20+
21+
/// Returns a favicon URL for the given site.
22+
func favicon(forURL siteURL: URL) async throws -> URL {
23+
if let faviconURL = cache.cachedFavicon(forURL: siteURL) {
24+
return faviconURL
25+
}
26+
let faviconURL = try await _favicon(forURL: siteURL)
27+
cache.storeCachedFaviconURL(faviconURL, forURL: siteURL)
28+
return faviconURL
29+
}
30+
31+
private func _favicon(forURL siteURL: URL) async throws -> URL {
32+
let task = tasks[siteURL] ?? FaviconTask { [session] in
33+
let (data, response) = try await session.data(from: siteURL)
34+
try validate(response: response)
35+
return await makeFavicon(from: data, siteURL: siteURL)
36+
}
37+
let subscriptionID = UUID()
38+
task.subscriptions.insert(subscriptionID)
39+
tasks[siteURL] = task
40+
return try await withTaskCancellationHandler {
41+
try await task.task.value
42+
} onCancel: {
43+
Task {
44+
await self.unsubscribe(subscriptionID, key: siteURL)
45+
}
46+
}
47+
}
48+
49+
private func unsubscribe(_ subscriptionID: UUID, key: URL) {
50+
guard let task = tasks[key],
51+
task.subscriptions.remove(subscriptionID) != nil,
52+
task.subscriptions.isEmpty else {
53+
return
54+
}
55+
task.task.cancel()
56+
tasks[key] = nil
57+
}
58+
}
59+
60+
enum FaviconError: Error {
61+
case unacceptableStatusCode(_ code: Int)
62+
}
63+
64+
private final class FaviconCache: @unchecked Sendable {
65+
private let cache = NSCache<AnyObject, AnyObject>()
66+
67+
func cachedFavicon(forURL siteURL: URL) -> URL? {
68+
cache.object(forKey: siteURL as NSURL) as? URL
69+
}
70+
71+
func storeCachedFaviconURL(_ faviconURL: URL, forURL siteURL: URL) {
72+
cache.setObject(faviconURL as NSURL, forKey: siteURL as NSURL)
73+
}
74+
}
75+
76+
private let regex: NSRegularExpression? = {
77+
let pattern = "<link[^>]*rel=\"apple-touch-icon\"[^>]*href=\"([^\"]+)\"[^>]*>"
78+
return try? NSRegularExpression(pattern: pattern, options: .caseInsensitive)
79+
}()
80+
81+
private func makeFavicon(from data: Data, siteURL: URL) async -> URL {
82+
let html = String(data: data, encoding: .utf8) ?? ""
83+
let range = NSRange(location: 0, length: html.utf16.count)
84+
if let match = regex?.firstMatch(in: html, options: [], range: range),
85+
let matchRange = Range(match.range(at: 1), in: html),
86+
let faviconURL = URL(string: String(html[matchRange]), relativeTo: siteURL) {
87+
return faviconURL
88+
}
89+
// Fallback to standard favicon path. It has low quality, but
90+
// it's better than nothing.
91+
return siteURL.appendingPathComponent("favicon.icon")
92+
}
93+
94+
private func validate(response: URLResponse) throws {
95+
guard let response = response as? HTTPURLResponse else {
96+
return
97+
}
98+
guard (200..<400).contains(response.statusCode) else {
99+
throw FaviconError.unacceptableStatusCode(response.statusCode)
100+
}
101+
}
102+
103+
private final class FaviconTask {
104+
var subscriptions = Set<UUID>()
105+
var isCancelled = false
106+
var task: Task<URL, Error>
107+
108+
init(_ closure: @escaping () async throws -> URL) {
109+
self.task = Task { try await closure() }
110+
}
111+
}

WordPress/Classes/Utility/Media/ImageView.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ final class ImageView: UIView {
1616
case spinner
1717
}
1818

19+
var isErrorViewEnabled = true
1920
var loadingStyle = LoadingStyle.background
2021

2122
override init(frame: CGRect) {
@@ -70,7 +71,9 @@ final class ImageView: UIView {
7071
imageView.isHidden = false
7172
backgroundColor = .clear
7273
case .failure:
73-
makeErrorView().isHidden = false
74+
if isErrorViewEnabled {
75+
makeErrorView().isHidden = false
76+
}
7477
}
7578
}
7679

WordPress/Classes/ViewRelated/Reader/ReaderCardsStreamViewController.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,15 @@ class ReaderCardsStreamViewController: ReaderStreamViewController {
9797
let cell = tableView.dequeueReusableCell(withIdentifier: readerCardTopicsIdentifier) as! ReaderTopicsCardCell
9898
cell.configure(interests)
9999
cell.delegate = self
100+
hideSeparator(for: cell)
100101
return cell
101102
}
102103

103104
func cell(for sites: [ReaderSiteTopic]) -> UITableViewCell {
104105
let cell = tableView.dequeueReusableCell(withIdentifier: readerCardSitesIdentifier) as! ReaderSitesCardCell
105106
cell.configure(sites)
106107
cell.delegate = self
108+
hideSeparator(for: cell)
107109
return cell
108110
}
109111

0 commit comments

Comments
 (0)