@@ -7,6 +7,7 @@ import SafariServices
77import WordPressData
88import WordPressShared
99import WebKit
10+ import CocoaLumberjackSwift
1011
1112class NewGutenbergViewController : UIViewController , PostEditor , PublishingEditor {
1213 let errorDomain : String = " GutenbergViewController.errorDomain "
@@ -79,6 +80,14 @@ class NewGutenbergViewController: UIViewController, PostEditor, PublishingEditor
7980 self . performAutoSave ( )
8081 }
8182
83+ // MARK: - Private Properties
84+
85+ private var keyboardShowObserver : Any ?
86+ private var keyboardHideObserver : Any ?
87+ private var keyboardFrame = CGRect . zero
88+ private var suggestionViewBottomConstraint : NSLayoutConstraint ?
89+ private var currentSuggestionsController : GutenbergSuggestionsViewController ?
90+
8291 // TODO: remove (none of these APIs are needed for the new editor)
8392 func prepopulateMediaItems( _ media: [ Media ] ) { }
8493 var debouncer = WordPressShared . Debouncer ( delay: 10 )
@@ -146,10 +155,15 @@ class NewGutenbergViewController: UIViewController, PostEditor, PublishingEditor
146155 fatalError ( )
147156 }
148157
158+ deinit {
159+ tearDownKeyboardObservers ( )
160+ }
161+
149162 // MARK: - Lifecycle methods
150163
151164 override func viewDidLoad( ) {
152165 super. viewDidLoad ( )
166+ setupKeyboardObservers ( )
153167
154168 view. backgroundColor = . systemBackground
155169
@@ -218,10 +232,9 @@ class NewGutenbergViewController: UIViewController, PostEditor, PublishingEditor
218232 setTitle ( post. postTitle ?? " " )
219233 editorViewController. setContent ( content)
220234
221- // TODO: reimplement
222- // SiteSuggestionService.shared.prefetchSuggestionsIfNeeded(for: post.blog) { [weak self] in
223- // self?.gutenberg.updateCapabilities()
224- // }
235+ SiteSuggestionService . shared. prefetchSuggestionsIfNeeded ( for: post. blog) {
236+ // Do nothing
237+ }
225238 }
226239
227240 private func refreshInterface( ) {
@@ -245,7 +258,7 @@ class NewGutenbergViewController: UIViewController, PostEditor, PublishingEditor
245258 let startTime = CFAbsoluteTimeGetCurrent ( )
246259 let editorData = try ? await editorViewController. getTitleAndContent ( )
247260 let duration = CFAbsoluteTimeGetCurrent ( ) - startTime
248- print ( " gutenbergkit-measure_get-latest-content: " , duration)
261+ DDLogDebug ( " gutenbergkit-measure_get-latest-content: \( duration) " )
249262
250263 if let title = editorData? . title,
251264 let content = editorData? . content,
@@ -273,6 +286,49 @@ class NewGutenbergViewController: UIViewController, PostEditor, PublishingEditor
273286 }
274287 }
275288
289+ // MARK: - Keyboard Observers
290+
291+ private func setupKeyboardObservers( ) {
292+ keyboardShowObserver = NotificationCenter . default. addObserver ( forName: UIResponder . keyboardDidShowNotification, object: nil , queue: . main) { [ weak self] ( notification) in
293+ if let self, let keyboardRect = notification. userInfo ? [ UIResponder . keyboardFrameEndUserInfoKey] as? CGRect {
294+ self . keyboardFrame = keyboardRect
295+ self . updateConstraintsToAvoidKeyboard ( frame: keyboardRect)
296+ }
297+ }
298+ keyboardHideObserver = NotificationCenter . default. addObserver ( forName: UIResponder . keyboardDidHideNotification, object: nil , queue: . main) { [ weak self] ( notification) in
299+ if let self, let keyboardRect = notification. userInfo ? [ UIResponder . keyboardFrameEndUserInfoKey] as? CGRect {
300+ self . keyboardFrame = keyboardRect
301+ self . updateConstraintsToAvoidKeyboard ( frame: keyboardRect)
302+ }
303+ }
304+ }
305+
306+ private func tearDownKeyboardObservers( ) {
307+ if let keyboardShowObserver {
308+ NotificationCenter . default. removeObserver ( keyboardShowObserver)
309+ }
310+ if let keyboardHideObserver {
311+ NotificationCenter . default. removeObserver ( keyboardHideObserver)
312+ }
313+ }
314+
315+ private func updateConstraintsToAvoidKeyboard( frame: CGRect ) {
316+ keyboardFrame = frame
317+ let minimumKeyboardHeight = CGFloat ( 50 )
318+ guard let suggestionViewBottomConstraint else {
319+ return
320+ }
321+
322+ // 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.
323+ // So in those scenarios, we just need to take in account the safe area and ignore the keyboard all together.
324+ if keyboardFrame. height < minimumKeyboardHeight {
325+ suggestionViewBottomConstraint. constant = - self . view. safeAreaInsets. bottom
326+ }
327+ else {
328+ suggestionViewBottomConstraint. constant = - self . keyboardFrame. height
329+ }
330+ }
331+
276332 // MARK: - Activity Indicator
277333
278334 private func showActivityIndicator( ) {
@@ -459,14 +515,41 @@ extension NewGutenbergViewController: GutenbergKit.EditorViewControllerDelegate
459515 }
460516 }
461517
518+ func editor( _ viewController: GutenbergKit . EditorViewController , didTriggerAutocompleter type: String ) {
519+ switch type {
520+ case " at-symbol " :
521+ showSuggestions ( type: . mention) { [ weak self] result in
522+ switch result {
523+ case . success( let suggestion) :
524+ // Appended space completes the autocomplete session
525+ self ? . editorViewController. appendTextAtCursor ( suggestion + " " )
526+ case . failure( let error) :
527+ DDLogError ( " Mention selection cancelled or failed: \( error) " )
528+ }
529+ }
530+ case " plus-symbol " :
531+ showSuggestions ( type: . xpost) { [ weak self] result in
532+ switch result {
533+ case . success( let suggestion) :
534+ // Appended space completes the autocomplete session
535+ self ? . editorViewController. appendTextAtCursor ( suggestion + " " )
536+ case . failure( let error) :
537+ DDLogError ( " Xpost selection cancelled or failed: \( error) " )
538+ }
539+ }
540+ default :
541+ DDLogError ( " Unknown autocompleter type: \( type) " )
542+ }
543+ }
544+
462545 private func convertMediaInfoArrayToJSONString( _ mediaInfoArray: [ MediaInfo ] ) -> String ? {
463546 do {
464547 let jsonData = try JSONEncoder ( ) . encode ( mediaInfoArray)
465548 if let jsonString = String ( data: jsonData, encoding: . utf8) {
466549 return jsonString
467550 }
468551 } catch {
469- print ( " Error encoding MediaInfo array: \( error) " )
552+ DDLogError ( " Error encoding MediaInfo array: \( error) " )
470553 }
471554 return nil
472555 }
@@ -521,18 +604,80 @@ extension NewGutenbergViewController {
521604 present ( lightboxVC, animated: true )
522605 }
523606
524- // TODO: reimplement
525- // func gutenbergDidRequestMention(callback: @escaping (Swift.Result<String, NSError>) -> Void) {
526- // DispatchQueue.main.async(execute: { [weak self] in
527- // self?.showSuggestions(type: .mention, callback: callback)
528- // })
529- // }
530- //
531- // func gutenbergDidRequestXpost(callback: @escaping (Swift.Result<String, NSError>) -> Void) {
532- // DispatchQueue.main.async(execute: { [weak self] in
533- // self?.showSuggestions(type: .xpost, callback: callback)
534- // })
535- // }
607+ }
608+
609+ // MARK: - Suggestions implementation
610+
611+ extension NewGutenbergViewController {
612+
613+ private func showSuggestions( type: SuggestionType , callback: @escaping ( Swift . Result < String , NSError > ) -> Void ) {
614+ // Prevent multiple suggestions UI instances - simply ignore if already showing
615+ guard currentSuggestionsController == nil else {
616+ return
617+ }
618+ guard let siteID = post. blog. dotComID else {
619+ callback ( . failure( GutenbergSuggestionsViewController . SuggestionError. notAvailable as NSError ) )
620+ return
621+ }
622+
623+ switch type {
624+ case . mention:
625+ guard SuggestionService . shared. shouldShowSuggestions ( for: post. blog) else { return }
626+ case . xpost:
627+ guard SiteSuggestionService . shared. shouldShowSuggestions ( for: post. blog) else { return }
628+ }
629+
630+ let previousFirstResponder = view. findFirstResponder ( )
631+ let suggestionsController = GutenbergSuggestionsViewController ( siteID: siteID, suggestionType: type)
632+ currentSuggestionsController = suggestionsController
633+ suggestionsController. onCompletion = { [ weak self] ( result) in
634+ callback ( result)
635+
636+ if let self {
637+ // Clear the current controller reference
638+ self . currentSuggestionsController = nil
639+ self . suggestionViewBottomConstraint = nil
640+
641+ // Clean up the UI (should only happen if parent still exists)
642+ suggestionsController. view. removeFromSuperview ( )
643+ suggestionsController. removeFromParent ( )
644+
645+ previousFirstResponder? . becomeFirstResponder ( )
646+ }
647+
648+ var analyticsName : String
649+ switch type {
650+ case . mention:
651+ analyticsName = " user "
652+ case . xpost:
653+ analyticsName = " xpost "
654+ }
655+
656+ var didSelectSuggestion = false
657+ if case let . success( text) = result, !text. isEmpty {
658+ didSelectSuggestion = true
659+ }
660+
661+ let analyticsProperties : [ String : Any ] = [
662+ " suggestion_type " : analyticsName,
663+ " did_select_suggestion " : didSelectSuggestion
664+ ]
665+
666+ WPAnalytics . track ( . gutenbergSuggestionSessionFinished, properties: analyticsProperties)
667+ }
668+ addChild ( suggestionsController)
669+ view. addSubview ( suggestionsController. view)
670+ let suggestionsBottomConstraint = suggestionsController. view. bottomAnchor. constraint ( equalTo: view. bottomAnchor, constant: 0 )
671+ NSLayoutConstraint . activate ( [
672+ suggestionsController. view. leadingAnchor. constraint ( equalTo: view. safeAreaLayoutGuide. leadingAnchor, constant: 0 ) ,
673+ suggestionsController. view. trailingAnchor. constraint ( equalTo: view. safeAreaLayoutGuide. trailingAnchor, constant: 0 ) ,
674+ suggestionsBottomConstraint,
675+ suggestionsController. view. topAnchor. constraint ( equalTo: view. safeAreaLayoutGuide. topAnchor)
676+ ] )
677+ self . suggestionViewBottomConstraint = suggestionsBottomConstraint
678+ updateConstraintsToAvoidKeyboard ( frame: keyboardFrame)
679+ suggestionsController. didMove ( toParent: self )
680+ }
536681}
537682
538683// MARK: - GutenbergBridgeDataSource
0 commit comments