aix 0.6.0 → 0.6.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/README.md +269 -70
- package/ios/EditMenuDefaultActions.swift +70 -0
- package/ios/HybridAix.swift +28 -3
- package/ios/HybridAixCellView.swift +1 -1
- package/ios/HybridAixComposer.swift +82 -0
- package/ios/HybridAixDropzone.swift +104 -0
- package/ios/HybridAixInputWrapper.swift +447 -0
- package/ios/InputType.swift +40 -0
- package/ios/PasteFileManager.swift +92 -0
- package/nitro.json +8 -0
- package/nitrogen/generated/android/Aix+autolinking.cmake +8 -0
- package/nitrogen/generated/android/AixOnLoad.cpp +26 -0
- package/nitrogen/generated/android/c++/JAixInputWrapperOnPasteEvent.hpp +70 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_AixInputWrapperOnPasteEvent_.hpp +98 -0
- package/nitrogen/generated/android/c++/JHybridAixComposerSpec.cpp +9 -0
- package/nitrogen/generated/android/c++/JHybridAixComposerSpec.hpp +2 -0
- package/nitrogen/generated/android/c++/JHybridAixDropzoneSpec.cpp +72 -0
- package/nitrogen/generated/android/c++/JHybridAixDropzoneSpec.hpp +66 -0
- package/nitrogen/generated/android/c++/JHybridAixInputWrapperSpec.cpp +144 -0
- package/nitrogen/generated/android/c++/JHybridAixInputWrapperSpec.hpp +74 -0
- package/nitrogen/generated/android/c++/views/JHybridAixComposerStateUpdater.cpp +4 -0
- package/nitrogen/generated/android/c++/views/JHybridAixDropzoneStateUpdater.cpp +56 -0
- package/nitrogen/generated/android/c++/views/JHybridAixDropzoneStateUpdater.hpp +49 -0
- package/nitrogen/generated/android/c++/views/JHybridAixInputWrapperStateUpdater.cpp +72 -0
- package/nitrogen/generated/android/c++/views/JHybridAixInputWrapperStateUpdater.hpp +49 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/AixInputWrapperOnPasteEvent.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/Func_void_std__vector_AixInputWrapperOnPasteEvent_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/HybridAixComposerSpec.kt +6 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/HybridAixDropzoneSpec.kt +67 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/HybridAixInputWrapperSpec.kt +91 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/views/HybridAixDropzoneManager.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/views/HybridAixDropzoneStateUpdater.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/views/HybridAixInputWrapperManager.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/views/HybridAixInputWrapperStateUpdater.kt +23 -0
- package/nitrogen/generated/ios/Aix-Swift-Cxx-Bridge.cpp +42 -0
- package/nitrogen/generated/ios/Aix-Swift-Cxx-Bridge.hpp +112 -0
- package/nitrogen/generated/ios/Aix-Swift-Cxx-Umbrella.hpp +14 -0
- package/nitrogen/generated/ios/AixAutolinking.mm +16 -0
- package/nitrogen/generated/ios/AixAutolinking.swift +30 -0
- package/nitrogen/generated/ios/c++/HybridAixComposerSpecSwift.hpp +7 -0
- package/nitrogen/generated/ios/c++/HybridAixDropzoneSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridAixDropzoneSpecSwift.hpp +80 -0
- package/nitrogen/generated/ios/c++/HybridAixInputWrapperSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridAixInputWrapperSpecSwift.hpp +108 -0
- package/nitrogen/generated/ios/c++/views/HybridAixComposerComponent.mm +5 -0
- package/nitrogen/generated/ios/c++/views/HybridAixDropzoneComponent.mm +96 -0
- package/nitrogen/generated/ios/c++/views/HybridAixInputWrapperComponent.mm +116 -0
- package/nitrogen/generated/ios/swift/AixInputWrapperOnPasteEvent.swift +107 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_AixInputWrapperOnPasteEvent_.swift +47 -0
- package/nitrogen/generated/ios/swift/HybridAixComposerSpec.swift +1 -0
- package/nitrogen/generated/ios/swift/HybridAixComposerSpec_cxx.swift +24 -0
- package/nitrogen/generated/ios/swift/HybridAixDropzoneSpec.swift +56 -0
- package/nitrogen/generated/ios/swift/HybridAixDropzoneSpec_cxx.swift +167 -0
- package/nitrogen/generated/ios/swift/HybridAixInputWrapperSpec.swift +60 -0
- package/nitrogen/generated/ios/swift/HybridAixInputWrapperSpec_cxx.swift +261 -0
- package/nitrogen/generated/shared/c++/AixInputWrapperOnPasteEvent.hpp +88 -0
- package/nitrogen/generated/shared/c++/HybridAixComposerSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridAixComposerSpec.hpp +2 -0
- package/nitrogen/generated/shared/c++/HybridAixDropzoneSpec.cpp +22 -0
- package/nitrogen/generated/shared/c++/HybridAixDropzoneSpec.hpp +67 -0
- package/nitrogen/generated/shared/c++/HybridAixInputWrapperSpec.cpp +30 -0
- package/nitrogen/generated/shared/c++/HybridAixInputWrapperSpec.hpp +76 -0
- package/nitrogen/generated/shared/c++/views/HybridAixComposerComponent.cpp +12 -0
- package/nitrogen/generated/shared/c++/views/HybridAixComposerComponent.hpp +1 -0
- package/nitrogen/generated/shared/c++/views/HybridAixDropzoneComponent.cpp +87 -0
- package/nitrogen/generated/shared/c++/views/HybridAixDropzoneComponent.hpp +109 -0
- package/nitrogen/generated/shared/c++/views/HybridAixInputWrapperComponent.cpp +135 -0
- package/nitrogen/generated/shared/c++/views/HybridAixInputWrapperComponent.hpp +114 -0
- package/nitrogen/generated/shared/json/AixComposerConfig.json +1 -0
- package/nitrogen/generated/shared/json/AixDropzoneConfig.json +10 -0
- package/nitrogen/generated/shared/json/AixInputWrapperConfig.json +14 -0
- package/package.json +1 -1
- package/src/dropzone.ios.tsx +27 -0
- package/src/dropzone.tsx +10 -0
- package/src/index.ts +3 -0
- package/src/input-wrapper.ios.tsx +30 -0
- package/src/input-wrapper.tsx +17 -0
- package/src/views/aix.nitro.ts +33 -19
package/README.md
CHANGED
|
@@ -25,57 +25,49 @@ Wrap your `ScrollView` with `Aix`, and wrap your messages with `AixCell`.
|
|
|
25
25
|
</details>
|
|
26
26
|
|
|
27
27
|
```tsx
|
|
28
|
-
import { Aix, AixCell } from 'aix'
|
|
29
|
-
import { Message } from 'path/to/your/message'
|
|
30
|
-
import { Composer } from 'path/to/your/composer'
|
|
28
|
+
import { Aix, AixCell } from 'aix'
|
|
29
|
+
import { Message } from 'path/to/your/message'
|
|
30
|
+
import { Composer } from 'path/to/your/composer'
|
|
31
31
|
|
|
32
32
|
export function ChatScreen({ messages }) {
|
|
33
33
|
return (
|
|
34
34
|
<Aix style={{ flex: 1 }}>
|
|
35
35
|
<ScrollView>
|
|
36
36
|
{messages.map((message) => (
|
|
37
|
-
<AixCell
|
|
38
|
-
key={message.id}
|
|
39
|
-
index={index}
|
|
40
|
-
isLast={index === messages.length - 1}
|
|
41
|
-
>
|
|
37
|
+
<AixCell key={message.id} index={index} isLast={index === messages.length - 1}>
|
|
42
38
|
<Message message={message} />
|
|
43
39
|
</AixCell>
|
|
44
40
|
))}
|
|
45
41
|
</ScrollView>
|
|
46
42
|
</Aix>
|
|
47
|
-
)
|
|
43
|
+
)
|
|
48
44
|
}
|
|
49
45
|
```
|
|
50
46
|
|
|
51
47
|
To add a floating composer which lets content scroll under it, you can use the `AixFooter` and `KeyboardStickyView` from `react-native-keyboard-controller`:
|
|
52
48
|
|
|
53
49
|
```tsx
|
|
54
|
-
import { Aix, AixCell, AixFooter } from 'aix'
|
|
55
|
-
import { KeyboardStickyView } from 'react-native-keyboard-controller'
|
|
50
|
+
import { Aix, AixCell, AixFooter } from 'aix'
|
|
51
|
+
import { KeyboardStickyView } from 'react-native-keyboard-controller'
|
|
56
52
|
|
|
57
53
|
export function ChatScreen({ messages }) {
|
|
58
54
|
return (
|
|
59
55
|
<Aix style={{ flex: 1 }}>
|
|
60
56
|
<ScrollView>
|
|
61
57
|
{messages.map((message) => (
|
|
62
|
-
<AixCell
|
|
63
|
-
key={message.id}
|
|
64
|
-
index={index}
|
|
65
|
-
isLast={index === messages.length - 1}
|
|
66
|
-
>
|
|
58
|
+
<AixCell key={message.id} index={index} isLast={index === messages.length - 1}>
|
|
67
59
|
<Message message={message} />
|
|
68
60
|
</AixCell>
|
|
69
61
|
))}
|
|
70
62
|
</ScrollView>
|
|
71
63
|
|
|
72
64
|
<KeyboardStickyView offset={{ opened: 0, closed: -bottomInsetPadding }}>
|
|
73
|
-
<AixFooter style={{ position: 'absolute', inset: 0, top: 'auto'}}>
|
|
65
|
+
<AixFooter fixInput style={{ position: 'absolute', inset: 0, top: 'auto' }}>
|
|
74
66
|
<Composer />
|
|
75
67
|
</AixFooter>
|
|
76
68
|
</KeyboardStickyView>
|
|
77
69
|
</Aix>
|
|
78
|
-
)
|
|
70
|
+
)
|
|
79
71
|
}
|
|
80
72
|
```
|
|
81
73
|
|
|
@@ -93,34 +85,34 @@ The main container component that provides keyboard-aware behavior and manages s
|
|
|
93
85
|
|
|
94
86
|
#### Props
|
|
95
87
|
|
|
96
|
-
| Prop
|
|
97
|
-
|
|
98
|
-
| `shouldStartAtEnd`
|
|
99
|
-
| `scrollOnFooterSizeUpdate`
|
|
100
|
-
| `scrollEndReachedThreshold`
|
|
101
|
-
| `onScrolledNearEndChange`
|
|
102
|
-
| `additionalContentInsets`
|
|
103
|
-
| `additionalScrollIndicatorInsets` | `object`
|
|
104
|
-
| `mainScrollViewID`
|
|
105
|
-
| `penultimateCellIndex`
|
|
88
|
+
| Prop | Type | Default | Description |
|
|
89
|
+
| --------------------------------- | ------------------------------ | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
90
|
+
| `shouldStartAtEnd` | `boolean` | - | Whether the scroll view should start scrolled to the end of the content. |
|
|
91
|
+
| `scrollOnFooterSizeUpdate` | `object` | `{ enabled: true, scrolledToEndThreshold: 100, animated: false }` | Control the behavior of scrolling when the footer size changes. By default, changing the height of the footer will shift content up in the scroll view. |
|
|
92
|
+
| `scrollEndReachedThreshold` | `number` | `max(blankSize, 200)` | The number of pixels from the bottom of the scroll view to the end of the content that is considered "near the end". Used by `onScrolledNearEndChange` and to determine if content should shift up when keyboard opens. |
|
|
93
|
+
| `onScrolledNearEndChange` | `(isNearEnd: boolean) => void` | - | Callback fired when the scroll position transitions between "near end" and "not near end" states. Reactive to user scrolling, content size changes, parent size changes, and keyboard height changes. Uses `scrollEndReachedThreshold` to determine the threshold. |
|
|
94
|
+
| `additionalContentInsets` | `object` | - | Additional content insets applied when keyboard is open or closed. Shape: `{ top?: { whenKeyboardOpen, whenKeyboardClosed }, bottom?: { whenKeyboardOpen, whenKeyboardClosed } }` |
|
|
95
|
+
| `additionalScrollIndicatorInsets` | `object` | - | Additional insets for the scroll indicator, added to existing safe area insets. Applied to `verticalScrollIndicatorInsets` on iOS. |
|
|
96
|
+
| `mainScrollViewID` | `string` | - | The `nativeID` of the scroll view to use. If provided, will search for a scroll view with this `accessibilityIdentifier`. |
|
|
97
|
+
| `penultimateCellIndex` | `number` | - | The index of the second-to-last message (typically the last user message in AI chat apps). Used to determine which message will be scrolled into view. Useful when you have custom message types like timestamps in your list. |
|
|
106
98
|
|
|
107
99
|
#### Ref Methods
|
|
108
100
|
|
|
109
101
|
Access these methods via `useAixRef()`:
|
|
110
102
|
|
|
111
103
|
```tsx
|
|
112
|
-
const aix = useAixRef()
|
|
104
|
+
const aix = useAixRef()
|
|
113
105
|
|
|
114
106
|
// Scroll to the end of the content
|
|
115
|
-
aix.current?.scrollToEnd(animated)
|
|
107
|
+
aix.current?.scrollToEnd(animated)
|
|
116
108
|
|
|
117
109
|
// Scroll to a specific index when the blank size is ready
|
|
118
|
-
aix.current?.scrollToIndexWhenBlankSizeReady(index, animated, waitForKeyboardToEnd)
|
|
110
|
+
aix.current?.scrollToIndexWhenBlankSizeReady(index, animated, waitForKeyboardToEnd)
|
|
119
111
|
```
|
|
120
112
|
|
|
121
|
-
| Method
|
|
122
|
-
|
|
123
|
-
| `scrollToEnd`
|
|
113
|
+
| Method | Parameters | Description |
|
|
114
|
+
| --------------------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
115
|
+
| `scrollToEnd` | `animated?: boolean` | Scrolls to the end of the content. |
|
|
124
116
|
| `scrollToIndexWhenBlankSizeReady` | `index: number, animated?: boolean, waitForKeyboardToEnd?: boolean` | Scrolls to a specific cell index once the blank size calculation is ready. |
|
|
125
117
|
|
|
126
118
|
---
|
|
@@ -131,10 +123,10 @@ A wrapper component for each message in the list. It communicates cell position
|
|
|
131
123
|
|
|
132
124
|
#### Props
|
|
133
125
|
|
|
134
|
-
| Prop
|
|
135
|
-
|
|
136
|
-
| `index`
|
|
137
|
-
| `isLast` | `boolean` | Yes
|
|
126
|
+
| Prop | Type | Required | Description |
|
|
127
|
+
| -------- | --------- | -------- | ------------------------------------------------------------------------------------------- |
|
|
128
|
+
| `index` | `number` | Yes | The index of this cell in the message list. |
|
|
129
|
+
| `isLast` | `boolean` | Yes | Whether this cell is the last item in the list. Used for scroll positioning and animations. |
|
|
138
130
|
|
|
139
131
|
---
|
|
140
132
|
|
|
@@ -142,21 +134,175 @@ A wrapper component for each message in the list. It communicates cell position
|
|
|
142
134
|
|
|
143
135
|
A footer component for floating composers that allows content to scroll underneath it. The footer's height is automatically tracked for proper scroll offset calculations.
|
|
144
136
|
|
|
145
|
-
#### Props
|
|
146
|
-
|
|
147
|
-
Accepts all standard React Native `View` props.
|
|
148
|
-
|
|
149
137
|
#### Important Notes
|
|
150
138
|
|
|
151
139
|
- **Do not apply vertical padding** (`padding`, `paddingBottom`) directly to `AixFooter`. Apply padding to a child view instead.
|
|
152
|
-
- Position the footer absolutely at the bottom of the `Aix` container
|
|
140
|
+
- Position the footer absolutely at the bottom of the `Aix` container.
|
|
141
|
+
|
|
142
|
+
#### Recipe
|
|
153
143
|
|
|
154
144
|
```tsx
|
|
155
|
-
<AixFooter
|
|
145
|
+
<AixFooter
|
|
146
|
+
fixInput
|
|
147
|
+
stickToKeyboard={{
|
|
148
|
+
enabled: true,
|
|
149
|
+
offset: {
|
|
150
|
+
whenKeyboardOpen: 0,
|
|
151
|
+
whenKeyboardClosed: bottomSafeAreaInset,
|
|
152
|
+
},
|
|
153
|
+
}}
|
|
154
|
+
style={{ position: 'absolute', inset: 0, top: 'auto' }}
|
|
155
|
+
>
|
|
156
156
|
<Composer />
|
|
157
157
|
</AixFooter>
|
|
158
158
|
```
|
|
159
159
|
|
|
160
|
+
#### Props
|
|
161
|
+
|
|
162
|
+
| Prop | Type | Default | Description |
|
|
163
|
+
| ----------------- | -------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
164
|
+
| `fixInput` | `boolean` | - | Patches the `TextInput` inside the footer to disable scroll bouncing, hide scroll indicators, enable interactive keyboard dismiss, and add swipe-up-to-focus. Recommended for multiline inputs. This may move to `AixInputWrapper` in the future. |
|
|
165
|
+
| `stickToKeyboard` | `AixStickToKeyboard` | - | Controls whether the footer translates upward with the keyboard. See shape below. |
|
|
166
|
+
|
|
167
|
+
Also accepts all standard React Native `View` props.
|
|
168
|
+
|
|
169
|
+
**`AixStickToKeyboard`**
|
|
170
|
+
|
|
171
|
+
| Field | Type | Required | Description |
|
|
172
|
+
| --------------------------- | --------- | -------- | --------------------------------------------------------------------------------------------- |
|
|
173
|
+
| `enabled` | `boolean` | Yes | Whether the footer should translate upward with the keyboard. |
|
|
174
|
+
| `offset.whenKeyboardOpen` | `number` | No | Additional vertical offset applied when the keyboard is fully open. |
|
|
175
|
+
| `offset.whenKeyboardClosed` | `number` | No | Additional vertical offset applied when the keyboard is closed (e.g. bottom safe area inset). |
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
### `AixDropzone`
|
|
180
|
+
|
|
181
|
+
A wrapper component that enables drag-and-drop file support for your chat interface. Users can drop images, documents, and other files onto the zone.
|
|
182
|
+
|
|
183
|
+
On iOS, this uses a native drop interaction handler. On other platforms, it renders its children without drop support.
|
|
184
|
+
|
|
185
|
+
#### Recipe
|
|
186
|
+
|
|
187
|
+
Wrap your chat screen with `AixDropzone` to handle dropped files:
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
import { useState } from 'react'
|
|
191
|
+
import { AixDropzone, Aix, type AixInputWrapperOnPasteEvent } from 'aix'
|
|
192
|
+
|
|
193
|
+
function ChatScreen() {
|
|
194
|
+
const [attachments, setAttachments] = useState<AixInputWrapperOnPasteEvent[]>([])
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<AixDropzone
|
|
198
|
+
onDrop={(events) => {
|
|
199
|
+
setAttachments((prev) => [...prev, ...events])
|
|
200
|
+
}}
|
|
201
|
+
>
|
|
202
|
+
<Aix shouldStartAtEnd style={{ flex: 1 }}>
|
|
203
|
+
{/* ScrollView, messages, footer... */}
|
|
204
|
+
</Aix>
|
|
205
|
+
</AixDropzone>
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
#### Props
|
|
211
|
+
|
|
212
|
+
| Prop | Type | Default | Description |
|
|
213
|
+
| ---------- | ------------------------------------------------- | ------- | -------------------------------------------------------------------------------------- |
|
|
214
|
+
| `onDrop` | `(events: AixInputWrapperOnPasteEvent[]) => void` | - | Called when the user drops files onto the zone. Each event describes one dropped item. |
|
|
215
|
+
| `children` | `ReactNode` | - | Content to render inside the drop zone. |
|
|
216
|
+
|
|
217
|
+
**`AixInputWrapperOnPasteEvent`**
|
|
218
|
+
|
|
219
|
+
| Field | Type | Description |
|
|
220
|
+
| --------------- | --------- | ------------------------------------------------- |
|
|
221
|
+
| `type` | `string` | The kind of item (e.g. `"image"`). |
|
|
222
|
+
| `filePath` | `string` | Local file path to the dropped or pasted content. |
|
|
223
|
+
| `fileExtension` | `string?` | File extension (e.g. `"png"`, `"pdf"`). |
|
|
224
|
+
| `fileName` | `string?` | Original file name. |
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
### `AixInputWrapper`
|
|
229
|
+
|
|
230
|
+
A wrapper for your `TextInput` that intercepts paste events, letting users paste images and files from their clipboard into the composer.
|
|
231
|
+
|
|
232
|
+
On iOS, this uses a native view that overrides paste behavior to capture rich content. On other platforms, it renders its children without paste interception.
|
|
233
|
+
|
|
234
|
+
#### Recipe
|
|
235
|
+
|
|
236
|
+
Wrap your `TextInput` with `AixInputWrapper` to handle pasted images and files:
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
import { useState } from 'react'
|
|
240
|
+
import { TextInput } from 'react-native'
|
|
241
|
+
import { AixInputWrapper, type AixInputWrapperOnPasteEvent } from 'aix'
|
|
242
|
+
|
|
243
|
+
function Composer() {
|
|
244
|
+
const [attachments, setAttachments] = useState<AixInputWrapperOnPasteEvent[]>([])
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<AixInputWrapper
|
|
248
|
+
editMenuDefaultActions={['paste']}
|
|
249
|
+
onPaste={(events) => {
|
|
250
|
+
setAttachments((prev) => [...prev, ...events])
|
|
251
|
+
}}
|
|
252
|
+
>
|
|
253
|
+
<TextInput placeholder='Type a message...' multiline />
|
|
254
|
+
</AixInputWrapper>
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Each event in `onPaste` has the same [`AixInputWrapperOnPasteEvent`](#aixinputwrapperonpasteevent) shape described above.
|
|
260
|
+
|
|
261
|
+
#### Props
|
|
262
|
+
|
|
263
|
+
| Prop | Type | Default | Description |
|
|
264
|
+
| ------------------------ | ------------------------------------------------- | ------- | -------------------------------------------------------------------------------- |
|
|
265
|
+
| `onPaste` | `(events: AixInputWrapperOnPasteEvent[]) => void` | - | Called when the user pastes rich content (images, files) into the wrapped input. |
|
|
266
|
+
| `pasteConfiguration` | `string[]` | - | UTI types to accept for paste events. |
|
|
267
|
+
| `editMenuDefaultActions` | `string[]` | - | Which default edit menu actions to show (e.g. `['paste']`). |
|
|
268
|
+
| `maxLines` | `number` | - | Maximum number of visible lines for the wrapped input. |
|
|
269
|
+
| `maxChars` | `number` | - | Maximum character count for the wrapped input. |
|
|
270
|
+
| `children` | `ReactNode` | - | Should contain your `TextInput`. |
|
|
271
|
+
|
|
272
|
+
Also accepts all standard React Native `View` props.
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
### `TextFadeInStaggeredIfStreaming`
|
|
277
|
+
|
|
278
|
+
Animates text children with a staggered word-by-word fade-in effect. Each word fades in over 500ms, staggered 32ms apart. A pool system limits concurrent animations for smooth performance.
|
|
279
|
+
|
|
280
|
+
The `disabled` state is captured on mount. If the component mounts with `disabled={true}`, animation stays off even if `disabled` later becomes `false`. This prevents completed messages from re-animating on re-render.
|
|
281
|
+
|
|
282
|
+
#### Recipe
|
|
283
|
+
|
|
284
|
+
```tsx
|
|
285
|
+
import { Text } from 'react-native'
|
|
286
|
+
import { TextFadeInStaggeredIfStreaming } from 'aix'
|
|
287
|
+
|
|
288
|
+
function AssistantMessage({ content, isStreaming }) {
|
|
289
|
+
return (
|
|
290
|
+
<Text>
|
|
291
|
+
<TextFadeInStaggeredIfStreaming disabled={!isStreaming}>
|
|
292
|
+
{content}
|
|
293
|
+
</TextFadeInStaggeredIfStreaming>
|
|
294
|
+
</Text>
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
#### Props
|
|
300
|
+
|
|
301
|
+
| Prop | Type | Required | Description |
|
|
302
|
+
| ---------- | ----------- | -------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
303
|
+
| `disabled` | `boolean` | Yes | Whether to disable the fade-in animation. Pass `true` for completed messages, `false` for currently-streaming messages. |
|
|
304
|
+
| `children` | `ReactNode` | - | Text content to animate. String children are split by word and each word fades in individually. |
|
|
305
|
+
|
|
160
306
|
---
|
|
161
307
|
|
|
162
308
|
### `useAixRef`
|
|
@@ -164,57 +310,110 @@ Accepts all standard React Native `View` props.
|
|
|
164
310
|
A hook that returns a ref to access imperative methods on the `Aix` component.
|
|
165
311
|
|
|
166
312
|
```tsx
|
|
167
|
-
import { useAixRef } from 'aix'
|
|
313
|
+
import { useAixRef } from 'aix'
|
|
168
314
|
|
|
169
315
|
function Chat({ messages }) {
|
|
170
|
-
const aix = useAixRef()
|
|
316
|
+
const aix = useAixRef()
|
|
171
317
|
const send = useSendMessage()
|
|
172
|
-
|
|
318
|
+
|
|
173
319
|
const handleSend = () => {
|
|
174
320
|
// Scroll to end after sending a message
|
|
175
|
-
send(message)
|
|
176
|
-
aix.current?.scrollToIndexWhenBlankSizeReady(messages.length + 1, true)
|
|
177
|
-
requestAnimationFrame(Keyboard.dismiss)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return <Aix ref={aix}>{/* ... */}</Aix
|
|
321
|
+
send(message)
|
|
322
|
+
aix.current?.scrollToIndexWhenBlankSizeReady(messages.length + 1, true)
|
|
323
|
+
requestAnimationFrame(Keyboard.dismiss)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return <Aix ref={aix}>{/* ... */}</Aix>
|
|
181
327
|
}
|
|
182
328
|
```
|
|
183
329
|
|
|
184
330
|
---
|
|
185
331
|
|
|
186
|
-
###
|
|
332
|
+
### `useContentInsetHandler`
|
|
187
333
|
|
|
188
|
-
|
|
334
|
+
A hook for receiving content inset updates from the native scroll view. Use this when you want to apply content insets yourself (e.g. via Reanimated shared values) instead of letting `Aix` apply them natively.
|
|
335
|
+
|
|
336
|
+
#### Recipe
|
|
337
|
+
|
|
338
|
+
Apply content insets with Reanimated for custom control:
|
|
189
339
|
|
|
190
340
|
```tsx
|
|
191
|
-
import { Aix,
|
|
192
|
-
import {
|
|
341
|
+
import { Aix, useContentInsetHandler } from 'aix'
|
|
342
|
+
import { useSharedValue, useAnimatedProps } from 'react-native-reanimated'
|
|
343
|
+
import Animated from 'react-native-reanimated'
|
|
193
344
|
|
|
194
345
|
function Chat() {
|
|
195
|
-
const
|
|
196
|
-
|
|
346
|
+
const bottomInset = useSharedValue<number | null>(null)
|
|
347
|
+
|
|
348
|
+
const contentInsetHandler = useContentInsetHandler((insets) => {
|
|
349
|
+
'worklet'
|
|
350
|
+
bottomInset.set(insets.bottom ?? null)
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
const animatedScrollViewProps = useAnimatedProps(() => ({
|
|
354
|
+
contentInset: {
|
|
355
|
+
top: 0,
|
|
356
|
+
bottom: bottomInset.get() ?? 0,
|
|
357
|
+
left: 0,
|
|
358
|
+
right: 0,
|
|
359
|
+
},
|
|
360
|
+
}))
|
|
197
361
|
|
|
198
362
|
return (
|
|
199
363
|
<Aix
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
364
|
+
shouldStartAtEnd
|
|
365
|
+
shouldApplyContentInsets={false}
|
|
366
|
+
onWillApplyContentInsets={contentInsetHandler}
|
|
203
367
|
>
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
<Button onPress={() => aix.current?.scrollToEnd(true)} />
|
|
208
|
-
)}
|
|
368
|
+
<Animated.ScrollView animatedProps={animatedScrollViewProps}>
|
|
369
|
+
{/* messages... */}
|
|
370
|
+
</Animated.ScrollView>
|
|
209
371
|
</Aix>
|
|
210
|
-
)
|
|
372
|
+
)
|
|
211
373
|
}
|
|
212
374
|
```
|
|
213
375
|
|
|
214
|
-
|
|
376
|
+
#### Parameters
|
|
377
|
+
|
|
378
|
+
| Parameter | Type | Description |
|
|
379
|
+
| -------------- | ------------------------------------ | ------------------------------------------------------------------------ |
|
|
380
|
+
| `handler` | `(insets: AixContentInsets) => void` | Callback receiving computed content insets. Can be a Reanimated worklet. |
|
|
381
|
+
| `dependencies` | `unknown[]` | Dependency array for the callback (default: `[]`). |
|
|
382
|
+
|
|
383
|
+
**`AixContentInsets`**
|
|
215
384
|
|
|
385
|
+
| Field | Type | Description |
|
|
386
|
+
| -------- | --------- | --------------------- |
|
|
387
|
+
| `top` | `number?` | Top content inset. |
|
|
388
|
+
| `left` | `number?` | Left content inset. |
|
|
389
|
+
| `bottom` | `number?` | Bottom content inset. |
|
|
390
|
+
| `right` | `number?` | Right content inset. |
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
### Scroll to End Button
|
|
395
|
+
|
|
396
|
+
You can use `onScrolledNearEndChange` to show a "scroll to end" button when the user scrolls away from the bottom:
|
|
397
|
+
|
|
398
|
+
```tsx
|
|
399
|
+
import { Aix, useAixRef } from 'aix'
|
|
400
|
+
import { useState } from 'react'
|
|
401
|
+
|
|
402
|
+
function Chat() {
|
|
403
|
+
const aix = useAixRef()
|
|
404
|
+
const [isNearEnd, setIsNearEnd] = useState(false)
|
|
405
|
+
|
|
406
|
+
return (
|
|
407
|
+
<Aix ref={aix} scrollEndReachedThreshold={200} onScrolledNearEndChange={setIsNearEnd}>
|
|
408
|
+
{/* ScrollView and messages... */}
|
|
409
|
+
|
|
410
|
+
{!isNearEnd && <Button onPress={() => aix.current?.scrollToEnd(true)} />}
|
|
411
|
+
</Aix>
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
```
|
|
216
415
|
|
|
217
416
|
## Requirements
|
|
218
417
|
|
|
219
418
|
- React Native v0.78.0 or higher
|
|
220
|
-
- Node 18.0.0 or higher
|
|
419
|
+
- Node 18.0.0 or higher
|
|
@@ -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
|
+
}
|
package/ios/HybridAix.swift
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
|
@@ -155,6 +154,7 @@ class HybridAixCellView: HybridAixCellViewSpec {
|
|
|
155
154
|
private func updateRegistration() {
|
|
156
155
|
guard let ctx = getAixContext() else { return }
|
|
157
156
|
ctx.registerCell(self)
|
|
157
|
+
updateBlankViewStatus()
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
/// Update blank view status (called when isLast changes)
|