@poolse/react-native 2.19.0 → 2.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -113,7 +113,7 @@ var radii = {
113
113
  lg: 16,
114
114
  xl: 24,
115
115
  pill: 999,
116
- bubble: 16
116
+ bubble: 18
117
117
  };
118
118
  var spacing = {
119
119
  xs: 4,
@@ -2345,7 +2345,12 @@ var styles12 = reactNative.StyleSheet.create({
2345
2345
  fontWeight: "500"
2346
2346
  }
2347
2347
  });
2348
- function AttachmentPreview({ attachment, onPress, maxWidth = 280 }) {
2348
+ function AttachmentPreview({
2349
+ attachment,
2350
+ onPress,
2351
+ maxWidth = 280,
2352
+ tone = "default"
2353
+ }) {
2349
2354
  const theme = usePoolseTheme();
2350
2355
  const isImage = (attachment.content_type ?? "").startsWith("image/");
2351
2356
  const { url } = react$1.useAttachmentUrl(attachment.id);
@@ -2379,6 +2384,9 @@ function AttachmentPreview({ attachment, onPress, maxWidth = 280 }) {
2379
2384
  }
2380
2385
  ) });
2381
2386
  }
2387
+ const onBrand = tone === "onBrand";
2388
+ const ink = onBrand ? theme.colors.onBrand : theme.colors.ink;
2389
+ const inkSoft = onBrand ? theme.colors.onBrand : theme.colors.ink3;
2382
2390
  return /* @__PURE__ */ jsxRuntime.jsxs(
2383
2391
  reactNative.Pressable,
2384
2392
  {
@@ -2387,23 +2395,23 @@ function AttachmentPreview({ attachment, onPress, maxWidth = 280 }) {
2387
2395
  style: [
2388
2396
  styles13.fileCard,
2389
2397
  {
2390
- backgroundColor: theme.colors.surface2,
2391
- borderColor: theme.colors.border,
2398
+ backgroundColor: onBrand ? "rgba(255,255,255,0.18)" : theme.colors.surface2,
2399
+ borderColor: onBrand ? "transparent" : theme.colors.border,
2392
2400
  borderRadius: theme.radii.md
2393
2401
  }
2394
2402
  ],
2395
2403
  children: [
2396
- /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "attachment", size: 18, color: theme.colors.ink2 }),
2404
+ /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "attachment", size: 18, color: onBrand ? ink : theme.colors.ink2 }),
2397
2405
  /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { flex: 1, marginLeft: 8 }, children: [
2398
2406
  /* @__PURE__ */ jsxRuntime.jsx(
2399
2407
  reactNative.Text,
2400
2408
  {
2401
- style: [styles13.fileName, { color: theme.colors.ink, fontFamily: theme.type.fontBody }],
2409
+ style: [styles13.fileName, { color: ink, fontFamily: theme.type.fontBody }],
2402
2410
  numberOfLines: 1,
2403
2411
  children: attachment.original_filename ?? "file"
2404
2412
  }
2405
2413
  ),
2406
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles13.fileSize, { color: theme.colors.ink3 }], children: formatBytes2(attachment.byte_size) })
2414
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles13.fileSize, { color: inkSoft, opacity: onBrand ? 0.8 : 1 }], children: formatBytes2(attachment.byte_size) })
2407
2415
  ] })
2408
2416
  ]
2409
2417
  }
@@ -2431,9 +2439,16 @@ var styles13 = reactNative.StyleSheet.create({
2431
2439
  fontSize: 11
2432
2440
  }
2433
2441
  });
2434
- function ImageMosaic({ images, maxWidth, radius }) {
2442
+ function ImageMosaic({
2443
+ images,
2444
+ maxWidth,
2445
+ radius,
2446
+ squareBottom = false,
2447
+ onLongPress
2448
+ }) {
2435
2449
  const theme = usePoolseTheme();
2436
2450
  const r = radius ?? theme.radii.md;
2451
+ const rBottom = squareBottom ? 5 : r;
2437
2452
  const [viewerIndex, setViewerIndex] = react.useState(null);
2438
2453
  if (images.length === 0) return null;
2439
2454
  const visible = images.slice(0, 4);
@@ -2446,14 +2461,24 @@ function ImageMosaic({ images, maxWidth, radius }) {
2446
2461
  style: [
2447
2462
  styles14.root,
2448
2463
  {
2449
- borderRadius: r,
2450
- ...maxWidth ? { maxWidth } : {}
2464
+ borderTopLeftRadius: r,
2465
+ borderTopRightRadius: r,
2466
+ borderBottomLeftRadius: rBottom,
2467
+ borderBottomRightRadius: rBottom,
2468
+ // Explicit pixel width, not just a cap: the mosaic sits
2469
+ // inside an auto-sized bubble, and a percentage width
2470
+ // against an indefinite parent collapses (or, worse,
2471
+ // resolves against the image's intrinsic size and blows
2472
+ // the row open). Media bubbles are fixed-width — the
2473
+ // WhatsApp convention anyway.
2474
+ ...maxWidth ? { width: maxWidth } : {}
2451
2475
  }
2452
2476
  ],
2453
2477
  children: [
2454
2478
  n === 1 ? /* @__PURE__ */ jsxRuntime.jsx(
2455
2479
  Tile,
2456
2480
  {
2481
+ onLongPress,
2457
2482
  attachment: visible[0],
2458
2483
  style: styles14.tileFull,
2459
2484
  onPress: () => setViewerIndex(0),
@@ -2464,6 +2489,7 @@ function ImageMosaic({ images, maxWidth, radius }) {
2464
2489
  /* @__PURE__ */ jsxRuntime.jsx(
2465
2490
  Tile,
2466
2491
  {
2492
+ onLongPress,
2467
2493
  attachment: visible[0],
2468
2494
  style: styles14.tileHalfSquare,
2469
2495
  onPress: () => setViewerIndex(0),
@@ -2473,6 +2499,7 @@ function ImageMosaic({ images, maxWidth, radius }) {
2473
2499
  /* @__PURE__ */ jsxRuntime.jsx(
2474
2500
  Tile,
2475
2501
  {
2502
+ onLongPress,
2476
2503
  attachment: visible[1],
2477
2504
  style: styles14.tileHalfSquare,
2478
2505
  onPress: () => setViewerIndex(1),
@@ -2484,6 +2511,7 @@ function ImageMosaic({ images, maxWidth, radius }) {
2484
2511
  /* @__PURE__ */ jsxRuntime.jsx(
2485
2512
  Tile,
2486
2513
  {
2514
+ onLongPress,
2487
2515
  attachment: visible[0],
2488
2516
  style: styles14.tileHero,
2489
2517
  onPress: () => setViewerIndex(0),
@@ -2494,6 +2522,7 @@ function ImageMosaic({ images, maxWidth, radius }) {
2494
2522
  /* @__PURE__ */ jsxRuntime.jsx(
2495
2523
  Tile,
2496
2524
  {
2525
+ onLongPress,
2497
2526
  attachment: visible[1],
2498
2527
  style: styles14.tileHalfSquare,
2499
2528
  onPress: () => setViewerIndex(1),
@@ -2503,6 +2532,7 @@ function ImageMosaic({ images, maxWidth, radius }) {
2503
2532
  /* @__PURE__ */ jsxRuntime.jsx(
2504
2533
  Tile,
2505
2534
  {
2535
+ onLongPress,
2506
2536
  attachment: visible[2],
2507
2537
  style: styles14.tileHalfSquare,
2508
2538
  onPress: () => setViewerIndex(2),
@@ -2516,6 +2546,7 @@ function ImageMosaic({ images, maxWidth, radius }) {
2516
2546
  /* @__PURE__ */ jsxRuntime.jsx(
2517
2547
  Tile,
2518
2548
  {
2549
+ onLongPress,
2519
2550
  attachment: visible[0],
2520
2551
  style: styles14.tileHalfSquare,
2521
2552
  onPress: () => setViewerIndex(0),
@@ -2525,6 +2556,7 @@ function ImageMosaic({ images, maxWidth, radius }) {
2525
2556
  /* @__PURE__ */ jsxRuntime.jsx(
2526
2557
  Tile,
2527
2558
  {
2559
+ onLongPress,
2528
2560
  attachment: visible[1],
2529
2561
  style: styles14.tileHalfSquare,
2530
2562
  onPress: () => setViewerIndex(1),
@@ -2536,6 +2568,7 @@ function ImageMosaic({ images, maxWidth, radius }) {
2536
2568
  /* @__PURE__ */ jsxRuntime.jsx(
2537
2569
  Tile,
2538
2570
  {
2571
+ onLongPress,
2539
2572
  attachment: visible[2],
2540
2573
  style: styles14.tileHalfSquare,
2541
2574
  onPress: () => setViewerIndex(2),
@@ -2545,6 +2578,7 @@ function ImageMosaic({ images, maxWidth, radius }) {
2545
2578
  /* @__PURE__ */ jsxRuntime.jsx(
2546
2579
  Tile,
2547
2580
  {
2581
+ onLongPress,
2548
2582
  attachment: visible[3],
2549
2583
  style: styles14.tileHalfSquare,
2550
2584
  onPress: () => setViewerIndex(3),
@@ -2578,11 +2612,12 @@ function Tile({
2578
2612
  attachment,
2579
2613
  style,
2580
2614
  overlay,
2581
- onPress
2615
+ onPress,
2616
+ onLongPress
2582
2617
  }) {
2583
2618
  const theme = usePoolseTheme();
2584
2619
  const { url } = react$1.useAttachmentUrl(attachment.id);
2585
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Pressable, { onPress, style: [styles14.tileBase, style], children: [
2620
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Pressable, { onPress, onLongPress, style: [styles14.tileBase, style], children: [
2586
2621
  url ? /* @__PURE__ */ jsxRuntime.jsx(
2587
2622
  reactNative.Image,
2588
2623
  {
@@ -2684,6 +2719,27 @@ var styles14 = reactNative.StyleSheet.create({
2684
2719
  borderRadius: 20
2685
2720
  }
2686
2721
  });
2722
+
2723
+ // src/internal/bubbleText.ts
2724
+ function hasMarkdown(body) {
2725
+ return /[*_~`#>|\\]|\[.*\]|^\s*(?:[-+]|\d+[.)])\s|https?:\/\/|www\./ms.test(body);
2726
+ }
2727
+ function emojiOnlyCount(body) {
2728
+ const text = body.trim();
2729
+ if (!text || text.length > 32) return 0;
2730
+ try {
2731
+ const unit = "(?:\\p{Regional_Indicator}{2}|\\p{Extended_Pictographic})[\\u{1F3FB}-\\u{1F3FF}]?\\uFE0F?(?:\\u200D\\p{Extended_Pictographic}[\\u{1F3FB}-\\u{1F3FF}]?\\uFE0F?)*";
2732
+ const whole = new RegExp(`^(?:${unit}){1,3}$`, "u");
2733
+ if (!whole.test(text)) return 0;
2734
+ const each = new RegExp(unit, "gu");
2735
+ return (text.match(each) ?? []).length;
2736
+ } catch {
2737
+ return 0;
2738
+ }
2739
+ }
2740
+ function emojiFontSize(count) {
2741
+ return count === 1 ? 44 : count === 2 ? 38 : 32;
2742
+ }
2687
2743
  var markdownit = Markdown.MarkdownIt({ typographer: false, linkify: true });
2688
2744
  function renderMarkdown(body, args) {
2689
2745
  const baseText = {
@@ -2743,107 +2799,163 @@ function MessageBubble({
2743
2799
  showSenderName = false,
2744
2800
  actionsTrigger,
2745
2801
  showAttachments = true,
2746
- markdown = true
2802
+ markdown = true,
2803
+ onLongPress
2747
2804
  }) {
2748
2805
  const theme = usePoolseTheme();
2749
2806
  const { width: screenWidth } = reactNative.useWindowDimensions();
2750
2807
  const isSelf = currentUserId !== null && message.sender_id === currentUserId;
2751
2808
  const [expanded, setExpanded] = react.useState(false);
2752
2809
  const bubbleMaxWidth = Math.floor(screenWidth * 0.78);
2753
- const contentMaxWidth = bubbleMaxWidth - 28;
2810
+ const mediaWidth = Math.min(bubbleMaxWidth - 6, 420);
2754
2811
  const sender = react$1.useUser(!isSelf ? message.sender_external_id : null);
2755
2812
  const senderFallbackName = message.sender_external_id ?? "Unknown";
2756
2813
  const senderLabel = sender.profile?.displayName ?? (message.sender_external_id ? labelFor?.(message.sender_external_id) ?? senderFallbackName : senderFallbackName);
2757
2814
  const senderTint = userColor(message.sender_external_id ?? message.sender_id ?? message.id);
2758
2815
  const tailCorner = groupPosition === "last" || groupPosition === "standalone";
2759
- const bubbleColor = isSelf ? theme.colors.selfBubble : theme.colors.otherBubble;
2816
+ const bubbleRadius = theme.radii.bubble;
2760
2817
  const textColor = isSelf ? theme.colors.selfBubbleText : theme.colors.otherBubbleText;
2818
+ const mutedOnBubble = isSelf ? theme.colors.onBrand : theme.colors.ink3;
2761
2819
  const radii2 = {
2762
- topLeft: theme.radii.bubble,
2763
- topRight: theme.radii.bubble,
2764
- bottomLeft: theme.radii.bubble,
2765
- bottomRight: theme.radii.bubble
2820
+ topLeft: bubbleRadius,
2821
+ topRight: bubbleRadius,
2822
+ bottomLeft: bubbleRadius,
2823
+ bottomRight: bubbleRadius
2766
2824
  };
2767
2825
  if (tailCorner) {
2768
- if (isSelf) radii2.bottomRight = 4;
2769
- else radii2.bottomLeft = 4;
2826
+ if (isSelf) radii2.bottomRight = 5;
2827
+ else radii2.bottomLeft = 5;
2770
2828
  }
2771
2829
  const body = message.body ?? "";
2772
- const truncated = maxBodyLength > 0 && body.length > maxBodyLength && !expanded ? body.slice(0, maxBodyLength) + "\u2026" : body;
2773
- const time = formatTime(message.inserted_at);
2774
2830
  const isDeleted = !!message.deleted_at;
2775
2831
  const isEdited = !!message.edited_at && !isDeleted;
2776
- const images = (message.attachments ?? []).filter((a) => isImageContentType(a.content_type));
2777
- const files = (message.attachments ?? []).filter((a) => !isImageContentType(a.content_type));
2778
- const renderAttachments = showAttachments && (images.length > 0 || files.length > 0);
2779
- return /* @__PURE__ */ jsxRuntime.jsx(
2780
- reactNative.View,
2781
- {
2782
- style: [
2783
- styles15.wrap,
2784
- {
2785
- alignSelf: isSelf ? "flex-end" : "flex-start",
2786
- // Pixel maxWidth (not percentage) so single-line and
2787
- // multi-line bubbles land at the same right edge — was
2788
- // 88% of the bubbleCol which produced inconsistent right
2789
- // padding when the row padding changed.
2790
- maxWidth: bubbleMaxWidth
2791
- }
2792
- ],
2793
- children: /* @__PURE__ */ jsxRuntime.jsxs(
2794
- reactNative.View,
2795
- {
2796
- style: [
2797
- styles15.bubble,
2832
+ const trimmable = maxBodyLength > 0 && body.length > maxBodyLength;
2833
+ const truncated = trimmable && !expanded ? body.slice(0, maxBodyLength) + "\u2026" : body;
2834
+ const time = formatTime(message.inserted_at);
2835
+ const images = showAttachments && !isDeleted ? (message.attachments ?? []).filter((a) => isImageContentType(a.content_type)) : [];
2836
+ const files = showAttachments && !isDeleted ? (message.attachments ?? []).filter((a) => !isImageContentType(a.content_type)) : [];
2837
+ const hasMedia = images.length > 0;
2838
+ const hasFiles = files.length > 0;
2839
+ const hasBody = !isDeleted && body.length > 0;
2840
+ const metaOverlay = hasMedia && !hasBody && !hasFiles;
2841
+ const jumboCount = hasBody && !hasMedia && !hasFiles && !message.quoted_message_id ? emojiOnlyCount(body) : 0;
2842
+ const inlineMeta = hasBody && jumboCount === 0 && !trimmable && (!markdown || !hasMarkdown(truncated));
2843
+ const sectionAbove = hasMedia || hasFiles || !!message.quoted_message || isDeleted;
2844
+ const ghost = ` ${isEdited ? "edited " : ""}${time}${isSelf && readState ? " \u2713\u2713" : ""}`;
2845
+ const metaRow = (variant) => {
2846
+ const color = variant === "overlay" ? "#FFFFFF" : variant === "jumbo" ? theme.colors.ink3 : mutedOnBubble;
2847
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2848
+ reactNative.View,
2849
+ {
2850
+ style: [
2851
+ styles15.metaRow,
2852
+ variant === "inline" && styles15.metaInline,
2853
+ variant === "overlay" && styles15.metaOverlay,
2854
+ variant === "block" && styles15.metaBlock,
2855
+ // File-only bubble: the meta row follows the cards directly —
2856
+ // give it air so the time doesn't sit glued to the card.
2857
+ variant === "block" && hasFiles && !hasBody && styles15.metaBlockAfterFiles,
2858
+ variant === "jumbo" && styles15.metaJumbo
2859
+ ],
2860
+ pointerEvents: "none",
2861
+ children: [
2862
+ isEdited ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles15.editedTag, { color, opacity: 0.8 }], children: "edited" }) : null,
2863
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles15.meta, { color, opacity: variant === "overlay" ? 1 : 0.75 }], children: time }),
2864
+ isSelf && readState ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles15.readTick, children: /* @__PURE__ */ jsxRuntime.jsx(
2865
+ PoolseIcon,
2866
+ {
2867
+ name: readState === "read" ? "check-double" : "check",
2868
+ size: 13,
2869
+ color
2870
+ }
2871
+ ) }) : null
2872
+ ]
2873
+ }
2874
+ );
2875
+ };
2876
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles15.wrap, { alignSelf: isSelf ? "flex-end" : "flex-start", maxWidth: bubbleMaxWidth }], children: [
2877
+ /* @__PURE__ */ jsxRuntime.jsxs(
2878
+ reactNative.View,
2879
+ {
2880
+ style: [
2881
+ jumboCount === 0 && {
2882
+ backgroundColor: isSelf ? theme.colors.selfBubble : theme.colors.otherBubble,
2883
+ borderColor: isSelf ? "transparent" : theme.colors.border,
2884
+ borderWidth: isSelf ? 0 : reactNative.StyleSheet.hairlineWidth,
2885
+ borderTopLeftRadius: radii2.topLeft,
2886
+ borderTopRightRadius: radii2.topRight,
2887
+ borderBottomLeftRadius: radii2.bottomLeft,
2888
+ borderBottomRightRadius: radii2.bottomRight
2889
+ },
2890
+ jumboCount === 0 && theme.shadows.sm
2891
+ ],
2892
+ children: [
2893
+ showSenderName && !isSelf && jumboCount === 0 && (groupPosition === "first" || groupPosition === "standalone") ? /* @__PURE__ */ jsxRuntime.jsx(
2894
+ reactNative.Text,
2895
+ {
2896
+ style: [styles15.senderName, { color: senderTint, fontFamily: theme.type.fontBody }],
2897
+ children: senderLabel
2898
+ }
2899
+ ) : null,
2900
+ message.quoted_message ? /* @__PURE__ */ jsxRuntime.jsx(
2901
+ QuotedCard,
2902
+ {
2903
+ quoted: message.quoted_message,
2904
+ isSelf,
2905
+ ...labelFor ? { labelFor } : {},
2906
+ ...onQuotedClick ? { onPress: () => onQuotedClick(message.quoted_message.id) } : {}
2907
+ }
2908
+ ) : null,
2909
+ hasMedia ? (
2910
+ // 3px of bubble color framing the picture; the mosaic clips
2911
+ // itself 3px tighter than the bubble so the curves stay
2912
+ // concentric.
2913
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles15.mediaFrame, children: [
2914
+ /* @__PURE__ */ jsxRuntime.jsx(
2915
+ ImageMosaic,
2916
+ {
2917
+ images,
2918
+ maxWidth: mediaWidth,
2919
+ radius: Math.max(bubbleRadius - 3, 8),
2920
+ squareBottom: !metaOverlay,
2921
+ ...onLongPress ? { onLongPress } : {}
2922
+ }
2923
+ ),
2924
+ metaOverlay ? metaRow("overlay") : null
2925
+ ] })
2926
+ ) : null,
2927
+ hasFiles ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles15.filesBlock, children: files.map((att) => /* @__PURE__ */ jsxRuntime.jsx(
2928
+ AttachmentPreview,
2798
2929
  {
2799
- backgroundColor: bubbleColor,
2800
- borderColor: isSelf ? "transparent" : theme.colors.border,
2801
- borderWidth: isSelf ? 0 : reactNative.StyleSheet.hairlineWidth,
2802
- borderTopLeftRadius: radii2.topLeft,
2803
- borderTopRightRadius: radii2.topRight,
2804
- borderBottomLeftRadius: radii2.bottomLeft,
2805
- borderBottomRightRadius: radii2.bottomRight,
2806
- // Slightly more breathing room — 14/10 reads tighter and
2807
- // matches the iMessage / WhatsApp bubble density.
2808
- paddingHorizontal: 14,
2809
- paddingVertical: 10,
2810
- // Floor the bubble at a width that always fits the meta row
2811
- // ("edited HH:MM ✓✓" → ~110px). Without this, short bodies
2812
- // like "ok" produce a bubble too narrow for the meta to lay
2813
- // out, and `edited` or the time gets clipped.
2814
- minWidth: isSelf && isEdited ? 130 : 90
2930
+ attachment: att,
2931
+ maxWidth: bubbleMaxWidth - 12,
2932
+ tone: isSelf ? "onBrand" : "default"
2815
2933
  },
2816
- theme.shadows.sm
2817
- ],
2818
- children: [
2819
- showSenderName && !isSelf && (groupPosition === "first" || groupPosition === "standalone") ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles15.senderName, { color: senderTint, fontFamily: theme.type.fontBody }], children: senderLabel }) : null,
2820
- message.quoted_message ? /* @__PURE__ */ jsxRuntime.jsx(
2821
- QuotedCard,
2822
- {
2823
- quoted: message.quoted_message,
2824
- isSelf,
2825
- ...labelFor ? { labelFor } : {},
2826
- ...onQuotedClick ? { onPress: () => onQuotedClick(message.quoted_message.id) } : {}
2827
- }
2828
- ) : null,
2829
- renderAttachments && images.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(ImageMosaic, { images, maxWidth: contentMaxWidth }) : null,
2830
- renderAttachments && files.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { gap: theme.spacing.xs, marginBottom: theme.spacing.xs }, children: files.map((att) => /* @__PURE__ */ jsxRuntime.jsx(AttachmentPreview, { attachment: att, maxWidth: contentMaxWidth }, att.id)) }) : null,
2831
- body.length > 0 ? isDeleted ? /* @__PURE__ */ jsxRuntime.jsx(
2832
- reactNative.Text,
2833
- {
2834
- style: [
2835
- styles15.body,
2836
- {
2837
- color: theme.colors.ink3,
2838
- fontFamily: theme.type.fontBody,
2839
- fontSize: theme.type.bodySize,
2840
- lineHeight: theme.type.lineHeight,
2841
- fontStyle: "italic"
2842
- }
2843
- ],
2844
- children: "Message deleted"
2845
- }
2846
- ) : markdown ? renderMarkdown(truncated, {
2934
+ att.id
2935
+ )) }) : null,
2936
+ isDeleted ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles15.textBlock, sectionAbove && styles15.textBlockAfter], children: /* @__PURE__ */ jsxRuntime.jsx(
2937
+ reactNative.Text,
2938
+ {
2939
+ style: {
2940
+ color: mutedOnBubble,
2941
+ opacity: 0.7,
2942
+ fontFamily: theme.type.fontBody,
2943
+ fontSize: theme.type.bodySize,
2944
+ lineHeight: theme.type.lineHeight,
2945
+ fontStyle: "italic"
2946
+ },
2947
+ children: "Message deleted"
2948
+ }
2949
+ ) }) : null,
2950
+ hasBody && jumboCount > 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles15.jumboBlock, children: /* @__PURE__ */ jsxRuntime.jsx(
2951
+ reactNative.Text,
2952
+ {
2953
+ style: { fontSize: emojiFontSize(jumboCount), lineHeight: emojiFontSize(jumboCount) + 8 },
2954
+ children: body
2955
+ }
2956
+ ) }) : null,
2957
+ hasBody && jumboCount === 0 ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles15.textBlock, sectionAbove && styles15.textBlockAfter], children: [
2958
+ markdown && !inlineMeta ? renderMarkdown(truncated, {
2847
2959
  textColor,
2848
2960
  linkColor: isSelf ? theme.colors.onBrand : theme.colors.brand,
2849
2961
  fontFamily: theme.type.fontBody,
@@ -2853,22 +2965,29 @@ function MessageBubble({
2853
2965
  void reactNative.Linking.openURL(url).catch(() => void 0);
2854
2966
  return false;
2855
2967
  }
2856
- }) : /* @__PURE__ */ jsxRuntime.jsx(
2968
+ }) : /* @__PURE__ */ jsxRuntime.jsxs(
2857
2969
  reactNative.Text,
2858
2970
  {
2859
- style: [
2860
- styles15.body,
2861
- {
2862
- color: textColor,
2863
- fontFamily: theme.type.fontBody,
2864
- fontSize: theme.type.bodySize,
2865
- lineHeight: theme.type.lineHeight
2866
- }
2867
- ],
2868
- children: truncated
2971
+ style: {
2972
+ color: textColor,
2973
+ fontFamily: theme.type.fontBody,
2974
+ fontSize: theme.type.bodySize,
2975
+ lineHeight: theme.type.lineHeight
2976
+ },
2977
+ children: [
2978
+ truncated,
2979
+ inlineMeta ? (
2980
+ // The invisible twin: reserves the meta's footprint
2981
+ // at the end of the last line so the absolutely-
2982
+ // positioned real meta never covers a glyph — and
2983
+ // when the line is full, the reservation wraps and
2984
+ // the meta drops down, flush right.
2985
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles15.ghost, children: ghost })
2986
+ ) : null
2987
+ ]
2869
2988
  }
2870
- ) : null,
2871
- maxBodyLength > 0 && body.length > maxBodyLength ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: () => setExpanded((e) => !e), children: /* @__PURE__ */ jsxRuntime.jsx(
2989
+ ),
2990
+ trimmable ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: () => setExpanded((e) => !e), children: /* @__PURE__ */ jsxRuntime.jsx(
2872
2991
  reactNative.Text,
2873
2992
  {
2874
2993
  style: [
@@ -2878,48 +2997,15 @@ function MessageBubble({
2878
2997
  children: expanded ? "Show less" : "Read more"
2879
2998
  }
2880
2999
  ) }) : null,
2881
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles15.metaRow, children: [
2882
- isEdited ? /* @__PURE__ */ jsxRuntime.jsx(
2883
- reactNative.Text,
2884
- {
2885
- style: [
2886
- styles15.editedTag,
2887
- {
2888
- color: isSelf ? theme.colors.onBrand : theme.colors.ink3,
2889
- opacity: 0.7
2890
- }
2891
- ],
2892
- children: "edited"
2893
- }
2894
- ) : null,
2895
- /* @__PURE__ */ jsxRuntime.jsx(
2896
- reactNative.Text,
2897
- {
2898
- style: [
2899
- styles15.meta,
2900
- {
2901
- color: isSelf ? theme.colors.onBrand : theme.colors.ink3,
2902
- opacity: isSelf ? 0.85 : 1
2903
- }
2904
- ],
2905
- children: time
2906
- }
2907
- ),
2908
- isSelf && readState ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles15.readTick, children: /* @__PURE__ */ jsxRuntime.jsx(
2909
- PoolseIcon,
2910
- {
2911
- name: readState === "read" ? "check-double" : "check",
2912
- size: 13,
2913
- color: theme.colors.onBrand
2914
- }
2915
- ) }) : null
2916
- ] }),
2917
- actionsTrigger ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles15.actionsSlot, children: actionsTrigger }) : null
2918
- ]
2919
- }
2920
- )
2921
- }
2922
- );
3000
+ inlineMeta ? metaRow("inline") : null
3001
+ ] }) : null,
3002
+ !inlineMeta && !metaOverlay && jumboCount === 0 ? metaRow("block") : null,
3003
+ actionsTrigger ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles15.actionsSlot, children: actionsTrigger }) : null
3004
+ ]
3005
+ }
3006
+ ),
3007
+ jumboCount > 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { alignSelf: isSelf ? "flex-end" : "flex-start" }, children: metaRow("jumbo") }) : null
3008
+ ] });
2923
3009
  }
2924
3010
  function QuotedCard({
2925
3011
  quoted,
@@ -2930,7 +3016,7 @@ function QuotedCard({
2930
3016
  const theme = usePoolseTheme();
2931
3017
  const resolved = react$1.useUser(quoted.sender_external_id);
2932
3018
  const senderName = quoted.sender_external_id ? labelFor?.(quoted.sender_external_id) ?? resolved.profile?.displayName ?? quoted.sender_external_id : "Unknown";
2933
- const bg = isSelf ? "rgba(255,255,255,0.18)" : theme.colors.surface2;
3019
+ const bg = isSelf ? "rgba(0,0,0,0.16)" : theme.colors.surface2;
2934
3020
  const accent = isSelf ? theme.colors.onBrand : theme.colors.brand;
2935
3021
  const txt = isSelf ? theme.colors.onBrand : theme.colors.ink;
2936
3022
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -2946,8 +3032,8 @@ function QuotedCard({
2946
3032
  }
2947
3033
  ],
2948
3034
  children: [
2949
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles15.quotedSender, { color: accent }], children: senderName }),
2950
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles15.quotedBody, { color: txt }], numberOfLines: 2, children: quoted.body ?? "\u2026" })
3035
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles15.quotedSender, { color: accent }], numberOfLines: 1, children: senderName }),
3036
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles15.quotedBody, { color: txt }], numberOfLines: 2, children: quoted.deleted_at ? "Original message deleted" : quoted.body ?? "\u2026" })
2951
3037
  ]
2952
3038
  }
2953
3039
  );
@@ -2983,16 +3069,34 @@ var styles15 = reactNative.StyleSheet.create({
2983
3069
  wrap: {
2984
3070
  marginBottom: 2
2985
3071
  },
2986
- bubble: {
2987
- position: "relative"
2988
- },
2989
3072
  senderName: {
2990
3073
  fontSize: 12,
2991
- fontWeight: "600",
2992
- marginBottom: 2
3074
+ fontWeight: "700",
3075
+ paddingTop: 7,
3076
+ paddingHorizontal: 12
2993
3077
  },
2994
- body: {
2995
- flexShrink: 1
3078
+ mediaFrame: {
3079
+ margin: 3
3080
+ },
3081
+ filesBlock: {
3082
+ paddingHorizontal: 6,
3083
+ paddingTop: 6,
3084
+ gap: 4
3085
+ },
3086
+ textBlock: {
3087
+ paddingHorizontal: 12,
3088
+ paddingVertical: 7
3089
+ },
3090
+ // A hair less air when media / files / a quote already sit above.
3091
+ textBlockAfter: {
3092
+ paddingTop: 6
3093
+ },
3094
+ jumboBlock: {
3095
+ paddingVertical: 2
3096
+ },
3097
+ ghost: {
3098
+ color: "transparent",
3099
+ fontSize: 11
2996
3100
  },
2997
3101
  readMore: {
2998
3102
  fontSize: 12,
@@ -3002,10 +3106,36 @@ var styles15 = reactNative.StyleSheet.create({
3002
3106
  metaRow: {
3003
3107
  flexDirection: "row",
3004
3108
  alignItems: "center",
3109
+ gap: 4
3110
+ },
3111
+ metaInline: {
3112
+ position: "absolute",
3113
+ right: 12,
3114
+ // Optically centers the meta against the body's last line (22px
3115
+ // line over the 7px bottom padding): 7 + (22 - 14) / 2. At 8 it
3116
+ // sat visibly low beside a one-line message.
3117
+ bottom: 11
3118
+ },
3119
+ metaOverlay: {
3120
+ position: "absolute",
3121
+ right: 6,
3122
+ bottom: 6,
3123
+ paddingHorizontal: 8,
3124
+ paddingVertical: 3,
3125
+ borderRadius: 999,
3126
+ backgroundColor: "rgba(0,0,0,0.42)"
3127
+ },
3128
+ metaBlock: {
3005
3129
  justifyContent: "flex-end",
3006
- marginTop: 6,
3007
- gap: 5,
3008
- flexShrink: 0
3130
+ paddingHorizontal: 12,
3131
+ paddingBottom: 6,
3132
+ marginTop: -2
3133
+ },
3134
+ metaBlockAfterFiles: {
3135
+ marginTop: 6
3136
+ },
3137
+ metaJumbo: {
3138
+ paddingTop: 2
3009
3139
  },
3010
3140
  meta: {
3011
3141
  fontSize: 11,
@@ -3018,7 +3148,7 @@ var styles15 = reactNative.StyleSheet.create({
3018
3148
  letterSpacing: 0.1
3019
3149
  },
3020
3150
  readTick: {
3021
- marginLeft: 2,
3151
+ marginLeft: 1,
3022
3152
  marginTop: 1
3023
3153
  },
3024
3154
  actionsSlot: {
@@ -3028,20 +3158,21 @@ var styles15 = reactNative.StyleSheet.create({
3028
3158
  },
3029
3159
  quoted: {
3030
3160
  borderLeftWidth: 3,
3031
- paddingLeft: 8,
3032
- paddingVertical: 4,
3033
- paddingRight: 6,
3034
- marginBottom: 6,
3035
- borderRadius: 4
3161
+ paddingLeft: 9,
3162
+ paddingVertical: 5,
3163
+ paddingRight: 8,
3164
+ margin: 4,
3165
+ marginBottom: 0,
3166
+ borderRadius: 10
3036
3167
  },
3037
3168
  quotedSender: {
3038
3169
  fontSize: 12,
3039
- fontWeight: "600",
3040
- marginBottom: 2
3170
+ fontWeight: "700",
3171
+ marginBottom: 1
3041
3172
  },
3042
3173
  quotedBody: {
3043
- fontSize: 12,
3044
- opacity: 0.85
3174
+ fontSize: 12.5,
3175
+ opacity: 0.8
3045
3176
  }
3046
3177
  });
3047
3178
  var DEFAULT_EMOJIS = ["\u{1F44D}", "\u2764\uFE0F", "\u{1F602}", "\u{1F389}", "\u{1F64F}", "\u{1F525}"];
@@ -3358,14 +3489,9 @@ function MessageRow({
3358
3489
  showSenderName,
3359
3490
  showAttachments: attachments,
3360
3491
  markdown,
3361
- actionsTrigger: actions ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: () => setActionsOpen(true), hitSlop: 8, children: /* @__PURE__ */ jsxRuntime.jsx(
3362
- PoolseIcon,
3363
- {
3364
- name: "chevron-down",
3365
- size: 14,
3366
- color: isSelf ? theme.colors.onBrand : theme.colors.ink3
3367
- }
3368
- ) }) : null
3492
+ onLongPress: () => {
3493
+ if (actions) setActionsOpen(true);
3494
+ }
3369
3495
  }
3370
3496
  )
3371
3497
  }