aix 0.5.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/ios/EditMenuDefaultActions.swift +70 -0
  2. package/ios/HybridAix.swift +28 -3
  3. package/ios/HybridAixCellView.swift +0 -1
  4. package/ios/HybridAixComposer.swift +82 -0
  5. package/ios/HybridAixDropzone.swift +104 -0
  6. package/ios/HybridAixInputWrapper.swift +447 -0
  7. package/ios/InputType.swift +40 -0
  8. package/ios/PasteFileManager.swift +92 -0
  9. package/nitro.json +8 -0
  10. package/nitrogen/generated/android/Aix+autolinking.cmake +8 -0
  11. package/nitrogen/generated/android/AixOnLoad.cpp +26 -0
  12. package/nitrogen/generated/android/c++/JAixInputWrapperOnPasteEvent.hpp +70 -0
  13. package/nitrogen/generated/android/c++/JFunc_void_std__vector_AixInputWrapperOnPasteEvent_.hpp +98 -0
  14. package/nitrogen/generated/android/c++/JHybridAixComposerSpec.cpp +9 -0
  15. package/nitrogen/generated/android/c++/JHybridAixComposerSpec.hpp +2 -0
  16. package/nitrogen/generated/android/c++/JHybridAixDropzoneSpec.cpp +72 -0
  17. package/nitrogen/generated/android/c++/JHybridAixDropzoneSpec.hpp +66 -0
  18. package/nitrogen/generated/android/c++/JHybridAixInputWrapperSpec.cpp +144 -0
  19. package/nitrogen/generated/android/c++/JHybridAixInputWrapperSpec.hpp +74 -0
  20. package/nitrogen/generated/android/c++/views/JHybridAixComposerStateUpdater.cpp +4 -0
  21. package/nitrogen/generated/android/c++/views/JHybridAixDropzoneStateUpdater.cpp +56 -0
  22. package/nitrogen/generated/android/c++/views/JHybridAixDropzoneStateUpdater.hpp +49 -0
  23. package/nitrogen/generated/android/c++/views/JHybridAixInputWrapperStateUpdater.cpp +72 -0
  24. package/nitrogen/generated/android/c++/views/JHybridAixInputWrapperStateUpdater.hpp +49 -0
  25. package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/AixInputWrapperOnPasteEvent.kt +47 -0
  26. package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/Func_void_std__vector_AixInputWrapperOnPasteEvent_.kt +80 -0
  27. package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/HybridAixComposerSpec.kt +6 -0
  28. package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/HybridAixDropzoneSpec.kt +67 -0
  29. package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/HybridAixInputWrapperSpec.kt +91 -0
  30. package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/views/HybridAixDropzoneManager.kt +50 -0
  31. package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/views/HybridAixDropzoneStateUpdater.kt +23 -0
  32. package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/views/HybridAixInputWrapperManager.kt +50 -0
  33. package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/views/HybridAixInputWrapperStateUpdater.kt +23 -0
  34. package/nitrogen/generated/ios/Aix-Swift-Cxx-Bridge.cpp +42 -0
  35. package/nitrogen/generated/ios/Aix-Swift-Cxx-Bridge.hpp +112 -0
  36. package/nitrogen/generated/ios/Aix-Swift-Cxx-Umbrella.hpp +14 -0
  37. package/nitrogen/generated/ios/AixAutolinking.mm +16 -0
  38. package/nitrogen/generated/ios/AixAutolinking.swift +30 -0
  39. package/nitrogen/generated/ios/c++/HybridAixComposerSpecSwift.hpp +7 -0
  40. package/nitrogen/generated/ios/c++/HybridAixDropzoneSpecSwift.cpp +11 -0
  41. package/nitrogen/generated/ios/c++/HybridAixDropzoneSpecSwift.hpp +80 -0
  42. package/nitrogen/generated/ios/c++/HybridAixInputWrapperSpecSwift.cpp +11 -0
  43. package/nitrogen/generated/ios/c++/HybridAixInputWrapperSpecSwift.hpp +108 -0
  44. package/nitrogen/generated/ios/c++/views/HybridAixComposerComponent.mm +5 -0
  45. package/nitrogen/generated/ios/c++/views/HybridAixDropzoneComponent.mm +96 -0
  46. package/nitrogen/generated/ios/c++/views/HybridAixInputWrapperComponent.mm +116 -0
  47. package/nitrogen/generated/ios/swift/AixInputWrapperOnPasteEvent.swift +107 -0
  48. package/nitrogen/generated/ios/swift/Func_void_std__vector_AixInputWrapperOnPasteEvent_.swift +47 -0
  49. package/nitrogen/generated/ios/swift/HybridAixComposerSpec.swift +1 -0
  50. package/nitrogen/generated/ios/swift/HybridAixComposerSpec_cxx.swift +24 -0
  51. package/nitrogen/generated/ios/swift/HybridAixDropzoneSpec.swift +56 -0
  52. package/nitrogen/generated/ios/swift/HybridAixDropzoneSpec_cxx.swift +167 -0
  53. package/nitrogen/generated/ios/swift/HybridAixInputWrapperSpec.swift +60 -0
  54. package/nitrogen/generated/ios/swift/HybridAixInputWrapperSpec_cxx.swift +261 -0
  55. package/nitrogen/generated/shared/c++/AixInputWrapperOnPasteEvent.hpp +88 -0
  56. package/nitrogen/generated/shared/c++/HybridAixComposerSpec.cpp +2 -0
  57. package/nitrogen/generated/shared/c++/HybridAixComposerSpec.hpp +2 -0
  58. package/nitrogen/generated/shared/c++/HybridAixDropzoneSpec.cpp +22 -0
  59. package/nitrogen/generated/shared/c++/HybridAixDropzoneSpec.hpp +67 -0
  60. package/nitrogen/generated/shared/c++/HybridAixInputWrapperSpec.cpp +30 -0
  61. package/nitrogen/generated/shared/c++/HybridAixInputWrapperSpec.hpp +76 -0
  62. package/nitrogen/generated/shared/c++/views/HybridAixComposerComponent.cpp +12 -0
  63. package/nitrogen/generated/shared/c++/views/HybridAixComposerComponent.hpp +1 -0
  64. package/nitrogen/generated/shared/c++/views/HybridAixDropzoneComponent.cpp +87 -0
  65. package/nitrogen/generated/shared/c++/views/HybridAixDropzoneComponent.hpp +109 -0
  66. package/nitrogen/generated/shared/c++/views/HybridAixInputWrapperComponent.cpp +135 -0
  67. package/nitrogen/generated/shared/c++/views/HybridAixInputWrapperComponent.hpp +114 -0
  68. package/nitrogen/generated/shared/json/AixComposerConfig.json +1 -0
  69. package/nitrogen/generated/shared/json/AixDropzoneConfig.json +10 -0
  70. package/nitrogen/generated/shared/json/AixInputWrapperConfig.json +14 -0
  71. package/package.json +1 -1
  72. package/src/dropzone.ios.tsx +27 -0
  73. package/src/dropzone.tsx +10 -0
  74. package/src/index.ts +3 -0
  75. package/src/input-wrapper.ios.tsx +30 -0
  76. package/src/input-wrapper.tsx +17 -0
  77. package/src/views/aix.nitro.ts +33 -19
@@ -0,0 +1,70 @@
1
+ import UIKit
2
+
3
+ enum EditMenuDefaultActions: String, CaseIterable {
4
+ case define
5
+ case translate
6
+ case promptForReplace
7
+ case cut
8
+ case copy
9
+ case paste
10
+ case delete
11
+ case select
12
+ case selectAll
13
+ case transliterateChinese
14
+ case insertDrawing
15
+ case captureTextFromCamera
16
+ case toggleBoldface
17
+ case toggleItalics
18
+ case toggleUnderline
19
+ case makeTextWritingDirectionRightToLeft
20
+ case makeTextWritingDirectionLeftToRight
21
+ case showTextFormattingOptions
22
+ case findSelected
23
+ case addShortcut
24
+ case accessibilitySpeak
25
+ case accessibilitySpeakLanguageSelection
26
+ case accessibilityPauseSpeaking
27
+ case share
28
+
29
+ var selectorString: String {
30
+ switch self {
31
+ case .define: return "_define:"
32
+ case .translate: return "_translate:"
33
+ case .promptForReplace: return "_promptForReplace:"
34
+ case .cut: return "cut:"
35
+ case .copy: return "copy:"
36
+ case .paste: return "paste:"
37
+ case .delete: return "delete:"
38
+ case .select: return "select:"
39
+ case .selectAll: return "selectAll:"
40
+ case .transliterateChinese: return "_transliterateChinese:"
41
+ case .insertDrawing: return "_insertDrawing:"
42
+ case .captureTextFromCamera: return "captureTextFromCamera:"
43
+ case .toggleBoldface: return "toggleBoldface:"
44
+ case .toggleItalics: return "toggleItalics:"
45
+ case .toggleUnderline: return "toggleUnderline:"
46
+ case .makeTextWritingDirectionRightToLeft: return "makeTextWritingDirectionRightToLeft:"
47
+ case .makeTextWritingDirectionLeftToRight: return "makeTextWritingDirectionLeftToRight:"
48
+ case .showTextFormattingOptions: return "_showTextFormattingOptions:"
49
+ case .findSelected: return "_findSelected:"
50
+ case .addShortcut: return "_addShortcut:"
51
+ case .accessibilitySpeak: return "_accessibilitySpeak:"
52
+ case .accessibilitySpeakLanguageSelection: return "_accessibilitySpeakLanguageSelection:"
53
+ case .accessibilityPauseSpeaking: return "_accessibilityPauseSpeaking:"
54
+ case .share: return "_share:"
55
+ }
56
+ }
57
+
58
+ var selector: Selector {
59
+ Selector(self.selectorString)
60
+ }
61
+
62
+ static func getSupportedActions(
63
+ forResponder responder: UIResponder,
64
+ withSender sender: Any?
65
+ ) -> [Self] {
66
+ Self.allCases.filter {
67
+ responder.canPerformAction($0.selector, withSender: sender)
68
+ }
69
+ }
70
+ }
@@ -91,6 +91,19 @@ extension UIView {
91
91
  }
92
92
  return nil
93
93
  }
94
+
95
+ /// Recursively search subviews to find the first UITextField or UITextView
96
+ func findTextInput() -> UIView? {
97
+ if self is UITextField || self is UITextView {
98
+ return self
99
+ }
100
+ for subview in subviews {
101
+ if let input = subview.findTextInput() {
102
+ return input
103
+ }
104
+ }
105
+ return nil
106
+ }
94
107
  }
95
108
 
96
109
  // MARK: - HybridAix (Root Context)
@@ -115,7 +128,15 @@ class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
115
128
 
116
129
  var scrollOnComposerSizeUpdate: AixScrollOnFooterSizeUpdate?
117
130
 
118
- var mainScrollViewID: String?
131
+ var mainScrollViewID: String? {
132
+ didSet {
133
+ guard mainScrollViewID != oldValue else { return }
134
+ removePanGestureObserver()
135
+ removeScrollViewObservers()
136
+ cachedScrollView = nil
137
+ didSetupPanGestureObserver = false
138
+ }
139
+ }
119
140
 
120
141
  func scrollToEnd(animated: Bool?) {
121
142
  // Dispatch to main thread since this may be called from RN background thread
@@ -144,7 +165,11 @@ class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
144
165
  }
145
166
  }
146
167
 
147
- private var didScrollToEndInitially = false
168
+ private var didScrollToEndInitiallyForId: String? = nil
169
+
170
+ private var didScrollToEndInitially: Bool {
171
+ return didScrollToEndInitiallyForId == (mainScrollViewID ?? "")
172
+ }
148
173
 
149
174
  // MARK: - Inner View
150
175
 
@@ -732,7 +757,7 @@ class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
732
757
  scrollToEndInternal(animated: false)
733
758
  prevIsScrolledNearEnd = getIsScrolledNearEnd(distFromEnd: distFromEnd)
734
759
  }
735
- didScrollToEndInitially = true
760
+ didScrollToEndInitiallyForId = mainScrollViewID ?? ""
736
761
  } else {
737
762
  applyContentInset()
738
763
  applyScrollIndicatorInsets()
@@ -22,7 +22,6 @@ class HybridAixCellView: HybridAixCellViewSpec {
22
22
 
23
23
  override init(frame: CGRect) {
24
24
  super.init(frame: frame)
25
- autoresizingMask = [.flexibleWidth]
26
25
  isUserInteractionEnabled = false
27
26
  }
28
27
 
@@ -24,6 +24,15 @@ class HybridAixComposer: HybridAixComposerSpec {
24
24
  }
25
25
  }
26
26
 
27
+ var fixInput: Bool? = nil {
28
+ didSet {
29
+ cachedTextInput = nil
30
+ if fixInput == true {
31
+ resolveTextInput()
32
+ }
33
+ }
34
+ }
35
+
27
36
  // MARK: - Inner View
28
37
 
29
38
  /// Custom UIView that notifies owner when layout changes
@@ -56,6 +65,26 @@ class HybridAixComposer: HybridAixComposerSpec {
56
65
  }
57
66
  }
58
67
 
68
+ // MARK: - Gesture Target
69
+
70
+ /// NSObject helper to bridge target-action for gesture recognizers,
71
+ /// since HybridAixComposer does not inherit from NSObject.
72
+ private class GestureTarget: NSObject, UIGestureRecognizerDelegate {
73
+ let handler: (UIPanGestureRecognizer) -> Void
74
+
75
+ init(handler: @escaping (UIPanGestureRecognizer) -> Void) {
76
+ self.handler = handler
77
+ }
78
+
79
+ @objc func handleGesture(_ gesture: UIPanGestureRecognizer) {
80
+ handler(gesture)
81
+ }
82
+
83
+ func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
84
+ return true
85
+ }
86
+ }
87
+
59
88
  // MARK: - Properties
60
89
 
61
90
  /// The UIView for this composer
@@ -67,6 +96,12 @@ class HybridAixComposer: HybridAixComposerSpec {
67
96
  /// Last reported height (to avoid reporting unchanged heights)
68
97
  private var lastReportedHeight: CGFloat = 0
69
98
 
99
+ /// Cached reference to the text input found inside the footer
100
+ private weak var cachedTextInput: UIView?
101
+
102
+ /// Gesture target for pan-to-focus
103
+ private var panGestureTarget: GestureTarget?
104
+
70
105
  // MARK: - Initialization
71
106
 
72
107
  override init() {
@@ -105,6 +140,9 @@ class HybridAixComposer: HybridAixComposerSpec {
105
140
  // Initial state
106
141
  applyKeyboardTransform(height: ctx.keyboardHeight, heightWhenOpen: ctx.keyboardHeightWhenOpen, animated: false)
107
142
  }
143
+
144
+ // Resolve text input once the hierarchy is connected
145
+ resolveTextInput()
108
146
  }
109
147
 
110
148
  /// Called when the view is about to be removed from superview
@@ -128,6 +166,50 @@ class HybridAixComposer: HybridAixComposerSpec {
128
166
  }
129
167
  }
130
168
 
169
+ // MARK: - Text Input
170
+
171
+ /// Find and cache the text input inside the footer's view hierarchy
172
+ var textInput: UIView? {
173
+ if let cached = cachedTextInput {
174
+ return cached
175
+ }
176
+ let searchRoot = view.superview ?? view
177
+ let input = searchRoot.findTextInput()
178
+ cachedTextInput = input
179
+ return input
180
+ }
181
+
182
+ /// Resolve the text input and apply patches
183
+ private func resolveTextInput() {
184
+ print("[Aix] resolveTextInput: fixInput=\(fixInput), textInput=\(textInput)")
185
+ guard fixInput == true, let input = textInput else { return }
186
+ guard let scrollView = input as? UIScrollView else { return }
187
+ print("[Aix] resolveTextInput: scrollView=\(scrollView)")
188
+ scrollView.showsVerticalScrollIndicator = false
189
+ scrollView.showsHorizontalScrollIndicator = false
190
+ scrollView.bounces = false
191
+ scrollView.alwaysBounceVertical = false
192
+ scrollView.alwaysBounceHorizontal = false
193
+ scrollView.keyboardDismissMode = .interactive
194
+ let target = GestureTarget { [weak self] gesture in
195
+ self?.handlePanToFocus(gesture)
196
+ }
197
+ self.panGestureTarget = target
198
+ let panGesture = UIPanGestureRecognizer(target: target, action: #selector(GestureTarget.handleGesture(_:)))
199
+ panGesture.delegate = target
200
+ scrollView.addGestureRecognizer(panGesture)
201
+ }
202
+
203
+ /// Handle pan to focus the text input
204
+ private func handlePanToFocus(_ gesture: UIPanGestureRecognizer) {
205
+ guard gesture.state == .began else { return }
206
+ guard let input = textInput else { return }
207
+ let velocity = gesture.velocity(in: input)
208
+ if velocity.y < -250.0 && !input.isFirstResponder {
209
+ input.becomeFirstResponder()
210
+ }
211
+ }
212
+
131
213
  // MARK: - Keyboard handling
132
214
 
133
215
  /// Apply keyboard transform to move the composer with the keyboard
@@ -0,0 +1,104 @@
1
+ import Foundation
2
+ import UIKit
3
+
4
+ class HybridAixDropzone: HybridAixDropzoneSpec {
5
+ var onDrop: ((_ events: [AixInputWrapperOnPasteEvent]) -> Void)?
6
+
7
+ private let dropDelegate = DropzoneDelegate()
8
+
9
+ // MARK: - Inner View
10
+ private final class InnerView: UIView {
11
+ weak var owner: HybridAixDropzone?
12
+
13
+ override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
14
+ return false
15
+ }
16
+
17
+ override func didMoveToWindow() {
18
+ super.didMoveToWindow()
19
+ owner?.setupDropInteraction()
20
+ }
21
+ }
22
+
23
+ let view: UIView
24
+
25
+ // MARK: - Init
26
+ override init() {
27
+ let inner = InnerView()
28
+ self.view = inner
29
+ super.init()
30
+ inner.owner = self
31
+ }
32
+
33
+ private var didSetup = false
34
+
35
+ private func setupDropInteraction() {
36
+ guard !didSetup, view.window != nil else { return }
37
+ didSetup = true
38
+
39
+ dropDelegate.onImages = { [weak self] images in
40
+ self?.handleDroppedImages(images)
41
+ }
42
+
43
+ // Add drop interaction to the Fabric container (superview) so it covers the full area
44
+ let target = view.superview ?? view
45
+ target.addInteraction(UIDropInteraction(delegate: dropDelegate))
46
+ }
47
+
48
+ // MARK: - Drop Handling
49
+ private func handleDroppedImages(_ images: [UIImage]) {
50
+ var events: [AixInputWrapperOnPasteEvent] = []
51
+ for image in images {
52
+ if let uri = try? PasteFileManager.save(image: image) {
53
+ let fileURL = URL(fileURLWithPath: uri)
54
+ events.append(AixInputWrapperOnPasteEvent(
55
+ type: "image",
56
+ filePath: uri,
57
+ fileExtension: fileURL.pathExtension,
58
+ fileName: fileURL.lastPathComponent
59
+ ))
60
+ }
61
+ }
62
+ if !events.isEmpty {
63
+ onDrop?(events)
64
+ }
65
+ }
66
+ }
67
+
68
+ // MARK: - Drop Delegate
69
+ private final class DropzoneDelegate: NSObject, UIDropInteractionDelegate {
70
+ var onImages: (([UIImage]) -> Void)?
71
+
72
+ func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool {
73
+ return session.items.contains { $0.itemProvider.canLoadObject(ofClass: UIImage.self) }
74
+ }
75
+
76
+ func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal {
77
+ return UIDropProposal(operation: .copy)
78
+ }
79
+
80
+ func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
81
+ let providers = session.items.map { $0.itemProvider }
82
+ var images: [UIImage] = []
83
+ let lock = NSLock()
84
+ let group = DispatchGroup()
85
+
86
+ for provider in providers {
87
+ guard provider.canLoadObject(ofClass: UIImage.self) else { continue }
88
+ group.enter()
89
+ provider.loadObject(ofClass: UIImage.self) { object, _ in
90
+ defer { group.leave() }
91
+ guard let image = object as? UIImage else { return }
92
+ lock.lock()
93
+ images.append(image)
94
+ lock.unlock()
95
+ }
96
+ }
97
+
98
+ group.notify(queue: .main) { [weak self] in
99
+ if !images.isEmpty {
100
+ self?.onImages?(images)
101
+ }
102
+ }
103
+ }
104
+ }