Skip to content

Commit

Permalink
Added a first-iteration implementation of Xposts
Browse files Browse the repository at this point in the history
  • Loading branch information
guarani committed Dec 23, 2020
1 parent 1645311 commit 4ad27f6
Show file tree
Hide file tree
Showing 15 changed files with 328 additions and 250 deletions.
2 changes: 1 addition & 1 deletion Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ target 'WordPress' do
## Gutenberg (React Native)
## =====================
##
gutenberg :tag => 'v1.43.0'
gutenberg :tag => 'v1.44.0-alpha1'

## Third party libraries
## =====================
Expand Down
166 changes: 83 additions & 83 deletions Podfile.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* [**] Choose a Domain will now return more options in the search results, sort the results to have exact matches first, and let you know if no exact matches were found. [#15482]
* [**] Page List: Adds duplicate page functionality [#15515]
* [*] Invite People: add link to user roles definition web page. [#15530]
* [***] Block Editor: Cross-post suggestions are now available by typing the + character (or long-pressing the toolbar button labelled with an @-symbol) in a post on a P2 site [#15139]

16.4
-----
Expand Down
6 changes: 1 addition & 5 deletions WordPress/Classes/Models/SiteSuggestion+CoreDataClass.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ enum DecoderError: Error {
}

@objc(SiteSuggestion)
public class SiteSuggestion: NSManagedObject, Decodable, Comparable {
public class SiteSuggestion: NSManagedObject, Decodable {
enum CodingKeys: String, CodingKey {
case title = "title"
case siteURL = "siteurl"
Expand All @@ -31,8 +31,4 @@ public class SiteSuggestion: NSManagedObject, Decodable, Comparable {
self.subdomain = try container.decode(String.self, forKey: .subdomain)
self.blavatarURL = try container.decode(URL.self, forKey: .blavatarURL)
}

public static func < (lhs: SiteSuggestion, rhs: SiteSuggestion) -> Bool {
return (lhs.title ?? "").localizedCaseInsensitiveCompare(rhs.title ?? "") == .orderedAscending
}
}
52 changes: 18 additions & 34 deletions WordPress/Classes/Models/UserSuggestion+CoreDataClass.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,30 @@ import Foundation
import CoreData

@objc(UserSuggestion)
public class UserSuggestion: NSManagedObject, Decodable, Comparable {
public class UserSuggestion: NSManagedObject {

enum CodingKeys: String, CodingKey {
case displayName = "display_name"
case imageURL = "image_URL"
case username = "user_login"
}
convenience init?(dictionary: [String: Any], context: NSManagedObjectContext) {
let userLoginValue = dictionary["user_login"] as? String
let displayNameValue = dictionary["display_name"] as? String

required convenience public init(from decoder: Decoder) throws {
guard let managedObjectContext = decoder.userInfo[CodingUserInfoKey.managedObjectContext] as? NSManagedObjectContext else {
throw DecoderError.missingManagedObjectContext
// A user suggestion is only valid when at least one of these is present.
guard userLoginValue != nil || displayNameValue != nil else {
return nil
}

self.init(context: managedObjectContext)

let container = try decoder.container(keyedBy: CodingKeys.self)
self.displayName = try container.decode(String.self, forKey: .displayName)
self.imageURL = try container.decode(URL.self, forKey: .imageURL)
self.username = try container.decode(String.self, forKey: .username)
}

public static func < (lhs: UserSuggestion, rhs: UserSuggestion) -> Bool {
return (lhs.displayName ?? "").localizedCaseInsensitiveCompare(rhs.displayName ?? "") == .orderedAscending
}
}

public class UserSuggestionsPayload: Decodable {
guard let entityDescription = NSEntityDescription.entity(forEntityName: "UserSuggestion", in: context) else {
return nil
}
self.init(entity: entityDescription, insertInto: context)

let suggestions: [UserSuggestion]
username = userLoginValue
displayName = displayNameValue

enum CodingKeys: String, CodingKey {
case suggestions = "suggestions"
}

public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let allSuggestions = try container.decode([UserSuggestion].self, forKey: .suggestions)
suggestions = allSuggestions.filter { suggestion in
// A user suggestion is only valid when at least one of these is present.
suggestion.displayName != nil || suggestion.username != nil
if let imageURLString = dictionary["image_URL"] as? String {
imageURL = URL(string: imageURLString)
} else {
imageURL = nil
}
}

}
119 changes: 119 additions & 0 deletions WordPress/Classes/Services/SiteSuggestionService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import Foundation

/// A service to fetch and persist a list of sites that can be xpost to from a post.
class SiteSuggestionService {

private var blogsCurrentlyBeingRequested = [Blog]()

static let shared = SiteSuggestionService()

/**
Fetch cached suggestions if available, otherwise from the network if the device is online.

@param the blog/site to retrieve suggestions for
@param completion callback containing list of suggestions, or nil if unavailable
*/
func suggestions(for blog: Blog, completion: @escaping ([SiteSuggestion]?) -> Void) {

if let suggestions = retrievePersistedSuggestions(for: blog), suggestions.isEmpty == false {
completion(suggestions)
} else if ReachabilityUtils.isInternetReachable() {
fetchAndPersistSuggestions(for: blog, completion: completion)
} else {
completion(nil)
}
}

/**
Performs a REST API request for the given blog.
Persists response objects to Core Data.

@param blog/site to retrieve suggestions for
*/
private func fetchAndPersistSuggestions(for blog: Blog, completion: @escaping ([SiteSuggestion]?) -> Void) {

// if there is already a request in place for this blog, just wait
guard !blogsCurrentlyBeingRequested.contains(blog) else { return }

guard let hostname = blog.hostname else { return }

let suggestPath = "/wpcom/v2/sites/\(hostname)/xposts"
let params = ["decode_html": true] as [String: AnyObject]

// add this blog to currently being requested list
blogsCurrentlyBeingRequested.append(blog)

defaultAccount()?.wordPressComRestApi.GET(suggestPath, parameters: params, success: { [weak self] responseObject, httpResponse in
guard let `self` = self else { return }

let context = ContextManager.shared.mainContext
guard let data = try? JSONSerialization.data(withJSONObject: responseObject) else { return }
let decoder = JSONDecoder()
decoder.userInfo[CodingUserInfoKey.managedObjectContext] = context
guard let suggestions = try? decoder.decode([SiteSuggestion].self, from: data) else { return }

// Delete any existing `SiteSuggestion` objects
self.retrievePersistedSuggestions(for: blog)?.forEach { suggestion in
context.delete(suggestion)
}

// Associate `SiteSuggestion` objects with blog
blog.siteSuggestions = Set(suggestions)

// Save the changes
try? ContextManager.shared.mainContext.save()

completion(suggestions)

// remove blog from the currently being requested list
self.blogsCurrentlyBeingRequested.removeAll { $0 == blog }
}, failure: { [weak self] error, _ in
guard let `self` = self else { return }

completion([])

// remove blog from the currently being requested list
self.blogsCurrentlyBeingRequested.removeAll { $0 == blog}

DDLogVerbose("[Rest API] ! \(error.localizedDescription)")
})
}

/**
Tells the caller if it is a good idea to show suggestions right now for a given blog/site.

@param blog blog/site to check for
@return BOOL Whether the caller should show suggestions
*/
func shouldShowSuggestions(for blog: Blog) -> Bool {

// The device must be online or there must be already persisted suggestions
guard ReachabilityUtils.isInternetReachable() || retrievePersistedSuggestions(for: blog)?.isEmpty == false else {
return false
}

return blog.supports(.xposts)
}

private func defaultAccount() -> WPAccount? {
let context = ContextManager.shared.mainContext
let accountService = AccountService(managedObjectContext: context)
return accountService.defaultWordPressComAccount()
}

func retrievePersistedSuggestions(for blog: Blog) -> [SiteSuggestion]? {
guard let suggestions = blog.siteSuggestions else { return nil }
return Array(suggestions)
}

/**
Retrieve the persisted blog/site for a given site ID

@param siteID the dotComID to retrieve
@return Blog the blog/site
*/
func persistedBlog(for siteID: NSNumber) -> Blog? {
let context = ContextManager.shared.mainContext
return BlogService(managedObjectContext: context).blog(byBlogId: siteID)
}
}
Loading

0 comments on commit 4ad27f6

Please sign in to comment.