@assistant-ui/react-native 0.1.30 → 0.1.31
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/dist/primitives/actionBar/ActionBarCopy.js.map +1 -1
- package/dist/primitives/actionBar/ActionBarFeedbackNegative.js.map +1 -1
- package/dist/primitives/actionBar/ActionBarFeedbackPositive.js.map +1 -1
- package/dist/primitives/attachment/AttachmentName.js.map +1 -1
- package/dist/primitives/attachment/AttachmentThumb.js.map +1 -1
- package/dist/primitives/branchPicker/BranchPickerCount.js.map +1 -1
- package/dist/primitives/branchPicker/BranchPickerNumber.js.map +1 -1
- package/dist/primitives/error/ErrorMessage.d.ts.map +1 -1
- package/dist/primitives/error/ErrorMessage.js.map +1 -1
- package/dist/primitives/error/ErrorRoot.d.ts.map +1 -1
- package/dist/primitives/message/MessageIf.js.map +1 -1
- package/dist/primitives/suggestion/SuggestionDescription.js.map +1 -1
- package/dist/primitives/suggestion/SuggestionTitle.js.map +1 -1
- package/dist/primitives/thread/ThreadIf.js.map +1 -1
- package/dist/primitives/thread/ThreadMessages.d.ts +12 -3
- package/dist/primitives/thread/ThreadMessages.d.ts.map +1 -1
- package/dist/primitives/thread/ThreadMessages.js +374 -33
- package/dist/primitives/thread/ThreadMessages.js.map +1 -1
- package/dist/primitives/thread.d.ts +3 -3
- package/dist/primitives/thread.js +3 -2
- package/dist/primitives/thread.js.map +1 -1
- package/package.json +8 -8
- package/src/primitives/thread/ThreadMessages.test.tsx +423 -9
- package/src/primitives/thread/ThreadMessages.tsx +318 -28
- package/src/primitives/thread.ts +2 -0
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type ComponentType,
|
|
3
3
|
type FC,
|
|
4
|
+
type ForwardedRef,
|
|
4
5
|
type ReactNode,
|
|
6
|
+
type RefObject,
|
|
5
7
|
forwardRef,
|
|
6
8
|
memo,
|
|
7
9
|
useCallback,
|
|
10
|
+
useEffect,
|
|
11
|
+
useRef,
|
|
8
12
|
} from "react";
|
|
9
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
FlatList,
|
|
15
|
+
type FlatListProps,
|
|
16
|
+
type LayoutChangeEvent,
|
|
17
|
+
type NativeScrollEvent,
|
|
18
|
+
type NativeSyntheticEvent,
|
|
19
|
+
} from "react-native";
|
|
10
20
|
import type { MessageState, ThreadMessage } from "@assistant-ui/core";
|
|
11
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
RenderChildrenWithAccessor,
|
|
23
|
+
useAuiEvent,
|
|
24
|
+
useAuiState,
|
|
25
|
+
} from "@assistant-ui/store";
|
|
12
26
|
import { MessageByIndexProvider } from "@assistant-ui/core/react";
|
|
13
27
|
|
|
14
28
|
type MessageComponents =
|
|
@@ -44,13 +58,22 @@ type MessagesContent =
|
|
|
44
58
|
components?: never;
|
|
45
59
|
};
|
|
46
60
|
|
|
47
|
-
export type
|
|
61
|
+
export type ThreadMessagesFlatListProps = Omit<
|
|
48
62
|
FlatListProps<ThreadMessage>,
|
|
49
63
|
"data" | "renderItem" | "children"
|
|
50
64
|
> &
|
|
51
|
-
MessagesContent
|
|
65
|
+
MessagesContent & {
|
|
66
|
+
autoScroll?: boolean | undefined;
|
|
67
|
+
scrollToBottomOnRunStart?: boolean | undefined;
|
|
68
|
+
scrollToBottomOnInitialize?: boolean | undefined;
|
|
69
|
+
scrollToBottomOnThreadSwitch?: boolean | undefined;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** @deprecated Use ThreadMessagesFlatListProps instead. */
|
|
73
|
+
export type ThreadMessagesProps = ThreadMessagesFlatListProps;
|
|
52
74
|
|
|
53
75
|
const DEFAULT_SYSTEM_MESSAGE = () => null;
|
|
76
|
+
const AT_BOTTOM_THRESHOLD = 4;
|
|
54
77
|
|
|
55
78
|
const getComponent = (
|
|
56
79
|
components: MessageComponents,
|
|
@@ -155,36 +178,303 @@ const ThreadMessageByChildren = memo(
|
|
|
155
178
|
);
|
|
156
179
|
ThreadMessageByChildren.displayName = "ThreadPrimitive.MessageByChildren";
|
|
157
180
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
181
|
+
const setForwardedRef = <T,>(ref: ForwardedRef<T>, value: T | null) => {
|
|
182
|
+
if (typeof ref === "function") {
|
|
183
|
+
ref(value);
|
|
184
|
+
} else if (ref) {
|
|
185
|
+
ref.current = value;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
163
188
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
189
|
+
const useComposedFlatListRef = (
|
|
190
|
+
forwardedRef: ForwardedRef<FlatList<ThreadMessage>>,
|
|
191
|
+
) => {
|
|
192
|
+
const flatListRef = useRef<FlatList<ThreadMessage> | null>(null);
|
|
193
|
+
|
|
194
|
+
const setFlatListRef = useCallback(
|
|
195
|
+
(node: FlatList<ThreadMessage> | null) => {
|
|
196
|
+
flatListRef.current = node;
|
|
197
|
+
setForwardedRef(forwardedRef, node);
|
|
198
|
+
},
|
|
199
|
+
[forwardedRef],
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
return [flatListRef, setFlatListRef] as const;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const useThreadMessagesFlatListAutoScroll = ({
|
|
206
|
+
flatListRef,
|
|
207
|
+
hasMessages,
|
|
208
|
+
autoScroll = true,
|
|
209
|
+
scrollToBottomOnRunStart = true,
|
|
210
|
+
scrollToBottomOnInitialize = true,
|
|
211
|
+
scrollToBottomOnThreadSwitch = true,
|
|
212
|
+
}: {
|
|
213
|
+
flatListRef: RefObject<FlatList<ThreadMessage> | null>;
|
|
214
|
+
hasMessages: boolean;
|
|
215
|
+
autoScroll?: boolean | undefined;
|
|
216
|
+
scrollToBottomOnRunStart?: boolean | undefined;
|
|
217
|
+
scrollToBottomOnInitialize?: boolean | undefined;
|
|
218
|
+
scrollToBottomOnThreadSwitch?: boolean | undefined;
|
|
219
|
+
}) => {
|
|
220
|
+
const metricsRef = useRef({
|
|
221
|
+
contentHeight: 0,
|
|
222
|
+
viewportHeight: 0,
|
|
223
|
+
scrollY: 0,
|
|
224
|
+
});
|
|
225
|
+
const isAtBottomRef = useRef(true);
|
|
226
|
+
const lastScrollEventYRef = useRef(0);
|
|
227
|
+
const initializeScrollRequestedRef = useRef(false);
|
|
228
|
+
const pendingScrollToBottomRef = useRef<false | { animated: boolean }>(false);
|
|
229
|
+
|
|
230
|
+
const updateIsAtBottom = useCallback(() => {
|
|
231
|
+
const { contentHeight, scrollY, viewportHeight } = metricsRef.current;
|
|
232
|
+
isAtBottomRef.current =
|
|
233
|
+
contentHeight <= viewportHeight ||
|
|
234
|
+
contentHeight - scrollY - viewportHeight <= AT_BOTTOM_THRESHOLD;
|
|
235
|
+
}, []);
|
|
236
|
+
|
|
237
|
+
// Commanding a scroll records the intended position immediately; the
|
|
238
|
+
// native scroll echo is bridged and throttled, so waiting for it lets a
|
|
239
|
+
// fast stream observe stale metrics and drop out of following.
|
|
240
|
+
const scrollToBottom = useCallback(
|
|
241
|
+
(animated: boolean) => {
|
|
242
|
+
const { contentHeight, viewportHeight } = metricsRef.current;
|
|
243
|
+
metricsRef.current.scrollY = Math.max(0, contentHeight - viewportHeight);
|
|
244
|
+
isAtBottomRef.current = true;
|
|
245
|
+
flatListRef.current?.scrollToEnd({ animated });
|
|
246
|
+
},
|
|
247
|
+
[flatListRef],
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const handleLayout = useCallback(
|
|
251
|
+
(event: LayoutChangeEvent) => {
|
|
252
|
+
metricsRef.current.viewportHeight = event.nativeEvent.layout.height;
|
|
253
|
+
updateIsAtBottom();
|
|
254
|
+
},
|
|
255
|
+
[updateIsAtBottom],
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const handleScroll = useCallback(
|
|
259
|
+
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
260
|
+
const { contentOffset, contentSize, layoutMeasurement } =
|
|
261
|
+
event.nativeEvent;
|
|
262
|
+
const previousEventY = lastScrollEventYRef.current;
|
|
263
|
+
const wasPinnedToBottom = isAtBottomRef.current;
|
|
264
|
+
lastScrollEventYRef.current = contentOffset.y;
|
|
265
|
+
metricsRef.current = {
|
|
266
|
+
contentHeight: contentSize.height,
|
|
267
|
+
viewportHeight: layoutMeasurement.height,
|
|
268
|
+
scrollY: contentOffset.y,
|
|
269
|
+
};
|
|
270
|
+
updateIsAtBottom();
|
|
271
|
+
const upwardMove = contentOffset.y < previousEventY;
|
|
272
|
+
// Only a deliberate upward move unpins or cancels a pending scroll.
|
|
273
|
+
// Gestures are detected echo-to-echo because a commanded scroll
|
|
274
|
+
// optimistically moves the tracked position ahead of its ascending
|
|
275
|
+
// animation echoes, and those echoes must not unpin mid-flight.
|
|
276
|
+
if (wasPinnedToBottom && !upwardMove) {
|
|
277
|
+
isAtBottomRef.current = true;
|
|
278
|
+
}
|
|
279
|
+
if (!isAtBottomRef.current && upwardMove) {
|
|
280
|
+
pendingScrollToBottomRef.current = false;
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
[updateIsAtBottom],
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
const handleContentSizeChange = useCallback(
|
|
287
|
+
(_width: number, height: number) => {
|
|
288
|
+
const metrics = metricsRef.current;
|
|
289
|
+
const previousContentHeight = metrics.contentHeight;
|
|
290
|
+
const wasAtBottom = isAtBottomRef.current;
|
|
291
|
+
metrics.contentHeight = height;
|
|
292
|
+
updateIsAtBottom();
|
|
293
|
+
|
|
294
|
+
// FlatList.scrollToEnd is a no-op before the list has measured, so the
|
|
295
|
+
// initialize and thread-switch scrolls land on the next content-size
|
|
296
|
+
// event, once real metrics exist.
|
|
297
|
+
const pendingScroll = pendingScrollToBottomRef.current;
|
|
298
|
+
if (pendingScroll) {
|
|
299
|
+
pendingScrollToBottomRef.current = false;
|
|
300
|
+
scrollToBottom(pendingScroll.animated);
|
|
301
|
+
return;
|
|
172
302
|
}
|
|
173
|
-
|
|
303
|
+
|
|
304
|
+
if (!autoScroll) return;
|
|
305
|
+
if (!wasAtBottom) return;
|
|
306
|
+
if (previousContentHeight === 0) return;
|
|
307
|
+
if (height <= previousContentHeight) return;
|
|
308
|
+
|
|
309
|
+
scrollToBottom(false);
|
|
174
310
|
},
|
|
175
|
-
[
|
|
311
|
+
[autoScroll, scrollToBottom, updateIsAtBottom],
|
|
176
312
|
);
|
|
177
313
|
|
|
178
|
-
|
|
314
|
+
useEffect(() => {
|
|
315
|
+
if (!scrollToBottomOnInitialize) return;
|
|
316
|
+
if (!hasMessages) {
|
|
317
|
+
initializeScrollRequestedRef.current = false;
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (initializeScrollRequestedRef.current) return;
|
|
321
|
+
|
|
322
|
+
initializeScrollRequestedRef.current = true;
|
|
323
|
+
pendingScrollToBottomRef.current = { animated: false };
|
|
324
|
+
scrollToBottom(false);
|
|
325
|
+
}, [hasMessages, scrollToBottom, scrollToBottomOnInitialize]);
|
|
326
|
+
|
|
327
|
+
useAuiEvent("thread.runStart", () => {
|
|
328
|
+
if (!scrollToBottomOnRunStart) return;
|
|
329
|
+
pendingScrollToBottomRef.current = { animated: true };
|
|
330
|
+
scrollToBottom(true);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
useAuiEvent("threadListItem.switchedTo", () => {
|
|
334
|
+
if (!scrollToBottomOnThreadSwitch) return;
|
|
335
|
+
initializeScrollRequestedRef.current = false;
|
|
336
|
+
lastScrollEventYRef.current = 0;
|
|
337
|
+
pendingScrollToBottomRef.current = { animated: false };
|
|
338
|
+
scrollToBottom(false);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
handleLayout,
|
|
343
|
+
handleScroll,
|
|
344
|
+
handleContentSizeChange,
|
|
345
|
+
};
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
export const ThreadMessagesFlatList = forwardRef<
|
|
349
|
+
FlatList<ThreadMessage>,
|
|
350
|
+
ThreadMessagesFlatListProps
|
|
351
|
+
>(
|
|
352
|
+
(
|
|
353
|
+
{
|
|
354
|
+
autoScroll,
|
|
355
|
+
components,
|
|
356
|
+
children,
|
|
357
|
+
onContentSizeChange,
|
|
358
|
+
onLayout,
|
|
359
|
+
onScroll,
|
|
360
|
+
scrollEventThrottle,
|
|
361
|
+
scrollToBottomOnInitialize,
|
|
362
|
+
scrollToBottomOnRunStart,
|
|
363
|
+
scrollToBottomOnThreadSwitch,
|
|
364
|
+
...flatListProps
|
|
365
|
+
},
|
|
366
|
+
forwardedRef,
|
|
367
|
+
) => {
|
|
368
|
+
const messages = useAuiState((s) => s.thread.messages);
|
|
369
|
+
const [flatListRef, setFlatListRef] = useComposedFlatListRef(forwardedRef);
|
|
370
|
+
const {
|
|
371
|
+
handleContentSizeChange: handleAutoScrollContentSizeChange,
|
|
372
|
+
handleLayout: handleAutoScrollLayout,
|
|
373
|
+
handleScroll: handleAutoScrollScroll,
|
|
374
|
+
} = useThreadMessagesFlatListAutoScroll({
|
|
375
|
+
flatListRef,
|
|
376
|
+
hasMessages: messages.length > 0,
|
|
377
|
+
autoScroll,
|
|
378
|
+
scrollToBottomOnInitialize,
|
|
379
|
+
scrollToBottomOnRunStart,
|
|
380
|
+
scrollToBottomOnThreadSwitch,
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
const renderItem = useCallback(
|
|
384
|
+
({ index }: { item: ThreadMessage; index: number }) => {
|
|
385
|
+
if (children) {
|
|
386
|
+
return (
|
|
387
|
+
<ThreadMessageByChildren index={index}>
|
|
388
|
+
{children}
|
|
389
|
+
</ThreadMessageByChildren>
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
return <ThreadMessageByIndex index={index} components={components!} />;
|
|
393
|
+
},
|
|
394
|
+
[components, children],
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
const keyExtractor = useCallback((item: ThreadMessage) => item.id, []);
|
|
398
|
+
|
|
399
|
+
const scrollTracking =
|
|
400
|
+
(autoScroll ?? true) ||
|
|
401
|
+
(scrollToBottomOnInitialize ?? true) ||
|
|
402
|
+
(scrollToBottomOnRunStart ?? true) ||
|
|
403
|
+
(scrollToBottomOnThreadSwitch ?? true);
|
|
404
|
+
|
|
405
|
+
const handleLayout = useCallback(
|
|
406
|
+
(event: LayoutChangeEvent) => {
|
|
407
|
+
handleAutoScrollLayout(event);
|
|
408
|
+
onLayout?.(event);
|
|
409
|
+
},
|
|
410
|
+
[handleAutoScrollLayout, onLayout],
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
const handleScroll = useCallback(
|
|
414
|
+
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
415
|
+
handleAutoScrollScroll(event);
|
|
416
|
+
onScroll?.(event);
|
|
417
|
+
},
|
|
418
|
+
[handleAutoScrollScroll, onScroll],
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
const handleContentSizeChange = useCallback(
|
|
422
|
+
(width: number, height: number) => {
|
|
423
|
+
handleAutoScrollContentSizeChange(width, height);
|
|
424
|
+
onContentSizeChange?.(width, height);
|
|
425
|
+
},
|
|
426
|
+
[handleAutoScrollContentSizeChange, onContentSizeChange],
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
return (
|
|
430
|
+
<FlatList
|
|
431
|
+
ref={setFlatListRef}
|
|
432
|
+
data={messages as unknown as ThreadMessage[]}
|
|
433
|
+
renderItem={renderItem}
|
|
434
|
+
keyExtractor={keyExtractor}
|
|
435
|
+
{...(scrollTracking
|
|
436
|
+
? {
|
|
437
|
+
onContentSizeChange: handleContentSizeChange,
|
|
438
|
+
onLayout: handleLayout,
|
|
439
|
+
onScroll: handleScroll,
|
|
440
|
+
scrollEventThrottle: scrollEventThrottle ?? 16,
|
|
441
|
+
}
|
|
442
|
+
: {
|
|
443
|
+
...(onContentSizeChange && { onContentSizeChange }),
|
|
444
|
+
...(onLayout && { onLayout }),
|
|
445
|
+
...(onScroll && { onScroll }),
|
|
446
|
+
...(scrollEventThrottle !== undefined && { scrollEventThrottle }),
|
|
447
|
+
})}
|
|
448
|
+
{...flatListProps}
|
|
449
|
+
/>
|
|
450
|
+
);
|
|
451
|
+
},
|
|
452
|
+
);
|
|
453
|
+
ThreadMessagesFlatList.displayName = "ThreadPrimitive.MessagesFlatList";
|
|
179
454
|
|
|
180
|
-
|
|
181
|
-
|
|
455
|
+
/** @deprecated Use ThreadPrimitive.MessagesFlatList instead. */
|
|
456
|
+
export const ThreadMessages = forwardRef<
|
|
457
|
+
FlatList<ThreadMessage>,
|
|
458
|
+
ThreadMessagesProps
|
|
459
|
+
>(
|
|
460
|
+
(
|
|
461
|
+
{
|
|
462
|
+
autoScroll = false,
|
|
463
|
+
scrollToBottomOnInitialize = false,
|
|
464
|
+
scrollToBottomOnRunStart = false,
|
|
465
|
+
scrollToBottomOnThreadSwitch = false,
|
|
466
|
+
...props
|
|
467
|
+
},
|
|
468
|
+
ref,
|
|
469
|
+
) => (
|
|
470
|
+
<ThreadMessagesFlatList
|
|
182
471
|
ref={ref}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
{
|
|
472
|
+
autoScroll={autoScroll}
|
|
473
|
+
scrollToBottomOnInitialize={scrollToBottomOnInitialize}
|
|
474
|
+
scrollToBottomOnRunStart={scrollToBottomOnRunStart}
|
|
475
|
+
scrollToBottomOnThreadSwitch={scrollToBottomOnThreadSwitch}
|
|
476
|
+
{...props}
|
|
187
477
|
/>
|
|
188
|
-
)
|
|
189
|
-
|
|
478
|
+
),
|
|
479
|
+
);
|
|
190
480
|
ThreadMessages.displayName = "ThreadPrimitive.Messages";
|
package/src/primitives/thread.ts
CHANGED
|
@@ -3,6 +3,8 @@ export {
|
|
|
3
3
|
type ThreadRootProps as RootProps,
|
|
4
4
|
} from "./thread/ThreadRoot";
|
|
5
5
|
export {
|
|
6
|
+
ThreadMessagesFlatList as MessagesFlatList,
|
|
7
|
+
type ThreadMessagesFlatListProps as MessagesFlatListProps,
|
|
6
8
|
ThreadMessages as Messages,
|
|
7
9
|
type ThreadMessagesProps as MessagesProps,
|
|
8
10
|
} from "./thread/ThreadMessages";
|