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