@poolse/react-native 2.0.12 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -7,6 +7,7 @@ var jsxRuntime = require('react/jsx-runtime');
7
7
  var reactNativeSvg = require('react-native-svg');
8
8
  var ImagePicker = require('expo-image-picker');
9
9
  var DocumentPicker = require('expo-document-picker');
10
+ var expoImageManipulator = require('expo-image-manipulator');
10
11
 
11
12
  function _interopNamespace(e) {
12
13
  if (e && e.__esModule) return e;
@@ -29,7 +30,12 @@ function _interopNamespace(e) {
29
30
  var ImagePicker__namespace = /*#__PURE__*/_interopNamespace(ImagePicker);
30
31
  var DocumentPicker__namespace = /*#__PURE__*/_interopNamespace(DocumentPicker);
31
32
 
32
- // src/PoolseInbox.tsx
33
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
34
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
35
+ }) : x)(function(x) {
36
+ if (typeof require !== "undefined") return require.apply(this, arguments);
37
+ throw Error('Dynamic require of "' + x + '" is not supported');
38
+ });
33
39
 
34
40
  // src/theme/default.ts
35
41
  var lightColors = {
@@ -1183,24 +1189,84 @@ async function uriToBlob(uri) {
1183
1189
  }
1184
1190
  return res.blob();
1185
1191
  }
1192
+ var COMPRESSION_LONG_SIDE_PX = 1600;
1193
+ var COMPRESSION_QUALITY = 0.75;
1194
+ function getImageSize(uri) {
1195
+ return new Promise((resolve, reject) => {
1196
+ reactNative.Image.getSize(uri, (width, height) => resolve({ width, height }), reject);
1197
+ });
1198
+ }
1199
+ async function compressImage({
1200
+ uri,
1201
+ width,
1202
+ height,
1203
+ filename
1204
+ }) {
1205
+ let w = width;
1206
+ let h = height;
1207
+ if (!w || !h) {
1208
+ try {
1209
+ const size = await getImageSize(uri);
1210
+ w = size.width;
1211
+ h = size.height;
1212
+ } catch {
1213
+ w = 0;
1214
+ h = 0;
1215
+ }
1216
+ }
1217
+ const longSide = Math.max(w ?? 0, h ?? 0);
1218
+ const scale = longSide > COMPRESSION_LONG_SIDE_PX ? COMPRESSION_LONG_SIDE_PX / longSide : 1;
1219
+ const actions = scale < 1 && w && h ? [{ resize: { width: Math.round(w * scale), height: Math.round(h * scale) } }] : [];
1220
+ const result = await expoImageManipulator.manipulateAsync(uri, actions, {
1221
+ compress: COMPRESSION_QUALITY,
1222
+ format: expoImageManipulator.SaveFormat.WEBP
1223
+ });
1224
+ const blob = await uriToBlob(result.uri);
1225
+ const base = (filename ?? `image-${Date.now()}`).replace(/\.[^.]+$/, "");
1226
+ return {
1227
+ blob,
1228
+ contentType: "image/webp",
1229
+ filename: `${base}.webp`
1230
+ };
1231
+ }
1186
1232
  async function defaultAdapter(kind) {
1187
1233
  if (kind === "image") {
1188
1234
  const res2 = await ImagePicker__namespace.launchImageLibraryAsync({
1189
1235
  mediaTypes: "images",
1190
- quality: 0.85
1236
+ // Quality 1 — picker hands back the source as-is and we do the
1237
+ // compression ourselves below. Letting the picker re-encode
1238
+ // first just chains two lossy passes.
1239
+ quality: 1
1191
1240
  });
1192
1241
  if (res2.canceled) return [];
1193
1242
  return Promise.all(
1194
1243
  res2.assets.map(async (asset) => {
1195
- const blob = await uriToBlob(asset.uri);
1196
- const contentType = asset.mimeType ?? blob.type ?? "image/jpeg";
1197
- return {
1198
- body: blob,
1199
- contentType,
1200
- byteSize: asset.fileSize ?? blob.size,
1201
- filename: asset.fileName ?? `image-${Date.now()}.jpg`,
1202
- _previewUri: asset.uri
1203
- };
1244
+ try {
1245
+ const compressed = await compressImage({
1246
+ uri: asset.uri,
1247
+ width: asset.width,
1248
+ height: asset.height,
1249
+ filename: asset.fileName ?? `image-${Date.now()}`
1250
+ });
1251
+ return {
1252
+ body: compressed.blob,
1253
+ contentType: compressed.contentType,
1254
+ byteSize: compressed.blob.size,
1255
+ filename: compressed.filename,
1256
+ _previewUri: asset.uri
1257
+ };
1258
+ } catch (err) {
1259
+ console.warn("[poolse/picker] image compression failed, sending original:", err);
1260
+ const blob = await uriToBlob(asset.uri);
1261
+ const contentType = asset.mimeType ?? blob.type ?? "image/jpeg";
1262
+ return {
1263
+ body: blob,
1264
+ contentType,
1265
+ byteSize: asset.fileSize ?? blob.size,
1266
+ filename: asset.fileName ?? `image-${Date.now()}.jpg`,
1267
+ _previewUri: asset.uri
1268
+ };
1269
+ }
1204
1270
  })
1205
1271
  );
1206
1272
  }
@@ -1211,6 +1277,24 @@ async function defaultAdapter(kind) {
1211
1277
  if (res.canceled) return [];
1212
1278
  return Promise.all(
1213
1279
  res.assets.map(async (asset) => {
1280
+ const isImage = (asset.mimeType ?? "").startsWith("image/");
1281
+ if (isImage) {
1282
+ try {
1283
+ const compressed = await compressImage({
1284
+ uri: asset.uri,
1285
+ filename: asset.name
1286
+ });
1287
+ return {
1288
+ body: compressed.blob,
1289
+ contentType: compressed.contentType,
1290
+ byteSize: compressed.blob.size,
1291
+ filename: compressed.filename,
1292
+ _previewUri: asset.uri
1293
+ };
1294
+ } catch (err) {
1295
+ console.warn("[poolse/picker] image compression failed, sending original:", err);
1296
+ }
1297
+ }
1214
1298
  const blob = await uriToBlob(asset.uri);
1215
1299
  const contentType = asset.mimeType ?? blob.type ?? "application/octet-stream";
1216
1300
  return {
@@ -1251,6 +1335,51 @@ var styles6 = reactNative.StyleSheet.create({
1251
1335
  fontWeight: "500"
1252
1336
  }
1253
1337
  });
1338
+ function DaySeparator({ label }) {
1339
+ const theme = usePoolseTheme();
1340
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles7.wrap, children: /* @__PURE__ */ jsxRuntime.jsx(
1341
+ reactNative.View,
1342
+ {
1343
+ style: [
1344
+ styles7.chip,
1345
+ {
1346
+ backgroundColor: theme.colors.surface2,
1347
+ borderColor: theme.colors.border
1348
+ }
1349
+ ],
1350
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1351
+ reactNative.Text,
1352
+ {
1353
+ style: [
1354
+ styles7.label,
1355
+ {
1356
+ color: theme.colors.ink2,
1357
+ fontFamily: theme.type.fontBody
1358
+ }
1359
+ ],
1360
+ children: label
1361
+ }
1362
+ )
1363
+ }
1364
+ ) });
1365
+ }
1366
+ var styles7 = reactNative.StyleSheet.create({
1367
+ wrap: {
1368
+ alignItems: "center",
1369
+ marginVertical: 8
1370
+ },
1371
+ chip: {
1372
+ paddingHorizontal: 12,
1373
+ paddingVertical: 4,
1374
+ borderRadius: 999,
1375
+ borderWidth: reactNative.StyleSheet.hairlineWidth
1376
+ },
1377
+ label: {
1378
+ fontSize: 11,
1379
+ fontWeight: "600",
1380
+ letterSpacing: 0.3
1381
+ }
1382
+ });
1254
1383
  function UploadQueueStrip() {
1255
1384
  const theme = usePoolseTheme();
1256
1385
  const upload = useSharedUpload();
@@ -1261,8 +1390,8 @@ function UploadQueueStrip() {
1261
1390
  {
1262
1391
  horizontal: true,
1263
1392
  showsHorizontalScrollIndicator: false,
1264
- contentContainerStyle: styles7.scroll,
1265
- style: [styles7.scrollStyle, { marginBottom: 8 }],
1393
+ contentContainerStyle: styles8.scroll,
1394
+ style: [styles8.scrollStyle, { marginBottom: 8 }],
1266
1395
  children: upload.queue.map((item) => {
1267
1396
  const isError = item.status === "error";
1268
1397
  const isUploading = item.status === "uploading" || item.status === "pending";
@@ -1273,7 +1402,7 @@ function UploadQueueStrip() {
1273
1402
  if (item.status === "ready" || isError) upload.remove(item.localId);
1274
1403
  else upload.cancel(item.localId);
1275
1404
  };
1276
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles7.tileWrap, children: [
1405
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles8.tileWrap, children: [
1277
1406
  isImage && previewUri ? /* @__PURE__ */ jsxRuntime.jsx(ImageTile, { uri: previewUri, uploading: isUploading, error: isError }) : /* @__PURE__ */ jsxRuntime.jsx(
1278
1407
  FileTile,
1279
1408
  {
@@ -1288,13 +1417,13 @@ function UploadQueueStrip() {
1288
1417
  {
1289
1418
  onPress: handleClose,
1290
1419
  hitSlop: 8,
1291
- style: [styles7.closeBtn, { backgroundColor: theme.colors.ink }],
1420
+ style: [styles8.closeBtn, { backgroundColor: theme.colors.ink }],
1292
1421
  accessibilityLabel: isError ? "Remove failed upload" : "Cancel upload",
1293
1422
  children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "close", size: 11, color: theme.colors.onBrand })
1294
1423
  }
1295
1424
  ) : null,
1296
- isUploading ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles7.progressOverlay, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles7.spinner, { borderColor: theme.colors.onBrand }] }) }) : null,
1297
- isError ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles7.errorOverlay, { backgroundColor: "rgba(229,72,77,0.85)" }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles7.errorText, { color: "#fff" }], children: "Failed" }) }) : null
1425
+ isUploading ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles8.progressOverlay, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles8.spinner, { borderColor: theme.colors.onBrand }] }) }) : null,
1426
+ isError ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles8.errorOverlay, { backgroundColor: "rgba(229,72,77,0.85)" }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles8.errorText, { color: "#fff" }], children: "Failed" }) }) : null
1298
1427
  ] }, item.localId);
1299
1428
  })
1300
1429
  }
@@ -1307,7 +1436,7 @@ function ImageTile({ uri, uploading, error }) {
1307
1436
  {
1308
1437
  source: { uri },
1309
1438
  style: [
1310
- styles7.imageTile,
1439
+ styles8.imageTile,
1311
1440
  {
1312
1441
  backgroundColor: theme.colors.surface2,
1313
1442
  borderColor: error ? theme.colors.error : "transparent",
@@ -1329,24 +1458,24 @@ function FileTile({
1329
1458
  reactNative.View,
1330
1459
  {
1331
1460
  style: [
1332
- styles7.fileTile,
1461
+ styles8.fileTile,
1333
1462
  {
1334
1463
  backgroundColor: theme.colors.surface2,
1335
1464
  borderColor: error ? theme.colors.error : uploading ? theme.colors.brand : theme.colors.border
1336
1465
  }
1337
1466
  ],
1338
1467
  children: [
1339
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles7.fileIconBox, { backgroundColor: theme.colors.brandSoft }], children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "attachment", size: 18, color: theme.colors.brand }) }),
1340
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles7.fileInfo, children: [
1468
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles8.fileIconBox, { backgroundColor: theme.colors.brandSoft }], children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "attachment", size: 18, color: theme.colors.brand }) }),
1469
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles8.fileInfo, children: [
1341
1470
  /* @__PURE__ */ jsxRuntime.jsx(
1342
1471
  reactNative.Text,
1343
1472
  {
1344
- style: [styles7.fileName, { color: theme.colors.ink, fontFamily: theme.type.fontBody }],
1473
+ style: [styles8.fileName, { color: theme.colors.ink, fontFamily: theme.type.fontBody }],
1345
1474
  numberOfLines: 1,
1346
1475
  children: filename
1347
1476
  }
1348
1477
  ),
1349
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles7.fileSize, { color: theme.colors.ink3 }], children: formatBytes(size) })
1478
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles8.fileSize, { color: theme.colors.ink3 }], children: formatBytes(size) })
1350
1479
  ] })
1351
1480
  ]
1352
1481
  }
@@ -1360,7 +1489,7 @@ function formatBytes(n) {
1360
1489
  return `${(n / 1024 / 1024 / 1024).toFixed(1)} GB`;
1361
1490
  }
1362
1491
  var TILE = 64;
1363
- var styles7 = reactNative.StyleSheet.create({
1492
+ var styles8 = reactNative.StyleSheet.create({
1364
1493
  scrollStyle: {
1365
1494
  overflow: "visible"
1366
1495
  },
@@ -1460,6 +1589,22 @@ var styles7 = reactNative.StyleSheet.create({
1460
1589
  fontWeight: "700"
1461
1590
  }
1462
1591
  });
1592
+ var FALLBACK = {
1593
+ top: reactNative.Platform.OS === "ios" ? 50 : 24,
1594
+ bottom: reactNative.Platform.OS === "ios" ? 34 : 0,
1595
+ left: 0,
1596
+ right: 0
1597
+ };
1598
+ var realHook = null;
1599
+ try {
1600
+ const mod = __require("react-native-safe-area-context");
1601
+ if (mod && typeof mod.useSafeAreaInsets === "function") {
1602
+ realHook = mod.useSafeAreaInsets;
1603
+ }
1604
+ } catch {
1605
+ }
1606
+ var useFallback = () => FALLBACK;
1607
+ var useSafeInsets = realHook ?? useFallback;
1463
1608
  var MessageComposer = react.forwardRef(
1464
1609
  function MessageComposer2({
1465
1610
  onSend,
@@ -1471,13 +1616,32 @@ var MessageComposer = react.forwardRef(
1471
1616
  labelFor,
1472
1617
  attachments = true,
1473
1618
  onAttachPress,
1474
- keyboardOffset: _keyboardOffset = 0
1619
+ keyboardOffset: _keyboardOffset = 0,
1620
+ editingMessage = null,
1621
+ onCancelEdit,
1622
+ onSaveEdit
1475
1623
  }, ref) {
1476
1624
  const theme = usePoolseTheme();
1477
1625
  const inputRef = react.useRef(null);
1478
1626
  const [value, setValue] = react.useState("");
1479
1627
  const [sending, setSending] = react.useState(false);
1628
+ const [keyboardOpen, setKeyboardOpen] = react.useState(false);
1480
1629
  const upload = useSharedUpload();
1630
+ const insets = useSafeInsets();
1631
+ react.useEffect(() => {
1632
+ const show = reactNative.Keyboard.addListener(
1633
+ reactNative.Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow",
1634
+ () => setKeyboardOpen(true)
1635
+ );
1636
+ const hide = reactNative.Keyboard.addListener(
1637
+ reactNative.Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide",
1638
+ () => setKeyboardOpen(false)
1639
+ );
1640
+ return () => {
1641
+ show.remove();
1642
+ hide.remove();
1643
+ };
1644
+ }, []);
1481
1645
  react.useImperativeHandle(
1482
1646
  ref,
1483
1647
  () => ({
@@ -1496,36 +1660,65 @@ var MessageComposer = react.forwardRef(
1496
1660
  const isUploading = upload.queue.some(
1497
1661
  (item) => item.status === "uploading" || item.status === "pending"
1498
1662
  );
1499
- const submitDisabled = disabled || sending || isUploading || value.trim().length === 0 && readyAttachmentIds.length === 0;
1663
+ const previousDraftRef = react.useRef(null);
1664
+ const editingIdRef = react.useRef(null);
1665
+ react.useEffect(() => {
1666
+ const id = editingMessage?.id ?? null;
1667
+ if (id === editingIdRef.current) return;
1668
+ if (id) {
1669
+ previousDraftRef.current = value;
1670
+ setValue(editingMessage?.body ?? "");
1671
+ inputRef.current?.focus();
1672
+ } else {
1673
+ setValue(previousDraftRef.current ?? "");
1674
+ previousDraftRef.current = null;
1675
+ }
1676
+ editingIdRef.current = id;
1677
+ }, [editingMessage?.id, editingMessage?.body]);
1678
+ const isEditing = !!editingMessage;
1679
+ const editChanged = isEditing && value.trim() !== (editingMessage?.body?.trim() ?? "");
1680
+ const submitDisabled = disabled || sending || isUploading || value.trim().length === 0 || (isEditing ? !editChanged : value.trim().length === 0 && readyAttachmentIds.length === 0);
1500
1681
  const handleSend = async () => {
1501
1682
  if (submitDisabled) return;
1502
1683
  const body = value.trim();
1503
- const replyId = replyingTo?.id;
1504
- const attachmentIds = readyAttachmentIds;
1505
- setValue("");
1506
- upload.reset();
1507
1684
  setSending(true);
1508
1685
  try {
1509
- await onSend(body, {
1510
- ...replyId ? { quoted_message_id: replyId } : {},
1511
- ...attachmentIds.length > 0 ? { attachment_ids: attachmentIds } : {}
1512
- });
1686
+ if (isEditing && onSaveEdit) {
1687
+ await onSaveEdit(body);
1688
+ } else {
1689
+ const replyId = replyingTo?.id;
1690
+ const attachmentIds = readyAttachmentIds;
1691
+ setValue("");
1692
+ upload.reset();
1693
+ await onSend(body, {
1694
+ ...replyId ? { quoted_message_id: replyId } : {},
1695
+ ...attachmentIds.length > 0 ? { attachment_ids: attachmentIds } : {}
1696
+ });
1697
+ }
1513
1698
  } finally {
1514
1699
  setSending(false);
1515
1700
  }
1516
1701
  };
1702
+ const paddingBottom = keyboardOpen ? 10 : Math.max(10, insets.bottom);
1517
1703
  return /* @__PURE__ */ jsxRuntime.jsxs(
1518
1704
  reactNative.View,
1519
1705
  {
1520
1706
  style: [
1521
- styles8.wrap,
1707
+ styles9.wrap,
1522
1708
  {
1523
1709
  backgroundColor: theme.colors.surface,
1524
- borderTopColor: theme.colors.border
1710
+ borderTopColor: theme.colors.border,
1711
+ paddingBottom
1525
1712
  }
1526
1713
  ],
1527
1714
  children: [
1528
- replyingTo ? /* @__PURE__ */ jsxRuntime.jsx(
1715
+ isEditing ? /* @__PURE__ */ jsxRuntime.jsx(
1716
+ EditChip,
1717
+ {
1718
+ message: editingMessage,
1719
+ ...onCancelEdit ? { onCancel: onCancelEdit } : {}
1720
+ }
1721
+ ) : replyingTo ? /* @__PURE__ */ jsxRuntime.jsx(
1529
1722
  ReplyChip,
1530
1723
  {
1531
1724
  message: replyingTo,
@@ -1533,14 +1726,14 @@ var MessageComposer = react.forwardRef(
1533
1726
  ...onCancelReply ? { onCancel: onCancelReply } : {}
1534
1727
  }
1535
1728
  ) : null,
1536
- attachments ? /* @__PURE__ */ jsxRuntime.jsx(UploadQueueStrip, {}) : null,
1537
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles8.row, children: [
1729
+ attachments && !isEditing ? /* @__PURE__ */ jsxRuntime.jsx(UploadQueueStrip, {}) : null,
1730
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles9.row, children: [
1538
1731
  attachments && onAttachPress ? /* @__PURE__ */ jsxRuntime.jsx(
1539
1732
  reactNative.Pressable,
1540
1733
  {
1541
1734
  onPress: onAttachPress,
1542
1735
  hitSlop: 8,
1543
- style: styles8.iconBtn,
1736
+ style: styles9.iconBtn,
1544
1737
  accessibilityLabel: "Attach file",
1545
1738
  children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "attachment", size: 22, color: theme.colors.ink2 })
1546
1739
  }
@@ -1559,7 +1752,7 @@ var MessageComposer = react.forwardRef(
1559
1752
  multiline: true,
1560
1753
  editable: !disabled,
1561
1754
  style: [
1562
- styles8.input,
1755
+ styles9.input,
1563
1756
  {
1564
1757
  color: theme.colors.ink,
1565
1758
  backgroundColor: theme.colors.surface2,
@@ -1576,7 +1769,7 @@ var MessageComposer = react.forwardRef(
1576
1769
  onPress: handleSend,
1577
1770
  disabled: submitDisabled,
1578
1771
  style: [
1579
- styles8.sendBtn,
1772
+ styles9.sendBtn,
1580
1773
  {
1581
1774
  backgroundColor: submitDisabled ? theme.colors.ink3 : theme.colors.brand,
1582
1775
  opacity: submitDisabled ? 0.6 : 1
@@ -1592,6 +1785,28 @@ var MessageComposer = react.forwardRef(
1592
1785
  );
1593
1786
  }
1594
1787
  );
1788
+ function EditChip({ message, onCancel }) {
1789
+ const theme = usePoolseTheme();
1790
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1791
+ reactNative.View,
1792
+ {
1793
+ style: [
1794
+ styles9.replyChip,
1795
+ {
1796
+ backgroundColor: theme.colors.surface2,
1797
+ borderLeftColor: theme.colors.brand
1798
+ }
1799
+ ],
1800
+ children: [
1801
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { flex: 1 }, children: [
1802
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles9.replyName, { color: theme.colors.brand }], children: "Editing message" }),
1803
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles9.replyBody, { color: theme.colors.ink2 }], numberOfLines: 1, children: message.body ?? "\u2026" })
1804
+ ] }),
1805
+ onCancel ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: onCancel, hitSlop: 8, style: styles9.replyCancel, children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "close", size: 16, color: theme.colors.ink2 }) }) : null
1806
+ ]
1807
+ }
1808
+ );
1809
+ }
1595
1810
  function ReplyChip({
1596
1811
  message,
1597
1812
  labelFor,
@@ -1604,7 +1819,7 @@ function ReplyChip({
1604
1819
  reactNative.View,
1605
1820
  {
1606
1821
  style: [
1607
- styles8.replyChip,
1822
+ styles9.replyChip,
1608
1823
  {
1609
1824
  backgroundColor: theme.colors.surface2,
1610
1825
  borderLeftColor: theme.colors.brand
@@ -1612,27 +1827,26 @@ function ReplyChip({
1612
1827
  ],
1613
1828
  children: [
1614
1829
  /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { flex: 1 }, children: [
1615
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { style: [styles8.replyName, { color: theme.colors.brand }], children: [
1830
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { style: [styles9.replyName, { color: theme.colors.brand }], children: [
1616
1831
  "Replying to ",
1617
1832
  senderName
1618
1833
  ] }),
1619
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles8.replyBody, { color: theme.colors.ink2 }], numberOfLines: 1, children: message.body ?? "\u2026" })
1834
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles9.replyBody, { color: theme.colors.ink2 }], numberOfLines: 1, children: message.body ?? "\u2026" })
1620
1835
  ] }),
1621
- onCancel ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: onCancel, hitSlop: 8, style: styles8.replyCancel, children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "close", size: 16, color: theme.colors.ink2 }) }) : null
1836
+ onCancel ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: onCancel, hitSlop: 8, style: styles9.replyCancel, children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "close", size: 16, color: theme.colors.ink2 }) }) : null
1622
1837
  ]
1623
1838
  }
1624
1839
  );
1625
1840
  }
1626
- var styles8 = reactNative.StyleSheet.create({
1841
+ var styles9 = reactNative.StyleSheet.create({
1627
1842
  wrap: {
1628
1843
  borderTopWidth: reactNative.StyleSheet.hairlineWidth,
1629
1844
  paddingTop: 8,
1630
- paddingBottom: 10,
1631
1845
  paddingHorizontal: 12
1632
- // Note: no safe-area inset here on purpose. Wrap your chat
1633
- // surface in <SafeAreaView> (or <SafeAreaView edges={['bottom']}>)
1634
- // at the app root so the home-indicator clearance is handled
1635
- // once. Doing it here too produces double padding.
1846
+ // paddingBottom is applied inline — it follows the keyboard state
1847
+ // (10 px while open, max(10, safeBottomInset) while closed) so the
1848
+ // send/attach buttons stay clear of the home indicator without
1849
+ // double-padding above the keyboard.
1636
1850
  },
1637
1851
  row: {
1638
1852
  flexDirection: "row",
@@ -1721,11 +1935,11 @@ function MentionInput({ conversationId, onSend, ...composerProps }) {
1721
1935
  transparent: true,
1722
1936
  animationType: "slide",
1723
1937
  onRequestClose: () => setPickerOpen(false),
1724
- children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { style: styles9.backdrop, onPress: () => setPickerOpen(false), children: /* @__PURE__ */ jsxRuntime.jsxs(
1938
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { style: styles10.backdrop, onPress: () => setPickerOpen(false), children: /* @__PURE__ */ jsxRuntime.jsxs(
1725
1939
  reactNative.View,
1726
1940
  {
1727
1941
  style: [
1728
- styles9.sheet,
1942
+ styles10.sheet,
1729
1943
  {
1730
1944
  backgroundColor: theme.colors.surface,
1731
1945
  borderTopLeftRadius: theme.radii.lg,
@@ -1736,7 +1950,7 @@ function MentionInput({ conversationId, onSend, ...composerProps }) {
1736
1950
  /* @__PURE__ */ jsxRuntime.jsx(
1737
1951
  reactNative.Text,
1738
1952
  {
1739
- style: [styles9.title, { color: theme.colors.ink, fontFamily: theme.type.fontBody }],
1953
+ style: [styles10.title, { color: theme.colors.ink, fontFamily: theme.type.fontBody }],
1740
1954
  children: "Mention a member"
1741
1955
  }
1742
1956
  ),
@@ -1767,12 +1981,12 @@ function MentionInput({ conversationId, onSend, ...composerProps }) {
1767
1981
  }
1768
1982
  function MentionRow({ member, onPress }) {
1769
1983
  const theme = usePoolseTheme();
1770
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Pressable, { onPress, style: styles9.row, children: [
1984
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Pressable, { onPress, style: styles10.row, children: [
1771
1985
  /* @__PURE__ */ jsxRuntime.jsx(Avatar, { src: null, name: member.external_id, seed: member.external_id, size: "sm" }),
1772
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles9.rowText, { color: theme.colors.ink, fontFamily: theme.type.fontBody }], children: member.external_id })
1986
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles10.rowText, { color: theme.colors.ink, fontFamily: theme.type.fontBody }], children: member.external_id })
1773
1987
  ] });
1774
1988
  }
1775
- var styles9 = reactNative.StyleSheet.create({
1989
+ var styles10 = reactNative.StyleSheet.create({
1776
1990
  backdrop: {
1777
1991
  flex: 1,
1778
1992
  backgroundColor: "rgba(0,0,0,0.35)",
@@ -1799,138 +2013,134 @@ var styles9 = reactNative.StyleSheet.create({
1799
2013
  fontSize: 14
1800
2014
  }
1801
2015
  });
2016
+ var AT_BOTTOM_THRESHOLD = 32;
1802
2017
  function MessageList({
1803
2018
  messages,
2019
+ items,
1804
2020
  loading = false,
1805
2021
  hasMore = false,
1806
2022
  onLoadMore,
1807
2023
  renderItem,
1808
2024
  ListHeader = null,
1809
2025
  ListFooter = null,
1810
- emptyState
2026
+ emptyState,
2027
+ meId = null
1811
2028
  }) {
1812
2029
  const theme = usePoolseTheme();
1813
- const data = [...messages].reverse();
2030
+ const flatListRef = react.useRef(null);
2031
+ const [atBottom, setAtBottom] = react.useState(true);
2032
+ const [newCount, setNewCount] = react.useState(0);
2033
+ const lastTailIdRef = react.useRef(null);
2034
+ const sourceItems = items ?? messages.map((msg) => ({ kind: "msg", msg }));
2035
+ const data = [...sourceItems].reverse();
2036
+ react.useEffect(() => {
2037
+ const tail = messages[messages.length - 1];
2038
+ if (!tail) return;
2039
+ const tailId = tail.id;
2040
+ if (tailId === lastTailIdRef.current) return;
2041
+ const isFirstSet = lastTailIdRef.current === null;
2042
+ lastTailIdRef.current = tailId;
2043
+ if (atBottom || isFirstSet) {
2044
+ setNewCount(0);
2045
+ return;
2046
+ }
2047
+ if (meId !== null && tail.sender_id === meId) return;
2048
+ setNewCount((n) => n + 1);
2049
+ }, [messages, atBottom, meId]);
2050
+ const handleScroll = react.useCallback((e) => {
2051
+ const y = e.nativeEvent.contentOffset.y;
2052
+ const next = y <= AT_BOTTOM_THRESHOLD;
2053
+ setAtBottom((prev) => prev === next ? prev : next);
2054
+ if (next) setNewCount(0);
2055
+ }, []);
2056
+ const scrollToBottom = react.useCallback(() => {
2057
+ flatListRef.current?.scrollToOffset({ offset: 0, animated: true });
2058
+ setNewCount(0);
2059
+ }, []);
1814
2060
  const handleRender = react.useCallback(
1815
- ({ item, index }) => renderItem(item, messages.length - 1 - index),
1816
- [renderItem, messages.length]
2061
+ ({ item, index }) => {
2062
+ if (item.kind === "separator") return item.node;
2063
+ const sourceIndex = data.length - 1 - index;
2064
+ return renderItem(item.msg, sourceIndex);
2065
+ },
2066
+ [renderItem, data.length]
1817
2067
  );
1818
2068
  if (loading && messages.length === 0) {
1819
- return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles10.placeholder, { backgroundColor: theme.colors.paper }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: theme.colors.brand }) });
2069
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles11.placeholder, { backgroundColor: theme.colors.paper }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: theme.colors.brand }) });
1820
2070
  }
1821
2071
  if (messages.length === 0) {
1822
- return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles10.placeholder, { backgroundColor: theme.colors.paper }], children: typeof emptyState === "string" ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: { color: theme.colors.ink3, fontFamily: theme.type.fontBody }, children: emptyState }) : emptyState ?? null });
2072
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles11.placeholder, { backgroundColor: theme.colors.paper }], children: typeof emptyState === "string" ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: { color: theme.colors.ink3, fontFamily: theme.type.fontBody }, children: emptyState }) : emptyState ?? null });
1823
2073
  }
1824
- return /* @__PURE__ */ jsxRuntime.jsx(
1825
- reactNative.FlatList,
1826
- {
1827
- inverted: true,
1828
- data,
1829
- keyExtractor: (item) => item.id,
1830
- renderItem: handleRender,
1831
- onEndReached: hasMore ? onLoadMore : void 0,
1832
- onEndReachedThreshold: 0.3,
1833
- ListHeaderComponent: ListHeader,
1834
- ListFooterComponent: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { children: [
1835
- hasMore && loading ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: theme.colors.ink3, style: { marginVertical: 8 } }) : null,
1836
- ListFooter
1837
- ] }),
1838
- style: { backgroundColor: theme.colors.paper },
1839
- contentContainerStyle: { paddingVertical: 8 }
1840
- }
1841
- );
2074
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles11.root, children: [
2075
+ /* @__PURE__ */ jsxRuntime.jsx(
2076
+ reactNative.FlatList,
2077
+ {
2078
+ ref: flatListRef,
2079
+ inverted: true,
2080
+ data,
2081
+ keyExtractor: (item) => item.kind === "msg" ? item.msg.id : item.id,
2082
+ renderItem: handleRender,
2083
+ onEndReached: hasMore ? onLoadMore : void 0,
2084
+ onEndReachedThreshold: 0.3,
2085
+ onScroll: handleScroll,
2086
+ scrollEventThrottle: 32,
2087
+ ListHeaderComponent: ListHeader,
2088
+ ListFooterComponent: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { children: [
2089
+ hasMore && loading ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: theme.colors.ink3, style: { marginVertical: 8 } }) : null,
2090
+ ListFooter
2091
+ ] }),
2092
+ style: { backgroundColor: theme.colors.paper },
2093
+ contentContainerStyle: { paddingVertical: 8 }
2094
+ }
2095
+ ),
2096
+ !atBottom && newCount > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(
2097
+ reactNative.Pressable,
2098
+ {
2099
+ onPress: scrollToBottom,
2100
+ style: [
2101
+ styles11.jumpButton,
2102
+ {
2103
+ backgroundColor: theme.colors.brand
2104
+ },
2105
+ theme.shadows.sm
2106
+ ],
2107
+ accessibilityLabel: `${newCount} new ${newCount === 1 ? "message" : "messages"}, jump to latest`,
2108
+ children: [
2109
+ /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "chevron-down", size: 14, color: theme.colors.onBrand }),
2110
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { style: [styles11.jumpText, { color: theme.colors.onBrand }], children: [
2111
+ newCount,
2112
+ " new ",
2113
+ newCount === 1 ? "message" : "messages"
2114
+ ] })
2115
+ ]
2116
+ }
2117
+ ) : null
2118
+ ] });
1842
2119
  }
1843
- var styles10 = reactNative.StyleSheet.create({
2120
+ var styles11 = reactNative.StyleSheet.create({
2121
+ root: {
2122
+ flex: 1
2123
+ },
1844
2124
  placeholder: {
1845
2125
  flex: 1,
1846
2126
  alignItems: "center",
1847
2127
  justifyContent: "center",
1848
2128
  padding: 24
1849
- }
1850
- });
1851
- function EditableMessageBubble({
1852
- message,
1853
- currentUserId,
1854
- onCancel,
1855
- onSave
1856
- }) {
1857
- const theme = usePoolseTheme();
1858
- const isSelf = currentUserId !== null && message.sender_id === currentUserId;
1859
- const [value, setValue] = react.useState(message.body ?? "");
1860
- const [saving, setSaving] = react.useState(false);
1861
- const submit = async () => {
1862
- if (saving || !value.trim()) return;
1863
- setSaving(true);
1864
- try {
1865
- await onSave(value.trim());
1866
- } finally {
1867
- setSaving(false);
1868
- }
1869
- };
1870
- const bg = isSelf ? theme.colors.selfBubble : theme.colors.otherBubble;
1871
- const ink = isSelf ? theme.colors.selfBubbleText : theme.colors.otherBubbleText;
1872
- return /* @__PURE__ */ jsxRuntime.jsx(
1873
- reactNative.View,
1874
- {
1875
- style: [
1876
- styles11.wrap,
1877
- {
1878
- alignSelf: isSelf ? "flex-end" : "flex-start",
1879
- maxWidth: "80%"
1880
- }
1881
- ],
1882
- children: /* @__PURE__ */ jsxRuntime.jsxs(
1883
- reactNative.View,
1884
- {
1885
- style: [
1886
- styles11.bubble,
1887
- {
1888
- backgroundColor: bg,
1889
- borderRadius: theme.radii.bubble,
1890
- paddingHorizontal: theme.spacing.md,
1891
- paddingVertical: theme.spacing.sm
1892
- },
1893
- theme.shadows.sm
1894
- ],
1895
- children: [
1896
- /* @__PURE__ */ jsxRuntime.jsx(
1897
- reactNative.TextInput,
1898
- {
1899
- value,
1900
- onChangeText: setValue,
1901
- multiline: true,
1902
- autoFocus: true,
1903
- style: {
1904
- color: ink,
1905
- fontFamily: theme.type.fontBody,
1906
- fontSize: theme.type.bodySize,
1907
- minHeight: 24
1908
- }
1909
- }
1910
- ),
1911
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles11.actions, children: [
1912
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: onCancel, hitSlop: 6, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles11.btn, { color: ink, opacity: 0.7 }], children: "Cancel" }) }),
1913
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: submit, disabled: saving, hitSlop: 6, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles11.btn, { color: ink, fontWeight: "700" }], children: saving ? "Saving\u2026" : "Save" }) })
1914
- ] })
1915
- ]
1916
- }
1917
- )
1918
- }
1919
- );
1920
- }
1921
- var styles11 = reactNative.StyleSheet.create({
1922
- wrap: {
1923
- marginBottom: 4
1924
2129
  },
1925
- bubble: {},
1926
- actions: {
2130
+ jumpButton: {
2131
+ position: "absolute",
2132
+ alignSelf: "center",
2133
+ bottom: 12,
1927
2134
  flexDirection: "row",
1928
- justifyContent: "flex-end",
1929
- gap: 12,
1930
- marginTop: 6
2135
+ alignItems: "center",
2136
+ gap: 6,
2137
+ paddingHorizontal: 14,
2138
+ paddingVertical: 8,
2139
+ borderRadius: 999
1931
2140
  },
1932
- btn: {
1933
- fontSize: 13
2141
+ jumpText: {
2142
+ fontSize: 13,
2143
+ fontWeight: "600"
1934
2144
  }
1935
2145
  });
1936
2146
  var DEFAULT_REACTION_EMOJIS = ["\u{1F44D}", "\u2764\uFE0F", "\u{1F602}", "\u{1F389}", "\u{1F64F}", "\u{1F525}"];
@@ -2393,6 +2603,77 @@ var styles14 = reactNative.StyleSheet.create({
2393
2603
  borderRadius: 20
2394
2604
  }
2395
2605
  });
2606
+ var MarkdownModule;
2607
+ var warned = false;
2608
+ function loadMarkdown() {
2609
+ if (MarkdownModule === void 0) {
2610
+ try {
2611
+ MarkdownModule = __require("react-native-markdown-display");
2612
+ } catch {
2613
+ MarkdownModule = null;
2614
+ }
2615
+ }
2616
+ const fn = MarkdownModule?.default;
2617
+ if (!fn) {
2618
+ if (!warned) {
2619
+ warned = true;
2620
+ console.warn(
2621
+ "[@poolse/react-native] Markdown rendering requires `react-native-markdown-display`. Install it to enable bold/italic/code/links in message bubbles. Falling back to plain text."
2622
+ );
2623
+ }
2624
+ return null;
2625
+ }
2626
+ return fn;
2627
+ }
2628
+ function renderMarkdown(body, args) {
2629
+ const Markdown = loadMarkdown();
2630
+ if (!Markdown) return null;
2631
+ const baseText = {
2632
+ color: args.textColor,
2633
+ fontFamily: args.fontFamily,
2634
+ fontSize: args.fontSize,
2635
+ lineHeight: args.lineHeight
2636
+ };
2637
+ const style = {
2638
+ body: baseText,
2639
+ paragraph: { ...baseText, marginTop: 0, marginBottom: 0 },
2640
+ text: baseText,
2641
+ strong: { ...baseText, fontWeight: "700" },
2642
+ em: { ...baseText, fontStyle: "italic" },
2643
+ s: { ...baseText, textDecorationLine: "line-through" },
2644
+ link: { color: args.linkColor, textDecorationLine: "underline" },
2645
+ code_inline: {
2646
+ ...baseText,
2647
+ backgroundColor: "rgba(127,127,127,0.18)",
2648
+ borderRadius: 4,
2649
+ paddingHorizontal: 4
2650
+ },
2651
+ code_block: {
2652
+ ...baseText,
2653
+ backgroundColor: "rgba(127,127,127,0.18)",
2654
+ padding: 8,
2655
+ borderRadius: 6
2656
+ },
2657
+ fence: {
2658
+ ...baseText,
2659
+ backgroundColor: "rgba(127,127,127,0.18)",
2660
+ padding: 8,
2661
+ borderRadius: 6
2662
+ },
2663
+ bullet_list: { marginTop: 2, marginBottom: 2 },
2664
+ ordered_list: { marginTop: 2, marginBottom: 2 },
2665
+ list_item: baseText,
2666
+ blockquote: {
2667
+ ...baseText,
2668
+ borderLeftWidth: 3,
2669
+ borderLeftColor: args.linkColor,
2670
+ paddingLeft: 8,
2671
+ marginVertical: 4,
2672
+ opacity: 0.85
2673
+ }
2674
+ };
2675
+ return react.createElement(Markdown, { style, onLinkPress: args.onLinkPress }, body);
2676
+ }
2396
2677
  function MessageBubble({
2397
2678
  message,
2398
2679
  currentUserId,
@@ -2403,7 +2684,8 @@ function MessageBubble({
2403
2684
  maxBodyLength = 0,
2404
2685
  showSenderName = false,
2405
2686
  actionsTrigger,
2406
- showAttachments = true
2687
+ showAttachments = true,
2688
+ markdown = true
2407
2689
  }) {
2408
2690
  const theme = usePoolseTheme();
2409
2691
  const { width: screenWidth } = reactNative.useWindowDimensions();
@@ -2488,22 +2770,53 @@ function MessageBubble({
2488
2770
  ) : null,
2489
2771
  renderAttachments && images.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(ImageMosaic, { images, maxWidth: contentMaxWidth }) : null,
2490
2772
  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,
2491
- body.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(
2492
- reactNative.Text,
2493
- {
2494
- style: [
2495
- styles15.body,
2773
+ body.length > 0 ? (() => {
2774
+ if (isDeleted) {
2775
+ return /* @__PURE__ */ jsxRuntime.jsx(
2776
+ reactNative.Text,
2496
2777
  {
2497
- color: isDeleted ? theme.colors.ink3 : textColor,
2498
- fontFamily: theme.type.fontBody,
2499
- fontSize: theme.type.bodySize,
2500
- lineHeight: theme.type.lineHeight,
2501
- fontStyle: isDeleted ? "italic" : "normal"
2778
+ style: [
2779
+ styles15.body,
2780
+ {
2781
+ color: theme.colors.ink3,
2782
+ fontFamily: theme.type.fontBody,
2783
+ fontSize: theme.type.bodySize,
2784
+ lineHeight: theme.type.lineHeight,
2785
+ fontStyle: "italic"
2786
+ }
2787
+ ],
2788
+ children: "Message deleted"
2502
2789
  }
2503
- ],
2504
- children: isDeleted ? "Message deleted" : truncated
2790
+ );
2505
2791
  }
2506
- ) : null,
2792
+ const md = markdown ? renderMarkdown(truncated, {
2793
+ textColor,
2794
+ linkColor: isSelf ? theme.colors.onBrand : theme.colors.brand,
2795
+ fontFamily: theme.type.fontBody,
2796
+ fontSize: theme.type.bodySize,
2797
+ lineHeight: theme.type.lineHeight,
2798
+ onLinkPress: (url) => {
2799
+ void reactNative.Linking.openURL(url).catch(() => void 0);
2800
+ return false;
2801
+ }
2802
+ }) : null;
2803
+ if (md) return md;
2804
+ return /* @__PURE__ */ jsxRuntime.jsx(
2805
+ reactNative.Text,
2806
+ {
2807
+ style: [
2808
+ styles15.body,
2809
+ {
2810
+ color: textColor,
2811
+ fontFamily: theme.type.fontBody,
2812
+ fontSize: theme.type.bodySize,
2813
+ lineHeight: theme.type.lineHeight
2814
+ }
2815
+ ],
2816
+ children: truncated
2817
+ }
2818
+ );
2819
+ })() : null,
2507
2820
  maxBodyLength > 0 && body.length > maxBodyLength ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: () => setExpanded((e) => !e), children: /* @__PURE__ */ jsxRuntime.jsx(
2508
2821
  reactNative.Text,
2509
2822
  {
@@ -2596,9 +2909,9 @@ function formatTime(iso) {
2596
2909
  try {
2597
2910
  const d = new Date(iso);
2598
2911
  const now = /* @__PURE__ */ new Date();
2599
- const sameDay = isSameDay(d, now);
2912
+ const sameDay2 = isSameDay(d, now);
2600
2913
  const time = d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
2601
- if (sameDay) return time;
2914
+ if (sameDay2) return time;
2602
2915
  const yesterday = new Date(now);
2603
2916
  yesterday.setDate(now.getDate() - 1);
2604
2917
  if (isSameDay(d, yesterday)) return `Yesterday ${time}`;
@@ -2706,12 +3019,26 @@ function ReactionStrip({
2706
3019
  styles16.chip,
2707
3020
  {
2708
3021
  backgroundColor: meReacted ? theme.colors.brandSoft : theme.colors.surface2,
2709
- borderColor: meReacted ? theme.colors.brand : theme.colors.border
3022
+ borderColor: meReacted ? theme.colors.brand : theme.colors.border,
3023
+ borderWidth: meReacted ? 1.5 : 1,
3024
+ transform: meReacted ? [{ scale: 1.04 }] : void 0
2710
3025
  }
2711
3026
  ],
2712
3027
  children: [
2713
3028
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles16.emoji, children: emoji }),
2714
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles16.count, { color: theme.colors.ink2 }], children: userArr.length })
3029
+ /* @__PURE__ */ jsxRuntime.jsx(
3030
+ reactNative.Text,
3031
+ {
3032
+ style: [
3033
+ styles16.count,
3034
+ {
3035
+ color: meReacted ? theme.colors.brand : theme.colors.ink2,
3036
+ fontWeight: meReacted ? "700" : "600"
3037
+ }
3038
+ ],
3039
+ children: userArr.length
3040
+ }
3041
+ )
2715
3042
  ]
2716
3043
  },
2717
3044
  emoji
@@ -2764,7 +3091,6 @@ var styles16 = reactNative.StyleSheet.create({
2764
3091
  paddingHorizontal: 9,
2765
3092
  paddingVertical: 4,
2766
3093
  borderRadius: 14,
2767
- borderWidth: 1,
2768
3094
  gap: 5,
2769
3095
  minHeight: 26,
2770
3096
  // Subtle elevation so chips lift off the surface
@@ -2780,7 +3106,6 @@ var styles16 = reactNative.StyleSheet.create({
2780
3106
  },
2781
3107
  count: {
2782
3108
  fontSize: 12,
2783
- fontWeight: "600",
2784
3109
  fontVariant: ["tabular-nums"]
2785
3110
  },
2786
3111
  backdrop: {
@@ -2818,11 +3143,10 @@ function MessageRow({
2818
3143
  actions = true,
2819
3144
  threads = true,
2820
3145
  quotations = true,
3146
+ markdown = true,
2821
3147
  readState,
2822
- editing = false,
3148
+ isEditing = false,
2823
3149
  onStartEdit,
2824
- onCancelEdit,
2825
- onSaveEdit,
2826
3150
  onDelete,
2827
3151
  onOpenThread,
2828
3152
  onQuote,
@@ -2843,7 +3167,7 @@ function MessageRow({
2843
3167
  ...msg.reactions ? { initialReactions: msg.reactions } : {}
2844
3168
  });
2845
3169
  const translateX = react.useRef(new reactNative.Animated.Value(0)).current;
2846
- const swipeEnabled = quotations && !!onQuote && !editing;
3170
+ const swipeEnabled = quotations && !!onQuote && !isEditing;
2847
3171
  const panResponder = react.useMemo(
2848
3172
  () => reactNative.PanResponder.create({
2849
3173
  // Tap / long-press still go to the inner Pressable.
@@ -2918,137 +3242,149 @@ function MessageRow({
2918
3242
  const rowSender = react$1.useUser(showAvatarSlot ? msg.sender_external_id : null);
2919
3243
  const avatarUrl = msg.sender_external_id ? avatarFor?.(msg.sender_external_id) ?? rowSender.profile?.avatarUrl ?? null : null;
2920
3244
  const senderLabel = msg.sender_external_id ? labelFor?.(msg.sender_external_id) ?? rowSender.profile?.displayName ?? msg.sender_external_id : null;
2921
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles17.row, { alignSelf: isSelf ? "flex-end" : "flex-start" }], children: [
2922
- showAvatarSlot ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles17.avatarSlot, children: showAvatarRender ? /* @__PURE__ */ jsxRuntime.jsx(
2923
- Avatar,
2924
- {
2925
- src: avatarUrl ?? null,
2926
- name: senderLabel,
2927
- seed: msg.sender_external_id ?? null,
2928
- size: "sm"
2929
- }
2930
- ) : null }) : null,
2931
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles17.bubbleCol, children: [
2932
- swipeEnabled ? /* @__PURE__ */ jsxRuntime.jsx(
2933
- reactNative.Animated.View,
2934
- {
2935
- pointerEvents: "none",
2936
- style: [
2937
- styles17.replyHint,
2938
- isSelf ? styles17.replyHintRight : styles17.replyHintLeft,
2939
- {
2940
- opacity: replyIconOpacity,
2941
- transform: [{ scale: replyIconScale }],
2942
- backgroundColor: theme.colors.surface2
2943
- }
2944
- ],
2945
- children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "reply", size: 16, color: theme.colors.brand })
2946
- }
2947
- ) : null,
2948
- /* @__PURE__ */ jsxRuntime.jsx(
2949
- reactNative.Animated.View,
2950
- {
2951
- ...swipeEnabled ? panResponder.panHandlers : {},
2952
- style: { transform: [{ translateX }] },
2953
- children: /* @__PURE__ */ jsxRuntime.jsx(
2954
- reactNative.Pressable,
3245
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3246
+ reactNative.View,
3247
+ {
3248
+ style: [
3249
+ styles17.row,
3250
+ { alignSelf: isSelf ? "flex-end" : "flex-start" },
3251
+ // Lift this row above the dim backdrop the parent renders
3252
+ // during edit mode. zIndex covers iOS / web; elevation does
3253
+ // the equivalent on Android.
3254
+ isEditing ? styles17.rowEditing : null
3255
+ ],
3256
+ children: [
3257
+ showAvatarSlot ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles17.avatarSlot, children: showAvatarRender ? /* @__PURE__ */ jsxRuntime.jsx(
3258
+ Avatar,
3259
+ {
3260
+ src: avatarUrl ?? null,
3261
+ name: senderLabel,
3262
+ seed: msg.sender_external_id ?? null,
3263
+ size: "sm"
3264
+ }
3265
+ ) : null }) : null,
3266
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles17.bubbleCol, children: [
3267
+ swipeEnabled ? /* @__PURE__ */ jsxRuntime.jsx(
3268
+ reactNative.Animated.View,
2955
3269
  {
2956
- onLongPress: () => {
2957
- if (!actions) return;
2958
- setActionsOpen(true);
2959
- },
2960
- accessibilityRole: "button",
2961
- accessibilityLabel: "Message actions",
2962
- children: editing ? /* @__PURE__ */ jsxRuntime.jsx(
2963
- EditableMessageBubble,
3270
+ pointerEvents: "none",
3271
+ style: [
3272
+ styles17.replyHint,
3273
+ isSelf ? styles17.replyHintRight : styles17.replyHintLeft,
2964
3274
  {
2965
- message: msg,
2966
- currentUserId: meId,
2967
- onCancel: onCancelEdit ?? (() => void 0),
2968
- onSave: onSaveEdit ?? (() => void 0)
3275
+ opacity: replyIconOpacity,
3276
+ transform: [{ scale: replyIconScale }],
3277
+ backgroundColor: theme.colors.surface2
2969
3278
  }
2970
- ) : /* @__PURE__ */ jsxRuntime.jsx(
2971
- MessageBubble,
3279
+ ],
3280
+ children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "reply", size: 16, color: theme.colors.brand })
3281
+ }
3282
+ ) : null,
3283
+ /* @__PURE__ */ jsxRuntime.jsx(
3284
+ reactNative.Animated.View,
3285
+ {
3286
+ ...swipeEnabled ? panResponder.panHandlers : {},
3287
+ style: { transform: [{ translateX }] },
3288
+ children: /* @__PURE__ */ jsxRuntime.jsx(
3289
+ reactNative.Pressable,
2972
3290
  {
2973
- message: msg,
2974
- currentUserId: meId,
2975
- ...readState ? { readState } : {},
2976
- ...labelFor ? { labelFor } : {},
2977
- ...onQuotedClick ? { onQuotedClick } : {},
2978
- groupPosition,
2979
- maxBodyLength,
2980
- showSenderName,
2981
- showAttachments: attachments,
2982
- actionsTrigger: actions ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: () => setActionsOpen(true), hitSlop: 8, children: /* @__PURE__ */ jsxRuntime.jsx(
2983
- PoolseIcon,
3291
+ onLongPress: () => {
3292
+ if (!actions) return;
3293
+ setActionsOpen(true);
3294
+ },
3295
+ accessibilityRole: "button",
3296
+ accessibilityLabel: "Message actions",
3297
+ children: /* @__PURE__ */ jsxRuntime.jsx(
3298
+ MessageBubble,
2984
3299
  {
2985
- name: "chevron-down",
2986
- size: 14,
2987
- color: isSelf ? theme.colors.onBrand : theme.colors.ink3
3300
+ message: msg,
3301
+ currentUserId: meId,
3302
+ ...readState ? { readState } : {},
3303
+ ...labelFor ? { labelFor } : {},
3304
+ ...onQuotedClick ? { onQuotedClick } : {},
3305
+ groupPosition,
3306
+ maxBodyLength,
3307
+ showSenderName,
3308
+ showAttachments: attachments,
3309
+ markdown,
3310
+ actionsTrigger: actions ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: () => setActionsOpen(true), hitSlop: 8, children: /* @__PURE__ */ jsxRuntime.jsx(
3311
+ PoolseIcon,
3312
+ {
3313
+ name: "chevron-down",
3314
+ size: 14,
3315
+ color: isSelf ? theme.colors.onBrand : theme.colors.ink3
3316
+ }
3317
+ ) }) : null
2988
3318
  }
2989
- ) }) : null
3319
+ )
2990
3320
  }
2991
3321
  )
2992
3322
  }
2993
- )
2994
- }
2995
- ),
2996
- threads && (msg.reply_count ?? 0) > 0 && onOpenThread ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: onOpenThread, style: styles17.threadPill, children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { style: [styles17.threadPillText, { color: theme.colors.brand }], children: [
2997
- "\u{1F4AC} ",
2998
- msg.reply_count,
2999
- " ",
3000
- msg.reply_count === 1 ? "reply" : "replies"
3001
- ] }) }) : null,
3002
- reactions ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles17.reactionsRow, { alignSelf: isSelf ? "flex-end" : "flex-start" }], children: /* @__PURE__ */ jsxRuntime.jsx(
3003
- ReactionStrip,
3004
- {
3005
- messageId: msg.id,
3006
- conversationId: msg.conversation_id,
3007
- meId,
3008
- ...msg.reactions ? { initialReactions: msg.reactions } : {}
3009
- }
3010
- ) }) : null
3011
- ] }),
3012
- actions && actionsOpen ? /* @__PURE__ */ jsxRuntime.jsx(
3013
- MessageActions,
3014
- {
3015
- visible: actionsOpen,
3016
- onClose: () => setActionsOpen(false),
3017
- isSelf,
3018
- ...reactions ? {
3019
- // Inline quick-react row at the top of the sheet —
3020
- // one tap = react + close, no second modal needed.
3021
- onPickReaction: (emoji) => {
3022
- void reactionsApi.addReaction(emoji);
3023
- }
3024
- } : {},
3025
- ...onStartEdit ? {
3026
- onEdit: () => {
3027
- setActionsOpen(false);
3028
- onStartEdit();
3029
- }
3030
- } : {},
3031
- ...onDelete ? {
3032
- onDelete: () => {
3033
- setActionsOpen(false);
3034
- onDelete();
3035
- }
3036
- } : {},
3037
- ...onOpenThread ? {
3038
- onReplyInThread: () => {
3039
- setActionsOpen(false);
3040
- onOpenThread();
3041
- }
3042
- } : {},
3043
- ...quotations && onQuote ? {
3044
- onQuote: () => {
3045
- setActionsOpen(false);
3046
- onQuote();
3323
+ ),
3324
+ threads && (msg.reply_count ?? 0) > 0 && onOpenThread ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: onOpenThread, style: styles17.threadPill, children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { style: [styles17.threadPillText, { color: theme.colors.brand }], children: [
3325
+ "\u{1F4AC} ",
3326
+ msg.reply_count,
3327
+ " ",
3328
+ msg.reply_count === 1 ? "reply" : "replies"
3329
+ ] }) }) : null,
3330
+ reactions ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles17.reactionsRow, { alignSelf: isSelf ? "flex-end" : "flex-start" }], children: /* @__PURE__ */ jsxRuntime.jsx(
3331
+ ReactionStrip,
3332
+ {
3333
+ messageId: msg.id,
3334
+ conversationId: msg.conversation_id,
3335
+ meId,
3336
+ ...msg.reactions ? { initialReactions: msg.reactions } : {}
3337
+ }
3338
+ ) }) : null
3339
+ ] }),
3340
+ actions && actionsOpen ? /* @__PURE__ */ jsxRuntime.jsx(
3341
+ MessageActions,
3342
+ {
3343
+ visible: actionsOpen,
3344
+ onClose: () => setActionsOpen(false),
3345
+ isSelf,
3346
+ ...reactions ? {
3347
+ // Inline quick-react row at the top of the sheet —
3348
+ // one tap = react + close, no second modal needed.
3349
+ onPickReaction: (emoji) => {
3350
+ void reactionsApi.addReaction(emoji);
3351
+ }
3352
+ } : {},
3353
+ ...msg.body && !msg.deleted_at ? {
3354
+ onCopy: () => {
3355
+ setActionsOpen(false);
3356
+ void copyToClipboard(msg.body ?? "");
3357
+ }
3358
+ } : {},
3359
+ ...onStartEdit ? {
3360
+ onEdit: () => {
3361
+ setActionsOpen(false);
3362
+ onStartEdit();
3363
+ }
3364
+ } : {},
3365
+ ...onDelete ? {
3366
+ onDelete: () => {
3367
+ setActionsOpen(false);
3368
+ onDelete();
3369
+ }
3370
+ } : {},
3371
+ ...onOpenThread ? {
3372
+ onReplyInThread: () => {
3373
+ setActionsOpen(false);
3374
+ onOpenThread();
3375
+ }
3376
+ } : {},
3377
+ ...quotations && onQuote ? {
3378
+ onQuote: () => {
3379
+ setActionsOpen(false);
3380
+ onQuote();
3381
+ }
3382
+ } : {}
3047
3383
  }
3048
- } : {}
3049
- }
3050
- ) : null
3051
- ] });
3384
+ ) : null
3385
+ ]
3386
+ }
3387
+ );
3052
3388
  }
3053
3389
  var styles17 = reactNative.StyleSheet.create({
3054
3390
  row: {
@@ -3064,6 +3400,12 @@ var styles17 = reactNative.StyleSheet.create({
3064
3400
  paddingHorizontal: 10,
3065
3401
  maxWidth: "95%"
3066
3402
  },
3403
+ rowEditing: {
3404
+ // Above ConversationView's editBackdrop (zIndex 5). Elevation
3405
+ // does the equivalent stacking on Android.
3406
+ zIndex: 10,
3407
+ elevation: 10
3408
+ },
3067
3409
  avatarSlot: {
3068
3410
  width: 36,
3069
3411
  marginRight: 6
@@ -3103,6 +3445,27 @@ var styles17 = reactNative.StyleSheet.create({
3103
3445
  marginTop: 2
3104
3446
  }
3105
3447
  });
3448
+ var clipboardModule;
3449
+ var clipboardWarned = false;
3450
+ async function copyToClipboard(text) {
3451
+ if (clipboardModule === void 0) {
3452
+ try {
3453
+ clipboardModule = __require("expo-clipboard");
3454
+ } catch {
3455
+ clipboardModule = null;
3456
+ }
3457
+ }
3458
+ if (!clipboardModule?.setStringAsync) {
3459
+ if (!clipboardWarned) {
3460
+ clipboardWarned = true;
3461
+ console.warn(
3462
+ "[@poolse/react-native] Copy action requires `expo-clipboard`. Install it to enable copy-to-clipboard."
3463
+ );
3464
+ }
3465
+ return;
3466
+ }
3467
+ await clipboardModule.setStringAsync(text);
3468
+ }
3106
3469
  var SHEET_HEIGHT_FRACTION = 0.88;
3107
3470
  function ThreadView({
3108
3471
  visible,
@@ -3158,31 +3521,34 @@ function ThreadSheet({
3158
3521
  const [pickerOpen, setPickerOpen] = react.useState(false);
3159
3522
  const [editingId, setEditingId] = react.useState(null);
3160
3523
  const renderReply = react.useCallback(
3161
- (msg) => /* @__PURE__ */ jsxRuntime.jsx(
3162
- MessageRow,
3163
- {
3164
- msg,
3165
- meId,
3166
- reactions: true,
3167
- attachments: true,
3168
- actions: true,
3169
- threads: false,
3170
- quotations: false,
3171
- editing: editingId === msg.id,
3172
- onStartEdit: () => setEditingId(msg.id),
3173
- onCancelEdit: () => setEditingId(null),
3174
- onSaveEdit: async (body) => {
3175
- await edit(msg.id, body);
3176
- setEditingId(null);
3177
- },
3178
- onDelete: () => {
3179
- void deleteReply(msg.id);
3180
- },
3181
- ...labelFor ? { labelFor } : {},
3182
- ...avatarFor ? { avatarFor } : {}
3183
- }
3184
- ),
3185
- [meId, editingId, edit, deleteReply, labelFor, avatarFor]
3524
+ (msg) => {
3525
+ const isSelf = meId !== null && msg.sender_id === meId;
3526
+ const canEdit = isSelf && !msg.deleted_at;
3527
+ return /* @__PURE__ */ jsxRuntime.jsx(
3528
+ MessageRow,
3529
+ {
3530
+ msg,
3531
+ meId,
3532
+ reactions: true,
3533
+ attachments: true,
3534
+ actions: true,
3535
+ threads: false,
3536
+ quotations: false,
3537
+ isEditing: editingId === msg.id,
3538
+ ...canEdit ? { onStartEdit: () => setEditingId(msg.id) } : {},
3539
+ onDelete: () => {
3540
+ void deleteReply(msg.id);
3541
+ },
3542
+ ...labelFor ? { labelFor } : {},
3543
+ ...avatarFor ? { avatarFor } : {}
3544
+ }
3545
+ );
3546
+ },
3547
+ [meId, editingId, deleteReply, labelFor, avatarFor]
3548
+ );
3549
+ const editingMessage = react.useMemo(
3550
+ () => editingId ? replies.find((m) => m.id === editingId) ?? null : null,
3551
+ [editingId, replies]
3186
3552
  );
3187
3553
  const rootRow = react.useMemo(
3188
3554
  () => /* @__PURE__ */ jsxRuntime.jsx(
@@ -3268,7 +3634,14 @@ function ThreadSheet({
3268
3634
  },
3269
3635
  placeholder: "Reply to thread\u2026",
3270
3636
  attachments: true,
3271
- onAttachPress: () => setPickerOpen(true)
3637
+ onAttachPress: () => setPickerOpen(true),
3638
+ editingMessage,
3639
+ onCancelEdit: () => setEditingId(null),
3640
+ onSaveEdit: async (body) => {
3641
+ if (!editingId) return;
3642
+ await edit(editingId, body);
3643
+ setEditingId(null);
3644
+ }
3272
3645
  }
3273
3646
  )
3274
3647
  ]
@@ -3432,6 +3805,7 @@ var styles19 = reactNative.StyleSheet.create({
3432
3805
  fontSize: 12
3433
3806
  }
3434
3807
  });
3808
+ var GROUPING_WINDOW_MS = 5 * 60 * 1e3;
3435
3809
  function ConversationView({
3436
3810
  conversationId,
3437
3811
  emptyState,
@@ -3448,6 +3822,7 @@ function ConversationView({
3448
3822
  threads = true,
3449
3823
  quotations = true,
3450
3824
  readReceipts = true,
3825
+ markdown = true,
3451
3826
  maxBodyLength = 200,
3452
3827
  senderLabels = "auto",
3453
3828
  avatars = "auto",
@@ -3466,9 +3841,25 @@ function ConversationView({
3466
3841
  send,
3467
3842
  edit,
3468
3843
  delete: deleteMsg,
3469
- markReadUpTo
3844
+ markReadUpTo,
3845
+ refetch
3470
3846
  } = react$1.useMessages(conversationId);
3471
3847
  const { typing, signalTyping } = react$1.useTyping(conversationId);
3848
+ const realtimeStatus = react$1.useRealtimeStatus();
3849
+ react.useEffect(() => {
3850
+ const sub = reactNative.AppState.addEventListener("change", (next) => {
3851
+ if (next === "active") void refetch();
3852
+ });
3853
+ return () => sub.remove();
3854
+ }, [refetch]);
3855
+ const prevStatusRef = react.useRef(realtimeStatus);
3856
+ react.useEffect(() => {
3857
+ const prev = prevStatusRef.current;
3858
+ prevStatusRef.current = realtimeStatus;
3859
+ if (realtimeStatus === "connected" && (prev === "reconnecting" || prev === "closed" || prev === "connecting")) {
3860
+ void refetch();
3861
+ }
3862
+ }, [realtimeStatus, refetch]);
3472
3863
  const wantsMembers = mentions || senderLabels === "auto" || avatars === "auto";
3473
3864
  const { members } = react$1.useMembers(wantsMembers ? conversationId : "");
3474
3865
  const isGroupChat = members.length > 2;
@@ -3499,10 +3890,24 @@ function ConversationView({
3499
3890
  });
3500
3891
  onMarkedRead?.(conversationId, latest.id);
3501
3892
  }, [messages, meId, readReceipts, markReadUpTo, onMarkedRead, conversationId]);
3893
+ const groupPositionById = react.useMemo(() => {
3894
+ const map = /* @__PURE__ */ new Map();
3895
+ for (let i = 0; i < messages.length; i++) {
3896
+ const msg = messages[i];
3897
+ if (!msg) continue;
3898
+ const prev = i > 0 ? messages[i - 1] ?? null : null;
3899
+ const next = i < messages.length - 1 ? messages[i + 1] ?? null : null;
3900
+ map.set(msg.id, react$1.computeGroupPosition(msg, prev, next, GROUPING_WINDOW_MS));
3901
+ }
3902
+ return map;
3903
+ }, [messages]);
3502
3904
  const renderItem = react.useCallback(
3503
3905
  (msg) => {
3504
3906
  if (renderMessage) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderMessage(msg, meId) });
3505
3907
  const readState = readReceipts && meId !== null && msg.sender_id === meId ? readStateForMessage(msg, members, meId, sequenceByMessageId) : void 0;
3908
+ const isSelf = meId !== null && msg.sender_id === meId;
3909
+ const canEdit = isSelf && !msg.deleted_at && (readState ?? "sent") === "sent";
3910
+ const groupPosition = groupPositionById.get(msg.id) ?? "standalone";
3506
3911
  return /* @__PURE__ */ jsxRuntime.jsx(
3507
3912
  MessageRow,
3508
3913
  {
@@ -3514,13 +3919,8 @@ function ConversationView({
3514
3919
  threads,
3515
3920
  quotations,
3516
3921
  ...readState ? { readState } : {},
3517
- editing: editingId === msg.id,
3518
- onStartEdit: () => setEditingId(msg.id),
3519
- onCancelEdit: () => setEditingId(null),
3520
- onSaveEdit: async (body) => {
3521
- await edit(msg.id, body);
3522
- setEditingId(null);
3523
- },
3922
+ isEditing: editingId === msg.id,
3923
+ ...canEdit ? { onStartEdit: () => setEditingId(msg.id) } : {},
3524
3924
  onDelete: () => deleteMsg(msg.id),
3525
3925
  ...threads ? { onOpenThread: () => setThreadRoot(msg) } : {},
3526
3926
  ...quotations ? { onQuote: () => setReplyingTo(msg) } : {},
@@ -3528,7 +3928,9 @@ function ConversationView({
3528
3928
  ...avatarFor ? { avatarFor } : {},
3529
3929
  showSenderName: showSenderLabels,
3530
3930
  showAvatar: showAvatars,
3531
- maxBodyLength
3931
+ maxBodyLength,
3932
+ groupPosition,
3933
+ markdown
3532
3934
  }
3533
3935
  );
3534
3936
  },
@@ -3543,16 +3945,38 @@ function ConversationView({
3543
3945
  threads,
3544
3946
  quotations,
3545
3947
  editingId,
3546
- edit,
3547
3948
  deleteMsg,
3548
3949
  labelFor,
3549
3950
  avatarFor,
3550
3951
  showSenderLabels,
3551
3952
  showAvatars,
3552
3953
  maxBodyLength,
3553
- sequenceByMessageId
3954
+ sequenceByMessageId,
3955
+ groupPositionById,
3956
+ markdown
3554
3957
  ]
3555
3958
  );
3959
+ const editingMessage = react.useMemo(
3960
+ () => editingId ? messages.find((m) => m.id === editingId) ?? null : null,
3961
+ [editingId, messages]
3962
+ );
3963
+ const listItems = react.useMemo(() => {
3964
+ const out = [];
3965
+ let prev = null;
3966
+ for (const msg of messages) {
3967
+ if (prev === null || !react$1.sameDay(msg.inserted_at, prev.inserted_at)) {
3968
+ const label = react$1.formatDayLabel(msg.inserted_at);
3969
+ out.push({
3970
+ kind: "separator",
3971
+ id: `sep-${msg.id}`,
3972
+ node: /* @__PURE__ */ jsxRuntime.jsx(DaySeparator, { label })
3973
+ });
3974
+ }
3975
+ out.push({ kind: "msg", msg });
3976
+ prev = msg;
3977
+ }
3978
+ return out;
3979
+ }, [messages]);
3556
3980
  const composerProps = react.useMemo(
3557
3981
  () => ({
3558
3982
  onSend: async (body, opts) => {
@@ -3566,9 +3990,28 @@ function ConversationView({
3566
3990
  onCancelReply: () => setReplyingTo(null),
3567
3991
  ...labelFor ? { labelFor } : {},
3568
3992
  attachments,
3569
- keyboardOffset
3993
+ keyboardOffset,
3994
+ editingMessage,
3995
+ onCancelEdit: () => setEditingId(null),
3996
+ onSaveEdit: async (body) => {
3997
+ if (!editingId) return;
3998
+ await edit(editingId, body);
3999
+ setEditingId(null);
4000
+ }
3570
4001
  }),
3571
- [send, signalTyping, attachments, replyingTo, labelFor, keyboardOffset, onSent, conversationId]
4002
+ [
4003
+ send,
4004
+ signalTyping,
4005
+ attachments,
4006
+ replyingTo,
4007
+ labelFor,
4008
+ keyboardOffset,
4009
+ onSent,
4010
+ conversationId,
4011
+ editingMessage,
4012
+ editingId,
4013
+ edit
4014
+ ]
3572
4015
  );
3573
4016
  const headerNode = header === false ? null : header === true ? /* @__PURE__ */ jsxRuntime.jsx(
3574
4017
  ChatHeader,
@@ -3588,18 +4031,30 @@ function ConversationView({
3588
4031
  keyboardVerticalOffset: keyboardOffset,
3589
4032
  children: [
3590
4033
  headerNode,
3591
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles20.listWrap, children: /* @__PURE__ */ jsxRuntime.jsx(
3592
- MessageList,
3593
- {
3594
- messages,
3595
- loading,
3596
- hasMore,
3597
- onLoadMore: loadMore,
3598
- renderItem,
3599
- ListHeader: /* @__PURE__ */ jsxRuntime.jsx(TypingIndicator, { typing, ...labelFor ? { labelFor } : {} }),
3600
- ...emptyState !== void 0 ? { emptyState } : {}
3601
- }
3602
- ) }),
4034
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles20.listWrap, children: [
4035
+ /* @__PURE__ */ jsxRuntime.jsx(
4036
+ MessageList,
4037
+ {
4038
+ messages,
4039
+ items: listItems,
4040
+ meId,
4041
+ loading,
4042
+ hasMore,
4043
+ onLoadMore: loadMore,
4044
+ renderItem,
4045
+ ListHeader: /* @__PURE__ */ jsxRuntime.jsx(TypingIndicator, { typing, ...labelFor ? { labelFor } : {} }),
4046
+ ...emptyState !== void 0 ? { emptyState } : {}
4047
+ }
4048
+ ),
4049
+ editingId ? /* @__PURE__ */ jsxRuntime.jsx(
4050
+ reactNative.Pressable,
4051
+ {
4052
+ style: styles20.editBackdrop,
4053
+ onPress: () => setEditingId(null),
4054
+ accessibilityLabel: "Cancel editing"
4055
+ }
4056
+ ) : null
4057
+ ] }),
3603
4058
  mentions ? /* @__PURE__ */ jsxRuntime.jsx(MentionInput, { conversationId, ...composerProps }) : /* @__PURE__ */ jsxRuntime.jsx(MessageComposer, { ...composerProps }),
3604
4059
  attachments ? /* @__PURE__ */ jsxRuntime.jsx(AttachmentPicker, { visible: pickerOpen, onClose: () => setPickerOpen(false) }) : null,
3605
4060
  threads && threadRoot ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -3635,6 +4090,14 @@ var styles20 = reactNative.StyleSheet.create({
3635
4090
  },
3636
4091
  listWrap: {
3637
4092
  flex: 1
4093
+ },
4094
+ editBackdrop: {
4095
+ ...reactNative.StyleSheet.absoluteFillObject,
4096
+ backgroundColor: "rgba(0,0,0,0.45)",
4097
+ // Sit above message rows by default but below any row that
4098
+ // explicitly elevates itself (MessageRow bumps zIndex/elevation
4099
+ // when isEditing is true).
4100
+ zIndex: 5
3638
4101
  }
3639
4102
  });
3640
4103
  function UserPickerSheet({
@@ -4499,7 +4962,7 @@ exports.Avatar = Avatar;
4499
4962
  exports.ChatHeader = ChatHeader;
4500
4963
  exports.ConversationList = ConversationList;
4501
4964
  exports.ConversationView = ConversationView;
4502
- exports.EditableMessageBubble = EditableMessageBubble;
4965
+ exports.DaySeparator = DaySeparator;
4503
4966
  exports.GroupDetailsSheet = GroupDetailsSheet;
4504
4967
  exports.ImageMosaic = ImageMosaic;
4505
4968
  exports.MemberList = MemberList;