From 6966f0de54a30f733104eff01f19f8da51c6b7a3 Mon Sep 17 00:00:00 2001 From: Paul Von Schrottky Date: Mon, 19 Oct 2020 19:19:56 -0300 Subject: [PATCH] Prepare suggestions to handle xposts - Update `SuggestionsTableView.m/.h` to use a more succinct initializer - Add new logic to `SuggestionsTableView.swift` to handle multiple mention types (i.e. mentions, and in the future, xposts) - Rename variable names in `SuggestionsTableViewCell.m/.h` to be @-mention agnostic - Rename `GutenbergMentionsViewController` to `GutenbergSuggestionsViewController` to reflect its role supporting not just mentions, but also xposts in the near future - Rename variable names in `GutenbergViewController` to be @-mention agnostic --- .../Models/UserSuggestion+ImageHelpers.swift | 33 ----- .../System/WordPress-Bridging-Header.h | 1 + .../Comments/CommentViewController.m | 4 +- ...FullScreenCommentReplyViewController.swift | 5 +- ... GutenbergSuggestionsViewController.swift} | 31 ++-- .../Gutenberg/GutenbergViewController.swift | 51 +++---- .../NotificationDetailsViewController.swift | 5 +- .../Reader/ReaderCommentsViewController.m | 4 +- .../Suggestions/SuggestionsTableView.h | 15 +- .../Suggestions/SuggestionsTableView.m | 86 +++-------- .../Suggestions/SuggestionsTableView.swift | 134 ++++++++++++++++++ .../Suggestions/SuggestionsTableViewCell.h | 10 +- .../Suggestions/SuggestionsTableViewCell.m | 63 ++++---- .../ViewRelated/SuggestionsTableView.swift | 8 -- WordPress/WordPress.xcodeproj/project.pbxproj | 20 ++- 15 files changed, 258 insertions(+), 212 deletions(-) delete mode 100644 WordPress/Classes/Models/UserSuggestion+ImageHelpers.swift rename WordPress/Classes/ViewRelated/Gutenberg/{GutenbergMentionsViewController.swift => GutenbergSuggestionsViewController.swift} (85%) create mode 100644 WordPress/Classes/ViewRelated/Suggestions/SuggestionsTableView.swift delete mode 100644 WordPress/Classes/ViewRelated/SuggestionsTableView.swift diff --git a/WordPress/Classes/Models/UserSuggestion+ImageHelpers.swift b/WordPress/Classes/Models/UserSuggestion+ImageHelpers.swift deleted file mode 100644 index 4a323ed09324..000000000000 --- a/WordPress/Classes/Models/UserSuggestion+ImageHelpers.swift +++ /dev/null @@ -1,33 +0,0 @@ -import Foundation - -@objc public extension UserSuggestion { - func cachedAvatar(with size: CGSize) -> UIImage? { - var hash: NSString? - let type = avatarSourceType(with: &hash) - - if let hash = hash, let type = type { - return WPAvatarSource.shared()?.cachedImage(forAvatarHash: hash as String, of: type, with: size) - } - return nil - } - - func fetchAvatar(with size: CGSize, success: ((UIImage?) -> Void)?) { - var hash: NSString? - let type = avatarSourceType(with: &hash) - - if let hash = hash, let type = type, let success = success { - WPAvatarSource.shared()?.fetchImage(forAvatarHash: hash as String, of: type, with: size, success: success) - } else { - success?(nil) - } - } -} - -extension UserSuggestion { - func avatarSourceType(with hash: inout NSString?) -> WPAvatarSourceType? { - if let imageURL = imageURL { - return WPAvatarSource.shared()?.parseURL(imageURL, forAvatarHash: &hash) - } - return .unknown - } -} diff --git a/WordPress/Classes/System/WordPress-Bridging-Header.h b/WordPress/Classes/System/WordPress-Bridging-Header.h index eaf966ba1263..87682d13a41d 100644 --- a/WordPress/Classes/System/WordPress-Bridging-Header.h +++ b/WordPress/Classes/System/WordPress-Bridging-Header.h @@ -77,6 +77,7 @@ #import "SourcePostAttribution.h" #import "StatsViewController.h" #import "SuggestionsTableView.h" +#import "SuggestionsTableViewCell.h" #import "SVProgressHUD+Dismiss.h" #import "Theme.h" diff --git a/WordPress/Classes/ViewRelated/Comments/CommentViewController.m b/WordPress/Classes/ViewRelated/Comments/CommentViewController.m index a1b8d17fbdf8..6fb923777a74 100644 --- a/WordPress/Classes/ViewRelated/Comments/CommentViewController.m +++ b/WordPress/Classes/ViewRelated/Comments/CommentViewController.m @@ -104,9 +104,7 @@ - (void)attachSuggestionsTableViewIfNeeded return; } - self.suggestionsTableView = [SuggestionsTableView new]; - self.suggestionsTableView.siteID = self.comment.blog.dotComID; - self.suggestionsTableView.suggestionsDelegate = self; + self.suggestionsTableView = [[SuggestionsTableView alloc] initWithSiteID:self.comment.blog.dotComID suggestionType:SuggestionTypeMention delegate:self]; [self.suggestionsTableView setTranslatesAutoresizingMaskIntoConstraints:NO]; [self.view addSubview:self.suggestionsTableView]; } diff --git a/WordPress/Classes/ViewRelated/Comments/FullScreenCommentReplyViewController.swift b/WordPress/Classes/ViewRelated/Comments/FullScreenCommentReplyViewController.swift index 50fbca881fe0..be83e5c66474 100644 --- a/WordPress/Classes/ViewRelated/Comments/FullScreenCommentReplyViewController.swift +++ b/WordPress/Classes/ViewRelated/Comments/FullScreenCommentReplyViewController.swift @@ -68,9 +68,8 @@ public class FullScreenCommentReplyViewController: EditCommentViewController, Su return } - let tableView = SuggestionsTableView() - tableView.siteID = siteID - tableView.suggestionsDelegate = self + guard let siteID = siteID else { return } + let tableView = SuggestionsTableView(siteID: siteID, suggestionType: .mention, delegate: self) tableView.useTransparentHeader = true tableView.translatesAutoresizingMaskIntoConstraints = false diff --git a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergMentionsViewController.swift b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergSuggestionsViewController.swift similarity index 85% rename from WordPress/Classes/ViewRelated/Gutenberg/GutenbergMentionsViewController.swift rename to WordPress/Classes/ViewRelated/Gutenberg/GutenbergSuggestionsViewController.swift index 8c2647c33972..8dd6caa8bde2 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergMentionsViewController.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergSuggestionsViewController.swift @@ -1,9 +1,8 @@ import Foundation import UIKit -public class GutenbergMentionsViewController: UIViewController { +public class GutenbergSuggestionsViewController: UIViewController { - static let mentionTriggerText = String("@") static let minimumHeaderHeight = CGFloat(50) public lazy var backgroundView: UIView = { @@ -23,8 +22,7 @@ public class GutenbergMentionsViewController: UIViewController { public lazy var searchView: UITextField = { let textField = UITextField(frame: CGRect.zero) - textField.placeholder = NSLocalizedString("Search users...", comment: "Placeholder message when showing mentions search field") - textField.text = Self.mentionTriggerText + textField.text = suggestionType.trigger textField.clearButtonMode = .whileEditing textField.translatesAutoresizingMaskIntoConstraints = false textField.delegate = self @@ -33,23 +31,22 @@ public class GutenbergMentionsViewController: UIViewController { }() public lazy var suggestionsView: SuggestionsTableView = { - let suggestionsView = SuggestionsTableView() + let suggestionsView = SuggestionsTableView(siteID: siteID, suggestionType: suggestionType, delegate: self) suggestionsView.animateWithKeyboard = false suggestionsView.enabled = true suggestionsView.showLoading = true - suggestionsView.showSuggestions(forWord: Self.mentionTriggerText) - suggestionsView.suggestionsDelegate = self suggestionsView.translatesAutoresizingMaskIntoConstraints = false - suggestionsView.siteID = siteID suggestionsView.useTransparentHeader = false return suggestionsView }() private let siteID: NSNumber + private let suggestionType: SuggestionType public var onCompletion: ((Result) -> Void)? - public init(siteID: NSNumber) { + public init(siteID: NSNumber, suggestionType: SuggestionType) { self.siteID = siteID + self.suggestionType = suggestionType super.init(nibName: nil, bundle: nil) } @@ -101,11 +98,11 @@ public class GutenbergMentionsViewController: UIViewController { } override public func viewDidAppear(_ animated: Bool) { - suggestionsView.showSuggestions(forWord: Self.mentionTriggerText) + suggestionsView.showSuggestions(forWord: suggestionType.trigger) } } -extension GutenbergMentionsViewController: UITextFieldDelegate { +extension GutenbergSuggestionsViewController: UITextFieldDelegate { public func textFieldShouldClear(_ textField: UITextField) -> Bool { onCompletion?(.failure(buildErrorForCancelation())) @@ -117,7 +114,7 @@ extension GutenbergMentionsViewController: UITextFieldDelegate { return true } let searchWord = nsString.replacingCharacters(in: range, with: string) - if searchWord.hasPrefix(Self.mentionTriggerText) { + if searchWord.hasPrefix(suggestionType.trigger) { suggestionsView.showSuggestions(forWord: searchWord) } else { // We are dispatching this async to allow this delegate to finish and process the keypress before executing the cancelation. @@ -136,7 +133,7 @@ extension GutenbergMentionsViewController: UITextFieldDelegate { } } -extension GutenbergMentionsViewController: SuggestionsTableViewDelegate { +extension GutenbergSuggestionsViewController: SuggestionsTableViewDelegate { public func suggestionsTableView(_ suggestionsTableView: SuggestionsTableView, didSelectSuggestion suggestion: String?, forSearchText text: String) { if let suggestion = suggestion { @@ -161,13 +158,13 @@ extension GutenbergMentionsViewController: SuggestionsTableViewDelegate { } } -extension GutenbergMentionsViewController { +extension GutenbergSuggestionsViewController { - enum MentionError: CustomNSError { + enum SuggestionError: CustomNSError { case canceled case notAvailable - static var errorDomain: String = "MentionErrorDomain" + static var errorDomain: String = "SuggestionErrorDomain" var errorCode: Int { switch self { @@ -184,6 +181,6 @@ extension GutenbergMentionsViewController { } private func buildErrorForCancelation() -> NSError { - return MentionError.canceled as NSError + return SuggestionError.canceled as NSError } } diff --git a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift index 210c35cbea10..4b864b66213a 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift @@ -387,7 +387,7 @@ class GutenbergViewController: UIViewController, PostEditor { private var keyboardShowObserver: Any? private var keyboardHideObserver: Any? private var keyboardFrame = CGRect.zero - private var mentionsBottomConstraint: NSLayoutConstraint? + private var suggestionViewBottomConstraint: NSLayoutConstraint? private var previousFirstResponder: UIView? private func setupKeyboardObservers() { @@ -835,23 +835,23 @@ extension GutenbergViewController: GutenbergBridgeDelegate { func updateConstraintsToAvoidKeyboard(frame: CGRect) { keyboardFrame = frame let minimumKeyboardHeight = CGFloat(50) - guard let mentionsBottomConstraint = mentionsBottomConstraint else { + guard let suggestionViewBottomConstraint = suggestionViewBottomConstraint else { return } // There are cases where the keyboard is not visible, but the system instead of returning zero, returns a low number, for example: 0, 3, 69. // So in those scenarios, we just need to take in account the safe area and ignore the keyboard all together. if keyboardFrame.height < minimumKeyboardHeight { - mentionsBottomConstraint.constant = -self.view.safeAreaInsets.bottom + suggestionViewBottomConstraint.constant = -self.view.safeAreaInsets.bottom } else { - mentionsBottomConstraint.constant = -self.keyboardFrame.height + suggestionViewBottomConstraint.constant = -self.keyboardFrame.height } } func gutenbergDidRequestMention(callback: @escaping (Swift.Result) -> Void) { DispatchQueue.main.async(execute: { [weak self] in - self?.mentionShow(callback: callback) + self?.showSuggestions(type: .mention, callback: callback) }) } @@ -871,40 +871,43 @@ extension GutenbergViewController: GutenbergBridgeDelegate { } } -// MARK: - Mention implementation +// MARK: - Suggestions implementation extension GutenbergViewController { - private func mentionShow(callback: @escaping (Swift.Result) -> Void) { - guard let siteID = post.blog.dotComID, - let blog = SuggestionService.shared.persistedBlog(for: siteID), - SuggestionService.shared.shouldShowSuggestions(for: blog) else { - callback(.failure(GutenbergMentionsViewController.MentionError.notAvailable as NSError)) + private func showSuggestions(type: SuggestionType, callback: @escaping (Swift.Result) -> Void) { + guard let siteID = post.blog.dotComID, let blog = SuggestionService.shared.persistedBlog(for: siteID) else { + callback(.failure(GutenbergSuggestionsViewController.SuggestionError.notAvailable as NSError)) return } + switch type { + case .mention: + guard SuggestionService.shared.shouldShowSuggestions(for: blog) else { return } + } + previousFirstResponder = view.findFirstResponder() - let mentionsController = GutenbergMentionsViewController(siteID: siteID) - mentionsController.onCompletion = { (result) in + let suggestionsController = GutenbergSuggestionsViewController(siteID: siteID, suggestionType: type) + suggestionsController.onCompletion = { (result) in callback(result) - mentionsController.view.removeFromSuperview() - mentionsController.removeFromParent() + suggestionsController.view.removeFromSuperview() + suggestionsController.removeFromParent() if let previousFirstResponder = self.previousFirstResponder { previousFirstResponder.becomeFirstResponder() } } - addChild(mentionsController) - view.addSubview(mentionsController.view) - let mentionsBottomConstraint = mentionsController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0) + addChild(suggestionsController) + view.addSubview(suggestionsController.view) + let suggestionsBottomConstraint = suggestionsController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0) NSLayoutConstraint.activate([ - mentionsController.view.leadingAnchor.constraint(equalTo: view.safeLeadingAnchor, constant: 0), - mentionsController.view.trailingAnchor.constraint(equalTo: view.safeTrailingAnchor, constant: 0), - mentionsBottomConstraint, - mentionsController.view.topAnchor.constraint(equalTo: view.safeTopAnchor) + suggestionsController.view.leadingAnchor.constraint(equalTo: view.safeLeadingAnchor, constant: 0), + suggestionsController.view.trailingAnchor.constraint(equalTo: view.safeTrailingAnchor, constant: 0), + suggestionsBottomConstraint, + suggestionsController.view.topAnchor.constraint(equalTo: view.safeTopAnchor) ]) - self.mentionsBottomConstraint = mentionsBottomConstraint + self.suggestionViewBottomConstraint = suggestionsBottomConstraint updateConstraintsToAvoidKeyboard(frame: keyboardFrame) - mentionsController.didMove(toParent: self) + suggestionsController.didMove(toParent: self) } } diff --git a/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationDetailsViewController.swift b/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationDetailsViewController.swift index dec1ebc9849d..36c397a266e0 100644 --- a/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationDetailsViewController.swift +++ b/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationDetailsViewController.swift @@ -434,9 +434,8 @@ extension NotificationDetailsViewController { } func setupSuggestionsView() { - suggestionsTableView = SuggestionsTableView() - suggestionsTableView.siteID = note.metaSiteID - suggestionsTableView.suggestionsDelegate = self + guard let siteID = note.metaSiteID else { return } + suggestionsTableView = SuggestionsTableView(siteID: siteID, suggestionType: .mention, delegate: self) suggestionsTableView.translatesAutoresizingMaskIntoConstraints = false } diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderCommentsViewController.m b/WordPress/Classes/ViewRelated/Reader/ReaderCommentsViewController.m index 075ae15c4f7a..ce708f20e458 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderCommentsViewController.m +++ b/WordPress/Classes/ViewRelated/Reader/ReaderCommentsViewController.m @@ -385,9 +385,7 @@ - (void)configureSuggestionsTableView NSNumber *siteID = self.siteID; NSParameterAssert(siteID); - self.suggestionsTableView = [SuggestionsTableView new]; - self.suggestionsTableView.siteID = siteID; - self.suggestionsTableView.suggestionsDelegate = self; + self.suggestionsTableView = [[SuggestionsTableView alloc] initWithSiteID:siteID suggestionType:SuggestionTypeMention delegate:self]; [self.suggestionsTableView setTranslatesAutoresizingMaskIntoConstraints:NO]; [self.view addSubview:self.suggestionsTableView]; } diff --git a/WordPress/Classes/ViewRelated/Suggestions/SuggestionsTableView.h b/WordPress/Classes/ViewRelated/Suggestions/SuggestionsTableView.h index 473dfb777e84..9aede401426c 100644 --- a/WordPress/Classes/ViewRelated/Suggestions/SuggestionsTableView.h +++ b/WordPress/Classes/ViewRelated/Suggestions/SuggestionsTableView.h @@ -1,17 +1,26 @@ #import +typedef NS_CLOSED_ENUM(NSUInteger, SuggestionType) { + SuggestionTypeMention +}; + @protocol SuggestionsTableViewDelegate; -@interface SuggestionsTableView : UIView +@interface SuggestionsTableView : UIView @property (nonatomic, nullable, weak) id suggestionsDelegate; @property (nonatomic, nullable, strong) NSNumber *siteID; +@property (nonatomic, assign) SuggestionType suggestionType; +@property (nonatomic, nonnull, strong) NSMutableArray *searchResults; +@property (nonatomic, nullable, strong) NSArray *suggestions; +@property (nonatomic, nonnull, strong) NSString *searchText; @property (nonatomic) BOOL useTransparentHeader; @property (nonatomic) BOOL animateWithKeyboard; @property (nonatomic) BOOL showLoading; -- (nonnull instancetype)init; - +- (nonnull instancetype)initWithSiteID:(NSNumber *_Nullable)siteID + suggestionType:(SuggestionType)suggestionType + delegate:(id _Nonnull)suggestionsDelegate; /** Enables or disables the SuggestionsTableView component. diff --git a/WordPress/Classes/ViewRelated/Suggestions/SuggestionsTableView.m b/WordPress/Classes/ViewRelated/Suggestions/SuggestionsTableView.m index d1e582581496..0e695281d65e 100644 --- a/WordPress/Classes/ViewRelated/Suggestions/SuggestionsTableView.m +++ b/WordPress/Classes/ViewRelated/Suggestions/SuggestionsTableView.m @@ -13,9 +13,6 @@ @interface SuggestionsTableView () @property (nonatomic, strong) UIView *headerView; @property (nonatomic, strong) UIView *separatorView; @property (nonatomic, strong) UITableView *tableView; -@property (nonatomic, strong) NSArray *suggestions; -@property (nonatomic, strong) NSString *searchText; -@property (nonatomic, strong) NSMutableArray *searchResults; @property (nonatomic, strong) NSLayoutConstraint *headerMinimumHeightConstraint; @property (nonatomic, strong) NSLayoutConstraint *heightConstraint; @@ -25,10 +22,15 @@ @implementation SuggestionsTableView #pragma mark Public methods -- (instancetype)init -{ +- (instancetype)initWithSiteID:(NSNumber *)siteID + suggestionType:(SuggestionType)suggestionType + delegate:(id )suggestionsDelegate +{ self = [super initWithFrame:CGRectZero]; if (self) { + _siteID = siteID; + _suggestionType = suggestionType; + _suggestionsDelegate = suggestionsDelegate; _searchText = @""; _enabled = YES; _searchResults = [[NSMutableArray alloc] init]; @@ -99,7 +101,7 @@ - (void)setupConstraints { // Pin the table view to the view's edges NSDictionary *views = @{@"headerview": self.headerView, - @"separatorview" : self.separatorView, + @"separatorview": self.separatorView, @"tableview": self.tableView }; NSDictionary *metrics = @{@"separatorheight" : @(STVSeparatorHeight)}; [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[headerview]|" @@ -232,13 +234,12 @@ - (BOOL)showSuggestionsForWord:(NSString *)word return NO; } - if ([word hasPrefix:@"@"]) { + if ([word hasPrefix:[self suggestionTrigger]]) { self.searchText = word; if (self.searchText.length > 1) { NSString *searchQuery = [word substringFromIndex:1]; - NSPredicate *resultPredicate = [NSPredicate predicateWithFormat:@"(displayName contains[c] %@) OR (username contains[c] %@)", - searchQuery, searchQuery]; - self.searchResults = [[self.suggestions filteredArrayUsingPredicate:resultPredicate] mutableCopy]; + NSPredicate *predicate = [self predicateFor: searchQuery]; + self.searchResults = [[self.suggestions filteredArrayUsingPredicate:predicate] mutableCopy]; } else { self.searchResults = [self.suggestions mutableCopy]; } @@ -297,77 +298,30 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N forIndexPath:indexPath]; if (!self.suggestions) { - cell.usernameLabel.text = NSLocalizedString(@"Loading...", @"Suggestions loading message"); - cell.displayNameLabel.text = nil; - [cell.avatarImageView setImage:nil]; + cell.titleLabel.text = NSLocalizedString(@"Loading...", @"Suggestions loading message"); + cell.subtitleLabel.text = nil; + [cell.iconImageView setImage:nil]; cell.selectionStyle = UITableViewCellSelectionStyleNone; return cell; } - - UserSuggestion *suggestion = [self.searchResults objectAtIndex:indexPath.row]; - cell.usernameLabel.text = [NSString stringWithFormat:@"@%@", suggestion.username]; - cell.displayNameLabel.text = suggestion.displayName; - cell.selectionStyle = UITableViewCellSelectionStyleDefault; - cell.avatarImageView.image = [UIImage imageNamed:@"gravatar"]; - cell.imageDownloadHash = suggestion.imageURL.hash; - [self loadAvatarForSuggestion:suggestion success:^(UIImage *image) { - if (indexPath.row >= self.searchResults.count) { - return; - } - - UserSuggestion *reloaded = [self.searchResults objectAtIndex:indexPath.row]; - if (cell.imageDownloadHash != reloaded.imageURL.hash) { - return; - } - cell.avatarImageView.image = image; - }]; + cell.selectionStyle = UITableViewCellSelectionStyleDefault; + id suggestion = [self.searchResults objectAtIndex:indexPath.row]; + cell.titleLabel.text = [self titleFor:suggestion]; + cell.subtitleLabel.text = [self subtitleFor:suggestion]; + [self loadImageFor:suggestion in:cell at:indexPath]; return cell; } -#pragma mark - UITableViewDelegate - -- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath -{ - UserSuggestion *suggestion = [self.searchResults objectAtIndex:indexPath.row]; - [self.suggestionsDelegate suggestionsTableView:self - didSelectSuggestion:suggestion.username - forSearchText:[self.searchText substringFromIndex:1]]; -} - #pragma mark - Suggestion list management - (NSArray *)suggestions { if (!_suggestions && _siteID != nil) { - [self suggestionsFor:self.siteID completion:^(NSArray * _Nullable results) { - if (!results) return; - self.suggestions = results; - [self showSuggestionsForWord:self.searchText]; - }]; + [self fetchSuggestionsFor:_siteID]; } return _suggestions; } -#pragma mark - Avatar helper - -- (void)loadAvatarForSuggestion:(UserSuggestion *)suggestion success:(void (^)(UIImage *))success -{ - CGSize imageSize = CGSizeMake(SuggestionsTableViewCellAvatarSize, SuggestionsTableViewCellAvatarSize); - UIImage *image = [suggestion cachedAvatarWith:imageSize]; - if (image) { - success(image); - return; - } - - [suggestion fetchAvatarWith:imageSize success:^(UIImage *image) { - if (!image) { - return; - } - - success(image); - }]; -} - @end diff --git a/WordPress/Classes/ViewRelated/Suggestions/SuggestionsTableView.swift b/WordPress/Classes/ViewRelated/Suggestions/SuggestionsTableView.swift new file mode 100644 index 000000000000..1ee7b4bbd4ca --- /dev/null +++ b/WordPress/Classes/ViewRelated/Suggestions/SuggestionsTableView.swift @@ -0,0 +1,134 @@ +import Foundation + +extension SuggestionType { + var trigger: String { + switch self { + case .mention: return "@" + } + } +} + +@objc public extension SuggestionsTableView { + + func suggestions(for siteID: NSNumber, completion: @escaping ([UserSuggestion]?) -> Void) { + guard let blog = SuggestionService.shared.persistedBlog(for: siteID) else { return } + SuggestionService.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) + } + } + + func title(for suggestion: AnyObject) -> String? { + let title: String? + switch (suggestionType, suggestion) { + case (.mention, let suggestion as UserSuggestion): + title = suggestion.username + default: title = nil + } + return title.map { suggestionType.trigger.appending($0) } + } + + func subtitle(for suggestion: AnyObject) -> String? { + switch (suggestionType, suggestion) { + case (.mention, let suggestion as UserSuggestion): + return suggestion.displayName + default: return nil + } + } + + private func imageURLForSuggestion(at indexPath: IndexPath) -> URL? { + let suggestion = searchResults[indexPath.row] + + switch (suggestionType, suggestion) { + case (.mention, let suggestion as UserSuggestion): + return suggestion.imageURL + default: return nil + } + } + + func loadImage(for suggestion: AnyObject, in cell: SuggestionsTableViewCell, at indexPath: IndexPath) { + cell.iconImageView.image = UIImage(named: "gravatar") + guard let imageURL = imageURLForSuggestion(at: indexPath) else { return } + cell.imageDownloadHash = imageURL.hashValue + + retrieveIcon(for: imageURL) { image in + guard indexPath.row < self.searchResults.count else { return } + if let reloadedImageURL = self.imageURLForSuggestion(at: indexPath), reloadedImageURL.hashValue == cell.imageDownloadHash { + cell.iconImageView.image = image + } + } + } + + func fetchSuggestions(for siteID: NSNumber) { + switch self.suggestionType { + case .mention: + suggestions(for: siteID) { userSuggestions in + self.suggestions = userSuggestions + self.showSuggestions(forWord: self.searchText) + } + } + } + + private func suggestionText(for suggestion: Any) -> String? { + switch (suggestionType, suggestion) { + case (.mention, let suggestion as UserSuggestion): + return suggestion.username + default: return nil + } + } + + private func retrieveIcon(for imageURL: URL?, success: @escaping (UIImage?) -> Void) { + let imageSize = CGSize(width: SuggestionsTableViewCellIconSize, height: SuggestionsTableViewCellIconSize) + + if let image = cachedIcon(for: imageURL, with: imageSize) { + success(image) + } else { + fetchIcon(for: imageURL, with: imageSize, success: success) + } + } + + private func cachedIcon(for imageURL: URL?, with size: CGSize) -> UIImage? { + var hash: NSString? + let type = avatarSourceType(for: imageURL, with: &hash) + + if let hash = hash, let type = type { + return WPAvatarSource.shared()?.cachedImage(forAvatarHash: hash as String, of: type, with: size) + } + return nil + } + + private func fetchIcon(for imageURL: URL?, with size: CGSize, success: @escaping ((UIImage?) -> Void)) { + var hash: NSString? + let type = avatarSourceType(for: imageURL, with: &hash) + + if let hash = hash, let type = type { + WPAvatarSource.shared()?.fetchImage(forAvatarHash: hash as String, of: type, with: size, success: success) + } else { + success(nil) + } + } +} + +extension SuggestionsTableView { + func avatarSourceType(for imageURL: URL?, with hash: inout NSString?) -> WPAvatarSourceType? { + if let imageURL = imageURL { + return WPAvatarSource.shared()?.parseURL(imageURL, forAvatarHash: &hash) + } + return .unknown + } +} + +extension SuggestionsTableView: UITableViewDelegate { + public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let suggestion = searchResults[indexPath.row] + let text = suggestionText(for: suggestion) + let currentSearchText = String(searchText.dropFirst()) + suggestionsDelegate?.suggestionsTableView?(self, didSelectSuggestion: text, forSearchText: currentSearchText) + } +} diff --git a/WordPress/Classes/ViewRelated/Suggestions/SuggestionsTableViewCell.h b/WordPress/Classes/ViewRelated/Suggestions/SuggestionsTableViewCell.h index c4abbe797de2..f88b95801d39 100644 --- a/WordPress/Classes/ViewRelated/Suggestions/SuggestionsTableViewCell.h +++ b/WordPress/Classes/ViewRelated/Suggestions/SuggestionsTableViewCell.h @@ -1,12 +1,12 @@ #import -extern NSInteger const SuggestionsTableViewCellAvatarSize; +extern NSInteger const SuggestionsTableViewCellIconSize; @interface SuggestionsTableViewCell : UITableViewCell -@property (nonatomic, strong) UILabel *usernameLabel; -@property (nonatomic, strong) UILabel *displayNameLabel; -@property (nonatomic, strong) UIImageView *avatarImageView; -@property (nonatomic, assign) NSUInteger imageDownloadHash; +@property (nonatomic, strong) UILabel *titleLabel; +@property (nonatomic, strong) UILabel *subtitleLabel; +@property (nonatomic, strong) UIImageView *iconImageView; +@property (nonatomic, assign) NSInteger imageDownloadHash; @end diff --git a/WordPress/Classes/ViewRelated/Suggestions/SuggestionsTableViewCell.m b/WordPress/Classes/ViewRelated/Suggestions/SuggestionsTableViewCell.m index 51821c9a4a8d..eff54b4d0cf3 100644 --- a/WordPress/Classes/ViewRelated/Suggestions/SuggestionsTableViewCell.m +++ b/WordPress/Classes/ViewRelated/Suggestions/SuggestionsTableViewCell.m @@ -2,7 +2,7 @@ #import #import "WordPress-Swift.h" -NSInteger const SuggestionsTableViewCellAvatarSize = 24; +NSInteger const SuggestionsTableViewCellIconSize = 24; @implementation SuggestionsTableViewCell @@ -10,9 +10,9 @@ - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSStr { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; if (self) { - [self setupUsernameLabel]; - [self setupDisplayNameLabel]; - [self setupAvatarImageView]; + [self setupTitleLabel]; + [self setupSubtitleLabel]; + [self setupIconImageView]; [self setupConstraints]; self.backgroundColor = [UIColor murielListForeground]; } @@ -24,53 +24,52 @@ - (void)prepareForReuse { self.imageDownloadHash = 0; } -- (void)setupUsernameLabel +- (void)setupTitleLabel { - _usernameLabel = [[UILabel alloc] init]; - [_usernameLabel setTextColor:[UIColor murielPrimary]]; - [_usernameLabel setFont:[WPFontManager systemRegularFontOfSize:17.0]]; - [_usernameLabel setTranslatesAutoresizingMaskIntoConstraints:NO]; - [self.contentView addSubview:_usernameLabel]; + _titleLabel = [[UILabel alloc] init]; + [_titleLabel setTextColor:[UIColor murielPrimary]]; + [_titleLabel setFont:[WPFontManager systemRegularFontOfSize:17.0]]; + [_titleLabel setTranslatesAutoresizingMaskIntoConstraints:NO]; + [self.contentView addSubview:_titleLabel]; } -- (void)setupDisplayNameLabel +- (void)setupSubtitleLabel { - _displayNameLabel = [[UILabel alloc] init]; - [_displayNameLabel setTextColor:[UIColor murielTextSubtle]]; - [_displayNameLabel setFont:[WPFontManager systemRegularFontOfSize:14.0]]; - [_displayNameLabel setTranslatesAutoresizingMaskIntoConstraints:NO]; - _displayNameLabel.textAlignment = NSTextAlignmentRight; - [self.contentView addSubview:_displayNameLabel]; + _subtitleLabel = [[UILabel alloc] init]; + [_subtitleLabel setTextColor:[UIColor murielTextSubtle]]; + [_subtitleLabel setFont:[WPFontManager systemRegularFontOfSize:14.0]]; + [_subtitleLabel setTranslatesAutoresizingMaskIntoConstraints:NO]; + _subtitleLabel.textAlignment = NSTextAlignmentRight; + [self.contentView addSubview:_subtitleLabel]; } -- (void)setupAvatarImageView +- (void)setupIconImageView { - _avatarImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, SuggestionsTableViewCellAvatarSize, SuggestionsTableViewCellAvatarSize)]; - _avatarImageView.contentMode = UIViewContentModeScaleAspectFit; - _avatarImageView.clipsToBounds = YES; - _avatarImageView.image = [UIImage imageNamed:@"gravatar.png"]; - [_avatarImageView setTranslatesAutoresizingMaskIntoConstraints:NO]; - [self.contentView addSubview:_avatarImageView]; + _iconImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, SuggestionsTableViewCellIconSize, SuggestionsTableViewCellIconSize)]; + _iconImageView.contentMode = UIViewContentModeScaleAspectFit; + _iconImageView.clipsToBounds = YES; + [_iconImageView setTranslatesAutoresizingMaskIntoConstraints:NO]; + [self.contentView addSubview:_iconImageView]; } - (void)setupConstraints { NSDictionary *views = @{@"contentview": self.contentView, - @"username": _usernameLabel, - @"displayname": _displayNameLabel, - @"avatar": _avatarImageView }; + @"title": _titleLabel, + @"subtitle": _subtitleLabel, + @"icon": _iconImageView }; - NSDictionary *metrics = @{@"avatarsize": @(SuggestionsTableViewCellAvatarSize) }; + NSDictionary *metrics = @{@"iconsize": @(SuggestionsTableViewCellIconSize) }; // Horizontal spacing - NSArray *horizConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[avatar(avatarsize)]-16-[username]-[displayname]-|" + NSArray *horizConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[icon(iconsize)]-16-[title]-[subtitle]-|" options:0 metrics:metrics views:views]; [self.contentView addConstraints:horizConstraints]; // Vertically constrain centers of each element - [self.contentView addConstraint:[NSLayoutConstraint constraintWithItem:_usernameLabel + [self.contentView addConstraint:[NSLayoutConstraint constraintWithItem:_titleLabel attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self.contentView @@ -78,7 +77,7 @@ - (void)setupConstraints multiplier:1.0 constant:0]]; - [self.contentView addConstraint:[NSLayoutConstraint constraintWithItem:_displayNameLabel + [self.contentView addConstraint:[NSLayoutConstraint constraintWithItem:_subtitleLabel attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self.contentView @@ -86,7 +85,7 @@ - (void)setupConstraints multiplier:1.0 constant:0]]; - [self.contentView addConstraint:[NSLayoutConstraint constraintWithItem:_avatarImageView + [self.contentView addConstraint:[NSLayoutConstraint constraintWithItem:_iconImageView attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self.contentView diff --git a/WordPress/Classes/ViewRelated/SuggestionsTableView.swift b/WordPress/Classes/ViewRelated/SuggestionsTableView.swift deleted file mode 100644 index 920fe7823b4c..000000000000 --- a/WordPress/Classes/ViewRelated/SuggestionsTableView.swift +++ /dev/null @@ -1,8 +0,0 @@ -import Foundation - -@objc public extension SuggestionsTableView { - func suggestions(for siteID: NSNumber, completion: @escaping ([UserSuggestion]?) -> Void) { - guard let blog = SuggestionService.shared.persistedBlog(for: siteID) else { return } - SuggestionService.shared.suggestions(for: blog, completion: completion) - } -} diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 5b7e9e3a832d..b63066e4fa9c 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -1516,12 +1516,12 @@ ADF544C2195A0F620092213D /* CustomHighlightButton.m in Sources */ = {isa = PBXBuildFile; fileRef = ADF544C1195A0F620092213D /* CustomHighlightButton.m */; }; B03B9234250BC593000A40AF /* SuggestionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B03B9233250BC593000A40AF /* SuggestionService.swift */; }; B03B9236250BC5FD000A40AF /* Suggestion.swift in Sources */ = {isa = PBXBuildFile; fileRef = B03B9235250BC5FD000A40AF /* Suggestion.swift */; }; - B0AC4FD6251E851D0039E022 /* SuggestionsTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0AC4FD5251E851D0039E022 /* SuggestionsTableView.swift */; }; + B0637527253E7CEC00FD45D2 /* SuggestionsTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0637526253E7CEB00FD45D2 /* SuggestionsTableView.swift */; }; + B0637543253E7E7A00FD45D2 /* GutenbergSuggestionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0637542253E7E7A00FD45D2 /* GutenbergSuggestionsViewController.swift */; }; B0AC50B4251E959B0039E022 /* CommentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0AC50B3251E959B0039E022 /* CommentViewController.swift */; }; 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 */; }; - B0DDC343252F9279002BAFB3 /* UserSuggestion+ImageHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0DDC342252F9279002BAFB3 /* UserSuggestion+ImageHelpers.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 */; }; @@ -2308,7 +2308,6 @@ FF37F90922385CA000AFA3DB /* RELEASE-NOTES.txt in Resources */ = {isa = PBXBuildFile; fileRef = FF37F90822385C9F00AFA3DB /* RELEASE-NOTES.txt */; }; FF4258501BA092EE00580C68 /* RelatedPostsSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FF42584F1BA092EE00580C68 /* RelatedPostsSettingsViewController.m */; }; FF4C069F206560E500E0B2BC /* MediaThumbnailCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4C069E206560E500E0B2BC /* MediaThumbnailCoordinator.swift */; }; - FF4DEAD62448FFC900ACA032 /* GutenbergMentionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4DEAD52448FFC900ACA032 /* GutenbergMentionsViewController.swift */; }; FF4DEAD8244B56E300ACA032 /* CoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FF4DEAD7244B56E200ACA032 /* CoreServices.framework */; }; FF5371631FDFF64F00619A3F /* MediaService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5371621FDFF64F00619A3F /* MediaService.swift */; }; FF54D4641D6F3FA900A0DC4D /* GutenbergSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF54D4631D6F3FA900A0DC4D /* GutenbergSettings.swift */; }; @@ -4076,13 +4075,13 @@ ADF544C1195A0F620092213D /* CustomHighlightButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CustomHighlightButton.m; sourceTree = ""; }; B03B9233250BC593000A40AF /* SuggestionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionService.swift; sourceTree = ""; }; B03B9235250BC5FD000A40AF /* Suggestion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Suggestion.swift; sourceTree = ""; }; - B0AC4FD5251E851D0039E022 /* SuggestionsTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionsTableView.swift; sourceTree = ""; }; + B0637526253E7CEB00FD45D2 /* SuggestionsTableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SuggestionsTableView.swift; path = Suggestions/SuggestionsTableView.swift; sourceTree = ""; }; + B0637542253E7E7A00FD45D2 /* GutenbergSuggestionsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GutenbergSuggestionsViewController.swift; sourceTree = ""; }; B0AC50B3251E959B0039E022 /* CommentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentViewController.swift; sourceTree = ""; }; B0AC50DC251E96270039E022 /* ReaderCommentsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderCommentsViewController.swift; sourceTree = ""; }; B0B68A9A252FA91E0001B28C /* UserSuggestion+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserSuggestion+CoreDataClass.swift"; sourceTree = ""; }; B0B68A9B252FA91E0001B28C /* UserSuggestion+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserSuggestion+CoreDataProperties.swift"; sourceTree = ""; }; B0DDC2EB252F7C4F002BAFB3 /* WordPress 100.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 100.xcdatamodel"; sourceTree = ""; }; - B0DDC342252F9279002BAFB3 /* UserSuggestion+ImageHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserSuggestion+ImageHelpers.swift"; sourceTree = ""; }; B5015C571D4FDBB300C9449E /* NotificationActionsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationActionsService.swift; sourceTree = ""; }; 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; }; @@ -5040,7 +5039,6 @@ FF42584E1BA092EE00580C68 /* RelatedPostsSettingsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RelatedPostsSettingsViewController.h; sourceTree = ""; }; FF42584F1BA092EE00580C68 /* RelatedPostsSettingsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RelatedPostsSettingsViewController.m; sourceTree = ""; }; FF4C069E206560E500E0B2BC /* MediaThumbnailCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaThumbnailCoordinator.swift; sourceTree = ""; }; - FF4DEAD52448FFC900ACA032 /* GutenbergMentionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GutenbergMentionsViewController.swift; sourceTree = ""; }; FF4DEAD7244B56E200ACA032 /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = System/Library/Frameworks/CoreServices.framework; sourceTree = SDKROOT; }; FF5371621FDFF64F00619A3F /* MediaService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaService.swift; sourceTree = ""; }; FF54D4631D6F3FA900A0DC4D /* GutenbergSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GutenbergSettings.swift; sourceTree = ""; }; @@ -5880,7 +5878,6 @@ 8B1CF0102433E61C00578582 /* AbstractPost+TitleForVisibility.swift */, B0B68A9A252FA91E0001B28C /* UserSuggestion+CoreDataClass.swift */, B0B68A9B252FA91E0001B28C /* UserSuggestion+CoreDataProperties.swift */, - B0DDC342252F9279002BAFB3 /* UserSuggestion+ImageHelpers.swift */, 5D42A3D8175E7452005CFF05 /* BasePost.h */, 5D42A3D9175E7452005CFF05 /* BasePost.m */, E19B17AD1E5C6944007517C6 /* BasePost.swift */, @@ -5967,7 +5964,7 @@ children = ( 319D6E7F19E44C680013871C /* SuggestionsTableView.h */, 319D6E8019E44C680013871C /* SuggestionsTableView.m */, - B0AC4FD5251E851D0039E022 /* SuggestionsTableView.swift */, + B0637526253E7CEB00FD45D2 /* SuggestionsTableView.swift */, 319D6E8319E44F7F0013871C /* SuggestionsTableViewCell.h */, 319D6E8419E44F7F0013871C /* SuggestionsTableViewCell.m */, ); @@ -7571,6 +7568,7 @@ 7E3E9B6E2177C9C300FD5797 /* Gutenberg */ = { isa = PBXGroup; children = ( + B0637542253E7E7A00FD45D2 /* GutenbergSuggestionsViewController.swift */, FF2EC3BE2209A105006176E1 /* Processors */, 1ED046CE244F26B1008F6365 /* GutenbergWeb */, 4631359224AD057E0017E65C /* Layout Picker */, @@ -7586,7 +7584,6 @@ 912347182213484300BD9F97 /* GutenbergViewController+InformativeDialog.swift */, 912347752216E27200BD9F97 /* GutenbergViewController+Localization.swift */, FFC02B82222687BF00E64FDE /* GutenbergImageLoader.swift */, - FF4DEAD52448FFC900ACA032 /* GutenbergMentionsViewController.swift */, 4625B5472537875E00C04AAD /* CollapsableHeader */, ); path = Gutenberg; @@ -12841,8 +12838,6 @@ B54346961C6A707D0010B3AD /* LanguageViewController.swift in Sources */, 43D74AD020F906EE004AD934 /* InlineEditableNameValueCell.swift in Sources */, 4089C51122371B120031CE78 /* TodayStatsRecordValue+CoreDataProperties.swift in Sources */, - FF4DEAD62448FFC900ACA032 /* GutenbergMentionsViewController.swift in Sources */, - B0AC4FD6251E851D0039E022 /* SuggestionsTableView.swift in Sources */, F928EDA3226140620030D451 /* WPCrashLoggingProvider.swift in Sources */, D8A3A5B3206A49BF00992576 /* StockPhotosMedia.swift in Sources */, 176BB87F20D0068500751DCE /* FancyAlertViewController+SavedPosts.swift in Sources */, @@ -12892,6 +12887,7 @@ F5E6312B243BC83E0088229D /* FilterSheetViewController.swift in Sources */, E6F2788021BC1A4A008B4DB5 /* Plan.swift in Sources */, B5AC00681BE3C4E100F8E7C3 /* DiscussionSettingsViewController.swift in Sources */, + B0637543253E7E7A00FD45D2 /* GutenbergSuggestionsViewController.swift in Sources */, D816C1F620E0896F00C4D82F /* TrashComment.swift in Sources */, 08AAD69F1CBEA47D002B2418 /* MenusService.m in Sources */, 9A4E216021F87AE200EFF212 /* QuickStartChecklistHeader.swift in Sources */, @@ -13042,7 +13038,6 @@ 2F161B0622CC2DC70066A5C5 /* LoadingStatusView.swift in Sources */, 080C44A91CE14A9F00B3A02F /* MenuDetailsViewController.m in Sources */, 2FA6511B21F26A57009AA935 /* InlineErrorRetryTableViewCell.swift in Sources */, - B0DDC343252F9279002BAFB3 /* UserSuggestion+ImageHelpers.swift in Sources */, 08216FCF1CDBF96000304BA7 /* MenuItemSourceCell.m in Sources */, 462F4E0A18369F0B0028D2F8 /* BlogDetailsViewController.m in Sources */, 3FCCAA1523F4A1A3004064C0 /* UIBarButtonItem+MeBarButton.swift in Sources */, @@ -13465,6 +13460,7 @@ 08216FCA1CDBF96000304BA7 /* MenuItemEditingFooterView.m in Sources */, E6D3E8491BEBD871002692E8 /* ReaderCrossPostCell.swift in Sources */, E10290741F30615A00DAC588 /* Role.swift in Sources */, + B0637527253E7CEC00FD45D2 /* SuggestionsTableView.swift in Sources */, 08216FD11CDBF96000304BA7 /* MenuItemSourceResultsViewController.m in Sources */, 435B762222973D0600511813 /* UIColor+MurielColors.swift in Sources */, 4666534A2501552A00165DD4 /* LayoutPreviewViewController.swift in Sources */,