@marktiderman/genesis-chat 0.2.0 → 0.2.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.
Files changed (77) hide show
  1. package/README.md +82 -7
  2. package/dist/{chunk-5D5A7I4Q.js → chunk-A4YJX7KT.js} +130 -16
  3. package/dist/chunk-A4YJX7KT.js.map +1 -0
  4. package/dist/chunk-AZS3HNLM.js +30 -0
  5. package/dist/chunk-AZS3HNLM.js.map +1 -0
  6. package/dist/{chunk-7S6HYNWW.js → chunk-GBJ3YI4E.js} +72 -5
  7. package/dist/chunk-GBJ3YI4E.js.map +1 -0
  8. package/dist/{chunk-PZTOY2LY.js → chunk-JU2FAPBI.js} +46 -13
  9. package/dist/chunk-JU2FAPBI.js.map +1 -0
  10. package/dist/{client-DMa2j7kz.d.ts → client-DUIG1hum.d.ts} +36 -1
  11. package/dist/index.d.ts +4 -4
  12. package/dist/index.js +2 -2
  13. package/dist/native.d.ts +36 -7
  14. package/dist/native.js +239 -116
  15. package/dist/native.js.map +1 -1
  16. package/dist/{outbox-B0y3phE0.d.ts → outbox-B9NAWQ58.d.ts} +74 -6
  17. package/dist/push.d.ts +2 -2
  18. package/dist/react.d.ts +48 -21
  19. package/dist/react.js +3 -3
  20. package/dist/server.d.ts +43 -2
  21. package/dist/server.js +115 -15
  22. package/dist/server.js.map +1 -1
  23. package/dist/{thread-DWXHKPdw.d.ts → thread-oPra7pkW.d.ts} +1 -1
  24. package/dist/web.d.ts +31 -11
  25. package/dist/web.js +97 -48
  26. package/dist/web.js.map +1 -1
  27. package/migrations/0003_genesis_chat_search_threads.sql +85 -0
  28. package/migrations/tests/0002_genesis_chat_coaching.test.sql +7 -3
  29. package/migrations/tests/0003_genesis_chat_search_threads.test.sql +50 -0
  30. package/package.json +13 -9
  31. package/src/__tests__/coaching-core.test.ts +3 -1
  32. package/src/__tests__/coaching-react.test.tsx +73 -2
  33. package/src/__tests__/coaching-web.test.tsx +78 -6
  34. package/src/__tests__/fake-xhr.ts +54 -0
  35. package/src/__tests__/outbox-hosted.test.ts +269 -0
  36. package/src/__tests__/review-279.test.ts +41 -0
  37. package/src/__tests__/review-fixes-web.test.tsx +13 -10
  38. package/src/__tests__/server-fake-video.test.ts +81 -0
  39. package/src/__tests__/server-fakes.ts +7 -1
  40. package/src/__tests__/server-safety.test.ts +17 -4
  41. package/src/__tests__/web.test.tsx +16 -0
  42. package/src/core/client.ts +28 -2
  43. package/src/core/outbox.ts +222 -16
  44. package/src/core/transfer.ts +71 -0
  45. package/src/core/types.ts +17 -0
  46. package/src/index.ts +1 -0
  47. package/src/native/__tests__/emoji.test.tsx +16 -0
  48. package/src/native/__tests__/organise-screens.test.tsx +80 -0
  49. package/src/native/__tests__/video-media.test.tsx +81 -4
  50. package/src/native/attachment-cards.tsx +22 -6
  51. package/src/native/inbox-screen.tsx +12 -6
  52. package/src/native/index.ts +2 -2
  53. package/src/native/message-actions-sheet.tsx +10 -5
  54. package/src/native/message-bubble.tsx +3 -11
  55. package/src/native/modules.ts +2 -0
  56. package/src/native/schedule-sheet.tsx +23 -9
  57. package/src/native/search-screen.tsx +26 -5
  58. package/src/native/thread-screen.tsx +30 -3
  59. package/src/native/topics.tsx +70 -15
  60. package/src/native/video-player.tsx +46 -6
  61. package/src/native/video-recorder.tsx +6 -3
  62. package/src/native/voice-player.tsx +18 -0
  63. package/src/react/index.tsx +103 -19
  64. package/src/react/previews.ts +51 -0
  65. package/src/server/fake-video.ts +171 -0
  66. package/src/server/index.ts +1 -0
  67. package/src/web/attachments.tsx +6 -6
  68. package/src/web/bubble.tsx +22 -0
  69. package/src/web/inbox.tsx +7 -6
  70. package/src/web/memory-client.ts +11 -3
  71. package/src/web/panel.tsx +2 -2
  72. package/src/web/schedule.tsx +14 -2
  73. package/src/web/thread.tsx +36 -8
  74. package/src/web/video.tsx +28 -34
  75. package/dist/chunk-5D5A7I4Q.js.map +0 -1
  76. package/dist/chunk-7S6HYNWW.js.map +0 -1
  77. package/dist/chunk-PZTOY2LY.js.map +0 -1
package/dist/native.js CHANGED
@@ -1,16 +1,18 @@
1
+ import { cardTitle, callAttachmentKind, attachmentPreview, messagePreview } from './chunk-AZS3HNLM.js';
2
+ export { attachmentPreview, cardTitle, messagePreview } from './chunk-AZS3HNLM.js';
1
3
  import { isHttpsUrl, extractLinks, scheduleLabel, scheduleQuickPicks, isSchedulable, topicKey, snippetParts } from './chunk-BJCE5O7V.js';
2
- import { useChat, useInbox, usePeople, chatKeys, useScheduled, useAvailability, useTopics, useConversation, usePins, useMessageActions, useFileUrls, useLinkPreviews, useStartConversation, useBroadcast, useSearch } from './chunk-PZTOY2LY.js';
3
- import { summarizeReactions, previewText, parseMentions, startsDay, dayLabel, itemTime, itemSender, startsGroup, activityLine } from './chunk-5D5A7I4Q.js';
4
- import { formatDuration, nextPlaybackRate, ChatError, CHAT_LIMITS, fileExtension, isAllowedExtension, attachmentKindFor, awaySummary, WEEKDAYS, minutesOf, waveformFromMetering, recordingLimitMs, CHAT_BUCKET, randomId } from './chunk-7S6HYNWW.js';
4
+ import { useChat, useInbox, usePeople, chatKeys, useScheduled, useAvailability, useTopics, useConversation, usePins, useMessageActions, useFileUrls, useLinkPreviews, useStartConversation, useBroadcast, useSearch } from './chunk-JU2FAPBI.js';
5
+ import { summarizeReactions, previewText, parseMentions, startsDay, dayLabel, itemTime, itemSender, startsGroup, activityLine } from './chunk-A4YJX7KT.js';
6
+ import { formatDuration, nextPlaybackRate, ChatError, CHAT_LIMITS, fileExtension, isAllowedExtension, attachmentKindFor, awaySummary, WEEKDAYS, minutesOf, waveformFromMetering, recordingLimitMs, CHAT_BUCKET, randomId } from './chunk-GBJ3YI4E.js';
5
7
  export { CHAT_MARK_READ_ACTION, CHAT_PUSH_CATEGORY, CHAT_REPLY_ACTION, PUSH_REPLY_WAIT_MS, chatNotificationActions, chatPath, chatPushCategory, chatPushCopy, chatPushPayload, chatThreadMatch, conversationIdFromData, failedReply, handleChatPushResponse, registerChatNotificationActions, replyClientId, replyClientIdFor } from './chunk-VEPZROQB.js';
6
8
  import { createContext, memo, useMemo, useRef, useContext, useSyncExternalStore, useState, useEffect, useCallback, useReducer } from 'react';
7
9
  import { NativeText, NativeAvatar, NativeImage, NativeIcon, NativeButton, NativeProgress, useToast, NativeSheet, NativeDatePicker, NativeTimePicker, NativeListItem, NativeSkeleton, NativeEmptyState, NativeTextarea, NativeSwitch, NativeCombobox, NativeChipGroup, NativeChip, NativeInput, NativeSearchBar, NativeInlineAlert, NativeFullScreenModal, FULL_SCREEN_DARK, useGenesisModules, NativeSheetScrollView, NativeCheckbox, NativeBadge } from '@marktiderman/genesis-ui-native/components';
8
10
  import { InboxPage, SettingsSubPage, getThreadPageStackOptions, formatInboxTime } from '@marktiderman/genesis-ui-native/layouts';
9
- import { useQueryClient } from '@tanstack/react-query';
10
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
11
- import { Pressable, Animated, PanResponder, View, Linking, useWindowDimensions, ActivityIndicator, Modal, AccessibilityInfo, ScrollView, FlatList, AppState, TextInput, Platform, KeyboardAvoidingView, Alert } from 'react-native';
11
+ import { Pressable, Animated, PanResponder, View, Linking, useWindowDimensions, ActivityIndicator, AppState, Modal, AccessibilityInfo, ScrollView, FlatList, TextInput, Platform, KeyboardAvoidingView, Alert } from 'react-native';
12
12
  import { useThemeRoles, useReducedMotion } from '@marktiderman/genesis-ui-native/hooks';
13
- import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
13
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
14
+ import { useQueryClient } from '@tanstack/react-query';
15
+ import { useSafeAreaInsets, SafeAreaView } from 'react-native-safe-area-context';
14
16
 
15
17
  // src/native/organise-copy.ts
16
18
  var chatOrganiseCopy = {
@@ -295,10 +297,10 @@ function isOpenableHref(href) {
295
297
  return false;
296
298
  }
297
299
  }
298
- function cardAction(kind, attachment, message, onOpenAttachment) {
300
+ function cardAction(kind, attachment, message, onOpenAttachment, ctx = { mine: false }) {
299
301
  let action = null;
300
302
  try {
301
- action = kind?.action?.(attachment);
303
+ action = callAttachmentKind(kind, "action", attachment, ctx);
302
304
  } catch {
303
305
  return null;
304
306
  }
@@ -313,14 +315,15 @@ function ChatAttachmentCard({ attachment, message, kind, mine = false, onOpenAtt
313
315
  const colors = useThemeRoles();
314
316
  let title = "";
315
317
  let subtitle = null;
318
+ const ctx = { mine };
316
319
  try {
317
- title = kind.title(attachment);
318
- subtitle = kind.subtitle?.(attachment);
320
+ title = callAttachmentKind(kind, "title", attachment, ctx) ?? "";
321
+ subtitle = callAttachmentKind(kind, "subtitle", attachment, ctx);
319
322
  } catch (e) {
320
323
  if (typeof __DEV__ !== "undefined" && __DEV__) console.warn(`[genesis-chat] attachment kind "${attachment.kind}" threw:`, e);
321
324
  }
322
325
  title = title || attachment.name || attachment.kind;
323
- const action = cardAction(kind, attachment, message, onOpenAttachment);
326
+ const action = cardAction(kind, attachment, message, onOpenAttachment, ctx);
324
327
  const fg = mine ? "text-primary-foreground" : void 0;
325
328
  return /* @__PURE__ */ jsxs(
326
329
  View,
@@ -450,6 +453,17 @@ function cycleVoiceNoteRate(id) {
450
453
  c?.cycleRate();
451
454
  return Boolean(c);
452
455
  }
456
+ function pauseChatPlayback(ids) {
457
+ if (!active) return false;
458
+ if (typeof ids === "function" ? !ids(active.id) : ids && !new Set(ids).has(active.id)) return false;
459
+ const { id, pause } = active;
460
+ try {
461
+ pause();
462
+ } catch {
463
+ }
464
+ releasePlayback(id);
465
+ return true;
466
+ }
453
467
  function activePlaybackId() {
454
468
  return active?.id ?? null;
455
469
  }
@@ -783,9 +797,14 @@ function ActiveVideo({
783
797
  const [muted, setMuted] = useState(false);
784
798
  const [rate, setRate] = useState(1);
785
799
  const ended = useRef(false);
800
+ const fullScreenOpen = useRef(false);
801
+ const setUp = useRef(false);
786
802
  useEffect(() => {
787
- claimPlayback(id, () => player.pause());
788
- setPlayingId(id, true);
803
+ const now = setUp.current ? Boolean(player.playing) : true;
804
+ setUp.current = true;
805
+ setPlaying(now);
806
+ setPlayingId(id, now);
807
+ if (now) claimPlayback(id, () => player.pause());
789
808
  const subs = [
790
809
  player.addListener("playingChange", ((payload) => {
791
810
  setPlaying(payload.isPlaying);
@@ -805,8 +824,21 @@ function ActiveVideo({
805
824
  if (payload.status === "error") latestBroken.current();
806
825
  }))
807
826
  ];
827
+ const appSub = AppState?.addEventListener?.("change", (next) => {
828
+ if (next === "background" && !fullScreenOpen.current) {
829
+ try {
830
+ player.pause();
831
+ } catch {
832
+ }
833
+ }
834
+ });
808
835
  return () => {
809
836
  subs.forEach((s) => s.remove());
837
+ appSub?.remove?.();
838
+ try {
839
+ player.pause();
840
+ } catch {
841
+ }
810
842
  setPlayingId(id, false);
811
843
  releasePlayback(id);
812
844
  };
@@ -834,7 +866,24 @@ function ActiveVideo({
834
866
  handles.current = { toggle, fullScreen, mute: toggleMute, rate: cycleRate };
835
867
  const { VideoView } = video;
836
868
  return /* @__PURE__ */ jsxs(View, { testID: `${testID}-active`, style, children: [
837
- /* @__PURE__ */ jsx(VideoView, { ref: viewRef, player, nativeControls: false, contentFit: "cover", allowsPictureInPicture: false, style: { width: "100%", height: "100%" }, testID: `${testID}-view` }),
869
+ /* @__PURE__ */ jsx(
870
+ VideoView,
871
+ {
872
+ ref: viewRef,
873
+ player,
874
+ nativeControls: false,
875
+ contentFit: "cover",
876
+ allowsPictureInPicture: false,
877
+ onFullscreenEnter: () => {
878
+ fullScreenOpen.current = true;
879
+ },
880
+ onFullscreenExit: () => {
881
+ fullScreenOpen.current = false;
882
+ },
883
+ style: { width: "100%", height: "100%" },
884
+ testID: `${testID}-view`
885
+ }
886
+ ),
838
887
  /* @__PURE__ */ jsx(
839
888
  Pressable,
840
889
  {
@@ -1054,7 +1103,7 @@ function MessageBubbleImpl(props) {
1054
1103
  const photos = attachments.filter((a) => a.kind === "image" && a.uri && props.onOpenImage);
1055
1104
  const videos = attachments.filter((a) => a.kind === "video" && modules.video && (a.uri || a.playbackId));
1056
1105
  const playingVideo = usePlayingVideo(videos.map((a) => a.key));
1057
- const cardActions = message ? cards.map((a) => cardAction(kinds?.[a.kind], a, message, props.onOpenAttachment)) : [];
1106
+ const cardActions = message ? cards.map((a) => cardAction(kinds?.[a.kind], a, message, props.onOpenAttachment, { mine })) : [];
1058
1107
  const playingVoice = usePlayingVoiceNote(voices.map((a) => a.key));
1059
1108
  const x = useRef(new Animated.Value(0)).current;
1060
1109
  const latest = useRef(props);
@@ -1096,7 +1145,7 @@ function MessageBubbleImpl(props) {
1096
1145
  senderName,
1097
1146
  message?.deletedAt ? copy.deleted : displayBody(body),
1098
1147
  attachments.map((a) => a.kind === "image" ? copy.photo : a.kind === "voice" ? copy.voice : a.kind === "video" ? copy.video : copy.file).join(", "),
1099
- cards.map((a) => cardTitle(kinds?.[a.kind], a)).join(", "),
1148
+ cards.map((a) => cardTitle(kinds?.[a.kind], a, { mine })).join(", "),
1100
1149
  preview ? fill(copy.linkPreview, { title: preview.title?.trim() || linkHost(preview.url), site: preview.siteName?.trim() || linkHost(preview.url) }) : null,
1101
1150
  time,
1102
1151
  message?.editedAt ? copy.edited : null,
@@ -1239,19 +1288,12 @@ function MessageBubbleImpl(props) {
1239
1288
  function SmallAction({ label, onPress, color, testID }) {
1240
1289
  return /* @__PURE__ */ jsx(Pressable, { testID, accessibilityRole: "button", accessibilityLabel: label, onPress, style: { minHeight: 44, minWidth: 44, paddingHorizontal: 8, alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsx(NativeText, { preset: "label", style: { color }, children: label }) });
1241
1290
  }
1242
- function cardTitle(kind, a) {
1243
- try {
1244
- return kind?.title(a) || a.name || a.kind;
1245
- } catch {
1246
- return a.name || a.kind;
1247
- }
1248
- }
1249
1291
  var MessageBubble = memo(MessageBubbleImpl);
1250
1292
  function inboxRow(item, ctx) {
1251
1293
  const other = item.otherUserId ? ctx.person(item.otherUserId) : void 0;
1252
1294
  const title = item.title ?? other?.name ?? ctx.copy.untitled;
1253
1295
  const last = item.lastMessage;
1254
- const text = last ? previewText(displayBody(last.body), last.attachment, last.deleted) : "";
1296
+ const text = last ? attachmentPreview(displayBody(last.body), last.attachment, ctx.kinds, last.deleted) : "";
1255
1297
  const preview = last && text && last.senderId === ctx.me && last.kind !== "system" ? `${ctx.copy.youPrefix}${text}` : text;
1256
1298
  return {
1257
1299
  id: item.conversationId,
@@ -1265,7 +1307,7 @@ function inboxRow(item, ctx) {
1265
1307
  };
1266
1308
  }
1267
1309
  function ChatInboxScreen({ onOpen, onStart, kind, kinds, excludeKinds, archived, pageProps, testID = "chat-inbox" }) {
1268
- const { client, userId } = useChat();
1310
+ const { client, userId, attachmentKinds } = useChat();
1269
1311
  const queryClient = useQueryClient();
1270
1312
  const { copy, person: known } = useChatNative();
1271
1313
  const inbox = useInbox({ kind, kinds, excludeKinds, archived });
@@ -1286,7 +1328,7 @@ function ChatInboxScreen({ onOpen, onStart, kind, kinds, excludeKinds, archived,
1286
1328
  void queryClient.invalidateQueries({ queryKey: chatKeys.unread() });
1287
1329
  }, [queryClient]);
1288
1330
  const items = inbox.items.map((item) => ({
1289
- ...inboxRow(item, { me: userId, copy, person }),
1331
+ ...inboxRow(item, { me: userId, copy, person, kinds: attachmentKinds }),
1290
1332
  onPress: () => onOpen(item.conversationId),
1291
1333
  swipeActions: [
1292
1334
  ...item.unread > 0 ? [{ id: "read", label: copy.markRead, onPress: () => void client.markRead(item.conversationId).then(settle, settle) }] : [],
@@ -1414,6 +1456,7 @@ function ChatVideoRecorder(props) {
1414
1456
  }
1415
1457
  function RecorderBody({ camera, onClose, limitMs, onStart, onTick, onRecorded, now = Date.now, testID }) {
1416
1458
  const { copy, modules } = useChatNative();
1459
+ const insets = useSafeAreaInsets();
1417
1460
  const colors = useThemeRoles();
1418
1461
  const [state, setState] = useState(() => initialVideoRecorder());
1419
1462
  const stateRef = useRef(state);
@@ -1516,7 +1559,7 @@ function RecorderBody({ camera, onClose, limitMs, onStart, onTick, onRecorded, n
1516
1559
  },
1517
1560
  state.cameraKey
1518
1561
  ) : null,
1519
- /* @__PURE__ */ jsxs(SafeAreaView, { edges: ["top", "bottom", "left", "right"], style: { flex: 1, justifyContent: "space-between" }, children: [
1562
+ /* @__PURE__ */ jsxs(View, { style: { flex: 1, justifyContent: "space-between", paddingTop: insets.top, paddingBottom: insets.bottom, paddingLeft: insets.left, paddingRight: insets.right }, children: [
1520
1563
  /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingHorizontal: 8, paddingTop: 4 }, children: [
1521
1564
  /* @__PURE__ */ jsx(
1522
1565
  Pressable,
@@ -2176,35 +2219,27 @@ function ChatScheduleSheet({ open, onOpenChange, onSchedule, now, testID = "chat
2176
2219
  setCustom(false);
2177
2220
  onSchedule(when);
2178
2221
  };
2179
- return /* @__PURE__ */ jsx(
2180
- NativeSheet,
2181
- {
2182
- open,
2183
- onOpenChange,
2184
- testID,
2185
- children: /* @__PURE__ */ jsxs(View, { style: { paddingVertical: 8 }, children: [
2186
- /* @__PURE__ */ jsxs(View, { style: { paddingHorizontal: 16, paddingBottom: 8, gap: 4 }, children: [
2187
- /* @__PURE__ */ jsx(NativeText, { preset: "h4", accessibilityRole: "header", children: copy.scheduleTitle }),
2188
- /* @__PURE__ */ jsx(NativeText, { preset: "caption", style: { color: colors.secondaryLabel }, children: copy.scheduleHint })
2189
- ] }),
2190
- custom ? /* @__PURE__ */ jsxs(View, { style: { paddingHorizontal: 16, gap: 12 }, testID: `${testID}-custom`, children: [
2191
- /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", gap: 12, minHeight: 44 }, children: [
2192
- /* @__PURE__ */ jsx(NativeText, { preset: "body", children: copy.scheduleDate }),
2193
- /* @__PURE__ */ jsx(NativeDatePicker, { testID: `${testID}-date`, value: at, onChange: (d) => setAt((x) => withDay(x, d)) })
2194
- ] }),
2195
- /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", gap: 12, minHeight: 44 }, children: [
2196
- /* @__PURE__ */ jsx(NativeText, { preset: "body", children: copy.scheduleTime }),
2197
- /* @__PURE__ */ jsx(NativeTimePicker, { testID: `${testID}-time`, value: at, onChange: (d) => setAt((x) => withTime(x, d)) })
2198
- ] }),
2199
- /* @__PURE__ */ jsx(NativeText, { testID: `${testID}-when`, preset: "caption", accessibilityLiveRegion: "polite", style: { color: ok ? colors.secondaryLabel : colors.destructive }, children: ok ? scheduleLabel(at, clock) : copy.scheduleOutOfRange }),
2200
- /* @__PURE__ */ jsx(NativeButton, { testID: `${testID}-confirm`, onPress: () => ok && choose(at), disabled: !ok, children: copy.scheduleConfirm })
2201
- ] }) : /* @__PURE__ */ jsxs(View, { children: [
2202
- picks.map((p, i) => /* @__PURE__ */ jsx(NativeListItem, { testID: `${testID}-pick-${i}`, title: p.label, leading: /* @__PURE__ */ jsx(NativeIcon, { name: "clock" }), onPress: () => choose(p.at) }, p.label)),
2203
- /* @__PURE__ */ jsx(NativeListItem, { testID: `${testID}-pick-custom`, title: copy.schedulePick, leading: /* @__PURE__ */ jsx(NativeIcon, { name: "calendar" }), showChevron: true, onPress: () => setCustom(true) })
2204
- ] })
2205
- ] })
2206
- }
2207
- );
2222
+ return /* @__PURE__ */ jsx(NativeSheet, { open, onOpenChange, fitContent: true, testID, children: /* @__PURE__ */ jsxs(View, { style: { paddingVertical: 8 }, children: [
2223
+ /* @__PURE__ */ jsxs(View, { style: { paddingHorizontal: 16, paddingBottom: 8, gap: 4 }, children: [
2224
+ /* @__PURE__ */ jsx(NativeText, { preset: "h4", accessibilityRole: "header", children: copy.scheduleTitle }),
2225
+ /* @__PURE__ */ jsx(NativeText, { preset: "caption", style: { color: colors.secondaryLabel }, children: copy.scheduleHint })
2226
+ ] }),
2227
+ custom ? /* @__PURE__ */ jsxs(View, { style: { paddingHorizontal: 16, gap: 12 }, testID: `${testID}-custom`, children: [
2228
+ /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", gap: 12, minHeight: 44 }, children: [
2229
+ /* @__PURE__ */ jsx(NativeText, { preset: "body", children: copy.scheduleDate }),
2230
+ /* @__PURE__ */ jsx(NativeDatePicker, { testID: `${testID}-date`, value: at, onChange: (d) => setAt((x) => withDay(x, d)) })
2231
+ ] }),
2232
+ /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", gap: 12, minHeight: 44 }, children: [
2233
+ /* @__PURE__ */ jsx(NativeText, { preset: "body", children: copy.scheduleTime }),
2234
+ /* @__PURE__ */ jsx(NativeTimePicker, { testID: `${testID}-time`, value: at, onChange: (d) => setAt((x) => withTime(x, d)) })
2235
+ ] }),
2236
+ /* @__PURE__ */ jsx(NativeText, { testID: `${testID}-when`, preset: "caption", accessibilityLiveRegion: "polite", style: { color: ok ? colors.secondaryLabel : colors.destructive }, children: ok ? scheduleLabel(at, clock) : copy.scheduleOutOfRange }),
2237
+ /* @__PURE__ */ jsx(NativeButton, { testID: `${testID}-confirm`, onPress: () => ok && choose(at), disabled: !ok, children: copy.scheduleConfirm })
2238
+ ] }) : /* @__PURE__ */ jsxs(View, { children: [
2239
+ picks.map((p, i) => /* @__PURE__ */ jsx(NativeListItem, { testID: `${testID}-pick-${i}`, title: p.label, leading: /* @__PURE__ */ jsx(NativeIcon, { name: "clock" }), onPress: () => choose(p.at) }, p.label)),
2240
+ /* @__PURE__ */ jsx(NativeListItem, { testID: `${testID}-pick-custom`, title: copy.schedulePick, leading: /* @__PURE__ */ jsx(NativeIcon, { name: "calendar" }), showChevron: true, onPress: () => setCustom(true) })
2241
+ ] })
2242
+ ] }) });
2208
2243
  }
2209
2244
  function ChatScheduledList({ conversationId, conversationTitle, testID = "chat-scheduled" }) {
2210
2245
  const { copy } = useChatNative();
@@ -2231,12 +2266,20 @@ function ChatScheduledList({ conversationId, conversationTitle, testID = "chat-s
2231
2266
  s.id
2232
2267
  )) });
2233
2268
  }
2269
+ function scheduledActionLabel(item, copy, kinds) {
2270
+ const text = attachmentPreview(displayBody(item.body), item.attachments[0]?.kind ?? null, kinds);
2271
+ const chars = Array.from(text);
2272
+ const short = chars.length > 60 ? `${chars.slice(0, 59).join("")}\u2026` : text;
2273
+ if (item.status === "failed") return [copy.scheduledRemove, short].filter(Boolean).join(", ");
2274
+ return [copy.scheduledCancel, short, fill(copy.scheduledFor, { when: scheduleLabel(item.sendAt) })].filter(Boolean).join(", ");
2275
+ }
2234
2276
  function ScheduledRow({ item, where, onCancel, testID }) {
2235
2277
  const { copy } = useChatNative();
2236
2278
  const colors = useThemeRoles();
2279
+ const { attachmentKinds } = useChat();
2237
2280
  const failed = item.status === "failed";
2238
2281
  const when = failed ? copy.scheduledCouldntSend : scheduleLabel(item.sendAt);
2239
- const text = previewText(displayBody(item.body), item.attachments[0]?.kind ?? null);
2282
+ const text = attachmentPreview(displayBody(item.body), item.attachments[0]?.kind ?? null, attachmentKinds);
2240
2283
  return /* @__PURE__ */ jsxs(View, { testID, style: { flexDirection: "row", alignItems: "center", gap: 12, paddingLeft: 16, paddingRight: 4, paddingVertical: 8, minHeight: 56 }, children: [
2241
2284
  /* @__PURE__ */ jsx(NativeIcon, { name: failed ? "warning" : "clock", size: 18, color: failed ? colors.destructive : colors.secondaryLabel }),
2242
2285
  /* @__PURE__ */ jsxs(View, { style: { flex: 1, gap: 2 }, accessible: true, accessibilityLabel: [when, where, text, item.error].filter(Boolean).join(", "), children: [
@@ -2251,7 +2294,7 @@ function ScheduledRow({ item, where, onCancel, testID }) {
2251
2294
  {
2252
2295
  testID: `${testID}-cancel`,
2253
2296
  accessibilityRole: "button",
2254
- accessibilityLabel: failed ? `${copy.scheduledRemove}, ${text}` : `${copy.scheduledCancel}, ${fill(copy.scheduledFor, { when })}`,
2297
+ accessibilityLabel: scheduledActionLabel(item, copy, attachmentKinds),
2255
2298
  onPress: onCancel,
2256
2299
  style: { minHeight: 44, minWidth: 44, paddingHorizontal: 12, alignItems: "center", justifyContent: "center" },
2257
2300
  children: /* @__PURE__ */ jsx(NativeText, { preset: "label", style: { color: colors.destructive }, children: failed ? copy.scheduledRemove : copy.scheduledCancel })
@@ -2556,6 +2599,12 @@ function checkTopicName(title, topics, renaming) {
2556
2599
  const key = topicKey(name);
2557
2600
  return topics.some((t) => t.topic === key) ? { error: "exists" } : { key };
2558
2601
  }
2602
+ function scrollToShow(chip, view, margin = 8) {
2603
+ if (view.width <= 0) return null;
2604
+ if (chip.x - margin < view.offset) return Math.max(0, chip.x - margin);
2605
+ if (chip.x + chip.width + margin > view.offset + view.width) return Math.max(0, chip.x + chip.width + margin - view.width);
2606
+ return null;
2607
+ }
2559
2608
  function ChatTopicBar({ topics, value, onValueChange, canManage = false, onSetTopic, testID = "chat-topics" }) {
2560
2609
  const { copy } = useChatNative();
2561
2610
  const colors = useThemeRoles();
@@ -2565,6 +2614,28 @@ function ChatTopicBar({ topics, value, onValueChange, canManage = false, onSetTo
2565
2614
  const [busy, setBusy] = useState(false);
2566
2615
  const [error, setError] = useState(null);
2567
2616
  const current = value ? topics.find((t) => t.topic === value) ?? null : null;
2617
+ const scrollRef = useRef(null);
2618
+ const chips = useRef(/* @__PURE__ */ new Map());
2619
+ const view = useRef({ offset: 0, width: 0 });
2620
+ const reveal = useCallback((key) => {
2621
+ const chip = chips.current.get(key ?? ALL);
2622
+ if (!chip) return;
2623
+ const to = scrollToShow(chip, view.current);
2624
+ if (to === null) return;
2625
+ view.current.offset = to;
2626
+ scrollRef.current?.scrollTo?.({ x: to, animated: true });
2627
+ }, []);
2628
+ const latestValue = useRef(value);
2629
+ latestValue.current = value;
2630
+ useEffect(() => reveal(value), [value, reveal]);
2631
+ const trackOffset = (e) => {
2632
+ view.current.offset = e.nativeEvent.contentOffset.x;
2633
+ };
2634
+ const onChipLayout = (key) => (e) => {
2635
+ const { x, width } = e.nativeEvent.layout;
2636
+ chips.current.set(key, { x, width });
2637
+ if (key === (latestValue.current ?? ALL)) reveal(latestValue.current);
2638
+ };
2568
2639
  const close = () => {
2569
2640
  if (busy) return;
2570
2641
  setSheet(null);
@@ -2606,15 +2677,34 @@ function ChatTopicBar({ topics, value, onValueChange, canManage = false, onSetTo
2606
2677
  }
2607
2678
  };
2608
2679
  return /* @__PURE__ */ jsxs(View, { testID, style: { flexDirection: "row", alignItems: "center", backgroundColor: colors.groupedBackground, paddingLeft: 8 }, children: [
2609
- /* @__PURE__ */ jsx(ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, accessibilityLabel: copy.topics, contentContainerStyle: { paddingVertical: 4, paddingRight: 8 }, style: { flex: 1 }, children: /* @__PURE__ */ jsxs(NativeChipGroup, { mode: "single", value: value ?? ALL, onChange: (v) => onValueChange(v === ALL ? null : v), className: "flex-nowrap", testID: `${testID}-group`, children: [
2610
- /* @__PURE__ */ jsx(NativeChip, { value: ALL, testID: `${testID}-all`, children: copy.topicAll }),
2611
- topics.map((t) => /* @__PURE__ */ jsx(NativeChip, { value: t.topic, testID: `${testID}-${t.topic}`, children: /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", alignItems: "center", gap: 4 }, children: [
2612
- t.closedAt ? /* @__PURE__ */ jsx(NativeIcon, { name: "lock", size: 13, color: colors.secondaryLabel, label: copy.topicClosed }) : null,
2613
- /* @__PURE__ */ jsx(NativeText, { preset: "body-sm", numberOfLines: 1, style: { maxWidth: 180, fontWeight: value === t.topic ? "600" : "400", color: value === t.topic ? colors.tint : colors.label }, children: t.title })
2614
- ] }) }, t.topic))
2615
- ] }) }),
2680
+ /* @__PURE__ */ jsx(
2681
+ ScrollView,
2682
+ {
2683
+ ref: scrollRef,
2684
+ horizontal: true,
2685
+ showsHorizontalScrollIndicator: false,
2686
+ accessibilityLabel: copy.topics,
2687
+ contentContainerStyle: { paddingVertical: 4, paddingRight: 8 },
2688
+ style: { flex: 1 },
2689
+ onLayout: (e) => {
2690
+ view.current.width = e.nativeEvent.layout.width;
2691
+ reveal(latestValue.current);
2692
+ },
2693
+ onScroll: trackOffset,
2694
+ onScrollEndDrag: trackOffset,
2695
+ onMomentumScrollEnd: trackOffset,
2696
+ scrollEventThrottle: 64,
2697
+ children: /* @__PURE__ */ jsxs(NativeChipGroup, { mode: "single", value: value ?? ALL, onChange: (v) => onValueChange(v === ALL ? null : v), className: "flex-nowrap", testID: `${testID}-group`, children: [
2698
+ /* @__PURE__ */ jsx(View, { onLayout: onChipLayout(ALL), children: /* @__PURE__ */ jsx(NativeChip, { value: ALL, testID: `${testID}-all`, children: copy.topicAll }) }),
2699
+ topics.map((t) => /* @__PURE__ */ jsx(View, { onLayout: onChipLayout(t.topic), children: /* @__PURE__ */ jsx(NativeChip, { value: t.topic, testID: `${testID}-${t.topic}`, children: /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", alignItems: "center", gap: 4 }, children: [
2700
+ t.closedAt ? /* @__PURE__ */ jsx(NativeIcon, { name: "lock", size: 13, color: colors.secondaryLabel, label: copy.topicClosed }) : null,
2701
+ /* @__PURE__ */ jsx(NativeText, { preset: "body-sm", numberOfLines: 1, style: { maxWidth: 180, fontWeight: value === t.topic ? "600" : "400", color: value === t.topic ? colors.tint : colors.label }, children: t.title })
2702
+ ] }) }) }, t.topic))
2703
+ ] })
2704
+ }
2705
+ ),
2616
2706
  canManage && onSetTopic ? /* @__PURE__ */ jsx(ChatIconButton, { testID: `${testID}-menu`, icon: "more", label: copy.topicOptions, onPress: () => setSheet({ step: "menu" }) }) : null,
2617
- /* @__PURE__ */ jsx(NativeSheet, { open: sheet !== null, onOpenChange: (o) => o ? void 0 : close(), testID: `${testID}-sheet`, children: sheet?.step === "name" ? /* @__PURE__ */ jsxs(View, { style: { padding: 16, gap: 12 }, children: [
2707
+ /* @__PURE__ */ jsx(NativeSheet, { open: sheet !== null, onOpenChange: (o) => o ? void 0 : close(), fitContent: true, testID: `${testID}-sheet`, children: sheet?.step === "name" ? /* @__PURE__ */ jsxs(View, { style: { padding: 16, gap: 12 }, children: [
2618
2708
  /* @__PURE__ */ jsx(NativeText, { preset: "h4", accessibilityRole: "header", children: sheet.renaming ? copy.topicRename : copy.topicNew }),
2619
2709
  /* @__PURE__ */ jsx(
2620
2710
  NativeInput,
@@ -3909,7 +3999,7 @@ function messageActionsFor(message, ctx) {
3909
3999
  return out;
3910
4000
  }
3911
4001
  var ICONS = { reply: "reply", copy: "copy", pin: "pin", unpin: "pin", delete: "trash" };
3912
- function MessageActionsSheet({ message, context, onClose, onReact, onAction, testID = "chat-message-actions" }) {
4002
+ function MessageActionsSheet({ message, context, onClose, onReact, onAction, attachmentKinds, testID = "chat-message-actions" }) {
3913
4003
  const { copy } = useChatNative();
3914
4004
  const colors = useThemeRoles();
3915
4005
  const actions = message ? messageActionsFor(message, context) : [];
@@ -3942,51 +4032,54 @@ function MessageActionsSheet({ message, context, onClose, onReact, onAction, tes
3942
4032
  onReact(message, emoji, !mineReactions.has(emoji));
3943
4033
  }
3944
4034
  }
3945
- ) }) : message ? /* @__PURE__ */ jsxs(View, { style: { gap: 12, paddingBottom: 8, paddingHorizontal: 16 }, children: [
3946
- /* @__PURE__ */ jsx(NativeText, { numberOfLines: 2, preset: "body-sm", style: { color: colors.secondaryLabel, paddingHorizontal: 4 }, children: previewText(message.body, message.attachments[0]?.kind, Boolean(message.deletedAt)) }),
3947
- canReactTo(message, context) ? /* @__PURE__ */ jsxs(View, { accessibilityRole: "toolbar", style: { flexDirection: "row", justifyContent: "space-between" }, children: [
3948
- QUICK_REACTIONS.map((emoji) => {
3949
- const on = mineReactions.has(emoji);
3950
- return /* @__PURE__ */ jsx(
4035
+ ) }) : message ? (
4036
+ // Room at the top: the iOS page sheet has no grabber, and its rounded corner clipped the caption.
4037
+ /* @__PURE__ */ jsxs(View, { style: { gap: 12, paddingTop: 20, paddingBottom: 8, paddingHorizontal: 16 }, children: [
4038
+ /* @__PURE__ */ jsx(NativeText, { testID: `${testID}-caption`, numberOfLines: 2, preset: "body-sm", style: { color: colors.secondaryLabel, paddingHorizontal: 4 }, children: messagePreview(message, attachmentKinds) }),
4039
+ canReactTo(message, context) ? /* @__PURE__ */ jsxs(View, { accessibilityRole: "toolbar", style: { flexDirection: "row", justifyContent: "space-between" }, children: [
4040
+ QUICK_REACTIONS.map((emoji) => {
4041
+ const on = mineReactions.has(emoji);
4042
+ return /* @__PURE__ */ jsx(
4043
+ Pressable,
4044
+ {
4045
+ testID: `${testID}-react-${emoji}`,
4046
+ accessibilityRole: "button",
4047
+ accessibilityState: { selected: on },
4048
+ accessibilityLabel: fill(copy.react, { emoji }),
4049
+ onPress: () => {
4050
+ onClose();
4051
+ onReact(message, emoji, !on);
4052
+ },
4053
+ style: { width: 48, height: 48, borderRadius: 24, alignItems: "center", justifyContent: "center", backgroundColor: on ? colors.fill : void 0 },
4054
+ children: /* @__PURE__ */ jsx(NativeText, { style: { fontSize: 26, lineHeight: 34 }, children: emoji })
4055
+ },
4056
+ emoji
4057
+ );
4058
+ }),
4059
+ /* @__PURE__ */ jsx(
3951
4060
  Pressable,
3952
4061
  {
3953
- testID: `${testID}-react-${emoji}`,
4062
+ testID: `${testID}-react-more`,
3954
4063
  accessibilityRole: "button",
3955
- accessibilityState: { selected: on },
3956
- accessibilityLabel: fill(copy.react, { emoji }),
3957
- onPress: () => {
3958
- onClose();
3959
- onReact(message, emoji, !on);
3960
- },
3961
- style: { width: 48, height: 48, borderRadius: 24, alignItems: "center", justifyContent: "center", backgroundColor: on ? colors.fill : void 0 },
3962
- children: /* @__PURE__ */ jsx(NativeText, { style: { fontSize: 26, lineHeight: 34 }, children: emoji })
3963
- },
3964
- emoji
3965
- );
3966
- }),
3967
- /* @__PURE__ */ jsx(
3968
- Pressable,
4064
+ accessibilityLabel: copy.moreReactions,
4065
+ onPress: () => setPicking(true),
4066
+ style: { width: 48, height: 48, borderRadius: 24, alignItems: "center", justifyContent: "center", backgroundColor: colors.fill },
4067
+ children: /* @__PURE__ */ jsx(NativeIcon, { name: "plus", size: 22, color: colors.secondaryLabel })
4068
+ }
4069
+ )
4070
+ ] }) : null,
4071
+ /* @__PURE__ */ jsx(View, { children: actions.map((action) => /* @__PURE__ */ jsx(
4072
+ NativeListItem,
3969
4073
  {
3970
- testID: `${testID}-react-more`,
3971
- accessibilityRole: "button",
3972
- accessibilityLabel: copy.moreReactions,
3973
- onPress: () => setPicking(true),
3974
- style: { width: 48, height: 48, borderRadius: 24, alignItems: "center", justifyContent: "center", backgroundColor: colors.fill },
3975
- children: /* @__PURE__ */ jsx(NativeIcon, { name: "plus", size: 22, color: colors.secondaryLabel })
3976
- }
3977
- )
3978
- ] }) : null,
3979
- /* @__PURE__ */ jsx(View, { children: actions.map((action) => /* @__PURE__ */ jsx(
3980
- NativeListItem,
3981
- {
3982
- testID: `${testID}-${action}`,
3983
- title: labels[action],
3984
- leading: /* @__PURE__ */ jsx(NativeIcon, { name: ICONS[action], size: 20, color: action === "delete" ? colors.destructive : colors.label }),
3985
- onPress: () => run(action)
3986
- },
3987
- action
3988
- )) })
3989
- ] }) : null });
4074
+ testID: `${testID}-${action}`,
4075
+ title: labels[action],
4076
+ leading: /* @__PURE__ */ jsx(NativeIcon, { name: ICONS[action], size: 20, color: action === "delete" ? colors.destructive : colors.label }),
4077
+ onPress: () => run(action)
4078
+ },
4079
+ action
4080
+ )) })
4081
+ ] })
4082
+ ) : null });
3990
4083
  }
3991
4084
 
3992
4085
  // src/native/rows.ts
@@ -4038,7 +4131,7 @@ function nearBottom(m, slack = AT_BOTTOM_POINTS) {
4038
4131
  return m.contentOffset.y + m.layoutMeasurement.height >= m.contentSize.height - slack;
4039
4132
  }
4040
4133
  function ChatThreadScreen(props) {
4041
- const { conversationId, threadRootId = null, keyboardVerticalOffset, onOpenAttachment, jumpTo, topic: topicProp, showTopics = false, scheduling = true, availability = true, testID = "chat-thread" } = props;
4134
+ const { conversationId, threadRootId = null, keyboardVerticalOffset, onOpenAttachment, jumpTo, topic: topicProp, showTopics = false, scheduling = true, availability = true, focused = true, testID = "chat-thread" } = props;
4042
4135
  const chat = useChat();
4043
4136
  const { userId, videoUpload, attachmentKinds, outbox } = chat;
4044
4137
  const linkPreviewImages = chat.linkPreviewImages === true;
@@ -4099,6 +4192,14 @@ function ChatThreadScreen(props) {
4099
4192
  const rows = useMemo(() => buildThreadRows(conv.items, { me: userId, firstUnreadId: conv.firstUnreadId, group }), [conv.items, userId, conv.firstUnreadId, group]);
4100
4193
  const paths = useMemo(() => conv.items.flatMap((i) => i.type === "message" ? attachmentPaths(i.message.attachments) : []), [conv.items]);
4101
4194
  const urls = useFileUrls(paths);
4195
+ const itemsNow = useRef(conv.items);
4196
+ itemsNow.current = conv.items;
4197
+ useEffect(() => {
4198
+ if (focused) return;
4199
+ pauseChatPlayback(
4200
+ (id) => itemsNow.current.some((i) => i.type === "message" ? i.message.attachments.some((a) => a.id === id) : i.type === "pending" && id.startsWith(`${i.pending.clientId}:`))
4201
+ );
4202
+ }, [focused]);
4102
4203
  const byId = useMemo(() => new Map(conv.messages.map((m) => [m.id, m])), [conv.messages]);
4103
4204
  const linkIds = useMemo(() => linkPreviewIds(conv.messages), [conv.messages]);
4104
4205
  const previews = useLinkPreviews(linkIds);
@@ -4198,7 +4299,13 @@ function ChatThreadScreen(props) {
4198
4299
  loadUntil,
4199
4300
  scrollTo: (index2, id) => {
4200
4301
  jumpingTo.current = { id, tries: 0 };
4201
- requestAnimationFrame(() => listRef.current?.scrollToIndex?.({ index: index2, animated: !reducedMotion, viewPosition: 0.5 }));
4302
+ const scroll = (at) => listRef.current?.scrollToIndex?.({ index: at, animated: !reducedMotion, viewPosition: 0.5 });
4303
+ requestAnimationFrame(() => scroll(index2));
4304
+ if (retryTimer.current) clearTimeout(retryTimer.current);
4305
+ retryTimer.current = setTimeout(() => {
4306
+ const at = rowIndexOf(rowsNow.current, id);
4307
+ if (at >= 0) scroll(at);
4308
+ }, 250);
4202
4309
  },
4203
4310
  onStart: () => {
4204
4311
  atBottom.current = false;
@@ -4302,7 +4409,7 @@ function ChatThreadScreen(props) {
4302
4409
  /* @__PURE__ */ jsxs(KeyboardAvoiding, { style: { flex: 1 }, ...keyboardProps, children: [
4303
4410
  showTopicBar ? /* @__PURE__ */ jsx(ChatTopicBar, { topics, value: topic ?? null, onValueChange: setChosenTopic, canManage: canManageTopics(conv.role), onSetTopic: topicsState.setTopic, testID: `${testID}-topics` }) : null,
4304
4411
  /* @__PURE__ */ jsx(ChatAwayLine, { text: awayLine, testID: `${testID}-away` }),
4305
- /* @__PURE__ */ jsx(View, { style: { flex: 1 }, children: conv.isLoading ? /* @__PURE__ */ jsx(LoadingThread, { testID: `${testID}-loading` }) : empty ? /* @__PURE__ */ jsx(View, { style: { flex: 1, justifyContent: "center" }, children: /* @__PURE__ */ jsx(NativeEmptyState, { variant: "first-run", title: copy.threadEmptyTitle, body: copy.threadEmptyBody, testID: `${testID}-empty` }) }) : /* @__PURE__ */ jsx(
4412
+ /* @__PURE__ */ jsx(View, { style: { flex: 1 }, children: conv.isLoading ? /* @__PURE__ */ jsx(LoadingThread, { testID: `${testID}-loading` }) : empty ? /* @__PURE__ */ jsx(View, { style: { flex: 1, justifyContent: "center" }, children: /* @__PURE__ */ jsx(NativeEmptyState, { variant: "first-run", title: copy.threadEmptyTitle, body: currentTopic?.closedAt ? copy.topicClosedNotice : copy.threadEmptyBody, testID: `${testID}-empty` }) }) : /* @__PURE__ */ jsx(
4306
4413
  List,
4307
4414
  {
4308
4415
  ref: listRef,
@@ -4379,6 +4486,7 @@ function ChatThreadScreen(props) {
4379
4486
  onClose: () => setActionsFor(null),
4380
4487
  onReact: react,
4381
4488
  onAction,
4489
+ attachmentKinds,
4382
4490
  testID: `${testID}-actions`
4383
4491
  }
4384
4492
  ),
@@ -4869,13 +4977,11 @@ function ChatSearchScreen({ conversationId, onOpen, conversationTitle, debounceM
4869
4977
  const where = conversationId ? null : fill(copy.searchIn, { name: titleOf(h.conversationId) });
4870
4978
  const when = formatInboxTime(new Date(h.createdAt), now());
4871
4979
  return /* @__PURE__ */ jsxs(
4872
- Pressable,
4980
+ HitRow,
4873
4981
  {
4874
4982
  testID: `${testID}-hit-${h.messageId}`,
4875
- accessibilityRole: "button",
4876
4983
  accessibilityLabel: [who, where, when, h.inTranscript ? copy.transcript : null, snippetText(h.snippet)].filter(Boolean).join(", "),
4877
4984
  onPress: () => onOpen(h),
4878
- style: ({ pressed }) => ({ minHeight: 56, paddingHorizontal: 16, paddingVertical: 10, gap: 4, backgroundColor: pressed ? colors.fill : "transparent" }),
4879
4985
  children: [
4880
4986
  /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", alignItems: "center", gap: 6 }, children: [
4881
4987
  /* @__PURE__ */ jsx(NativeText, { preset: "label", numberOfLines: 1, style: { flexShrink: 1 }, children: who }),
@@ -4897,7 +5003,24 @@ function ChatSearchScreen({ conversationId, onOpen, conversationTitle, debounceM
4897
5003
  )
4898
5004
  ] });
4899
5005
  }
5006
+ function HitRow({ testID, accessibilityLabel, onPress, children }) {
5007
+ const colors = useThemeRoles();
5008
+ const [pressed, setPressed] = useState(false);
5009
+ return /* @__PURE__ */ jsx(
5010
+ Pressable,
5011
+ {
5012
+ testID,
5013
+ accessibilityRole: "button",
5014
+ accessibilityLabel,
5015
+ onPress,
5016
+ onPressIn: () => setPressed(true),
5017
+ onPressOut: () => setPressed(false),
5018
+ style: { minHeight: 56, paddingHorizontal: 16, paddingVertical: 10, gap: 4, backgroundColor: pressed ? colors.fill : "transparent" },
5019
+ children
5020
+ }
5021
+ );
5022
+ }
4900
5023
 
4901
- export { AT_BOTTOM_POINTS, ChatAccessError, ChatAttachmentCard, ChatAutoRetry, ChatAvailabilityScreen, ChatAwayLine, ChatBroadcastSheet, ChatComposer, ChatEmojiPicker, ChatEmojiPickerSheet, ChatIconButton, ChatInboxScreen, ChatLinkPreviewCard, ChatMediaViewer, ChatNativeProvider, ChatPhotoViewer, ChatScheduleSheet, ChatScheduledBar, ChatScheduledList, ChatSearchScreen, ChatStartButton, ChatStartSheet, ChatThreadScreen, ChatTopicBar, ChatVideoPlayer, ChatVideoRecorder, DISMISS_DISTANCE, DISMISS_VELOCITY, EMOJI_CATEGORIES, MAX_ZOOM, MessageActionsSheet, MessageAttachments, MessageBubble, ModerationMarker, QUICK_REACTIONS, RECENT_EMOJI_MAX, SearchSnippet, TUS_CHUNK_BYTES, VIDEO_CHUNK_BYTES, VIDEO_HOLD_MS, VIDEO_MIN_MS, VideoUploadTray, VoiceNotePlayer, activePlaybackId, attachmentPaths, availabilityInput, awayLineFor, barsThatFit, base64ToBytes, base64Utf8, broadcastOutcome, broadcastRows, buildThreadRows, canManageTopics, canReactTo, canSend, cardAction, chatNativeCopy, chatOrganiseCopy, chatThreadStackOptions, checkTopicName, chunkedPut, claimPlayback, clearDrafts, clockToDate, composerDisabledReason, composerReducer, createDraftSaver, createTusUpload, createVideoUploads, cycleVideoRate, cycleVoiceNoteRate, dateToClock, defaultScheduleAt, defaultWeek, deviceTimeZone, displayBody, draftKey, emojiEntry, emojiInCategory, emptyComposer, fileFromDataUri, fileSource, filesFromAssets, fill, forgetRecentEmoji, formFrom, formatClock, fullScreenVideo, galleryFor, haptic, hoursFromWeek, idleRecorder, inboxRow, initialVideoRecorder, initials, insertAtSelection, invertedRows, isLocalUri, isOnline, isOpenableHref, isVideoPlaying, linkHost, linkPreviewIds, liveBars, loadDraft, loadRecentEmoji, memoryDraftStorage, messageActionsFor, moderationNote, moderationText, muxPosterUrl, muxStreamUrl, nearBottom, nextOffset, openChatLink, panLimit, pinchScale, previewToShow, pushRecentEmoji, recentEmojiKey, recordedVideoFile, recorderStep, recordingType, releasePlayback, replyTargetFor, resolveChatCopy, rowIndexOf, rowKey, sameTimeDays, saveRecentEmoji, scheduleInputFrom, scrubSeconds, searchEmoji, shouldDismiss, snippetText, splitCardAttachments, threadListMode, timeZones, toSend, toggleVideo, toggleVideoMute, toggleVoiceNote, topicClosedFor, topicMenuFor, useAwayLine, useChatAutoRetry, useChatComposer, useChatNative, useChatThreadStackOptions, useJumpToMessage, usePlayingVideo, usePlayingVoiceNote, useScheduleSend, useVideoUploads, useVoiceRecorder, videoRecorderStep, videoSource, videoState, videoUploads, voiceNoteFile, waveformBars, weekFromHours, withDay, withTime };
5024
+ export { AT_BOTTOM_POINTS, ChatAccessError, ChatAttachmentCard, ChatAutoRetry, ChatAvailabilityScreen, ChatAwayLine, ChatBroadcastSheet, ChatComposer, ChatEmojiPicker, ChatEmojiPickerSheet, ChatIconButton, ChatInboxScreen, ChatLinkPreviewCard, ChatMediaViewer, ChatNativeProvider, ChatPhotoViewer, ChatScheduleSheet, ChatScheduledBar, ChatScheduledList, ChatSearchScreen, ChatStartButton, ChatStartSheet, ChatThreadScreen, ChatTopicBar, ChatVideoPlayer, ChatVideoRecorder, DISMISS_DISTANCE, DISMISS_VELOCITY, EMOJI_CATEGORIES, MAX_ZOOM, MessageActionsSheet, MessageAttachments, MessageBubble, ModerationMarker, QUICK_REACTIONS, RECENT_EMOJI_MAX, SearchSnippet, TUS_CHUNK_BYTES, VIDEO_CHUNK_BYTES, VIDEO_HOLD_MS, VIDEO_MIN_MS, VideoUploadTray, VoiceNotePlayer, activePlaybackId, attachmentPaths, availabilityInput, awayLineFor, barsThatFit, base64ToBytes, base64Utf8, broadcastOutcome, broadcastRows, buildThreadRows, canManageTopics, canReactTo, canSend, cardAction, chatNativeCopy, chatOrganiseCopy, chatThreadStackOptions, checkTopicName, chunkedPut, claimPlayback, clearDrafts, clockToDate, composerDisabledReason, composerReducer, createDraftSaver, createTusUpload, createVideoUploads, cycleVideoRate, cycleVoiceNoteRate, dateToClock, defaultScheduleAt, defaultWeek, deviceTimeZone, displayBody, draftKey, emojiEntry, emojiInCategory, emptyComposer, fileFromDataUri, fileSource, filesFromAssets, fill, forgetRecentEmoji, formFrom, formatClock, fullScreenVideo, galleryFor, haptic, hoursFromWeek, idleRecorder, inboxRow, initialVideoRecorder, initials, insertAtSelection, invertedRows, isLocalUri, isOnline, isOpenableHref, isVideoPlaying, linkHost, linkPreviewIds, liveBars, loadDraft, loadRecentEmoji, memoryDraftStorage, messageActionsFor, moderationNote, moderationText, muxPosterUrl, muxStreamUrl, nearBottom, nextOffset, openChatLink, panLimit, pauseChatPlayback, pinchScale, previewToShow, pushRecentEmoji, recentEmojiKey, recordedVideoFile, recorderStep, recordingType, releasePlayback, replyTargetFor, resolveChatCopy, rowIndexOf, rowKey, sameTimeDays, saveRecentEmoji, scheduleInputFrom, scrubSeconds, searchEmoji, shouldDismiss, snippetText, splitCardAttachments, threadListMode, timeZones, toSend, toggleVideo, toggleVideoMute, toggleVoiceNote, topicClosedFor, topicMenuFor, useAwayLine, useChatAutoRetry, useChatComposer, useChatNative, useChatThreadStackOptions, useJumpToMessage, usePlayingVideo, usePlayingVoiceNote, useScheduleSend, useVideoUploads, useVoiceRecorder, videoRecorderStep, videoSource, videoState, videoUploads, voiceNoteFile, waveformBars, weekFromHours, withDay, withTime };
4902
5025
  //# sourceMappingURL=native.js.map
4903
5026
  //# sourceMappingURL=native.js.map