Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
25 changes: 19 additions & 6 deletions Modules/Sources/WordPressKit/ReaderFeed.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
/// (read/feed?q=query)
///
public struct ReaderFeed: Decodable {
public let url: URL
public let title: String
public let url: URL?
public let title: String?
public let feedDescription: String?
public let feedID: String?
public let blogID: String?
Expand All @@ -28,9 +28,11 @@
case site
}

private enum SiteKeys: CodingKey {
private enum SiteKeys: String, CodingKey {
case description
case icon
case url = "URL"
case name
}

private enum IconKeys: CodingKey {
Expand All @@ -44,8 +46,8 @@
// - We want to decode whatever we can get, and not fail if neither of those exist
let rootContainer = try decoder.container(keyedBy: CodingKeys.self)

url = try rootContainer.decode(URL.self, forKey: .url)
title = try rootContainer.decode(String.self, forKey: .title)
var feedURL = try? rootContainer.decodeIfPresent(URL.self, forKey: .url)
var title = try? rootContainer.decodeIfPresent(String.self, forKey: .title)

Check warning on line 50 in Modules/Sources/WordPressKit/ReaderFeed.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename "title" which has the same name as the field declared at line 9.

See more on https://sonarcloud.io/project/issues?id=wordpress-mobile_WordPress-iOS&issues=AZrB3saO2v82-KtyOdIE&open=AZrB3saO2v82-KtyOdIE&pullRequest=25022
feedID = try? rootContainer.decode(String.self, forKey: .feedID)
blogID = try? rootContainer.decode(String.self, forKey: .blogID)

Expand All @@ -60,16 +62,27 @@

let iconContainer = try siteContainer.nestedContainer(keyedBy: IconKeys.self, forKey: .icon)
blavatarURL = try? iconContainer.decode(URL.self, forKey: .img)

// Fixes CMM-1002: in some cases, the backend fails to embed certain fields
// directly in the feed object
if feedURL == nil {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not very elegant, but matches the rest of the code and gets the job done.

Both title and url fields are now optional. Hopefully, for each response, there will be a combination of at least some of these fields. The UI displays what's available.

feedURL = try? siteContainer.decodeIfPresent(URL.self, forKey: .url)
}
if title == nil {
title = try? siteContainer.decodeIfPresent(String.self, forKey: .name)
}
} catch {
}

self.url = feedURL
self.title = title
self.feedDescription = feedDescription
self.blavatarURL = blavatarURL
}
}

extension ReaderFeed: CustomStringConvertible {
public var description: String {
return "<Feed | URL: \(url), title: \(title), feedID: \(String(describing: feedID)), blogID: \(String(describing: blogID))>"
return "<Feed | URL: \(String(describing: url)), title: \(String(describing: title)), feedID: \(String(describing: feedID)), blogID: \(String(describing: blogID))>"
}
}
10 changes: 5 additions & 5 deletions Modules/Sources/WordPressKit/ReaderSiteSearchServiceRemote.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class ReaderSiteSearchServiceRemote: ServiceRemoteWordPressComREST {
public func performSearch(_ query: String,
offset: Int = 0,
count: Int,
success: @escaping (_ results: [ReaderFeed], _ hasMore: Bool, _ feedCount: Int) -> Void,
success: @escaping (_ results: [ReaderFeed], _ hasMore: Bool, _ total: Int?) -> Void,
failure: @escaping (Error) -> Void) {
let endpoint = "read/feed"
let path = self.path(forEndpoint: endpoint, withVersion: ._1_1)
Expand All @@ -38,7 +38,7 @@ public class ReaderSiteSearchServiceRemote: ServiceRemoteWordPressComREST {
success: { response, _ in
do {
let (results, total) = try self.mapSearchResponse(response)
let hasMore = total > (offset + count)
let hasMore = (total ?? 0) > (offset + count)
success(results, hasMore, total)
} catch {
failure(error)
Expand All @@ -52,7 +52,7 @@ public class ReaderSiteSearchServiceRemote: ServiceRemoteWordPressComREST {

private extension ReaderSiteSearchServiceRemote {

func mapSearchResponse(_ response: Any) throws -> ([ReaderFeed], Int) {
func mapSearchResponse(_ response: Any) throws -> ([ReaderFeed], Int?) {
do {
let decoder = JSONDecoder()
let data = try JSONSerialization.data(withJSONObject: response, options: [])
Expand All @@ -73,9 +73,9 @@ private extension ReaderSiteSearchServiceRemote {
/// The Reader feed search endpoint returns feeds in a key named `feeds` key.
/// This entity allows us to do parse that and the total feed count using JSONDecoder.
///
private struct ReaderFeedEnvelope: Decodable {
struct ReaderFeedEnvelope: Decodable {
let feeds: [ReaderFeed]
let total: Int
let total: Int?
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is not ideal, but it was another missing field. The service assumes that if total is nil, then there is no paging.

Copy link
Contributor

Choose a reason for hiding this comment

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

This is probably fine 🤷


private enum CodingKeys: String, CodingKey {
case feeds = "feeds"
Expand Down
145 changes: 145 additions & 0 deletions Tests/WordPressKitTests/WordPressKitTests/Tests/ReederFeedTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import Foundation
import Testing

@testable import WordPressKit

struct ReaderFeedTests {

@Test func decodesReaderFeedEnvelopeWithSiteFallbacks() throws {
// GIVEN: JSON response where URL and title are not embedded at root level
let jsonData = try #require(readerFeedJSON.data(using: .utf8))

// WHEN: Decoding the envelope
let decoder = JSONDecoder()
let envelope = try decoder.decode(ReaderFeedEnvelope.self, from: jsonData)

// THEN: Envelope contains feeds array
#expect(envelope.feeds.count == 1)

let feed = try #require(envelope.feeds.first)

// THEN: Feed ID is decoded from root level
#expect(feed.feedID == "188407")

// THEN: URL falls back to data.site.URL since not present at root
#expect(feed.url?.absoluteString == "https://ma.tt")

Check warning on line 25 in Tests/WordPressKitTests/WordPressKitTests/Tests/ReederFeedTests.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor your code to get this URI from a customizable parameter.

See more on https://sonarcloud.io/project/issues?id=wordpress-mobile_WordPress-iOS&issues=AZrB3sdq2v82-KtyOdIF&open=AZrB3sdq2v82-KtyOdIF&pullRequest=25022

// THEN: Title falls back to data.site.name since not present at root
#expect(feed.title == "Matt Mullenweg")

// THEN: Description is decoded from data.site.description
#expect(feed.feedDescription == "Unlucky in Cards")

// THEN: Blavatar URL is decoded from data.site.icon.img
#expect(feed.blavatarURL?.absoluteString == "https://ma.tt/files/2024/01/cropped-matt-favicon.png")

Check warning on line 34 in Tests/WordPressKitTests/WordPressKitTests/Tests/ReederFeedTests.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor your code to get this URI from a customizable parameter.

See more on https://sonarcloud.io/project/issues?id=wordpress-mobile_WordPress-iOS&issues=AZrB3sdq2v82-KtyOdIG&open=AZrB3sdq2v82-KtyOdIG&pullRequest=25022
}
}

// MARK: - Test Data

private let readerFeedJSON = """
{
"feeds": [
{
"subscribe_URL": "https://ma.tt/feed/",
"feed_ID": "188407",
"meta": {
"links": {
"feed": "https://public-api.wordpress.com/rest/v1.1/read/feed/188407",
"site": "https://public-api.wordpress.com/rest/v1.1/read/sites/1047865"
},
"data": {
"site": {
"ID": 1047865,
"name": "Matt Mullenweg",
"description": "Unlucky in Cards",
"URL": "https://ma.tt",
"jetpack": true,
"jetpack_connection": true,
"post_count": 5599,
"subscribers_count": 4520,
"lang": "en-US",
"icon": {
"img": "https://ma.tt/files/2024/01/cropped-matt-favicon.png",
"ico": "https://ma.tt/files/2024/01/cropped-matt-favicon.png?w=16"
},
"logo": {
"id": 0,
"sizes": [],
"url": ""
},
"visible": true,
"is_private": false,
"is_coming_soon": false,
"is_following": false,
"organization_id": 0,
"meta": {
"links": {
"self": "https://public-api.wordpress.com/rest/v1.1/read/sites/1047865",
"help": "https://public-api.wordpress.com/rest/v1.1/read/sites/1047865/help",
"posts": "https://public-api.wordpress.com/rest/v1.1/read/sites/1047865/posts/",
"comments": "https://public-api.wordpress.com/rest/v1.1/sites/1047865/comments/",
"xmlrpc": "https://ma.tt/blog/xmlrpc.php"
}
},
"launch_status": false,
"site_migration": {
"is_complete": false,
"in_progress": false
},
"is_fse_active": false,
"is_fse_eligible": false,
"is_core_site_editor_enabled": false,
"is_wpcom_atomic": false,
"is_wpcom_staging_site": false,
"is_deleted": false,
"is_a4a_client": false,
"is_a4a_dev_site": false,
"is_wpcom_flex": false,
"capabilities": {
"edit_pages": false,
"edit_posts": false,
"edit_others_posts": false,
"edit_theme_options": false,
"list_users": false,
"manage_categories": false,
"manage_options": false,
"publish_posts": false,
"upload_files": false,
"view_stats": false
},
"is_multi_author": true,
"feed_ID": 188407,
"feed_URL": "http://ma.tt/feed",
"header_image": false,
"owner": {
"ID": 5,
"login": "matt",
"name": "Matt",
"first_name": "Matt",
"last_name": "Mullenweg",
"nice_name": "matt",
"URL": "https://matt.blog/",
"avatar_URL": "https://0.gravatar.com/avatar/33252cd1f33526af53580fcb1736172f06e6716f32afdd1be19ec3096d15dea5?s=96&d=retro&r=G",
"profile_URL": "https://gravatar.com/matt",
"ip_address": false,
"site_visible": true,
"has_avatar": true
},
"subscription": {
"delivery_methods": {
"email": null,
"notification": {
"send_posts": false
}
}
},
"is_blocked": false,
"unseen_count": 0
}
}
}
}
]
}
"""
6 changes: 3 additions & 3 deletions WordPress/Classes/Services/ReaderSiteSearchService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import WordPressData
import WordPressShared
import WordPressKit

typealias ReaderSiteSearchSuccessBlock = (_ feeds: [ReaderFeed], _ hasMore: Bool, _ feedCount: Int) -> Void
typealias ReaderSiteSearchSuccessBlock = (_ feeds: [ReaderFeed], _ hasMore: Bool, _ total: Int?) -> Void
typealias ReaderSiteSearchFailureBlock = (_ error: Error?) -> Void

/// Allows searching for sites / feeds in the Reader.
Expand Down Expand Up @@ -50,8 +50,8 @@ class ReaderSiteSearchService {
remote.performSearch(query,
offset: page * Constants.pageSize,
count: Constants.pageSize,
success: { (feeds, hasMore, feedCount) in
success(feeds, hasMore, feedCount)
success: { (feeds, hasMore, total) in
success(feeds, hasMore, total)
}, failure: { error in
DDLogError("Error while performing Reader site search: \(String(describing: error))")
failure(error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,24 @@ struct ReaderFeedCell: View {
.font(.body)
.lineLimit(1)

Text(subtitle)
.font(.footnote)
.foregroundStyle(.secondary)
.lineLimit(2)
if let subtitle {
Text(subtitle)
.font(.footnote)
.foregroundStyle(.secondary)
.lineLimit(2)
}
}
}
}

var title: String {
let title = feed.title.stringByDecodingXMLCharacters()
if !title.isEmpty {
if let title = feed.title?.stringByDecodingXMLCharacters(), !title.isEmpty {
return title
}
return feed.urlForDisplay
return feed.urlForDisplay ?? "–"
}

var subtitle: String {
var subtitle: String? {
if let description = feed.feedDescription, !description.isEmpty {
return description.stringByDecodingXMLCharacters()
}
Expand All @@ -51,7 +52,10 @@ extension SiteIconViewModel {
private extension ReaderFeed {
/// Strips the protocol and query from the URL.
///
var urlForDisplay: String {
var urlForDisplay: String? {
guard let url else {
return nil
}
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let host = components.host else {
return url.absoluteString
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,10 @@ class ReaderSiteSearchViewController: UITableViewController {
reloadData()
}
}
fileprivate var totalFeedCount: Int = 0

var searchQuery: String? = nil {
didSet {
feeds = []
totalFeedCount = 0

syncHelper.syncContentWithUserInteraction(false)
}
}
Expand Down Expand Up @@ -77,9 +74,8 @@ class ReaderSiteSearchViewController: UITableViewController {
let service = ReaderSiteSearchService(coreDataStack: ContextManager.shared)
service.performSearch(with: query,
page: page,
success: { [weak self] (feeds, hasMore, totalFeeds) in
success: { [weak self] feeds, hasMore, _ in
self?.feeds.append(contentsOf: feeds)
self?.totalFeedCount = totalFeeds
self?.reloadData(hasMoreResults: hasMore)
success?(hasMore)
}, failure: { [weak self] error in
Expand Down Expand Up @@ -175,7 +171,7 @@ private extension ReaderSiteSearchViewController {
}

func showLoadingView() {
configureAndDisplayStatus(title: StatusText.loadingTitle, accessoryView: NoResultsViewController.loadingAccessoryView())
configureAndDisplayStatus(title: "", accessoryView: NoResultsViewController.loadingAccessoryView())
}

func showLoadingFailedView() {
Expand Down Expand Up @@ -208,11 +204,6 @@ private extension ReaderSiteSearchViewController {
}

struct StatusText {
static let loadingTitle = NSLocalizedString(
"reader.blog.search.loading.title",
value: "Fetching blogs...",
comment: "A brief prompt when the user is searching for blogs in the Reader."
)
static let loadingFailedTitle = NSLocalizedString(
"reader.blog.search.loading.error",
value: "Problem loading blogs",
Expand Down