Skip to content

Commit

Permalink
Add support for X-posting in Gutenberg
Browse files Browse the repository at this point in the history
- Add new Core Data model `WordPress 103` with new entity `SiteSuggestion`
- Add a one-to-many relationship between `Blog` and `SiteSuggestion`
- Add `XPostSuggestionService` for fetching xpost suggestions from the remote server
- Add xpost support to Gutenberg in `GutenbergViewController`
- Extend `SuggestionsTableView` to support xposts
  • Loading branch information
guarani committed Oct 21, 2020
1 parent eb82431 commit 7e6df8e
Show file tree
Hide file tree
Showing 17 changed files with 1,326 additions and 118 deletions.
7 changes: 7 additions & 0 deletions MIGRATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
This file documents changes in the data model. Please explain any changes to the
data model as well as any custom migrations.

## WordPress 103

@guarani 2020-10-20

- Add a new `SiteSuggestion` entity to support Gutenberg's xpost implementation
- Add a one-to-many relationship between `Blog` and `SiteSuggestion`

## WordPress 101

@emilylaguna 2020-10-09
Expand Down
2 changes: 1 addition & 1 deletion Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ target 'WordPress' do
## Gutenberg (React Native)
## =====================
##
gutenberg :tag => 'v1.39.0'
gutenberg :commit => 'c3c47584dd18fdafc171e0fa160780ec2db41050'

## Third party libraries
## =====================
Expand Down
174 changes: 87 additions & 87 deletions Podfile.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions WordPress/Classes/Models/Blog.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ NS_ASSUME_NONNULL_BEGIN
@class Role;
@class QuickStartTourState;
@class UserSuggestion;
@class SiteSuggestion;
@class PageTemplateCategory;

extern NSString * const BlogEntityName;
Expand Down Expand Up @@ -103,6 +104,7 @@ typedef NS_ENUM(NSInteger, SiteVisibility) {
@property (nonatomic, strong, readwrite, nullable) NSSet *themes;
@property (nonatomic, strong, readwrite, nullable) NSSet *media;
@property (nonatomic, strong, readwrite, nullable) NSSet<UserSuggestion *> *userSuggestions;
@property (nonatomic, strong, readwrite, nullable) NSSet<SiteSuggestion *> *siteSuggestions;
@property (nonatomic, strong, readwrite, nullable) NSOrderedSet *menus;
@property (nonatomic, strong, readwrite, nullable) NSOrderedSet *menuLocations;
@property (nonatomic, strong, readwrite, nullable) NSSet<Role *> *roles;
Expand Down
34 changes: 34 additions & 0 deletions WordPress/Classes/Models/SiteSuggestion+CoreDataClass.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Foundation
import CoreData

extension CodingUserInfoKey {
static let managedObjectContext = CodingUserInfoKey(rawValue: "managedObjectContext")!
}

enum DecoderError: Error {
case missingManagedObjectContext
}

@objc(SiteSuggestion)
public class SiteSuggestion: NSManagedObject, Decodable {
enum CodingKeys: String, CodingKey {
case title = "title"
case siteURL = "siteurl"
case subdomain = "subdomain"
case blavatarURL = "blavatar"
}

required convenience public init(from decoder: Decoder) throws {
guard let managedObjectContext = decoder.userInfo[CodingUserInfoKey.managedObjectContext] as? NSManagedObjectContext else {
throw DecoderError.missingManagedObjectContext
}

self.init(context: managedObjectContext)

let container = try decoder.container(keyedBy: CodingKeys.self)
self.title = try container.decode(String.self, forKey: .title)
self.siteURL = try container.decode(URL.self, forKey: .siteURL)
self.subdomain = try container.decode(String.self, forKey: .subdomain)
self.blavatarURL = try container.decode(URL.self, forKey: .blavatarURL)
}
}
17 changes: 17 additions & 0 deletions WordPress/Classes/Models/SiteSuggestion+CoreDataProperties.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Foundation
import CoreData


extension SiteSuggestion {

@nonobjc public class func fetchRequest() -> NSFetchRequest<SiteSuggestion> {
return NSFetchRequest<SiteSuggestion>(entityName: "SiteSuggestion")
}

@NSManaged public var title: String?
@NSManaged public var siteURL: URL?
@NSManaged public var subdomain: String?
@NSManaged public var blavatarURL: URL?
@NSManaged public var blog: Blog?

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

/// A service to fetch and persist a list of sites that can be used for x-posting.
struct XpostSuggestionService {

enum ServiceError: Error {
case missingAPI
case missingManagedObjectContext
case hostnameNotAvailable
case noResultsAvailable
}

static var hasRequested = false

/**
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
*/
static func suggestions(for blog: Blog, completion: @escaping (Result<[SiteSuggestion], Error>) -> Void) {

if let results = retrievePersistedResults(for: blog), results.isEmpty == false {
completion(.success(results))
} else if ReachabilityUtils.isInternetReachable() {
fetchAndPersistSuggestions(for: blog, completion: completion)
} else {
completion(.failure(ServiceError.noResultsAvailable))
}
}

private static func fetchAndPersistSuggestions(for blog: Blog, completion: @escaping (Result<[SiteSuggestion], Error>) -> Void) {

guard !hasRequested else { return }
self.hasRequested = true

guard let api = blog.wordPressComRestApi() else {
completion(.failure(ServiceError.missingAPI))
return
}

guard let managedObjectContext = blog.managedObjectContext else {
completion(.failure(ServiceError.missingManagedObjectContext))
return
}

guard let hostname = blog.hostname else {
completion(.failure(ServiceError.hostnameNotAvailable))
return
}

let urlString = "/wpcom/v2/sites/\(hostname)/xposts"

api.GET(urlString, parameters: nil) { responseObject, httpResponse in
do {
let data = try JSONSerialization.data(withJSONObject: responseObject)

try self.purgeExistingResults(for: blog, using: managedObjectContext)

let siteSuggestions = try self.persist(data: data, to: blog, using: managedObjectContext)
completion(.success(siteSuggestions))
} catch {
completion(.failure(error))
}

self.hasRequested = false
} failure: { error, _ in
completion(.failure(error))
self.hasRequested = false
}
}

private static func purgeExistingResults(for blog: Blog, using managedObjectContext: NSManagedObjectContext) throws {
blog.siteSuggestions?.forEach { siteSuggestion in
managedObjectContext.delete(siteSuggestion)
}
try managedObjectContext.save()
}

private static func persist(data: Data, to blog: Blog, using managedObjectContext: NSManagedObjectContext) throws -> [SiteSuggestion] {
let decoder = JSONDecoder()
decoder.userInfo[CodingUserInfoKey.managedObjectContext] = managedObjectContext
let siteSuggestions = try decoder.decode([SiteSuggestion].self, from: data)
blog.siteSuggestions = Set(siteSuggestions)
try managedObjectContext.save()
return siteSuggestions
}

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

extension XpostSuggestionService.ServiceError: CustomNSError {
static var errorDomain: String { return "XpostSuggestionService.ServiceError" }

var errorCode: Int { return 0 }

var errorUserInfo: [String : Any] {
switch self {
case .missingAPI: return [NSDebugDescriptionErrorKey: "Blog hostname not available"]
case .missingManagedObjectContext: return [NSDebugDescriptionErrorKey: "Managed object context not available"]
case .hostnameNotAvailable: return [NSDebugDescriptionErrorKey: "Blog hostname not available"]
case .noResultsAvailable: return [NSDebugDescriptionErrorKey: "The device is offline and there are no suggestions in the cache"]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ - (void)attachSuggestionsTableViewIfNeeded
return;
}

self.suggestionsTableView = [[SuggestionsTableView alloc] initWithSiteID:self.comment.blog.dotComID suggestionType:SuggestionTypeMention delegate:self];
self.suggestionsTableView = [[SuggestionsTableView alloc] initWithSiteID:self.comment.blog.dotComID suggestionType:SuggestionTypeMentions delegate:self];
[self.suggestionsTableView setTranslatesAutoresizingMaskIntoConstraints:NO];
[self.view addSubview:self.suggestionsTableView];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public class FullScreenCommentReplyViewController: EditCommentViewController, Su
}

guard let siteID = siteID else { return }
let tableView = SuggestionsTableView(siteID: siteID, suggestionType: .mention, delegate: self)
let tableView = SuggestionsTableView(siteID: siteID, suggestionType: .mentions, delegate: self)
tableView.useTransparentHeader = true
tableView.translatesAutoresizingMaskIntoConstraints = false

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -851,7 +851,13 @@ extension GutenbergViewController: GutenbergBridgeDelegate {

func gutenbergDidRequestMention(callback: @escaping (Swift.Result<String, NSError>) -> Void) {
DispatchQueue.main.async(execute: { [weak self] in
self?.showSuggestions(type: .mention, callback: callback)
self?.showSuggestions(type: .mentions, callback: callback)
})
}

func gutenbergDidRequestXpost(callback: @escaping (Swift.Result<String, NSError>) -> Void) {
DispatchQueue.main.async(execute: { [weak self] in
self?.showSuggestions(type: .xposts, callback: callback)
})
}

Expand Down Expand Up @@ -882,8 +888,10 @@ extension GutenbergViewController {
}

switch type {
case .mention:
case .mentions:
guard SuggestionService.shared.shouldShowSuggestions(for: blog) else { return }
case .xposts:
break
}

previousFirstResponder = view.findFirstResponder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ extension NotificationDetailsViewController {

func setupSuggestionsView() {
guard let siteID = note.metaSiteID else { return }
suggestionsTableView = SuggestionsTableView(siteID: siteID, suggestionType: .mention, delegate: self)
suggestionsTableView = SuggestionsTableView(siteID: siteID, suggestionType: .mentions, delegate: self)
suggestionsTableView.translatesAutoresizingMaskIntoConstraints = false
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ - (void)configureSuggestionsTableView
NSNumber *siteID = self.siteID;
NSParameterAssert(siteID);

self.suggestionsTableView = [[SuggestionsTableView alloc] initWithSiteID:siteID suggestionType:SuggestionTypeMention delegate:self];
self.suggestionsTableView = [[SuggestionsTableView alloc] initWithSiteID:siteID suggestionType:SuggestionTypeMentions delegate:self];
[self.suggestionsTableView setTranslatesAutoresizingMaskIntoConstraints:NO];
[self.view addSubview:self.suggestionsTableView];
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#import <UIKit/UIKit.h>

typedef NS_CLOSED_ENUM(NSUInteger, SuggestionType) {
SuggestionTypeMention
SuggestionTypeMentions,
SuggestionTypeXposts
};

@protocol SuggestionsTableViewDelegate;
Expand All @@ -20,7 +21,7 @@ typedef NS_CLOSED_ENUM(NSUInteger, SuggestionType) {

- (nonnull instancetype)initWithSiteID:(NSNumber *_Nullable)siteID
suggestionType:(SuggestionType)suggestionType
delegate:(id <SuggestionsTableViewDelegate>_Nonnull)suggestionsDelegate;
delegate:(id <SuggestionsTableViewDelegate>_Nonnull)suggestionsDelegate NS_DESIGNATED_INITIALIZER;

/**
Enables or disables the SuggestionsTableView component.
Expand Down
Loading

0 comments on commit 7e6df8e

Please sign in to comment.