Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Xpost Suggestions for Gutenberg #15139

Merged
merged 7 commits into from
Dec 24, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
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
2 changes: 2 additions & 0 deletions WordPress/Classes/Models/Blog.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ typedef NS_ENUM(NSUInteger, BlogFeature) {
BlogFeatureActivity,
/// Does the blog support mentions?
BlogFeatureMentions,
/// Does the blog support xposts?
BlogFeatureXposts,
/// Does the blog support push notifications?
BlogFeaturePushNotifications,
/// Does the blog support theme browsing?
Expand Down
2 changes: 2 additions & 0 deletions WordPress/Classes/Models/Blog.m
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,8 @@ - (BOOL)supports:(BlogFeature)feature
return [self isHostedAtWPcom];
case BlogFeatureMentions:
return [self isAccessibleThroughWPCom];
case BlogFeatureXposts:
return [self isAccessibleThroughWPCom];
case BlogFeatureReblog:
case BlogFeaturePlans:
return [self isHostedAtWPcom] && [self isAdmin];
Expand Down
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 {
guarani marked this conversation as resolved.
Show resolved Hide resolved
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)
}
}
5 changes: 5 additions & 0 deletions WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ enum FeatureFlag: Int, CaseIterable, OverrideableFlag {
case swiftCoreData
case homepageSettings
case gutenbergMentions
case gutenbergXposts
case gutenbergModalLayoutPicker
case whatIsNew
case newNavBarAppearance
Expand Down Expand Up @@ -40,6 +41,8 @@ enum FeatureFlag: Int, CaseIterable, OverrideableFlag {
return true
case .gutenbergMentions:
return true
case .gutenbergXposts:
return true
case .gutenbergModalLayoutPicker:
return true
case .whatIsNew:
Expand Down Expand Up @@ -100,6 +103,8 @@ extension FeatureFlag {
return "Homepage Settings"
case .gutenbergMentions:
return "Mentions in Gutenberg"
case .gutenbergXposts:
return "Xposts in Gutenberg"
case .gutenbergModalLayoutPicker:
return "Gutenberg Modal Layout Picker"
case .whatIsNew:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -852,6 +852,12 @@ extension GutenbergViewController: GutenbergBridgeDelegate {
})
}

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

func gutenbergDidRequestStarterPageTemplatesTooltipShown() -> Bool {
return gutenbergSettings.starterPageTemplatesTooltipShown
}
Expand All @@ -878,6 +884,13 @@ extension GutenbergViewController {
return
}

switch type {
case .mention:
guard SuggestionService.shared.shouldShowSuggestions(for: post.blog) else { return }
case .xpost:
guard SiteSuggestionService.shared.shouldShowSuggestions(for: post.blog) else { return }
}

previousFirstResponder = view.findFirstResponder()
let suggestionsController = GutenbergSuggestionsViewController(siteID: siteID, suggestionType: type)
suggestionsController.onCompletion = { (result) in
Expand Down Expand Up @@ -950,6 +963,7 @@ extension GutenbergViewController: GutenbergBridgeDataSource {
func gutenbergCapabilities() -> [Capabilities: Bool] {
return [
.mentions: FeatureFlag.gutenbergMentions.enabled && SuggestionService.shared.shouldShowSuggestions(for: post.blog),
.xposts: FeatureFlag.gutenbergXposts.enabled && SiteSuggestionService.shared.shouldShowSuggestions(for: post.blog),
.unsupportedBlockEditor: isUnsupportedBlockEditorEnabled,
.canEnableUnsupportedBlockEditor: post.blog.jetpack?.isConnected ?? false,
.modalLayoutPicker: FeatureFlag.gutenbergModalLayoutPicker.enabled,
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
SuggestionTypeMention,
SuggestionTypeXpost
};

@protocol SuggestionsTableViewDelegate;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,31 @@ extension SuggestionType {
var trigger: String {
switch self {
case .mention: return "@"
case .xpost: return "+"
}
}
}

@objc public extension SuggestionsTableView {

func suggestions(for siteID: NSNumber, completion: @escaping ([UserSuggestion]?) -> Void) {
func userSuggestions(for siteID: NSNumber, completion: @escaping ([UserSuggestion]?) -> Void) {
guard let blog = SuggestionService.shared.persistedBlog(for: siteID) else { return }
SuggestionService.shared.suggestions(for: blog, completion: completion)
}

func siteSuggestions(for siteID: NSNumber, completion: @escaping ([SiteSuggestion]?) -> Void) {
guard let blog = SuggestionService.shared.persistedBlog(for: siteID) else { return }
SiteSuggestionService.shared.suggestions(for: blog, completion: completion)
}

var suggestionTrigger: String { return suggestionType.trigger }

func predicate(for searchQuery: String) -> NSPredicate {
switch suggestionType {
case .mention:
return NSPredicate(format: "(displayName contains[c] %@) OR (username contains[c] %@)", searchQuery, searchQuery)
case .xpost:
return NSPredicate(format: "(title contains[cd] %@) OR (siteURL.absoluteString contains[cd] %@)", searchQuery, searchQuery)
}
}

Expand All @@ -29,7 +37,10 @@ extension SuggestionType {
switch (suggestionType, suggestion) {
case (.mention, let suggestion as UserSuggestion):
title = suggestion.username
default: title = nil
case (.xpost, let suggestion as SiteSuggestion):
title = suggestion.subdomain
default:
return nil
}
return title.map { suggestionType.trigger.appending($0) }
}
Expand All @@ -38,7 +49,10 @@ extension SuggestionType {
switch (suggestionType, suggestion) {
case (.mention, let suggestion as UserSuggestion):
return suggestion.displayName
default: return nil
case (.xpost, let suggestion as SiteSuggestion):
return suggestion.title
default:
return nil
}
}

Expand All @@ -48,7 +62,10 @@ extension SuggestionType {
switch (suggestionType, suggestion) {
case (.mention, let suggestion as UserSuggestion):
return suggestion.imageURL
default: return nil
case (.xpost, let suggestion as SiteSuggestion):
return suggestion.blavatarURL
default:
return nil
}
}

Expand All @@ -68,17 +85,24 @@ extension SuggestionType {
func fetchSuggestions(for siteID: NSNumber) {
switch self.suggestionType {
case .mention:
suggestions(for: siteID) { userSuggestions in
userSuggestions(for: siteID) { userSuggestions in
self.suggestions = userSuggestions
self.showSuggestions(forWord: self.searchText)
}
case .xpost:
siteSuggestions(for: siteID) { siteSuggestions in
self.suggestions = siteSuggestions
self.showSuggestions(forWord: self.searchText)
}
}
}

private func suggestionText(for suggestion: Any) -> String? {
switch (suggestionType, suggestion) {
case (.mention, let suggestion as UserSuggestion):
return suggestion.username
case (.xpost, let suggestion as SiteSuggestion):
return suggestion.title
default: return nil
}
}
Expand Down
4 changes: 4 additions & 0 deletions WordPress/WordPress.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1536,6 +1536,7 @@
B0AC50DD251E96270039E022 /* ReaderCommentsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0AC50DC251E96270039E022 /* ReaderCommentsViewController.swift */; };
B0B68A9C252FA91E0001B28C /* UserSuggestion+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0B68A9A252FA91E0001B28C /* UserSuggestion+CoreDataClass.swift */; };
B0B68A9D252FA91E0001B28C /* UserSuggestion+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0B68A9B252FA91E0001B28C /* UserSuggestion+CoreDataProperties.swift */; };
B0F2EFBF259378E600C7EB6D /* SiteSuggestionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0F2EFBE259378E600C7EB6D /* SiteSuggestionService.swift */; };
B5015C581D4FDBB300C9449E /* NotificationActionsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5015C571D4FDBB300C9449E /* NotificationActionsService.swift */; };
B50248AF1C96FF6200AFBDED /* WPStyleGuide+Share.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50248AE1C96FF6200AFBDED /* WPStyleGuide+Share.swift */; };
B50248C21C96FFCC00AFBDED /* WordPressShare-Lumberjack.m in Sources */ = {isa = PBXBuildFile; fileRef = B50248BC1C96FFCC00AFBDED /* WordPressShare-Lumberjack.m */; };
Expand Down Expand Up @@ -4107,6 +4108,7 @@
B0B68A9A252FA91E0001B28C /* UserSuggestion+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserSuggestion+CoreDataClass.swift"; sourceTree = "<group>"; };
B0B68A9B252FA91E0001B28C /* UserSuggestion+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserSuggestion+CoreDataProperties.swift"; sourceTree = "<group>"; };
B0DDC2EB252F7C4F002BAFB3 /* WordPress 100.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 100.xcdatamodel"; sourceTree = "<group>"; };
B0F2EFBE259378E600C7EB6D /* SiteSuggestionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteSuggestionService.swift; sourceTree = "<group>"; };
B5015C571D4FDBB300C9449E /* NotificationActionsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationActionsService.swift; sourceTree = "<group>"; };
B50248AE1C96FF6200AFBDED /* WPStyleGuide+Share.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "WPStyleGuide+Share.swift"; path = "WordPressShareExtension/WPStyleGuide+Share.swift"; sourceTree = SOURCE_ROOT; };
B50248B81C96FFB000AFBDED /* WordPressShare-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "WordPressShare-Bridging-Header.h"; path = "WordPressShareExtension/WordPressShare-Bridging-Header.h"; sourceTree = SOURCE_ROOT; };
Expand Down Expand Up @@ -8426,6 +8428,7 @@
D8CB561F2181A8CE00554EAE /* SiteSegmentsService.swift */,
D8A468E421828D940094B82F /* SiteVerticalsService.swift */,
B03B9233250BC593000A40AF /* SuggestionService.swift */,
B0F2EFBE259378E600C7EB6D /* SiteSuggestionService.swift */,
59A9AB331B4C33A500A433DC /* ThemeService.h */,
59A9AB341B4C33A500A433DC /* ThemeService.m */,
93DEB88019E5BF7100F9546D /* TodayExtensionService.h */,
Expand Down Expand Up @@ -13285,6 +13288,7 @@
5D8D53F119250412003C8859 /* BlogSelectorViewController.m in Sources */,
17523381246C4F9200870B4A /* HomepageSettingsViewController.swift in Sources */,
C81CCD7F243BF7A600A83E27 /* TenorMediaGroup.swift in Sources */,
B0F2EFBF259378E600C7EB6D /* SiteSuggestionService.swift in Sources */,
4388FF0020A4E19C00783948 /* NotificationsViewController+PushPrimer.swift in Sources */,
5D3D559718F88C3500782892 /* ReaderPostService.m in Sources */,
7EA30DB521ADA20F0092F894 /* EditorMediaUtility.swift in Sources */,
Expand Down