@assistant-ui/react-native 0.1.43 → 0.1.44

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.
Files changed (188) hide show
  1. package/dist/context/AssistantContext.d.ts +2 -2
  2. package/dist/context/AssistantContext.d.ts.map +1 -0
  3. package/dist/context/providers/AttachmentByIndexProvider.d.ts +2 -2
  4. package/dist/context/providers/AttachmentByIndexProvider.d.ts.map +1 -0
  5. package/dist/context/providers/RuntimeAdapterProvider.d.ts +2 -2
  6. package/dist/context/providers/RuntimeAdapterProvider.d.ts.map +1 -0
  7. package/dist/context/providers/ThreadListItemRuntimeProvider.d.ts +2 -2
  8. package/dist/context/providers/ThreadListItemRuntimeProvider.d.ts.map +1 -0
  9. package/dist/index.d.ts +54 -23
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +4 -4
  12. package/dist/internal.d.ts +4 -3
  13. package/dist/internal.d.ts.map +1 -0
  14. package/dist/primitives/actionBar/ActionBarCopy.d.ts +7 -9
  15. package/dist/primitives/actionBar/ActionBarCopy.d.ts.map +1 -1
  16. package/dist/primitives/actionBar/ActionBarEdit.d.ts +5 -7
  17. package/dist/primitives/actionBar/ActionBarEdit.d.ts.map +1 -1
  18. package/dist/primitives/actionBar/ActionBarFeedbackNegative.d.ts +6 -8
  19. package/dist/primitives/actionBar/ActionBarFeedbackNegative.d.ts.map +1 -1
  20. package/dist/primitives/actionBar/ActionBarFeedbackNegative.js +28 -20
  21. package/dist/primitives/actionBar/ActionBarFeedbackNegative.js.map +1 -1
  22. package/dist/primitives/actionBar/ActionBarFeedbackPositive.d.ts +6 -8
  23. package/dist/primitives/actionBar/ActionBarFeedbackPositive.d.ts.map +1 -1
  24. package/dist/primitives/actionBar/ActionBarFeedbackPositive.js +28 -20
  25. package/dist/primitives/actionBar/ActionBarFeedbackPositive.js.map +1 -1
  26. package/dist/primitives/actionBar/ActionBarReload.d.ts +5 -7
  27. package/dist/primitives/actionBar/ActionBarReload.d.ts.map +1 -1
  28. package/dist/primitives/actionBar/ActionBarSpeak.d.ts +7 -9
  29. package/dist/primitives/actionBar/ActionBarSpeak.d.ts.map +1 -1
  30. package/dist/primitives/actionBar/ActionBarStopSpeaking.d.ts +7 -9
  31. package/dist/primitives/actionBar/ActionBarStopSpeaking.d.ts.map +1 -1
  32. package/dist/primitives/actionBar.d.ts +7 -12
  33. package/dist/primitives/actionBar.d.ts.map +1 -1
  34. package/dist/primitives/attachment/AttachmentName.d.ts +2 -4
  35. package/dist/primitives/attachment/AttachmentName.d.ts.map +1 -1
  36. package/dist/primitives/attachment/AttachmentRemove.d.ts +2 -4
  37. package/dist/primitives/attachment/AttachmentRemove.d.ts.map +1 -1
  38. package/dist/primitives/attachment/AttachmentRoot.d.ts +3 -5
  39. package/dist/primitives/attachment/AttachmentRoot.d.ts.map +1 -1
  40. package/dist/primitives/attachment/AttachmentThumb.d.ts +2 -4
  41. package/dist/primitives/attachment/AttachmentThumb.d.ts.map +1 -1
  42. package/dist/primitives/attachment.d.ts +4 -9
  43. package/dist/primitives/attachment.d.ts.map +1 -1
  44. package/dist/primitives/branchPicker/BranchPickerCount.d.ts +1 -3
  45. package/dist/primitives/branchPicker/BranchPickerCount.d.ts.map +1 -1
  46. package/dist/primitives/branchPicker/BranchPickerNext.d.ts +5 -7
  47. package/dist/primitives/branchPicker/BranchPickerNext.d.ts.map +1 -1
  48. package/dist/primitives/branchPicker/BranchPickerNumber.d.ts +1 -3
  49. package/dist/primitives/branchPicker/BranchPickerNumber.d.ts.map +1 -1
  50. package/dist/primitives/branchPicker/BranchPickerPrevious.d.ts +5 -7
  51. package/dist/primitives/branchPicker/BranchPickerPrevious.d.ts.map +1 -1
  52. package/dist/primitives/branchPicker.d.ts +4 -9
  53. package/dist/primitives/branchPicker.d.ts.map +1 -1
  54. package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.d.ts +2 -4
  55. package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.d.ts.map +1 -1
  56. package/dist/primitives/chainOfThought/ChainOfThoughtRoot.d.ts +3 -5
  57. package/dist/primitives/chainOfThought/ChainOfThoughtRoot.d.ts.map +1 -1
  58. package/dist/primitives/chainOfThought.d.ts +3 -8
  59. package/dist/primitives/chainOfThought.d.ts.map +1 -1
  60. package/dist/primitives/composer/ComposerAddAttachment.d.ts +2 -4
  61. package/dist/primitives/composer/ComposerAddAttachment.d.ts.map +1 -1
  62. package/dist/primitives/composer/ComposerAttachments.d.ts +1 -3
  63. package/dist/primitives/composer/ComposerAttachments.d.ts.map +1 -1
  64. package/dist/primitives/composer/ComposerCancel.d.ts +2 -4
  65. package/dist/primitives/composer/ComposerCancel.d.ts.map +1 -1
  66. package/dist/primitives/composer/ComposerIf.d.ts +2 -2
  67. package/dist/primitives/composer/ComposerIf.d.ts.map +1 -0
  68. package/dist/primitives/composer/ComposerInput.d.ts +8 -10
  69. package/dist/primitives/composer/ComposerInput.d.ts.map +1 -1
  70. package/dist/primitives/composer/ComposerInput.js +1 -1
  71. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  72. package/dist/primitives/composer/ComposerQueue.d.ts +2 -2
  73. package/dist/primitives/composer/ComposerQueue.d.ts.map +1 -0
  74. package/dist/primitives/composer/ComposerQuote.d.ts +3 -5
  75. package/dist/primitives/composer/ComposerQuote.d.ts.map +1 -1
  76. package/dist/primitives/composer/ComposerQuoteDismiss.d.ts +2 -4
  77. package/dist/primitives/composer/ComposerQuoteDismiss.d.ts.map +1 -1
  78. package/dist/primitives/composer/ComposerQuoteText.d.ts +3 -5
  79. package/dist/primitives/composer/ComposerQuoteText.d.ts.map +1 -1
  80. package/dist/primitives/composer/ComposerRoot.d.ts +3 -5
  81. package/dist/primitives/composer/ComposerRoot.d.ts.map +1 -1
  82. package/dist/primitives/composer/ComposerSend.d.ts +2 -4
  83. package/dist/primitives/composer/ComposerSend.d.ts.map +1 -1
  84. package/dist/primitives/composer.d.ts +11 -16
  85. package/dist/primitives/composer.d.ts.map +1 -1
  86. package/dist/primitives/error/ErrorMessage.d.ts +5 -7
  87. package/dist/primitives/error/ErrorMessage.d.ts.map +1 -1
  88. package/dist/primitives/error/ErrorRoot.d.ts +5 -7
  89. package/dist/primitives/error/ErrorRoot.d.ts.map +1 -1
  90. package/dist/primitives/error/index.d.ts +2 -7
  91. package/dist/primitives/error/index.d.ts.map +1 -1
  92. package/dist/primitives/message/MessageAttachments.d.ts +1 -3
  93. package/dist/primitives/message/MessageAttachments.d.ts.map +1 -1
  94. package/dist/primitives/message/MessageContent.d.ts +46 -47
  95. package/dist/primitives/message/MessageContent.d.ts.map +1 -1
  96. package/dist/primitives/message/MessageContent.js +27 -21
  97. package/dist/primitives/message/MessageContent.js.map +1 -1
  98. package/dist/primitives/message/MessageIf.d.ts +7 -9
  99. package/dist/primitives/message/MessageIf.d.ts.map +1 -1
  100. package/dist/primitives/message/MessageParts.d.ts +4 -6
  101. package/dist/primitives/message/MessageParts.d.ts.map +1 -1
  102. package/dist/primitives/message/MessageRoot.d.ts +3 -5
  103. package/dist/primitives/message/MessageRoot.d.ts.map +1 -1
  104. package/dist/primitives/message.d.ts +6 -11
  105. package/dist/primitives/message.d.ts.map +1 -1
  106. package/dist/primitives/messagePart/MessagePartImage.d.ts +4 -6
  107. package/dist/primitives/messagePart/MessagePartImage.d.ts.map +1 -1
  108. package/dist/primitives/messagePart/MessagePartInProgress.d.ts +2 -2
  109. package/dist/primitives/messagePart/MessagePartInProgress.d.ts.map +1 -0
  110. package/dist/primitives/messagePart/MessagePartText.d.ts +4 -6
  111. package/dist/primitives/messagePart/MessagePartText.d.ts.map +1 -1
  112. package/dist/primitives/messagePart.d.ts +4 -9
  113. package/dist/primitives/messagePart.d.ts.map +1 -1
  114. package/dist/primitives/queueItem/QueueItemRemove.d.ts +2 -4
  115. package/dist/primitives/queueItem/QueueItemRemove.d.ts.map +1 -1
  116. package/dist/primitives/queueItem/QueueItemSteer.d.ts +2 -4
  117. package/dist/primitives/queueItem/QueueItemSteer.d.ts.map +1 -1
  118. package/dist/primitives/queueItem/QueueItemText.d.ts +3 -5
  119. package/dist/primitives/queueItem/QueueItemText.d.ts.map +1 -1
  120. package/dist/primitives/queueItem.d.ts +3 -8
  121. package/dist/primitives/queueItem.d.ts.map +1 -1
  122. package/dist/primitives/suggestion/SuggestionDescription.d.ts +3 -5
  123. package/dist/primitives/suggestion/SuggestionDescription.d.ts.map +1 -1
  124. package/dist/primitives/suggestion/SuggestionTitle.d.ts +3 -5
  125. package/dist/primitives/suggestion/SuggestionTitle.d.ts.map +1 -1
  126. package/dist/primitives/suggestion/SuggestionTrigger.d.ts +13 -15
  127. package/dist/primitives/suggestion/SuggestionTrigger.d.ts.map +1 -1
  128. package/dist/primitives/suggestion/SuggestionTrigger.js +1 -1
  129. package/dist/primitives/suggestion.d.ts +3 -8
  130. package/dist/primitives/suggestion.d.ts.map +1 -1
  131. package/dist/primitives/thread/ThreadEmpty.d.ts +2 -4
  132. package/dist/primitives/thread/ThreadEmpty.d.ts.map +1 -1
  133. package/dist/primitives/thread/ThreadIf.d.ts +4 -6
  134. package/dist/primitives/thread/ThreadIf.d.ts.map +1 -1
  135. package/dist/primitives/thread/ThreadMessages.d.ts +37 -37
  136. package/dist/primitives/thread/ThreadMessages.d.ts.map +1 -1
  137. package/dist/primitives/thread/ThreadMessages.js +70 -58
  138. package/dist/primitives/thread/ThreadMessages.js.map +1 -1
  139. package/dist/primitives/thread/ThreadRoot.d.ts +3 -5
  140. package/dist/primitives/thread/ThreadRoot.d.ts.map +1 -1
  141. package/dist/primitives/thread/ThreadSuggestion.d.ts +15 -17
  142. package/dist/primitives/thread/ThreadSuggestion.d.ts.map +1 -1
  143. package/dist/primitives/thread.d.ts +8 -11
  144. package/dist/primitives/thread.d.ts.map +1 -1
  145. package/dist/primitives/threadList/ThreadListItems.d.ts +6 -8
  146. package/dist/primitives/threadList/ThreadListItems.d.ts.map +1 -1
  147. package/dist/primitives/threadList/ThreadListItems.js +1 -1
  148. package/dist/primitives/threadList/ThreadListNew.d.ts +5 -7
  149. package/dist/primitives/threadList/ThreadListNew.d.ts.map +1 -1
  150. package/dist/primitives/threadList/ThreadListNew.js +21 -17
  151. package/dist/primitives/threadList/ThreadListNew.js.map +1 -1
  152. package/dist/primitives/threadList/ThreadListRoot.d.ts +3 -5
  153. package/dist/primitives/threadList/ThreadListRoot.d.ts.map +1 -1
  154. package/dist/primitives/threadList.d.ts +3 -8
  155. package/dist/primitives/threadList.d.ts.map +1 -1
  156. package/dist/primitives/threadListItem/ThreadListItemArchive.d.ts +2 -4
  157. package/dist/primitives/threadListItem/ThreadListItemArchive.d.ts.map +1 -1
  158. package/dist/primitives/threadListItem/ThreadListItemDelete.d.ts +2 -4
  159. package/dist/primitives/threadListItem/ThreadListItemDelete.d.ts.map +1 -1
  160. package/dist/primitives/threadListItem/ThreadListItemRoot.d.ts +3 -5
  161. package/dist/primitives/threadListItem/ThreadListItemRoot.d.ts.map +1 -1
  162. package/dist/primitives/threadListItem/ThreadListItemTrigger.d.ts +5 -7
  163. package/dist/primitives/threadListItem/ThreadListItemTrigger.d.ts.map +1 -1
  164. package/dist/primitives/threadListItem/ThreadListItemTrigger.js +21 -17
  165. package/dist/primitives/threadListItem/ThreadListItemTrigger.js.map +1 -1
  166. package/dist/primitives/threadListItem/ThreadListItemUnarchive.d.ts +2 -4
  167. package/dist/primitives/threadListItem/ThreadListItemUnarchive.d.ts.map +1 -1
  168. package/dist/primitives/threadListItem.d.ts +6 -11
  169. package/dist/primitives/threadListItem.d.ts.map +1 -1
  170. package/dist/runtimes/useLocalRuntime.d.ts +2 -2
  171. package/dist/runtimes/useLocalRuntime.d.ts.map +1 -0
  172. package/dist/runtimes/useRemoteThreadListRuntime.d.ts +2 -2
  173. package/dist/runtimes/useRemoteThreadListRuntime.d.ts.map +1 -0
  174. package/package.json +7 -7
  175. package/src/index.ts +90 -1
  176. package/src/primitives/actionBar/ActionBar.test.tsx +76 -0
  177. package/src/primitives/actionBar/ActionBarFeedbackNegative.tsx +4 -1
  178. package/src/primitives/actionBar/ActionBarFeedbackPositive.tsx +4 -1
  179. package/src/primitives/composer/ComposerInput.test.tsx +17 -1
  180. package/src/primitives/composer/ComposerInput.tsx +6 -1
  181. package/src/primitives/message/MessageContent.test.tsx +64 -3
  182. package/src/primitives/message/MessageContent.tsx +5 -3
  183. package/src/primitives/thread/ThreadMessages.test.tsx +111 -5
  184. package/src/primitives/thread/ThreadMessages.tsx +41 -11
  185. package/src/primitives/threadList/ThreadListNew.test.tsx +35 -8
  186. package/src/primitives/threadList/ThreadListNew.tsx +4 -1
  187. package/src/primitives/threadListItem/ThreadListItemTrigger.test.tsx +32 -6
  188. package/src/primitives/threadListItem/ThreadListItemTrigger.tsx +4 -1
@@ -81,6 +81,9 @@ const ToolUIDisplay = ({
81
81
  addResult={partMethods.addToolResult}
82
82
  resume={partMethods.resumeToolCall}
83
83
  respondToApproval={partMethods.respondToToolApproval}
84
+ {...(partMethods.unstable_recordInteraction && {
85
+ unstable_recordInteraction: partMethods.unstable_recordInteraction,
86
+ })}
84
87
  />
85
88
  );
86
89
  }
@@ -103,9 +106,8 @@ const DataUIDisplay = ({
103
106
  index: number;
104
107
  }) => {
105
108
  const Render = useAuiState((s) => {
106
- const renders = s.dataRenderers.renderers[part.name];
107
- if (Array.isArray(renders)) return renders[0];
108
- return renders;
109
+ const named = s.dataRenderers.renderers[part.name]?.[0];
110
+ return named ?? s.dataRenderers.fallbacks[0];
109
111
  });
110
112
  if (Render) return <Render {...(part as DataMessagePartProps)} />;
111
113
  if (Fallback) return <Fallback part={part} index={index} />;
@@ -3,6 +3,7 @@ import { createRoot, type Root } from "react-dom/client";
3
3
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
4
  import type { FlatList, FlatListProps } from "react-native";
5
5
  import type { ThreadMessage } from "@assistant-ui/core";
6
+ import type { MessageState } from "@assistant-ui/core/store";
6
7
  import { ThreadMessages, ThreadMessagesFlatList } from "./ThreadMessages";
7
8
 
8
9
  type Msg = { id: string; role: string };
@@ -12,7 +13,7 @@ const h = vi.hoisted(() => ({
12
13
  thread: { messages: [] as Msg[] },
13
14
  message: { role: "user" as string, composer: { isEditing: false } },
14
15
  },
15
- itemState: { role: "user" } as { role: string },
16
+ itemState: { role: "user" } as { role: string; parts?: unknown[] },
16
17
  events: {} as Record<string, Set<() => void>>,
17
18
  flatListProps: null as Record<string, unknown> | null,
18
19
  scrollToOffset: vi.fn(),
@@ -303,13 +304,15 @@ describe("ThreadMessages", () => {
303
304
  describe("children mode", () => {
304
305
  it("renders via the children render prop", async () => {
305
306
  h.state.thread.messages = [{ id: "1", role: "user" }];
306
- h.itemState = { role: "user" };
307
- const children = vi.fn(({ message }: { message: { role: string } }) => (
308
- <span data-testid="child">child:{message.role}</span>
307
+ h.itemState = { role: "user", parts: [] };
308
+ const children = vi.fn(({ message }: { message: MessageState }) => (
309
+ <span data-testid="child">
310
+ child:{message.role}:{message.parts.length}
311
+ </span>
309
312
  ));
310
313
  await mount({ children });
311
314
  const el = container.querySelector('[data-testid="child"]');
312
- expect(el?.textContent).toBe("child:user");
315
+ expect(el?.textContent).toBe("child:user:0");
313
316
  expect(children).toHaveBeenCalled();
314
317
  });
315
318
  });
@@ -420,6 +423,109 @@ describe("ThreadMessages", () => {
420
423
  expect(getFlatListProps().onStartReached).toBeUndefined();
421
424
  });
422
425
 
426
+ it("loads one page when start reached fires again before rerender", async () => {
427
+ const loadMore = vi.fn();
428
+ const callerOnStartReached = vi.fn();
429
+
430
+ await mountFlatList({
431
+ components: messageComponents,
432
+ history: { hasMore: true, isLoadingMore: false, loadMore },
433
+ onStartReached: callerOnStartReached,
434
+ });
435
+ const installedOnStartReached = getFlatListProps().onStartReached;
436
+
437
+ installedOnStartReached?.({ distanceFromStart: 0 });
438
+ installedOnStartReached?.({ distanceFromStart: 0 });
439
+
440
+ expect(loadMore).toHaveBeenCalledOnce();
441
+ expect(callerOnStartReached).toHaveBeenCalledTimes(2);
442
+ });
443
+
444
+ it("loads again after a commit without a loading transition", async () => {
445
+ const loadMore = vi.fn();
446
+ const history = { hasMore: true, isLoadingMore: false, loadMore };
447
+
448
+ await mountFlatList({ components: messageComponents, history });
449
+ getFlatListProps().onStartReached?.({ distanceFromStart: 0 });
450
+
451
+ await mountFlatList({ components: messageComponents, history });
452
+ getFlatListProps().onStartReached?.({ distanceFromStart: 0 });
453
+
454
+ expect(loadMore).toHaveBeenCalledTimes(2);
455
+ });
456
+
457
+ it("does not carry a request latch to another history source", async () => {
458
+ const firstLoadMore = vi.fn();
459
+ const secondLoadMore = vi.fn();
460
+
461
+ await mountFlatList({
462
+ components: messageComponents,
463
+ history: {
464
+ hasMore: true,
465
+ isLoadingMore: false,
466
+ loadMore: firstLoadMore,
467
+ },
468
+ });
469
+ getFlatListProps().onStartReached?.({ distanceFromStart: 0 });
470
+
471
+ await mountFlatList({
472
+ components: messageComponents,
473
+ history: {
474
+ hasMore: true,
475
+ isLoadingMore: false,
476
+ loadMore: secondLoadMore,
477
+ },
478
+ });
479
+ getFlatListProps().onStartReached?.({ distanceFromStart: 0 });
480
+
481
+ expect(firstLoadMore).toHaveBeenCalledOnce();
482
+ expect(secondLoadMore).toHaveBeenCalledOnce();
483
+ });
484
+
485
+ it("loads again after the previous history request settles", async () => {
486
+ const loadMore = vi.fn();
487
+
488
+ await mountFlatList({
489
+ components: messageComponents,
490
+ history: { hasMore: true, isLoadingMore: false, loadMore },
491
+ });
492
+ getFlatListProps().onStartReached?.({ distanceFromStart: 0 });
493
+
494
+ await mountFlatList({
495
+ components: messageComponents,
496
+ history: { hasMore: true, isLoadingMore: true, loadMore },
497
+ });
498
+ await mountFlatList({
499
+ components: messageComponents,
500
+ history: { hasMore: true, isLoadingMore: false, loadMore },
501
+ });
502
+ getFlatListProps().onStartReached?.({ distanceFromStart: 0 });
503
+
504
+ expect(loadMore).toHaveBeenCalledTimes(2);
505
+ });
506
+
507
+ it("allows retrying when loadMore throws synchronously", async () => {
508
+ const loadError = new Error("load failed");
509
+ const loadMore = vi
510
+ .fn()
511
+ .mockImplementationOnce(() => {
512
+ throw loadError;
513
+ })
514
+ .mockImplementationOnce(() => undefined);
515
+
516
+ await mountFlatList({
517
+ components: messageComponents,
518
+ history: { hasMore: true, isLoadingMore: false, loadMore },
519
+ });
520
+ const onStartReached = getFlatListProps().onStartReached;
521
+
522
+ expect(() => onStartReached?.({ distanceFromStart: 0 })).toThrow(
523
+ loadError,
524
+ );
525
+ expect(() => onStartReached?.({ distanceFromStart: 0 })).not.toThrow();
526
+ expect(loadMore).toHaveBeenCalledTimes(2);
527
+ });
528
+
423
529
  it("defaults the history threshold and preserves a caller override", async () => {
424
530
  const history = {
425
531
  hasMore: true,
@@ -17,7 +17,8 @@ import {
17
17
  type NativeScrollEvent,
18
18
  type NativeSyntheticEvent,
19
19
  } from "react-native";
20
- import type { MessageState, ThreadMessage } from "@assistant-ui/core";
20
+ import type { ThreadMessage } from "@assistant-ui/core";
21
+ import type { MessageState } from "@assistant-ui/core/store";
21
22
  import {
22
23
  RenderChildrenWithAccessor,
23
24
  useAuiEvent,
@@ -82,6 +83,42 @@ export type ThreadMessagesProps = ThreadMessagesFlatListProps;
82
83
  const DEFAULT_SYSTEM_MESSAGE = () => null;
83
84
  const AT_BOTTOM_THRESHOLD = 4;
84
85
 
86
+ const useHistoryLoad = (
87
+ history: ThreadMessagesFlatListProps["history"],
88
+ onStartReached: FlatListProps<ThreadMessage>["onStartReached"],
89
+ ) => {
90
+ const loadRequestedRef = useRef(false);
91
+ const hasMore = history?.hasMore ?? false;
92
+ const isLoadingMore = history?.isLoadingMore ?? false;
93
+
94
+ // The latch is scoped to one commit so a no-op load cannot disable paging.
95
+ useEffect(() => {
96
+ loadRequestedRef.current = isLoadingMore;
97
+ });
98
+
99
+ const handleStartReached = useCallback<
100
+ NonNullable<FlatListProps<ThreadMessage>["onStartReached"]>
101
+ >(
102
+ (info) => {
103
+ onStartReached?.(info);
104
+ if (loadRequestedRef.current) return;
105
+ loadRequestedRef.current = true;
106
+ try {
107
+ history?.loadMore();
108
+ } catch (error) {
109
+ loadRequestedRef.current = false;
110
+ throw error;
111
+ }
112
+ },
113
+ [history, onStartReached],
114
+ );
115
+
116
+ return {
117
+ canLoadMore: hasMore && !isLoadingMore,
118
+ handleStartReached,
119
+ };
120
+ };
121
+
85
122
  const getComponent = (
86
123
  components: MessageComponents,
87
124
  role: ThreadMessage["role"],
@@ -495,16 +532,9 @@ export const ThreadMessagesFlatList = forwardRef<
495
532
  [handleAutoScrollContentSizeChange, onContentSizeChange],
496
533
  );
497
534
 
498
- const canLoadMore = history?.hasMore && !history.isLoadingMore;
499
-
500
- const handleStartReached = useCallback<
501
- NonNullable<FlatListProps<ThreadMessage>["onStartReached"]>
502
- >(
503
- (info) => {
504
- onStartReached?.(info);
505
- history?.loadMore();
506
- },
507
- [history, onStartReached],
535
+ const { canLoadMore, handleStartReached } = useHistoryLoad(
536
+ history,
537
+ onStartReached,
508
538
  );
509
539
 
510
540
  return (
@@ -6,6 +6,7 @@ import { ThreadListNew } from "./ThreadListNew";
6
6
  const h = vi.hoisted(() => ({
7
7
  switchToNewThread: vi.fn<() => void>(),
8
8
  state: { threads: { newThreadId: "new", mainThreadId: "other" } },
9
+ platform: { os: "web" },
9
10
  pressableProps: null as Record<string, unknown> | null,
10
11
  }));
11
12
 
@@ -41,7 +42,16 @@ vi.mock("react-native", async (importOriginal) => {
41
42
  );
42
43
  });
43
44
 
44
- return { ...actual, Pressable: PressableMock };
45
+ return {
46
+ ...actual,
47
+ Platform: {
48
+ ...actual.Platform,
49
+ get OS() {
50
+ return h.platform.os;
51
+ },
52
+ },
53
+ Pressable: PressableMock,
54
+ };
45
55
  });
46
56
 
47
57
  (globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
@@ -58,6 +68,7 @@ describe("ThreadListNew", () => {
58
68
  beforeEach(() => {
59
69
  h.switchToNewThread.mockReset();
60
70
  h.state.threads = { newThreadId: "new", mainThreadId: "other" };
71
+ h.platform.os = "web";
61
72
  h.pressableProps = null;
62
73
 
63
74
  container = document.createElement("div");
@@ -110,39 +121,55 @@ describe("ThreadListNew", () => {
110
121
  expect(h.switchToNewThread).not.toHaveBeenCalled();
111
122
  });
112
123
 
113
- it("marks itself selected while the new thread is the current one", async () => {
124
+ it("marks itself current on the web while the new thread is current", async () => {
114
125
  h.state.threads = { newThreadId: "new", mainThreadId: "new" };
115
126
 
116
- await mount();
127
+ const el = await mount();
117
128
 
129
+ expect(el.getAttribute("aria-current")).toBe("true");
130
+ expect(el.hasAttribute("aria-selected")).toBe(false);
118
131
  expect(accessibilityState()).toMatchObject({ selected: true });
119
132
  });
120
133
 
121
- it("is not selected while another thread is current", async () => {
122
- await mount();
134
+ it("is not current while another thread is current", async () => {
135
+ const el = await mount();
123
136
 
137
+ expect(el.hasAttribute("aria-current")).toBe(false);
124
138
  expect(accessibilityState()).toMatchObject({ selected: false });
125
139
  });
126
140
 
141
+ it("marks itself selected without aria-current on native", async () => {
142
+ h.platform.os = "ios";
143
+ h.state.threads = { newThreadId: "new", mainThreadId: "new" };
144
+
145
+ const el = await mount();
146
+
147
+ expect(el.hasAttribute("aria-current")).toBe(false);
148
+ expect(accessibilityState()).toMatchObject({ selected: true });
149
+ });
150
+
127
151
  it("keeps other accessibility state the caller passes", async () => {
128
152
  h.state.threads = { newThreadId: "new", mainThreadId: "new" };
129
153
 
130
- await mount({ accessibilityState: { busy: true } });
154
+ const el = await mount({ accessibilityState: { busy: true } });
131
155
 
156
+ expect(el.getAttribute("aria-current")).toBe("true");
132
157
  expect(accessibilityState()).toMatchObject({ selected: true, busy: true });
133
158
  });
134
159
 
135
160
  it("lets the caller override the selected state", async () => {
136
161
  h.state.threads = { newThreadId: "new", mainThreadId: "new" };
137
162
 
138
- await mount({ accessibilityState: { selected: false } });
163
+ const el = await mount({ accessibilityState: { selected: false } });
139
164
 
165
+ expect(el.hasAttribute("aria-current")).toBe(false);
140
166
  expect(accessibilityState()).toMatchObject({ selected: false });
141
167
  });
142
168
 
143
169
  it("lets the caller select a control the state reports inactive", async () => {
144
- await mount({ accessibilityState: { selected: true } });
170
+ const el = await mount({ accessibilityState: { selected: true } });
145
171
 
172
+ expect(el.getAttribute("aria-current")).toBe("true");
146
173
  expect(accessibilityState()).toMatchObject({ selected: true });
147
174
  });
148
175
 
@@ -1,5 +1,6 @@
1
1
  import type { ReactNode } from "react";
2
2
  import {
3
+ Platform,
3
4
  Pressable,
4
5
  type PressableProps,
5
6
  type PressableStateCallbackType,
@@ -27,12 +28,14 @@ export const ThreadListNew = ({
27
28
  (s) => s.threads.newThreadId === s.threads.mainThreadId,
28
29
  );
29
30
  const { switchToNewThread } = useThreadListNew();
31
+ const selected = accessibilityState?.selected ?? isActive;
30
32
 
31
33
  return (
32
34
  <Pressable
33
35
  onPress={switchToNewThread}
34
36
  accessibilityRole="button"
35
- accessibilityState={{ selected: isActive, ...accessibilityState }}
37
+ accessibilityState={{ ...accessibilityState, selected }}
38
+ aria-current={Platform.OS === "web" && selected ? "true" : undefined}
36
39
  {...pressableProps}
37
40
  >
38
41
  {typeof children === "function"
@@ -9,6 +9,7 @@ const h = vi.hoisted(() => ({
9
9
  threads: { mainThreadId: "thread-1" },
10
10
  threadListItem: { id: "thread-1" },
11
11
  },
12
+ platform: { os: "web" },
12
13
  pressableProps: null as Record<string, unknown> | null,
13
14
  }));
14
15
 
@@ -37,7 +38,16 @@ vi.mock("react-native", async (importOriginal) => {
37
38
  { ...props, ref },
38
39
  );
39
40
  });
40
- return { ...actual, Pressable: PressableMock };
41
+ return {
42
+ ...actual,
43
+ Platform: {
44
+ ...actual.Platform,
45
+ get OS() {
46
+ return h.platform.os;
47
+ },
48
+ },
49
+ Pressable: PressableMock,
50
+ };
41
51
  });
42
52
 
43
53
  (globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
@@ -55,6 +65,7 @@ describe("ThreadListItemTrigger", () => {
55
65
  h.switchTo.mockReset();
56
66
  h.state.threads.mainThreadId = "thread-1";
57
67
  h.state.threadListItem.id = "thread-1";
68
+ h.platform.os = "web";
58
69
  h.pressableProps = null;
59
70
 
60
71
  container = document.createElement("div");
@@ -107,23 +118,38 @@ describe("ThreadListItemTrigger", () => {
107
118
  expect(h.switchTo).not.toHaveBeenCalled();
108
119
  });
109
120
 
110
- it("marks the current thread selected", async () => {
111
- await mount();
121
+ it("marks the current thread current on the web", async () => {
122
+ const el = await mount();
112
123
 
124
+ expect(el.getAttribute("aria-current")).toBe("true");
125
+ expect(el.hasAttribute("aria-selected")).toBe(false);
113
126
  expect(accessibilityState()).toMatchObject({ selected: true });
114
127
  });
115
128
 
116
- it("does not mark another thread selected", async () => {
129
+ it("does not mark another thread current", async () => {
117
130
  h.state.threads.mainThreadId = "thread-2";
118
131
 
119
- await mount();
132
+ const el = await mount();
120
133
 
134
+ expect(el.hasAttribute("aria-current")).toBe(false);
121
135
  expect(accessibilityState()).toMatchObject({ selected: false });
122
136
  });
123
137
 
138
+ it("marks the current thread selected without aria-current on native", async () => {
139
+ h.platform.os = "ios";
140
+
141
+ const el = await mount();
142
+
143
+ expect(el.hasAttribute("aria-current")).toBe(false);
144
+ expect(accessibilityState()).toMatchObject({ selected: true });
145
+ });
146
+
124
147
  it("keeps caller accessibility state overrides", async () => {
125
- await mount({ accessibilityState: { busy: true, selected: false } });
148
+ const el = await mount({
149
+ accessibilityState: { busy: true, selected: false },
150
+ });
126
151
 
152
+ expect(el.hasAttribute("aria-current")).toBe(false);
127
153
  expect(accessibilityState()).toMatchObject({ busy: true, selected: false });
128
154
  });
129
155
 
@@ -1,5 +1,6 @@
1
1
  import type { ReactNode } from "react";
2
2
  import {
3
+ Platform,
3
4
  Pressable,
4
5
  type PressableProps,
5
6
  type PressableStateCallbackType,
@@ -27,12 +28,14 @@ export const ThreadListItemTrigger = ({
27
28
  (s) => s.threads.mainThreadId === s.threadListItem.id,
28
29
  );
29
30
  const { switchTo } = useThreadListItemTrigger();
31
+ const selected = accessibilityState?.selected ?? isActive;
30
32
 
31
33
  return (
32
34
  <Pressable
33
35
  onPress={switchTo}
34
36
  accessibilityRole="button"
35
- accessibilityState={{ selected: isActive, ...accessibilityState }}
37
+ accessibilityState={{ ...accessibilityState, selected }}
38
+ aria-current={Platform.OS === "web" && selected ? "true" : undefined}
36
39
  {...pressableProps}
37
40
  >
38
41
  {typeof children === "function"