Skip to content

Commit 6895a24

Browse files
committed
Merge branch 'release/26.5' into trunk
2 parents 04e7fe0 + b8bc6f1 commit 6895a24

File tree

47 files changed

+680
-594
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+680
-594
lines changed

Modules/Sources/AsyncImageKit/Helpers/FaviconService.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ private final class FaviconCache: @unchecked Sendable {
7474
}
7575

7676
private let regex: NSRegularExpression? = {
77-
let pattern = "<link[^>]*rel=\"apple-touch-icon\"[^>]*href=\"([^\"]+)\"[^>]*>"
77+
let pattern = "<link[^>]*rel=\"apple-touch-icon(?:-precomposed)\"[^>]*href=\"([^\"]+)\"[^>]*>"
7878
return try? NSRegularExpression(pattern: pattern, options: .caseInsensitive)
7979
}()
8080

Modules/Sources/WordPressKit/ReaderFeed.swift

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import Foundation
55
/// (read/feed?q=query)
66
///
77
public struct ReaderFeed: Decodable {
8-
public let url: URL
9-
public let title: String
8+
public let url: URL?
9+
public let title: String?
1010
public let feedDescription: String?
1111
public let feedID: String?
1212
public let blogID: String?
@@ -26,15 +26,7 @@ public struct ReaderFeed: Decodable {
2626

2727
private enum DataKeys: CodingKey {
2828
case site
29-
}
30-
31-
private enum SiteKeys: CodingKey {
32-
case description
33-
case icon
34-
}
35-
36-
private enum IconKeys: CodingKey {
37-
case img
29+
case feed
3830
}
3931

4032
public init(from decoder: Decoder) throws {
@@ -44,32 +36,77 @@ public struct ReaderFeed: Decodable {
4436
// - We want to decode whatever we can get, and not fail if neither of those exist
4537
let rootContainer = try decoder.container(keyedBy: CodingKeys.self)
4638

47-
url = try rootContainer.decode(URL.self, forKey: .url)
48-
title = try rootContainer.decode(String.self, forKey: .title)
39+
var feedURL = try? rootContainer.decodeIfPresent(URL.self, forKey: .url)
40+
var title = try? rootContainer.decodeIfPresent(String.self, forKey: .title)
4941
feedID = try? rootContainer.decode(String.self, forKey: .feedID)
5042
blogID = try? rootContainer.decode(String.self, forKey: .blogID)
5143

5244
var feedDescription: String?
5345
var blavatarURL: URL?
5446

47+
// Try to parse both site and feed data from meta.data
5548
do {
5649
let metaContainer = try rootContainer.nestedContainer(keyedBy: MetaKeys.self, forKey: .meta)
5750
let dataContainer = try metaContainer.nestedContainer(keyedBy: DataKeys.self, forKey: .data)
58-
let siteContainer = try dataContainer.nestedContainer(keyedBy: SiteKeys.self, forKey: .site)
59-
feedDescription = try? siteContainer.decode(String.self, forKey: .description)
6051

61-
let iconContainer = try siteContainer.nestedContainer(keyedBy: IconKeys.self, forKey: .icon)
62-
blavatarURL = try? iconContainer.decode(URL.self, forKey: .img)
52+
let siteData = try? dataContainer.decode(SiteOrFeedData.self, forKey: .site)
53+
let feedData = try? dataContainer.decode(SiteOrFeedData.self, forKey: .feed)
54+
55+
// Use data from either source, preferring site data when both are available
56+
feedDescription = siteData?.description ?? feedData?.description
57+
blavatarURL = siteData?.iconURL ?? feedData?.iconURL
58+
59+
// Fixes CMM-1002: in some cases, the backend fails to embed certain fields
60+
// directly in the feed object
61+
if feedURL == nil {
62+
feedURL = siteData?.url ?? feedData?.url
63+
}
64+
if title == nil {
65+
title = siteData?.title ?? feedData?.title
66+
}
6367
} catch {
6468
}
6569

70+
self.url = feedURL
71+
self.title = title
6672
self.feedDescription = feedDescription
6773
self.blavatarURL = blavatarURL
6874
}
6975
}
7076

77+
private struct SiteOrFeedData: Decodable {
78+
var title: String?
79+
var description: String?
80+
var iconURL: URL?
81+
var url: URL?
82+
83+
enum CodingKeys: String, CodingKey {
84+
case description
85+
case icon
86+
case url = "URL"
87+
case name
88+
}
89+
90+
private enum IconKeys: CodingKey {
91+
case img
92+
}
93+
94+
init(from decoder: Decoder) throws {
95+
let container = try decoder.container(keyedBy: CodingKeys.self)
96+
97+
title = try? container.decodeIfPresent(String.self, forKey: .name)
98+
description = try? container.decodeIfPresent(String.self, forKey: .description)
99+
url = try? container.decodeIfPresent(URL.self, forKey: .url)
100+
101+
// Try to decode the icon URL from the nested icon dictionary
102+
if let iconContainer = try? container.nestedContainer(keyedBy: IconKeys.self, forKey: .icon) {
103+
iconURL = try? iconContainer.decode(URL.self, forKey: .img)
104+
}
105+
}
106+
}
107+
71108
extension ReaderFeed: CustomStringConvertible {
72109
public var description: String {
73-
return "<Feed | URL: \(url), title: \(title), feedID: \(String(describing: feedID)), blogID: \(String(describing: blogID))>"
110+
return "<Feed | URL: \(String(describing: url)), title: \(String(describing: title)), feedID: \(String(describing: feedID)), blogID: \(String(describing: blogID))>"
74111
}
75112
}

Modules/Sources/WordPressKit/ReaderPostServiceRemote+V2.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
import Foundation
22

3+
public struct ResolvedReaderPost: Decodable {
4+
public let postId: UInt64
5+
public let siteId: UInt64
6+
7+
enum CodingKeys: String, CodingKey {
8+
case postId = "post_id"
9+
case siteId = "site_id"
10+
}
11+
}
12+
313
extension ReaderPostServiceRemote {
414
/// Returns a collection of RemoteReaderPost
515
/// This method returns the best available content for the given topics.
@@ -31,6 +41,23 @@ extension ReaderPostServiceRemote {
3141
})
3242
}
3343

44+
public func resolveUrl(
45+
_ url: URL,
46+
success: @escaping (ResolvedReaderPost) -> Void,
47+
failure: @escaping (Error) -> Void
48+
) {
49+
let path = "/wpcom/v2/mobile/resolve-reader-url"
50+
51+
Task { @MainActor [wordPressComRestApi] in
52+
await wordPressComRestApi.perform(.get, URLString: path, parameters: [
53+
"url": url.absoluteString,
54+
], type: ResolvedReaderPost.self)
55+
.map { $0.body }
56+
.eraseToError()
57+
.execute(onSuccess: success, onFailure: failure)
58+
}
59+
}
60+
3461
private func postsEndpoint(for topics: [String], page: String? = nil) -> String? {
3562
var path = URLComponents(string: "read/tags/posts")
3663

Modules/Sources/WordPressKit/ReaderSiteSearchServiceRemote.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public class ReaderSiteSearchServiceRemote: ServiceRemoteWordPressComREST {
2020
public func performSearch(_ query: String,
2121
offset: Int = 0,
2222
count: Int,
23-
success: @escaping (_ results: [ReaderFeed], _ hasMore: Bool, _ feedCount: Int) -> Void,
23+
success: @escaping (_ results: [ReaderFeed], _ hasMore: Bool, _ total: Int?) -> Void,
2424
failure: @escaping (Error) -> Void) {
2525
let endpoint = "read/feed"
2626
let path = self.path(forEndpoint: endpoint, withVersion: ._1_1)
@@ -29,7 +29,7 @@ public class ReaderSiteSearchServiceRemote: ServiceRemoteWordPressComREST {
2929
"offset": offset as AnyObject,
3030
"exclude_followed": false as AnyObject,
3131
"sort": "relevance" as AnyObject,
32-
"meta": "site" as AnyObject,
32+
"meta": "site,feed" as AnyObject,
3333
"q": query as AnyObject
3434
]
3535

@@ -38,7 +38,7 @@ public class ReaderSiteSearchServiceRemote: ServiceRemoteWordPressComREST {
3838
success: { response, _ in
3939
do {
4040
let (results, total) = try self.mapSearchResponse(response)
41-
let hasMore = total > (offset + count)
41+
let hasMore = (total ?? 0) > (offset + count)
4242
success(results, hasMore, total)
4343
} catch {
4444
failure(error)
@@ -52,7 +52,7 @@ public class ReaderSiteSearchServiceRemote: ServiceRemoteWordPressComREST {
5252

5353
private extension ReaderSiteSearchServiceRemote {
5454

55-
func mapSearchResponse(_ response: Any) throws -> ([ReaderFeed], Int) {
55+
func mapSearchResponse(_ response: Any) throws -> ([ReaderFeed], Int?) {
5656
do {
5757
let decoder = JSONDecoder()
5858
let data = try JSONSerialization.data(withJSONObject: response, options: [])
@@ -73,9 +73,9 @@ private extension ReaderSiteSearchServiceRemote {
7373
/// The Reader feed search endpoint returns feeds in a key named `feeds` key.
7474
/// This entity allows us to do parse that and the total feed count using JSONDecoder.
7575
///
76-
private struct ReaderFeedEnvelope: Decodable {
76+
struct ReaderFeedEnvelope: Decodable {
7777
let feeds: [ReaderFeed]
78-
let total: Int
78+
let total: Int?
7979

8080
private enum CodingKeys: String, CodingKey {
8181
case feeds = "feeds"

Modules/Sources/WordPressReader/Settings/ReaderDisplaySettings.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,8 @@ public struct ReaderDisplaySettings: Codable, Equatable, Hashable, Sendable {
249249
case normal
250250
case large
251251
case extraLarge
252+
case extraExtraLarge
253+
case extraExtraExtraLarge
252254

253255
public var scale: Double {
254256
switch self {
@@ -262,6 +264,10 @@ public struct ReaderDisplaySettings: Codable, Equatable, Hashable, Sendable {
262264
return 1.15
263265
case .extraLarge:
264266
return 1.25
267+
case .extraExtraLarge:
268+
return 1.4
269+
case .extraExtraExtraLarge:
270+
return 1.6
265271
}
266272
}
267273

@@ -297,6 +303,18 @@ public struct ReaderDisplaySettings: Codable, Equatable, Hashable, Sendable {
297303
value: "Extra Large",
298304
comment: "Accessibility label for the Extra Large size option, used in the Reader's reading preferences."
299305
)
306+
case .extraExtraLarge:
307+
return NSLocalizedString(
308+
"reader.preferences.size.extraExtraLarge",
309+
value: "Extra Extra Large",
310+
comment: "Accessibility label for the Extra Extra Large size option, used in the Reader's reading preferences."
311+
)
312+
case .extraExtraExtraLarge:
313+
return NSLocalizedString(
314+
"reader.preferences.size.extraExtraExtraLarge",
315+
value: "Extra Extra Extra Large",
316+
comment: "Accessibility label for the Extra Extra Extra Large size option, used in the Reader's reading preferences."
317+
)
300318
}
301319
}
302320

@@ -312,6 +330,10 @@ public struct ReaderDisplaySettings: Codable, Equatable, Hashable, Sendable {
312330
return "large"
313331
case .extraLarge:
314332
return "extra_large"
333+
case .extraExtraLarge:
334+
return "extra_extra_large"
335+
case .extraExtraExtraLarge:
336+
return "extra_extra_extra_large"
315337
}
316338
}
317339
}

RELEASE-NOTES.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,22 @@
1010
* [*] Add "Access" section to "Post Settings" [#24942]
1111
* [*] Add "Discussion" to "Post Settings" [#24948]
1212
* [*] Add "File Size" to Site Media Details [#24947]
13+
* [*] Fix an issue with posts duplicated in Discover [#25015]
1314
* [*] Add "Email to Subscribers" row to "Publishing" sheet [#24946]
1415
* [*] Add permalink preview in the slug editor and make other improvements [#24949]
16+
* [*] Add two accessible font sizes to Reader display settings [#25013]
17+
* [*] Fix an issue with Reader failing to parse some search results [#25022]
18+
* [*] Fix search for RSS feeds in Reader [#25023]
1519
* [*] Add "Taxonomies" to Site Settings [#24955]
1620
* [*] Update "Categories" picker to indicate multiple selection [#24952]
1721
* [*] Fix a bug where the app can't access some Jetpack connected sites [#24976]
1822
* [*] Add support for editing custom taxonomy terms from "Post Settings" [#24964]
23+
* [*] Fix a bug where the app can't access some Jetpack connected sites [#24976]
24+
* [*] Add support for editing custom taxonomy terms from "Post Settings" [#24964]
25+
* [*] Fix overly long related post titles in Reader [#25011]
26+
* [*] Increase number of lines for post tiles in Reader to three [#25019]
27+
* [*] Fix horizontal insets in Reader article view [#25010]
28+
* [*] Fixed several reader bugs causing posts to load strangely, or not at all [#25016]
1929

2030
26.4
2131
-----

Sources/WordPressData/Swift/ReaderCard+CoreDataClass.swift

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,27 +43,47 @@ public class ReaderCard: NSManagedObject {
4343
sites?.array as? [ReaderSiteTopic] ?? []
4444
}
4545

46-
public convenience init?(context: NSManagedObjectContext, from remoteCard: RemoteReaderCard) {
46+
public static func createOrReuse(context: NSManagedObjectContext, from remoteCard: RemoteReaderCard) -> ReaderCard? {
4747
guard remoteCard.type != .unknown else {
4848
return nil
4949
}
5050

51-
self.init(context: context)
52-
5351
switch remoteCard.type {
5452
case .post:
55-
post = ReaderPost.createOrReplace(fromRemotePost: remoteCard.post, for: nil, context: context)
53+
let post = ReaderPost.createOrReplace(fromRemotePost: remoteCard.post, for: nil, context: context)
54+
55+
// Check if a card already exists with this post to prevent duplicates
56+
if let existingCard = findExistingCard(with: post, context: context) {
57+
return existingCard
58+
}
59+
60+
let card = ReaderCard(context: context)
61+
card.post = post
62+
return card
63+
5664
case .interests:
57-
topics = NSOrderedSet(array: remoteCard.interests?.prefix(5).map {
58-
ReaderTagTopic.createOrUpdateIfNeeded(from: $0, context: context)
59-
} ?? [])
65+
return nil // Disabled in v26.5
6066
case .sites:
61-
sites = NSOrderedSet(array: remoteCard.sites?.prefix(3).map {
67+
let card = ReaderCard(context: context)
68+
card.sites = NSOrderedSet(array: remoteCard.sites?.prefix(3).map {
6269
ReaderSiteTopic.createIfNeeded(from: $0, context: context)
6370
} ?? [])
71+
return card
6472

6573
default:
66-
break
74+
return nil
6775
}
6876
}
77+
78+
private static func findExistingCard(with post: ReaderPost?, context: NSManagedObjectContext) -> ReaderCard? {
79+
guard let post else {
80+
return nil
81+
}
82+
83+
let fetchRequest = ReaderCard.fetchRequest()
84+
fetchRequest.predicate = NSPredicate(format: "post = %@", post)
85+
fetchRequest.fetchLimit = 1
86+
87+
return try? context.fetch(fetchRequest).first
88+
}
6989
}

Sources/WordPressData/Swift/ReaderPost+PostContentProvider.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ extension ReaderPost {
1212
}
1313

1414
@objc public override var featuredImageURL: URL? {
15-
if !self.featuredImage.isEmpty {
16-
return URL(string: self.featuredImage)
15+
if let featuredImage, !featuredImage.isEmpty {
16+
return URL(string: featuredImage)
1717
}
1818
return nil
1919
}

Sources/WordPressData/Swift/ReaderSiteTopic+Lookup.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@ import Foundation
33

44
public extension ReaderSiteTopic {
55

6+
/// The preferred display title for the site.
7+
/// Returns the site title if not empty, otherwise returns the site host, or "–" if unavailable.
8+
var preferredDisplayTitle: String {
9+
if !title.isEmpty {
10+
return title
11+
}
12+
return URL(string: siteURL)?.host ?? ""
13+
}
14+
615
/// Find a site topic by its site id
716
///
817
/// - Parameter siteID: The site id of the topic

0 commit comments

Comments
 (0)