aix 0.6.2 → 0.7.0
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 +154 -270
- package/ios/HybridAix.swift +18 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,32 @@
|
|
|
1
|
-
|
|
1
|
+
<img src="https://github.com/vercel/aix/blob/main/Aix.png?raw=true"
|
|
2
|
+
alt="aix" width="1600" height="900" />
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
# AIX
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
UI Primitives for building AI apps in React Native.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
> aix is currently in alpha preview. The API is likely to change.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- Start a chat scrolled to end on the first frame
|
|
13
|
+
- Animate scrolling to new messages when they send
|
|
14
|
+
- Float messages to the top of the screen with automated "blank size" handling
|
|
15
|
+
- Animate message content as it streams
|
|
16
|
+
- Keyboard handling out-of-the-box with no external dependencies
|
|
17
|
+
- Support for absolute-positioned composers
|
|
18
|
+
- Detect "is scrolled near end" for ScrollToEnd buttons
|
|
19
|
+
|
|
20
|
+
To learn about the motivation behind AIX, you can read our blog post on
|
|
21
|
+
[How we built the v0 iOS app](https://vercel.com/blog/how-we-built-the-v0-ios-app).
|
|
22
|
+
AIX is an opinionated, feature-complete, and extensible way to implement every
|
|
23
|
+
single feature mentioned in that blog post.
|
|
24
|
+
|
|
25
|
+
When building AIX, we started by copying the code from v0 into a separate
|
|
26
|
+
repository. However, as we worked to make it flexible for use cases outside of
|
|
27
|
+
our own app, we decided to rewrite it from scratch in native code. What you see
|
|
28
|
+
here is a Nitro Module which handles all business logic in UIKit. We plan on
|
|
29
|
+
adding support for Android as well and welcome contributions.
|
|
10
30
|
|
|
11
31
|
## Installation
|
|
12
32
|
|
|
@@ -14,16 +34,15 @@ aix is a native module with UIKit with Nitro Modules.
|
|
|
14
34
|
npm i aix react-native-nitro-modules
|
|
15
35
|
```
|
|
16
36
|
|
|
17
|
-
Next, rebuild your native app. For Expo users, run `npx expo prebuild` and
|
|
37
|
+
Next, rebuild your native app. For Expo users, run `npx expo prebuild` and
|
|
38
|
+
rebuild.
|
|
39
|
+
|
|
40
|
+
- For a full example, see the [example app](./react-native-aix/example/App.tsx).
|
|
18
41
|
|
|
19
42
|
## Usage
|
|
20
43
|
|
|
21
44
|
Wrap your `ScrollView` with `Aix`, and wrap your messages with `AixCell`.
|
|
22
45
|
|
|
23
|
-
<details>
|
|
24
|
-
<summary>Click here to view a full example</summary>
|
|
25
|
-
</details>
|
|
26
|
-
|
|
27
46
|
```tsx
|
|
28
47
|
import { Aix, AixCell } from 'aix'
|
|
29
48
|
import { Message } from 'path/to/your/message'
|
|
@@ -31,10 +50,14 @@ import { Composer } from 'path/to/your/composer'
|
|
|
31
50
|
|
|
32
51
|
export function ChatScreen({ messages }) {
|
|
33
52
|
return (
|
|
34
|
-
<Aix style={{ flex: 1 }}>
|
|
53
|
+
<Aix style={{ flex: 1 }} shouldStartAtEnd>
|
|
35
54
|
<ScrollView>
|
|
36
55
|
{messages.map((message) => (
|
|
37
|
-
<AixCell
|
|
56
|
+
<AixCell
|
|
57
|
+
key={message.id}
|
|
58
|
+
index={index}
|
|
59
|
+
isLast={index === messages.length - 1}
|
|
60
|
+
>
|
|
38
61
|
<Message message={message} />
|
|
39
62
|
</AixCell>
|
|
40
63
|
))}
|
|
@@ -44,44 +67,123 @@ export function ChatScreen({ messages }) {
|
|
|
44
67
|
}
|
|
45
68
|
```
|
|
46
69
|
|
|
47
|
-
|
|
70
|
+
### Add a floating composer
|
|
71
|
+
|
|
72
|
+
To add a floating composer which lets content scroll under it, you can use the
|
|
73
|
+
`AixFooter`. Pair it with `Aix.scrollOnFooterSizeUpdate` to ensure content
|
|
74
|
+
scrolls under the footer when it changes size.
|
|
48
75
|
|
|
49
76
|
```tsx
|
|
50
77
|
import { Aix, AixCell, AixFooter } from 'aix'
|
|
51
|
-
import { KeyboardStickyView } from 'react-native-keyboard-controller'
|
|
52
78
|
|
|
53
79
|
export function ChatScreen({ messages }) {
|
|
80
|
+
const { bottom } = useSafeAreaInsets()
|
|
81
|
+
|
|
54
82
|
return (
|
|
55
|
-
<Aix
|
|
83
|
+
<Aix
|
|
84
|
+
style={{ flex: 1 }}
|
|
85
|
+
shouldStartAtEnd
|
|
86
|
+
scrollOnFooterSizeUpdate={{
|
|
87
|
+
enabled: true,
|
|
88
|
+
scrolledToEndThreshold: 100,
|
|
89
|
+
animated: false,
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
56
92
|
<ScrollView>
|
|
57
93
|
{messages.map((message) => (
|
|
58
|
-
<AixCell
|
|
94
|
+
<AixCell
|
|
95
|
+
key={message.id}
|
|
96
|
+
index={index}
|
|
97
|
+
isLast={index === messages.length - 1}
|
|
98
|
+
>
|
|
59
99
|
<Message message={message} />
|
|
60
100
|
</AixCell>
|
|
61
101
|
))}
|
|
62
102
|
</ScrollView>
|
|
63
103
|
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
104
|
+
<AixFooter
|
|
105
|
+
style={{ position: 'absolute', inset: 0, top: 'auto' }}
|
|
106
|
+
stickToKeyboard={{
|
|
107
|
+
enabled: true,
|
|
108
|
+
offset: {
|
|
109
|
+
whenKeyboardOpen: 0,
|
|
110
|
+
whenKeyboardClosed: -bottom,
|
|
111
|
+
},
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
<Composer />
|
|
115
|
+
</AixFooter>
|
|
69
116
|
</Aix>
|
|
70
117
|
)
|
|
71
118
|
}
|
|
72
119
|
```
|
|
73
120
|
|
|
74
|
-
|
|
121
|
+
### Send a message
|
|
75
122
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
123
|
+
When sending a message, you will likely want to scroll to it after it gets added
|
|
124
|
+
to the list.
|
|
125
|
+
|
|
126
|
+
Simply call `aix.current?.scrollToIndexWhenBlankSizeReady(index)` in your submit
|
|
127
|
+
handler.
|
|
128
|
+
|
|
129
|
+
The `index` you pass should correspond to the newest message in the list. For AI
|
|
130
|
+
chats, this is typically the next assistant message index.
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
import { Keyboard } from 'react-native'
|
|
134
|
+
import { useAixRef } from 'aix'
|
|
135
|
+
|
|
136
|
+
function Chat() {
|
|
137
|
+
const aix = useAixRef()
|
|
138
|
+
|
|
139
|
+
const onSubmit = () => {
|
|
140
|
+
aix.current?.scrollToIndexWhenBlankSizeReady(messages.length + 1, true)
|
|
141
|
+
requestAnimationFrame(Keyboard.dismiss)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return <Aix ref={aix}>{/* ... */}</Aix>
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Add a scroll to end button
|
|
149
|
+
|
|
150
|
+
You can use `onScrolledNearEndChange` to show a "scroll to end" button when the
|
|
151
|
+
user scrolls away from the bottom:
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
import { Aix, useAixRef } from 'aix'
|
|
155
|
+
import { useState } from 'react'
|
|
156
|
+
import { Button } from 'react-native'
|
|
157
|
+
|
|
158
|
+
function Chat() {
|
|
159
|
+
const aix = useAixRef()
|
|
160
|
+
const [isNearEnd, setIsNearEnd] = useState(false)
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<Aix
|
|
164
|
+
ref={aix}
|
|
165
|
+
scrollEndReachedThreshold={200}
|
|
166
|
+
onScrolledNearEndChange={setIsNearEnd}
|
|
167
|
+
>
|
|
168
|
+
{/* ScrollView and messages... */}
|
|
169
|
+
|
|
170
|
+
{!isNearEnd && (
|
|
171
|
+
<Button
|
|
172
|
+
onPress={() => aix.current?.scrollToEnd(true)}
|
|
173
|
+
title='Scroll to end'
|
|
174
|
+
/>
|
|
175
|
+
)}
|
|
176
|
+
</Aix>
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
```
|
|
79
180
|
|
|
80
181
|
## API Reference
|
|
81
182
|
|
|
82
183
|
### `Aix`
|
|
83
184
|
|
|
84
|
-
The main container component that provides keyboard-aware behavior and manages
|
|
185
|
+
The main container component that provides keyboard-aware behavior and manages
|
|
186
|
+
scrolling for chat interfaces.
|
|
85
187
|
|
|
86
188
|
#### Props
|
|
87
189
|
|
|
@@ -107,7 +209,11 @@ const aix = useAixRef()
|
|
|
107
209
|
aix.current?.scrollToEnd(animated)
|
|
108
210
|
|
|
109
211
|
// Scroll to a specific index when the blank size is ready
|
|
110
|
-
aix.current?.scrollToIndexWhenBlankSizeReady(
|
|
212
|
+
aix.current?.scrollToIndexWhenBlankSizeReady(
|
|
213
|
+
index,
|
|
214
|
+
animated,
|
|
215
|
+
waitForKeyboardToEnd
|
|
216
|
+
)
|
|
111
217
|
```
|
|
112
218
|
|
|
113
219
|
| Method | Parameters | Description |
|
|
@@ -119,7 +225,8 @@ aix.current?.scrollToIndexWhenBlankSizeReady(index, animated, waitForKeyboardToE
|
|
|
119
225
|
|
|
120
226
|
### `AixCell`
|
|
121
227
|
|
|
122
|
-
A wrapper component for each message in the list. It communicates cell position
|
|
228
|
+
A wrapper component for each message in the list. It communicates cell position
|
|
229
|
+
and dimensions to the parent `Aix` component.
|
|
123
230
|
|
|
124
231
|
#### Props
|
|
125
232
|
|
|
@@ -132,177 +239,26 @@ A wrapper component for each message in the list. It communicates cell position
|
|
|
132
239
|
|
|
133
240
|
### `AixFooter`
|
|
134
241
|
|
|
135
|
-
A footer component for floating composers that allows content to scroll
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
- **Do not apply vertical padding** (`padding`, `paddingBottom`) directly to `AixFooter`. Apply padding to a child view instead.
|
|
140
|
-
- Position the footer absolutely at the bottom of the `Aix` container.
|
|
141
|
-
|
|
142
|
-
#### Recipe
|
|
143
|
-
|
|
144
|
-
```tsx
|
|
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
|
-
<Composer />
|
|
157
|
-
</AixFooter>
|
|
158
|
-
```
|
|
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.
|
|
242
|
+
A footer component for floating composers that allows content to scroll
|
|
243
|
+
underneath it. The footer's height is automatically tracked for proper scroll
|
|
244
|
+
offset calculations.
|
|
260
245
|
|
|
261
246
|
#### Props
|
|
262
247
|
|
|
263
|
-
|
|
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.
|
|
248
|
+
Accepts all standard React Native `View` props.
|
|
279
249
|
|
|
280
|
-
|
|
250
|
+
#### Important Notes
|
|
281
251
|
|
|
282
|
-
|
|
252
|
+
- **Do not apply vertical padding** (`padding`, `paddingBottom`) directly to
|
|
253
|
+
`AixFooter`. Apply padding to a child view instead.
|
|
254
|
+
- Position the footer absolutely at the bottom of the `Aix` container:
|
|
283
255
|
|
|
284
256
|
```tsx
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
function AssistantMessage({ content, isStreaming }) {
|
|
289
|
-
return (
|
|
290
|
-
<Text>
|
|
291
|
-
<TextFadeInStaggeredIfStreaming disabled={!isStreaming}>
|
|
292
|
-
{content}
|
|
293
|
-
</TextFadeInStaggeredIfStreaming>
|
|
294
|
-
</Text>
|
|
295
|
-
)
|
|
296
|
-
}
|
|
257
|
+
<AixFooter style={{ position: 'absolute', inset: 0, top: 'auto' }}>
|
|
258
|
+
<Composer />
|
|
259
|
+
</AixFooter>
|
|
297
260
|
```
|
|
298
261
|
|
|
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
|
-
|
|
306
262
|
---
|
|
307
263
|
|
|
308
264
|
### `useAixRef`
|
|
@@ -327,91 +283,19 @@ function Chat({ messages }) {
|
|
|
327
283
|
}
|
|
328
284
|
```
|
|
329
285
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
### `useContentInsetHandler`
|
|
333
|
-
|
|
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:
|
|
339
|
-
|
|
340
|
-
```tsx
|
|
341
|
-
import { Aix, useContentInsetHandler } from 'aix'
|
|
342
|
-
import { useSharedValue, useAnimatedProps } from 'react-native-reanimated'
|
|
343
|
-
import Animated from 'react-native-reanimated'
|
|
344
|
-
|
|
345
|
-
function Chat() {
|
|
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
|
-
}))
|
|
361
|
-
|
|
362
|
-
return (
|
|
363
|
-
<Aix
|
|
364
|
-
shouldStartAtEnd
|
|
365
|
-
shouldApplyContentInsets={false}
|
|
366
|
-
onWillApplyContentInsets={contentInsetHandler}
|
|
367
|
-
>
|
|
368
|
-
<Animated.ScrollView animatedProps={animatedScrollViewProps}>
|
|
369
|
-
{/* messages... */}
|
|
370
|
-
</Animated.ScrollView>
|
|
371
|
-
</Aix>
|
|
372
|
-
)
|
|
373
|
-
}
|
|
374
|
-
```
|
|
375
|
-
|
|
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`**
|
|
286
|
+
### Rules
|
|
384
287
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
| `left` | `number?` | Left content inset. |
|
|
389
|
-
| `bottom` | `number?` | Bottom content inset. |
|
|
390
|
-
| `right` | `number?` | Right content inset. |
|
|
288
|
+
- Do **NOT** apply padding to `contentContainerStyle`. Instead, use padding on
|
|
289
|
+
children directly.
|
|
290
|
+
- Do **NOT** apply padding to `AixFooter`. Instead, use padding on its children
|
|
391
291
|
|
|
392
292
|
---
|
|
393
293
|
|
|
394
|
-
|
|
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... */}
|
|
294
|
+
## TODOs
|
|
409
295
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
}
|
|
414
|
-
```
|
|
296
|
+
- [ ] Android support
|
|
297
|
+
- [ ] LegendList support
|
|
298
|
+
- [ ] FlashList support
|
|
415
299
|
|
|
416
300
|
## Requirements
|
|
417
301
|
|
package/ios/HybridAix.swift
CHANGED
|
@@ -432,14 +432,29 @@ class HybridAix: HybridAixSpec, AixContext, KeyboardNotificationsDelegate {
|
|
|
432
432
|
private func calculateBlankSize(keyboardHeight: CGFloat, additionalContentInsetBottom: CGFloat) -> CGFloat {
|
|
433
433
|
guard let scrollView, let blankView else { return 0 }
|
|
434
434
|
|
|
435
|
-
let
|
|
436
|
-
let
|
|
435
|
+
let startIndex: Int
|
|
436
|
+
let endIndex = Int(blankView.index) - 1
|
|
437
|
+
if let penultimateCellIndex {
|
|
438
|
+
startIndex = Int(penultimateCellIndex)
|
|
439
|
+
} else {
|
|
440
|
+
startIndex = endIndex
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
var cellsBeforeBlankViewHeight: CGFloat = 0
|
|
444
|
+
if startIndex <= endIndex {
|
|
445
|
+
for i in startIndex...endIndex {
|
|
446
|
+
if let cell = getCell(index: i) {
|
|
447
|
+
cellsBeforeBlankViewHeight += cell.view.frame.height
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
437
452
|
let blankViewHeight = blankView.view.frame.height
|
|
438
453
|
|
|
439
454
|
// Calculate visible area above all bottom chrome (keyboard, composer, additional insets)
|
|
440
455
|
// The blank size fills the remaining space so the last message can scroll to the top
|
|
441
456
|
let visibleAreaHeight = scrollView.bounds.height - keyboardHeight - composerHeight - additionalContentInsetBottom
|
|
442
|
-
let inset = visibleAreaHeight - blankViewHeight -
|
|
457
|
+
let inset = visibleAreaHeight - blankViewHeight - cellsBeforeBlankViewHeight
|
|
443
458
|
return max(0, inset)
|
|
444
459
|
}
|
|
445
460
|
|