@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.js CHANGED
@@ -86,7 +86,7 @@ var radii = {
86
86
  lg: 16,
87
87
  xl: 24,
88
88
  pill: 999,
89
- bubble: 16
89
+ bubble: 18
90
90
  };
91
91
  var spacing = {
92
92
  xs: 4,
@@ -2318,7 +2318,12 @@ var styles12 = StyleSheet.create({
2318
2318
  fontWeight: "500"
2319
2319
  }
2320
2320
  });
2321
- function AttachmentPreview({ attachment, onPress, maxWidth = 280 }) {
2321
+ function AttachmentPreview({
2322
+ attachment,
2323
+ onPress,
2324
+ maxWidth = 280,
2325
+ tone = "default"
2326
+ }) {
2322
2327
  const theme = usePoolseTheme();
2323
2328
  const isImage = (attachment.content_type ?? "").startsWith("image/");
2324
2329
  const { url } = useAttachmentUrl(attachment.id);
@@ -2352,6 +2357,9 @@ function AttachmentPreview({ attachment, onPress, maxWidth = 280 }) {
2352
2357
  }
2353
2358
  ) });
2354
2359
  }
2360
+ const onBrand = tone === "onBrand";
2361
+ const ink = onBrand ? theme.colors.onBrand : theme.colors.ink;
2362
+ const inkSoft = onBrand ? theme.colors.onBrand : theme.colors.ink3;
2355
2363
  return /* @__PURE__ */ jsxs(
2356
2364
  Pressable,
2357
2365
  {
@@ -2360,23 +2368,23 @@ function AttachmentPreview({ attachment, onPress, maxWidth = 280 }) {
2360
2368
  style: [
2361
2369
  styles13.fileCard,
2362
2370
  {
2363
- backgroundColor: theme.colors.surface2,
2364
- borderColor: theme.colors.border,
2371
+ backgroundColor: onBrand ? "rgba(255,255,255,0.18)" : theme.colors.surface2,
2372
+ borderColor: onBrand ? "transparent" : theme.colors.border,
2365
2373
  borderRadius: theme.radii.md
2366
2374
  }
2367
2375
  ],
2368
2376
  children: [
2369
- /* @__PURE__ */ jsx(PoolseIcon, { name: "attachment", size: 18, color: theme.colors.ink2 }),
2377
+ /* @__PURE__ */ jsx(PoolseIcon, { name: "attachment", size: 18, color: onBrand ? ink : theme.colors.ink2 }),
2370
2378
  /* @__PURE__ */ jsxs(View, { style: { flex: 1, marginLeft: 8 }, children: [
2371
2379
  /* @__PURE__ */ jsx(
2372
2380
  Text,
2373
2381
  {
2374
- style: [styles13.fileName, { color: theme.colors.ink, fontFamily: theme.type.fontBody }],
2382
+ style: [styles13.fileName, { color: ink, fontFamily: theme.type.fontBody }],
2375
2383
  numberOfLines: 1,
2376
2384
  children: attachment.original_filename ?? "file"
2377
2385
  }
2378
2386
  ),
2379
- /* @__PURE__ */ jsx(Text, { style: [styles13.fileSize, { color: theme.colors.ink3 }], children: formatBytes2(attachment.byte_size) })
2387
+ /* @__PURE__ */ jsx(Text, { style: [styles13.fileSize, { color: inkSoft, opacity: onBrand ? 0.8 : 1 }], children: formatBytes2(attachment.byte_size) })
2380
2388
  ] })
2381
2389
  ]
2382
2390
  }
@@ -2404,9 +2412,16 @@ var styles13 = StyleSheet.create({
2404
2412
  fontSize: 11
2405
2413
  }
2406
2414
  });
2407
- function ImageMosaic({ images, maxWidth, radius }) {
2415
+ function ImageMosaic({
2416
+ images,
2417
+ maxWidth,
2418
+ radius,
2419
+ squareBottom = false,
2420
+ onLongPress
2421
+ }) {
2408
2422
  const theme = usePoolseTheme();
2409
2423
  const r = radius ?? theme.radii.md;
2424
+ const rBottom = squareBottom ? 5 : r;
2410
2425
  const [viewerIndex, setViewerIndex] = useState(null);
2411
2426
  if (images.length === 0) return null;
2412
2427
  const visible = images.slice(0, 4);
@@ -2419,14 +2434,24 @@ function ImageMosaic({ images, maxWidth, radius }) {
2419
2434
  style: [
2420
2435
  styles14.root,
2421
2436
  {
2422
- borderRadius: r,
2423
- ...maxWidth ? { maxWidth } : {}
2437
+ borderTopLeftRadius: r,
2438
+ borderTopRightRadius: r,
2439
+ borderBottomLeftRadius: rBottom,
2440
+ borderBottomRightRadius: rBottom,
2441
+ // Explicit pixel width, not just a cap: the mosaic sits
2442
+ // inside an auto-sized bubble, and a percentage width
2443
+ // against an indefinite parent collapses (or, worse,
2444
+ // resolves against the image's intrinsic size and blows
2445
+ // the row open). Media bubbles are fixed-width — the
2446
+ // WhatsApp convention anyway.
2447
+ ...maxWidth ? { width: maxWidth } : {}
2424
2448
  }
2425
2449
  ],
2426
2450
  children: [
2427
2451
  n === 1 ? /* @__PURE__ */ jsx(
2428
2452
  Tile,
2429
2453
  {
2454
+ onLongPress,
2430
2455
  attachment: visible[0],
2431
2456
  style: styles14.tileFull,
2432
2457
  onPress: () => setViewerIndex(0),
@@ -2437,6 +2462,7 @@ function ImageMosaic({ images, maxWidth, radius }) {
2437
2462
  /* @__PURE__ */ jsx(
2438
2463
  Tile,
2439
2464
  {
2465
+ onLongPress,
2440
2466
  attachment: visible[0],
2441
2467
  style: styles14.tileHalfSquare,
2442
2468
  onPress: () => setViewerIndex(0),
@@ -2446,6 +2472,7 @@ function ImageMosaic({ images, maxWidth, radius }) {
2446
2472
  /* @__PURE__ */ jsx(
2447
2473
  Tile,
2448
2474
  {
2475
+ onLongPress,
2449
2476
  attachment: visible[1],
2450
2477
  style: styles14.tileHalfSquare,
2451
2478
  onPress: () => setViewerIndex(1),
@@ -2457,6 +2484,7 @@ function ImageMosaic({ images, maxWidth, radius }) {
2457
2484
  /* @__PURE__ */ jsx(
2458
2485
  Tile,
2459
2486
  {
2487
+ onLongPress,
2460
2488
  attachment: visible[0],
2461
2489
  style: styles14.tileHero,
2462
2490
  onPress: () => setViewerIndex(0),
@@ -2467,6 +2495,7 @@ function ImageMosaic({ images, maxWidth, radius }) {
2467
2495
  /* @__PURE__ */ jsx(
2468
2496
  Tile,
2469
2497
  {
2498
+ onLongPress,
2470
2499
  attachment: visible[1],
2471
2500
  style: styles14.tileHalfSquare,
2472
2501
  onPress: () => setViewerIndex(1),
@@ -2476,6 +2505,7 @@ function ImageMosaic({ images, maxWidth, radius }) {
2476
2505
  /* @__PURE__ */ jsx(
2477
2506
  Tile,
2478
2507
  {
2508
+ onLongPress,
2479
2509
  attachment: visible[2],
2480
2510
  style: styles14.tileHalfSquare,
2481
2511
  onPress: () => setViewerIndex(2),
@@ -2489,6 +2519,7 @@ function ImageMosaic({ images, maxWidth, radius }) {
2489
2519
  /* @__PURE__ */ jsx(
2490
2520
  Tile,
2491
2521
  {
2522
+ onLongPress,
2492
2523
  attachment: visible[0],
2493
2524
  style: styles14.tileHalfSquare,
2494
2525
  onPress: () => setViewerIndex(0),
@@ -2498,6 +2529,7 @@ function ImageMosaic({ images, maxWidth, radius }) {
2498
2529
  /* @__PURE__ */ jsx(
2499
2530
  Tile,
2500
2531
  {
2532
+ onLongPress,
2501
2533
  attachment: visible[1],
2502
2534
  style: styles14.tileHalfSquare,
2503
2535
  onPress: () => setViewerIndex(1),
@@ -2509,6 +2541,7 @@ function ImageMosaic({ images, maxWidth, radius }) {
2509
2541
  /* @__PURE__ */ jsx(
2510
2542
  Tile,
2511
2543
  {
2544
+ onLongPress,
2512
2545
  attachment: visible[2],
2513
2546
  style: styles14.tileHalfSquare,
2514
2547
  onPress: () => setViewerIndex(2),
@@ -2518,6 +2551,7 @@ function ImageMosaic({ images, maxWidth, radius }) {
2518
2551
  /* @__PURE__ */ jsx(
2519
2552
  Tile,
2520
2553
  {
2554
+ onLongPress,
2521
2555
  attachment: visible[3],
2522
2556
  style: styles14.tileHalfSquare,
2523
2557
  onPress: () => setViewerIndex(3),
@@ -2551,11 +2585,12 @@ function Tile({
2551
2585
  attachment,
2552
2586
  style,
2553
2587
  overlay,
2554
- onPress
2588
+ onPress,
2589
+ onLongPress
2555
2590
  }) {
2556
2591
  const theme = usePoolseTheme();
2557
2592
  const { url } = useAttachmentUrl(attachment.id);
2558
- return /* @__PURE__ */ jsxs(Pressable, { onPress, style: [styles14.tileBase, style], children: [
2593
+ return /* @__PURE__ */ jsxs(Pressable, { onPress, onLongPress, style: [styles14.tileBase, style], children: [
2559
2594
  url ? /* @__PURE__ */ jsx(
2560
2595
  Image,
2561
2596
  {
@@ -2657,6 +2692,27 @@ var styles14 = StyleSheet.create({
2657
2692
  borderRadius: 20
2658
2693
  }
2659
2694
  });
2695
+
2696
+ // src/internal/bubbleText.ts
2697
+ function hasMarkdown(body) {
2698
+ return /[*_~`#>|\\]|\[.*\]|^\s*(?:[-+]|\d+[.)])\s|https?:\/\/|www\./ms.test(body);
2699
+ }
2700
+ function emojiOnlyCount(body) {
2701
+ const text = body.trim();
2702
+ if (!text || text.length > 32) return 0;
2703
+ try {
2704
+ const unit = "(?:\\p{Regional_Indicator}{2}|\\p{Extended_Pictographic})[\\u{1F3FB}-\\u{1F3FF}]?\\uFE0F?(?:\\u200D\\p{Extended_Pictographic}[\\u{1F3FB}-\\u{1F3FF}]?\\uFE0F?)*";
2705
+ const whole = new RegExp(`^(?:${unit}){1,3}$`, "u");
2706
+ if (!whole.test(text)) return 0;
2707
+ const each = new RegExp(unit, "gu");
2708
+ return (text.match(each) ?? []).length;
2709
+ } catch {
2710
+ return 0;
2711
+ }
2712
+ }
2713
+ function emojiFontSize(count) {
2714
+ return count === 1 ? 44 : count === 2 ? 38 : 32;
2715
+ }
2660
2716
  var markdownit = MarkdownIt({ typographer: false, linkify: true });
2661
2717
  function renderMarkdown(body, args) {
2662
2718
  const baseText = {
@@ -2716,107 +2772,163 @@ function MessageBubble({
2716
2772
  showSenderName = false,
2717
2773
  actionsTrigger,
2718
2774
  showAttachments = true,
2719
- markdown = true
2775
+ markdown = true,
2776
+ onLongPress
2720
2777
  }) {
2721
2778
  const theme = usePoolseTheme();
2722
2779
  const { width: screenWidth } = useWindowDimensions();
2723
2780
  const isSelf = currentUserId !== null && message.sender_id === currentUserId;
2724
2781
  const [expanded, setExpanded] = useState(false);
2725
2782
  const bubbleMaxWidth = Math.floor(screenWidth * 0.78);
2726
- const contentMaxWidth = bubbleMaxWidth - 28;
2783
+ const mediaWidth = Math.min(bubbleMaxWidth - 6, 420);
2727
2784
  const sender = useUser(!isSelf ? message.sender_external_id : null);
2728
2785
  const senderFallbackName = message.sender_external_id ?? "Unknown";
2729
2786
  const senderLabel = sender.profile?.displayName ?? (message.sender_external_id ? labelFor?.(message.sender_external_id) ?? senderFallbackName : senderFallbackName);
2730
2787
  const senderTint = userColor(message.sender_external_id ?? message.sender_id ?? message.id);
2731
2788
  const tailCorner = groupPosition === "last" || groupPosition === "standalone";
2732
- const bubbleColor = isSelf ? theme.colors.selfBubble : theme.colors.otherBubble;
2789
+ const bubbleRadius = theme.radii.bubble;
2733
2790
  const textColor = isSelf ? theme.colors.selfBubbleText : theme.colors.otherBubbleText;
2791
+ const mutedOnBubble = isSelf ? theme.colors.onBrand : theme.colors.ink3;
2734
2792
  const radii2 = {
2735
- topLeft: theme.radii.bubble,
2736
- topRight: theme.radii.bubble,
2737
- bottomLeft: theme.radii.bubble,
2738
- bottomRight: theme.radii.bubble
2793
+ topLeft: bubbleRadius,
2794
+ topRight: bubbleRadius,
2795
+ bottomLeft: bubbleRadius,
2796
+ bottomRight: bubbleRadius
2739
2797
  };
2740
2798
  if (tailCorner) {
2741
- if (isSelf) radii2.bottomRight = 4;
2742
- else radii2.bottomLeft = 4;
2799
+ if (isSelf) radii2.bottomRight = 5;
2800
+ else radii2.bottomLeft = 5;
2743
2801
  }
2744
2802
  const body = message.body ?? "";
2745
- const truncated = maxBodyLength > 0 && body.length > maxBodyLength && !expanded ? body.slice(0, maxBodyLength) + "\u2026" : body;
2746
- const time = formatTime(message.inserted_at);
2747
2803
  const isDeleted = !!message.deleted_at;
2748
2804
  const isEdited = !!message.edited_at && !isDeleted;
2749
- const images = (message.attachments ?? []).filter((a) => isImageContentType(a.content_type));
2750
- const files = (message.attachments ?? []).filter((a) => !isImageContentType(a.content_type));
2751
- const renderAttachments = showAttachments && (images.length > 0 || files.length > 0);
2752
- return /* @__PURE__ */ jsx(
2753
- View,
2754
- {
2755
- style: [
2756
- styles15.wrap,
2757
- {
2758
- alignSelf: isSelf ? "flex-end" : "flex-start",
2759
- // Pixel maxWidth (not percentage) so single-line and
2760
- // multi-line bubbles land at the same right edge — was
2761
- // 88% of the bubbleCol which produced inconsistent right
2762
- // padding when the row padding changed.
2763
- maxWidth: bubbleMaxWidth
2764
- }
2765
- ],
2766
- children: /* @__PURE__ */ jsxs(
2767
- View,
2768
- {
2769
- style: [
2770
- styles15.bubble,
2805
+ const trimmable = maxBodyLength > 0 && body.length > maxBodyLength;
2806
+ const truncated = trimmable && !expanded ? body.slice(0, maxBodyLength) + "\u2026" : body;
2807
+ const time = formatTime(message.inserted_at);
2808
+ const images = showAttachments && !isDeleted ? (message.attachments ?? []).filter((a) => isImageContentType(a.content_type)) : [];
2809
+ const files = showAttachments && !isDeleted ? (message.attachments ?? []).filter((a) => !isImageContentType(a.content_type)) : [];
2810
+ const hasMedia = images.length > 0;
2811
+ const hasFiles = files.length > 0;
2812
+ const hasBody = !isDeleted && body.length > 0;
2813
+ const metaOverlay = hasMedia && !hasBody && !hasFiles;
2814
+ const jumboCount = hasBody && !hasMedia && !hasFiles && !message.quoted_message_id ? emojiOnlyCount(body) : 0;
2815
+ const inlineMeta = hasBody && jumboCount === 0 && !trimmable && (!markdown || !hasMarkdown(truncated));
2816
+ const sectionAbove = hasMedia || hasFiles || !!message.quoted_message || isDeleted;
2817
+ const ghost = ` ${isEdited ? "edited " : ""}${time}${isSelf && readState ? " \u2713\u2713" : ""}`;
2818
+ const metaRow = (variant) => {
2819
+ const color = variant === "overlay" ? "#FFFFFF" : variant === "jumbo" ? theme.colors.ink3 : mutedOnBubble;
2820
+ return /* @__PURE__ */ jsxs(
2821
+ View,
2822
+ {
2823
+ style: [
2824
+ styles15.metaRow,
2825
+ variant === "inline" && styles15.metaInline,
2826
+ variant === "overlay" && styles15.metaOverlay,
2827
+ variant === "block" && styles15.metaBlock,
2828
+ // File-only bubble: the meta row follows the cards directly —
2829
+ // give it air so the time doesn't sit glued to the card.
2830
+ variant === "block" && hasFiles && !hasBody && styles15.metaBlockAfterFiles,
2831
+ variant === "jumbo" && styles15.metaJumbo
2832
+ ],
2833
+ pointerEvents: "none",
2834
+ children: [
2835
+ isEdited ? /* @__PURE__ */ jsx(Text, { style: [styles15.editedTag, { color, opacity: 0.8 }], children: "edited" }) : null,
2836
+ /* @__PURE__ */ jsx(Text, { style: [styles15.meta, { color, opacity: variant === "overlay" ? 1 : 0.75 }], children: time }),
2837
+ isSelf && readState ? /* @__PURE__ */ jsx(View, { style: styles15.readTick, children: /* @__PURE__ */ jsx(
2838
+ PoolseIcon,
2839
+ {
2840
+ name: readState === "read" ? "check-double" : "check",
2841
+ size: 13,
2842
+ color
2843
+ }
2844
+ ) }) : null
2845
+ ]
2846
+ }
2847
+ );
2848
+ };
2849
+ return /* @__PURE__ */ jsxs(View, { style: [styles15.wrap, { alignSelf: isSelf ? "flex-end" : "flex-start", maxWidth: bubbleMaxWidth }], children: [
2850
+ /* @__PURE__ */ jsxs(
2851
+ View,
2852
+ {
2853
+ style: [
2854
+ jumboCount === 0 && {
2855
+ backgroundColor: isSelf ? theme.colors.selfBubble : theme.colors.otherBubble,
2856
+ borderColor: isSelf ? "transparent" : theme.colors.border,
2857
+ borderWidth: isSelf ? 0 : StyleSheet.hairlineWidth,
2858
+ borderTopLeftRadius: radii2.topLeft,
2859
+ borderTopRightRadius: radii2.topRight,
2860
+ borderBottomLeftRadius: radii2.bottomLeft,
2861
+ borderBottomRightRadius: radii2.bottomRight
2862
+ },
2863
+ jumboCount === 0 && theme.shadows.sm
2864
+ ],
2865
+ children: [
2866
+ showSenderName && !isSelf && jumboCount === 0 && (groupPosition === "first" || groupPosition === "standalone") ? /* @__PURE__ */ jsx(
2867
+ Text,
2868
+ {
2869
+ style: [styles15.senderName, { color: senderTint, fontFamily: theme.type.fontBody }],
2870
+ children: senderLabel
2871
+ }
2872
+ ) : null,
2873
+ message.quoted_message ? /* @__PURE__ */ jsx(
2874
+ QuotedCard,
2875
+ {
2876
+ quoted: message.quoted_message,
2877
+ isSelf,
2878
+ ...labelFor ? { labelFor } : {},
2879
+ ...onQuotedClick ? { onPress: () => onQuotedClick(message.quoted_message.id) } : {}
2880
+ }
2881
+ ) : null,
2882
+ hasMedia ? (
2883
+ // 3px of bubble color framing the picture; the mosaic clips
2884
+ // itself 3px tighter than the bubble so the curves stay
2885
+ // concentric.
2886
+ /* @__PURE__ */ jsxs(View, { style: styles15.mediaFrame, children: [
2887
+ /* @__PURE__ */ jsx(
2888
+ ImageMosaic,
2889
+ {
2890
+ images,
2891
+ maxWidth: mediaWidth,
2892
+ radius: Math.max(bubbleRadius - 3, 8),
2893
+ squareBottom: !metaOverlay,
2894
+ ...onLongPress ? { onLongPress } : {}
2895
+ }
2896
+ ),
2897
+ metaOverlay ? metaRow("overlay") : null
2898
+ ] })
2899
+ ) : null,
2900
+ hasFiles ? /* @__PURE__ */ jsx(View, { style: styles15.filesBlock, children: files.map((att) => /* @__PURE__ */ jsx(
2901
+ AttachmentPreview,
2771
2902
  {
2772
- backgroundColor: bubbleColor,
2773
- borderColor: isSelf ? "transparent" : theme.colors.border,
2774
- borderWidth: isSelf ? 0 : StyleSheet.hairlineWidth,
2775
- borderTopLeftRadius: radii2.topLeft,
2776
- borderTopRightRadius: radii2.topRight,
2777
- borderBottomLeftRadius: radii2.bottomLeft,
2778
- borderBottomRightRadius: radii2.bottomRight,
2779
- // Slightly more breathing room — 14/10 reads tighter and
2780
- // matches the iMessage / WhatsApp bubble density.
2781
- paddingHorizontal: 14,
2782
- paddingVertical: 10,
2783
- // Floor the bubble at a width that always fits the meta row
2784
- // ("edited HH:MM ✓✓" → ~110px). Without this, short bodies
2785
- // like "ok" produce a bubble too narrow for the meta to lay
2786
- // out, and `edited` or the time gets clipped.
2787
- minWidth: isSelf && isEdited ? 130 : 90
2903
+ attachment: att,
2904
+ maxWidth: bubbleMaxWidth - 12,
2905
+ tone: isSelf ? "onBrand" : "default"
2788
2906
  },
2789
- theme.shadows.sm
2790
- ],
2791
- children: [
2792
- showSenderName && !isSelf && (groupPosition === "first" || groupPosition === "standalone") ? /* @__PURE__ */ jsx(Text, { style: [styles15.senderName, { color: senderTint, fontFamily: theme.type.fontBody }], children: senderLabel }) : null,
2793
- message.quoted_message ? /* @__PURE__ */ jsx(
2794
- QuotedCard,
2795
- {
2796
- quoted: message.quoted_message,
2797
- isSelf,
2798
- ...labelFor ? { labelFor } : {},
2799
- ...onQuotedClick ? { onPress: () => onQuotedClick(message.quoted_message.id) } : {}
2800
- }
2801
- ) : null,
2802
- renderAttachments && images.length > 0 ? /* @__PURE__ */ jsx(ImageMosaic, { images, maxWidth: contentMaxWidth }) : null,
2803
- 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,
2804
- body.length > 0 ? isDeleted ? /* @__PURE__ */ jsx(
2805
- Text,
2806
- {
2807
- style: [
2808
- styles15.body,
2809
- {
2810
- color: theme.colors.ink3,
2811
- fontFamily: theme.type.fontBody,
2812
- fontSize: theme.type.bodySize,
2813
- lineHeight: theme.type.lineHeight,
2814
- fontStyle: "italic"
2815
- }
2816
- ],
2817
- children: "Message deleted"
2818
- }
2819
- ) : markdown ? renderMarkdown(truncated, {
2907
+ att.id
2908
+ )) }) : null,
2909
+ isDeleted ? /* @__PURE__ */ jsx(View, { style: [styles15.textBlock, sectionAbove && styles15.textBlockAfter], children: /* @__PURE__ */ jsx(
2910
+ Text,
2911
+ {
2912
+ style: {
2913
+ color: mutedOnBubble,
2914
+ opacity: 0.7,
2915
+ fontFamily: theme.type.fontBody,
2916
+ fontSize: theme.type.bodySize,
2917
+ lineHeight: theme.type.lineHeight,
2918
+ fontStyle: "italic"
2919
+ },
2920
+ children: "Message deleted"
2921
+ }
2922
+ ) }) : null,
2923
+ hasBody && jumboCount > 0 ? /* @__PURE__ */ jsx(View, { style: styles15.jumboBlock, children: /* @__PURE__ */ jsx(
2924
+ Text,
2925
+ {
2926
+ style: { fontSize: emojiFontSize(jumboCount), lineHeight: emojiFontSize(jumboCount) + 8 },
2927
+ children: body
2928
+ }
2929
+ ) }) : null,
2930
+ hasBody && jumboCount === 0 ? /* @__PURE__ */ jsxs(View, { style: [styles15.textBlock, sectionAbove && styles15.textBlockAfter], children: [
2931
+ markdown && !inlineMeta ? renderMarkdown(truncated, {
2820
2932
  textColor,
2821
2933
  linkColor: isSelf ? theme.colors.onBrand : theme.colors.brand,
2822
2934
  fontFamily: theme.type.fontBody,
@@ -2826,22 +2938,29 @@ function MessageBubble({
2826
2938
  void Linking.openURL(url).catch(() => void 0);
2827
2939
  return false;
2828
2940
  }
2829
- }) : /* @__PURE__ */ jsx(
2941
+ }) : /* @__PURE__ */ jsxs(
2830
2942
  Text,
2831
2943
  {
2832
- style: [
2833
- styles15.body,
2834
- {
2835
- color: textColor,
2836
- fontFamily: theme.type.fontBody,
2837
- fontSize: theme.type.bodySize,
2838
- lineHeight: theme.type.lineHeight
2839
- }
2840
- ],
2841
- children: truncated
2944
+ style: {
2945
+ color: textColor,
2946
+ fontFamily: theme.type.fontBody,
2947
+ fontSize: theme.type.bodySize,
2948
+ lineHeight: theme.type.lineHeight
2949
+ },
2950
+ children: [
2951
+ truncated,
2952
+ inlineMeta ? (
2953
+ // The invisible twin: reserves the meta's footprint
2954
+ // at the end of the last line so the absolutely-
2955
+ // positioned real meta never covers a glyph — and
2956
+ // when the line is full, the reservation wraps and
2957
+ // the meta drops down, flush right.
2958
+ /* @__PURE__ */ jsx(Text, { style: styles15.ghost, children: ghost })
2959
+ ) : null
2960
+ ]
2842
2961
  }
2843
- ) : null,
2844
- maxBodyLength > 0 && body.length > maxBodyLength ? /* @__PURE__ */ jsx(Pressable, { onPress: () => setExpanded((e) => !e), children: /* @__PURE__ */ jsx(
2962
+ ),
2963
+ trimmable ? /* @__PURE__ */ jsx(Pressable, { onPress: () => setExpanded((e) => !e), children: /* @__PURE__ */ jsx(
2845
2964
  Text,
2846
2965
  {
2847
2966
  style: [
@@ -2851,48 +2970,15 @@ function MessageBubble({
2851
2970
  children: expanded ? "Show less" : "Read more"
2852
2971
  }
2853
2972
  ) }) : null,
2854
- /* @__PURE__ */ jsxs(View, { style: styles15.metaRow, children: [
2855
- isEdited ? /* @__PURE__ */ jsx(
2856
- Text,
2857
- {
2858
- style: [
2859
- styles15.editedTag,
2860
- {
2861
- color: isSelf ? theme.colors.onBrand : theme.colors.ink3,
2862
- opacity: 0.7
2863
- }
2864
- ],
2865
- children: "edited"
2866
- }
2867
- ) : null,
2868
- /* @__PURE__ */ jsx(
2869
- Text,
2870
- {
2871
- style: [
2872
- styles15.meta,
2873
- {
2874
- color: isSelf ? theme.colors.onBrand : theme.colors.ink3,
2875
- opacity: isSelf ? 0.85 : 1
2876
- }
2877
- ],
2878
- children: time
2879
- }
2880
- ),
2881
- isSelf && readState ? /* @__PURE__ */ jsx(View, { style: styles15.readTick, children: /* @__PURE__ */ jsx(
2882
- PoolseIcon,
2883
- {
2884
- name: readState === "read" ? "check-double" : "check",
2885
- size: 13,
2886
- color: theme.colors.onBrand
2887
- }
2888
- ) }) : null
2889
- ] }),
2890
- actionsTrigger ? /* @__PURE__ */ jsx(View, { style: styles15.actionsSlot, children: actionsTrigger }) : null
2891
- ]
2892
- }
2893
- )
2894
- }
2895
- );
2973
+ inlineMeta ? metaRow("inline") : null
2974
+ ] }) : null,
2975
+ !inlineMeta && !metaOverlay && jumboCount === 0 ? metaRow("block") : null,
2976
+ actionsTrigger ? /* @__PURE__ */ jsx(View, { style: styles15.actionsSlot, children: actionsTrigger }) : null
2977
+ ]
2978
+ }
2979
+ ),
2980
+ jumboCount > 0 ? /* @__PURE__ */ jsx(View, { style: { alignSelf: isSelf ? "flex-end" : "flex-start" }, children: metaRow("jumbo") }) : null
2981
+ ] });
2896
2982
  }
2897
2983
  function QuotedCard({
2898
2984
  quoted,
@@ -2903,7 +2989,7 @@ function QuotedCard({
2903
2989
  const theme = usePoolseTheme();
2904
2990
  const resolved = useUser(quoted.sender_external_id);
2905
2991
  const senderName = quoted.sender_external_id ? labelFor?.(quoted.sender_external_id) ?? resolved.profile?.displayName ?? quoted.sender_external_id : "Unknown";
2906
- const bg = isSelf ? "rgba(255,255,255,0.18)" : theme.colors.surface2;
2992
+ const bg = isSelf ? "rgba(0,0,0,0.16)" : theme.colors.surface2;
2907
2993
  const accent = isSelf ? theme.colors.onBrand : theme.colors.brand;
2908
2994
  const txt = isSelf ? theme.colors.onBrand : theme.colors.ink;
2909
2995
  return /* @__PURE__ */ jsxs(
@@ -2919,8 +3005,8 @@ function QuotedCard({
2919
3005
  }
2920
3006
  ],
2921
3007
  children: [
2922
- /* @__PURE__ */ jsx(Text, { style: [styles15.quotedSender, { color: accent }], children: senderName }),
2923
- /* @__PURE__ */ jsx(Text, { style: [styles15.quotedBody, { color: txt }], numberOfLines: 2, children: quoted.body ?? "\u2026" })
3008
+ /* @__PURE__ */ jsx(Text, { style: [styles15.quotedSender, { color: accent }], numberOfLines: 1, children: senderName }),
3009
+ /* @__PURE__ */ jsx(Text, { style: [styles15.quotedBody, { color: txt }], numberOfLines: 2, children: quoted.deleted_at ? "Original message deleted" : quoted.body ?? "\u2026" })
2924
3010
  ]
2925
3011
  }
2926
3012
  );
@@ -2956,16 +3042,34 @@ var styles15 = StyleSheet.create({
2956
3042
  wrap: {
2957
3043
  marginBottom: 2
2958
3044
  },
2959
- bubble: {
2960
- position: "relative"
2961
- },
2962
3045
  senderName: {
2963
3046
  fontSize: 12,
2964
- fontWeight: "600",
2965
- marginBottom: 2
3047
+ fontWeight: "700",
3048
+ paddingTop: 7,
3049
+ paddingHorizontal: 12
2966
3050
  },
2967
- body: {
2968
- flexShrink: 1
3051
+ mediaFrame: {
3052
+ margin: 3
3053
+ },
3054
+ filesBlock: {
3055
+ paddingHorizontal: 6,
3056
+ paddingTop: 6,
3057
+ gap: 4
3058
+ },
3059
+ textBlock: {
3060
+ paddingHorizontal: 12,
3061
+ paddingVertical: 7
3062
+ },
3063
+ // A hair less air when media / files / a quote already sit above.
3064
+ textBlockAfter: {
3065
+ paddingTop: 6
3066
+ },
3067
+ jumboBlock: {
3068
+ paddingVertical: 2
3069
+ },
3070
+ ghost: {
3071
+ color: "transparent",
3072
+ fontSize: 11
2969
3073
  },
2970
3074
  readMore: {
2971
3075
  fontSize: 12,
@@ -2975,10 +3079,36 @@ var styles15 = StyleSheet.create({
2975
3079
  metaRow: {
2976
3080
  flexDirection: "row",
2977
3081
  alignItems: "center",
3082
+ gap: 4
3083
+ },
3084
+ metaInline: {
3085
+ position: "absolute",
3086
+ right: 12,
3087
+ // Optically centers the meta against the body's last line (22px
3088
+ // line over the 7px bottom padding): 7 + (22 - 14) / 2. At 8 it
3089
+ // sat visibly low beside a one-line message.
3090
+ bottom: 11
3091
+ },
3092
+ metaOverlay: {
3093
+ position: "absolute",
3094
+ right: 6,
3095
+ bottom: 6,
3096
+ paddingHorizontal: 8,
3097
+ paddingVertical: 3,
3098
+ borderRadius: 999,
3099
+ backgroundColor: "rgba(0,0,0,0.42)"
3100
+ },
3101
+ metaBlock: {
2978
3102
  justifyContent: "flex-end",
2979
- marginTop: 6,
2980
- gap: 5,
2981
- flexShrink: 0
3103
+ paddingHorizontal: 12,
3104
+ paddingBottom: 6,
3105
+ marginTop: -2
3106
+ },
3107
+ metaBlockAfterFiles: {
3108
+ marginTop: 6
3109
+ },
3110
+ metaJumbo: {
3111
+ paddingTop: 2
2982
3112
  },
2983
3113
  meta: {
2984
3114
  fontSize: 11,
@@ -2991,7 +3121,7 @@ var styles15 = StyleSheet.create({
2991
3121
  letterSpacing: 0.1
2992
3122
  },
2993
3123
  readTick: {
2994
- marginLeft: 2,
3124
+ marginLeft: 1,
2995
3125
  marginTop: 1
2996
3126
  },
2997
3127
  actionsSlot: {
@@ -3001,20 +3131,21 @@ var styles15 = StyleSheet.create({
3001
3131
  },
3002
3132
  quoted: {
3003
3133
  borderLeftWidth: 3,
3004
- paddingLeft: 8,
3005
- paddingVertical: 4,
3006
- paddingRight: 6,
3007
- marginBottom: 6,
3008
- borderRadius: 4
3134
+ paddingLeft: 9,
3135
+ paddingVertical: 5,
3136
+ paddingRight: 8,
3137
+ margin: 4,
3138
+ marginBottom: 0,
3139
+ borderRadius: 10
3009
3140
  },
3010
3141
  quotedSender: {
3011
3142
  fontSize: 12,
3012
- fontWeight: "600",
3013
- marginBottom: 2
3143
+ fontWeight: "700",
3144
+ marginBottom: 1
3014
3145
  },
3015
3146
  quotedBody: {
3016
- fontSize: 12,
3017
- opacity: 0.85
3147
+ fontSize: 12.5,
3148
+ opacity: 0.8
3018
3149
  }
3019
3150
  });
3020
3151
  var DEFAULT_EMOJIS = ["\u{1F44D}", "\u2764\uFE0F", "\u{1F602}", "\u{1F389}", "\u{1F64F}", "\u{1F525}"];
@@ -3331,14 +3462,9 @@ function MessageRow({
3331
3462
  showSenderName,
3332
3463
  showAttachments: attachments,
3333
3464
  markdown,
3334
- actionsTrigger: actions ? /* @__PURE__ */ jsx(Pressable, { onPress: () => setActionsOpen(true), hitSlop: 8, children: /* @__PURE__ */ jsx(
3335
- PoolseIcon,
3336
- {
3337
- name: "chevron-down",
3338
- size: 14,
3339
- color: isSelf ? theme.colors.onBrand : theme.colors.ink3
3340
- }
3341
- ) }) : null
3465
+ onLongPress: () => {
3466
+ if (actions) setActionsOpen(true);
3467
+ }
3342
3468
  }
3343
3469
  )
3344
3470
  }