react-native-keyboard-controller 1.21.0-beta.1 → 1.21.0-beta.2
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.
- package/ios/animations/KeyboardAnimation.swift +2 -2
- package/ios/animations/SpringAnimation.swift +1 -1
- package/ios/animations/TimingAnimation.swift +3 -1
- package/ios/delegates/KCTextInputCompositeDelegate.swift +2 -2
- package/ios/extensions/CGFloat.swift +3 -1
- package/ios/interactive/KeyboardAreaExtender.swift +1 -2
- package/ios/observers/FocusedInputObserver.swift +2 -4
- package/ios/observers/movement/KeyboardTrackingView.swift +1 -4
- package/ios/observers/movement/observer/KeyboardMovementObserver+Interactive.swift +0 -2
- package/ios/observers/movement/observer/KeyboardMovementObserver+Listeners.swift +7 -30
- package/ios/observers/movement/observer/KeyboardMovementObserver.swift +48 -2
- package/ios/protocols/TextInput.swift +1 -1
- package/ios/swizzling/UIResponderSwizzle.swift +4 -2
- package/ios/traversal/FocusedInputHolder.swift +3 -3
- package/ios/traversal/KeyboardView.swift +1 -1
- package/ios/traversal/ViewHierarchyNavigator.swift +2 -2
- package/ios/views/KeyboardControllerViewManager.swift +5 -5
- package/jest/index.js +3 -0
- package/lib/commonjs/components/ChatKit/TODO.md +20 -0
- package/lib/commonjs/components/ChatKit/hooks.js +13 -0
- package/lib/commonjs/components/ChatKit/hooks.js.map +1 -0
- package/lib/commonjs/components/ChatKit/index.js +55 -0
- package/lib/commonjs/components/ChatKit/index.js.map +1 -0
- package/lib/commonjs/components/ChatKit/types.js +6 -0
- package/lib/commonjs/components/ChatKit/types.js.map +1 -0
- package/lib/commonjs/components/ChatKit/useChatKeyboard/helpers.js +101 -0
- package/lib/commonjs/components/ChatKit/useChatKeyboard/helpers.js.map +1 -0
- package/lib/commonjs/components/ChatKit/useChatKeyboard/index.js +147 -0
- package/lib/commonjs/components/ChatKit/useChatKeyboard/index.js.map +1 -0
- package/lib/commonjs/components/KeyboardAwareScrollView/index.js +7 -36
- package/lib/commonjs/components/KeyboardAwareScrollView/index.js.map +1 -1
- package/lib/commonjs/components/KeyboardStickyView/index.js +7 -5
- package/lib/commonjs/components/KeyboardStickyView/index.js.map +1 -1
- package/lib/commonjs/components/ScrollViewWithBottomPadding/index.js +38 -14
- package/lib/commonjs/components/ScrollViewWithBottomPadding/index.js.map +1 -1
- package/lib/commonjs/components/index.js +7 -0
- package/lib/commonjs/components/index.js.map +1 -1
- package/lib/commonjs/index.js +7 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/components/ChatKit/TODO.md +20 -0
- package/lib/module/components/ChatKit/hooks.js +2 -0
- package/lib/module/components/ChatKit/hooks.js.map +1 -0
- package/lib/module/components/ChatKit/index.js +48 -0
- package/lib/module/components/ChatKit/index.js.map +1 -0
- package/lib/module/components/ChatKit/types.js +2 -0
- package/lib/module/components/ChatKit/types.js.map +1 -0
- package/lib/module/components/ChatKit/useChatKeyboard/helpers.js +92 -0
- package/lib/module/components/ChatKit/useChatKeyboard/helpers.js.map +1 -0
- package/lib/module/components/ChatKit/useChatKeyboard/index.js +141 -0
- package/lib/module/components/ChatKit/useChatKeyboard/index.js.map +1 -0
- package/lib/module/components/KeyboardAwareScrollView/index.js +7 -36
- package/lib/module/components/KeyboardAwareScrollView/index.js.map +1 -1
- package/lib/module/components/KeyboardStickyView/index.js +7 -5
- package/lib/module/components/KeyboardStickyView/index.js.map +1 -1
- package/lib/module/components/ScrollViewWithBottomPadding/index.js +38 -14
- package/lib/module/components/ScrollViewWithBottomPadding/index.js.map +1 -1
- package/lib/module/components/index.js +1 -0
- package/lib/module/components/index.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/components/ChatKit/hooks.d.ts +2 -0
- package/lib/typescript/components/ChatKit/index.d.ts +14 -0
- package/lib/typescript/components/ChatKit/types.d.ts +49 -0
- package/lib/typescript/components/ChatKit/useChatKeyboard/helpers.d.ts +57 -0
- package/lib/typescript/components/ChatKit/useChatKeyboard/index.d.ts +36 -0
- package/lib/typescript/components/ScrollViewWithBottomPadding/index.d.ts +6 -1
- package/lib/typescript/components/index.d.ts +2 -0
- package/lib/typescript/index.d.ts +2 -2
- package/package.json +12 -4
- package/src/components/ChatKit/TODO.md +20 -0
- package/src/components/ChatKit/hooks.ts +2 -0
- package/src/components/ChatKit/index.tsx +63 -0
- package/src/components/ChatKit/types.ts +50 -0
- package/src/components/ChatKit/useChatKeyboard/helpers.ts +118 -0
- package/src/components/ChatKit/useChatKeyboard/index.ts +228 -0
- package/src/components/KeyboardAwareScrollView/index.tsx +7 -43
- package/src/components/KeyboardStickyView/index.tsx +4 -5
- package/src/components/ScrollViewWithBottomPadding/index.tsx +66 -17
- package/src/components/index.ts +2 -0
- package/src/index.ts +2 -0
|
@@ -26,7 +26,7 @@ public class KeyboardAnimation: KeyboardAnimationProtocol {
|
|
|
26
26
|
let duration: Double
|
|
27
27
|
let speed: Double
|
|
28
28
|
let timestamp: CFTimeInterval
|
|
29
|
-
|
|
29
|
+
// internal state
|
|
30
30
|
var lastValue: Double
|
|
31
31
|
|
|
32
32
|
init(fromValue: Double, toValue: Double, animation: CAMediaTiming, duration: Double) {
|
|
@@ -39,7 +39,7 @@ public class KeyboardAnimation: KeyboardAnimationProtocol {
|
|
|
39
39
|
timestamp = CACurrentMediaTime()
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
// public getters
|
|
43
43
|
var startTime: CFTimeInterval {
|
|
44
44
|
// when concurrent animation happens, then `.beginTime` remains the same
|
|
45
45
|
return max(animation?.beginTime ?? timestamp, timestamp)
|
|
@@ -60,7 +60,7 @@ public final class SpringAnimation: KeyboardAnimation {
|
|
|
60
60
|
super.init(fromValue: fromValue, toValue: toValue, animation: animation, duration: animation.settlingDuration)
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
// public functions
|
|
64
64
|
override func valueAt(time: Double) -> Double {
|
|
65
65
|
let t = time * speed
|
|
66
66
|
|
|
@@ -51,7 +51,7 @@ class KCTextInputCompositeDelegate: NSObject, UITextViewDelegate, UITextFieldDel
|
|
|
51
51
|
weak var textViewDelegate: UITextViewDelegate?
|
|
52
52
|
weak var textFieldDelegate: UITextFieldDelegate?
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
// Keep track of which textField we’re observing (iOS < 13 only)
|
|
55
55
|
private weak var observedTextFieldForSelection: UITextField?
|
|
56
56
|
|
|
57
57
|
init(
|
|
@@ -111,7 +111,7 @@ class KCTextInputCompositeDelegate: NSObject, UITextViewDelegate, UITextFieldDel
|
|
|
111
111
|
return true
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
// Getter for the active delegate
|
|
115
115
|
var activeDelegate: AnyObject? {
|
|
116
116
|
return textViewDelegate ?? textFieldDelegate
|
|
117
117
|
}
|
|
@@ -16,6 +16,8 @@ public extension CGFloat {
|
|
|
16
16
|
let outputMax = outputRange.max() ?? 1
|
|
17
17
|
|
|
18
18
|
let normalizedValue = (currentValue - inputMin) / (inputMax - inputMin)
|
|
19
|
-
|
|
19
|
+
let interpolatedValue = outputMin + (outputMax - outputMin) * normalizedValue
|
|
20
|
+
|
|
21
|
+
return interpolatedValue
|
|
20
22
|
}
|
|
21
23
|
}
|
|
@@ -55,8 +55,7 @@ class KeyboardAreaExtender: NSObject {
|
|
|
55
55
|
let responder = UIResponder.current
|
|
56
56
|
if let activeTextInput = responder as? TextInput,
|
|
57
57
|
let offset = KeyboardOffsetProvider.shared.getOffset(
|
|
58
|
-
forTextInputNativeID: responder.nativeID
|
|
59
|
-
),
|
|
58
|
+
forTextInputNativeID: responder.nativeID),
|
|
60
59
|
responder?.inputAccessoryView == nil
|
|
61
60
|
{
|
|
62
61
|
currentInputAccessoryView = InvisibleInputAccessoryView(height: CGFloat(truncating: offset))
|
|
@@ -32,7 +32,7 @@ public class FocusedInputObserver: NSObject {
|
|
|
32
32
|
var onFocusDidSet: (NSDictionary) -> Void
|
|
33
33
|
/// delegates
|
|
34
34
|
var delegate: KCTextInputCompositeDelegate
|
|
35
|
-
|
|
35
|
+
// state variables
|
|
36
36
|
private var isMounted = false
|
|
37
37
|
// input tracking
|
|
38
38
|
private var currentInput: UIView?
|
|
@@ -142,9 +142,7 @@ public class FocusedInputObserver: NSObject {
|
|
|
142
142
|
setupObservers()
|
|
143
143
|
// dispatch onSelectionChange on focus
|
|
144
144
|
if let textInput = responder as? UITextInput {
|
|
145
|
-
|
|
146
|
-
updateSelectionPosition(textInput: textInput, sendEvent: self.onSelectionChange)
|
|
147
|
-
}
|
|
145
|
+
updateSelectionPosition(textInput: textInput, sendEvent: onSelectionChange)
|
|
148
146
|
}
|
|
149
147
|
|
|
150
148
|
syncUpLayout()
|
|
@@ -12,10 +12,7 @@ import UIKit
|
|
|
12
12
|
* and uses `keyboardLayoutGuide` on iOS 26+.
|
|
13
13
|
*/
|
|
14
14
|
public final class KeyboardTrackingView: UIView {
|
|
15
|
-
private var keyboardView: UIView? {
|
|
16
|
-
KeyboardViewLocator.shared.resolve()
|
|
17
|
-
}
|
|
18
|
-
|
|
15
|
+
private var keyboardView: UIView? { KeyboardViewLocator.shared.resolve() }
|
|
19
16
|
private var keyboardHeight = 0.0
|
|
20
17
|
private weak var currentAttachedView: UIView?
|
|
21
18
|
private var isAttaching = false
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
extension KeyboardMovementObserver {
|
|
9
9
|
@objc func keyboardWillAppear(_ notification: Notification) {
|
|
10
|
-
guard !UIResponder.isKeyboardPreloading else { return }
|
|
10
|
+
guard !KeyboardEventsIgnorer.shared.shouldIgnore, !UIResponder.isKeyboardPreloading else { return }
|
|
11
11
|
|
|
12
12
|
let (duration, frame) = notification.keyboardMetaData()
|
|
13
13
|
if let keyboardFrame = frame {
|
|
@@ -15,12 +15,6 @@ extension KeyboardMovementObserver {
|
|
|
15
15
|
let keyboardHeight = keyboardFrame.cgRectValue.size.height
|
|
16
16
|
self.keyboardHeight = keyboardHeight
|
|
17
17
|
self.notification = notification
|
|
18
|
-
|
|
19
|
-
guard !KeyboardEventsIgnorer.shared.shouldIgnore else {
|
|
20
|
-
KeyboardEventsIgnorer.shared.shouldIgnoreKeyboardEvents = false
|
|
21
|
-
return
|
|
22
|
-
}
|
|
23
|
-
|
|
24
18
|
self.duration = duration
|
|
25
19
|
|
|
26
20
|
onRequestAnimation()
|
|
@@ -29,7 +23,6 @@ extension KeyboardMovementObserver {
|
|
|
29
23
|
|
|
30
24
|
setupKeyboardWatcher()
|
|
31
25
|
initializeAnimation(fromValue: prevKeyboardPosition, toValue: self.keyboardHeight)
|
|
32
|
-
scheduleDidEvent(height: self.keyboardHeight, duration: animation?.duration ?? CGFloat(duration) / 1000)
|
|
33
26
|
}
|
|
34
27
|
}
|
|
35
28
|
|
|
@@ -51,7 +44,6 @@ extension KeyboardMovementObserver {
|
|
|
51
44
|
setupKeyboardWatcher()
|
|
52
45
|
removeKVObserver()
|
|
53
46
|
initializeAnimation(fromValue: prevKeyboardPosition, toValue: 0)
|
|
54
|
-
scheduleDidEvent(height: 0, duration: animation?.duration ?? CGFloat(duration) / 1000)
|
|
55
47
|
}
|
|
56
48
|
|
|
57
49
|
@objc func keyboardDidAppear(_ notification: Notification) {
|
|
@@ -63,7 +55,12 @@ extension KeyboardMovementObserver {
|
|
|
63
55
|
tag = UIResponder.current.reactViewTag
|
|
64
56
|
self.keyboardHeight = keyboardHeight
|
|
65
57
|
|
|
66
|
-
|
|
58
|
+
guard !KeyboardEventsIgnorer.shared.shouldIgnore else {
|
|
59
|
+
KeyboardEventsIgnorer.shared.shouldIgnoreKeyboardEvents = false
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let height = self.keyboardHeight - KeyboardAreaExtender.shared.offset
|
|
67
64
|
// always limit progress to the maximum possible value
|
|
68
65
|
let progress = min(height / self.keyboardHeight, 1.0)
|
|
69
66
|
|
|
@@ -93,24 +90,4 @@ extension KeyboardMovementObserver {
|
|
|
93
90
|
removeKeyboardWatcher()
|
|
94
91
|
animation = nil
|
|
95
92
|
}
|
|
96
|
-
|
|
97
|
-
@objc func scheduleDidEvent(height: CGFloat, duration: CGFloat) {
|
|
98
|
-
keyboardDidTask?.cancel()
|
|
99
|
-
|
|
100
|
-
guard let notification = notification else {
|
|
101
|
-
return
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
let task = DispatchWorkItem { [weak self] in
|
|
105
|
-
guard let self = self else { return }
|
|
106
|
-
if height > 0 {
|
|
107
|
-
self.keyboardDidAppear(notification)
|
|
108
|
-
} else {
|
|
109
|
-
self.keyboardDidDisappear(notification)
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
keyboardDidTask = task
|
|
114
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + duration, execute: task)
|
|
115
|
-
}
|
|
116
93
|
}
|
|
@@ -19,7 +19,30 @@ public class KeyboardMovementObserver: NSObject {
|
|
|
19
19
|
var onCancelAnimation: () -> Void
|
|
20
20
|
// progress tracker
|
|
21
21
|
@objc public var keyboardTrackingView = KeyboardTrackingView()
|
|
22
|
-
var animation: KeyboardAnimation?
|
|
22
|
+
var animation: KeyboardAnimation? {
|
|
23
|
+
didSet {
|
|
24
|
+
keyboardDidTask?.cancel()
|
|
25
|
+
|
|
26
|
+
guard let animation = animation, let notification = notification else {
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let toValue = animation.toValue
|
|
31
|
+
let duration = animation.duration
|
|
32
|
+
|
|
33
|
+
let task = DispatchWorkItem { [weak self] in
|
|
34
|
+
guard let self = self else { return }
|
|
35
|
+
if toValue > 0 {
|
|
36
|
+
self.keyboardDidAppear(notification)
|
|
37
|
+
} else {
|
|
38
|
+
self.keyboardDidDisappear(notification)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
keyboardDidTask = task
|
|
43
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + duration, execute: task)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
23
46
|
|
|
24
47
|
var prevKeyboardPosition = 0.0
|
|
25
48
|
var displayLink: CADisplayLink!
|
|
@@ -32,7 +55,30 @@ public class KeyboardMovementObserver: NSObject {
|
|
|
32
55
|
set { _keyboardHeight = newValue }
|
|
33
56
|
}
|
|
34
57
|
|
|
35
|
-
var duration = 0
|
|
58
|
+
var duration = 0 {
|
|
59
|
+
didSet {
|
|
60
|
+
keyboardDidTask?.cancel()
|
|
61
|
+
|
|
62
|
+
guard let notification = notification,
|
|
63
|
+
let height = notification.keyboardMetaData().1?.cgRectValue.size.height, duration == 0
|
|
64
|
+
else {
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let task = DispatchWorkItem { [weak self] in
|
|
69
|
+
guard let self = self else { return }
|
|
70
|
+
if height > 0 {
|
|
71
|
+
self.keyboardDidAppear(notification)
|
|
72
|
+
} else {
|
|
73
|
+
self.keyboardDidDisappear(notification)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
keyboardDidTask = task
|
|
78
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + UIUtils.nextFrame, execute: task)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
36
82
|
var tag: NSNumber = -1
|
|
37
83
|
// manual did events
|
|
38
84
|
var notification: Notification?
|
|
@@ -15,7 +15,7 @@ public protocol TextInput: UIView {
|
|
|
15
15
|
var inputView: UIView? { get set }
|
|
16
16
|
var keyboardType: UIKeyboardType { get }
|
|
17
17
|
var keyboardAppearance: UIKeyboardAppearance { get }
|
|
18
|
-
|
|
18
|
+
// custom methods/properties
|
|
19
19
|
func focus()
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -94,7 +94,8 @@ extension UIResponder {
|
|
|
94
94
|
let castOriginalResignFirstResponder = unsafeBitCast(
|
|
95
95
|
originalResignFirstResponder, to: Function.self
|
|
96
96
|
)
|
|
97
|
-
|
|
97
|
+
let result = castOriginalResignFirstResponder(self, selector)
|
|
98
|
+
return result
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
private func callOriginalBecomeFirstResponder(_ selector: Selector) -> Bool {
|
|
@@ -103,6 +104,7 @@ extension UIResponder {
|
|
|
103
104
|
let castOriginalBecomeFirstResponder = unsafeBitCast(
|
|
104
105
|
originalBecomeFirstResponder, to: Function.self
|
|
105
106
|
)
|
|
106
|
-
|
|
107
|
+
let result = castOriginalBecomeFirstResponder(self, selector)
|
|
108
|
+
return result
|
|
107
109
|
}
|
|
108
110
|
}
|
|
@@ -11,10 +11,10 @@ import Foundation
|
|
|
11
11
|
class FocusedInputHolder {
|
|
12
12
|
static let shared = FocusedInputHolder()
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
// Using TextInput protocol as the type for currentFocusedInput
|
|
15
15
|
private weak var currentFocusedInput: TextInput?
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
// Sets the currentFocusedInput to the passed instance conforming to TextInput or clears it if nil is passed
|
|
18
18
|
func set(_ input: TextInput?) {
|
|
19
19
|
currentFocusedInput = input
|
|
20
20
|
}
|
|
@@ -23,7 +23,7 @@ class FocusedInputHolder {
|
|
|
23
23
|
return currentFocusedInput
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
// Requests focus for the currentFocusedInput if it's set
|
|
27
27
|
func focus() {
|
|
28
28
|
currentFocusedInput?.focus()
|
|
29
29
|
}
|
|
@@ -13,7 +13,7 @@ enum KeyboardView {
|
|
|
13
13
|
private static let windowPrefix = "<UITextEffectsWindow"
|
|
14
14
|
private static let containerPrefixes = ["<UIInputSetContainerView", "<UITrackingWindowView"]
|
|
15
15
|
private static let hostPrefixes = ["<UIInputSetHostView", "<UIKeyboardItemContainerView"]
|
|
16
|
-
|
|
16
|
+
// inspired by https://stackoverflow.com/questions/32598490/show-uiview-with-buttons-over-keyboard-like-in-skype-viber-messengers-swift-i
|
|
17
17
|
static func find() -> UIView? {
|
|
18
18
|
let windows = UIApplication.shared.windows
|
|
19
19
|
for window in windows {
|
|
@@ -36,7 +36,7 @@ public class ViewHierarchyNavigator: NSObject {
|
|
|
36
36
|
return []
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
// Helper function to recursively search for TextInput views
|
|
40
40
|
func findTextInputs(in view: UIView?) {
|
|
41
41
|
guard let view = view else { return }
|
|
42
42
|
|
|
@@ -104,7 +104,7 @@ public class ViewHierarchyNavigator: NSObject {
|
|
|
104
104
|
return nil // No valid UITextField or UITextView found
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
// Function to check if the view is a valid text field or text view
|
|
108
108
|
private static func isValidTextInput(_ view: UIView) -> TextInput? {
|
|
109
109
|
if let textField = view as? UITextField, textField.isEnabled {
|
|
110
110
|
return textField
|
|
@@ -27,7 +27,7 @@ class KeyboardControllerView: UIView {
|
|
|
27
27
|
var inputObserver: FocusedInputObserver?
|
|
28
28
|
private var eventDispatcher: RCTEventDispatcherProtocol
|
|
29
29
|
private var bridge: RCTBridge
|
|
30
|
-
|
|
30
|
+
// internal state
|
|
31
31
|
private var lastScreenSize: CGSize = .zero
|
|
32
32
|
// react callbacks
|
|
33
33
|
/// keyboard
|
|
@@ -39,7 +39,7 @@ class KeyboardControllerView: UIView {
|
|
|
39
39
|
@objc var onFocusedInputLayoutChanged: RCTDirectEventBlock?
|
|
40
40
|
@objc var onFocusedInputTextChanged: RCTDirectEventBlock?
|
|
41
41
|
@objc var onFocusedInputSelectionChanged: RCTDirectEventBlock?
|
|
42
|
-
|
|
42
|
+
// react props
|
|
43
43
|
@objc var enabled: ObjCBool = true {
|
|
44
44
|
didSet {
|
|
45
45
|
if enabled.boolValue {
|
|
@@ -83,9 +83,9 @@ class KeyboardControllerView: UIView {
|
|
|
83
83
|
fatalError("init(coder:) has not been implemented")
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
// for mounting/unmounting observers for lifecycle events we're using willMove(toSuperview) method
|
|
87
|
+
// not willMove(toWindow)
|
|
88
|
+
// see https://github.com/kirillzyusko/react-native-keyboard-controller/issues/271
|
|
89
89
|
override func willMove(toSuperview newSuperview: UIView?) {
|
|
90
90
|
super.willMove(toSuperview: newSuperview)
|
|
91
91
|
|
package/jest/index.js
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Findings
|
|
2
|
+
|
|
3
|
+
1. We can not translate whole `ScrollView` because interactive dismissal will have double scroll if we move it by `translateY` or content will be truncated if we don't do it (same with external container).
|
|
4
|
+
2. So we need to move only content inside `ScrollView`.
|
|
5
|
+
3. If we use a separate `scrollTo` we still may have a random jump. The only one way is to use `contentOffset` + `padding` on `ScrollView`.
|
|
6
|
+
4. Input must be always in `KeyboardStickyView` because it's always pushing above the keyboard (unlike content, which may stay in place).
|
|
7
|
+
5. Changing `contentOffset` on `ScrollView` will not work. On iOS it works, but on Android sometimes it doesn't scroll to correct position (when you scroll list to the end) and on paper architecture it is flickering.
|
|
8
|
+
6. Next idea is to use `contentInset` on iOS and create custom `ClippingScrollView` on Android (with polyfill `contentInsetBottom` prop).
|
|
9
|
+
|
|
10
|
+
## To Do
|
|
11
|
+
|
|
12
|
+
- [x] check how `ClippingScrollView` works in `KeyboardAwareScrollView` on Android (seems to be working well)
|
|
13
|
+
- [x] introduce `useCombinedRef` hook in separate PR
|
|
14
|
+
- [x] make sure, that style property doesn't affect the behavior with/without `ClippingScrollView` (test on a ScrollView that doesn't tke full screen?) <-- tested by comparing iOS with Android + `ClippingScrollView` (`contentInsetBottom` never override `paddingBottom` from `style`/`contentContainerStyle` even if ScrollView has `maxHeight=150` and we have only 2 inputs limitation + also tested `ScrollView` without height restrictions, in this case `paddingBottom` on `style` is not getting applied (it doesn't have an effect on both iOS/Android), but `contentContainerStyle` + `paddingBottom` works as expected and if I add `contentInsetBottom` to `ClippingScrollView` it doesn't overwrite a padding and increases scrollable area)
|
|
15
|
+
- [x] `ClippingScrollView` + `KeyboardAwareScrollView` check by e2e tests (seems to be working, though on Android emulator + e2e tests sometimes when keyboard disappear we still have keyboard space)
|
|
16
|
+
- [x] introduce `ClippingScrollView` in separate PR
|
|
17
|
+
- [x] create a polyfill-version of ScrollView `that` can add scrollable padding in the bottom of the content? How to wrap custom `ScrollView` with it (cause we may pass one from react-native-gesture-handler)? Should just adjust `contentInset` (scroll management should belong to `KeyboardAwareScrollView`) (ScrollViewWithKeyboardPadding/ScrollViewWithKeyboardSpace)
|
|
18
|
+
- [x] use `ClippingScrollView` in `KeyboardAwareScrollView` in separate PR
|
|
19
|
+
- [-] introduce `ChatKit`
|
|
20
|
+
- [] create issue in reanimated repo about `scrollTo` performance regression
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
Object.defineProperty(exports, "useChatKeyboard", {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: function () {
|
|
9
|
+
return _useChatKeyboard.useChatKeyboard;
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
var _useChatKeyboard = require("./useChatKeyboard");
|
|
13
|
+
//# sourceMappingURL=hooks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["_useChatKeyboard","require"],"sources":["hooks.ts"],"sourcesContent":["export { useChatKeyboard } from \"./useChatKeyboard\";\nexport type { KeyboardLiftBehavior } from \"./useChatKeyboard\";\n"],"mappings":";;;;;;;;;;;AAAA,IAAAA,gBAAA,GAAAC,OAAA","ignoreList":[]}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
var _react = _interopRequireWildcard(require("react"));
|
|
8
|
+
var _reactNativeReanimated = _interopRequireWildcard(require("react-native-reanimated"));
|
|
9
|
+
var _useCombinedRef = _interopRequireDefault(require("../hooks/useCombinedRef"));
|
|
10
|
+
var _ScrollViewWithBottomPadding = _interopRequireDefault(require("../ScrollViewWithBottomPadding"));
|
|
11
|
+
var _useChatKeyboard = require("./useChatKeyboard");
|
|
12
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
13
|
+
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
14
|
+
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
|
|
15
|
+
const ChatScrollView = /*#__PURE__*/(0, _react.forwardRef)(({
|
|
16
|
+
children,
|
|
17
|
+
ScrollViewComponent = _reactNativeReanimated.default.ScrollView,
|
|
18
|
+
inverted = false,
|
|
19
|
+
keyboardLiftBehavior = "always",
|
|
20
|
+
freeze = false,
|
|
21
|
+
offset = 0,
|
|
22
|
+
...rest
|
|
23
|
+
}, ref) => {
|
|
24
|
+
const scrollViewRef = (0, _reactNativeReanimated.useAnimatedRef)();
|
|
25
|
+
const onRef = (0, _useCombinedRef.default)(ref, scrollViewRef);
|
|
26
|
+
const {
|
|
27
|
+
padding,
|
|
28
|
+
contentOffsetY,
|
|
29
|
+
containerTranslateY
|
|
30
|
+
} = (0, _useChatKeyboard.useChatKeyboard)(scrollViewRef, {
|
|
31
|
+
inverted,
|
|
32
|
+
keyboardLiftBehavior,
|
|
33
|
+
freeze,
|
|
34
|
+
offset
|
|
35
|
+
});
|
|
36
|
+
const containerStyle = (0, _reactNativeReanimated.useAnimatedStyle)(() => ({
|
|
37
|
+
transform: [{
|
|
38
|
+
translateY: containerTranslateY.value
|
|
39
|
+
}]
|
|
40
|
+
}), []);
|
|
41
|
+
return /*#__PURE__*/_react.default.createElement(_ScrollViewWithBottomPadding.default, _extends({
|
|
42
|
+
ref: onRef
|
|
43
|
+
}, rest, {
|
|
44
|
+
bottomPadding: padding,
|
|
45
|
+
containerStyle: containerStyle,
|
|
46
|
+
contentOffsetY: contentOffsetY,
|
|
47
|
+
inverted: inverted,
|
|
48
|
+
ScrollViewComponent: ScrollViewComponent
|
|
49
|
+
}), children);
|
|
50
|
+
});
|
|
51
|
+
const ChatKit = {
|
|
52
|
+
ScrollView: ChatScrollView
|
|
53
|
+
};
|
|
54
|
+
var _default = exports.default = ChatKit;
|
|
55
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["_react","_interopRequireWildcard","require","_reactNativeReanimated","_useCombinedRef","_interopRequireDefault","_ScrollViewWithBottomPadding","_useChatKeyboard","e","__esModule","default","t","WeakMap","r","n","o","i","f","__proto__","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","_extends","assign","bind","arguments","length","apply","ChatScrollView","forwardRef","children","ScrollViewComponent","Reanimated","ScrollView","inverted","keyboardLiftBehavior","freeze","offset","rest","ref","scrollViewRef","useAnimatedRef","onRef","useCombinedRef","padding","contentOffsetY","containerTranslateY","useChatKeyboard","containerStyle","useAnimatedStyle","transform","translateY","value","createElement","bottomPadding","ChatKit","_default","exports"],"sources":["index.tsx"],"sourcesContent":["import React, { forwardRef } from \"react\";\nimport { useAnimatedRef, useAnimatedStyle } from \"react-native-reanimated\";\nimport Reanimated from \"react-native-reanimated\";\n\nimport useCombinedRef from \"../hooks/useCombinedRef\";\nimport ScrollViewWithBottomPadding from \"../ScrollViewWithBottomPadding\";\n\nimport { useChatKeyboard } from \"./useChatKeyboard\";\n\nimport type { ChatKitScrollViewProps } from \"./types\";\n\nconst ChatScrollView = forwardRef<\n Reanimated.ScrollView,\n React.PropsWithChildren<ChatKitScrollViewProps>\n>(\n (\n {\n children,\n ScrollViewComponent = Reanimated.ScrollView,\n inverted = false,\n keyboardLiftBehavior = \"always\",\n freeze = false,\n offset = 0,\n ...rest\n },\n ref,\n ) => {\n const scrollViewRef = useAnimatedRef<Reanimated.ScrollView>();\n const onRef = useCombinedRef(ref, scrollViewRef);\n\n const { padding, contentOffsetY, containerTranslateY } = useChatKeyboard(\n scrollViewRef,\n { inverted, keyboardLiftBehavior, freeze, offset },\n );\n\n const containerStyle = useAnimatedStyle(\n () => ({\n transform: [{ translateY: containerTranslateY.value }],\n }),\n [],\n );\n\n return (\n <ScrollViewWithBottomPadding\n ref={onRef}\n {...rest}\n bottomPadding={padding}\n containerStyle={containerStyle}\n contentOffsetY={contentOffsetY}\n inverted={inverted}\n ScrollViewComponent={ScrollViewComponent}\n >\n {children}\n </ScrollViewWithBottomPadding>\n );\n },\n);\n\nconst ChatKit = {\n ScrollView: ChatScrollView,\n};\n\nexport default ChatKit;\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sources":["types.ts"],"sourcesContent":["import type { AnimatedScrollViewComponent } from \"../ScrollViewWithBottomPadding\";\nimport type { ScrollViewProps } from \"react-native\";\n\ntype KeyboardLiftBehavior = \"always\" | \"whenAtEnd\" | \"persistent\" | \"never\";\n\nexport type ChatKitScrollViewProps = {\n /** Custom component for `ScrollView`. Default is `ScrollView`. */\n ScrollViewComponent: AnimatedScrollViewComponent;\n /** Whether list are using `inverted` prop. Default is `false`. */\n inverted?: boolean;\n /**\n * The distance (in pixels) between the bottom of the screen and the `ScrollView`.\n * When the keyboard appears, the scroll view will only push content by the effective\n * distance (`keyboardHeight - offset`) instead of the full keyboard height.\n *\n * Useful when the input is not at the very bottom of the screen (e.g., above safe area, above\n * bottom tabs, etc. - in this case offset should be equal to the height of the elements between\n * `ScrollView` and bottom of the screen).\n *\n * Default is `0`.\n */\n offset?: number;\n /**\n * Determines how the chat content should behave when the keyboard appears, specifically whether\n * the scroll view should automatically lift its content to keep it visible above the keyboard.\n *\n * Possible values:\n * - `'always'`: The content always lifts along with the keyboard, ensuring the messages from the bottom part of screen\n * remain visible regardless of the current scroll position. This is the default behavior for most chat applications (used in Telegram).\n * - `'whenAtEnd'`: The content lifts only if the scroll view is at the end (i.e., the last message\n * is visible or near the bottom). This prevents unnecessary adjustments when the user is scrolling\n * through older messages (ChatGPT mobile app behavior).\n * - `'persistent'`: The content always lifts when the keyboard appears (similar to `'always'`), but\n * does not reset (lower) when the keyboard hides. This mimics behaviors where the view remains adjusted\n * after keyboard dismissal to maintain focus on the latest content without shifting back (Claude mobile app behavior).\n * - `'never'`: The content does not lift at all when the keyboard appears. Use this for scenarios\n * when you don't want to disturb user attention with animations (Perplexity mobile app behavior).\n *\n * Default is `'always'`.\n */\n keyboardLiftBehavior?: KeyboardLiftBehavior;\n /**\n * When `true`, freezes all keyboard-driven layout changes (padding, content offset, scroll position).\n * Useful when dismissing the keyboard to open a bottom sheet — prevents visual disruption\n * while the sheet is visible.\n *\n * Default is `false`.\n */\n freeze?: boolean;\n} & ScrollViewProps;\n"],"mappings":"","ignoreList":[]}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.clampedScrollTarget = clampedScrollTarget;
|
|
7
|
+
exports.computeIOSContentOffset = computeIOSContentOffset;
|
|
8
|
+
exports.isScrollAtEnd = isScrollAtEnd;
|
|
9
|
+
exports.shouldShiftContent = shouldShiftContent;
|
|
10
|
+
const AT_END_THRESHOLD = 20;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Check whether the scroll view is at the end of its content.
|
|
14
|
+
*
|
|
15
|
+
* @param scrollOffset - Current vertical scroll offset.
|
|
16
|
+
* @param layoutHeight - Visible height of the scroll view.
|
|
17
|
+
* @param contentHeight - Total height of the scrollable content.
|
|
18
|
+
* @returns `true` if the scroll position is within the threshold of the content end.
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* const atEnd = isScrollAtEnd(100, 800, 920); // true (100 + 800 >= 920 - 20)
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
function isScrollAtEnd(scrollOffset, layoutHeight, contentHeight) {
|
|
25
|
+
"worklet";
|
|
26
|
+
|
|
27
|
+
return scrollOffset + layoutHeight >= contentHeight - AT_END_THRESHOLD;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Decide whether content should be shifted based on the keyboard lift behavior.
|
|
32
|
+
*
|
|
33
|
+
* @param behavior - The configured keyboard lift behavior.
|
|
34
|
+
* @param isAtEnd - Whether the scroll view is currently at the end.
|
|
35
|
+
* @returns `true` if content should be shifted.
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* shouldShiftContent("always", false); // true
|
|
39
|
+
* shouldShiftContent("whenAtEnd", false); // false
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
function shouldShiftContent(behavior, isAtEnd) {
|
|
43
|
+
"worklet";
|
|
44
|
+
|
|
45
|
+
switch (behavior) {
|
|
46
|
+
case "always":
|
|
47
|
+
return true;
|
|
48
|
+
case "never":
|
|
49
|
+
return false;
|
|
50
|
+
case "whenAtEnd":
|
|
51
|
+
return isAtEnd;
|
|
52
|
+
case "persistent":
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Compute the clamped scroll target for non-inverted lists.
|
|
59
|
+
*
|
|
60
|
+
* @param offsetBeforeScroll - Scroll position before keyboard appeared.
|
|
61
|
+
* @param keyboardHeight - Current keyboard height.
|
|
62
|
+
* @param contentHeight - Total height of the scrollable content.
|
|
63
|
+
* @param layoutHeight - Visible height of the scroll view.
|
|
64
|
+
* @returns Clamped scroll target between 0 and maxScroll.
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* clampedScrollTarget(100, 300, 1000, 800); // 400
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
function clampedScrollTarget(offsetBeforeScroll, keyboardHeight, contentHeight, layoutHeight) {
|
|
71
|
+
"worklet";
|
|
72
|
+
|
|
73
|
+
const maxScroll = Math.max(contentHeight - layoutHeight + keyboardHeight, 0);
|
|
74
|
+
return Math.min(Math.max(offsetBeforeScroll + keyboardHeight, 0), maxScroll);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Compute contentOffset.y for iOS lists.
|
|
79
|
+
*
|
|
80
|
+
* @param relativeScroll - Scroll position relative to current inset.
|
|
81
|
+
* @param keyboardHeight - Target keyboard height.
|
|
82
|
+
* @param contentHeight - Total height of the scrollable content.
|
|
83
|
+
* @param layoutHeight - Visible height of the scroll view.
|
|
84
|
+
* @param inverted - Whether the list is inverted.
|
|
85
|
+
* @returns The absolute contentOffset.y to set.
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* computeIOSContentOffset(100, 300, 1000, 800, false); // 400
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
function computeIOSContentOffset(relativeScroll, keyboardHeight, contentHeight, layoutHeight, inverted) {
|
|
92
|
+
"worklet";
|
|
93
|
+
|
|
94
|
+
if (inverted) {
|
|
95
|
+
const maxScroll = Math.max(contentHeight - layoutHeight, 0);
|
|
96
|
+
return Math.max(Math.min(relativeScroll - keyboardHeight, maxScroll), -keyboardHeight);
|
|
97
|
+
}
|
|
98
|
+
const maxScroll = Math.max(contentHeight - layoutHeight + keyboardHeight, 0);
|
|
99
|
+
return Math.min(Math.max(keyboardHeight + relativeScroll, 0), maxScroll);
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=helpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["AT_END_THRESHOLD","isScrollAtEnd","scrollOffset","layoutHeight","contentHeight","shouldShiftContent","behavior","isAtEnd","clampedScrollTarget","offsetBeforeScroll","keyboardHeight","maxScroll","Math","max","min","computeIOSContentOffset","relativeScroll","inverted"],"sources":["helpers.ts"],"sourcesContent":["type KeyboardLiftBehavior = \"always\" | \"whenAtEnd\" | \"persistent\" | \"never\";\n\nconst AT_END_THRESHOLD = 20;\n\n/**\n * Check whether the scroll view is at the end of its content.\n *\n * @param scrollOffset - Current vertical scroll offset.\n * @param layoutHeight - Visible height of the scroll view.\n * @param contentHeight - Total height of the scrollable content.\n * @returns `true` if the scroll position is within the threshold of the content end.\n * @example\n * ```ts\n * const atEnd = isScrollAtEnd(100, 800, 920); // true (100 + 800 >= 920 - 20)\n * ```\n */\nexport function isScrollAtEnd(\n scrollOffset: number,\n layoutHeight: number,\n contentHeight: number,\n): boolean {\n \"worklet\";\n\n return scrollOffset + layoutHeight >= contentHeight - AT_END_THRESHOLD;\n}\n\n/**\n * Decide whether content should be shifted based on the keyboard lift behavior.\n *\n * @param behavior - The configured keyboard lift behavior.\n * @param isAtEnd - Whether the scroll view is currently at the end.\n * @returns `true` if content should be shifted.\n * @example\n * ```ts\n * shouldShiftContent(\"always\", false); // true\n * shouldShiftContent(\"whenAtEnd\", false); // false\n * ```\n */\nexport function shouldShiftContent(\n behavior: KeyboardLiftBehavior,\n isAtEnd: boolean,\n): boolean {\n \"worklet\";\n\n switch (behavior) {\n case \"always\":\n return true;\n case \"never\":\n return false;\n case \"whenAtEnd\":\n return isAtEnd;\n case \"persistent\":\n return true;\n }\n}\n\n/**\n * Compute the clamped scroll target for non-inverted lists.\n *\n * @param offsetBeforeScroll - Scroll position before keyboard appeared.\n * @param keyboardHeight - Current keyboard height.\n * @param contentHeight - Total height of the scrollable content.\n * @param layoutHeight - Visible height of the scroll view.\n * @returns Clamped scroll target between 0 and maxScroll.\n * @example\n * ```ts\n * clampedScrollTarget(100, 300, 1000, 800); // 400\n * ```\n */\nexport function clampedScrollTarget(\n offsetBeforeScroll: number,\n keyboardHeight: number,\n contentHeight: number,\n layoutHeight: number,\n): number {\n \"worklet\";\n\n const maxScroll = Math.max(contentHeight - layoutHeight + keyboardHeight, 0);\n\n return Math.min(Math.max(offsetBeforeScroll + keyboardHeight, 0), maxScroll);\n}\n\n/**\n * Compute contentOffset.y for iOS lists.\n *\n * @param relativeScroll - Scroll position relative to current inset.\n * @param keyboardHeight - Target keyboard height.\n * @param contentHeight - Total height of the scrollable content.\n * @param layoutHeight - Visible height of the scroll view.\n * @param inverted - Whether the list is inverted.\n * @returns The absolute contentOffset.y to set.\n * @example\n * ```ts\n * computeIOSContentOffset(100, 300, 1000, 800, false); // 400\n * ```\n */\nexport function computeIOSContentOffset(\n relativeScroll: number,\n keyboardHeight: number,\n contentHeight: number,\n layoutHeight: number,\n inverted: boolean,\n): number {\n \"worklet\";\n\n if (inverted) {\n const maxScroll = Math.max(contentHeight - layoutHeight, 0);\n\n return Math.max(\n Math.min(relativeScroll - keyboardHeight, maxScroll),\n -keyboardHeight,\n );\n }\n\n const maxScroll = Math.max(contentHeight - layoutHeight + keyboardHeight, 0);\n\n return Math.min(Math.max(keyboardHeight + relativeScroll, 0), maxScroll);\n}\n"],"mappings":";;;;;;;;;AAEA,MAAMA,gBAAgB,GAAG,EAAE;;AAE3B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASC,aAAaA,CAC3BC,YAAoB,EACpBC,YAAoB,EACpBC,aAAqB,EACZ;EACT,SAAS;;EAET,OAAOF,YAAY,GAAGC,YAAY,IAAIC,aAAa,GAAGJ,gBAAgB;AACxE;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASK,kBAAkBA,CAChCC,QAA8B,EAC9BC,OAAgB,EACP;EACT,SAAS;;EAET,QAAQD,QAAQ;IACd,KAAK,QAAQ;MACX,OAAO,IAAI;IACb,KAAK,OAAO;MACV,OAAO,KAAK;IACd,KAAK,WAAW;MACd,OAAOC,OAAO;IAChB,KAAK,YAAY;MACf,OAAO,IAAI;EACf;AACF;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASC,mBAAmBA,CACjCC,kBAA0B,EAC1BC,cAAsB,EACtBN,aAAqB,EACrBD,YAAoB,EACZ;EACR,SAAS;;EAET,MAAMQ,SAAS,GAAGC,IAAI,CAACC,GAAG,CAACT,aAAa,GAAGD,YAAY,GAAGO,cAAc,EAAE,CAAC,CAAC;EAE5E,OAAOE,IAAI,CAACE,GAAG,CAACF,IAAI,CAACC,GAAG,CAACJ,kBAAkB,GAAGC,cAAc,EAAE,CAAC,CAAC,EAAEC,SAAS,CAAC;AAC9E;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASI,uBAAuBA,CACrCC,cAAsB,EACtBN,cAAsB,EACtBN,aAAqB,EACrBD,YAAoB,EACpBc,QAAiB,EACT;EACR,SAAS;;EAET,IAAIA,QAAQ,EAAE;IACZ,MAAMN,SAAS,GAAGC,IAAI,CAACC,GAAG,CAACT,aAAa,GAAGD,YAAY,EAAE,CAAC,CAAC;IAE3D,OAAOS,IAAI,CAACC,GAAG,CACbD,IAAI,CAACE,GAAG,CAACE,cAAc,GAAGN,cAAc,EAAEC,SAAS,CAAC,EACpD,CAACD,cACH,CAAC;EACH;EAEA,MAAMC,SAAS,GAAGC,IAAI,CAACC,GAAG,CAACT,aAAa,GAAGD,YAAY,GAAGO,cAAc,EAAE,CAAC,CAAC;EAE5E,OAAOE,IAAI,CAACE,GAAG,CAACF,IAAI,CAACC,GAAG,CAACH,cAAc,GAAGM,cAAc,EAAE,CAAC,CAAC,EAAEL,SAAS,CAAC;AAC1E","ignoreList":[]}
|