Skip to content

Commit

Permalink
Merge pull request #13942 from wordpress-mobile/issue/implement_menti…
Browse files Browse the repository at this point in the history
…ons_in_gutenberg

Issue/implement mentions in gutenberg
  • Loading branch information
SergioEstevao authored May 20, 2020
2 parents 5683f66 + 022bdab commit ea0717d
Show file tree
Hide file tree
Showing 11 changed files with 442 additions and 90 deletions.
4 changes: 2 additions & 2 deletions Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def aztec
## pod 'WordPress-Editor-iOS', :git => 'https://github.com/wordpress-mobile/AztecEditor-iOS.git', :commit => 'ba8524aba1332550efb05cad583a85ed3511beb5'
## pod 'WordPress-Editor-iOS', :git => 'https://github.com/wordpress-mobile/AztecEditor-iOS.git', :tag => '1.5.0.beta.1'
## pod 'WordPress-Editor-iOS', :path => '../AztecEditor-iOS'
pod 'WordPress-Editor-iOS', '~> 1.19.0'
pod 'WordPress-Editor-iOS', '~> 1.19.1'
end

def wordpress_ui
Expand Down Expand Up @@ -147,7 +147,7 @@ target 'WordPress' do
## Gutenberg (React Native)
## =====================
##
gutenberg :tag => 'v1.28.0'
gutenberg :commit => '1f5ee28cfd84a9a7fd962f0d63f560baeb777ba7'

## Third party libraries
## =====================
Expand Down
144 changes: 72 additions & 72 deletions Podfile.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@

14.8
-----
* Block Editor: Mentions insertion are now available for WP.com and Jetpack sites.

* Block editor: Prefill caption for image blocks when available on the Media library
* Block editor: New block: Buttons. From now you’ll be able to add the individual Button block only inside the Buttons block
* Block editor: Fix bug where whitespaces at start of text blocks were being removed
Expand Down
14 changes: 12 additions & 2 deletions WordPress/Classes/Categories/WPStyleGuide+Suggestions.m
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,18 @@
@implementation WPStyleGuide (Suggestions)

+ (UIColor *)suggestionsHeaderSmoke
{
return [UIColor colorWithRed:0. green:0. blue:0. alpha:0.3];
{
if (@available(iOS 13.0, *)) {
return [UIColor colorWithDynamicProvider:^(UITraitCollection *traitCollection) {
if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
return [UIColor colorWithRed:0. green:0. blue:0. alpha:0.7];
} else {
return [UIColor colorWithRed:0. green:0. blue:0. alpha:0.3];
}
}];
} else {
return [UIColor colorWithRed:0. green:0. blue:0. alpha:0.3];
}
}

+ (UIColor *)suggestionsSeparatorSmoke
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import Foundation
import UIKit

public class GutenbergMentionsViewController: UIViewController {

static let mentionTriggerText = String("@")

public lazy var backgroundView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = .listForeground
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()

public lazy var separatorView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = UIColor.divider
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = UIColor(light: UIColor.colorFromHex("e9eff3"), dark: UIColor.colorFromHex("2e2e2e"))
return view
}()

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.clearButtonMode = .whileEditing
textField.translatesAutoresizingMaskIntoConstraints = false
textField.delegate = self
textField.textColor = .text
return textField
}()

public lazy var suggestionsView: SuggestionsTableView = {
let suggestionsView = SuggestionsTableView()
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
public var onCompletion: ((Result<String, NSError>) -> Void)?

public init(siteID: NSNumber) {
self.siteID = siteID
super.init(nibName: nil, bundle: nil)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override public func viewDidLoad() {
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .clear

let toolbarSize = CGFloat(44)

view.addSubview(backgroundView)
NSLayoutConstraint.activate([
backgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
backgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
backgroundView.bottomAnchor.constraint(equalTo: view.safeBottomAnchor),
backgroundView.heightAnchor.constraint(equalToConstant: toolbarSize)
])

let margin = CGFloat(10)
view.addSubview(searchView)
searchView.becomeFirstResponder()
NSLayoutConstraint.activate([
searchView.leadingAnchor.constraint(equalTo: view.safeLeadingAnchor, constant: margin),
searchView.trailingAnchor.constraint(equalTo: view.safeTrailingAnchor, constant: -margin),
searchView.bottomAnchor.constraint(equalTo: view.safeBottomAnchor),
searchView.heightAnchor.constraint(equalToConstant: toolbarSize)
])

view.addSubview(suggestionsView)
NSLayoutConstraint.activate([
suggestionsView.leadingAnchor.constraint(equalTo: view.safeLeadingAnchor, constant: 0),
suggestionsView.trailingAnchor.constraint(equalTo: view.safeTrailingAnchor, constant: 0),
suggestionsView.bottomAnchor.constraint(equalTo: searchView.topAnchor),
suggestionsView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
])

view.addSubview(separatorView)
NSLayoutConstraint.activate([
separatorView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
separatorView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
separatorView.bottomAnchor.constraint(equalTo: backgroundView.topAnchor),
separatorView.heightAnchor.constraint(equalToConstant: 1.0)
])

view.setNeedsUpdateConstraints()
}

override public func viewDidAppear(_ animated: Bool) {
suggestionsView.showSuggestions(forWord: Self.mentionTriggerText)
}
}

extension GutenbergMentionsViewController: UITextFieldDelegate {

public func textFieldShouldClear(_ textField: UITextField) -> Bool {
onCompletion?(.failure(buildErrorForCancelation()))
return true
}

public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let nsString = textField.text as NSString? else {
return true
}
let searchWord = nsString.replacingCharacters(in: range, with: string)
if searchWord.hasPrefix(Self.mentionTriggerText) {
suggestionsView.showSuggestions(forWord: searchWord)
} else {
// We are dispatching this async to allow this delegate to finish and process the keypress before executing the cancelation.
DispatchQueue.main.async {
self.onCompletion?(.failure(self.buildErrorForCancelation()))
}
}
return true
}

public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if suggestionsView.numberOfSuggestions() == 1 {
suggestionsView.selectSuggestion(atPosition: 0)
}
return true
}
}

extension GutenbergMentionsViewController: SuggestionsTableViewDelegate {

public func suggestionsTableView(_ suggestionsTableView: SuggestionsTableView, didSelectSuggestion suggestion: String?, forSearchText text: String) {
if let suggestion = suggestion {
onCompletion?(.success(suggestion))
}
}

public func suggestionsTableView(_ suggestionsTableView: SuggestionsTableView, didChangeTableBounds bounds: CGRect) {

}

public func suggestionsTableViewMaxDisplayedRows(_ suggestionsTableView: SuggestionsTableView) -> Int {
return 7
}

public func suggestionsTableViewDidTapHeader(_ suggestionsTableView: SuggestionsTableView) {
onCompletion?(.failure(buildErrorForCancelation()))
}
}

extension GutenbergMentionsViewController {

enum MentionError: CustomNSError {
case canceled
case notAvailable

static var errorDomain: String = "MentionErrorDomain"

var errorCode: Int {
switch self {
case .canceled:
return 1
case .notAvailable:
return 2
}
}

var errorUserInfo: [String: Any] {
return [:]
}
}

private func buildErrorForCancelation() -> NSError {
return MentionError.canceled as NSError
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ class GutenbergViewController: UIViewController, PostEditor {
}

deinit {
tearDownKeyboardObservers()
removeObservers(fromPost: post)
gutenberg.invalidate()
attachmentDelegate.cancelAllPendingMediaRequests()
Expand All @@ -263,6 +264,7 @@ class GutenbergViewController: UIViewController, PostEditor {

override func viewDidLoad() {
super.viewDidLoad()
setupKeyboardObservers()
WPFontManager.loadNotoFontFamily()
createRevisionOfPost(loadAutosaveRevision: loadAutosaveRevision)
setupGutenbergView()
Expand All @@ -280,6 +282,36 @@ class GutenbergViewController: UIViewController, PostEditor {

// MARK: - Functions

private var keyboardShowObserver: Any?
private var keyboardHideObserver: Any?
private var keyboardFrame = CGRect.zero
private var mentionsBottomConstraint: NSLayoutConstraint?
private var previousFirstResponder: UIView?

private func setupKeyboardObservers() {
keyboardShowObserver = NotificationCenter.default.addObserver(forName: UIResponder.keyboardDidShowNotification, object: nil, queue: .main) { (notification) in
if let keyboardRect = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
self.keyboardFrame = keyboardRect
self.updateConstraintsToAvoidKeyboard(frame: keyboardRect)
}
}
keyboardHideObserver = NotificationCenter.default.addObserver(forName: UIResponder.keyboardDidShowNotification, object: nil, queue: .main) { (notification) in
if let keyboardRect = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
self.keyboardFrame = keyboardRect
self.updateConstraintsToAvoidKeyboard(frame: keyboardRect)
}
}
}

private func tearDownKeyboardObservers() {
if let keyboardShowObserver = keyboardShowObserver {
NotificationCenter.default.removeObserver(keyboardShowObserver)
}
if let keyboardHideObserver = keyboardHideObserver {
NotificationCenter.default.removeObserver(keyboardHideObserver)
}
}

private func configureNavigationBar() {
navigationController?.navigationBar.isTranslucent = false
navigationController?.navigationBar.accessibilityIdentifier = "Gutenberg Editor Navigation Bar"
Expand Down Expand Up @@ -614,6 +646,65 @@ extension GutenbergViewController: GutenbergBridgeDelegate {
controller.modalPresentationStyle = .overCurrentContext
self.present(controller, animated: true)
}

func updateConstraintsToAvoidKeyboard(frame: CGRect) {
keyboardFrame = frame
let minimumKeyboardHeight = CGFloat(50)
guard let mentionsBottomConstraint = mentionsBottomConstraint 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
}
else {
mentionsBottomConstraint.constant = -self.keyboardFrame.height
}
}

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

// MARK: - Mention implementation

extension GutenbergViewController {

private func mentionShow(callback: @escaping (Swift.Result<String, NSError>) -> Void) {
guard let siteID = post.blog.dotComID else {
callback(.failure(GutenbergMentionsViewController.MentionError.notAvailable as NSError))
return
}

previousFirstResponder = view.findFirstResponder()
let mentionsController = GutenbergMentionsViewController(siteID: siteID)
mentionsController.onCompletion = { (result) in
callback(result)
mentionsController.view.removeFromSuperview()
mentionsController.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)
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)
])
self.mentionsBottomConstraint = mentionsBottomConstraint
updateConstraintsToAvoidKeyboard(frame: keyboardFrame)
mentionsController.didMove(toParent: self)
}

}

// MARK: - GutenbergBridgeDataSource
Expand Down Expand Up @@ -650,6 +741,12 @@ extension GutenbergViewController: GutenbergBridgeDataSource {
.filesApp,
].compactMap { $0 }
}

func gutenbergCapabilities() -> [String: Bool]? {
return [
"mentions": post.blog.isAccessibleThroughWPCom()
]
}
}

// MARK: - PostEditorStateContextDelegate
Expand Down
15 changes: 15 additions & 0 deletions WordPress/Classes/ViewRelated/Suggestions/SuggestionsTableView.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
@property (nonatomic, nullable, weak) id <SuggestionsTableViewDelegate> suggestionsDelegate;
@property (nonatomic, nullable, strong) NSNumber *siteID;
@property (nonatomic) BOOL useTransparentHeader;
@property (nonatomic) BOOL animateWithKeyboard;
@property (nonatomic) BOOL showLoading;

- (nonnull instancetype)init;

Expand All @@ -27,6 +29,14 @@
- (BOOL)showSuggestionsForWord:(nonnull NSString *)word;

- (void)hideSuggestions;

/// Tells the number of suggestions available for the current search
- (NSInteger)numberOfSuggestions;

/// Select the suggestion at a certain position and triggers the selection delegate
/// @param position the index to select
- (void)selectSuggestionAtPosition:(NSInteger)position;

@end

@protocol SuggestionsTableViewDelegate <NSObject>
Expand All @@ -53,4 +63,9 @@
*/
- (NSInteger)suggestionsTableViewMaxDisplayedRows:(nonnull SuggestionsTableView *)suggestionsTableView;

/// This method is called every the header view above the suggestion is tapped.
/// @param suggestionsTableView the suggestion view.
- (void)suggestionsTableViewDidTapHeader:(nonnull SuggestionsTableView *)suggestionsTableView;


@end
Loading

0 comments on commit ea0717d

Please sign in to comment.