@poolse/react-native 0.1.1 → 2.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -3,10 +3,33 @@
3
3
  var react$1 = require('@poolse/react');
4
4
  var react = require('react');
5
5
  var reactNative = require('react-native');
6
- var reactNativeSvg = require('react-native-svg');
7
6
  var jsxRuntime = require('react/jsx-runtime');
7
+ var reactNativeSvg = require('react-native-svg');
8
+ var ImagePicker = require('expo-image-picker');
9
+ var DocumentPicker = require('expo-document-picker');
10
+
11
+ function _interopNamespace(e) {
12
+ if (e && e.__esModule) return e;
13
+ var n = Object.create(null);
14
+ if (e) {
15
+ Object.keys(e).forEach(function (k) {
16
+ if (k !== 'default') {
17
+ var d = Object.getOwnPropertyDescriptor(e, k);
18
+ Object.defineProperty(n, k, d.get ? d : {
19
+ enumerable: true,
20
+ get: function () { return e[k]; }
21
+ });
22
+ }
23
+ });
24
+ }
25
+ n.default = e;
26
+ return Object.freeze(n);
27
+ }
8
28
 
9
- // src/ConversationView.tsx
29
+ var ImagePicker__namespace = /*#__PURE__*/_interopNamespace(ImagePicker);
30
+ var DocumentPicker__namespace = /*#__PURE__*/_interopNamespace(DocumentPicker);
31
+
32
+ // src/PoolseInbox.tsx
10
33
 
11
34
  // src/theme/default.ts
12
35
  var lightColors = {
@@ -172,6 +195,111 @@ function usePoolseTheme() {
172
195
  return react.useContext(ThemeContext) ?? defaultLightTheme;
173
196
  }
174
197
 
198
+ // src/primitives/userColor.ts
199
+ var PALETTE = [
200
+ "#6366f1",
201
+ // indigo
202
+ "#10b981",
203
+ // emerald
204
+ "#f59e0b",
205
+ // amber
206
+ "#ec4899",
207
+ // rose
208
+ "#0ea5e9",
209
+ // sky
210
+ "#8b5cf6",
211
+ // violet
212
+ "#14b8a6",
213
+ // teal
214
+ "#d946ef"
215
+ // fuchsia
216
+ ];
217
+ function userColor(id) {
218
+ let h = 0;
219
+ for (let i = 0; i < id.length; i++) {
220
+ h = h * 31 + id.charCodeAt(i) | 0;
221
+ }
222
+ return PALETTE[Math.abs(h) % PALETTE.length];
223
+ }
224
+ var SIZES = { sm: 28, md: 40, lg: 56 };
225
+ function Avatar({ src, name, seed, online, size = "md" }) {
226
+ const theme = usePoolseTheme();
227
+ const dim = SIZES[size];
228
+ const initials = computeInitials(name);
229
+ const tint = seed ? userColor(seed) : theme.colors.brand;
230
+ const fontSize = size === "sm" ? 11 : size === "lg" ? 22 : 15;
231
+ return /* @__PURE__ */ jsxRuntime.jsxs(
232
+ reactNative.View,
233
+ {
234
+ style: [
235
+ styles.root,
236
+ {
237
+ width: dim,
238
+ height: dim,
239
+ borderRadius: dim / 2,
240
+ backgroundColor: tint
241
+ }
242
+ ],
243
+ accessibilityRole: "image",
244
+ accessibilityLabel: name ?? "avatar",
245
+ children: [
246
+ src ? /* @__PURE__ */ jsxRuntime.jsx(
247
+ reactNative.Image,
248
+ {
249
+ source: { uri: src },
250
+ style: { width: dim, height: dim, borderRadius: dim / 2 },
251
+ accessible: false
252
+ }
253
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
254
+ reactNative.Text,
255
+ {
256
+ style: {
257
+ color: theme.colors.onBrand,
258
+ fontSize,
259
+ fontWeight: "600",
260
+ fontFamily: theme.type.fontBody
261
+ },
262
+ children: initials
263
+ }
264
+ ),
265
+ online ? /* @__PURE__ */ jsxRuntime.jsx(
266
+ reactNative.View,
267
+ {
268
+ style: [
269
+ styles.presence,
270
+ {
271
+ backgroundColor: theme.colors.presence,
272
+ borderColor: theme.colors.surface,
273
+ width: dim * 0.32,
274
+ height: dim * 0.32,
275
+ borderRadius: dim * 0.16
276
+ }
277
+ ]
278
+ }
279
+ ) : null
280
+ ]
281
+ }
282
+ );
283
+ }
284
+ function computeInitials(name) {
285
+ if (!name) return "?";
286
+ const parts = name.trim().split(/\s+/).slice(0, 2);
287
+ return parts.map((p) => p.charAt(0).toUpperCase()).join("").slice(0, 2) || "?";
288
+ }
289
+ var styles = reactNative.StyleSheet.create({
290
+ root: {
291
+ alignItems: "center",
292
+ justifyContent: "center",
293
+ overflow: "visible"
294
+ },
295
+ presence: {
296
+ position: "absolute",
297
+ bottom: 0,
298
+ right: 0,
299
+ borderWidth: 2
300
+ }
301
+ });
302
+
175
303
  // src/primitives/icons-data.ts
176
304
  var ICONS = {
177
305
  attachment: {
@@ -348,296 +476,950 @@ function PoolseIcon({ name, size = 20, color, label }) {
348
476
  const a11y = label === null ? { accessibilityRole: "none" } : { accessibilityRole: "image", accessibilityLabel: label ?? name };
349
477
  return /* @__PURE__ */ jsxRuntime.jsx(reactNativeSvg.SvgXml, { xml, width: size, height: size, ...a11y });
350
478
  }
351
- function AttachmentPicker({ visible, onClose, onPickInputs }) {
479
+ function MemberList({
480
+ conversationId,
481
+ labelFor,
482
+ avatarFor,
483
+ onlineExternalIds,
484
+ canRemove,
485
+ renderItem,
486
+ emptyState
487
+ }) {
352
488
  const theme = usePoolseTheme();
353
- const upload = react$1.useAttachmentUpload();
354
- const pickFromKind = async (kind) => {
355
- try {
356
- const adapter = onPickInputs ?? defaultAdapter;
357
- const inputs = await adapter(kind);
358
- for (const input of inputs) {
359
- void upload.upload(input);
360
- }
361
- } catch (err) {
362
- console.warn("[poolse/picker]", err);
363
- } finally {
364
- onClose();
365
- }
366
- };
367
- return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Modal, { visible, transparent: true, animationType: "slide", onRequestClose: onClose, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { style: styles.backdrop, onPress: onClose, children: /* @__PURE__ */ jsxRuntime.jsxs(
368
- reactNative.View,
489
+ const { members, loading, error, removeMember } = react$1.useMembers(conversationId);
490
+ const { online: presenceOnline } = react$1.usePresence(
491
+ onlineExternalIds === void 0 ? conversationId : ""
492
+ );
493
+ const effectiveOnline = onlineExternalIds ?? presenceOnline;
494
+ if (loading && members.length === 0) {
495
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles2.placeholder, { backgroundColor: theme.colors.paper }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: theme.colors.brand }) });
496
+ }
497
+ if (error) {
498
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles2.placeholder, { backgroundColor: theme.colors.paper }], children: [
499
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: { color: theme.colors.error, fontFamily: theme.type.fontBody }, children: "Failed to load members." }),
500
+ error.message ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.errorDetail, { color: theme.colors.ink3 }], children: error.message }) : null
501
+ ] });
502
+ }
503
+ if (members.length === 0) {
504
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles2.placeholder, { backgroundColor: theme.colors.paper }], children: typeof emptyState === "string" || emptyState == null ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: { color: theme.colors.ink3, fontFamily: theme.type.fontBody }, children: emptyState ?? "No members." }) : emptyState });
505
+ }
506
+ return /* @__PURE__ */ jsxRuntime.jsx(
507
+ reactNative.FlatList,
369
508
  {
370
- style: [
371
- styles.sheet,
372
- {
373
- backgroundColor: theme.colors.surface,
374
- borderTopLeftRadius: theme.radii.lg,
375
- borderTopRightRadius: theme.radii.lg
376
- },
377
- theme.shadows.lg
378
- ],
379
- children: [
380
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Pressable, { style: styles.item, onPress: () => pickFromKind("image"), children: [
381
- /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "attachment", size: 20, color: theme.colors.ink2 }),
382
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.itemText, { color: theme.colors.ink }], children: "Photo or video" })
383
- ] }),
384
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Pressable, { style: styles.item, onPress: () => pickFromKind("document"), children: [
385
- /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "attachment", size: 20, color: theme.colors.ink2 }),
386
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.itemText, { color: theme.colors.ink }], children: "File" })
387
- ] }),
388
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { style: styles.cancel, onPress: onClose, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.itemText, { color: theme.colors.brand }], children: "Cancel" }) })
389
- ]
509
+ data: members,
510
+ keyExtractor: (m) => m.user_id,
511
+ style: { backgroundColor: theme.colors.paper },
512
+ renderItem: ({ item }) => {
513
+ if (renderItem) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderItem(item) });
514
+ const name = labelFor?.(item.external_id) ?? item.external_id;
515
+ const avatarUrl = avatarFor?.(item.external_id) ?? null;
516
+ const isOnline = effectiveOnline.has(item.external_id);
517
+ const showRemove = canRemove?.(item) ?? false;
518
+ return /* @__PURE__ */ jsxRuntime.jsx(
519
+ MemberRow,
520
+ {
521
+ name,
522
+ externalId: item.external_id,
523
+ avatarUrl,
524
+ role: item.role,
525
+ online: isOnline,
526
+ ...showRemove ? { onRemove: () => removeMember(item.external_id) } : {}
527
+ }
528
+ );
529
+ }
390
530
  }
391
- ) }) });
392
- }
393
- async function defaultAdapter(kind) {
394
- if (kind === "image") {
395
- const mod2 = await tryImport("expo-image-picker");
396
- if (!mod2)
397
- throw new Error("expo-image-picker not installed \u2014 `npx expo install expo-image-picker`");
398
- const res2 = await mod2.launchImageLibraryAsync({ mediaTypes: "images", quality: 0.85 });
399
- if (res2.canceled) return [];
400
- return res2.assets.map(
401
- (asset) => ({
402
- body: {
403
- uri: asset.uri,
404
- name: asset.fileName ?? "image.jpg",
405
- type: asset.mimeType ?? "image/jpeg"
406
- },
407
- contentType: asset.mimeType ?? "image/jpeg",
408
- byteSize: asset.fileSize ?? 0,
409
- filename: asset.fileName ?? "image.jpg"
410
- })
411
- );
412
- }
413
- const mod = await tryImport("expo-document-picker");
414
- if (!mod)
415
- throw new Error("expo-document-picker not installed \u2014 `npx expo install expo-document-picker`");
416
- const res = await mod.getDocumentAsync({ copyToCacheDirectory: true, multiple: true });
417
- if (res.canceled) return [];
418
- return res.assets.map(
419
- (asset) => ({
420
- body: {
421
- uri: asset.uri,
422
- name: asset.name,
423
- type: asset.mimeType ?? "application/octet-stream"
424
- },
425
- contentType: asset.mimeType ?? "application/octet-stream",
426
- byteSize: asset.size ?? 0,
427
- filename: asset.name
428
- })
429
531
  );
430
532
  }
431
- async function tryImport(name) {
432
- try {
433
- return await new Function("m", "return import(m)")(name);
434
- } catch {
435
- return null;
436
- }
533
+ function MemberRow({
534
+ name,
535
+ externalId,
536
+ avatarUrl,
537
+ role,
538
+ online,
539
+ onRemove
540
+ }) {
541
+ const theme = usePoolseTheme();
542
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.row, children: [
543
+ /* @__PURE__ */ jsxRuntime.jsx(Avatar, { src: avatarUrl, name, seed: externalId, online, size: "md" }),
544
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.body, children: [
545
+ /* @__PURE__ */ jsxRuntime.jsx(
546
+ reactNative.Text,
547
+ {
548
+ style: [styles2.name, { color: theme.colors.ink, fontFamily: theme.type.fontBody }],
549
+ numberOfLines: 1,
550
+ children: name
551
+ }
552
+ ),
553
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.role, { color: theme.colors.ink3 }], children: role })
554
+ ] }),
555
+ onRemove ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: onRemove, hitSlop: 8, style: styles2.removeBtn, children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "close", size: 16, color: theme.colors.ink2 }) }) : null
556
+ ] });
437
557
  }
438
- var styles = reactNative.StyleSheet.create({
439
- backdrop: {
558
+ var styles2 = reactNative.StyleSheet.create({
559
+ placeholder: {
440
560
  flex: 1,
441
- backgroundColor: "rgba(0,0,0,0.35)",
442
- justifyContent: "flex-end"
561
+ alignItems: "center",
562
+ justifyContent: "center",
563
+ padding: 24
443
564
  },
444
- sheet: {
445
- paddingVertical: 8,
446
- paddingBottom: 24
565
+ errorDetail: {
566
+ marginTop: 6,
567
+ fontSize: 11
447
568
  },
448
- item: {
569
+ row: {
449
570
  flexDirection: "row",
450
571
  alignItems: "center",
451
- paddingHorizontal: 20,
452
- paddingVertical: 14,
453
- gap: 14
572
+ paddingHorizontal: 12,
573
+ paddingVertical: 8,
574
+ gap: 10
454
575
  },
455
- cancel: {
456
- paddingHorizontal: 20,
457
- paddingVertical: 14,
458
- alignItems: "center",
459
- marginTop: 4
576
+ body: {
577
+ flex: 1,
578
+ gap: 1
460
579
  },
461
- itemText: {
462
- fontSize: 15,
580
+ name: {
581
+ fontSize: 14,
463
582
  fontWeight: "500"
583
+ },
584
+ role: {
585
+ fontSize: 11,
586
+ textTransform: "capitalize"
587
+ },
588
+ removeBtn: {
589
+ padding: 6
464
590
  }
465
- });
466
-
467
- // src/primitives/userColor.ts
468
- var PALETTE = [
469
- "#6366f1",
470
- // indigo
471
- "#10b981",
472
- // emerald
473
- "#f59e0b",
474
- // amber
475
- "#ec4899",
476
- // rose
477
- "#0ea5e9",
478
- // sky
479
- "#8b5cf6",
480
- // violet
481
- "#14b8a6",
482
- // teal
483
- "#d946ef"
484
- // fuchsia
485
- ];
486
- function userColor(id) {
487
- let h = 0;
488
- for (let i = 0; i < id.length; i++) {
489
- h = h * 31 + id.charCodeAt(i) | 0;
490
- }
491
- return PALETTE[Math.abs(h) % PALETTE.length];
591
+ });
592
+ function useDismissableSheet({ onClose, sheetHeight, dismissThreshold = 0.22 }) {
593
+ const translateY = react.useRef(new reactNative.Animated.Value(0)).current;
594
+ const panResponder = react.useMemo(
595
+ () => reactNative.PanResponder.create({
596
+ onMoveShouldSetPanResponder: (_, g) => {
597
+ return g.dy > 6 && g.dy > Math.abs(g.dx);
598
+ },
599
+ onPanResponderMove: (_, g) => {
600
+ translateY.setValue(Math.max(0, g.dy));
601
+ },
602
+ onPanResponderRelease: (_, g) => {
603
+ const committed = g.dy > sheetHeight * dismissThreshold || g.vy > 0.6;
604
+ if (committed) {
605
+ reactNative.Animated.timing(translateY, {
606
+ toValue: sheetHeight,
607
+ duration: 180,
608
+ useNativeDriver: true
609
+ }).start(() => {
610
+ translateY.setValue(0);
611
+ onClose();
612
+ });
613
+ } else {
614
+ reactNative.Animated.spring(translateY, {
615
+ toValue: 0,
616
+ useNativeDriver: true,
617
+ speed: 22,
618
+ bounciness: 4
619
+ }).start();
620
+ }
621
+ },
622
+ onPanResponderTerminate: () => {
623
+ reactNative.Animated.spring(translateY, {
624
+ toValue: 0,
625
+ useNativeDriver: true,
626
+ speed: 22,
627
+ bounciness: 4
628
+ }).start();
629
+ }
630
+ }),
631
+ [onClose, sheetHeight, dismissThreshold, translateY]
632
+ );
633
+ return { translateY, panHandlers: panResponder.panHandlers };
492
634
  }
493
- var SIZES = { sm: 28, md: 40, lg: 56 };
494
- function Avatar({ src, name, seed, online, size = "md" }) {
635
+ function GroupDetailsSheet({
636
+ visible,
637
+ onClose,
638
+ conversationId,
639
+ labelFor,
640
+ avatarFor,
641
+ canRemoveMember
642
+ }) {
495
643
  const theme = usePoolseTheme();
496
- const dim = SIZES[size];
497
- const initials = computeInitials(name);
498
- const tint = seed ? userColor(seed) : theme.colors.brand;
499
- const fontSize = size === "sm" ? 11 : size === "lg" ? 22 : 15;
500
- return /* @__PURE__ */ jsxRuntime.jsxs(
501
- reactNative.View,
644
+ const { conversation } = react$1.useConversation(conversationId);
645
+ const { members } = react$1.useMembers(conversationId);
646
+ const { height: screenHeight } = reactNative.useWindowDimensions();
647
+ const { translateY, panHandlers } = useDismissableSheet({
648
+ onClose,
649
+ sheetHeight: screenHeight * 0.85
650
+ });
651
+ const title = conversation?.name ?? "Conversation";
652
+ const subtitle = `${members.length} ${members.length === 1 ? "member" : "members"}`;
653
+ return /* @__PURE__ */ jsxRuntime.jsx(
654
+ reactNative.Modal,
502
655
  {
503
- style: [
504
- styles2.root,
505
- {
506
- width: dim,
507
- height: dim,
508
- borderRadius: dim / 2,
509
- backgroundColor: tint
510
- }
511
- ],
512
- accessibilityRole: "image",
513
- accessibilityLabel: name ?? "avatar",
514
- children: [
515
- src ? /* @__PURE__ */ jsxRuntime.jsx(
516
- reactNative.Image,
517
- {
518
- source: { uri: src },
519
- style: { width: dim, height: dim, borderRadius: dim / 2 },
520
- accessible: false
521
- }
522
- ) : /* @__PURE__ */ jsxRuntime.jsx(
523
- reactNative.Text,
656
+ visible,
657
+ transparent: true,
658
+ animationType: "slide",
659
+ onRequestClose: onClose,
660
+ statusBarTranslucent: true,
661
+ children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles3.modalRoot, children: [
662
+ /* @__PURE__ */ jsxRuntime.jsx(
663
+ reactNative.Pressable,
524
664
  {
525
- style: {
526
- color: theme.colors.onBrand,
527
- fontSize,
528
- fontWeight: "600",
529
- fontFamily: theme.type.fontBody
530
- },
531
- children: initials
665
+ style: styles3.backdrop,
666
+ onPress: onClose,
667
+ accessibilityLabel: "Close group details"
532
668
  }
533
669
  ),
534
- online ? /* @__PURE__ */ jsxRuntime.jsx(
535
- reactNative.View,
670
+ /* @__PURE__ */ jsxRuntime.jsxs(
671
+ reactNative.Animated.View,
536
672
  {
537
673
  style: [
538
- styles2.presence,
674
+ styles3.sheet,
539
675
  {
540
- backgroundColor: theme.colors.presence,
541
- borderColor: theme.colors.surface,
542
- width: dim * 0.32,
543
- height: dim * 0.32,
544
- borderRadius: dim * 0.16
545
- }
676
+ backgroundColor: theme.colors.paper,
677
+ borderTopLeftRadius: theme.radii.xl,
678
+ borderTopRightRadius: theme.radii.xl,
679
+ transform: [{ translateY }]
680
+ },
681
+ theme.shadows.lg
682
+ ],
683
+ children: [
684
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ...panHandlers, style: styles3.dragHandle, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles3.dragPill, { backgroundColor: theme.colors.ink3 }] }) }),
685
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles3.headerRow, children: [
686
+ /* @__PURE__ */ jsxRuntime.jsx(
687
+ reactNative.Pressable,
688
+ {
689
+ onPress: onClose,
690
+ hitSlop: 16,
691
+ style: [styles3.closeBtn, { backgroundColor: theme.colors.surface2 }],
692
+ accessibilityLabel: "Close",
693
+ children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "close", size: 18, color: theme.colors.ink })
694
+ }
695
+ ),
696
+ /* @__PURE__ */ jsxRuntime.jsx(
697
+ reactNative.Text,
698
+ {
699
+ style: [
700
+ styles3.headerTitle,
701
+ { color: theme.colors.ink, fontFamily: theme.type.fontDisplay }
702
+ ],
703
+ children: "Group details"
704
+ }
705
+ )
706
+ ] }),
707
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles3.summary, children: [
708
+ /* @__PURE__ */ jsxRuntime.jsx(Avatar, { src: null, name: title, seed: conversationId, size: "lg" }),
709
+ /* @__PURE__ */ jsxRuntime.jsx(
710
+ reactNative.Text,
711
+ {
712
+ style: [
713
+ styles3.title,
714
+ { color: theme.colors.ink, fontFamily: theme.type.fontDisplay }
715
+ ],
716
+ numberOfLines: 1,
717
+ children: title
718
+ }
719
+ ),
720
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles3.subtitle, { color: theme.colors.ink3 }], children: subtitle })
721
+ ] }),
722
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles3.divider, { backgroundColor: theme.colors.border }] }),
723
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles3.sectionLabelRow, children: /* @__PURE__ */ jsxRuntime.jsx(
724
+ reactNative.Text,
725
+ {
726
+ style: [
727
+ styles3.sectionLabel,
728
+ { color: theme.colors.ink2, fontFamily: theme.type.fontBody }
729
+ ],
730
+ children: "Members"
731
+ }
732
+ ) }),
733
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(
734
+ MemberList,
735
+ {
736
+ conversationId,
737
+ ...labelFor ? { labelFor } : {},
738
+ ...avatarFor ? { avatarFor } : {},
739
+ ...canRemoveMember ? { canRemove: canRemoveMember } : {},
740
+ emptyState: "No members yet."
741
+ }
742
+ ) })
546
743
  ]
547
744
  }
548
- ) : null
549
- ]
745
+ )
746
+ ] })
550
747
  }
551
748
  );
552
749
  }
553
- function computeInitials(name) {
554
- if (!name) return "?";
555
- const parts = name.trim().split(/\s+/).slice(0, 2);
556
- return parts.map((p) => p.charAt(0).toUpperCase()).join("").slice(0, 2) || "?";
557
- }
558
- var styles2 = reactNative.StyleSheet.create({
559
- root: {
750
+ var styles3 = reactNative.StyleSheet.create({
751
+ modalRoot: {
752
+ flex: 1,
753
+ justifyContent: "flex-end"
754
+ },
755
+ backdrop: {
756
+ ...reactNative.StyleSheet.absoluteFillObject,
757
+ backgroundColor: "rgba(0,0,0,0.4)"
758
+ },
759
+ sheet: {
760
+ height: "85%",
761
+ overflow: "hidden"
762
+ },
763
+ dragHandle: {
560
764
  alignItems: "center",
561
- justifyContent: "center",
562
- overflow: "visible"
765
+ paddingTop: 8,
766
+ paddingBottom: 4
563
767
  },
564
- presence: {
565
- position: "absolute",
566
- bottom: 0,
567
- right: 0,
568
- borderWidth: 2
768
+ dragPill: {
769
+ width: 36,
770
+ height: 4,
771
+ borderRadius: 2,
772
+ opacity: 0.4
773
+ },
774
+ headerRow: {
775
+ flexDirection: "row",
776
+ alignItems: "center",
777
+ paddingHorizontal: 12,
778
+ paddingTop: 4,
779
+ paddingBottom: 12,
780
+ gap: 10
781
+ },
782
+ closeBtn: {
783
+ width: 32,
784
+ height: 32,
785
+ borderRadius: 16,
786
+ alignItems: "center",
787
+ justifyContent: "center"
788
+ },
789
+ headerTitle: {
790
+ fontSize: 16,
791
+ fontWeight: "600"
792
+ },
793
+ summary: {
794
+ alignItems: "center",
795
+ paddingHorizontal: 16,
796
+ paddingBottom: 16,
797
+ gap: 8
798
+ },
799
+ title: {
800
+ fontSize: 20,
801
+ fontWeight: "700"
802
+ },
803
+ subtitle: {
804
+ fontSize: 13
805
+ },
806
+ divider: {
807
+ height: reactNative.StyleSheet.hairlineWidth
808
+ },
809
+ sectionLabelRow: {
810
+ paddingHorizontal: 16,
811
+ paddingTop: 14,
812
+ paddingBottom: 6
813
+ },
814
+ sectionLabel: {
815
+ fontSize: 11,
816
+ fontWeight: "700",
817
+ letterSpacing: 0.5,
818
+ textTransform: "uppercase"
569
819
  }
570
820
  });
571
- function UploadQueueStrip() {
821
+ function ChatHeader({
822
+ conversationId,
823
+ labelFor,
824
+ avatarFor,
825
+ onMembersPress,
826
+ onPress,
827
+ onBack
828
+ }) {
572
829
  const theme = usePoolseTheme();
573
- const upload = react$1.useAttachmentUpload();
574
- if (upload.queue.length === 0) return null;
575
- return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles3.strip, children: upload.queue.map((item) => {
576
- const isError = item.status === "error";
577
- const isReady = item.status === "ready";
578
- return /* @__PURE__ */ jsxRuntime.jsxs(
830
+ const { conversation } = react$1.useConversation(conversationId);
831
+ const { members } = react$1.useMembers(conversationId);
832
+ const { me } = react$1.useMe();
833
+ const { online } = react$1.usePresence(conversationId);
834
+ const [detailsOpen, setDetailsOpen] = react.useState(false);
835
+ const isDirect = conversation?.type === "direct";
836
+ const otherMember = isDirect ? members.find((m) => m.user_id !== me?.id) : null;
837
+ const otherExtId = otherMember?.external_id ?? null;
838
+ const otherUser = react$1.useUser(otherExtId);
839
+ if (!conversation) {
840
+ return /* @__PURE__ */ jsxRuntime.jsx(
579
841
  reactNative.View,
580
842
  {
581
843
  style: [
582
- styles3.chip,
844
+ styles4.root,
845
+ {
846
+ backgroundColor: theme.colors.surface,
847
+ borderBottomColor: theme.colors.border
848
+ }
849
+ ]
850
+ }
851
+ );
852
+ }
853
+ const fallbackName = otherExtId ?? "";
854
+ const directName = otherUser.profile?.displayName ?? (otherExtId ? labelFor?.(otherExtId) ?? fallbackName : "Direct");
855
+ const title = isDirect ? directName : conversation.name ?? "Conversation";
856
+ const isOnline = isDirect && otherExtId ? online.has(otherExtId) : false;
857
+ const subtitle = isDirect ? isOnline ? "Online" : null : `${members.length} ${members.length === 1 ? "member" : "members"}`;
858
+ const avatarUrl = isDirect && otherExtId ? avatarFor?.(otherExtId) ?? otherUser.profile?.avatarUrl ?? null : null;
859
+ const avatarSeed = isDirect ? otherExtId ?? conversationId : conversationId;
860
+ const isGroup = !isDirect;
861
+ const headerOnPress = onPress ?? (isGroup ? () => setDetailsOpen(true) : void 0);
862
+ const Wrapper = headerOnPress ? reactNative.Pressable : reactNative.View;
863
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
864
+ /* @__PURE__ */ jsxRuntime.jsxs(
865
+ Wrapper,
866
+ {
867
+ ...headerOnPress ? { onPress: headerOnPress } : {},
868
+ style: [
869
+ styles4.root,
583
870
  {
584
- backgroundColor: theme.colors.surface2,
585
- borderColor: isError ? theme.colors.error : theme.colors.border,
586
- borderRadius: theme.radii.sm
871
+ backgroundColor: theme.colors.surface,
872
+ borderBottomColor: theme.colors.border
587
873
  }
588
874
  ],
589
875
  children: [
590
- /* @__PURE__ */ jsxRuntime.jsx(
591
- reactNative.Text,
592
- {
593
- style: [styles3.name, { color: isError ? theme.colors.error : theme.colors.ink }],
594
- numberOfLines: 1,
595
- children: item.filename
596
- }
597
- ),
598
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles3.status, { color: theme.colors.ink3 }], children: isReady ? "ready" : isError ? "failed" : item.status }),
599
- /* @__PURE__ */ jsxRuntime.jsx(
876
+ onBack ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: onBack, hitSlop: 12, style: styles4.backBtn, accessibilityLabel: "Back", children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "reply", size: 20, color: theme.colors.ink }) }) : null,
877
+ /* @__PURE__ */ jsxRuntime.jsx(Avatar, { src: avatarUrl, name: title, seed: avatarSeed, online: isOnline, size: "md" }),
878
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles4.titleCol, children: [
879
+ /* @__PURE__ */ jsxRuntime.jsx(
880
+ reactNative.Text,
881
+ {
882
+ style: [styles4.title, { color: theme.colors.ink, fontFamily: theme.type.fontDisplay }],
883
+ numberOfLines: 1,
884
+ children: title
885
+ }
886
+ ),
887
+ subtitle ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles4.subtitle, { color: theme.colors.ink3 }], numberOfLines: 1, children: subtitle }) : null
888
+ ] }),
889
+ onMembersPress ? /* @__PURE__ */ jsxRuntime.jsx(
600
890
  reactNative.Pressable,
601
891
  {
602
- hitSlop: 6,
603
- onPress: () => isReady || isError ? upload.remove(item.localId) : upload.cancel(item.localId),
604
- style: styles3.btn,
605
- accessibilityLabel: "Remove upload",
606
- children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "close", size: 12, color: theme.colors.ink2 })
892
+ onPress: onMembersPress,
893
+ hitSlop: 12,
894
+ style: styles4.actionBtn,
895
+ accessibilityLabel: "Show members",
896
+ children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "users", size: 20, color: theme.colors.ink2 })
607
897
  }
608
- )
898
+ ) : null
609
899
  ]
610
- },
611
- item.localId
612
- );
613
- }) });
900
+ }
901
+ ),
902
+ isGroup ? /* @__PURE__ */ jsxRuntime.jsx(
903
+ GroupDetailsSheet,
904
+ {
905
+ visible: detailsOpen,
906
+ onClose: () => setDetailsOpen(false),
907
+ conversationId,
908
+ ...labelFor ? { labelFor } : {},
909
+ ...avatarFor ? { avatarFor } : {}
910
+ }
911
+ ) : null
912
+ ] });
614
913
  }
615
- var styles3 = reactNative.StyleSheet.create({
616
- strip: {
914
+ var styles4 = reactNative.StyleSheet.create({
915
+ root: {
617
916
  flexDirection: "row",
618
- flexWrap: "wrap",
619
- gap: 6,
620
- marginBottom: 6
917
+ alignItems: "center",
918
+ paddingHorizontal: 14,
919
+ paddingVertical: 10,
920
+ borderBottomWidth: reactNative.StyleSheet.hairlineWidth,
921
+ gap: 12
621
922
  },
622
- chip: {
623
- flexDirection: "row",
923
+ titleCol: {
924
+ flex: 1,
925
+ gap: 2
926
+ },
927
+ title: {
928
+ fontSize: 16,
929
+ fontWeight: "600"
930
+ },
931
+ subtitle: {
932
+ fontSize: 12,
933
+ fontWeight: "500"
934
+ },
935
+ actionBtn: {
936
+ width: 36,
937
+ height: 36,
938
+ borderRadius: 18,
939
+ alignItems: "center",
940
+ justifyContent: "center"
941
+ },
942
+ backBtn: {
943
+ width: 36,
944
+ height: 36,
945
+ alignItems: "center",
946
+ justifyContent: "center",
947
+ marginLeft: -6
948
+ }
949
+ });
950
+ function ConversationList({
951
+ selectedId,
952
+ onSelect,
953
+ renderItem,
954
+ emptyState,
955
+ conversations: controlled,
956
+ loading: controlledLoading,
957
+ error: controlledError,
958
+ unreadCounts
959
+ }) {
960
+ const theme = usePoolseTheme();
961
+ const auto = react$1.useConversations();
962
+ const isControlled = controlled !== void 0;
963
+ const conversations = isControlled ? controlled : auto.conversations;
964
+ const loading = isControlled ? controlledLoading ?? false : auto.loading;
965
+ const error = isControlled ? controlledError ?? null : auto.error;
966
+ const counts = unreadCounts ?? (isControlled ? void 0 : auto.unreadCounts);
967
+ if (loading && conversations.length === 0) {
968
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles5.placeholder, { backgroundColor: theme.colors.paper }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: theme.colors.brand }) });
969
+ }
970
+ if (error) {
971
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles5.placeholder, { backgroundColor: theme.colors.paper }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: { color: theme.colors.error, fontFamily: theme.type.fontBody }, children: "Failed to load conversations." }) });
972
+ }
973
+ if (conversations.length === 0) {
974
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles5.placeholder, { backgroundColor: theme.colors.paper }], children: typeof emptyState === "string" || emptyState == null ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: { color: theme.colors.ink3, fontFamily: theme.type.fontBody }, children: emptyState ?? "No conversations yet." }) : emptyState });
975
+ }
976
+ return /* @__PURE__ */ jsxRuntime.jsx(
977
+ reactNative.FlatList,
978
+ {
979
+ data: conversations,
980
+ keyExtractor: (c) => c.id,
981
+ style: { backgroundColor: theme.colors.paper },
982
+ renderItem: ({ item }) => {
983
+ const selected = selectedId === item.id;
984
+ if (renderItem) {
985
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderItem(item, selected) });
986
+ }
987
+ const unread = counts?.[item.id] ?? 0;
988
+ return /* @__PURE__ */ jsxRuntime.jsxs(
989
+ reactNative.Pressable,
990
+ {
991
+ onPress: () => onSelect?.(item),
992
+ style: [
993
+ styles5.row,
994
+ {
995
+ backgroundColor: selected ? theme.colors.surface2 : "transparent"
996
+ }
997
+ ],
998
+ children: [
999
+ /* @__PURE__ */ jsxRuntime.jsx(Avatar, { src: null, name: item.name ?? null, seed: item.id, size: "md" }),
1000
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles5.body, children: [
1001
+ /* @__PURE__ */ jsxRuntime.jsx(
1002
+ reactNative.Text,
1003
+ {
1004
+ style: [styles5.name, { color: theme.colors.ink, fontFamily: theme.type.fontBody }],
1005
+ numberOfLines: 1,
1006
+ children: item.name ?? "Untitled conversation"
1007
+ }
1008
+ ),
1009
+ item.last_message_at ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles5.preview, { color: theme.colors.ink3 }], numberOfLines: 1, children: formatRelative(item.last_message_at) }) : null
1010
+ ] }),
1011
+ unread > 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles5.unreadPill, { backgroundColor: theme.colors.unreadPill }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles5.unreadText, { color: theme.colors.unreadPillText }], children: unread > 99 ? "99+" : String(unread) }) }) : null
1012
+ ]
1013
+ }
1014
+ );
1015
+ }
1016
+ }
1017
+ );
1018
+ }
1019
+ function formatRelative(iso) {
1020
+ try {
1021
+ const d = new Date(iso);
1022
+ const now = Date.now();
1023
+ const diff = now - d.getTime();
1024
+ if (diff < 6e4) return "just now";
1025
+ if (diff < 36e5) return `${Math.floor(diff / 6e4)}m`;
1026
+ if (diff < 864e5) return `${Math.floor(diff / 36e5)}h`;
1027
+ return `${Math.floor(diff / 864e5)}d`;
1028
+ } catch {
1029
+ return "";
1030
+ }
1031
+ }
1032
+ var styles5 = reactNative.StyleSheet.create({
1033
+ placeholder: {
1034
+ flex: 1,
1035
+ alignItems: "center",
1036
+ justifyContent: "center",
1037
+ padding: 24
1038
+ },
1039
+ row: {
1040
+ flexDirection: "row",
1041
+ alignItems: "center",
1042
+ paddingHorizontal: 12,
1043
+ paddingVertical: 10,
1044
+ gap: 10
1045
+ },
1046
+ body: {
1047
+ flex: 1,
1048
+ gap: 2
1049
+ },
1050
+ name: {
1051
+ fontSize: 15,
1052
+ fontWeight: "600"
1053
+ },
1054
+ preview: {
1055
+ fontSize: 12
1056
+ },
1057
+ unreadPill: {
1058
+ minWidth: 22,
1059
+ height: 22,
1060
+ borderRadius: 11,
1061
+ paddingHorizontal: 6,
1062
+ alignItems: "center",
1063
+ justifyContent: "center"
1064
+ },
1065
+ unreadText: {
1066
+ fontSize: 11,
1067
+ fontWeight: "700"
1068
+ }
1069
+ });
1070
+ var UploadCtx = react.createContext(null);
1071
+ function UploadProvider({ children }) {
1072
+ const upload = react$1.useAttachmentUpload();
1073
+ const previews = react.useRef(/* @__PURE__ */ new Map());
1074
+ const setPreview = react.useCallback((filename, uri) => {
1075
+ previews.current.set(filename, uri);
1076
+ }, []);
1077
+ const getPreview = react.useCallback((filename) => {
1078
+ return previews.current.get(filename) ?? null;
1079
+ }, []);
1080
+ return /* @__PURE__ */ jsxRuntime.jsx(UploadCtx.Provider, { value: { upload, setPreview, getPreview }, children });
1081
+ }
1082
+ function useSharedUpload() {
1083
+ const fromCtx = react.useContext(UploadCtx);
1084
+ const fallback = react$1.useAttachmentUpload();
1085
+ return fromCtx?.upload ?? fallback;
1086
+ }
1087
+ function useUploadPreview() {
1088
+ const ctx = react.useContext(UploadCtx);
1089
+ return {
1090
+ setPreview: ctx?.setPreview ?? (() => void 0),
1091
+ getPreview: ctx?.getPreview ?? (() => null)
1092
+ };
1093
+ }
1094
+ function AttachmentPicker({ visible, onClose, onPickInputs }) {
1095
+ const theme = usePoolseTheme();
1096
+ const upload = useSharedUpload();
1097
+ const { setPreview } = useUploadPreview();
1098
+ const pickFromKind = async (kind) => {
1099
+ try {
1100
+ const adapter = onPickInputs ?? defaultAdapter;
1101
+ const inputs = await adapter(kind);
1102
+ for (const input of inputs) {
1103
+ const previewUri = input._previewUri ?? null;
1104
+ if (previewUri && input.filename) {
1105
+ setPreview(input.filename, previewUri);
1106
+ }
1107
+ void upload.upload(input);
1108
+ }
1109
+ } catch (err) {
1110
+ console.warn("[poolse/picker]", err);
1111
+ } finally {
1112
+ onClose();
1113
+ }
1114
+ };
1115
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Modal, { visible, transparent: true, animationType: "slide", onRequestClose: onClose, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { style: styles6.backdrop, onPress: onClose, children: /* @__PURE__ */ jsxRuntime.jsxs(
1116
+ reactNative.View,
1117
+ {
1118
+ style: [
1119
+ styles6.sheet,
1120
+ {
1121
+ backgroundColor: theme.colors.surface,
1122
+ borderTopLeftRadius: theme.radii.lg,
1123
+ borderTopRightRadius: theme.radii.lg
1124
+ },
1125
+ theme.shadows.lg
1126
+ ],
1127
+ children: [
1128
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Pressable, { style: styles6.item, onPress: () => pickFromKind("image"), children: [
1129
+ /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "attachment", size: 20, color: theme.colors.ink2 }),
1130
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles6.itemText, { color: theme.colors.ink }], children: "Photo or video" })
1131
+ ] }),
1132
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Pressable, { style: styles6.item, onPress: () => pickFromKind("document"), children: [
1133
+ /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "attachment", size: 20, color: theme.colors.ink2 }),
1134
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles6.itemText, { color: theme.colors.ink }], children: "File" })
1135
+ ] }),
1136
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { style: styles6.cancel, onPress: onClose, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles6.itemText, { color: theme.colors.brand }], children: "Cancel" }) })
1137
+ ]
1138
+ }
1139
+ ) }) });
1140
+ }
1141
+ async function uriToBlob(uri) {
1142
+ const res = await fetch(uri);
1143
+ if (!res.ok && res.status !== 0) {
1144
+ throw new Error(`failed to read picked file (HTTP ${res.status})`);
1145
+ }
1146
+ return res.blob();
1147
+ }
1148
+ async function defaultAdapter(kind) {
1149
+ if (kind === "image") {
1150
+ const res2 = await ImagePicker__namespace.launchImageLibraryAsync({
1151
+ mediaTypes: "images",
1152
+ quality: 0.85
1153
+ });
1154
+ if (res2.canceled) return [];
1155
+ return Promise.all(
1156
+ res2.assets.map(async (asset) => {
1157
+ const blob = await uriToBlob(asset.uri);
1158
+ const contentType = asset.mimeType ?? blob.type ?? "image/jpeg";
1159
+ return {
1160
+ body: blob,
1161
+ contentType,
1162
+ byteSize: asset.fileSize ?? blob.size,
1163
+ filename: asset.fileName ?? `image-${Date.now()}.jpg`,
1164
+ _previewUri: asset.uri
1165
+ };
1166
+ })
1167
+ );
1168
+ }
1169
+ const res = await DocumentPicker__namespace.getDocumentAsync({
1170
+ copyToCacheDirectory: true,
1171
+ multiple: true
1172
+ });
1173
+ if (res.canceled) return [];
1174
+ return Promise.all(
1175
+ res.assets.map(async (asset) => {
1176
+ const blob = await uriToBlob(asset.uri);
1177
+ const contentType = asset.mimeType ?? blob.type ?? "application/octet-stream";
1178
+ return {
1179
+ body: blob,
1180
+ contentType,
1181
+ byteSize: asset.size ?? blob.size,
1182
+ filename: asset.name,
1183
+ _previewUri: asset.uri
1184
+ };
1185
+ })
1186
+ );
1187
+ }
1188
+ var styles6 = reactNative.StyleSheet.create({
1189
+ backdrop: {
1190
+ flex: 1,
1191
+ backgroundColor: "rgba(0,0,0,0.35)",
1192
+ justifyContent: "flex-end"
1193
+ },
1194
+ sheet: {
1195
+ paddingVertical: 8,
1196
+ paddingBottom: 24
1197
+ },
1198
+ item: {
1199
+ flexDirection: "row",
1200
+ alignItems: "center",
1201
+ paddingHorizontal: 20,
1202
+ paddingVertical: 14,
1203
+ gap: 14
1204
+ },
1205
+ cancel: {
1206
+ paddingHorizontal: 20,
1207
+ paddingVertical: 14,
1208
+ alignItems: "center",
1209
+ marginTop: 4
1210
+ },
1211
+ itemText: {
1212
+ fontSize: 15,
1213
+ fontWeight: "500"
1214
+ }
1215
+ });
1216
+ function UploadQueueStrip() {
1217
+ const theme = usePoolseTheme();
1218
+ const upload = useSharedUpload();
1219
+ const { getPreview } = useUploadPreview();
1220
+ if (upload.queue.length === 0) return null;
1221
+ return /* @__PURE__ */ jsxRuntime.jsx(
1222
+ reactNative.ScrollView,
1223
+ {
1224
+ horizontal: true,
1225
+ showsHorizontalScrollIndicator: false,
1226
+ contentContainerStyle: styles7.scroll,
1227
+ style: [styles7.scrollStyle, { marginBottom: 8 }],
1228
+ children: upload.queue.map((item) => {
1229
+ const isError = item.status === "error";
1230
+ const isUploading = item.status === "uploading" || item.status === "pending";
1231
+ const isImage = (item.contentType ?? "").startsWith("image/");
1232
+ const previewUri = getPreview(item.filename);
1233
+ const canTerminate = isUploading || item.status === "ready" || isError;
1234
+ const handleClose = () => {
1235
+ if (item.status === "ready" || isError) upload.remove(item.localId);
1236
+ else upload.cancel(item.localId);
1237
+ };
1238
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles7.tileWrap, children: [
1239
+ isImage && previewUri ? /* @__PURE__ */ jsxRuntime.jsx(ImageTile, { uri: previewUri, uploading: isUploading, error: isError }) : /* @__PURE__ */ jsxRuntime.jsx(
1240
+ FileTile,
1241
+ {
1242
+ filename: item.filename,
1243
+ size: item.byteSize,
1244
+ uploading: isUploading,
1245
+ error: isError
1246
+ }
1247
+ ),
1248
+ canTerminate ? /* @__PURE__ */ jsxRuntime.jsx(
1249
+ reactNative.Pressable,
1250
+ {
1251
+ onPress: handleClose,
1252
+ hitSlop: 8,
1253
+ style: [styles7.closeBtn, { backgroundColor: theme.colors.ink }],
1254
+ accessibilityLabel: isError ? "Remove failed upload" : "Cancel upload",
1255
+ children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "close", size: 11, color: theme.colors.onBrand })
1256
+ }
1257
+ ) : null,
1258
+ isUploading ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles7.progressOverlay, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles7.spinner, { borderColor: theme.colors.onBrand }] }) }) : null,
1259
+ isError ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles7.errorOverlay, { backgroundColor: "rgba(229,72,77,0.85)" }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles7.errorText, { color: "#fff" }], children: "Failed" }) }) : null
1260
+ ] }, item.localId);
1261
+ })
1262
+ }
1263
+ );
1264
+ }
1265
+ function ImageTile({ uri, uploading, error }) {
1266
+ const theme = usePoolseTheme();
1267
+ return /* @__PURE__ */ jsxRuntime.jsx(
1268
+ reactNative.Image,
1269
+ {
1270
+ source: { uri },
1271
+ style: [
1272
+ styles7.imageTile,
1273
+ {
1274
+ backgroundColor: theme.colors.surface2,
1275
+ borderColor: error ? theme.colors.error : "transparent",
1276
+ opacity: uploading ? 0.65 : 1
1277
+ }
1278
+ ],
1279
+ resizeMode: "cover"
1280
+ }
1281
+ );
1282
+ }
1283
+ function FileTile({
1284
+ filename,
1285
+ size,
1286
+ uploading,
1287
+ error
1288
+ }) {
1289
+ const theme = usePoolseTheme();
1290
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1291
+ reactNative.View,
1292
+ {
1293
+ style: [
1294
+ styles7.fileTile,
1295
+ {
1296
+ backgroundColor: theme.colors.surface2,
1297
+ borderColor: error ? theme.colors.error : uploading ? theme.colors.brand : theme.colors.border
1298
+ }
1299
+ ],
1300
+ children: [
1301
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles7.fileIconBox, { backgroundColor: theme.colors.brandSoft }], children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "attachment", size: 18, color: theme.colors.brand }) }),
1302
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles7.fileInfo, children: [
1303
+ /* @__PURE__ */ jsxRuntime.jsx(
1304
+ reactNative.Text,
1305
+ {
1306
+ style: [styles7.fileName, { color: theme.colors.ink, fontFamily: theme.type.fontBody }],
1307
+ numberOfLines: 1,
1308
+ children: filename
1309
+ }
1310
+ ),
1311
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles7.fileSize, { color: theme.colors.ink3 }], children: formatBytes(size) })
1312
+ ] })
1313
+ ]
1314
+ }
1315
+ );
1316
+ }
1317
+ function formatBytes(n) {
1318
+ if (!n || n < 0) return "";
1319
+ if (n < 1024) return `${n} B`;
1320
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
1321
+ if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;
1322
+ return `${(n / 1024 / 1024 / 1024).toFixed(1)} GB`;
1323
+ }
1324
+ var TILE = 64;
1325
+ var styles7 = reactNative.StyleSheet.create({
1326
+ scrollStyle: {
1327
+ overflow: "visible"
1328
+ },
1329
+ scroll: {
1330
+ paddingHorizontal: 8,
1331
+ // paddingTop creates room for the X to overflow the tile without
1332
+ // being clipped on Android (where overflow:visible isn't enough).
1333
+ paddingTop: 8,
1334
+ gap: 10,
1335
+ flexDirection: "row"
1336
+ },
1337
+ tileWrap: {
1338
+ position: "relative",
1339
+ overflow: "visible"
1340
+ },
1341
+ imageTile: {
1342
+ width: TILE,
1343
+ height: TILE,
1344
+ borderRadius: 10,
1345
+ borderWidth: 1.5
1346
+ },
1347
+ fileTile: {
1348
+ height: TILE,
1349
+ minWidth: 200,
1350
+ maxWidth: 260,
1351
+ flexDirection: "row",
1352
+ alignItems: "center",
1353
+ paddingHorizontal: 10,
1354
+ paddingRight: 28,
1355
+ borderRadius: 10,
1356
+ borderWidth: 1,
1357
+ gap: 10
1358
+ },
1359
+ fileIconBox: {
1360
+ width: 36,
1361
+ height: 36,
1362
+ borderRadius: 8,
1363
+ alignItems: "center",
1364
+ justifyContent: "center"
1365
+ },
1366
+ fileInfo: {
1367
+ flex: 1,
1368
+ gap: 2
1369
+ },
1370
+ fileName: {
1371
+ fontSize: 13,
1372
+ fontWeight: "600"
1373
+ },
1374
+ fileSize: {
1375
+ fontSize: 11
1376
+ },
1377
+ closeBtn: {
1378
+ // Outside the tile (top-right corner overflowing). Parent
1379
+ // scroll container + tileWrap set overflow:visible and the
1380
+ // scroll contentContainer has paddingTop:8 so the overflow
1381
+ // isn't clipped on Android.
1382
+ position: "absolute",
1383
+ top: -6,
1384
+ right: -6,
1385
+ width: 22,
1386
+ height: 22,
1387
+ borderRadius: 11,
1388
+ alignItems: "center",
1389
+ justifyContent: "center",
1390
+ shadowColor: "#000",
1391
+ shadowOffset: { width: 0, height: 1 },
1392
+ shadowOpacity: 0.3,
1393
+ shadowRadius: 2,
1394
+ elevation: 3
1395
+ },
1396
+ progressOverlay: {
1397
+ ...reactNative.StyleSheet.absoluteFillObject,
624
1398
  alignItems: "center",
625
- paddingHorizontal: 8,
626
- paddingVertical: 4,
627
- borderWidth: reactNative.StyleSheet.hairlineWidth,
628
- gap: 6,
629
- maxWidth: 200
1399
+ justifyContent: "center",
1400
+ pointerEvents: "none"
630
1401
  },
631
- name: {
632
- fontSize: 12,
633
- flex: 1
1402
+ spinner: {
1403
+ width: 24,
1404
+ height: 24,
1405
+ borderRadius: 12,
1406
+ borderWidth: 2.5,
1407
+ borderTopColor: "transparent"
1408
+ // RN can't loop a rotation purely in StyleSheet, but the static
1409
+ // 3/4-circle still reads as "in progress." A real Animated.View
1410
+ // rotation lands in a follow-up.
634
1411
  },
635
- status: {
636
- fontSize: 10,
637
- textTransform: "capitalize"
1412
+ errorOverlay: {
1413
+ position: "absolute",
1414
+ bottom: 0,
1415
+ left: 0,
1416
+ right: 0,
1417
+ paddingVertical: 3,
1418
+ alignItems: "center"
638
1419
  },
639
- btn: {
640
- padding: 2
1420
+ errorText: {
1421
+ fontSize: 10,
1422
+ fontWeight: "700"
641
1423
  }
642
1424
  });
643
1425
  var MessageComposer = react.forwardRef(
@@ -650,13 +1432,14 @@ var MessageComposer = react.forwardRef(
650
1432
  onCancelReply,
651
1433
  labelFor,
652
1434
  attachments = true,
653
- onAttachPress
1435
+ onAttachPress,
1436
+ keyboardOffset: _keyboardOffset = 0
654
1437
  }, ref) {
655
1438
  const theme = usePoolseTheme();
656
1439
  const inputRef = react.useRef(null);
657
1440
  const [value, setValue] = react.useState("");
658
1441
  const [sending, setSending] = react.useState(false);
659
- const upload = react$1.useAttachmentUpload();
1442
+ const upload = useSharedUpload();
660
1443
  react.useImperativeHandle(
661
1444
  ref,
662
1445
  () => ({
@@ -690,87 +1473,80 @@ var MessageComposer = react.forwardRef(
690
1473
  setSending(false);
691
1474
  }
692
1475
  };
693
- return /* @__PURE__ */ jsxRuntime.jsx(
694
- reactNative.KeyboardAvoidingView,
1476
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1477
+ reactNative.View,
695
1478
  {
696
- behavior: reactNative.Platform.OS === "ios" ? "padding" : void 0,
697
- keyboardVerticalOffset: reactNative.Platform.OS === "ios" ? 80 : 0,
698
- children: /* @__PURE__ */ jsxRuntime.jsxs(
699
- reactNative.View,
1479
+ style: [
1480
+ styles8.wrap,
700
1481
  {
701
- style: [
702
- styles4.wrap,
1482
+ backgroundColor: theme.colors.surface,
1483
+ borderTopColor: theme.colors.border
1484
+ }
1485
+ ],
1486
+ children: [
1487
+ replyingTo ? /* @__PURE__ */ jsxRuntime.jsx(
1488
+ ReplyChip,
1489
+ {
1490
+ message: replyingTo,
1491
+ ...labelFor ? { labelFor } : {},
1492
+ ...onCancelReply ? { onCancel: onCancelReply } : {}
1493
+ }
1494
+ ) : null,
1495
+ attachments ? /* @__PURE__ */ jsxRuntime.jsx(UploadQueueStrip, {}) : null,
1496
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles8.row, children: [
1497
+ attachments && onAttachPress ? /* @__PURE__ */ jsxRuntime.jsx(
1498
+ reactNative.Pressable,
703
1499
  {
704
- backgroundColor: theme.colors.surface,
705
- borderTopColor: theme.colors.border
1500
+ onPress: onAttachPress,
1501
+ hitSlop: 8,
1502
+ style: styles8.iconBtn,
1503
+ accessibilityLabel: "Attach file",
1504
+ children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "attachment", size: 22, color: theme.colors.ink2 })
706
1505
  }
707
- ],
708
- children: [
709
- replyingTo ? /* @__PURE__ */ jsxRuntime.jsx(
710
- ReplyChip,
711
- {
712
- message: replyingTo,
713
- ...labelFor ? { labelFor } : {},
714
- ...onCancelReply ? { onCancel: onCancelReply } : {}
715
- }
716
- ) : null,
717
- attachments ? /* @__PURE__ */ jsxRuntime.jsx(UploadQueueStrip, {}) : null,
718
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles4.row, children: [
719
- attachments && onAttachPress ? /* @__PURE__ */ jsxRuntime.jsx(
720
- reactNative.Pressable,
721
- {
722
- onPress: onAttachPress,
723
- hitSlop: 8,
724
- style: styles4.iconBtn,
725
- accessibilityLabel: "Attach file",
726
- children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "attachment", size: 22, color: theme.colors.ink2 })
727
- }
728
- ) : null,
729
- /* @__PURE__ */ jsxRuntime.jsx(
730
- reactNative.TextInput,
1506
+ ) : null,
1507
+ /* @__PURE__ */ jsxRuntime.jsx(
1508
+ reactNative.TextInput,
1509
+ {
1510
+ ref: inputRef,
1511
+ value,
1512
+ onChangeText: (text) => {
1513
+ setValue(text);
1514
+ onTyping?.();
1515
+ },
1516
+ placeholder,
1517
+ placeholderTextColor: theme.colors.ink3,
1518
+ multiline: true,
1519
+ editable: !disabled,
1520
+ style: [
1521
+ styles8.input,
731
1522
  {
732
- ref: inputRef,
733
- value,
734
- onChangeText: (text) => {
735
- setValue(text);
736
- onTyping?.();
737
- },
738
- placeholder,
739
- placeholderTextColor: theme.colors.ink3,
740
- multiline: true,
741
- editable: !disabled,
742
- style: [
743
- styles4.input,
744
- {
745
- color: theme.colors.ink,
746
- backgroundColor: theme.colors.surface2,
747
- borderRadius: theme.radii.lg,
748
- fontFamily: theme.type.fontBody,
749
- fontSize: theme.type.bodySize
750
- }
751
- ]
1523
+ color: theme.colors.ink,
1524
+ backgroundColor: theme.colors.surface2,
1525
+ borderRadius: theme.radii.lg,
1526
+ fontFamily: theme.type.fontBody,
1527
+ fontSize: theme.type.bodySize
752
1528
  }
753
- ),
754
- /* @__PURE__ */ jsxRuntime.jsx(
755
- reactNative.Pressable,
1529
+ ]
1530
+ }
1531
+ ),
1532
+ /* @__PURE__ */ jsxRuntime.jsx(
1533
+ reactNative.Pressable,
1534
+ {
1535
+ onPress: handleSend,
1536
+ disabled: submitDisabled,
1537
+ style: [
1538
+ styles8.sendBtn,
756
1539
  {
757
- onPress: handleSend,
758
- disabled: submitDisabled,
759
- style: [
760
- styles4.sendBtn,
761
- {
762
- backgroundColor: submitDisabled ? theme.colors.ink3 : theme.colors.brand,
763
- opacity: submitDisabled ? 0.6 : 1
764
- }
765
- ],
766
- accessibilityLabel: "Send message",
767
- children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "send", size: 18, color: theme.colors.onBrand })
1540
+ backgroundColor: submitDisabled ? theme.colors.ink3 : theme.colors.brand,
1541
+ opacity: submitDisabled ? 0.6 : 1
768
1542
  }
769
- )
770
- ] })
771
- ]
772
- }
773
- )
1543
+ ],
1544
+ accessibilityLabel: "Send message",
1545
+ children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "send", size: 18, color: theme.colors.onBrand })
1546
+ }
1547
+ )
1548
+ ] })
1549
+ ]
774
1550
  }
775
1551
  );
776
1552
  }
@@ -786,7 +1562,7 @@ function ReplyChip({
786
1562
  reactNative.View,
787
1563
  {
788
1564
  style: [
789
- styles4.replyChip,
1565
+ styles8.replyChip,
790
1566
  {
791
1567
  backgroundColor: theme.colors.surface2,
792
1568
  borderLeftColor: theme.colors.brand
@@ -794,22 +1570,27 @@ function ReplyChip({
794
1570
  ],
795
1571
  children: [
796
1572
  /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { flex: 1 }, children: [
797
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { style: [styles4.replyName, { color: theme.colors.brand }], children: [
1573
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { style: [styles8.replyName, { color: theme.colors.brand }], children: [
798
1574
  "Replying to ",
799
1575
  senderName
800
1576
  ] }),
801
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles4.replyBody, { color: theme.colors.ink2 }], numberOfLines: 1, children: message.body ?? "\u2026" })
1577
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles8.replyBody, { color: theme.colors.ink2 }], numberOfLines: 1, children: message.body ?? "\u2026" })
802
1578
  ] }),
803
- onCancel ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: onCancel, hitSlop: 8, style: styles4.replyCancel, children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "close", size: 16, color: theme.colors.ink2 }) }) : null
1579
+ onCancel ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: onCancel, hitSlop: 8, style: styles8.replyCancel, children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "close", size: 16, color: theme.colors.ink2 }) }) : null
804
1580
  ]
805
1581
  }
806
1582
  );
807
1583
  }
808
- var styles4 = reactNative.StyleSheet.create({
1584
+ var styles8 = reactNative.StyleSheet.create({
809
1585
  wrap: {
810
1586
  borderTopWidth: reactNative.StyleSheet.hairlineWidth,
811
- paddingHorizontal: 12,
812
- paddingVertical: 8
1587
+ paddingTop: 8,
1588
+ paddingBottom: 10,
1589
+ paddingHorizontal: 12
1590
+ // Note: no safe-area inset here on purpose. Wrap your chat
1591
+ // surface in <SafeAreaView> (or <SafeAreaView edges={['bottom']}>)
1592
+ // at the app root so the home-indicator clearance is handled
1593
+ // once. Doing it here too produces double padding.
813
1594
  },
814
1595
  row: {
815
1596
  flexDirection: "row",
@@ -862,7 +1643,7 @@ function MentionInput({ conversationId, onSend, ...composerProps }) {
862
1643
  const theme = usePoolseTheme();
863
1644
  const { members } = react$1.useMembers(conversationId);
864
1645
  const [pickerOpen, setPickerOpen] = react.useState(false);
865
- const [pickerQuery, setPickerQuery] = react.useState("");
1646
+ const [pickerQuery, _setPickerQuery] = react.useState("");
866
1647
  const [pendingMentionUuids, setPendingMentionUuids] = react.useState([]);
867
1648
  const [text, setText] = react.useState("");
868
1649
  const filteredMembers = react.useMemo(() => {
@@ -898,11 +1679,11 @@ function MentionInput({ conversationId, onSend, ...composerProps }) {
898
1679
  transparent: true,
899
1680
  animationType: "slide",
900
1681
  onRequestClose: () => setPickerOpen(false),
901
- children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { style: styles5.backdrop, onPress: () => setPickerOpen(false), children: /* @__PURE__ */ jsxRuntime.jsxs(
1682
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { style: styles9.backdrop, onPress: () => setPickerOpen(false), children: /* @__PURE__ */ jsxRuntime.jsxs(
902
1683
  reactNative.View,
903
1684
  {
904
1685
  style: [
905
- styles5.sheet,
1686
+ styles9.sheet,
906
1687
  {
907
1688
  backgroundColor: theme.colors.surface,
908
1689
  borderTopLeftRadius: theme.radii.lg,
@@ -913,7 +1694,7 @@ function MentionInput({ conversationId, onSend, ...composerProps }) {
913
1694
  /* @__PURE__ */ jsxRuntime.jsx(
914
1695
  reactNative.Text,
915
1696
  {
916
- style: [styles5.title, { color: theme.colors.ink, fontFamily: theme.type.fontBody }],
1697
+ style: [styles9.title, { color: theme.colors.ink, fontFamily: theme.type.fontBody }],
917
1698
  children: "Mention a member"
918
1699
  }
919
1700
  ),
@@ -944,12 +1725,12 @@ function MentionInput({ conversationId, onSend, ...composerProps }) {
944
1725
  }
945
1726
  function MentionRow({ member, onPress }) {
946
1727
  const theme = usePoolseTheme();
947
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Pressable, { onPress, style: styles5.row, children: [
1728
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Pressable, { onPress, style: styles9.row, children: [
948
1729
  /* @__PURE__ */ jsxRuntime.jsx(Avatar, { src: null, name: member.external_id, seed: member.external_id, size: "sm" }),
949
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles5.rowText, { color: theme.colors.ink, fontFamily: theme.type.fontBody }], children: member.external_id })
1730
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles9.rowText, { color: theme.colors.ink, fontFamily: theme.type.fontBody }], children: member.external_id })
950
1731
  ] });
951
1732
  }
952
- var styles5 = reactNative.StyleSheet.create({
1733
+ var styles9 = reactNative.StyleSheet.create({
953
1734
  backdrop: {
954
1735
  flex: 1,
955
1736
  backgroundColor: "rgba(0,0,0,0.35)",
@@ -993,10 +1774,10 @@ function MessageList({
993
1774
  [renderItem, messages.length]
994
1775
  );
995
1776
  if (loading && messages.length === 0) {
996
- return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles6.placeholder, { backgroundColor: theme.colors.paper }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: theme.colors.brand }) });
1777
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles10.placeholder, { backgroundColor: theme.colors.paper }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: theme.colors.brand }) });
997
1778
  }
998
1779
  if (messages.length === 0) {
999
- return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles6.placeholder, { backgroundColor: theme.colors.paper }], children: typeof emptyState === "string" ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: { color: theme.colors.ink3, fontFamily: theme.type.fontBody }, children: emptyState }) : emptyState ?? null });
1780
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles10.placeholder, { backgroundColor: theme.colors.paper }], children: typeof emptyState === "string" ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: { color: theme.colors.ink3, fontFamily: theme.type.fontBody }, children: emptyState }) : emptyState ?? null });
1000
1781
  }
1001
1782
  return /* @__PURE__ */ jsxRuntime.jsx(
1002
1783
  reactNative.FlatList,
@@ -1017,7 +1798,7 @@ function MessageList({
1017
1798
  }
1018
1799
  );
1019
1800
  }
1020
- var styles6 = reactNative.StyleSheet.create({
1801
+ var styles10 = reactNative.StyleSheet.create({
1021
1802
  placeholder: {
1022
1803
  flex: 1,
1023
1804
  alignItems: "center",
@@ -1050,7 +1831,7 @@ function EditableMessageBubble({
1050
1831
  reactNative.View,
1051
1832
  {
1052
1833
  style: [
1053
- styles7.wrap,
1834
+ styles11.wrap,
1054
1835
  {
1055
1836
  alignSelf: isSelf ? "flex-end" : "flex-start",
1056
1837
  maxWidth: "80%"
@@ -1060,7 +1841,7 @@ function EditableMessageBubble({
1060
1841
  reactNative.View,
1061
1842
  {
1062
1843
  style: [
1063
- styles7.bubble,
1844
+ styles11.bubble,
1064
1845
  {
1065
1846
  backgroundColor: bg,
1066
1847
  borderRadius: theme.radii.bubble,
@@ -1085,9 +1866,9 @@ function EditableMessageBubble({
1085
1866
  }
1086
1867
  }
1087
1868
  ),
1088
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles7.actions, children: [
1089
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: onCancel, hitSlop: 6, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles7.btn, { color: ink, opacity: 0.7 }], children: "Cancel" }) }),
1090
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: submit, disabled: saving, hitSlop: 6, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles7.btn, { color: ink, fontWeight: "700" }], children: saving ? "Saving\u2026" : "Save" }) })
1869
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles11.actions, children: [
1870
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: onCancel, hitSlop: 6, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles11.btn, { color: ink, opacity: 0.7 }], children: "Cancel" }) }),
1871
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: submit, disabled: saving, hitSlop: 6, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles11.btn, { color: ink, fontWeight: "700" }], children: saving ? "Saving\u2026" : "Save" }) })
1091
1872
  ] })
1092
1873
  ]
1093
1874
  }
@@ -1095,7 +1876,7 @@ function EditableMessageBubble({
1095
1876
  }
1096
1877
  );
1097
1878
  }
1098
- var styles7 = reactNative.StyleSheet.create({
1879
+ var styles11 = reactNative.StyleSheet.create({
1099
1880
  wrap: {
1100
1881
  marginBottom: 4
1101
1882
  },
@@ -1110,21 +1891,22 @@ var styles7 = reactNative.StyleSheet.create({
1110
1891
  fontSize: 13
1111
1892
  }
1112
1893
  });
1894
+ var DEFAULT_REACTION_EMOJIS = ["\u{1F44D}", "\u2764\uFE0F", "\u{1F602}", "\u{1F389}", "\u{1F64F}", "\u{1F525}"];
1113
1895
  function MessageActions({
1114
1896
  visible,
1115
1897
  onClose,
1116
1898
  isSelf,
1899
+ emojis = DEFAULT_REACTION_EMOJIS,
1900
+ onPickReaction,
1117
1901
  onReply,
1118
1902
  onReplyInThread,
1119
1903
  onQuote,
1120
1904
  onEdit,
1121
1905
  onDelete,
1122
- onCopy,
1123
- onReact
1906
+ onCopy
1124
1907
  }) {
1125
1908
  const theme = usePoolseTheme();
1126
1909
  const items = [];
1127
- if (onReact) items.push({ label: "React", icon: "emoji", onPress: onReact });
1128
1910
  if (onReply) items.push({ label: "Reply", icon: "reply", onPress: onReply });
1129
1911
  if (onQuote) items.push({ label: "Quote reply", icon: "reply", onPress: onQuote });
1130
1912
  if (onReplyInThread)
@@ -1133,11 +1915,12 @@ function MessageActions({
1133
1915
  if (isSelf && onEdit) items.push({ label: "Edit", icon: "edit", onPress: onEdit });
1134
1916
  if (isSelf && onDelete)
1135
1917
  items.push({ label: "Delete", icon: "trash", onPress: onDelete, danger: true });
1136
- return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Modal, { visible, transparent: true, animationType: "fade", onRequestClose: onClose, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { style: styles8.backdrop, onPress: onClose, children: /* @__PURE__ */ jsxRuntime.jsx(
1918
+ const showEmojiRow = !!onPickReaction && emojis.length > 0;
1919
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Modal, { visible, transparent: true, animationType: "fade", onRequestClose: onClose, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { style: styles12.backdrop, onPress: onClose, children: /* @__PURE__ */ jsxRuntime.jsxs(
1137
1920
  reactNative.View,
1138
1921
  {
1139
1922
  style: [
1140
- styles8.sheet,
1923
+ styles12.sheet,
1141
1924
  {
1142
1925
  backgroundColor: theme.colors.surface,
1143
1926
  borderTopLeftRadius: theme.radii.lg,
@@ -1145,42 +1928,78 @@ function MessageActions({
1145
1928
  },
1146
1929
  theme.shadows.lg
1147
1930
  ],
1148
- children: items.map((item) => /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Pressable, { onPress: item.onPress, style: styles8.item, children: [
1149
- /* @__PURE__ */ jsxRuntime.jsx(
1150
- PoolseIcon,
1151
- {
1152
- name: item.icon,
1153
- size: 18,
1154
- color: item.danger ? theme.colors.error : theme.colors.ink2
1155
- }
1156
- ),
1157
- /* @__PURE__ */ jsxRuntime.jsx(
1158
- reactNative.Text,
1931
+ children: [
1932
+ showEmojiRow ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles12.emojiRow, { borderBottomColor: theme.colors.border }], children: emojis.map((emoji) => /* @__PURE__ */ jsxRuntime.jsx(
1933
+ reactNative.Pressable,
1159
1934
  {
1160
- style: [
1161
- styles8.itemText,
1162
- {
1163
- color: item.danger ? theme.colors.error : theme.colors.ink,
1164
- fontFamily: theme.type.fontBody
1165
- }
1166
- ],
1167
- children: item.label
1168
- }
1169
- )
1170
- ] }, item.label))
1935
+ onPress: () => {
1936
+ onPickReaction(emoji);
1937
+ onClose();
1938
+ },
1939
+ style: styles12.emojiBtn,
1940
+ hitSlop: 6,
1941
+ accessibilityLabel: `React with ${emoji}`,
1942
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles12.emoji, children: emoji })
1943
+ },
1944
+ emoji
1945
+ )) }) : null,
1946
+ items.map((item) => /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Pressable, { onPress: item.onPress, style: styles12.item, children: [
1947
+ /* @__PURE__ */ jsxRuntime.jsx(
1948
+ PoolseIcon,
1949
+ {
1950
+ name: item.icon,
1951
+ size: 18,
1952
+ color: item.danger ? theme.colors.error : theme.colors.ink2
1953
+ }
1954
+ ),
1955
+ /* @__PURE__ */ jsxRuntime.jsx(
1956
+ reactNative.Text,
1957
+ {
1958
+ style: [
1959
+ styles12.itemText,
1960
+ {
1961
+ color: item.danger ? theme.colors.error : theme.colors.ink,
1962
+ fontFamily: theme.type.fontBody
1963
+ }
1964
+ ],
1965
+ children: item.label
1966
+ }
1967
+ )
1968
+ ] }, item.label))
1969
+ ]
1171
1970
  }
1172
1971
  ) }) });
1173
1972
  }
1174
- var styles8 = reactNative.StyleSheet.create({
1973
+ var styles12 = reactNative.StyleSheet.create({
1175
1974
  backdrop: {
1176
1975
  flex: 1,
1177
1976
  backgroundColor: "rgba(0,0,0,0.35)",
1178
1977
  justifyContent: "flex-end"
1179
1978
  },
1180
1979
  sheet: {
1181
- paddingVertical: 8,
1980
+ paddingTop: 8,
1182
1981
  paddingBottom: 24
1183
1982
  },
1983
+ emojiRow: {
1984
+ flexDirection: "row",
1985
+ alignItems: "center",
1986
+ justifyContent: "space-around",
1987
+ paddingHorizontal: 12,
1988
+ paddingVertical: 10,
1989
+ borderBottomWidth: reactNative.StyleSheet.hairlineWidth,
1990
+ marginBottom: 6
1991
+ },
1992
+ emojiBtn: {
1993
+ paddingHorizontal: 6,
1994
+ paddingVertical: 6,
1995
+ borderRadius: 999,
1996
+ alignItems: "center",
1997
+ justifyContent: "center"
1998
+ },
1999
+ emoji: {
2000
+ fontSize: 30,
2001
+ lineHeight: 36
2002
+ },
1184
2003
  item: {
1185
2004
  flexDirection: "row",
1186
2005
  alignItems: "center",
@@ -1195,88 +2014,341 @@ var styles8 = reactNative.StyleSheet.create({
1195
2014
  });
1196
2015
  function AttachmentPreview({ attachment, onPress, maxWidth = 280 }) {
1197
2016
  const theme = usePoolseTheme();
1198
- const isImage = (attachment.content_type ?? "").startsWith("image/");
2017
+ const isImage = (attachment.content_type ?? "").startsWith("image/");
2018
+ const { url } = react$1.useAttachmentUrl(attachment.id);
2019
+ if (isImage) {
2020
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress, disabled: !onPress, children: url ? /* @__PURE__ */ jsxRuntime.jsx(
2021
+ reactNative.Image,
2022
+ {
2023
+ source: { uri: url },
2024
+ style: {
2025
+ width: maxWidth,
2026
+ height: 200,
2027
+ borderRadius: theme.radii.md,
2028
+ backgroundColor: theme.colors.surface2
2029
+ },
2030
+ resizeMode: "cover",
2031
+ accessible: true,
2032
+ accessibilityLabel: attachment.original_filename ?? "attachment image"
2033
+ }
2034
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
2035
+ reactNative.View,
2036
+ {
2037
+ style: [
2038
+ styles13.imagePlaceholder,
2039
+ {
2040
+ width: maxWidth,
2041
+ height: 200,
2042
+ borderRadius: theme.radii.md,
2043
+ backgroundColor: theme.colors.surface2
2044
+ }
2045
+ ]
2046
+ }
2047
+ ) });
2048
+ }
2049
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2050
+ reactNative.Pressable,
2051
+ {
2052
+ onPress,
2053
+ disabled: !onPress,
2054
+ style: [
2055
+ styles13.fileCard,
2056
+ {
2057
+ backgroundColor: theme.colors.surface2,
2058
+ borderColor: theme.colors.border,
2059
+ borderRadius: theme.radii.md
2060
+ }
2061
+ ],
2062
+ children: [
2063
+ /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "attachment", size: 18, color: theme.colors.ink2 }),
2064
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { flex: 1, marginLeft: 8 }, children: [
2065
+ /* @__PURE__ */ jsxRuntime.jsx(
2066
+ reactNative.Text,
2067
+ {
2068
+ style: [styles13.fileName, { color: theme.colors.ink, fontFamily: theme.type.fontBody }],
2069
+ numberOfLines: 1,
2070
+ children: attachment.original_filename ?? "file"
2071
+ }
2072
+ ),
2073
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles13.fileSize, { color: theme.colors.ink3 }], children: formatBytes2(attachment.byte_size) })
2074
+ ] })
2075
+ ]
2076
+ }
2077
+ );
2078
+ }
2079
+ function formatBytes2(n) {
2080
+ if (n < 1024) return `${n} B`;
2081
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
2082
+ if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;
2083
+ return `${(n / 1024 / 1024 / 1024).toFixed(1)} GB`;
2084
+ }
2085
+ var styles13 = reactNative.StyleSheet.create({
2086
+ imagePlaceholder: {},
2087
+ fileCard: {
2088
+ flexDirection: "row",
2089
+ alignItems: "center",
2090
+ padding: 8,
2091
+ borderWidth: reactNative.StyleSheet.hairlineWidth
2092
+ },
2093
+ fileName: {
2094
+ fontSize: 13,
2095
+ fontWeight: "500"
2096
+ },
2097
+ fileSize: {
2098
+ fontSize: 11
2099
+ }
2100
+ });
2101
+ function ImageMosaic({ images, maxWidth, radius }) {
2102
+ const theme = usePoolseTheme();
2103
+ const r = radius ?? theme.radii.md;
2104
+ const [viewerIndex, setViewerIndex] = react.useState(null);
2105
+ if (images.length === 0) return null;
2106
+ const visible = images.slice(0, 4);
2107
+ const overflow = images.length > 4 ? images.length - 4 : 0;
2108
+ const n = visible.length;
2109
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2110
+ /* @__PURE__ */ jsxRuntime.jsxs(
2111
+ reactNative.View,
2112
+ {
2113
+ style: [
2114
+ styles14.root,
2115
+ {
2116
+ borderRadius: r,
2117
+ ...maxWidth ? { maxWidth } : {}
2118
+ }
2119
+ ],
2120
+ children: [
2121
+ n === 1 ? /* @__PURE__ */ jsxRuntime.jsx(
2122
+ Tile,
2123
+ {
2124
+ attachment: visible[0],
2125
+ style: styles14.tileFull,
2126
+ onPress: () => setViewerIndex(0),
2127
+ overlay: null
2128
+ }
2129
+ ) : null,
2130
+ n === 2 ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles14.rowEven, children: [
2131
+ /* @__PURE__ */ jsxRuntime.jsx(
2132
+ Tile,
2133
+ {
2134
+ attachment: visible[0],
2135
+ style: styles14.tileHalfSquare,
2136
+ onPress: () => setViewerIndex(0),
2137
+ overlay: null
2138
+ }
2139
+ ),
2140
+ /* @__PURE__ */ jsxRuntime.jsx(
2141
+ Tile,
2142
+ {
2143
+ attachment: visible[1],
2144
+ style: styles14.tileHalfSquare,
2145
+ onPress: () => setViewerIndex(1),
2146
+ overlay: null
2147
+ }
2148
+ )
2149
+ ] }) : null,
2150
+ n === 3 ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2151
+ /* @__PURE__ */ jsxRuntime.jsx(
2152
+ Tile,
2153
+ {
2154
+ attachment: visible[0],
2155
+ style: styles14.tileHero,
2156
+ onPress: () => setViewerIndex(0),
2157
+ overlay: null
2158
+ }
2159
+ ),
2160
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles14.rowEven, { marginTop: 2 }], children: [
2161
+ /* @__PURE__ */ jsxRuntime.jsx(
2162
+ Tile,
2163
+ {
2164
+ attachment: visible[1],
2165
+ style: styles14.tileHalfSquare,
2166
+ onPress: () => setViewerIndex(1),
2167
+ overlay: null
2168
+ }
2169
+ ),
2170
+ /* @__PURE__ */ jsxRuntime.jsx(
2171
+ Tile,
2172
+ {
2173
+ attachment: visible[2],
2174
+ style: styles14.tileHalfSquare,
2175
+ onPress: () => setViewerIndex(2),
2176
+ overlay: null
2177
+ }
2178
+ )
2179
+ ] })
2180
+ ] }) : null,
2181
+ n === 4 ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2182
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles14.rowEven, children: [
2183
+ /* @__PURE__ */ jsxRuntime.jsx(
2184
+ Tile,
2185
+ {
2186
+ attachment: visible[0],
2187
+ style: styles14.tileHalfSquare,
2188
+ onPress: () => setViewerIndex(0),
2189
+ overlay: null
2190
+ }
2191
+ ),
2192
+ /* @__PURE__ */ jsxRuntime.jsx(
2193
+ Tile,
2194
+ {
2195
+ attachment: visible[1],
2196
+ style: styles14.tileHalfSquare,
2197
+ onPress: () => setViewerIndex(1),
2198
+ overlay: null
2199
+ }
2200
+ )
2201
+ ] }),
2202
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles14.rowEven, { marginTop: 2 }], children: [
2203
+ /* @__PURE__ */ jsxRuntime.jsx(
2204
+ Tile,
2205
+ {
2206
+ attachment: visible[2],
2207
+ style: styles14.tileHalfSquare,
2208
+ onPress: () => setViewerIndex(2),
2209
+ overlay: null
2210
+ }
2211
+ ),
2212
+ /* @__PURE__ */ jsxRuntime.jsx(
2213
+ Tile,
2214
+ {
2215
+ attachment: visible[3],
2216
+ style: styles14.tileHalfSquare,
2217
+ onPress: () => setViewerIndex(3),
2218
+ overlay: overflow > 0 ? `+${overflow}` : null
2219
+ }
2220
+ )
2221
+ ] })
2222
+ ] }) : null
2223
+ ]
2224
+ }
2225
+ ),
2226
+ /* @__PURE__ */ jsxRuntime.jsx(
2227
+ reactNative.Modal,
2228
+ {
2229
+ visible: viewerIndex !== null,
2230
+ transparent: true,
2231
+ animationType: "fade",
2232
+ onRequestClose: () => setViewerIndex(null),
2233
+ children: viewerIndex !== null ? /* @__PURE__ */ jsxRuntime.jsx(
2234
+ FullScreenViewer,
2235
+ {
2236
+ attachment: images[viewerIndex],
2237
+ onClose: () => setViewerIndex(null)
2238
+ }
2239
+ ) : null
2240
+ }
2241
+ )
2242
+ ] });
2243
+ }
2244
+ function Tile({
2245
+ attachment,
2246
+ style,
2247
+ overlay,
2248
+ onPress
2249
+ }) {
2250
+ const theme = usePoolseTheme();
1199
2251
  const { url } = react$1.useAttachmentUrl(attachment.id);
1200
- if (isImage) {
1201
- return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress, disabled: !onPress, children: url ? /* @__PURE__ */ jsxRuntime.jsx(
2252
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Pressable, { onPress, style: [styles14.tileBase, style], children: [
2253
+ url ? /* @__PURE__ */ jsxRuntime.jsx(
1202
2254
  reactNative.Image,
1203
2255
  {
1204
2256
  source: { uri: url },
1205
- style: {
1206
- width: maxWidth,
1207
- height: 200,
1208
- borderRadius: theme.radii.md,
1209
- backgroundColor: theme.colors.surface2
1210
- },
2257
+ style: [styles14.tileImage, { backgroundColor: theme.colors.surface2 }],
1211
2258
  resizeMode: "cover",
1212
2259
  accessible: true,
1213
- accessibilityLabel: attachment.original_filename ?? "attachment image"
2260
+ accessibilityLabel: attachment.original_filename ?? "image"
1214
2261
  }
1215
- ) : /* @__PURE__ */ jsxRuntime.jsx(
1216
- reactNative.View,
2262
+ ) : /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles14.tileImage, { backgroundColor: theme.colors.surface2 }] }),
2263
+ overlay ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles14.overflow, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles14.overflowText, children: overlay }) }) : null
2264
+ ] });
2265
+ }
2266
+ function FullScreenViewer({
2267
+ attachment,
2268
+ onClose
2269
+ }) {
2270
+ const { url } = react$1.useAttachmentUrl(attachment.id);
2271
+ const { width, height } = reactNative.Dimensions.get("window");
2272
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Pressable, { style: styles14.viewer, onPress: onClose, children: [
2273
+ url ? /* @__PURE__ */ jsxRuntime.jsx(
2274
+ reactNative.Image,
1217
2275
  {
1218
- style: [
1219
- styles9.imagePlaceholder,
1220
- {
1221
- width: maxWidth,
1222
- height: 200,
1223
- borderRadius: theme.radii.md,
1224
- backgroundColor: theme.colors.surface2
1225
- }
1226
- ]
2276
+ source: { uri: url },
2277
+ style: { width, height },
2278
+ resizeMode: "contain",
2279
+ accessible: true,
2280
+ accessibilityLabel: attachment.original_filename ?? "image"
1227
2281
  }
1228
- ) });
1229
- }
1230
- return /* @__PURE__ */ jsxRuntime.jsxs(
1231
- reactNative.Pressable,
1232
- {
1233
- onPress,
1234
- disabled: !onPress,
1235
- style: [
1236
- styles9.fileCard,
1237
- {
1238
- backgroundColor: theme.colors.surface2,
1239
- borderColor: theme.colors.border,
1240
- borderRadius: theme.radii.md
1241
- }
1242
- ],
1243
- children: [
1244
- /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "attachment", size: 18, color: theme.colors.ink2 }),
1245
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { flex: 1, marginLeft: 8 }, children: [
1246
- /* @__PURE__ */ jsxRuntime.jsx(
1247
- reactNative.Text,
1248
- {
1249
- style: [styles9.fileName, { color: theme.colors.ink, fontFamily: theme.type.fontBody }],
1250
- numberOfLines: 1,
1251
- children: attachment.original_filename ?? "file"
1252
- }
1253
- ),
1254
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles9.fileSize, { color: theme.colors.ink3 }], children: formatBytes(attachment.byte_size) })
1255
- ] })
1256
- ]
1257
- }
1258
- );
1259
- }
1260
- function formatBytes(n) {
1261
- if (n < 1024) return `${n} B`;
1262
- if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
1263
- if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;
1264
- return `${(n / 1024 / 1024 / 1024).toFixed(1)} GB`;
2282
+ ) : null,
2283
+ /* @__PURE__ */ jsxRuntime.jsx(
2284
+ reactNative.Pressable,
2285
+ {
2286
+ onPress: onClose,
2287
+ hitSlop: 12,
2288
+ style: styles14.closeBtn,
2289
+ accessibilityLabel: "Close viewer",
2290
+ children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "close", size: 20, color: "#ffffff" })
2291
+ }
2292
+ )
2293
+ ] });
1265
2294
  }
1266
- var styles9 = reactNative.StyleSheet.create({
1267
- imagePlaceholder: {},
1268
- fileCard: {
2295
+ var styles14 = reactNative.StyleSheet.create({
2296
+ root: {
2297
+ width: "100%",
2298
+ overflow: "hidden",
2299
+ marginBottom: 4
2300
+ },
2301
+ rowEven: {
1269
2302
  flexDirection: "row",
2303
+ gap: 2
2304
+ },
2305
+ tileBase: {
2306
+ overflow: "hidden"
2307
+ },
2308
+ // n=1: full bleed at the bubble's available width, 16:10 aspect.
2309
+ tileFull: {
2310
+ width: "100%",
2311
+ aspectRatio: 16 / 10
2312
+ },
2313
+ // n=2 / n=4 (per row): two equal squares side-by-side.
2314
+ tileHalfSquare: {
2315
+ flex: 1,
2316
+ aspectRatio: 1
2317
+ },
2318
+ // n=3 hero: full-bleed top tile, 16:9.
2319
+ tileHero: {
2320
+ width: "100%",
2321
+ aspectRatio: 16 / 9
2322
+ },
2323
+ tileImage: {
2324
+ width: "100%",
2325
+ height: "100%"
2326
+ },
2327
+ overflow: {
2328
+ position: "absolute",
2329
+ inset: 0,
2330
+ backgroundColor: "rgba(0,0,0,0.55)",
1270
2331
  alignItems: "center",
1271
- padding: 8,
1272
- borderWidth: reactNative.StyleSheet.hairlineWidth
2332
+ justifyContent: "center"
1273
2333
  },
1274
- fileName: {
1275
- fontSize: 13,
1276
- fontWeight: "500"
2334
+ overflowText: {
2335
+ color: "#ffffff",
2336
+ fontWeight: "700",
2337
+ fontSize: 22
1277
2338
  },
1278
- fileSize: {
1279
- fontSize: 11
2339
+ viewer: {
2340
+ flex: 1,
2341
+ backgroundColor: "#000000",
2342
+ alignItems: "center",
2343
+ justifyContent: "center"
2344
+ },
2345
+ closeBtn: {
2346
+ position: "absolute",
2347
+ top: 48,
2348
+ right: 16,
2349
+ padding: 8,
2350
+ backgroundColor: "rgba(0,0,0,0.5)",
2351
+ borderRadius: 20
1280
2352
  }
1281
2353
  });
1282
2354
  function MessageBubble({
@@ -1292,8 +2364,11 @@ function MessageBubble({
1292
2364
  showAttachments = true
1293
2365
  }) {
1294
2366
  const theme = usePoolseTheme();
2367
+ const { width: screenWidth } = reactNative.useWindowDimensions();
1295
2368
  const isSelf = currentUserId !== null && message.sender_id === currentUserId;
1296
2369
  const [expanded, setExpanded] = react.useState(false);
2370
+ const bubbleMaxWidth = Math.floor(screenWidth * 0.78);
2371
+ const contentMaxWidth = bubbleMaxWidth - 28;
1297
2372
  const sender = react$1.useUser(!isSelf ? message.sender_external_id : null);
1298
2373
  const senderFallbackName = message.sender_external_id ?? "Unknown";
1299
2374
  const senderLabel = sender.profile?.displayName ?? (message.sender_external_id ? labelFor?.(message.sender_external_id) ?? senderFallbackName : senderFallbackName);
@@ -1323,17 +2398,21 @@ function MessageBubble({
1323
2398
  reactNative.View,
1324
2399
  {
1325
2400
  style: [
1326
- styles10.wrap,
2401
+ styles15.wrap,
1327
2402
  {
1328
2403
  alignSelf: isSelf ? "flex-end" : "flex-start",
1329
- maxWidth: "80%"
2404
+ // Pixel maxWidth (not percentage) so single-line and
2405
+ // multi-line bubbles land at the same right edge — was
2406
+ // 88% of the bubbleCol which produced inconsistent right
2407
+ // padding when the row padding changed.
2408
+ maxWidth: bubbleMaxWidth
1330
2409
  }
1331
2410
  ],
1332
2411
  children: /* @__PURE__ */ jsxRuntime.jsxs(
1333
2412
  reactNative.View,
1334
2413
  {
1335
2414
  style: [
1336
- styles10.bubble,
2415
+ styles15.bubble,
1337
2416
  {
1338
2417
  backgroundColor: bubbleColor,
1339
2418
  borderColor: isSelf ? "transparent" : theme.colors.border,
@@ -1342,13 +2421,20 @@ function MessageBubble({
1342
2421
  borderTopRightRadius: radii2.topRight,
1343
2422
  borderBottomLeftRadius: radii2.bottomLeft,
1344
2423
  borderBottomRightRadius: radii2.bottomRight,
1345
- paddingHorizontal: theme.spacing.md,
1346
- paddingVertical: theme.spacing.sm
2424
+ // Slightly more breathing room — 14/10 reads tighter and
2425
+ // matches the iMessage / WhatsApp bubble density.
2426
+ paddingHorizontal: 14,
2427
+ paddingVertical: 10,
2428
+ // Floor the bubble at a width that always fits the meta row
2429
+ // ("edited HH:MM ✓✓" → ~110px). Without this, short bodies
2430
+ // like "ok" produce a bubble too narrow for the meta to lay
2431
+ // out, and `edited` or the time gets clipped.
2432
+ minWidth: isSelf && isEdited ? 130 : 90
1347
2433
  },
1348
2434
  theme.shadows.sm
1349
2435
  ],
1350
2436
  children: [
1351
- showSenderName && !isSelf && (groupPosition === "first" || groupPosition === "standalone") ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles10.senderName, { color: senderTint, fontFamily: theme.type.fontBody }], children: senderLabel }) : null,
2437
+ showSenderName && !isSelf && (groupPosition === "first" || groupPosition === "standalone") ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles15.senderName, { color: senderTint, fontFamily: theme.type.fontBody }], children: senderLabel }) : null,
1352
2438
  message.quoted_message ? /* @__PURE__ */ jsxRuntime.jsx(
1353
2439
  QuotedCard,
1354
2440
  {
@@ -1358,13 +2444,13 @@ function MessageBubble({
1358
2444
  ...onQuotedClick ? { onPress: () => onQuotedClick(message.quoted_message.id) } : {}
1359
2445
  }
1360
2446
  ) : null,
1361
- renderAttachments && images.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { gap: theme.spacing.xs, marginBottom: theme.spacing.xs }, children: images.map((att) => /* @__PURE__ */ jsxRuntime.jsx(AttachmentPreview, { attachment: att }, att.id)) }) : null,
1362
- renderAttachments && files.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { gap: theme.spacing.xs, marginBottom: theme.spacing.xs }, children: files.map((att) => /* @__PURE__ */ jsxRuntime.jsx(AttachmentPreview, { attachment: att }, att.id)) }) : null,
2447
+ renderAttachments && images.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(ImageMosaic, { images, maxWidth: contentMaxWidth }) : null,
2448
+ renderAttachments && files.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { gap: theme.spacing.xs, marginBottom: theme.spacing.xs }, children: files.map((att) => /* @__PURE__ */ jsxRuntime.jsx(AttachmentPreview, { attachment: att, maxWidth: contentMaxWidth }, att.id)) }) : null,
1363
2449
  body.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(
1364
2450
  reactNative.Text,
1365
2451
  {
1366
2452
  style: [
1367
- styles10.body,
2453
+ styles15.body,
1368
2454
  {
1369
2455
  color: isDeleted ? theme.colors.ink3 : textColor,
1370
2456
  fontFamily: theme.type.fontBody,
@@ -1380,34 +2466,49 @@ function MessageBubble({
1380
2466
  reactNative.Text,
1381
2467
  {
1382
2468
  style: [
1383
- styles10.readMore,
2469
+ styles15.readMore,
1384
2470
  { color: isSelf ? theme.colors.onBrand : theme.colors.brand }
1385
2471
  ],
1386
2472
  children: expanded ? "Show less" : "Read more"
1387
2473
  }
1388
2474
  ) }) : null,
1389
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles10.metaRow, children: [
1390
- isEdited ? /* @__PURE__ */ jsxRuntime.jsxs(
2475
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles15.metaRow, children: [
2476
+ isEdited ? /* @__PURE__ */ jsxRuntime.jsx(
1391
2477
  reactNative.Text,
1392
2478
  {
1393
- style: [styles10.meta, { color: isSelf ? theme.colors.onBrand : theme.colors.ink3 }],
1394
- children: [
1395
- "edited \xB7",
1396
- " "
1397
- ]
2479
+ style: [
2480
+ styles15.editedTag,
2481
+ {
2482
+ color: isSelf ? theme.colors.onBrand : theme.colors.ink3,
2483
+ opacity: 0.7
2484
+ }
2485
+ ],
2486
+ children: "edited"
1398
2487
  }
1399
2488
  ) : null,
1400
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles10.meta, { color: isSelf ? theme.colors.onBrand : theme.colors.ink3 }], children: time }),
1401
- isSelf && readState ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { marginLeft: 4 }, children: /* @__PURE__ */ jsxRuntime.jsx(
2489
+ /* @__PURE__ */ jsxRuntime.jsx(
2490
+ reactNative.Text,
2491
+ {
2492
+ style: [
2493
+ styles15.meta,
2494
+ {
2495
+ color: isSelf ? theme.colors.onBrand : theme.colors.ink3,
2496
+ opacity: isSelf ? 0.85 : 1
2497
+ }
2498
+ ],
2499
+ children: time
2500
+ }
2501
+ ),
2502
+ isSelf && readState ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles15.readTick, children: /* @__PURE__ */ jsxRuntime.jsx(
1402
2503
  PoolseIcon,
1403
2504
  {
1404
2505
  name: readState === "read" ? "check-double" : "check",
1405
- size: 12,
2506
+ size: 13,
1406
2507
  color: theme.colors.onBrand
1407
2508
  }
1408
2509
  ) }) : null
1409
2510
  ] }),
1410
- actionsTrigger ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles10.actionsSlot, children: actionsTrigger }) : null
2511
+ actionsTrigger ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles15.actionsSlot, children: actionsTrigger }) : null
1411
2512
  ]
1412
2513
  }
1413
2514
  )
@@ -1431,84 +2532,241 @@ function QuotedCard({
1431
2532
  onPress,
1432
2533
  disabled: !onPress,
1433
2534
  style: [
1434
- styles10.quoted,
2535
+ styles15.quoted,
1435
2536
  {
1436
2537
  backgroundColor: bg,
1437
2538
  borderLeftColor: accent
1438
2539
  }
1439
2540
  ],
1440
2541
  children: [
1441
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles10.quotedSender, { color: accent }], children: senderName }),
1442
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles10.quotedBody, { color: txt }], numberOfLines: 2, children: quoted.body ?? "\u2026" })
2542
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles15.quotedSender, { color: accent }], children: senderName }),
2543
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles15.quotedBody, { color: txt }], numberOfLines: 2, children: quoted.body ?? "\u2026" })
1443
2544
  ]
1444
2545
  }
1445
- );
1446
- }
1447
- function isImageContentType(ct) {
1448
- if (!ct) return false;
1449
- return ct.startsWith("image/");
1450
- }
1451
- function formatTime(iso) {
1452
- try {
1453
- const d = new Date(iso);
1454
- return d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
1455
- } catch {
1456
- return "";
1457
- }
2546
+ );
2547
+ }
2548
+ function isImageContentType(ct) {
2549
+ if (!ct) return false;
2550
+ return ct.startsWith("image/");
2551
+ }
2552
+ function formatTime(iso) {
2553
+ try {
2554
+ const d = new Date(iso);
2555
+ const now = /* @__PURE__ */ new Date();
2556
+ const sameDay = isSameDay(d, now);
2557
+ const time = d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
2558
+ if (sameDay) return time;
2559
+ const yesterday = new Date(now);
2560
+ yesterday.setDate(now.getDate() - 1);
2561
+ if (isSameDay(d, yesterday)) return `Yesterday ${time}`;
2562
+ const diffMs = now.getTime() - d.getTime();
2563
+ const diffDays = Math.floor(diffMs / 864e5);
2564
+ if (diffDays < 7) {
2565
+ return `${d.toLocaleDateString([], { weekday: "short" })} ${time}`;
2566
+ }
2567
+ return d.toLocaleDateString([], { month: "short", day: "numeric" });
2568
+ } catch {
2569
+ return "";
2570
+ }
2571
+ }
2572
+ function isSameDay(a, b) {
2573
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
2574
+ }
2575
+ var styles15 = reactNative.StyleSheet.create({
2576
+ wrap: {
2577
+ marginBottom: 2
2578
+ },
2579
+ bubble: {
2580
+ position: "relative"
2581
+ },
2582
+ senderName: {
2583
+ fontSize: 12,
2584
+ fontWeight: "600",
2585
+ marginBottom: 2
2586
+ },
2587
+ body: {
2588
+ flexShrink: 1
2589
+ },
2590
+ readMore: {
2591
+ fontSize: 12,
2592
+ fontWeight: "600",
2593
+ marginTop: 4
2594
+ },
2595
+ metaRow: {
2596
+ flexDirection: "row",
2597
+ alignItems: "center",
2598
+ justifyContent: "flex-end",
2599
+ marginTop: 6,
2600
+ gap: 5,
2601
+ flexShrink: 0
2602
+ },
2603
+ meta: {
2604
+ fontSize: 11,
2605
+ fontVariant: ["tabular-nums"],
2606
+ includeFontPadding: false
2607
+ },
2608
+ editedTag: {
2609
+ fontSize: 10,
2610
+ fontStyle: "italic",
2611
+ letterSpacing: 0.1
2612
+ },
2613
+ readTick: {
2614
+ marginLeft: 2,
2615
+ marginTop: 1
2616
+ },
2617
+ actionsSlot: {
2618
+ position: "absolute",
2619
+ top: 4,
2620
+ right: 4
2621
+ },
2622
+ quoted: {
2623
+ borderLeftWidth: 3,
2624
+ paddingLeft: 8,
2625
+ paddingVertical: 4,
2626
+ paddingRight: 6,
2627
+ marginBottom: 6,
2628
+ borderRadius: 4
2629
+ },
2630
+ quotedSender: {
2631
+ fontSize: 12,
2632
+ fontWeight: "600",
2633
+ marginBottom: 2
2634
+ },
2635
+ quotedBody: {
2636
+ fontSize: 12,
2637
+ opacity: 0.85
2638
+ }
2639
+ });
2640
+ var DEFAULT_EMOJIS = ["\u{1F44D}", "\u2764\uFE0F", "\u{1F602}", "\u{1F389}", "\u{1F64F}", "\u{1F525}"];
2641
+ function ReactionStrip({
2642
+ messageId,
2643
+ conversationId,
2644
+ meId,
2645
+ initialReactions
2646
+ }) {
2647
+ const theme = usePoolseTheme();
2648
+ const { reactions, addReaction, removeReaction } = react$1.useReactions(messageId, {
2649
+ conversationId,
2650
+ currentUserId: meId,
2651
+ ...initialReactions ? { initialReactions } : {}
2652
+ });
2653
+ const entries = Object.entries(reactions);
2654
+ if (entries.length === 0) return null;
2655
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles16.strip, children: entries.map(([emoji, userIds]) => {
2656
+ const userArr = Array.isArray(userIds) ? userIds : [];
2657
+ const meReacted = meId !== null && userArr.includes(meId);
2658
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2659
+ reactNative.Pressable,
2660
+ {
2661
+ onPress: () => meReacted ? removeReaction(emoji) : addReaction(emoji),
2662
+ style: [
2663
+ styles16.chip,
2664
+ {
2665
+ backgroundColor: meReacted ? theme.colors.brandSoft : theme.colors.surface2,
2666
+ borderColor: meReacted ? theme.colors.brand : theme.colors.border
2667
+ }
2668
+ ],
2669
+ children: [
2670
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles16.emoji, children: emoji }),
2671
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles16.count, { color: theme.colors.ink2 }], children: userArr.length })
2672
+ ]
2673
+ },
2674
+ emoji
2675
+ );
2676
+ }) });
2677
+ }
2678
+ function ReactionPicker({
2679
+ visible,
2680
+ onClose,
2681
+ onPick,
2682
+ emojis = DEFAULT_EMOJIS
2683
+ }) {
2684
+ const theme = usePoolseTheme();
2685
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Modal, { visible, transparent: true, animationType: "fade", onRequestClose: onClose, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { style: styles16.backdrop, onPress: onClose, children: /* @__PURE__ */ jsxRuntime.jsx(
2686
+ reactNative.View,
2687
+ {
2688
+ style: [
2689
+ styles16.sheet,
2690
+ {
2691
+ backgroundColor: theme.colors.surface,
2692
+ borderRadius: theme.radii.lg
2693
+ },
2694
+ theme.shadows.lg
2695
+ ],
2696
+ children: emojis.map((emoji) => /* @__PURE__ */ jsxRuntime.jsx(
2697
+ reactNative.Pressable,
2698
+ {
2699
+ style: styles16.emojiBtn,
2700
+ onPress: () => {
2701
+ onPick(emoji);
2702
+ onClose();
2703
+ },
2704
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles16.bigEmoji, children: emoji })
2705
+ },
2706
+ emoji
2707
+ ))
2708
+ }
2709
+ ) }) });
1458
2710
  }
1459
- var styles10 = reactNative.StyleSheet.create({
1460
- wrap: {
1461
- marginBottom: 4
1462
- },
1463
- bubble: {
1464
- position: "relative"
2711
+ var styles16 = reactNative.StyleSheet.create({
2712
+ strip: {
2713
+ flexDirection: "row",
2714
+ flexWrap: "wrap",
2715
+ gap: 4,
2716
+ marginTop: 2
1465
2717
  },
1466
- senderName: {
1467
- fontSize: 12,
1468
- fontWeight: "600",
1469
- marginBottom: 2
2718
+ chip: {
2719
+ flexDirection: "row",
2720
+ alignItems: "center",
2721
+ paddingHorizontal: 9,
2722
+ paddingVertical: 4,
2723
+ borderRadius: 14,
2724
+ borderWidth: 1,
2725
+ gap: 5,
2726
+ minHeight: 26,
2727
+ // Subtle elevation so chips lift off the surface
2728
+ shadowColor: "#000",
2729
+ shadowOffset: { width: 0, height: 1 },
2730
+ shadowOpacity: 0.06,
2731
+ shadowRadius: 2,
2732
+ elevation: 1
1470
2733
  },
1471
- body: {
1472
- flexShrink: 1
2734
+ emoji: {
2735
+ fontSize: 15,
2736
+ lineHeight: 18
1473
2737
  },
1474
- readMore: {
2738
+ count: {
1475
2739
  fontSize: 12,
1476
2740
  fontWeight: "600",
1477
- marginTop: 4
2741
+ fontVariant: ["tabular-nums"]
1478
2742
  },
1479
- metaRow: {
1480
- flexDirection: "row",
2743
+ backdrop: {
2744
+ flex: 1,
2745
+ backgroundColor: "rgba(0,0,0,0.4)",
2746
+ justifyContent: "center",
1481
2747
  alignItems: "center",
1482
- justifyContent: "flex-end",
1483
- marginTop: 4
1484
- },
1485
- meta: {
1486
- fontSize: 10,
1487
- opacity: 0.7
2748
+ paddingHorizontal: 24
1488
2749
  },
1489
- actionsSlot: {
1490
- position: "absolute",
1491
- top: 4,
1492
- right: 4
1493
- },
1494
- quoted: {
1495
- borderLeftWidth: 3,
1496
- paddingLeft: 8,
1497
- paddingVertical: 4,
1498
- paddingRight: 6,
1499
- marginBottom: 6,
1500
- borderRadius: 4
2750
+ sheet: {
2751
+ flexDirection: "row",
2752
+ paddingHorizontal: 12,
2753
+ paddingVertical: 12,
2754
+ gap: 4,
2755
+ alignItems: "center"
1501
2756
  },
1502
- quotedSender: {
1503
- fontSize: 12,
1504
- fontWeight: "600",
1505
- marginBottom: 2
2757
+ emojiBtn: {
2758
+ paddingHorizontal: 8,
2759
+ paddingVertical: 6,
2760
+ borderRadius: 999,
2761
+ alignItems: "center",
2762
+ justifyContent: "center"
1506
2763
  },
1507
- quotedBody: {
1508
- fontSize: 12,
1509
- opacity: 0.85
2764
+ bigEmoji: {
2765
+ fontSize: 32,
2766
+ lineHeight: 38
1510
2767
  }
1511
2768
  });
2769
+ var SWIPE_REPLY_THRESHOLD = 50;
1512
2770
  function MessageRow({
1513
2771
  msg,
1514
2772
  meId,
@@ -1536,12 +2794,88 @@ function MessageRow({
1536
2794
  const theme = usePoolseTheme();
1537
2795
  const isSelf = meId !== null && msg.sender_id === meId;
1538
2796
  const [actionsOpen, setActionsOpen] = react.useState(false);
2797
+ const reactionsApi = react$1.useReactions(msg.id, {
2798
+ conversationId: msg.conversation_id,
2799
+ currentUserId: meId,
2800
+ ...msg.reactions ? { initialReactions: msg.reactions } : {}
2801
+ });
2802
+ const translateX = react.useRef(new reactNative.Animated.Value(0)).current;
2803
+ const swipeEnabled = quotations && !!onQuote && !editing;
2804
+ const panResponder = react.useMemo(
2805
+ () => reactNative.PanResponder.create({
2806
+ // Tap / long-press still go to the inner Pressable.
2807
+ onStartShouldSetPanResponder: () => false,
2808
+ onStartShouldSetPanResponderCapture: () => false,
2809
+ // Capture-phase claim: we intercept the gesture BEFORE the
2810
+ // child Pressable as soon as the move shows clear horizontal
2811
+ // intent. Without capture-phase, the Pressable's touch handling
2812
+ // wins first and the pan never starts (this is why self-bubble
2813
+ // left swipes felt totally dead — the Pressable was attached
2814
+ // to a touch that started with the user's finger over the
2815
+ // bubble center, and we never got a turn).
2816
+ onMoveShouldSetPanResponder: () => false,
2817
+ onMoveShouldSetPanResponderCapture: (_, g) => {
2818
+ if (!swipeEnabled) return false;
2819
+ return Math.abs(g.dx) > 4 && Math.abs(g.dx) > Math.abs(g.dy);
2820
+ },
2821
+ // Don't surrender the gesture mid-swipe (e.g. parent FlatList
2822
+ // asking for it back) — once we're translating the bubble we
2823
+ // hold on until release.
2824
+ onPanResponderTerminationRequest: () => false,
2825
+ // 1:1 finger-follow up to the threshold, then square-root
2826
+ // damping past it so the bubble never slides off-screen.
2827
+ // Matches iMessage / WhatsApp — feels weightless during the
2828
+ // committed range and stiffens once you've already triggered.
2829
+ onPanResponderMove: (_, g) => {
2830
+ const dx = isSelf ? Math.min(0, g.dx) : Math.max(0, g.dx);
2831
+ const abs = Math.abs(dx);
2832
+ let translated;
2833
+ if (abs <= SWIPE_REPLY_THRESHOLD) {
2834
+ translated = dx;
2835
+ } else {
2836
+ const excess = abs - SWIPE_REPLY_THRESHOLD;
2837
+ const dampened = SWIPE_REPLY_THRESHOLD + Math.sqrt(excess * 6);
2838
+ translated = dx < 0 ? -dampened : dampened;
2839
+ }
2840
+ translateX.setValue(translated);
2841
+ },
2842
+ onPanResponderRelease: (_, g) => {
2843
+ const past = Math.abs(g.dx) >= SWIPE_REPLY_THRESHOLD;
2844
+ if (past && onQuote) onQuote();
2845
+ reactNative.Animated.spring(translateX, {
2846
+ toValue: 0,
2847
+ useNativeDriver: false,
2848
+ speed: 28,
2849
+ bounciness: 4
2850
+ }).start();
2851
+ },
2852
+ onPanResponderTerminate: () => {
2853
+ reactNative.Animated.spring(translateX, {
2854
+ toValue: 0,
2855
+ useNativeDriver: false,
2856
+ speed: 28,
2857
+ bounciness: 4
2858
+ }).start();
2859
+ }
2860
+ }),
2861
+ [swipeEnabled, isSelf, onQuote, translateX]
2862
+ );
2863
+ const replyIconOpacity = translateX.interpolate({
2864
+ inputRange: isSelf ? [-SWIPE_REPLY_THRESHOLD, -SWIPE_REPLY_THRESHOLD / 2, 0] : [0, SWIPE_REPLY_THRESHOLD / 2, SWIPE_REPLY_THRESHOLD],
2865
+ outputRange: isSelf ? [1, 0.3, 0] : [0, 0.3, 1],
2866
+ extrapolate: "clamp"
2867
+ });
2868
+ const replyIconScale = translateX.interpolate({
2869
+ inputRange: isSelf ? [-SWIPE_REPLY_THRESHOLD, 0] : [0, SWIPE_REPLY_THRESHOLD],
2870
+ outputRange: isSelf ? [1, 0.5] : [0.5, 1],
2871
+ extrapolate: "clamp"
2872
+ });
1539
2873
  const showAvatarSlot = showAvatar && !isSelf;
1540
2874
  const showAvatarRender = showAvatarSlot && (groupPosition === "last" || groupPosition === "standalone");
1541
2875
  const avatarUrl = avatarFor && msg.sender_external_id ? avatarFor(msg.sender_external_id) : null;
1542
2876
  const senderLabel = msg.sender_external_id && labelFor ? labelFor(msg.sender_external_id) : msg.sender_external_id ?? null;
1543
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles11.row, { alignSelf: isSelf ? "flex-end" : "flex-start" }], children: [
1544
- showAvatarSlot ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles11.avatarSlot, children: showAvatarRender ? /* @__PURE__ */ jsxRuntime.jsx(
2877
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles17.row, { alignSelf: isSelf ? "flex-end" : "flex-start" }], children: [
2878
+ showAvatarSlot ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles17.avatarSlot, children: showAvatarRender ? /* @__PURE__ */ jsxRuntime.jsx(
1545
2879
  Avatar,
1546
2880
  {
1547
2881
  src: avatarUrl ?? null,
@@ -1550,68 +2884,86 @@ function MessageRow({
1550
2884
  size: "sm"
1551
2885
  }
1552
2886
  ) : null }) : null,
1553
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles11.bubbleCol, children: [
1554
- /* @__PURE__ */ jsxRuntime.jsx(
1555
- reactNative.Pressable,
2887
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles17.bubbleCol, children: [
2888
+ swipeEnabled ? /* @__PURE__ */ jsxRuntime.jsx(
2889
+ reactNative.Animated.View,
1556
2890
  {
1557
- onLongPress: () => actions && setActionsOpen(true),
1558
- accessibilityRole: "button",
1559
- accessibilityLabel: "Message actions",
1560
- children: editing ? /* @__PURE__ */ jsxRuntime.jsx(
1561
- EditableMessageBubble,
2891
+ pointerEvents: "none",
2892
+ style: [
2893
+ styles17.replyHint,
2894
+ isSelf ? styles17.replyHintRight : styles17.replyHintLeft,
1562
2895
  {
1563
- message: msg,
1564
- currentUserId: meId,
1565
- onCancel: onCancelEdit ?? (() => void 0),
1566
- onSave: onSaveEdit ?? (() => void 0)
2896
+ opacity: replyIconOpacity,
2897
+ transform: [{ scale: replyIconScale }],
2898
+ backgroundColor: theme.colors.surface2
1567
2899
  }
1568
- ) : /* @__PURE__ */ jsxRuntime.jsx(
1569
- MessageBubble,
2900
+ ],
2901
+ children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "reply", size: 16, color: theme.colors.brand })
2902
+ }
2903
+ ) : null,
2904
+ /* @__PURE__ */ jsxRuntime.jsx(
2905
+ reactNative.Animated.View,
2906
+ {
2907
+ ...swipeEnabled ? panResponder.panHandlers : {},
2908
+ style: { transform: [{ translateX }] },
2909
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2910
+ reactNative.Pressable,
1570
2911
  {
1571
- message: msg,
1572
- currentUserId: meId,
1573
- ...readState ? { readState } : {},
1574
- ...labelFor ? { labelFor } : {},
1575
- ...onQuotedClick ? { onQuotedClick } : {},
1576
- groupPosition,
1577
- maxBodyLength,
1578
- showSenderName,
1579
- showAttachments: attachments,
1580
- actionsTrigger: actions ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: () => setActionsOpen(true), hitSlop: 8, children: /* @__PURE__ */ jsxRuntime.jsx(
1581
- PoolseIcon,
2912
+ onLongPress: () => {
2913
+ if (!actions) return;
2914
+ setActionsOpen(true);
2915
+ },
2916
+ accessibilityRole: "button",
2917
+ accessibilityLabel: "Message actions",
2918
+ children: editing ? /* @__PURE__ */ jsxRuntime.jsx(
2919
+ EditableMessageBubble,
1582
2920
  {
1583
- name: "chevron-down",
1584
- size: 14,
1585
- color: isSelf ? theme.colors.onBrand : theme.colors.ink3
2921
+ message: msg,
2922
+ currentUserId: meId,
2923
+ onCancel: onCancelEdit ?? (() => void 0),
2924
+ onSave: onSaveEdit ?? (() => void 0)
1586
2925
  }
1587
- ) }) : null
2926
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
2927
+ MessageBubble,
2928
+ {
2929
+ message: msg,
2930
+ currentUserId: meId,
2931
+ ...readState ? { readState } : {},
2932
+ ...labelFor ? { labelFor } : {},
2933
+ ...onQuotedClick ? { onQuotedClick } : {},
2934
+ groupPosition,
2935
+ maxBodyLength,
2936
+ showSenderName,
2937
+ showAttachments: attachments,
2938
+ actionsTrigger: actions ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: () => setActionsOpen(true), hitSlop: 8, children: /* @__PURE__ */ jsxRuntime.jsx(
2939
+ PoolseIcon,
2940
+ {
2941
+ name: "chevron-down",
2942
+ size: 14,
2943
+ color: isSelf ? theme.colors.onBrand : theme.colors.ink3
2944
+ }
2945
+ ) }) : null
2946
+ }
2947
+ )
1588
2948
  }
1589
2949
  )
1590
2950
  }
1591
2951
  ),
1592
- threads && (msg.reply_count ?? 0) > 0 && onOpenThread ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: onOpenThread, style: styles11.threadPill, children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { style: [styles11.threadPillText, { color: theme.colors.brand }], children: [
2952
+ threads && (msg.reply_count ?? 0) > 0 && onOpenThread ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: onOpenThread, style: styles17.threadPill, children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { style: [styles17.threadPillText, { color: theme.colors.brand }], children: [
1593
2953
  "\u{1F4AC} ",
1594
2954
  msg.reply_count,
1595
2955
  " ",
1596
2956
  msg.reply_count === 1 ? "reply" : "replies"
1597
2957
  ] }) }) : null,
1598
- reactions && msg.reactions && Object.keys(msg.reactions).length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles11.reactionsRow, children: Object.entries(msg.reactions).map(([emoji, userIds]) => /* @__PURE__ */ jsxRuntime.jsxs(
1599
- reactNative.View,
2958
+ reactions ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles17.reactionsRow, { alignSelf: isSelf ? "flex-end" : "flex-start" }], children: /* @__PURE__ */ jsxRuntime.jsx(
2959
+ ReactionStrip,
1600
2960
  {
1601
- style: [
1602
- styles11.reactionChip,
1603
- {
1604
- backgroundColor: theme.colors.surface2,
1605
- borderColor: theme.colors.border
1606
- }
1607
- ],
1608
- children: [
1609
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles11.reactionEmoji, children: emoji }),
1610
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles11.reactionCount, { color: theme.colors.ink2 }], children: Array.isArray(userIds) ? userIds.length : 0 })
1611
- ]
1612
- },
1613
- emoji
1614
- )) }) : null
2961
+ messageId: msg.id,
2962
+ conversationId: msg.conversation_id,
2963
+ meId,
2964
+ ...msg.reactions ? { initialReactions: msg.reactions } : {}
2965
+ }
2966
+ ) }) : null
1615
2967
  ] }),
1616
2968
  actions && actionsOpen ? /* @__PURE__ */ jsxRuntime.jsx(
1617
2969
  MessageActions,
@@ -1619,6 +2971,13 @@ function MessageRow({
1619
2971
  visible: actionsOpen,
1620
2972
  onClose: () => setActionsOpen(false),
1621
2973
  isSelf,
2974
+ ...reactions ? {
2975
+ // Inline quick-react row at the top of the sheet —
2976
+ // one tap = react + close, no second modal needed.
2977
+ onPickReaction: (emoji) => {
2978
+ void reactionsApi.addReaction(emoji);
2979
+ }
2980
+ } : {},
1622
2981
  ...onStartEdit ? {
1623
2982
  onEdit: () => {
1624
2983
  setActionsOpen(false);
@@ -1647,12 +3006,18 @@ function MessageRow({
1647
3006
  ) : null
1648
3007
  ] });
1649
3008
  }
1650
- var styles11 = reactNative.StyleSheet.create({
3009
+ var styles17 = reactNative.StyleSheet.create({
1651
3010
  row: {
1652
3011
  flexDirection: "row",
1653
3012
  alignItems: "flex-end",
1654
- marginVertical: 2,
1655
- paddingHorizontal: 8,
3013
+ // Tighter vertical rhythm — was 2px each side (4px between rows)
3014
+ // plus the bubble's 4px marginBottom = 8px. Now 1+1+2 = 4px,
3015
+ // closer to iMessage / WhatsApp density.
3016
+ marginVertical: 1,
3017
+ // 10px on each side — tight enough to use the available width
3018
+ // for content, loose enough to keep bubbles off the screen edge
3019
+ // on every device.
3020
+ paddingHorizontal: 10,
1656
3021
  maxWidth: "95%"
1657
3022
  },
1658
3023
  avatarSlot: {
@@ -1660,7 +3025,25 @@ var styles11 = reactNative.StyleSheet.create({
1660
3025
  marginRight: 6
1661
3026
  },
1662
3027
  bubbleCol: {
1663
- flexShrink: 1
3028
+ flexShrink: 1,
3029
+ position: "relative"
3030
+ },
3031
+ replyHint: {
3032
+ position: "absolute",
3033
+ top: "50%",
3034
+ marginTop: -16,
3035
+ width: 32,
3036
+ height: 32,
3037
+ borderRadius: 16,
3038
+ alignItems: "center",
3039
+ justifyContent: "center",
3040
+ zIndex: -1
3041
+ },
3042
+ replyHintLeft: {
3043
+ left: -44
3044
+ },
3045
+ replyHintRight: {
3046
+ right: -44
1664
3047
  },
1665
3048
  threadPill: {
1666
3049
  alignSelf: "flex-start",
@@ -1673,111 +3056,224 @@ var styles11 = reactNative.StyleSheet.create({
1673
3056
  fontWeight: "600"
1674
3057
  },
1675
3058
  reactionsRow: {
1676
- flexDirection: "row",
1677
- flexWrap: "wrap",
1678
- marginTop: 4,
1679
- gap: 4
1680
- },
1681
- reactionChip: {
1682
- flexDirection: "row",
1683
- alignItems: "center",
1684
- paddingHorizontal: 6,
1685
- paddingVertical: 2,
1686
- borderRadius: 12,
1687
- borderWidth: reactNative.StyleSheet.hairlineWidth
1688
- },
1689
- reactionEmoji: {
1690
- fontSize: 13,
1691
- marginRight: 3
1692
- },
1693
- reactionCount: {
1694
- fontSize: 11,
1695
- fontWeight: "600"
3059
+ marginTop: 2
1696
3060
  }
1697
3061
  });
3062
+ var SHEET_HEIGHT_FRACTION = 0.88;
1698
3063
  function ThreadView({
1699
3064
  visible,
1700
3065
  onClose,
1701
3066
  conversationId,
1702
3067
  rootMessage,
1703
- labelFor
3068
+ labelFor,
3069
+ avatarFor
3070
+ }) {
3071
+ return /* @__PURE__ */ jsxRuntime.jsx(
3072
+ reactNative.Modal,
3073
+ {
3074
+ visible,
3075
+ transparent: true,
3076
+ animationType: "slide",
3077
+ onRequestClose: onClose,
3078
+ statusBarTranslucent: true,
3079
+ children: /* @__PURE__ */ jsxRuntime.jsx(UploadProvider, { children: /* @__PURE__ */ jsxRuntime.jsx(
3080
+ ThreadSheet,
3081
+ {
3082
+ onClose,
3083
+ conversationId,
3084
+ rootMessage,
3085
+ ...labelFor ? { labelFor } : {},
3086
+ ...avatarFor ? { avatarFor } : {}
3087
+ }
3088
+ ) })
3089
+ }
3090
+ );
3091
+ }
3092
+ function ThreadSheet({
3093
+ onClose,
3094
+ conversationId,
3095
+ rootMessage,
3096
+ labelFor,
3097
+ avatarFor
1704
3098
  }) {
1705
3099
  const theme = usePoolseTheme();
1706
3100
  const { me } = react$1.useMe();
1707
3101
  const meId = me?.id ?? null;
1708
- const { replies, sendReply, loadMore, hasMore } = react$1.useThread(conversationId, rootMessage.id);
1709
- return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Modal, { visible, animationType: "slide", onRequestClose: onClose, children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles12.root, { backgroundColor: theme.colors.paper }], children: [
3102
+ const {
3103
+ replies,
3104
+ sendReply,
3105
+ edit,
3106
+ delete: deleteReply,
3107
+ loadMore,
3108
+ hasMore
3109
+ } = react$1.useThread(conversationId, rootMessage.id);
3110
+ const { height: screenHeight } = reactNative.useWindowDimensions();
3111
+ const sheetHeight = Math.round(screenHeight * SHEET_HEIGHT_FRACTION);
3112
+ const gutter = screenHeight - sheetHeight;
3113
+ const { translateY, panHandlers } = useDismissableSheet({ onClose, sheetHeight });
3114
+ const [pickerOpen, setPickerOpen] = react.useState(false);
3115
+ const [editingId, setEditingId] = react.useState(null);
3116
+ const renderReply = react.useCallback(
3117
+ (msg) => /* @__PURE__ */ jsxRuntime.jsx(
3118
+ MessageRow,
3119
+ {
3120
+ msg,
3121
+ meId,
3122
+ reactions: true,
3123
+ attachments: true,
3124
+ actions: true,
3125
+ threads: false,
3126
+ quotations: false,
3127
+ editing: editingId === msg.id,
3128
+ onStartEdit: () => setEditingId(msg.id),
3129
+ onCancelEdit: () => setEditingId(null),
3130
+ onSaveEdit: async (body) => {
3131
+ await edit(msg.id, body);
3132
+ setEditingId(null);
3133
+ },
3134
+ onDelete: () => {
3135
+ void deleteReply(msg.id);
3136
+ },
3137
+ ...labelFor ? { labelFor } : {},
3138
+ ...avatarFor ? { avatarFor } : {}
3139
+ }
3140
+ ),
3141
+ [meId, editingId, edit, deleteReply, labelFor, avatarFor]
3142
+ );
3143
+ const rootRow = react.useMemo(
3144
+ () => /* @__PURE__ */ jsxRuntime.jsx(
3145
+ MessageRow,
3146
+ {
3147
+ msg: rootMessage,
3148
+ meId,
3149
+ reactions: true,
3150
+ attachments: true,
3151
+ actions: false,
3152
+ threads: false,
3153
+ quotations: false,
3154
+ ...labelFor ? { labelFor } : {},
3155
+ ...avatarFor ? { avatarFor } : {}
3156
+ }
3157
+ ),
3158
+ [rootMessage, meId, labelFor, avatarFor]
3159
+ );
3160
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles18.modalRoot, children: [
3161
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { style: styles18.backdrop, onPress: onClose, accessibilityLabel: "Close thread" }),
1710
3162
  /* @__PURE__ */ jsxRuntime.jsxs(
1711
- reactNative.View,
3163
+ reactNative.Animated.View,
1712
3164
  {
1713
3165
  style: [
1714
- styles12.header,
3166
+ styles18.sheet,
1715
3167
  {
1716
- borderBottomColor: theme.colors.border,
1717
- backgroundColor: theme.colors.surface
1718
- }
3168
+ height: sheetHeight,
3169
+ backgroundColor: theme.colors.paper,
3170
+ borderTopLeftRadius: theme.radii.xl,
3171
+ borderTopRightRadius: theme.radii.xl,
3172
+ transform: [{ translateY }]
3173
+ },
3174
+ theme.shadows.lg
1719
3175
  ],
1720
3176
  children: [
1721
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: onClose, hitSlop: 8, style: styles12.closeBtn, children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "close", size: 18, color: theme.colors.ink }) }),
1722
- /* @__PURE__ */ jsxRuntime.jsx(
1723
- reactNative.Text,
3177
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { ...panHandlers, children: [
3178
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles18.dragHandle, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles18.dragPill, { backgroundColor: theme.colors.ink3 }] }) }),
3179
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles18.header, { borderBottomColor: theme.colors.border }], children: [
3180
+ /* @__PURE__ */ jsxRuntime.jsx(
3181
+ reactNative.Pressable,
3182
+ {
3183
+ onPress: onClose,
3184
+ hitSlop: 16,
3185
+ style: [styles18.closeBtn, { backgroundColor: theme.colors.surface2 }],
3186
+ accessibilityLabel: "Close thread",
3187
+ children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "close", size: 18, color: theme.colors.ink })
3188
+ }
3189
+ ),
3190
+ /* @__PURE__ */ jsxRuntime.jsx(
3191
+ reactNative.Text,
3192
+ {
3193
+ style: [
3194
+ styles18.title,
3195
+ { color: theme.colors.ink, fontFamily: theme.type.fontDisplay }
3196
+ ],
3197
+ children: "Thread"
3198
+ }
3199
+ )
3200
+ ] })
3201
+ ] }),
3202
+ /* @__PURE__ */ jsxRuntime.jsxs(
3203
+ reactNative.KeyboardAvoidingView,
1724
3204
  {
1725
- style: [styles12.title, { color: theme.colors.ink, fontFamily: theme.type.fontDisplay }],
1726
- children: "Thread"
3205
+ style: styles18.kavBody,
3206
+ behavior: reactNative.Platform.OS === "ios" ? "padding" : "height",
3207
+ keyboardVerticalOffset: reactNative.Platform.OS === "ios" ? gutter : 0,
3208
+ children: [
3209
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles18.rootPreview, { backgroundColor: theme.colors.surface2 }], children: rootRow }),
3210
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles18.listWrap, children: /* @__PURE__ */ jsxRuntime.jsx(
3211
+ MessageList,
3212
+ {
3213
+ messages: replies,
3214
+ hasMore,
3215
+ onLoadMore: loadMore,
3216
+ renderItem: renderReply
3217
+ }
3218
+ ) }),
3219
+ /* @__PURE__ */ jsxRuntime.jsx(
3220
+ MessageComposer,
3221
+ {
3222
+ onSend: async (body, opts) => {
3223
+ await sendReply({ body, ...opts });
3224
+ },
3225
+ placeholder: "Reply to thread\u2026",
3226
+ attachments: true,
3227
+ onAttachPress: () => setPickerOpen(true)
3228
+ }
3229
+ )
3230
+ ]
1727
3231
  }
1728
- )
3232
+ ),
3233
+ /* @__PURE__ */ jsxRuntime.jsx(AttachmentPicker, { visible: pickerOpen, onClose: () => setPickerOpen(false) })
1729
3234
  ]
1730
3235
  }
1731
- ),
1732
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles12.rootPreview, { backgroundColor: theme.colors.surface2 }], children: /* @__PURE__ */ jsxRuntime.jsx(
1733
- MessageBubble,
1734
- {
1735
- message: rootMessage,
1736
- currentUserId: meId,
1737
- ...labelFor ? { labelFor } : {}
1738
- }
1739
- ) }),
1740
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(
1741
- MessageList,
1742
- {
1743
- messages: replies,
1744
- hasMore,
1745
- onLoadMore: loadMore,
1746
- renderItem: (msg) => /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { paddingHorizontal: 8 }, children: /* @__PURE__ */ jsxRuntime.jsx(
1747
- MessageBubble,
1748
- {
1749
- message: msg,
1750
- currentUserId: meId,
1751
- ...labelFor ? { labelFor } : {}
1752
- }
1753
- ) }, msg.id)
1754
- }
1755
- ) }),
1756
- /* @__PURE__ */ jsxRuntime.jsx(
1757
- MessageComposer,
1758
- {
1759
- onSend: async (body, opts) => {
1760
- await sendReply({ body, ...opts });
1761
- },
1762
- placeholder: "Reply to thread\u2026"
1763
- }
1764
3236
  )
1765
- ] }) });
3237
+ ] });
1766
3238
  }
1767
- var styles12 = reactNative.StyleSheet.create({
1768
- root: {
1769
- flex: 1
3239
+ var styles18 = reactNative.StyleSheet.create({
3240
+ modalRoot: {
3241
+ flex: 1,
3242
+ justifyContent: "flex-end"
3243
+ },
3244
+ backdrop: {
3245
+ ...reactNative.StyleSheet.absoluteFillObject,
3246
+ backgroundColor: "rgba(0,0,0,0.4)"
3247
+ },
3248
+ sheet: {
3249
+ overflow: "hidden"
3250
+ },
3251
+ dragHandle: {
3252
+ alignItems: "center",
3253
+ paddingTop: 8,
3254
+ paddingBottom: 4
3255
+ },
3256
+ dragPill: {
3257
+ width: 36,
3258
+ height: 4,
3259
+ borderRadius: 2,
3260
+ opacity: 0.4
1770
3261
  },
1771
3262
  header: {
1772
3263
  flexDirection: "row",
1773
3264
  alignItems: "center",
1774
3265
  paddingHorizontal: 12,
1775
- paddingVertical: 12,
3266
+ paddingTop: 6,
3267
+ paddingBottom: 10,
1776
3268
  borderBottomWidth: reactNative.StyleSheet.hairlineWidth,
1777
- gap: 8
3269
+ gap: 10
1778
3270
  },
1779
3271
  closeBtn: {
1780
- padding: 6
3272
+ width: 32,
3273
+ height: 32,
3274
+ borderRadius: 16,
3275
+ alignItems: "center",
3276
+ justifyContent: "center"
1781
3277
  },
1782
3278
  title: {
1783
3279
  fontSize: 16,
@@ -1786,6 +3282,12 @@ var styles12 = reactNative.StyleSheet.create({
1786
3282
  rootPreview: {
1787
3283
  paddingVertical: 8,
1788
3284
  paddingHorizontal: 8
3285
+ },
3286
+ kavBody: {
3287
+ flex: 1
3288
+ },
3289
+ listWrap: {
3290
+ flex: 1
1789
3291
  }
1790
3292
  });
1791
3293
  function UserName({ externalId, fallback, labelFor }) {
@@ -1806,7 +3308,7 @@ function TypingIndicator({ typing, labelFor }) {
1806
3308
  const ids = Array.from(typing);
1807
3309
  const isTyping = ids.length > 0;
1808
3310
  if (!isTyping) return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { height: 0 } });
1809
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles13.row, children: [
3311
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles19.row, children: [
1810
3312
  /* @__PURE__ */ jsxRuntime.jsx(TypingDots, {}),
1811
3313
  /* @__PURE__ */ jsxRuntime.jsx(TypingLabel, { ids, ...labelFor ? { labelFor } : {} })
1812
3314
  ] });
@@ -1851,7 +3353,7 @@ function TypingDots() {
1851
3353
  ],
1852
3354
  opacity: val.interpolate({ inputRange: [0, 1], outputRange: [0.5, 1] })
1853
3355
  });
1854
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles13.dotsWrap, children: [
3356
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles19.dotsWrap, children: [
1855
3357
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.Animated.View, { style: dot(a) }),
1856
3358
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.Animated.View, { style: dot(b) }),
1857
3359
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.Animated.View, { style: dot(c) })
@@ -1867,9 +3369,9 @@ function TypingLabel({
1867
3369
  const name1 = useDisplayName(id1, labelFor);
1868
3370
  const name2 = useDisplayName(id2, labelFor);
1869
3371
  const text = ids.length === 1 ? `${name1} is typing` : ids.length === 2 ? `${name1} and ${name2} are typing` : `${ids.length} people are typing`;
1870
- return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles13.label, { color: theme.colors.ink2, fontFamily: theme.type.fontBody }], children: text });
3372
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles19.label, { color: theme.colors.ink2, fontFamily: theme.type.fontBody }], children: text });
1871
3373
  }
1872
- var styles13 = reactNative.StyleSheet.create({
3374
+ var styles19 = reactNative.StyleSheet.create({
1873
3375
  row: {
1874
3376
  flexDirection: "row",
1875
3377
  alignItems: "center",
@@ -1892,6 +3394,9 @@ function ConversationView({
1892
3394
  renderMessage,
1893
3395
  labelFor,
1894
3396
  avatarFor,
3397
+ header = true,
3398
+ onHeaderMembersPress,
3399
+ onHeaderPress,
1895
3400
  reactions = true,
1896
3401
  mentions = true,
1897
3402
  attachments = true,
@@ -1902,7 +3407,9 @@ function ConversationView({
1902
3407
  maxBodyLength = 200,
1903
3408
  senderLabels = "auto",
1904
3409
  avatars = "auto",
1905
- onMarkedRead
3410
+ onMarkedRead,
3411
+ onSent,
3412
+ keyboardOffset = 0
1906
3413
  }) {
1907
3414
  const theme = usePoolseTheme();
1908
3415
  const { me } = react$1.useMe();
@@ -1914,7 +3421,8 @@ function ConversationView({
1914
3421
  loadMore,
1915
3422
  send,
1916
3423
  edit,
1917
- delete: deleteMsg
3424
+ delete: deleteMsg,
3425
+ markReadUpTo
1918
3426
  } = react$1.useMessages(conversationId);
1919
3427
  const { typing, signalTyping } = react$1.useTyping(conversationId);
1920
3428
  const wantsMembers = mentions || senderLabels === "auto" || avatars === "auto";
@@ -1926,10 +3434,31 @@ function ConversationView({
1926
3434
  const [editingId, setEditingId] = react.useState(null);
1927
3435
  const [threadRoot, setThreadRoot] = react.useState(null);
1928
3436
  const [pickerOpen, setPickerOpen] = react.useState(false);
3437
+ const sequenceByMessageId = react.useMemo(() => {
3438
+ const m = /* @__PURE__ */ new Map();
3439
+ for (const msg of messages) {
3440
+ if (typeof msg.sequence === "number") m.set(msg.id, msg.sequence);
3441
+ }
3442
+ return m;
3443
+ }, [messages]);
3444
+ const lastMarkedRef = react.useRef(null);
3445
+ react.useEffect(() => {
3446
+ if (!readReceipts) return;
3447
+ if (messages.length === 0) return;
3448
+ const latest = messages[messages.length - 1];
3449
+ if (!latest || !latest.id) return;
3450
+ if (latest.id === lastMarkedRef.current) return;
3451
+ if (typeof latest.sequence !== "number" || latest.sequence <= 0) return;
3452
+ lastMarkedRef.current = latest.id;
3453
+ void markReadUpTo(latest.id).catch(() => {
3454
+ lastMarkedRef.current = null;
3455
+ });
3456
+ onMarkedRead?.(conversationId, latest.id);
3457
+ }, [messages, meId, readReceipts, markReadUpTo, onMarkedRead, conversationId]);
1929
3458
  const renderItem = react.useCallback(
1930
3459
  (msg) => {
1931
3460
  if (renderMessage) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderMessage(msg, meId) });
1932
- const readState = readReceipts && meId !== null && msg.sender_id === meId ? readStateForMessage(msg, members, meId) : void 0;
3461
+ const readState = readReceipts && meId !== null && msg.sender_id === meId ? readStateForMessage(msg, members, meId, sequenceByMessageId) : void 0;
1933
3462
  return /* @__PURE__ */ jsxRuntime.jsx(
1934
3463
  MessageRow,
1935
3464
  {
@@ -1976,7 +3505,8 @@ function ConversationView({
1976
3505
  avatarFor,
1977
3506
  showSenderLabels,
1978
3507
  showAvatars,
1979
- maxBodyLength
3508
+ maxBodyLength,
3509
+ sequenceByMessageId
1980
3510
  ]
1981
3511
  );
1982
3512
  const composerProps = react.useMemo(
@@ -1984,52 +3514,78 @@ function ConversationView({
1984
3514
  onSend: async (body, opts) => {
1985
3515
  await send({ body, ...opts });
1986
3516
  setReplyingTo(null);
3517
+ onSent?.(conversationId);
1987
3518
  },
1988
3519
  onTyping: signalTyping,
1989
3520
  ...attachments ? { onAttachPress: () => setPickerOpen(true) } : {},
1990
3521
  ...replyingTo ? { replyingTo } : {},
1991
3522
  onCancelReply: () => setReplyingTo(null),
1992
3523
  ...labelFor ? { labelFor } : {},
1993
- attachments
3524
+ attachments,
3525
+ keyboardOffset
1994
3526
  }),
1995
- [send, signalTyping, attachments, replyingTo, labelFor]
3527
+ [send, signalTyping, attachments, replyingTo, labelFor, keyboardOffset, onSent, conversationId]
1996
3528
  );
1997
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles14.root, { backgroundColor: theme.colors.paper }], children: [
1998
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles14.listWrap, children: /* @__PURE__ */ jsxRuntime.jsx(
1999
- MessageList,
2000
- {
2001
- messages,
2002
- loading,
2003
- hasMore,
2004
- onLoadMore: loadMore,
2005
- renderItem,
2006
- ListHeader: /* @__PURE__ */ jsxRuntime.jsx(TypingIndicator, { typing, ...labelFor ? { labelFor } : {} }),
2007
- ...emptyState !== void 0 ? { emptyState } : {}
2008
- }
2009
- ) }),
2010
- mentions ? /* @__PURE__ */ jsxRuntime.jsx(MentionInput, { conversationId, ...composerProps }) : /* @__PURE__ */ jsxRuntime.jsx(MessageComposer, { ...composerProps }),
2011
- attachments ? /* @__PURE__ */ jsxRuntime.jsx(AttachmentPicker, { visible: pickerOpen, onClose: () => setPickerOpen(false) }) : null,
2012
- threads && threadRoot ? /* @__PURE__ */ jsxRuntime.jsx(
2013
- ThreadView,
2014
- {
2015
- visible: true,
2016
- onClose: () => setThreadRoot(null),
2017
- conversationId,
2018
- rootMessage: threadRoot,
2019
- ...labelFor ? { labelFor } : {}
2020
- }
2021
- ) : null
2022
- ] });
3529
+ const headerNode = header === false ? null : header === true ? /* @__PURE__ */ jsxRuntime.jsx(
3530
+ ChatHeader,
3531
+ {
3532
+ conversationId,
3533
+ ...labelFor ? { labelFor } : {},
3534
+ ...avatarFor ? { avatarFor } : {},
3535
+ ...onHeaderMembersPress ? { onMembersPress: onHeaderMembersPress } : {},
3536
+ ...onHeaderPress ? { onPress: onHeaderPress } : {}
3537
+ }
3538
+ ) : header;
3539
+ return /* @__PURE__ */ jsxRuntime.jsx(UploadProvider, { children: /* @__PURE__ */ jsxRuntime.jsxs(
3540
+ reactNative.KeyboardAvoidingView,
3541
+ {
3542
+ style: [styles20.root, { backgroundColor: theme.colors.paper }],
3543
+ behavior: reactNative.Platform.OS === "ios" ? "padding" : "height",
3544
+ keyboardVerticalOffset: keyboardOffset,
3545
+ children: [
3546
+ headerNode,
3547
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles20.listWrap, children: /* @__PURE__ */ jsxRuntime.jsx(
3548
+ MessageList,
3549
+ {
3550
+ messages,
3551
+ loading,
3552
+ hasMore,
3553
+ onLoadMore: loadMore,
3554
+ renderItem,
3555
+ ListHeader: /* @__PURE__ */ jsxRuntime.jsx(TypingIndicator, { typing, ...labelFor ? { labelFor } : {} }),
3556
+ ...emptyState !== void 0 ? { emptyState } : {}
3557
+ }
3558
+ ) }),
3559
+ mentions ? /* @__PURE__ */ jsxRuntime.jsx(MentionInput, { conversationId, ...composerProps }) : /* @__PURE__ */ jsxRuntime.jsx(MessageComposer, { ...composerProps }),
3560
+ attachments ? /* @__PURE__ */ jsxRuntime.jsx(AttachmentPicker, { visible: pickerOpen, onClose: () => setPickerOpen(false) }) : null,
3561
+ threads && threadRoot ? /* @__PURE__ */ jsxRuntime.jsx(
3562
+ ThreadView,
3563
+ {
3564
+ visible: true,
3565
+ onClose: () => setThreadRoot(null),
3566
+ conversationId,
3567
+ rootMessage: threadRoot,
3568
+ ...labelFor ? { labelFor } : {}
3569
+ }
3570
+ ) : null
3571
+ ]
3572
+ }
3573
+ ) });
2023
3574
  }
2024
- function readStateForMessage(msg, members, meId) {
3575
+ function readStateForMessage(msg, members, meId, sequenceByMessageId) {
2025
3576
  const otherMembers = members.filter((m) => m.user_id !== meId);
2026
3577
  if (otherMembers.length === 0) return "sent";
2027
- const anyRead = otherMembers.some(
2028
- (m) => m.last_read_message_id !== null && msg.id !== null && m.last_read_message_id >= msg.id
2029
- );
3578
+ if (typeof msg.sequence !== "number") return "sent";
3579
+ const mySeq = msg.sequence;
3580
+ const anyRead = otherMembers.some((m) => {
3581
+ if (!m.last_read_message_id) return false;
3582
+ const readSeq = sequenceByMessageId.get(m.last_read_message_id);
3583
+ if (readSeq === void 0) return false;
3584
+ return readSeq >= mySeq;
3585
+ });
2030
3586
  return anyRead ? "read" : "sent";
2031
3587
  }
2032
- var styles14 = reactNative.StyleSheet.create({
3588
+ var styles20 = reactNative.StyleSheet.create({
2033
3589
  root: {
2034
3590
  flex: 1
2035
3591
  },
@@ -2037,351 +3593,776 @@ var styles14 = reactNative.StyleSheet.create({
2037
3593
  flex: 1
2038
3594
  }
2039
3595
  });
2040
- function ConversationList({
2041
- selectedId,
2042
- onSelect,
2043
- renderItem,
2044
- emptyState,
2045
- conversations: controlled,
2046
- loading: controlledLoading,
2047
- error: controlledError,
2048
- unreadCounts
3596
+ function UserPickerSheet({
3597
+ visible,
3598
+ onClose,
3599
+ users,
3600
+ excludeIds,
3601
+ mode,
3602
+ onPickDirect,
3603
+ onCreateGroup,
3604
+ title,
3605
+ searchable
2049
3606
  }) {
2050
3607
  const theme = usePoolseTheme();
2051
- const auto = react$1.useConversations();
2052
- const isControlled = controlled !== void 0;
2053
- const conversations = isControlled ? controlled : auto.conversations;
2054
- const loading = isControlled ? controlledLoading ?? false : auto.loading;
2055
- const error = isControlled ? controlledError ?? null : auto.error;
2056
- const counts = unreadCounts ?? (isControlled ? void 0 : auto.unreadCounts);
2057
- if (loading && conversations.length === 0) {
2058
- return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles15.placeholder, { backgroundColor: theme.colors.paper }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: theme.colors.brand }) });
2059
- }
2060
- if (error) {
2061
- return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles15.placeholder, { backgroundColor: theme.colors.paper }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: { color: theme.colors.error, fontFamily: theme.type.fontBody }, children: "Failed to load conversations." }) });
2062
- }
2063
- if (conversations.length === 0) {
2064
- return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles15.placeholder, { backgroundColor: theme.colors.paper }], children: typeof emptyState === "string" || emptyState == null ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: { color: theme.colors.ink3, fontFamily: theme.type.fontBody }, children: emptyState ?? "No conversations yet." }) : emptyState });
2065
- }
3608
+ const [query, setQuery] = react.useState("");
3609
+ const [selectedIds, setSelectedIds] = react.useState([]);
3610
+ const [groupName, setGroupName] = react.useState("");
3611
+ const { height: screenHeight } = reactNative.useWindowDimensions();
3612
+ const { translateY, panHandlers } = useDismissableSheet({
3613
+ onClose: () => {
3614
+ setQuery("");
3615
+ setSelectedIds([]);
3616
+ setGroupName("");
3617
+ onClose();
3618
+ },
3619
+ sheetHeight: screenHeight * 0.85
3620
+ });
3621
+ const excludeSet = react.useMemo(() => new Set(excludeIds ?? []), [excludeIds]);
3622
+ const filtered = react.useMemo(() => {
3623
+ const q = query.trim().toLowerCase();
3624
+ return users.filter((u) => {
3625
+ if (excludeSet.has(u.externalId)) return false;
3626
+ if (!q) return true;
3627
+ return u.name.toLowerCase().includes(q) || (u.subtitle ?? "").toLowerCase().includes(q) || u.externalId.toLowerCase().includes(q);
3628
+ });
3629
+ }, [users, query, excludeSet]);
3630
+ const showSearch = searchable ?? users.length >= 8;
3631
+ const isGroup = mode === "group";
3632
+ const heading = title ?? (isGroup ? "New group" : "New chat");
3633
+ const reset = () => {
3634
+ setQuery("");
3635
+ setSelectedIds([]);
3636
+ setGroupName("");
3637
+ };
3638
+ const handleClose = () => {
3639
+ reset();
3640
+ onClose();
3641
+ };
3642
+ const toggle = (externalId) => {
3643
+ setSelectedIds(
3644
+ (prev) => prev.includes(externalId) ? prev.filter((id) => id !== externalId) : [...prev, externalId]
3645
+ );
3646
+ };
3647
+ const canCreate = isGroup && selectedIds.length > 0 && groupName.trim().length > 0;
3648
+ const handleCreate = () => {
3649
+ if (!canCreate) return;
3650
+ onCreateGroup?.(groupName.trim(), selectedIds);
3651
+ reset();
3652
+ onClose();
3653
+ };
2066
3654
  return /* @__PURE__ */ jsxRuntime.jsx(
2067
- reactNative.FlatList,
3655
+ reactNative.Modal,
2068
3656
  {
2069
- data: conversations,
2070
- keyExtractor: (c) => c.id,
2071
- style: { backgroundColor: theme.colors.paper },
2072
- renderItem: ({ item }) => {
2073
- const selected = selectedId === item.id;
2074
- if (renderItem) {
2075
- return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderItem(item, selected) });
2076
- }
2077
- const unread = counts?.[item.id] ?? 0;
2078
- return /* @__PURE__ */ jsxRuntime.jsxs(
2079
- reactNative.Pressable,
3657
+ visible,
3658
+ transparent: true,
3659
+ animationType: "slide",
3660
+ onRequestClose: handleClose,
3661
+ statusBarTranslucent: true,
3662
+ children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles21.modalRoot, children: [
3663
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { style: styles21.backdrop, onPress: handleClose }),
3664
+ /* @__PURE__ */ jsxRuntime.jsxs(
3665
+ reactNative.Animated.View,
2080
3666
  {
2081
- onPress: () => onSelect?.(item),
2082
3667
  style: [
2083
- styles15.row,
3668
+ styles21.sheet,
2084
3669
  {
2085
- backgroundColor: selected ? theme.colors.surface2 : "transparent"
2086
- }
3670
+ backgroundColor: theme.colors.paper,
3671
+ borderTopLeftRadius: theme.radii.xl,
3672
+ borderTopRightRadius: theme.radii.xl,
3673
+ transform: [{ translateY }]
3674
+ },
3675
+ theme.shadows.lg
2087
3676
  ],
2088
3677
  children: [
2089
- /* @__PURE__ */ jsxRuntime.jsx(Avatar, { src: null, name: item.name ?? null, seed: item.id, size: "md" }),
2090
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles15.body, children: [
3678
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ...panHandlers, style: styles21.dragHandle, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles21.dragPill, { backgroundColor: theme.colors.ink3 }] }) }),
3679
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles21.header, children: [
3680
+ /* @__PURE__ */ jsxRuntime.jsx(
3681
+ reactNative.Pressable,
3682
+ {
3683
+ onPress: handleClose,
3684
+ hitSlop: 16,
3685
+ style: [styles21.closeBtn, { backgroundColor: theme.colors.surface2 }],
3686
+ accessibilityLabel: "Close picker",
3687
+ children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "close", size: 18, color: theme.colors.ink })
3688
+ }
3689
+ ),
2091
3690
  /* @__PURE__ */ jsxRuntime.jsx(
2092
3691
  reactNative.Text,
2093
3692
  {
2094
- style: [styles15.name, { color: theme.colors.ink, fontFamily: theme.type.fontBody }],
3693
+ style: [
3694
+ styles21.title,
3695
+ { color: theme.colors.ink, fontFamily: theme.type.fontDisplay }
3696
+ ],
2095
3697
  numberOfLines: 1,
2096
- children: item.name ?? "Untitled conversation"
3698
+ children: heading
2097
3699
  }
2098
3700
  ),
2099
- item.last_message_at ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles15.preview, { color: theme.colors.ink3 }], numberOfLines: 1, children: formatRelative(item.last_message_at) }) : null
3701
+ isGroup ? /* @__PURE__ */ jsxRuntime.jsx(
3702
+ reactNative.Pressable,
3703
+ {
3704
+ onPress: handleCreate,
3705
+ disabled: !canCreate,
3706
+ hitSlop: 6,
3707
+ style: [
3708
+ styles21.createBtn,
3709
+ {
3710
+ backgroundColor: canCreate ? theme.colors.brand : theme.colors.surface2,
3711
+ opacity: canCreate ? 1 : 0.6
3712
+ }
3713
+ ],
3714
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
3715
+ reactNative.Text,
3716
+ {
3717
+ style: {
3718
+ color: canCreate ? theme.colors.onBrand : theme.colors.ink3,
3719
+ fontWeight: "700",
3720
+ fontSize: 13
3721
+ },
3722
+ children: [
3723
+ "Create",
3724
+ selectedIds.length > 0 ? ` (${selectedIds.length})` : ""
3725
+ ]
3726
+ }
3727
+ )
3728
+ }
3729
+ ) : null
2100
3730
  ] }),
2101
- unread > 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles15.unreadPill, { backgroundColor: theme.colors.unreadPill }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles15.unreadText, { color: theme.colors.unreadPillText }], children: unread > 99 ? "99+" : String(unread) }) }) : null
3731
+ isGroup ? /* @__PURE__ */ jsxRuntime.jsx(
3732
+ reactNative.TextInput,
3733
+ {
3734
+ placeholder: "Group name",
3735
+ placeholderTextColor: theme.colors.ink3,
3736
+ value: groupName,
3737
+ onChangeText: setGroupName,
3738
+ style: [
3739
+ styles21.groupNameInput,
3740
+ {
3741
+ color: theme.colors.ink,
3742
+ backgroundColor: theme.colors.surface2,
3743
+ borderRadius: theme.radii.md,
3744
+ fontFamily: theme.type.fontBody,
3745
+ fontSize: theme.type.bodySize
3746
+ }
3747
+ ]
3748
+ }
3749
+ ) : null,
3750
+ showSearch ? /* @__PURE__ */ jsxRuntime.jsx(
3751
+ reactNative.TextInput,
3752
+ {
3753
+ placeholder: "Search\u2026",
3754
+ placeholderTextColor: theme.colors.ink3,
3755
+ value: query,
3756
+ onChangeText: setQuery,
3757
+ style: [
3758
+ styles21.searchInput,
3759
+ {
3760
+ color: theme.colors.ink,
3761
+ backgroundColor: theme.colors.surface2,
3762
+ borderRadius: theme.radii.md,
3763
+ fontFamily: theme.type.fontBody,
3764
+ fontSize: 14
3765
+ }
3766
+ ]
3767
+ }
3768
+ ) : null,
3769
+ /* @__PURE__ */ jsxRuntime.jsx(
3770
+ reactNative.FlatList,
3771
+ {
3772
+ data: filtered,
3773
+ keyExtractor: (u) => u.externalId,
3774
+ ListEmptyComponent: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles21.empty, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles21.emptyText, { color: theme.colors.ink3 }], children: query ? "No matches." : "No users to show." }) }),
3775
+ renderItem: ({ item }) => /* @__PURE__ */ jsxRuntime.jsx(
3776
+ UserRow,
3777
+ {
3778
+ user: item,
3779
+ selected: selectedIds.includes(item.externalId),
3780
+ isGroup,
3781
+ onPress: () => {
3782
+ if (isGroup) toggle(item.externalId);
3783
+ else {
3784
+ onPickDirect?.(item.externalId);
3785
+ reset();
3786
+ onClose();
3787
+ }
3788
+ }
3789
+ }
3790
+ )
3791
+ }
3792
+ )
2102
3793
  ]
2103
3794
  }
2104
- );
2105
- }
3795
+ )
3796
+ ] })
2106
3797
  }
2107
3798
  );
2108
3799
  }
2109
- function formatRelative(iso) {
2110
- try {
2111
- const d = new Date(iso);
2112
- const now = Date.now();
2113
- const diff = now - d.getTime();
2114
- if (diff < 6e4) return "just now";
2115
- if (diff < 36e5) return `${Math.floor(diff / 6e4)}m`;
2116
- if (diff < 864e5) return `${Math.floor(diff / 36e5)}h`;
2117
- return `${Math.floor(diff / 864e5)}d`;
2118
- } catch {
2119
- return "";
2120
- }
2121
- }
2122
- var styles15 = reactNative.StyleSheet.create({
2123
- placeholder: {
2124
- flex: 1,
2125
- alignItems: "center",
2126
- justifyContent: "center",
2127
- padding: 24
2128
- },
2129
- row: {
2130
- flexDirection: "row",
2131
- alignItems: "center",
2132
- paddingHorizontal: 12,
2133
- paddingVertical: 10,
2134
- gap: 10
2135
- },
2136
- body: {
2137
- flex: 1,
2138
- gap: 2
2139
- },
2140
- name: {
2141
- fontSize: 15,
2142
- fontWeight: "600"
2143
- },
2144
- preview: {
2145
- fontSize: 12
2146
- },
2147
- unreadPill: {
2148
- minWidth: 22,
2149
- height: 22,
2150
- borderRadius: 11,
2151
- paddingHorizontal: 6,
2152
- alignItems: "center",
2153
- justifyContent: "center"
2154
- },
2155
- unreadText: {
2156
- fontSize: 11,
2157
- fontWeight: "700"
2158
- }
2159
- });
2160
- function MemberList({
2161
- conversationId,
2162
- labelFor,
2163
- avatarFor,
2164
- onlineExternalIds,
2165
- canRemove,
2166
- renderItem,
2167
- emptyState
3800
+ function UserRow({
3801
+ user,
3802
+ selected,
3803
+ isGroup,
3804
+ onPress
2168
3805
  }) {
2169
3806
  const theme = usePoolseTheme();
2170
- const { members, loading, error, removeMember } = react$1.useMembers(conversationId);
2171
- const { online: presenceOnline } = react$1.usePresence(
2172
- onlineExternalIds === void 0 ? conversationId : ""
2173
- );
2174
- const effectiveOnline = onlineExternalIds ?? presenceOnline;
2175
- if (loading && members.length === 0) {
2176
- return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles16.placeholder, { backgroundColor: theme.colors.paper }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: theme.colors.brand }) });
2177
- }
2178
- if (error) {
2179
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles16.placeholder, { backgroundColor: theme.colors.paper }], children: [
2180
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: { color: theme.colors.error, fontFamily: theme.type.fontBody }, children: "Failed to load members." }),
2181
- error.message ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles16.errorDetail, { color: theme.colors.ink3 }], children: error.message }) : null
2182
- ] });
2183
- }
2184
- if (members.length === 0) {
2185
- return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles16.placeholder, { backgroundColor: theme.colors.paper }], children: typeof emptyState === "string" || emptyState == null ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: { color: theme.colors.ink3, fontFamily: theme.type.fontBody }, children: emptyState ?? "No members." }) : emptyState });
2186
- }
2187
- return /* @__PURE__ */ jsxRuntime.jsx(
2188
- reactNative.FlatList,
3807
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3808
+ reactNative.Pressable,
2189
3809
  {
2190
- data: members,
2191
- keyExtractor: (m) => m.user_id,
2192
- style: { backgroundColor: theme.colors.paper },
2193
- renderItem: ({ item }) => {
2194
- if (renderItem) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderItem(item) });
2195
- const name = labelFor?.(item.external_id) ?? item.external_id;
2196
- const avatarUrl = avatarFor?.(item.external_id) ?? null;
2197
- const isOnline = effectiveOnline.has(item.external_id);
2198
- const showRemove = canRemove?.(item) ?? false;
2199
- return /* @__PURE__ */ jsxRuntime.jsx(
2200
- MemberRow,
3810
+ onPress,
3811
+ style: [
3812
+ styles21.row,
3813
+ {
3814
+ backgroundColor: selected ? theme.colors.brandSoft : "transparent"
3815
+ }
3816
+ ],
3817
+ children: [
3818
+ user.avatarUrl ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Image, { source: { uri: user.avatarUrl }, style: styles21.avatarImg }) : /* @__PURE__ */ jsxRuntime.jsx(Avatar, { src: null, name: user.name, seed: user.externalId, size: "md" }),
3819
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles21.body, children: [
3820
+ /* @__PURE__ */ jsxRuntime.jsx(
3821
+ reactNative.Text,
3822
+ {
3823
+ style: [styles21.name, { color: theme.colors.ink, fontFamily: theme.type.fontBody }],
3824
+ numberOfLines: 1,
3825
+ children: user.name
3826
+ }
3827
+ ),
3828
+ user.subtitle ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles21.subtitle, { color: theme.colors.ink3 }], numberOfLines: 1, children: user.subtitle }) : null
3829
+ ] }),
3830
+ isGroup ? /* @__PURE__ */ jsxRuntime.jsx(
3831
+ reactNative.View,
2201
3832
  {
2202
- name,
2203
- externalId: item.external_id,
2204
- avatarUrl,
2205
- role: item.role,
2206
- online: isOnline,
2207
- ...showRemove ? { onRemove: () => removeMember(item.external_id) } : {}
3833
+ style: [
3834
+ styles21.checkbox,
3835
+ {
3836
+ borderColor: selected ? theme.colors.brand : theme.colors.border,
3837
+ backgroundColor: selected ? theme.colors.brand : "transparent"
3838
+ }
3839
+ ],
3840
+ children: selected ? /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "check", size: 14, color: theme.colors.onBrand }) : null
2208
3841
  }
2209
- );
2210
- }
3842
+ ) : null
3843
+ ]
2211
3844
  }
2212
3845
  );
2213
3846
  }
2214
- function MemberRow({
2215
- name,
2216
- externalId,
2217
- avatarUrl,
2218
- role,
2219
- online,
2220
- onRemove
2221
- }) {
2222
- const theme = usePoolseTheme();
2223
- const displayName = useDisplayName(externalId) || name;
2224
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles16.row, children: [
2225
- /* @__PURE__ */ jsxRuntime.jsx(Avatar, { src: avatarUrl, name: displayName, seed: externalId, online, size: "md" }),
2226
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles16.body, children: [
2227
- /* @__PURE__ */ jsxRuntime.jsx(
2228
- reactNative.Text,
2229
- {
2230
- style: [styles16.name, { color: theme.colors.ink, fontFamily: theme.type.fontBody }],
2231
- numberOfLines: 1,
2232
- children: displayName
2233
- }
2234
- ),
2235
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles16.role, { color: theme.colors.ink3 }], children: role })
2236
- ] }),
2237
- onRemove ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: onRemove, hitSlop: 8, style: styles16.removeBtn, children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "close", size: 16, color: theme.colors.ink2 }) }) : null
2238
- ] });
2239
- }
2240
- var styles16 = reactNative.StyleSheet.create({
2241
- placeholder: {
3847
+ var styles21 = reactNative.StyleSheet.create({
3848
+ modalRoot: {
2242
3849
  flex: 1,
3850
+ justifyContent: "flex-end"
3851
+ },
3852
+ backdrop: {
3853
+ ...reactNative.StyleSheet.absoluteFillObject,
3854
+ backgroundColor: "rgba(0,0,0,0.4)"
3855
+ },
3856
+ sheet: {
3857
+ height: "85%",
3858
+ overflow: "hidden"
3859
+ },
3860
+ dragHandle: {
2243
3861
  alignItems: "center",
2244
- justifyContent: "center",
2245
- padding: 24
3862
+ paddingTop: 8,
3863
+ paddingBottom: 4
2246
3864
  },
2247
- errorDetail: {
2248
- marginTop: 6,
2249
- fontSize: 11
3865
+ dragPill: {
3866
+ width: 36,
3867
+ height: 4,
3868
+ borderRadius: 2,
3869
+ opacity: 0.4
2250
3870
  },
2251
- row: {
3871
+ header: {
2252
3872
  flexDirection: "row",
2253
3873
  alignItems: "center",
2254
3874
  paddingHorizontal: 12,
2255
- paddingVertical: 8,
3875
+ paddingTop: 4,
3876
+ paddingBottom: 12,
2256
3877
  gap: 10
2257
3878
  },
3879
+ closeBtn: {
3880
+ width: 32,
3881
+ height: 32,
3882
+ borderRadius: 16,
3883
+ alignItems: "center",
3884
+ justifyContent: "center"
3885
+ },
3886
+ title: {
3887
+ fontSize: 17,
3888
+ fontWeight: "700",
3889
+ flex: 1
3890
+ },
3891
+ createBtn: {
3892
+ paddingHorizontal: 14,
3893
+ paddingVertical: 7,
3894
+ borderRadius: 999
3895
+ },
3896
+ groupNameInput: {
3897
+ marginHorizontal: 16,
3898
+ marginBottom: 8,
3899
+ paddingHorizontal: 12,
3900
+ paddingVertical: 10,
3901
+ fontWeight: "600"
3902
+ },
3903
+ searchInput: {
3904
+ marginHorizontal: 16,
3905
+ marginBottom: 8,
3906
+ paddingHorizontal: 12,
3907
+ paddingVertical: 8
3908
+ },
3909
+ row: {
3910
+ flexDirection: "row",
3911
+ alignItems: "center",
3912
+ paddingHorizontal: 16,
3913
+ paddingVertical: 10,
3914
+ gap: 12
3915
+ },
3916
+ avatarImg: {
3917
+ width: 40,
3918
+ height: 40,
3919
+ borderRadius: 20
3920
+ },
2258
3921
  body: {
2259
3922
  flex: 1,
2260
- gap: 1
3923
+ gap: 2
2261
3924
  },
2262
3925
  name: {
2263
- fontSize: 14,
2264
- fontWeight: "500"
3926
+ fontSize: 15,
3927
+ fontWeight: "600"
2265
3928
  },
2266
- role: {
2267
- fontSize: 11,
2268
- textTransform: "capitalize"
3929
+ subtitle: {
3930
+ fontSize: 12
2269
3931
  },
2270
- removeBtn: {
2271
- padding: 6
3932
+ checkbox: {
3933
+ width: 22,
3934
+ height: 22,
3935
+ borderRadius: 11,
3936
+ borderWidth: 1.5,
3937
+ alignItems: "center",
3938
+ justifyContent: "center"
3939
+ },
3940
+ empty: {
3941
+ paddingVertical: 40,
3942
+ alignItems: "center"
3943
+ },
3944
+ emptyText: {
3945
+ fontSize: 13
2272
3946
  }
2273
3947
  });
2274
- var DEFAULT_EMOJIS = ["\u{1F44D}", "\u2764\uFE0F", "\u{1F602}", "\u{1F389}", "\u{1F64F}", "\u{1F525}"];
2275
- function ReactionStrip({
2276
- messageId,
2277
- conversationId,
2278
- meId,
2279
- initialReactions
2280
- }) {
2281
- const theme = usePoolseTheme();
2282
- const { reactions, addReaction, removeReaction } = react$1.useReactions(messageId, {
2283
- conversationId,
2284
- currentUserId: meId,
2285
- ...initialReactions ? { initialReactions } : {}
2286
- });
2287
- const entries = Object.entries(reactions);
2288
- if (entries.length === 0) return null;
2289
- return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles17.strip, children: entries.map(([emoji, userIds]) => {
2290
- const userArr = Array.isArray(userIds) ? userIds : [];
2291
- const meReacted = meId !== null && userArr.includes(meId);
2292
- return /* @__PURE__ */ jsxRuntime.jsxs(
2293
- reactNative.Pressable,
2294
- {
2295
- onPress: () => meReacted ? removeReaction(emoji) : addReaction(emoji),
2296
- style: [
2297
- styles17.chip,
3948
+ var CHAT_HEADER_HEIGHT = 56;
3949
+ var PoolseInbox = react.forwardRef(
3950
+ function PoolseInbox2(props, ref) {
3951
+ const {
3952
+ title = "Chats",
3953
+ users,
3954
+ labelFor,
3955
+ avatarFor,
3956
+ renderRightAction,
3957
+ renderConversationRow,
3958
+ emptyState,
3959
+ keyboardOffset = 0
3960
+ } = props;
3961
+ const theme = usePoolseTheme();
3962
+ const { width: screenWidth } = reactNative.useWindowDimensions();
3963
+ const { me } = react$1.useMe();
3964
+ const {
3965
+ conversations,
3966
+ create,
3967
+ unreadCounts,
3968
+ markConversationRead,
3969
+ refetch: refetchConversations
3970
+ } = react$1.useConversations();
3971
+ const meExternalId = me?.external_id ?? null;
3972
+ const detailKeyboardOffset = CHAT_HEADER_HEIGHT;
3973
+ const [activeId, setActiveId] = react.useState(null);
3974
+ const [userPickerOpen, setUserPickerOpen] = react.useState(false);
3975
+ const [groupPickerOpen, setGroupPickerOpen] = react.useState(false);
3976
+ const slide = react.useRef(new reactNative.Animated.Value(0)).current;
3977
+ const draggingRef = react.useRef(false);
3978
+ react.useEffect(() => {
3979
+ if (draggingRef.current) return;
3980
+ reactNative.Animated.timing(slide, {
3981
+ toValue: activeId ? 1 : 0,
3982
+ duration: 280,
3983
+ easing: reactNative.Easing.out(reactNative.Easing.cubic),
3984
+ useNativeDriver: true
3985
+ }).start();
3986
+ }, [activeId, slide]);
3987
+ const listTransform = slide.interpolate({
3988
+ inputRange: [0, 1],
3989
+ outputRange: [0, -Math.round(screenWidth * 0.25)]
3990
+ });
3991
+ const detailTransform = slide.interpolate({
3992
+ inputRange: [0, 1],
3993
+ outputRange: [screenWidth, 0]
3994
+ });
3995
+ const EDGE_WIDTH = 24;
3996
+ const DISMISS_VX_THRESHOLD = 0.4;
3997
+ const activeIdRef = react.useRef(activeId);
3998
+ react.useEffect(() => {
3999
+ activeIdRef.current = activeId;
4000
+ }, [activeId]);
4001
+ const panResponder = react.useMemo(() => {
4002
+ const dismissDxThreshold = screenWidth * 0.35;
4003
+ return reactNative.PanResponder.create({
4004
+ // Never claim the touch on start — that would swallow taps on
4005
+ // the back button in the ChatHeader strip.
4006
+ onStartShouldSetPanResponder: () => false,
4007
+ onStartShouldSetPanResponderCapture: () => false,
4008
+ onMoveShouldSetPanResponder: (_e, gs) => {
4009
+ if (!activeIdRef.current) return false;
4010
+ if (gs.x0 > EDGE_WIDTH) return false;
4011
+ return gs.dx > 6 && Math.abs(gs.dx) > Math.abs(gs.dy);
4012
+ },
4013
+ onMoveShouldSetPanResponderCapture: (_e, gs) => {
4014
+ if (!activeIdRef.current) return false;
4015
+ if (gs.x0 > EDGE_WIDTH) return false;
4016
+ return gs.dx > 6 && Math.abs(gs.dx) > Math.abs(gs.dy);
4017
+ },
4018
+ onPanResponderGrant: () => {
4019
+ draggingRef.current = true;
4020
+ slide.stopAnimation();
4021
+ },
4022
+ onPanResponderMove: (_e, gs) => {
4023
+ const dx = Math.max(0, gs.dx);
4024
+ const progress = Math.min(1, dx / screenWidth);
4025
+ slide.setValue(1 - progress);
4026
+ },
4027
+ onPanResponderRelease: (_e, gs) => {
4028
+ draggingRef.current = false;
4029
+ const past = gs.dx > dismissDxThreshold || gs.vx > DISMISS_VX_THRESHOLD;
4030
+ if (past) {
4031
+ setActiveId(null);
4032
+ } else {
4033
+ reactNative.Animated.timing(slide, {
4034
+ toValue: 1,
4035
+ duration: 220,
4036
+ easing: reactNative.Easing.out(reactNative.Easing.cubic),
4037
+ useNativeDriver: true
4038
+ }).start();
4039
+ }
4040
+ },
4041
+ onPanResponderTerminate: () => {
4042
+ draggingRef.current = false;
4043
+ reactNative.Animated.timing(slide, {
4044
+ toValue: 1,
4045
+ duration: 220,
4046
+ easing: reactNative.Easing.out(reactNative.Easing.cubic),
4047
+ useNativeDriver: true
4048
+ }).start();
4049
+ },
4050
+ onPanResponderTerminationRequest: () => false
4051
+ });
4052
+ }, [slide, screenWidth]);
4053
+ const openDirect = react.useCallback(
4054
+ async (externalId) => {
4055
+ const conv = await create({
4056
+ type: "direct",
4057
+ member_external_ids: [externalId]
4058
+ });
4059
+ setActiveId(conv.id);
4060
+ },
4061
+ [create]
4062
+ );
4063
+ const openGroup = react.useCallback(
4064
+ async (name, memberExternalIds) => {
4065
+ const conv = await create({
4066
+ type: "group",
4067
+ name,
4068
+ member_external_ids: memberExternalIds
4069
+ });
4070
+ setActiveId(conv.id);
4071
+ },
4072
+ [create]
4073
+ );
4074
+ react.useImperativeHandle(
4075
+ ref,
4076
+ () => ({
4077
+ open: setActiveId,
4078
+ openDirect,
4079
+ openGroup,
4080
+ promptNewChat: () => setUserPickerOpen(true),
4081
+ promptNewGroup: () => setGroupPickerOpen(true),
4082
+ close: () => setActiveId(null)
4083
+ }),
4084
+ [openDirect, openGroup]
4085
+ );
4086
+ const renderRow = react.useCallback(
4087
+ (conv, selected) => {
4088
+ if (renderConversationRow) {
4089
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderConversationRow(conv, meExternalId, selected) });
4090
+ }
4091
+ const unread = unreadCounts?.[conv.id] ?? conv.unread_count ?? 0;
4092
+ return /* @__PURE__ */ jsxRuntime.jsx(
4093
+ DefaultConversationRow,
2298
4094
  {
2299
- backgroundColor: meReacted ? theme.colors.brandSoft : theme.colors.surface2,
2300
- borderColor: meReacted ? theme.colors.brand : theme.colors.border
4095
+ conv,
4096
+ selected,
4097
+ meExternalId,
4098
+ unread,
4099
+ ...labelFor ? { labelFor } : {},
4100
+ ...avatarFor ? { avatarFor } : {},
4101
+ onPress: () => setActiveId(conv.id)
2301
4102
  }
2302
- ],
2303
- children: [
2304
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles17.emoji, children: emoji }),
2305
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles17.count, { color: theme.colors.ink2 }], children: userArr.length })
2306
- ]
4103
+ );
2307
4104
  },
2308
- emoji
4105
+ [renderConversationRow, meExternalId, labelFor, avatarFor, unreadCounts]
2309
4106
  );
2310
- }) });
4107
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles22.root, { backgroundColor: theme.colors.paper }], children: [
4108
+ /* @__PURE__ */ jsxRuntime.jsxs(
4109
+ reactNative.Animated.View,
4110
+ {
4111
+ style: [reactNative.StyleSheet.absoluteFillObject, { transform: [{ translateX: listTransform }] }],
4112
+ pointerEvents: activeId ? "none" : "auto",
4113
+ children: [
4114
+ /* @__PURE__ */ jsxRuntime.jsxs(
4115
+ reactNative.View,
4116
+ {
4117
+ style: [
4118
+ styles22.listHeader,
4119
+ {
4120
+ backgroundColor: theme.colors.surface,
4121
+ borderBottomColor: theme.colors.border
4122
+ }
4123
+ ],
4124
+ children: [
4125
+ /* @__PURE__ */ jsxRuntime.jsx(
4126
+ reactNative.Text,
4127
+ {
4128
+ style: [
4129
+ styles22.listTitle,
4130
+ { color: theme.colors.ink, fontFamily: theme.type.fontDisplay }
4131
+ ],
4132
+ children: title
4133
+ }
4134
+ ),
4135
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles22.headerActions, children: [
4136
+ users ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4137
+ /* @__PURE__ */ jsxRuntime.jsx(
4138
+ HeaderIconButton,
4139
+ {
4140
+ icon: "compose",
4141
+ onPress: () => setUserPickerOpen(true),
4142
+ label: "New chat"
4143
+ }
4144
+ ),
4145
+ /* @__PURE__ */ jsxRuntime.jsx(
4146
+ HeaderIconButton,
4147
+ {
4148
+ icon: "users",
4149
+ onPress: () => setGroupPickerOpen(true),
4150
+ label: "New group"
4151
+ }
4152
+ )
4153
+ ] }) : null,
4154
+ renderRightAction?.()
4155
+ ] })
4156
+ ]
4157
+ }
4158
+ ),
4159
+ /* @__PURE__ */ jsxRuntime.jsx(
4160
+ ConversationList,
4161
+ {
4162
+ conversations,
4163
+ renderItem: renderRow,
4164
+ ...emptyState !== void 0 ? { emptyState } : {}
4165
+ }
4166
+ )
4167
+ ]
4168
+ }
4169
+ ),
4170
+ /* @__PURE__ */ jsxRuntime.jsx(
4171
+ reactNative.Animated.View,
4172
+ {
4173
+ style: [reactNative.StyleSheet.absoluteFillObject, { transform: [{ translateX: detailTransform }] }],
4174
+ pointerEvents: activeId ? "auto" : "none",
4175
+ ...panResponder.panHandlers,
4176
+ children: activeId ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { flex: 1 }, children: [
4177
+ /* @__PURE__ */ jsxRuntime.jsx(
4178
+ ChatHeader,
4179
+ {
4180
+ conversationId: activeId,
4181
+ onBack: () => setActiveId(null),
4182
+ ...labelFor ? { labelFor } : {},
4183
+ ...avatarFor ? { avatarFor } : {}
4184
+ }
4185
+ ),
4186
+ /* @__PURE__ */ jsxRuntime.jsx(
4187
+ ConversationView,
4188
+ {
4189
+ conversationId: activeId,
4190
+ header: false,
4191
+ keyboardOffset: keyboardOffset || detailKeyboardOffset,
4192
+ onMarkedRead: (convId) => {
4193
+ markConversationRead(convId);
4194
+ },
4195
+ onSent: () => {
4196
+ void refetchConversations();
4197
+ },
4198
+ ...labelFor ? { labelFor } : {},
4199
+ ...avatarFor ? { avatarFor } : {}
4200
+ },
4201
+ activeId
4202
+ )
4203
+ ] }) : null
4204
+ }
4205
+ ),
4206
+ users ? /* @__PURE__ */ jsxRuntime.jsx(
4207
+ UserPickerSheet,
4208
+ {
4209
+ visible: userPickerOpen,
4210
+ onClose: () => setUserPickerOpen(false),
4211
+ users,
4212
+ excludeIds: meExternalId ? [meExternalId] : [],
4213
+ mode: "single",
4214
+ onPickDirect: (externalId) => {
4215
+ void openDirect(externalId);
4216
+ }
4217
+ }
4218
+ ) : null,
4219
+ users ? /* @__PURE__ */ jsxRuntime.jsx(
4220
+ UserPickerSheet,
4221
+ {
4222
+ visible: groupPickerOpen,
4223
+ onClose: () => setGroupPickerOpen(false),
4224
+ users,
4225
+ excludeIds: meExternalId ? [meExternalId] : [],
4226
+ mode: "group",
4227
+ onCreateGroup: (name, ids) => {
4228
+ void openGroup(name, ids);
4229
+ }
4230
+ }
4231
+ ) : null
4232
+ ] });
4233
+ }
4234
+ );
4235
+ function HeaderIconButton({
4236
+ icon,
4237
+ onPress,
4238
+ label
4239
+ }) {
4240
+ const theme = usePoolseTheme();
4241
+ return /* @__PURE__ */ jsxRuntime.jsx(
4242
+ reactNative.Pressable,
4243
+ {
4244
+ onPress,
4245
+ hitSlop: 10,
4246
+ accessibilityLabel: label,
4247
+ style: ({ pressed }) => [
4248
+ styles22.iconBtn,
4249
+ {
4250
+ backgroundColor: pressed ? theme.colors.surface2 : "transparent"
4251
+ }
4252
+ ],
4253
+ children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: icon, size: 20, color: theme.colors.ink })
4254
+ }
4255
+ );
2311
4256
  }
2312
- function ReactionPicker({
2313
- visible,
2314
- onClose,
2315
- onPick,
2316
- emojis = DEFAULT_EMOJIS
4257
+ function DefaultConversationRow({
4258
+ conv,
4259
+ selected,
4260
+ meExternalId,
4261
+ unread,
4262
+ labelFor,
4263
+ avatarFor,
4264
+ onPress
2317
4265
  }) {
2318
4266
  const theme = usePoolseTheme();
2319
- return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Modal, { visible, transparent: true, animationType: "fade", onRequestClose: onClose, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { style: styles17.backdrop, onPress: onClose, children: /* @__PURE__ */ jsxRuntime.jsx(
2320
- reactNative.View,
4267
+ const isDirect = conv.type === "direct";
4268
+ const otherExtId = isDirect && meExternalId ? (conv.member_external_ids ?? []).find((x) => x !== meExternalId) : null;
4269
+ const title = isDirect ? otherExtId ? labelFor?.(otherExtId) ?? otherExtId : "Direct chat" : conv.name ?? "Untitled group";
4270
+ const avatarUrl = isDirect && otherExtId ? avatarFor?.(otherExtId) ?? null : null;
4271
+ const avatarSeed = isDirect ? otherExtId ?? conv.id : conv.id;
4272
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4273
+ reactNative.Pressable,
2321
4274
  {
4275
+ onPress,
2322
4276
  style: [
2323
- styles17.sheet,
4277
+ styles22.row,
2324
4278
  {
2325
- backgroundColor: theme.colors.surface,
2326
- borderRadius: theme.radii.lg
2327
- },
2328
- theme.shadows.lg
4279
+ backgroundColor: selected ? theme.colors.surface2 : "transparent",
4280
+ borderBottomColor: theme.colors.border
4281
+ }
2329
4282
  ],
2330
- children: emojis.map((emoji) => /* @__PURE__ */ jsxRuntime.jsx(
2331
- reactNative.Pressable,
2332
- {
2333
- style: styles17.emojiBtn,
2334
- onPress: () => {
2335
- onPick(emoji);
2336
- onClose();
2337
- },
2338
- children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles17.bigEmoji, children: emoji })
2339
- },
2340
- emoji
2341
- ))
4283
+ children: [
4284
+ avatarUrl ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Image, { source: { uri: avatarUrl }, style: styles22.rowAvatar }) : /* @__PURE__ */ jsxRuntime.jsx(Avatar, { src: null, name: title, seed: avatarSeed, size: "md" }),
4285
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles22.rowBody, children: [
4286
+ /* @__PURE__ */ jsxRuntime.jsx(
4287
+ reactNative.Text,
4288
+ {
4289
+ style: [styles22.rowTitle, { color: theme.colors.ink, fontFamily: theme.type.fontBody }],
4290
+ numberOfLines: 1,
4291
+ children: title
4292
+ }
4293
+ ),
4294
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles22.rowSubtitle, { color: theme.colors.ink3 }], numberOfLines: 1, children: conv.last_message_preview ?? (isDirect ? otherExtId ?? "" : `${(conv.member_external_ids ?? []).length} members`) })
4295
+ ] }),
4296
+ unread > 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles22.unreadPill, { backgroundColor: theme.colors.unreadPill }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles22.unreadText, { color: theme.colors.unreadPillText }], children: unread > 99 ? "99+" : String(unread) }) }) : null
4297
+ ]
2342
4298
  }
2343
- ) }) });
4299
+ );
2344
4300
  }
2345
- var styles17 = reactNative.StyleSheet.create({
2346
- strip: {
4301
+ var styles22 = reactNative.StyleSheet.create({
4302
+ root: {
4303
+ flex: 1,
4304
+ overflow: "hidden"
4305
+ },
4306
+ listHeader: {
2347
4307
  flexDirection: "row",
2348
- flexWrap: "wrap",
2349
- gap: 4,
2350
- marginTop: 4
4308
+ alignItems: "center",
4309
+ paddingHorizontal: 14,
4310
+ paddingVertical: 10,
4311
+ borderBottomWidth: reactNative.StyleSheet.hairlineWidth,
4312
+ gap: 12
2351
4313
  },
2352
- chip: {
4314
+ listTitle: {
4315
+ flex: 1,
4316
+ fontSize: 20,
4317
+ fontWeight: "700"
4318
+ },
4319
+ headerActions: {
2353
4320
  flexDirection: "row",
2354
4321
  alignItems: "center",
2355
- paddingHorizontal: 8,
2356
- paddingVertical: 3,
2357
- borderRadius: 12,
2358
- borderWidth: reactNative.StyleSheet.hairlineWidth,
2359
4322
  gap: 4
2360
4323
  },
2361
- emoji: {
2362
- fontSize: 14
4324
+ iconBtn: {
4325
+ width: 36,
4326
+ height: 36,
4327
+ borderRadius: 18,
4328
+ alignItems: "center",
4329
+ justifyContent: "center"
2363
4330
  },
2364
- count: {
2365
- fontSize: 11,
2366
- fontWeight: "600"
4331
+ row: {
4332
+ flexDirection: "row",
4333
+ alignItems: "center",
4334
+ paddingHorizontal: 14,
4335
+ paddingVertical: 12,
4336
+ borderBottomWidth: reactNative.StyleSheet.hairlineWidth,
4337
+ gap: 12
2367
4338
  },
2368
- backdrop: {
4339
+ rowAvatar: {
4340
+ width: 44,
4341
+ height: 44,
4342
+ borderRadius: 22
4343
+ },
4344
+ rowBody: {
2369
4345
  flex: 1,
2370
- backgroundColor: "rgba(0,0,0,0.35)",
2371
- justifyContent: "center",
2372
- alignItems: "center"
4346
+ gap: 2
2373
4347
  },
2374
- sheet: {
2375
- flexDirection: "row",
2376
- padding: 8,
2377
- gap: 4
4348
+ rowTitle: {
4349
+ fontSize: 15,
4350
+ fontWeight: "600"
2378
4351
  },
2379
- emojiBtn: {
2380
- paddingHorizontal: 10,
2381
- paddingVertical: 8
4352
+ rowSubtitle: {
4353
+ fontSize: 12
2382
4354
  },
2383
- bigEmoji: {
2384
- fontSize: 28
4355
+ unreadPill: {
4356
+ minWidth: 22,
4357
+ height: 22,
4358
+ borderRadius: 11,
4359
+ paddingHorizontal: 7,
4360
+ alignItems: "center",
4361
+ justifyContent: "center"
4362
+ },
4363
+ unreadText: {
4364
+ fontSize: 11,
4365
+ fontWeight: "700"
2385
4366
  }
2386
4367
  });
2387
4368
  var TILE_PATH = "M50 4 C20 4 4 20 4 50 C4 80 20 96 50 96 C80 96 96 80 96 50 C96 20 80 4 50 4 Z";
@@ -2470,9 +4451,12 @@ function useAppStateLifecycle() {
2470
4451
  exports.AttachmentPicker = AttachmentPicker;
2471
4452
  exports.AttachmentPreview = AttachmentPreview;
2472
4453
  exports.Avatar = Avatar;
4454
+ exports.ChatHeader = ChatHeader;
2473
4455
  exports.ConversationList = ConversationList;
2474
4456
  exports.ConversationView = ConversationView;
2475
4457
  exports.EditableMessageBubble = EditableMessageBubble;
4458
+ exports.GroupDetailsSheet = GroupDetailsSheet;
4459
+ exports.ImageMosaic = ImageMosaic;
2476
4460
  exports.MemberList = MemberList;
2477
4461
  exports.MentionInput = MentionInput;
2478
4462
  exports.MessageActions = MessageActions;
@@ -2481,6 +4465,7 @@ exports.MessageComposer = MessageComposer;
2481
4465
  exports.MessageList = MessageList;
2482
4466
  exports.MessageRow = MessageRow;
2483
4467
  exports.PoolseIcon = PoolseIcon;
4468
+ exports.PoolseInbox = PoolseInbox;
2484
4469
  exports.PoolseLogo = PoolseLogo;
2485
4470
  exports.PoolseTheme = PoolseTheme;
2486
4471
  exports.Primitives = primitives;