@marktiderman/genesis-chat 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +119 -6
  2. package/dist/{chunk-VEPZROQB.js → chunk-BTG4JWLF.js} +5 -4
  3. package/dist/chunk-BTG4JWLF.js.map +1 -0
  4. package/dist/{chunk-AZS3HNLM.js → chunk-IEFULAC2.js} +25 -3
  5. package/dist/chunk-IEFULAC2.js.map +1 -0
  6. package/dist/{chunk-JU2FAPBI.js → chunk-XQ2ULNHV.js} +2 -2
  7. package/dist/{chunk-JU2FAPBI.js.map → chunk-XQ2ULNHV.js.map} +1 -1
  8. package/dist/native.d.ts +158 -6
  9. package/dist/native.js +404 -181
  10. package/dist/native.js.map +1 -1
  11. package/dist/push.d.ts +2 -1
  12. package/dist/push.js +1 -1
  13. package/dist/react.d.ts +6 -2
  14. package/dist/react.js +1 -1
  15. package/dist/server.js +1 -1
  16. package/dist/web.d.ts +6 -2
  17. package/dist/web.js +6 -18
  18. package/dist/web.js.map +1 -1
  19. package/package.json +9 -9
  20. package/src/native/__tests__/attachment-slot.test.tsx +353 -0
  21. package/src/native/__tests__/organise-screens.test.tsx +12 -0
  22. package/src/native/__tests__/push-ids.test.ts +18 -0
  23. package/src/native/__tests__/push.test.ts +10 -4
  24. package/src/native/attachment-cards.tsx +249 -11
  25. package/src/native/copy.ts +15 -0
  26. package/src/native/index.ts +28 -4
  27. package/src/native/media.tsx +89 -2
  28. package/src/native/message-bubble.tsx +78 -12
  29. package/src/native/push.ts +5 -2
  30. package/src/native/search-screen.tsx +77 -69
  31. package/src/native/thread-screen.tsx +23 -6
  32. package/src/react/error-boundary.tsx +46 -0
  33. package/src/react/index.tsx +6 -2
  34. package/src/web/cards.tsx +2 -2
  35. package/src/web/error-boundary.tsx +2 -30
  36. package/dist/chunk-AZS3HNLM.js.map +0 -1
  37. package/dist/chunk-VEPZROQB.js.map +0 -1
package/dist/native.js CHANGED
@@ -1,11 +1,11 @@
1
- import { cardTitle, callAttachmentKind, attachmentPreview, messagePreview } from './chunk-AZS3HNLM.js';
2
- export { attachmentPreview, cardTitle, messagePreview } from './chunk-AZS3HNLM.js';
1
+ import { cardTitle, attachmentPreview, callAttachmentKind, ChatErrorBoundary, messagePreview } from './chunk-IEFULAC2.js';
2
+ export { attachmentPreview, cardTitle, messagePreview } from './chunk-IEFULAC2.js';
3
3
  import { isHttpsUrl, extractLinks, scheduleLabel, scheduleQuickPicks, isSchedulable, topicKey, snippetParts } from './chunk-BJCE5O7V.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';
4
+ import { useChat, useInbox, usePeople, chatKeys, useScheduled, useAvailability, useTopics, useConversation, usePins, useMessageActions, useFileUrls, useLinkPreviews, useStartConversation, useBroadcast, useSearch } from './chunk-XQ2ULNHV.js';
5
+ import { summarizeReactions, parseMentions, startsDay, dayLabel, itemTime, itemSender, startsGroup, activityLine } from './chunk-A4YJX7KT.js';
6
6
  import { formatDuration, nextPlaybackRate, ChatError, CHAT_LIMITS, fileExtension, isAllowedExtension, attachmentKindFor, awaySummary, WEEKDAYS, minutesOf, waveformFromMetering, recordingLimitMs, CHAT_BUCKET, randomId } from './chunk-GBJ3YI4E.js';
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';
8
- import { createContext, memo, useMemo, useRef, useContext, useSyncExternalStore, useState, useEffect, useCallback, useReducer } from 'react';
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-BTG4JWLF.js';
8
+ import { createContext, memo, useMemo, useState, useCallback, useRef, useContext, useSyncExternalStore, useLayoutEffect, useEffect, useReducer } from 'react';
9
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';
10
10
  import { InboxPage, SettingsSubPage, getThreadPageStackOptions, formatInboxTime } from '@marktiderman/genesis-ui-native/layouts';
11
11
  import { Pressable, Animated, PanResponder, View, Linking, useWindowDimensions, ActivityIndicator, AppState, Modal, AccessibilityInfo, ScrollView, FlatList, TextInput, Platform, KeyboardAvoidingView, Alert } from 'react-native';
@@ -195,6 +195,12 @@ var chatNativeCopy = {
195
195
  openLink: "Opens the link",
196
196
  openLinkAction: "Open link",
197
197
  attachmentCard: "{title}, {subtitle}",
198
+ cardActionFailed: "That didn't work. Try again.",
199
+ cardActionTimedOut: "That's taking too long. Try again.",
200
+ transcribing: "Transcribing\u2026",
201
+ transcriptLabel: "Transcript: {text}",
202
+ showMore: "Show more",
203
+ showLess: "Show less",
198
204
  openImage: "Open photo",
199
205
  closeViewer: "Close",
200
206
  photoOf: "Photo {n} of {count}",
@@ -281,11 +287,15 @@ var MEDIA_SCRIM = "rgba(0,0,0,0.5)";
281
287
  var MEDIA_SCRIM_LIGHT = "rgba(0,0,0,0.35)";
282
288
  var ON_TINT_FILL = "rgba(255,255,255,0.16)";
283
289
  var ON_TINT_FILL_STRONG = "rgba(255,255,255,0.2)";
284
- function splitCardAttachments(attachments, kinds) {
285
- if (!kinds) return { cards: [], rest: [...attachments] };
290
+ var BUILT_IN_KINDS = /* @__PURE__ */ new Set(["image", "voice", "video", "file", "link"]);
291
+ function attachmentKindOf(kinds, kind) {
292
+ return kinds && Object.prototype.hasOwnProperty.call(kinds, kind) ? kinds[kind] : void 0;
293
+ }
294
+ function splitCardAttachments(attachments, kinds, slot = false) {
295
+ if (!kinds && !slot) return { cards: [], rest: [...attachments] };
286
296
  const cards = [];
287
297
  const rest = [];
288
- for (const a of attachments) (Object.prototype.hasOwnProperty.call(kinds, a.kind) ? cards : rest).push(a);
298
+ for (const a of attachments) (attachmentKindOf(kinds, a.kind) || slot && !BUILT_IN_KINDS.has(a.kind) ? cards : rest).push(a);
289
299
  return { cards, rest };
290
300
  }
291
301
  function isOpenableHref(href) {
@@ -310,7 +320,63 @@ function cardAction(kind, attachment, message, onOpenAttachment, ctx = { mine: f
310
320
  const run = onPress ? onPress : onOpenAttachment ? () => onOpenAttachment(attachment, message) : href ? () => void Linking.openURL(href).catch(() => void 0) : null;
311
321
  return run ? { label: action.label, run } : null;
312
322
  }
313
- function ChatAttachmentCard({ attachment, message, kind, mine = false, onOpenAttachment, testID = "chat-attachment-card" }) {
323
+ var IDLE = { pending: false, error: null };
324
+ var CARD_ACTION_TIMEOUT_MS = 3e4;
325
+ function useCardRuns(options) {
326
+ const { failedText, timedOutText = failedText, timeoutMs = CARD_ACTION_TIMEOUT_MS } = typeof options === "string" ? { failedText: options } : options;
327
+ const [states, setStates] = useState({});
328
+ const busy = useRef(/* @__PURE__ */ new Set());
329
+ const run = useCallback(
330
+ async (id, task) => {
331
+ if (busy.current.has(id)) return;
332
+ const failed = (e, text = failedText) => {
333
+ if (typeof __DEV__ !== "undefined" && __DEV__) console.warn("[genesis-chat] an attachment card's action failed:", e);
334
+ setStates((s) => ({ ...s, [id]: { pending: false, error: text } }));
335
+ };
336
+ let result;
337
+ try {
338
+ result = task();
339
+ } catch (e) {
340
+ failed(e);
341
+ return;
342
+ }
343
+ if (!result || typeof result.then !== "function") {
344
+ setStates((s) => s[id] ? { ...s, [id]: IDLE } : s);
345
+ return;
346
+ }
347
+ busy.current.add(id);
348
+ setStates((s) => ({ ...s, [id]: { pending: true, error: null } }));
349
+ const TIMED_OUT = /* @__PURE__ */ Symbol("timed out");
350
+ let timer;
351
+ const limit = timeoutMs > 0 ? new Promise((resolve) => timer = setTimeout(() => resolve(TIMED_OUT), timeoutMs)) : null;
352
+ try {
353
+ const settled = await (limit ? Promise.race([result, limit]) : result);
354
+ if (settled === TIMED_OUT) {
355
+ failed(new Error(`timed out after ${timeoutMs} ms`), timedOutText);
356
+ void Promise.resolve(result).catch(() => void 0);
357
+ } else setStates((s) => ({ ...s, [id]: IDLE }));
358
+ } catch (e) {
359
+ failed(e);
360
+ } finally {
361
+ if (timer !== void 0) clearTimeout(timer);
362
+ busy.current.delete(id);
363
+ }
364
+ },
365
+ [failedText, timedOutText, timeoutMs]
366
+ );
367
+ const stateOf = useCallback((id) => states[id] ?? IDLE, [states]);
368
+ return { stateOf, run };
369
+ }
370
+ function slotUrl(url) {
371
+ if (!url) return null;
372
+ try {
373
+ const u = new URL(url);
374
+ return (u.protocol === "https:" || u.protocol === "http:") && u.hostname ? url : null;
375
+ } catch {
376
+ return null;
377
+ }
378
+ }
379
+ function ChatAttachmentCard({ attachment, message, kind, mine = false, onOpenAttachment, run, state = IDLE, testID = "chat-attachment-card" }) {
314
380
  const { copy } = useChatNative();
315
381
  const colors = useThemeRoles();
316
382
  let title = "";
@@ -325,31 +391,82 @@ function ChatAttachmentCard({ attachment, message, kind, mine = false, onOpenAtt
325
391
  title = title || attachment.name || attachment.kind;
326
392
  const action = cardAction(kind, attachment, message, onOpenAttachment, ctx);
327
393
  const fg = mine ? "text-primary-foreground" : void 0;
328
- return /* @__PURE__ */ jsxs(
329
- View,
330
- {
331
- testID,
332
- style: { flexDirection: "row", alignItems: "center", gap: 10, padding: 10, borderRadius: 12, minWidth: 220, backgroundColor: mine ? ON_TINT_FILL : colors.groupedBackground },
333
- children: [
334
- /* @__PURE__ */ jsxs(
335
- View,
336
- {
337
- accessible: true,
338
- accessibilityLabel: subtitle ? fill(copy.attachmentCard, { title, subtitle }) : title,
339
- style: { flexDirection: "row", alignItems: "center", gap: 10, flex: 1 },
340
- children: [
341
- /* @__PURE__ */ jsx(View, { style: { width: 40, height: 40, borderRadius: 10, alignItems: "center", justifyContent: "center", backgroundColor: mine ? ON_TINT_FILL_STRONG : colors.fill }, children: /* @__PURE__ */ jsx(NativeIcon, { name: "file", size: 20, color: mine ? colors.surface : colors.tint }) }),
342
- /* @__PURE__ */ jsxs(View, { style: { flex: 1, minWidth: 0 }, children: [
343
- /* @__PURE__ */ jsx(NativeText, { numberOfLines: 1, className: fg, style: mine ? { fontWeight: "600", fontSize: 15 } : { fontWeight: "600", fontSize: 15, color: colors.label }, children: title }),
344
- subtitle ? /* @__PURE__ */ jsx(NativeText, { preset: "caption", numberOfLines: 1, className: fg, style: mine ? { opacity: 0.85 } : { color: colors.secondaryLabel }, children: subtitle }) : null
345
- ] })
346
- ]
347
- }
348
- ),
349
- action ? /* @__PURE__ */ jsx(NativeButton, { testID: `${testID}-action`, variant: mine ? "secondary" : "default", onPress: action.run, children: action.label }) : null
350
- ]
351
- }
352
- );
394
+ return /* @__PURE__ */ jsxs(View, { testID, style: { gap: 4, minWidth: 220 }, children: [
395
+ /* @__PURE__ */ jsxs(
396
+ View,
397
+ {
398
+ style: { flexDirection: "row", alignItems: "center", gap: 10, padding: 10, borderRadius: 12, backgroundColor: mine ? ON_TINT_FILL : colors.groupedBackground },
399
+ children: [
400
+ /* @__PURE__ */ jsxs(
401
+ View,
402
+ {
403
+ accessible: true,
404
+ accessibilityLabel: subtitle ? fill(copy.attachmentCard, { title, subtitle }) : title,
405
+ style: { flexDirection: "row", alignItems: "center", gap: 10, flex: 1 },
406
+ children: [
407
+ /* @__PURE__ */ jsx(View, { style: { width: 40, height: 40, borderRadius: 10, alignItems: "center", justifyContent: "center", backgroundColor: mine ? ON_TINT_FILL_STRONG : colors.fill }, children: /* @__PURE__ */ jsx(NativeIcon, { name: "file", size: 20, color: mine ? colors.surface : colors.tint }) }),
408
+ /* @__PURE__ */ jsxs(View, { style: { flex: 1, minWidth: 0 }, children: [
409
+ /* @__PURE__ */ jsx(NativeText, { numberOfLines: 1, className: fg, style: mine ? { fontWeight: "600", fontSize: 15 } : { fontWeight: "600", fontSize: 15, color: colors.label }, children: title }),
410
+ subtitle ? /* @__PURE__ */ jsx(NativeText, { preset: "caption", numberOfLines: 1, className: fg, style: mine ? { opacity: 0.85 } : { color: colors.secondaryLabel }, children: subtitle }) : null
411
+ ] })
412
+ ]
413
+ }
414
+ ),
415
+ action ? /* @__PURE__ */ jsx(
416
+ NativeButton,
417
+ {
418
+ testID: `${testID}-action`,
419
+ variant: mine ? "secondary" : "default",
420
+ className: "h-11",
421
+ loading: state.pending,
422
+ onPress: () => run ? run(action.run) : action.run(),
423
+ children: action.label
424
+ }
425
+ ) : null
426
+ ]
427
+ }
428
+ ),
429
+ state.error ? /* @__PURE__ */ jsx(CardError, { text: state.error, mine, testID: `${testID}-error` }) : null
430
+ ] });
431
+ }
432
+ function CardError({ text, mine, testID }) {
433
+ const colors = useThemeRoles();
434
+ return /* @__PURE__ */ jsx(NativeText, { testID, preset: "caption", accessibilityLiveRegion: "polite", className: mine ? "text-primary-foreground" : void 0, style: mine ? { paddingHorizontal: 4 } : { paddingHorizontal: 4, color: colors.destructive }, children: text });
435
+ }
436
+ function AppAttachment({ attachment, message, kind, url, mine, slot, onOpenAttachment, onEmpty, run, state, testID }) {
437
+ const hidden = slot === null && !kind;
438
+ useLayoutEffect(() => {
439
+ if (slot === null || slot === void 0) onEmpty?.(attachment.id, hidden);
440
+ }, [slot, hidden, attachment.id, onEmpty]);
441
+ const fallback = /* @__PURE__ */ jsx(ChatAttachmentCard, { attachment, message, kind, mine, onOpenAttachment, run, state, testID });
442
+ if (slot === void 0) return fallback;
443
+ if (slot === null) return hidden ? null : fallback;
444
+ const found = cardAction(kind, attachment, message, onOpenAttachment, { mine });
445
+ const action = found ? { label: found.label, run: () => run(found.run) } : null;
446
+ const props = { attachment, message, url: slotUrl(url), mine, kind, action, run, pending: state.pending, error: state.error, fallback, testID };
447
+ return /* @__PURE__ */ jsx(ChatErrorBoundary, { fallback: /* @__PURE__ */ jsx(Reported, { id: attachment.id, empty: false, onEmpty, children: fallback }), resetKeys: [attachment, message, kind, slot], children: /* @__PURE__ */ jsx(SlotProbe, { slot, props, onEmpty }, slotKey(slot)) });
448
+ }
449
+ var slotKeys = /* @__PURE__ */ new WeakMap();
450
+ var nextSlotKey = 0;
451
+ function slotKey(slot) {
452
+ let k = slotKeys.get(slot);
453
+ if (k === void 0) slotKeys.set(slot, k = ++nextSlotKey);
454
+ return k;
455
+ }
456
+ function isPlainFunction(slot) {
457
+ return typeof slot === "function" && !slot.prototype?.isReactComponent;
458
+ }
459
+ function SlotProbe({ slot, props, onEmpty }) {
460
+ const Slot = slot;
461
+ const out = isPlainFunction(slot) ? slot(props) : /* @__PURE__ */ jsx(Slot, { ...props });
462
+ const empty = out === null || out === void 0 || out === false;
463
+ return /* @__PURE__ */ jsx(Reported, { id: props.attachment.id, empty, onEmpty, children: empty ? null : out });
464
+ }
465
+ function Reported({ id, empty, onEmpty, children }) {
466
+ useLayoutEffect(() => {
467
+ onEmpty?.(id, empty);
468
+ }, [id, empty, onEmpty]);
469
+ return /* @__PURE__ */ jsx(Fragment, { children });
353
470
  }
354
471
  function linkHost(url) {
355
472
  try {
@@ -941,7 +1058,9 @@ function fromServer(a, urls) {
941
1058
  provider: a.provider,
942
1059
  playbackId: a.playbackId,
943
1060
  status: a.status,
944
- posterUri: a.posterPath ? urls[a.posterPath] ?? null : null
1061
+ posterUri: a.posterPath ? urls[a.posterPath] ?? null : null,
1062
+ transcript: a.transcript,
1063
+ transcriptStatus: a.transcriptStatus
945
1064
  };
946
1065
  }
947
1066
  function fromPending(clientId, index2, file, preview) {
@@ -1013,7 +1132,10 @@ function MessageAttachments({ attachments, mine, onOpenImage, testID }) {
1013
1132
  );
1014
1133
  }) }) : null,
1015
1134
  others.map(
1016
- (a, i) => a.kind === "voice" ? /* @__PURE__ */ jsx(VoiceNotePlayer, { id: a.key, uri: a.uri, durationMs: a.durationMs, waveform: a.waveform, mine, testID: `${testID}-voice-${i}` }, a.key) : a.kind === "video" && modules.video ? /* @__PURE__ */ jsx(
1135
+ (a, i) => a.kind === "voice" ? /* @__PURE__ */ jsxs(View, { style: { gap: 2 }, children: [
1136
+ /* @__PURE__ */ jsx(VoiceNotePlayer, { id: a.key, uri: a.uri, durationMs: a.durationMs, waveform: a.waveform, mine, testID: `${testID}-voice-${i}` }),
1137
+ /* @__PURE__ */ jsx(VoiceTranscript, { transcript: a.transcript, status: a.transcriptStatus, mine, testID: `${testID}-voice-${i}-transcript` })
1138
+ ] }, a.key) : a.kind === "video" && modules.video ? /* @__PURE__ */ jsx(
1017
1139
  ChatVideoPlayer,
1018
1140
  {
1019
1141
  id: a.key,
@@ -1046,6 +1168,53 @@ function MessageAttachments({ attachments, mine, onOpenImage, testID }) {
1046
1168
  )
1047
1169
  ] });
1048
1170
  }
1171
+ var TRANSCRIPT_LINES = 3;
1172
+ function transcriptIsLong(text, measuredLines) {
1173
+ if (typeof measuredLines === "number") return measuredLines > TRANSCRIPT_LINES;
1174
+ return text.length > 140 || text.split("\n").length > TRANSCRIPT_LINES;
1175
+ }
1176
+ function VoiceTranscript({ transcript, status, mine, testID }) {
1177
+ const { copy } = useChatNative();
1178
+ const colors = useThemeRoles();
1179
+ const [open, setOpen] = useState(false);
1180
+ const [lines, setLines] = useState(null);
1181
+ const fg = mine ? "text-primary-foreground" : void 0;
1182
+ const text = transcript?.trim();
1183
+ if (status === "pending" && !text) {
1184
+ return /* @__PURE__ */ jsx(NativeText, { testID, preset: "caption", className: fg, style: mine ? { fontStyle: "italic", opacity: 0.85, paddingHorizontal: 8 } : { fontStyle: "italic", color: colors.secondaryLabel, paddingHorizontal: 8 }, children: copy.transcribing });
1185
+ }
1186
+ if (!text) return null;
1187
+ const long = transcriptIsLong(text, lines);
1188
+ const textStyle = mine ? { fontSize: 15, lineHeight: 20 } : { fontSize: 15, lineHeight: 20, color: colors.label };
1189
+ return /* @__PURE__ */ jsxs(View, { testID, style: { paddingHorizontal: 8, alignItems: "flex-start" }, children: [
1190
+ /* @__PURE__ */ jsx(NativeText, { testID: `${testID}-text`, className: fg, numberOfLines: open || !long ? void 0 : TRANSCRIPT_LINES, style: textStyle, children: text }),
1191
+ /* @__PURE__ */ jsx(
1192
+ NativeText,
1193
+ {
1194
+ testID: `${testID}-measure`,
1195
+ "aria-hidden": true,
1196
+ accessibilityElementsHidden: true,
1197
+ importantForAccessibility: "no-hide-descendants",
1198
+ onTextLayout: (e) => setLines(e.nativeEvent.lines.length),
1199
+ style: [textStyle, { position: "absolute", left: 8, right: 8, top: 0, opacity: 0 }],
1200
+ pointerEvents: "none",
1201
+ children: text
1202
+ }
1203
+ ),
1204
+ long ? /* @__PURE__ */ jsx(
1205
+ Pressable,
1206
+ {
1207
+ testID: `${testID}-toggle`,
1208
+ accessibilityRole: "button",
1209
+ accessibilityState: { expanded: open },
1210
+ onPress: () => setOpen((o) => !o),
1211
+ hitSlop: { top: 8, bottom: 8 },
1212
+ style: { minHeight: 44, minWidth: 44, justifyContent: "center" },
1213
+ children: /* @__PURE__ */ jsx(NativeText, { preset: "caption", className: fg, style: mine ? { fontWeight: "600" } : { fontWeight: "600", color: colors.tint }, children: open ? copy.showLess : copy.showMore })
1214
+ }
1215
+ ) : null
1216
+ ] });
1217
+ }
1049
1218
  function moderationNote(message, opts) {
1050
1219
  if (!message || message.deletedAt) return null;
1051
1220
  if (message.moderation === "hidden") return opts.mine && !opts.observer ? "hidden-mine" : "hidden";
@@ -1087,10 +1256,23 @@ function MessageBubbleImpl(props) {
1087
1256
  const sender = person(row.senderId);
1088
1257
  const senderName = mine ? copy.you : sender?.name ?? "";
1089
1258
  const kinds = props.attachmentKinds;
1259
+ const slot = props.attachmentSlot;
1090
1260
  const { cards, rest: plain } = useMemo(
1091
- () => splitCardAttachments(message && !message.deletedAt ? message.attachments : [], kinds),
1092
- [message, kinds]
1261
+ () => splitCardAttachments(message && !message.deletedAt ? message.attachments : [], kinds, slot !== void 0),
1262
+ [message, kinds, slot]
1093
1263
  );
1264
+ const runs = useCardRuns({ failedText: copy.cardActionFailed, timedOutText: copy.cardActionTimedOut, timeoutMs: props.cardActionTimeoutMs });
1265
+ const [empty, setEmpty] = useState(() => /* @__PURE__ */ new Set());
1266
+ const onEmpty = useCallback((id, isEmpty) => {
1267
+ setEmpty((s) => {
1268
+ if (s.has(id) === isEmpty) return s;
1269
+ const next = new Set(s);
1270
+ if (isEmpty) next.add(id);
1271
+ else next.delete(id);
1272
+ return next;
1273
+ });
1274
+ }, []);
1275
+ const shownCards = cards.filter((a) => !empty.has(a.id));
1094
1276
  const attachments = useMemo(() => {
1095
1277
  if (message) return message.deletedAt ? [] : plain.map((a) => fromServer(a, urls));
1096
1278
  return [
@@ -1103,7 +1285,11 @@ function MessageBubbleImpl(props) {
1103
1285
  const photos = attachments.filter((a) => a.kind === "image" && a.uri && props.onOpenImage);
1104
1286
  const videos = attachments.filter((a) => a.kind === "video" && modules.video && (a.uri || a.playbackId));
1105
1287
  const playingVideo = usePlayingVideo(videos.map((a) => a.key));
1106
- const cardActions = message ? cards.map((a) => cardAction(kinds?.[a.kind], a, message, props.onOpenAttachment, { mine })) : [];
1288
+ const cardActions = message ? cards.map((a) => {
1289
+ if (empty.has(a.id)) return null;
1290
+ const c = cardAction(attachmentKindOf(kinds, a.kind), a, message, props.onOpenAttachment, { mine });
1291
+ return c ? { label: c.label, run: () => void runs.run(a.id, c.run) } : null;
1292
+ }) : [];
1107
1293
  const playingVoice = usePlayingVoiceNote(voices.map((a) => a.key));
1108
1294
  const x = useRef(new Animated.Value(0)).current;
1109
1295
  const latest = useRef(props);
@@ -1144,8 +1330,13 @@ function MessageBubbleImpl(props) {
1144
1330
  const label = [
1145
1331
  senderName,
1146
1332
  message?.deletedAt ? copy.deleted : displayBody(body),
1147
- attachments.map((a) => a.kind === "image" ? copy.photo : a.kind === "voice" ? copy.voice : a.kind === "video" ? copy.video : copy.file).join(", "),
1148
- cards.map((a) => cardTitle(kinds?.[a.kind], a, { mine })).join(", "),
1333
+ attachments.map(
1334
+ (a) => a.kind === "image" ? copy.photo : a.kind === "voice" ? [copy.voice, a.transcript?.trim() ? fill(copy.transcriptLabel, { text: a.transcript.trim() }) : null].filter(Boolean).join(", ") : a.kind === "video" ? copy.video : copy.file
1335
+ ).join(", "),
1336
+ shownCards.map((a) => {
1337
+ const s = runs.stateOf(a.id);
1338
+ return [cardTitle(attachmentKindOf(kinds, a.kind), a, { mine }), s.error].filter(Boolean).join(", ");
1339
+ }).join(", "),
1149
1340
  preview ? fill(copy.linkPreview, { title: preview.title?.trim() || linkHost(preview.url), site: preview.siteName?.trim() || linkHost(preview.url) }) : null,
1150
1341
  time,
1151
1342
  message?.editedAt ? copy.edited : null,
@@ -1196,94 +1387,120 @@ function MessageBubbleImpl(props) {
1196
1387
  };
1197
1388
  const quote = message?.replyToId ?? pending?.input.replyToId ?? null;
1198
1389
  const quoted = quote && repliedTo ? repliedTo : null;
1390
+ const invisible = Boolean(message && !message.deletedAt && !body && !attachments.length && !preview && !quote && !moderation && cards.length && !shownCards.length);
1199
1391
  const bubbleColor = mine ? colors.tint : colors.surface;
1200
1392
  const fg = mine ? "text-primary-foreground" : void 0;
1201
1393
  const textStyle = mine ? { fontSize: 17, lineHeight: 22 } : { fontSize: 17, lineHeight: 22, color: colors.label };
1202
- return /* @__PURE__ */ jsxs(View, { testID, style: { paddingHorizontal: 8, paddingTop: row.groupStart ? 8 : 2, backgroundColor: props.highlighted ? colors.fill : void 0 }, children: [
1203
- row.showName && sender?.name ? /* @__PURE__ */ jsx(NativeText, { preset: "caption", style: { marginLeft: group ? 48 : 12, marginBottom: 2, color: colors.secondaryLabel }, children: sender.name }) : null,
1204
- /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", alignItems: "flex-end", justifyContent: mine ? "flex-end" : "flex-start" }, children: [
1205
- group && !mine ? /* @__PURE__ */ jsx(View, { style: { width: 40, alignItems: "flex-start" }, children: row.showAvatar ? /* @__PURE__ */ jsx(NativeAvatar, { size: 32, src: sender?.avatarUrl ?? null, fallback: initials(sender?.name), alt: sender?.name, recyclingKey: row.senderId ?? void 0 }) : null }) : null,
1206
- /* @__PURE__ */ jsx(
1207
- Animated.View,
1208
- {
1209
- style: { maxWidth: "80%", transform: [{ translateX: x }] },
1210
- ...canSwipe ? responder.panHandlers : {},
1211
- children: /* @__PURE__ */ jsxs(
1212
- Pressable,
1394
+ return /* @__PURE__ */ jsxs(
1395
+ View,
1396
+ {
1397
+ testID,
1398
+ accessibilityElementsHidden: invisible || void 0,
1399
+ importantForAccessibility: invisible ? "no-hide-descendants" : void 0,
1400
+ style: { paddingHorizontal: 8, paddingTop: row.groupStart ? 8 : 2, backgroundColor: props.highlighted ? colors.fill : void 0, display: invisible ? "none" : void 0 },
1401
+ children: [
1402
+ row.showName && sender?.name ? /* @__PURE__ */ jsx(NativeText, { preset: "caption", style: { marginLeft: group ? 48 : 12, marginBottom: 2, color: colors.secondaryLabel }, children: sender.name }) : null,
1403
+ /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", alignItems: "flex-end", justifyContent: mine ? "flex-end" : "flex-start" }, children: [
1404
+ group && !mine ? /* @__PURE__ */ jsx(View, { style: { width: 40, alignItems: "flex-start" }, children: row.showAvatar ? /* @__PURE__ */ jsx(NativeAvatar, { size: 32, src: sender?.avatarUrl ?? null, fallback: initials(sender?.name), alt: sender?.name, recyclingKey: row.senderId ?? void 0 }) : null }) : null,
1405
+ /* @__PURE__ */ jsx(
1406
+ Animated.View,
1213
1407
  {
1214
- testID: `${testID}-bubble`,
1215
- accessible: true,
1216
- accessibilityLabel: label,
1217
- accessibilityHint: failed ? copy.retry : void 0,
1218
- accessibilityActions: actions,
1219
- onAccessibilityAction: (e) => onAction(e.nativeEvent.actionName),
1220
- onLongPress: message && !message.deletedAt ? () => props.onLongPress?.(message) : void 0,
1221
- delayLongPress: 350,
1222
- style: {
1223
- paddingHorizontal: attachments.length && !body ? 4 : 12,
1224
- paddingVertical: attachments.length && !body ? 4 : 8,
1225
- borderRadius: 18,
1226
- backgroundColor: bubbleColor,
1227
- opacity: sending ? 0.6 : moderation === "hidden-mine" ? 0.7 : 1,
1228
- gap: 4
1229
- },
1230
- children: [
1231
- quote ? /* @__PURE__ */ jsxs(
1232
- View,
1233
- {
1234
- testID: `${testID}-quote`,
1235
- style: { borderLeftWidth: 3, borderLeftColor: mine ? colors.surface : colors.tint, paddingLeft: 8, paddingVertical: 2, opacity: 0.85 },
1236
- children: [
1237
- /* @__PURE__ */ jsx(NativeText, { preset: "caption", className: fg, style: mine ? { fontWeight: "600" } : { fontWeight: "600", color: colors.secondaryLabel }, children: quoted ? quoted.senderId === me ? copy.you : person(quoted.senderId)?.name ?? "" : "" }),
1238
- /* @__PURE__ */ jsx(NativeText, { preset: "caption", numberOfLines: 2, className: fg, style: mine ? void 0 : { color: colors.secondaryLabel }, children: quoted ? previewText(quoted.body, quoted.attachments[0]?.kind, Boolean(quoted.deletedAt)) : "\u2026" })
1239
- ]
1240
- }
1241
- ) : null,
1242
- attachments.length ? /* @__PURE__ */ jsx(MessageAttachments, { attachments, mine, onOpenImage: props.onOpenImage, testID }) : null,
1243
- message && cards.length ? cards.map((a, i) => /* @__PURE__ */ jsx(ChatAttachmentCard, { attachment: a, message, kind: kinds[a.kind], mine, onOpenAttachment: props.onOpenAttachment, testID: `${testID}-card-${i}` }, a.id)) : null,
1244
- message?.deletedAt ? /* @__PURE__ */ jsx(NativeText, { className: fg, style: [textStyle, { fontStyle: "italic", opacity: 0.7 }], children: copy.deleted }) : body ? /* @__PURE__ */ jsx(NativeText, { className: fg, style: [textStyle, attachments.length ? { paddingHorizontal: 8, paddingBottom: 4 } : null], children: displayBody(body) }) : null,
1245
- moderation ? /* @__PURE__ */ jsx(ModerationMarker, { note: moderation, mine, testID: `${testID}-moderation` }) : null,
1246
- preview ? /* @__PURE__ */ jsx(ChatLinkPreviewCard, { preview, mine, images: props.linkPreviewImages, testID: `${testID}-preview` }) : null
1247
- ]
1408
+ style: { maxWidth: "80%", transform: [{ translateX: x }] },
1409
+ ...canSwipe ? responder.panHandlers : {},
1410
+ children: /* @__PURE__ */ jsxs(
1411
+ Pressable,
1412
+ {
1413
+ testID: `${testID}-bubble`,
1414
+ accessible: true,
1415
+ accessibilityLabel: label,
1416
+ accessibilityHint: failed ? copy.retry : void 0,
1417
+ accessibilityActions: actions,
1418
+ onAccessibilityAction: (e) => onAction(e.nativeEvent.actionName),
1419
+ onLongPress: message && !message.deletedAt ? () => props.onLongPress?.(message) : void 0,
1420
+ delayLongPress: 350,
1421
+ style: {
1422
+ paddingHorizontal: attachments.length && !body ? 4 : 12,
1423
+ paddingVertical: attachments.length && !body ? 4 : 8,
1424
+ borderRadius: 18,
1425
+ backgroundColor: bubbleColor,
1426
+ opacity: sending ? 0.6 : moderation === "hidden-mine" ? 0.7 : 1,
1427
+ gap: 4
1428
+ },
1429
+ children: [
1430
+ quote ? /* @__PURE__ */ jsxs(
1431
+ View,
1432
+ {
1433
+ testID: `${testID}-quote`,
1434
+ style: { borderLeftWidth: 3, borderLeftColor: mine ? colors.surface : colors.tint, paddingLeft: 8, paddingVertical: 2, opacity: 0.85 },
1435
+ children: [
1436
+ /* @__PURE__ */ jsx(NativeText, { preset: "caption", className: fg, style: mine ? { fontWeight: "600" } : { fontWeight: "600", color: colors.secondaryLabel }, children: quoted ? quoted.senderId === me ? copy.you : person(quoted.senderId)?.name ?? "" : "" }),
1437
+ /* @__PURE__ */ jsx(NativeText, { preset: "caption", numberOfLines: 2, className: fg, style: mine ? void 0 : { color: colors.secondaryLabel }, children: quoted ? attachmentPreview(displayBody(quoted.body), quoted.attachments[0], kinds, Boolean(quoted.deletedAt)) : "\u2026" })
1438
+ ]
1439
+ }
1440
+ ) : null,
1441
+ attachments.length ? /* @__PURE__ */ jsx(MessageAttachments, { attachments, mine, onOpenImage: props.onOpenImage, testID }) : null,
1442
+ message && cards.length ? cards.map((a, i) => /* @__PURE__ */ jsx(
1443
+ AppAttachment,
1444
+ {
1445
+ attachment: a,
1446
+ message,
1447
+ kind: attachmentKindOf(kinds, a.kind),
1448
+ url: (a.path ? urls[a.path] : null) ?? a.url ?? null,
1449
+ mine,
1450
+ slot,
1451
+ onOpenAttachment: props.onOpenAttachment,
1452
+ onEmpty,
1453
+ run: (task) => void runs.run(a.id, task),
1454
+ state: runs.stateOf(a.id),
1455
+ testID: `${testID}-card-${i}`
1456
+ },
1457
+ a.id
1458
+ )) : null,
1459
+ message?.deletedAt ? /* @__PURE__ */ jsx(NativeText, { className: fg, style: [textStyle, { fontStyle: "italic", opacity: 0.7 }], children: copy.deleted }) : body ? /* @__PURE__ */ jsx(NativeText, { className: fg, style: [textStyle, attachments.length ? { paddingHorizontal: 8, paddingBottom: 4 } : null], children: displayBody(body) }) : null,
1460
+ moderation ? /* @__PURE__ */ jsx(ModerationMarker, { note: moderation, mine, testID: `${testID}-moderation` }) : null,
1461
+ preview ? /* @__PURE__ */ jsx(ChatLinkPreviewCard, { preview, mine, images: props.linkPreviewImages, testID: `${testID}-preview` }) : null
1462
+ ]
1463
+ }
1464
+ )
1248
1465
  }
1249
1466
  )
1250
- }
1251
- )
1252
- ] }),
1253
- reactions.length ? /* @__PURE__ */ jsx(View, { style: { flexDirection: "row", flexWrap: "wrap", gap: 4, marginTop: 2, justifyContent: mine ? "flex-end" : "flex-start", marginLeft: group && !mine ? 40 : 0 }, children: reactions.map((r) => /* @__PURE__ */ jsxs(
1254
- Pressable,
1255
- {
1256
- testID: `${testID}-reaction-${r.emoji}`,
1257
- accessibilityRole: "button",
1258
- accessibilityState: { selected: r.mine },
1259
- accessibilityLabel: `${r.emoji} ${r.count}`,
1260
- disabled: !message || !props.onToggleReaction,
1261
- onPress: () => message && props.onToggleReaction?.(message, r.emoji, !r.mine),
1262
- hitSlop: 6,
1263
- style: {
1264
- flexDirection: "row",
1265
- alignItems: "center",
1266
- gap: 4,
1267
- minHeight: 32,
1268
- paddingHorizontal: 10,
1269
- borderRadius: 16,
1270
- backgroundColor: r.mine ? colors.fill : colors.surface,
1271
- borderWidth: r.mine ? 1 : 0,
1272
- borderColor: colors.tint
1273
- },
1274
- children: [
1275
- /* @__PURE__ */ jsx(NativeText, { style: { fontSize: 15 }, children: r.emoji }),
1276
- /* @__PURE__ */ jsx(NativeText, { preset: "caption", style: { color: colors.secondaryLabel }, children: String(r.count) })
1277
- ]
1278
- },
1279
- r.emoji
1280
- )) }) : null,
1281
- row.groupEnd || failed ? /* @__PURE__ */ jsx(View, { style: { flexDirection: "row", justifyContent: mine ? "flex-end" : "flex-start", alignItems: "center", gap: 4, marginTop: 2, marginLeft: group && !mine ? 52 : 12, marginRight: 12 }, children: failed ? /* @__PURE__ */ jsxs(Fragment, { children: [
1282
- /* @__PURE__ */ jsx(NativeText, { testID: `${testID}-failed`, preset: "caption", style: { color: colors.destructive }, children: pending?.error ?? copy.notSent }),
1283
- lost ? props.onReattach ? /* @__PURE__ */ jsx(SmallAction, { testID: `${testID}-reattach`, label: copy.attachAgain, onPress: () => props.onReattach?.(pending.clientId, pending.files ?? []), color: colors.tint }) : null : /* @__PURE__ */ jsx(SmallAction, { testID: `${testID}-retry`, label: copy.retry, onPress: () => props.onRetry?.(pending.clientId), color: colors.tint }),
1284
- /* @__PURE__ */ jsx(SmallAction, { testID: `${testID}-discard`, label: copy.discard, onPress: () => props.onDiscard?.(pending.clientId), color: colors.destructive })
1285
- ] }) : /* @__PURE__ */ jsx(NativeText, { preset: "caption", style: { color: colors.tertiaryLabel }, children: [time, message?.editedAt ? copy.edited : null, sending ? copy.sending : null].filter(Boolean).join(" \xB7 ") }) }) : null
1286
- ] });
1467
+ ] }),
1468
+ reactions.length ? /* @__PURE__ */ jsx(View, { style: { flexDirection: "row", flexWrap: "wrap", gap: 4, marginTop: 2, justifyContent: mine ? "flex-end" : "flex-start", marginLeft: group && !mine ? 40 : 0 }, children: reactions.map((r) => /* @__PURE__ */ jsxs(
1469
+ Pressable,
1470
+ {
1471
+ testID: `${testID}-reaction-${r.emoji}`,
1472
+ accessibilityRole: "button",
1473
+ accessibilityState: { selected: r.mine },
1474
+ accessibilityLabel: `${r.emoji} ${r.count}`,
1475
+ disabled: !message || !props.onToggleReaction,
1476
+ onPress: () => message && props.onToggleReaction?.(message, r.emoji, !r.mine),
1477
+ hitSlop: 6,
1478
+ style: {
1479
+ flexDirection: "row",
1480
+ alignItems: "center",
1481
+ gap: 4,
1482
+ minHeight: 32,
1483
+ paddingHorizontal: 10,
1484
+ borderRadius: 16,
1485
+ backgroundColor: r.mine ? colors.fill : colors.surface,
1486
+ borderWidth: r.mine ? 1 : 0,
1487
+ borderColor: colors.tint
1488
+ },
1489
+ children: [
1490
+ /* @__PURE__ */ jsx(NativeText, { style: { fontSize: 15 }, children: r.emoji }),
1491
+ /* @__PURE__ */ jsx(NativeText, { preset: "caption", style: { color: colors.secondaryLabel }, children: String(r.count) })
1492
+ ]
1493
+ },
1494
+ r.emoji
1495
+ )) }) : null,
1496
+ row.groupEnd || failed ? /* @__PURE__ */ jsx(View, { style: { flexDirection: "row", justifyContent: mine ? "flex-end" : "flex-start", alignItems: "center", gap: 4, marginTop: 2, marginLeft: group && !mine ? 52 : 12, marginRight: 12 }, children: failed ? /* @__PURE__ */ jsxs(Fragment, { children: [
1497
+ /* @__PURE__ */ jsx(NativeText, { testID: `${testID}-failed`, preset: "caption", style: { color: colors.destructive }, children: pending?.error ?? copy.notSent }),
1498
+ lost ? props.onReattach ? /* @__PURE__ */ jsx(SmallAction, { testID: `${testID}-reattach`, label: copy.attachAgain, onPress: () => props.onReattach?.(pending.clientId, pending.files ?? []), color: colors.tint }) : null : /* @__PURE__ */ jsx(SmallAction, { testID: `${testID}-retry`, label: copy.retry, onPress: () => props.onRetry?.(pending.clientId), color: colors.tint }),
1499
+ /* @__PURE__ */ jsx(SmallAction, { testID: `${testID}-discard`, label: copy.discard, onPress: () => props.onDiscard?.(pending.clientId), color: colors.destructive })
1500
+ ] }) : /* @__PURE__ */ jsx(NativeText, { preset: "caption", style: { color: colors.tertiaryLabel }, children: [time, message?.editedAt ? copy.edited : null, sending ? copy.sending : null].filter(Boolean).join(" \xB7 ") }) }) : null
1501
+ ]
1502
+ }
1503
+ );
1287
1504
  }
1288
1505
  function SmallAction({ label, onPress, color, testID }) {
1289
1506
  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 }) });
@@ -4134,6 +4351,8 @@ function ChatThreadScreen(props) {
4134
4351
  const { conversationId, threadRootId = null, keyboardVerticalOffset, onOpenAttachment, jumpTo, topic: topicProp, showTopics = false, scheduling = true, availability = true, focused = true, testID = "chat-thread" } = props;
4135
4352
  const chat = useChat();
4136
4353
  const { userId, videoUpload, attachmentKinds, outbox } = chat;
4354
+ const attachmentSlot = props.slots?.attachment;
4355
+ const cardActionTimeoutMs = props.cardActionTimeoutMs;
4137
4356
  const linkPreviewImages = chat.linkPreviewImages === true;
4138
4357
  const topicsState = useTopics(threadRootId ? null : conversationId);
4139
4358
  const [chosenTopic, setChosenTopic] = useState(null);
@@ -4214,9 +4433,9 @@ function ChatThreadScreen(props) {
4214
4433
  }, [modules.haptics, toast, copy.actionFailed]);
4215
4434
  const reply = useCallback(
4216
4435
  (m) => {
4217
- setReply(replyTargetFor(m, nameOf(m.senderId), previewText(displayBody(m.body), m.attachments[0]?.kind, Boolean(m.deletedAt))));
4436
+ setReply(replyTargetFor(m, nameOf(m.senderId), attachmentPreview(displayBody(m.body), m.attachments[0], attachmentKinds, Boolean(m.deletedAt))));
4218
4437
  },
4219
- [setReply, nameOf]
4438
+ [setReply, nameOf, attachmentKinds]
4220
4439
  );
4221
4440
  const onAction = useCallback(
4222
4441
  (m, action) => {
@@ -4313,7 +4532,7 @@ function ChatThreadScreen(props) {
4313
4532
  },
4314
4533
  onLanded: (id) => {
4315
4534
  const m = byId.get(id);
4316
- if (m) AccessibilityInfo.announceForAccessibility?.(fill(copy.jumpedTo, { name: nameOf(m.senderId), text: previewText(displayBody(m.body), m.attachments[0]?.kind, Boolean(m.deletedAt)) }));
4535
+ if (m) AccessibilityInfo.announceForAccessibility?.(fill(copy.jumpedTo, { name: nameOf(m.senderId), text: attachmentPreview(displayBody(m.body), m.attachments[0], attachmentKinds, Boolean(m.deletedAt)) }));
4317
4536
  },
4318
4537
  onGone: () => {
4319
4538
  toast.show({ title: copy.messageGone });
@@ -4352,11 +4571,13 @@ function ChatThreadScreen(props) {
4352
4571
  linkPreviewImages,
4353
4572
  attachmentKinds,
4354
4573
  onOpenAttachment,
4574
+ attachmentSlot,
4575
+ cardActionTimeoutMs,
4355
4576
  testID: `${testID}-message-${m?.id ?? row.key}`
4356
4577
  }
4357
4578
  );
4358
4579
  },
4359
- [testID, copy.newMessages, userId, urls, byId, group, reducedMotion, onLongPress, allowed, reply, onSwipeReply, onRetry, onDiscard, react, onOpenImage, picker, reattachable, onReattach, previews, linkPreviewImages, attachmentKinds, onOpenAttachment, observer, highlightedId]
4580
+ [testID, copy.newMessages, userId, urls, byId, group, reducedMotion, onLongPress, allowed, reply, onSwipeReply, onRetry, onDiscard, react, onOpenImage, picker, reattachable, onReattach, previews, linkPreviewImages, attachmentKinds, onOpenAttachment, attachmentSlot, cardActionTimeoutMs, observer, highlightedId]
4360
4581
  );
4361
4582
  const [videoOpen, setVideoOpen] = useState(false);
4362
4583
  const preparedVideo = useRef(null);
@@ -4949,59 +5170,61 @@ function ChatSearchScreen({ conversationId, onOpen, conversationTitle, debounceM
4949
5170
  useEffect(() => {
4950
5171
  if (announce) AccessibilityInfo.announceForAccessibility?.(announce);
4951
5172
  }, [announce]);
4952
- return /* @__PURE__ */ jsxs(SafeAreaView, { testID, edges: ["left", "right"], style: { flex: 1, backgroundColor: colors.groupedBackground }, children: [
4953
- /* @__PURE__ */ jsx(View, { style: { paddingHorizontal: 16, paddingVertical: 8 }, children: /* @__PURE__ */ jsx(
4954
- NativeSearchBar,
4955
- {
4956
- testID: `${testID}-field`,
4957
- value,
4958
- onChange: setValue,
4959
- placeholder: conversationId ? copy.searchThreadPlaceholder : copy.searchPlaceholder,
4960
- cancelLabel: copy.cancel,
4961
- showCancel: false
4962
- }
4963
- ) }),
4964
- short ? /* @__PURE__ */ jsx(NativeText, { preset: "caption", style: { paddingHorizontal: 20, color: colors.secondaryLabel }, testID: `${testID}-hint`, children: copy.searchHint }) : isSearching && hits.length === 0 ? /* @__PURE__ */ jsxs(View, { testID: `${testID}-loading`, style: { padding: 16, gap: 10 }, children: [
4965
- /* @__PURE__ */ jsx(NativeSkeleton, { height: 52, className: "w-full" }),
4966
- /* @__PURE__ */ jsx(NativeSkeleton, { height: 52, className: "w-full" })
4967
- ] }) : hits.length === 0 ? /* @__PURE__ */ jsx(NativeEmptyState, { variant: "no-results", title: copy.searchNoResults, body: fill(copy.searchNoResultsBody, { query }), testID: `${testID}-empty` }) : /* @__PURE__ */ jsx(
4968
- FlatList,
4969
- {
4970
- testID: `${testID}-results`,
4971
- data: hits,
4972
- keyExtractor: (h) => `${h.messageId}:${h.inTranscript}`,
4973
- keyboardShouldPersistTaps: "handled",
4974
- keyboardDismissMode: "on-drag",
4975
- renderItem: ({ item: h }) => {
4976
- const who = nameOf(h.senderId);
4977
- const where = conversationId ? null : fill(copy.searchIn, { name: titleOf(h.conversationId) });
4978
- const when = formatInboxTime(new Date(h.createdAt), now());
4979
- return /* @__PURE__ */ jsxs(
4980
- HitRow,
4981
- {
4982
- testID: `${testID}-hit-${h.messageId}`,
4983
- accessibilityLabel: [who, where, when, h.inTranscript ? copy.transcript : null, snippetText(h.snippet)].filter(Boolean).join(", "),
4984
- onPress: () => onOpen(h),
4985
- children: [
4986
- /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", alignItems: "center", gap: 6 }, children: [
4987
- /* @__PURE__ */ jsx(NativeText, { preset: "label", numberOfLines: 1, style: { flexShrink: 1 }, children: who }),
4988
- where ? /* @__PURE__ */ jsx(NativeText, { preset: "caption", numberOfLines: 1, style: { flexShrink: 1, color: colors.secondaryLabel }, children: where }) : null,
4989
- /* @__PURE__ */ jsx(NativeText, { preset: "caption", style: { marginLeft: "auto", color: colors.tertiaryLabel }, children: when })
4990
- ] }),
4991
- /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", alignItems: "flex-start", gap: 6 }, children: [
4992
- h.inTranscript ? /* @__PURE__ */ jsx(NativeBadge, { variant: "muted", testID: `${testID}-transcript-${h.messageId}`, children: /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", alignItems: "center", gap: 3 }, children: [
4993
- /* @__PURE__ */ jsx(NativeIcon, { name: "mic", size: 11, color: colors.secondaryLabel }),
4994
- /* @__PURE__ */ jsx(NativeText, { preset: "caption", style: { color: colors.secondaryLabel }, children: copy.transcript })
4995
- ] }) }) : null,
4996
- /* @__PURE__ */ jsx(View, { style: { flex: 1 }, children: /* @__PURE__ */ jsx(SearchSnippet, { snippet: h.snippet }) })
4997
- ] })
4998
- ]
4999
- }
5000
- );
5001
- }
5173
+ const field = /* @__PURE__ */ jsx(View, { style: { paddingHorizontal: 16, paddingVertical: 8 }, children: /* @__PURE__ */ jsx(
5174
+ NativeSearchBar,
5175
+ {
5176
+ testID: `${testID}-field`,
5177
+ value,
5178
+ onChange: setValue,
5179
+ placeholder: conversationId ? copy.searchThreadPlaceholder : copy.searchPlaceholder,
5180
+ cancelLabel: copy.cancel,
5181
+ showCancel: false
5182
+ }
5183
+ ) });
5184
+ const empty = short ? /* @__PURE__ */ jsx(NativeText, { preset: "caption", style: { paddingHorizontal: 20, color: colors.secondaryLabel }, testID: `${testID}-hint`, children: copy.searchHint }) : isSearching ? /* @__PURE__ */ jsxs(View, { testID: `${testID}-loading`, style: { padding: 16, gap: 10 }, children: [
5185
+ /* @__PURE__ */ jsx(NativeSkeleton, { height: 52, className: "w-full" }),
5186
+ /* @__PURE__ */ jsx(NativeSkeleton, { height: 52, className: "w-full" })
5187
+ ] }) : /* @__PURE__ */ jsx(NativeEmptyState, { variant: "no-results", title: copy.searchNoResults, body: fill(copy.searchNoResultsBody, { query }), testID: `${testID}-empty` });
5188
+ return /* @__PURE__ */ jsx(SafeAreaView, { testID, edges: ["left", "right"], style: { flex: 1, backgroundColor: colors.groupedBackground }, children: /* @__PURE__ */ jsx(
5189
+ FlatList,
5190
+ {
5191
+ testID: `${testID}-results`,
5192
+ data: short ? [] : hits,
5193
+ keyExtractor: (h) => `${h.messageId}:${h.inTranscript}`,
5194
+ contentInsetAdjustmentBehavior: "automatic",
5195
+ keyboardShouldPersistTaps: "handled",
5196
+ keyboardDismissMode: "on-drag",
5197
+ ListHeaderComponent: field,
5198
+ ListEmptyComponent: empty,
5199
+ renderItem: ({ item: h }) => {
5200
+ const who = nameOf(h.senderId);
5201
+ const where = conversationId ? null : fill(copy.searchIn, { name: titleOf(h.conversationId) });
5202
+ const when = formatInboxTime(new Date(h.createdAt), now());
5203
+ return /* @__PURE__ */ jsxs(
5204
+ HitRow,
5205
+ {
5206
+ testID: `${testID}-hit-${h.messageId}`,
5207
+ accessibilityLabel: [who, where, when, h.inTranscript ? copy.transcript : null, snippetText(h.snippet)].filter(Boolean).join(", "),
5208
+ onPress: () => onOpen(h),
5209
+ children: [
5210
+ /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", alignItems: "center", gap: 6 }, children: [
5211
+ /* @__PURE__ */ jsx(NativeText, { preset: "label", numberOfLines: 1, style: { flexShrink: 1 }, children: who }),
5212
+ where ? /* @__PURE__ */ jsx(NativeText, { preset: "caption", numberOfLines: 1, style: { flexShrink: 1, color: colors.secondaryLabel }, children: where }) : null,
5213
+ /* @__PURE__ */ jsx(NativeText, { preset: "caption", style: { marginLeft: "auto", color: colors.tertiaryLabel }, children: when })
5214
+ ] }),
5215
+ /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", alignItems: "flex-start", gap: 6 }, children: [
5216
+ h.inTranscript ? /* @__PURE__ */ jsx(NativeBadge, { variant: "muted", testID: `${testID}-transcript-${h.messageId}`, children: /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", alignItems: "center", gap: 3 }, children: [
5217
+ /* @__PURE__ */ jsx(NativeIcon, { name: "mic", size: 11, color: colors.secondaryLabel }),
5218
+ /* @__PURE__ */ jsx(NativeText, { preset: "caption", style: { color: colors.secondaryLabel }, children: copy.transcript })
5219
+ ] }) }) : null,
5220
+ /* @__PURE__ */ jsx(View, { style: { flex: 1 }, children: /* @__PURE__ */ jsx(SearchSnippet, { snippet: h.snippet }) })
5221
+ ] })
5222
+ ]
5223
+ }
5224
+ );
5002
5225
  }
5003
- )
5004
- ] });
5226
+ }
5227
+ ) });
5005
5228
  }
5006
5229
  function HitRow({ testID, accessibilityLabel, onPress, children }) {
5007
5230
  const colors = useThemeRoles();
@@ -5021,6 +5244,6 @@ function HitRow({ testID, accessibilityLabel, onPress, children }) {
5021
5244
  );
5022
5245
  }
5023
5246
 
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 };
5247
+ export { AT_BOTTOM_POINTS, AppAttachment, BUILT_IN_KINDS, CARD_ACTION_TIMEOUT_MS, CardError, 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, TRANSCRIPT_LINES, TUS_CHUNK_BYTES, VIDEO_CHUNK_BYTES, VIDEO_HOLD_MS, VIDEO_MIN_MS, VideoUploadTray, VoiceNotePlayer, VoiceTranscript, activePlaybackId, attachmentKindOf, 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, slotUrl, snippetText, splitCardAttachments, threadListMode, timeZones, toSend, toggleVideo, toggleVideoMute, toggleVoiceNote, topicClosedFor, topicMenuFor, transcriptIsLong, useAwayLine, useCardRuns, useChatAutoRetry, useChatComposer, useChatNative, useChatThreadStackOptions, useJumpToMessage, usePlayingVideo, usePlayingVoiceNote, useScheduleSend, useVideoUploads, useVoiceRecorder, videoRecorderStep, videoSource, videoState, videoUploads, voiceNoteFile, waveformBars, weekFromHours, withDay, withTime };
5025
5248
  //# sourceMappingURL=native.js.map
5026
5249
  //# sourceMappingURL=native.js.map