@assistant-ui/react-native 0.1.40 → 0.1.42

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.
@@ -206,6 +206,7 @@ const useComposedFlatListRef = (
206
206
  const useThreadMessagesFlatListAutoScroll = ({
207
207
  flatListRef,
208
208
  hasMessages,
209
+ horizontal = false,
209
210
  autoScroll = true,
210
211
  scrollToBottomOnRunStart = true,
211
212
  scrollToBottomOnInitialize = true,
@@ -213,6 +214,7 @@ const useThreadMessagesFlatListAutoScroll = ({
213
214
  }: {
214
215
  flatListRef: RefObject<FlatList<ThreadMessage> | null>;
215
216
  hasMessages: boolean;
217
+ horizontal?: boolean | null | undefined;
216
218
  autoScroll?: boolean | undefined;
217
219
  scrollToBottomOnRunStart?: boolean | undefined;
218
220
  scrollToBottomOnInitialize?: boolean | undefined;
@@ -224,9 +226,16 @@ const useThreadMessagesFlatListAutoScroll = ({
224
226
  scrollY: 0,
225
227
  });
226
228
  const isAtBottomRef = useRef(true);
227
- const lastScrollEventYRef = useRef(0);
229
+ const lastScrollEventOffsetRef = useRef(0);
228
230
  const initializeScrollRequestedRef = useRef(false);
229
- const pendingScrollToBottomRef = useRef<false | { animated: boolean }>(false);
231
+ const contentSizeVersionRef = useRef(0);
232
+ const pendingScrollToBottomRef = useRef<
233
+ | false
234
+ | {
235
+ animated: boolean;
236
+ minimumContentSizeVersion: number;
237
+ }
238
+ >(false);
230
239
 
231
240
  const updateIsAtBottom = useCallback(() => {
232
241
  const { contentHeight, scrollY, viewportHeight } = metricsRef.current;
@@ -241,9 +250,10 @@ const useThreadMessagesFlatListAutoScroll = ({
241
250
  const scrollToBottom = useCallback(
242
251
  (animated: boolean) => {
243
252
  const { contentHeight, viewportHeight } = metricsRef.current;
244
- metricsRef.current.scrollY = Math.max(0, contentHeight - viewportHeight);
253
+ const offset = Math.max(0, contentHeight - viewportHeight);
254
+ metricsRef.current.scrollY = offset;
245
255
  isAtBottomRef.current = true;
246
- flatListRef.current?.scrollToEnd({ animated });
256
+ flatListRef.current?.scrollToOffset({ offset, animated });
247
257
  },
248
258
  [flatListRef],
249
259
  );
@@ -252,9 +262,21 @@ const useThreadMessagesFlatListAutoScroll = ({
252
262
  (event: LayoutChangeEvent) => {
253
263
  const wasAtBottom = isAtBottomRef.current;
254
264
  const previousViewportHeight = metricsRef.current.viewportHeight;
255
- const viewportHeight = event.nativeEvent.layout.height;
265
+ const viewportHeight = horizontal
266
+ ? event.nativeEvent.layout.width
267
+ : event.nativeEvent.layout.height;
256
268
  metricsRef.current.viewportHeight = viewportHeight;
257
269
  updateIsAtBottom();
270
+ const pending = pendingScrollToBottomRef.current;
271
+ if (
272
+ pending &&
273
+ contentSizeVersionRef.current >= pending.minimumContentSizeVersion &&
274
+ viewportHeight > 0
275
+ ) {
276
+ pendingScrollToBottomRef.current = false;
277
+ scrollToBottom(pending.animated);
278
+ return;
279
+ }
258
280
  if (!wasAtBottom) return;
259
281
  // Layout changes are never user gestures, so they must not unpin. Past
260
282
  // the first measurement, a viewport change while pinned re-commands the
@@ -265,29 +287,31 @@ const useThreadMessagesFlatListAutoScroll = ({
265
287
  previousViewportHeight !== 0 &&
266
288
  viewportHeight !== previousViewportHeight
267
289
  ) {
268
- const pending = pendingScrollToBottomRef.current;
269
290
  scrollToBottom(pending ? pending.animated : false);
270
291
  } else {
271
292
  isAtBottomRef.current = true;
272
293
  }
273
294
  },
274
- [autoScroll, scrollToBottom, updateIsAtBottom],
295
+ [autoScroll, horizontal, scrollToBottom, updateIsAtBottom],
275
296
  );
276
297
 
277
298
  const handleScroll = useCallback(
278
299
  (event: NativeSyntheticEvent<NativeScrollEvent>) => {
279
300
  const { contentOffset, contentSize, layoutMeasurement } =
280
301
  event.nativeEvent;
281
- const previousEventY = lastScrollEventYRef.current;
302
+ const scrollOffset = horizontal ? contentOffset.x : contentOffset.y;
303
+ const previousEventOffset = lastScrollEventOffsetRef.current;
282
304
  const wasPinnedToBottom = isAtBottomRef.current;
283
- lastScrollEventYRef.current = contentOffset.y;
305
+ lastScrollEventOffsetRef.current = scrollOffset;
284
306
  metricsRef.current = {
285
- contentHeight: contentSize.height,
286
- viewportHeight: layoutMeasurement.height,
287
- scrollY: contentOffset.y,
307
+ contentHeight: horizontal ? contentSize.width : contentSize.height,
308
+ viewportHeight: horizontal
309
+ ? layoutMeasurement.width
310
+ : layoutMeasurement.height,
311
+ scrollY: scrollOffset,
288
312
  };
289
313
  updateIsAtBottom();
290
- const upwardMove = contentOffset.y < previousEventY;
314
+ const upwardMove = scrollOffset < previousEventOffset;
291
315
  // Only a deliberate upward move unpins or cancels a pending scroll.
292
316
  // Gestures are detected echo-to-echo because a commanded scroll
293
317
  // optimistically moves the tracked position ahead of its ascending
@@ -299,22 +323,28 @@ const useThreadMessagesFlatListAutoScroll = ({
299
323
  pendingScrollToBottomRef.current = false;
300
324
  }
301
325
  },
302
- [updateIsAtBottom],
326
+ [horizontal, updateIsAtBottom],
303
327
  );
304
328
 
305
329
  const handleContentSizeChange = useCallback(
306
- (_width: number, height: number) => {
330
+ (width: number, height: number) => {
307
331
  const metrics = metricsRef.current;
332
+ const contentHeight = horizontal ? width : height;
308
333
  const previousContentHeight = metrics.contentHeight;
309
334
  const wasAtBottom = isAtBottomRef.current;
310
- metrics.contentHeight = height;
335
+ if (contentHeight > 0) contentSizeVersionRef.current += 1;
336
+ metrics.contentHeight = contentHeight;
311
337
  updateIsAtBottom();
312
338
 
313
- // FlatList.scrollToEnd is a no-op before the list has measured, so the
314
- // initialize and thread-switch scrolls land on the next content-size
315
- // event, once real metrics exist.
339
+ // Initialize and thread-switch requests are repeated after the list has
340
+ // measured so the explicit bottom offset uses real content metrics.
316
341
  const pendingScroll = pendingScrollToBottomRef.current;
317
- if (pendingScroll) {
342
+ if (
343
+ pendingScroll &&
344
+ contentSizeVersionRef.current >=
345
+ pendingScroll.minimumContentSizeVersion &&
346
+ metrics.viewportHeight > 0
347
+ ) {
318
348
  pendingScrollToBottomRef.current = false;
319
349
  scrollToBottom(pendingScroll.animated);
320
350
  return;
@@ -323,11 +353,11 @@ const useThreadMessagesFlatListAutoScroll = ({
323
353
  if (!autoScroll) return;
324
354
  if (!wasAtBottom) return;
325
355
  if (previousContentHeight === 0) return;
326
- if (height <= previousContentHeight) return;
356
+ if (contentHeight <= previousContentHeight) return;
327
357
 
328
358
  scrollToBottom(false);
329
359
  },
330
- [autoScroll, scrollToBottom, updateIsAtBottom],
360
+ [autoScroll, horizontal, scrollToBottom, updateIsAtBottom],
331
361
  );
332
362
 
333
363
  useEffect(() => {
@@ -339,21 +369,30 @@ const useThreadMessagesFlatListAutoScroll = ({
339
369
  if (initializeScrollRequestedRef.current) return;
340
370
 
341
371
  initializeScrollRequestedRef.current = true;
342
- pendingScrollToBottomRef.current = { animated: false };
372
+ pendingScrollToBottomRef.current = {
373
+ animated: false,
374
+ minimumContentSizeVersion: Math.max(1, contentSizeVersionRef.current),
375
+ };
343
376
  scrollToBottom(false);
344
377
  }, [hasMessages, scrollToBottom, scrollToBottomOnInitialize]);
345
378
 
346
379
  useAuiEvent("thread.runStart", () => {
347
380
  if (!scrollToBottomOnRunStart) return;
348
- pendingScrollToBottomRef.current = { animated: true };
381
+ pendingScrollToBottomRef.current = {
382
+ animated: true,
383
+ minimumContentSizeVersion: contentSizeVersionRef.current + 1,
384
+ };
349
385
  scrollToBottom(true);
350
386
  });
351
387
 
352
388
  useAuiEvent("threads.selectionChanged", () => {
353
389
  if (!scrollToBottomOnThreadSwitch) return;
354
390
  initializeScrollRequestedRef.current = false;
355
- lastScrollEventYRef.current = 0;
356
- pendingScrollToBottomRef.current = { animated: false };
391
+ lastScrollEventOffsetRef.current = 0;
392
+ pendingScrollToBottomRef.current = {
393
+ animated: false,
394
+ minimumContentSizeVersion: contentSizeVersionRef.current + 1,
395
+ };
357
396
  scrollToBottom(false);
358
397
  });
359
398
 
@@ -393,6 +432,7 @@ export const ThreadMessagesFlatList = forwardRef<
393
432
  } = useThreadMessagesFlatListAutoScroll({
394
433
  flatListRef,
395
434
  hasMessages: messages.length > 0,
435
+ horizontal: flatListProps.horizontal,
396
436
  autoScroll,
397
437
  scrollToBottomOnInitialize,
398
438
  scrollToBottomOnRunStart,
@@ -451,6 +491,7 @@ export const ThreadMessagesFlatList = forwardRef<
451
491
  data={messages as unknown as ThreadMessage[]}
452
492
  renderItem={renderItem}
453
493
  keyExtractor={keyExtractor}
494
+ maintainVisibleContentPosition={{ minIndexForVisible: 0 }}
454
495
  {...(scrollTracking
455
496
  ? {
456
497
  onContentSizeChange: handleContentSizeChange,