@poolse/react-native 2.0.13 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -8,6 +8,10 @@ var reactNativeSvg = require('react-native-svg');
8
8
  var ImagePicker = require('expo-image-picker');
9
9
  var DocumentPicker = require('expo-document-picker');
10
10
  var expoImageManipulator = require('expo-image-manipulator');
11
+ var Clipboard = require('expo-clipboard');
12
+ var Markdown = require('react-native-markdown-display');
13
+
14
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
11
15
 
12
16
  function _interopNamespace(e) {
13
17
  if (e && e.__esModule) return e;
@@ -29,8 +33,15 @@ function _interopNamespace(e) {
29
33
 
30
34
  var ImagePicker__namespace = /*#__PURE__*/_interopNamespace(ImagePicker);
31
35
  var DocumentPicker__namespace = /*#__PURE__*/_interopNamespace(DocumentPicker);
36
+ var Clipboard__namespace = /*#__PURE__*/_interopNamespace(Clipboard);
37
+ var Markdown__default = /*#__PURE__*/_interopDefault(Markdown);
32
38
 
33
- // src/PoolseInbox.tsx
39
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
40
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
41
+ }) : x)(function(x) {
42
+ if (typeof require !== "undefined") return require.apply(this, arguments);
43
+ throw Error('Dynamic require of "' + x + '" is not supported');
44
+ });
34
45
 
35
46
  // src/theme/default.ts
36
47
  var lightColors = {
@@ -311,6 +322,10 @@ var ICONS = {
311
322
  vb: "0 0 24 24",
312
323
  d: '<path d="M6 9.2a6 6 0 0 1 12 0c0 4.8 1.4 6.3 1.9 6.8H4.1C4.6 15.5 6 14 6 9.2Z"/><path d="M9.6 19.8a2.5 2.5 0 0 0 4.8 0"/>'
313
324
  },
325
+ camera: {
326
+ vb: "0 0 24 24",
327
+ d: '<path d="M3 8.5a2 2 0 0 1 2-2h2.4l1.4-2h6.4l1.4 2H19a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-9Z"/><circle cx="12" cy="13" r="3.5"/>'
328
+ },
314
329
  "check-double": {
315
330
  vb: "0 0 24 24",
316
331
  d: '<path d="M2.5 12.8 6.9 17 15.4 7.6"/><path d="M11.4 16.4 12.3 17.2 21 7.6"/>'
@@ -1130,7 +1145,12 @@ function useUploadPreview() {
1130
1145
  getPreview: ctx?.getPreview ?? (() => null)
1131
1146
  };
1132
1147
  }
1133
- function AttachmentPicker({ visible, onClose, onPickInputs }) {
1148
+ function AttachmentPicker({
1149
+ visible,
1150
+ onClose,
1151
+ onPickInputs,
1152
+ camera = true
1153
+ }) {
1134
1154
  const theme = usePoolseTheme();
1135
1155
  const upload = useSharedUpload();
1136
1156
  const { setPreview } = useUploadPreview();
@@ -1164,8 +1184,12 @@ function AttachmentPicker({ visible, onClose, onPickInputs }) {
1164
1184
  theme.shadows.lg
1165
1185
  ],
1166
1186
  children: [
1187
+ camera ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Pressable, { style: styles6.item, onPress: () => pickFromKind("camera"), children: [
1188
+ /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "camera", size: 20, color: theme.colors.ink2 }),
1189
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles6.itemText, { color: theme.colors.ink }], children: "Camera" })
1190
+ ] }) : null,
1167
1191
  /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Pressable, { style: styles6.item, onPress: () => pickFromKind("image"), children: [
1168
- /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "attachment", size: 20, color: theme.colors.ink2 }),
1192
+ /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "image", size: 20, color: theme.colors.ink2 }),
1169
1193
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles6.itemText, { color: theme.colors.ink }], children: "Photo or video" })
1170
1194
  ] }),
1171
1195
  /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Pressable, { style: styles6.item, onPress: () => pickFromKind("document"), children: [
@@ -1224,7 +1248,52 @@ async function compressImage({
1224
1248
  filename: `${base}.webp`
1225
1249
  };
1226
1250
  }
1251
+ async function assetsToInputs(assets) {
1252
+ return Promise.all(
1253
+ assets.map(async (asset) => {
1254
+ try {
1255
+ const compressed = await compressImage({
1256
+ uri: asset.uri,
1257
+ width: asset.width,
1258
+ height: asset.height,
1259
+ filename: asset.fileName ?? `image-${Date.now()}`
1260
+ });
1261
+ return {
1262
+ body: compressed.blob,
1263
+ contentType: compressed.contentType,
1264
+ byteSize: compressed.blob.size,
1265
+ filename: compressed.filename,
1266
+ _previewUri: asset.uri
1267
+ };
1268
+ } catch (err) {
1269
+ console.warn("[poolse/picker] image compression failed, sending original:", err);
1270
+ const blob = await uriToBlob(asset.uri);
1271
+ const contentType = asset.mimeType ?? blob.type ?? "image/jpeg";
1272
+ return {
1273
+ body: blob,
1274
+ contentType,
1275
+ byteSize: asset.fileSize ?? blob.size,
1276
+ filename: asset.fileName ?? `image-${Date.now()}.jpg`,
1277
+ _previewUri: asset.uri
1278
+ };
1279
+ }
1280
+ })
1281
+ );
1282
+ }
1227
1283
  async function defaultAdapter(kind) {
1284
+ if (kind === "camera") {
1285
+ const perm = await ImagePicker__namespace.requestCameraPermissionsAsync();
1286
+ if (!perm.granted) {
1287
+ console.warn("[poolse/picker] camera permission denied");
1288
+ return [];
1289
+ }
1290
+ const res2 = await ImagePicker__namespace.launchCameraAsync({
1291
+ mediaTypes: "images",
1292
+ quality: 1
1293
+ });
1294
+ if (res2.canceled) return [];
1295
+ return assetsToInputs(res2.assets);
1296
+ }
1228
1297
  if (kind === "image") {
1229
1298
  const res2 = await ImagePicker__namespace.launchImageLibraryAsync({
1230
1299
  mediaTypes: "images",
@@ -1234,36 +1303,7 @@ async function defaultAdapter(kind) {
1234
1303
  quality: 1
1235
1304
  });
1236
1305
  if (res2.canceled) return [];
1237
- return Promise.all(
1238
- res2.assets.map(async (asset) => {
1239
- try {
1240
- const compressed = await compressImage({
1241
- uri: asset.uri,
1242
- width: asset.width,
1243
- height: asset.height,
1244
- filename: asset.fileName ?? `image-${Date.now()}`
1245
- });
1246
- return {
1247
- body: compressed.blob,
1248
- contentType: compressed.contentType,
1249
- byteSize: compressed.blob.size,
1250
- filename: compressed.filename,
1251
- _previewUri: asset.uri
1252
- };
1253
- } catch (err) {
1254
- console.warn("[poolse/picker] image compression failed, sending original:", err);
1255
- const blob = await uriToBlob(asset.uri);
1256
- const contentType = asset.mimeType ?? blob.type ?? "image/jpeg";
1257
- return {
1258
- body: blob,
1259
- contentType,
1260
- byteSize: asset.fileSize ?? blob.size,
1261
- filename: asset.fileName ?? `image-${Date.now()}.jpg`,
1262
- _previewUri: asset.uri
1263
- };
1264
- }
1265
- })
1266
- );
1306
+ return assetsToInputs(res2.assets);
1267
1307
  }
1268
1308
  const res = await DocumentPicker__namespace.getDocumentAsync({
1269
1309
  copyToCacheDirectory: true,
@@ -1330,6 +1370,51 @@ var styles6 = reactNative.StyleSheet.create({
1330
1370
  fontWeight: "500"
1331
1371
  }
1332
1372
  });
1373
+ function DaySeparator({ label }) {
1374
+ const theme = usePoolseTheme();
1375
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles7.wrap, children: /* @__PURE__ */ jsxRuntime.jsx(
1376
+ reactNative.View,
1377
+ {
1378
+ style: [
1379
+ styles7.chip,
1380
+ {
1381
+ backgroundColor: theme.colors.surface2,
1382
+ borderColor: theme.colors.border
1383
+ }
1384
+ ],
1385
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1386
+ reactNative.Text,
1387
+ {
1388
+ style: [
1389
+ styles7.label,
1390
+ {
1391
+ color: theme.colors.ink2,
1392
+ fontFamily: theme.type.fontBody
1393
+ }
1394
+ ],
1395
+ children: label
1396
+ }
1397
+ )
1398
+ }
1399
+ ) });
1400
+ }
1401
+ var styles7 = reactNative.StyleSheet.create({
1402
+ wrap: {
1403
+ alignItems: "center",
1404
+ marginVertical: 8
1405
+ },
1406
+ chip: {
1407
+ paddingHorizontal: 12,
1408
+ paddingVertical: 4,
1409
+ borderRadius: 999,
1410
+ borderWidth: reactNative.StyleSheet.hairlineWidth
1411
+ },
1412
+ label: {
1413
+ fontSize: 11,
1414
+ fontWeight: "600",
1415
+ letterSpacing: 0.3
1416
+ }
1417
+ });
1333
1418
  function UploadQueueStrip() {
1334
1419
  const theme = usePoolseTheme();
1335
1420
  const upload = useSharedUpload();
@@ -1340,8 +1425,8 @@ function UploadQueueStrip() {
1340
1425
  {
1341
1426
  horizontal: true,
1342
1427
  showsHorizontalScrollIndicator: false,
1343
- contentContainerStyle: styles7.scroll,
1344
- style: [styles7.scrollStyle, { marginBottom: 8 }],
1428
+ contentContainerStyle: styles8.scroll,
1429
+ style: [styles8.scrollStyle, { marginBottom: 8 }],
1345
1430
  children: upload.queue.map((item) => {
1346
1431
  const isError = item.status === "error";
1347
1432
  const isUploading = item.status === "uploading" || item.status === "pending";
@@ -1352,7 +1437,7 @@ function UploadQueueStrip() {
1352
1437
  if (item.status === "ready" || isError) upload.remove(item.localId);
1353
1438
  else upload.cancel(item.localId);
1354
1439
  };
1355
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles7.tileWrap, children: [
1440
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles8.tileWrap, children: [
1356
1441
  isImage && previewUri ? /* @__PURE__ */ jsxRuntime.jsx(ImageTile, { uri: previewUri, uploading: isUploading, error: isError }) : /* @__PURE__ */ jsxRuntime.jsx(
1357
1442
  FileTile,
1358
1443
  {
@@ -1367,13 +1452,13 @@ function UploadQueueStrip() {
1367
1452
  {
1368
1453
  onPress: handleClose,
1369
1454
  hitSlop: 8,
1370
- style: [styles7.closeBtn, { backgroundColor: theme.colors.ink }],
1455
+ style: [styles8.closeBtn, { backgroundColor: theme.colors.ink }],
1371
1456
  accessibilityLabel: isError ? "Remove failed upload" : "Cancel upload",
1372
1457
  children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "close", size: 11, color: theme.colors.onBrand })
1373
1458
  }
1374
1459
  ) : null,
1375
- isUploading ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles7.progressOverlay, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles7.spinner, { borderColor: theme.colors.onBrand }] }) }) : null,
1376
- 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
1460
+ isUploading ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles8.progressOverlay, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles8.spinner, { borderColor: theme.colors.onBrand }] }) }) : null,
1461
+ 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
1377
1462
  ] }, item.localId);
1378
1463
  })
1379
1464
  }
@@ -1386,7 +1471,7 @@ function ImageTile({ uri, uploading, error }) {
1386
1471
  {
1387
1472
  source: { uri },
1388
1473
  style: [
1389
- styles7.imageTile,
1474
+ styles8.imageTile,
1390
1475
  {
1391
1476
  backgroundColor: theme.colors.surface2,
1392
1477
  borderColor: error ? theme.colors.error : "transparent",
@@ -1408,24 +1493,24 @@ function FileTile({
1408
1493
  reactNative.View,
1409
1494
  {
1410
1495
  style: [
1411
- styles7.fileTile,
1496
+ styles8.fileTile,
1412
1497
  {
1413
1498
  backgroundColor: theme.colors.surface2,
1414
1499
  borderColor: error ? theme.colors.error : uploading ? theme.colors.brand : theme.colors.border
1415
1500
  }
1416
1501
  ],
1417
1502
  children: [
1418
- /* @__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 }) }),
1419
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles7.fileInfo, children: [
1503
+ /* @__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 }) }),
1504
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles8.fileInfo, children: [
1420
1505
  /* @__PURE__ */ jsxRuntime.jsx(
1421
1506
  reactNative.Text,
1422
1507
  {
1423
- style: [styles7.fileName, { color: theme.colors.ink, fontFamily: theme.type.fontBody }],
1508
+ style: [styles8.fileName, { color: theme.colors.ink, fontFamily: theme.type.fontBody }],
1424
1509
  numberOfLines: 1,
1425
1510
  children: filename
1426
1511
  }
1427
1512
  ),
1428
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles7.fileSize, { color: theme.colors.ink3 }], children: formatBytes(size) })
1513
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles8.fileSize, { color: theme.colors.ink3 }], children: formatBytes(size) })
1429
1514
  ] })
1430
1515
  ]
1431
1516
  }
@@ -1439,7 +1524,7 @@ function formatBytes(n) {
1439
1524
  return `${(n / 1024 / 1024 / 1024).toFixed(1)} GB`;
1440
1525
  }
1441
1526
  var TILE = 64;
1442
- var styles7 = reactNative.StyleSheet.create({
1527
+ var styles8 = reactNative.StyleSheet.create({
1443
1528
  scrollStyle: {
1444
1529
  overflow: "visible"
1445
1530
  },
@@ -1539,6 +1624,22 @@ var styles7 = reactNative.StyleSheet.create({
1539
1624
  fontWeight: "700"
1540
1625
  }
1541
1626
  });
1627
+ var FALLBACK = {
1628
+ top: reactNative.Platform.OS === "ios" ? 50 : 24,
1629
+ bottom: reactNative.Platform.OS === "ios" ? 34 : 0,
1630
+ left: 0,
1631
+ right: 0
1632
+ };
1633
+ var realHook = null;
1634
+ try {
1635
+ const mod = __require("react-native-safe-area-context");
1636
+ if (mod && typeof mod.useSafeAreaInsets === "function") {
1637
+ realHook = mod.useSafeAreaInsets;
1638
+ }
1639
+ } catch {
1640
+ }
1641
+ var useFallback = () => FALLBACK;
1642
+ var useSafeInsets = realHook ?? useFallback;
1542
1643
  var MessageComposer = react.forwardRef(
1543
1644
  function MessageComposer2({
1544
1645
  onSend,
@@ -1550,13 +1651,32 @@ var MessageComposer = react.forwardRef(
1550
1651
  labelFor,
1551
1652
  attachments = true,
1552
1653
  onAttachPress,
1553
- keyboardOffset: _keyboardOffset = 0
1654
+ keyboardOffset: _keyboardOffset = 0,
1655
+ editingMessage = null,
1656
+ onCancelEdit,
1657
+ onSaveEdit
1554
1658
  }, ref) {
1555
1659
  const theme = usePoolseTheme();
1556
1660
  const inputRef = react.useRef(null);
1557
1661
  const [value, setValue] = react.useState("");
1558
1662
  const [sending, setSending] = react.useState(false);
1663
+ const [keyboardOpen, setKeyboardOpen] = react.useState(false);
1559
1664
  const upload = useSharedUpload();
1665
+ const insets = useSafeInsets();
1666
+ react.useEffect(() => {
1667
+ const show = reactNative.Keyboard.addListener(
1668
+ reactNative.Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow",
1669
+ () => setKeyboardOpen(true)
1670
+ );
1671
+ const hide = reactNative.Keyboard.addListener(
1672
+ reactNative.Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide",
1673
+ () => setKeyboardOpen(false)
1674
+ );
1675
+ return () => {
1676
+ show.remove();
1677
+ hide.remove();
1678
+ };
1679
+ }, []);
1560
1680
  react.useImperativeHandle(
1561
1681
  ref,
1562
1682
  () => ({
@@ -1575,36 +1695,65 @@ var MessageComposer = react.forwardRef(
1575
1695
  const isUploading = upload.queue.some(
1576
1696
  (item) => item.status === "uploading" || item.status === "pending"
1577
1697
  );
1578
- const submitDisabled = disabled || sending || isUploading || value.trim().length === 0 && readyAttachmentIds.length === 0;
1698
+ const previousDraftRef = react.useRef(null);
1699
+ const editingIdRef = react.useRef(null);
1700
+ react.useEffect(() => {
1701
+ const id = editingMessage?.id ?? null;
1702
+ if (id === editingIdRef.current) return;
1703
+ if (id) {
1704
+ previousDraftRef.current = value;
1705
+ setValue(editingMessage?.body ?? "");
1706
+ inputRef.current?.focus();
1707
+ } else {
1708
+ setValue(previousDraftRef.current ?? "");
1709
+ previousDraftRef.current = null;
1710
+ }
1711
+ editingIdRef.current = id;
1712
+ }, [editingMessage?.id, editingMessage?.body]);
1713
+ const isEditing = !!editingMessage;
1714
+ const editChanged = isEditing && value.trim() !== (editingMessage?.body?.trim() ?? "");
1715
+ const submitDisabled = disabled || sending || isUploading || value.trim().length === 0 || (isEditing ? !editChanged : value.trim().length === 0 && readyAttachmentIds.length === 0);
1579
1716
  const handleSend = async () => {
1580
1717
  if (submitDisabled) return;
1581
1718
  const body = value.trim();
1582
- const replyId = replyingTo?.id;
1583
- const attachmentIds = readyAttachmentIds;
1584
- setValue("");
1585
- upload.reset();
1586
1719
  setSending(true);
1587
1720
  try {
1588
- await onSend(body, {
1589
- ...replyId ? { quoted_message_id: replyId } : {},
1590
- ...attachmentIds.length > 0 ? { attachment_ids: attachmentIds } : {}
1591
- });
1721
+ if (isEditing && onSaveEdit) {
1722
+ await onSaveEdit(body);
1723
+ } else {
1724
+ const replyId = replyingTo?.id;
1725
+ const attachmentIds = readyAttachmentIds;
1726
+ setValue("");
1727
+ upload.reset();
1728
+ await onSend(body, {
1729
+ ...replyId ? { quoted_message_id: replyId } : {},
1730
+ ...attachmentIds.length > 0 ? { attachment_ids: attachmentIds } : {}
1731
+ });
1732
+ }
1592
1733
  } finally {
1593
1734
  setSending(false);
1594
1735
  }
1595
1736
  };
1737
+ const paddingBottom = keyboardOpen ? 10 : Math.max(10, insets.bottom);
1596
1738
  return /* @__PURE__ */ jsxRuntime.jsxs(
1597
1739
  reactNative.View,
1598
1740
  {
1599
1741
  style: [
1600
- styles8.wrap,
1742
+ styles9.wrap,
1601
1743
  {
1602
1744
  backgroundColor: theme.colors.surface,
1603
- borderTopColor: theme.colors.border
1745
+ borderTopColor: theme.colors.border,
1746
+ paddingBottom
1604
1747
  }
1605
1748
  ],
1606
1749
  children: [
1607
- replyingTo ? /* @__PURE__ */ jsxRuntime.jsx(
1750
+ isEditing ? /* @__PURE__ */ jsxRuntime.jsx(
1751
+ EditChip,
1752
+ {
1753
+ message: editingMessage,
1754
+ ...onCancelEdit ? { onCancel: onCancelEdit } : {}
1755
+ }
1756
+ ) : replyingTo ? /* @__PURE__ */ jsxRuntime.jsx(
1608
1757
  ReplyChip,
1609
1758
  {
1610
1759
  message: replyingTo,
@@ -1612,14 +1761,14 @@ var MessageComposer = react.forwardRef(
1612
1761
  ...onCancelReply ? { onCancel: onCancelReply } : {}
1613
1762
  }
1614
1763
  ) : null,
1615
- attachments ? /* @__PURE__ */ jsxRuntime.jsx(UploadQueueStrip, {}) : null,
1616
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles8.row, children: [
1764
+ attachments && !isEditing ? /* @__PURE__ */ jsxRuntime.jsx(UploadQueueStrip, {}) : null,
1765
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles9.row, children: [
1617
1766
  attachments && onAttachPress ? /* @__PURE__ */ jsxRuntime.jsx(
1618
1767
  reactNative.Pressable,
1619
1768
  {
1620
1769
  onPress: onAttachPress,
1621
1770
  hitSlop: 8,
1622
- style: styles8.iconBtn,
1771
+ style: styles9.iconBtn,
1623
1772
  accessibilityLabel: "Attach file",
1624
1773
  children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "attachment", size: 22, color: theme.colors.ink2 })
1625
1774
  }
@@ -1638,7 +1787,7 @@ var MessageComposer = react.forwardRef(
1638
1787
  multiline: true,
1639
1788
  editable: !disabled,
1640
1789
  style: [
1641
- styles8.input,
1790
+ styles9.input,
1642
1791
  {
1643
1792
  color: theme.colors.ink,
1644
1793
  backgroundColor: theme.colors.surface2,
@@ -1655,7 +1804,7 @@ var MessageComposer = react.forwardRef(
1655
1804
  onPress: handleSend,
1656
1805
  disabled: submitDisabled,
1657
1806
  style: [
1658
- styles8.sendBtn,
1807
+ styles9.sendBtn,
1659
1808
  {
1660
1809
  backgroundColor: submitDisabled ? theme.colors.ink3 : theme.colors.brand,
1661
1810
  opacity: submitDisabled ? 0.6 : 1
@@ -1671,6 +1820,28 @@ var MessageComposer = react.forwardRef(
1671
1820
  );
1672
1821
  }
1673
1822
  );
1823
+ function EditChip({ message, onCancel }) {
1824
+ const theme = usePoolseTheme();
1825
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1826
+ reactNative.View,
1827
+ {
1828
+ style: [
1829
+ styles9.replyChip,
1830
+ {
1831
+ backgroundColor: theme.colors.surface2,
1832
+ borderLeftColor: theme.colors.brand
1833
+ }
1834
+ ],
1835
+ children: [
1836
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { flex: 1 }, children: [
1837
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles9.replyName, { color: theme.colors.brand }], children: "Editing message" }),
1838
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles9.replyBody, { color: theme.colors.ink2 }], numberOfLines: 1, children: message.body ?? "\u2026" })
1839
+ ] }),
1840
+ 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
1841
+ ]
1842
+ }
1843
+ );
1844
+ }
1674
1845
  function ReplyChip({
1675
1846
  message,
1676
1847
  labelFor,
@@ -1683,7 +1854,7 @@ function ReplyChip({
1683
1854
  reactNative.View,
1684
1855
  {
1685
1856
  style: [
1686
- styles8.replyChip,
1857
+ styles9.replyChip,
1687
1858
  {
1688
1859
  backgroundColor: theme.colors.surface2,
1689
1860
  borderLeftColor: theme.colors.brand
@@ -1691,27 +1862,26 @@ function ReplyChip({
1691
1862
  ],
1692
1863
  children: [
1693
1864
  /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { flex: 1 }, children: [
1694
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { style: [styles8.replyName, { color: theme.colors.brand }], children: [
1865
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { style: [styles9.replyName, { color: theme.colors.brand }], children: [
1695
1866
  "Replying to ",
1696
1867
  senderName
1697
1868
  ] }),
1698
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles8.replyBody, { color: theme.colors.ink2 }], numberOfLines: 1, children: message.body ?? "\u2026" })
1869
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles9.replyBody, { color: theme.colors.ink2 }], numberOfLines: 1, children: message.body ?? "\u2026" })
1699
1870
  ] }),
1700
- 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
1871
+ 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
1701
1872
  ]
1702
1873
  }
1703
1874
  );
1704
1875
  }
1705
- var styles8 = reactNative.StyleSheet.create({
1876
+ var styles9 = reactNative.StyleSheet.create({
1706
1877
  wrap: {
1707
1878
  borderTopWidth: reactNative.StyleSheet.hairlineWidth,
1708
1879
  paddingTop: 8,
1709
- paddingBottom: 10,
1710
1880
  paddingHorizontal: 12
1711
- // Note: no safe-area inset here on purpose. Wrap your chat
1712
- // surface in <SafeAreaView> (or <SafeAreaView edges={['bottom']}>)
1713
- // at the app root so the home-indicator clearance is handled
1714
- // once. Doing it here too produces double padding.
1881
+ // paddingBottom is applied inline — it follows the keyboard state
1882
+ // (10 px while open, max(10, safeBottomInset) while closed) so the
1883
+ // send/attach buttons stay clear of the home indicator without
1884
+ // double-padding above the keyboard.
1715
1885
  },
1716
1886
  row: {
1717
1887
  flexDirection: "row",
@@ -1800,11 +1970,11 @@ function MentionInput({ conversationId, onSend, ...composerProps }) {
1800
1970
  transparent: true,
1801
1971
  animationType: "slide",
1802
1972
  onRequestClose: () => setPickerOpen(false),
1803
- children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { style: styles9.backdrop, onPress: () => setPickerOpen(false), children: /* @__PURE__ */ jsxRuntime.jsxs(
1973
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { style: styles10.backdrop, onPress: () => setPickerOpen(false), children: /* @__PURE__ */ jsxRuntime.jsxs(
1804
1974
  reactNative.View,
1805
1975
  {
1806
1976
  style: [
1807
- styles9.sheet,
1977
+ styles10.sheet,
1808
1978
  {
1809
1979
  backgroundColor: theme.colors.surface,
1810
1980
  borderTopLeftRadius: theme.radii.lg,
@@ -1815,7 +1985,7 @@ function MentionInput({ conversationId, onSend, ...composerProps }) {
1815
1985
  /* @__PURE__ */ jsxRuntime.jsx(
1816
1986
  reactNative.Text,
1817
1987
  {
1818
- style: [styles9.title, { color: theme.colors.ink, fontFamily: theme.type.fontBody }],
1988
+ style: [styles10.title, { color: theme.colors.ink, fontFamily: theme.type.fontBody }],
1819
1989
  children: "Mention a member"
1820
1990
  }
1821
1991
  ),
@@ -1846,12 +2016,12 @@ function MentionInput({ conversationId, onSend, ...composerProps }) {
1846
2016
  }
1847
2017
  function MentionRow({ member, onPress }) {
1848
2018
  const theme = usePoolseTheme();
1849
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Pressable, { onPress, style: styles9.row, children: [
2019
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Pressable, { onPress, style: styles10.row, children: [
1850
2020
  /* @__PURE__ */ jsxRuntime.jsx(Avatar, { src: null, name: member.external_id, seed: member.external_id, size: "sm" }),
1851
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles9.rowText, { color: theme.colors.ink, fontFamily: theme.type.fontBody }], children: member.external_id })
2021
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles10.rowText, { color: theme.colors.ink, fontFamily: theme.type.fontBody }], children: member.external_id })
1852
2022
  ] });
1853
2023
  }
1854
- var styles9 = reactNative.StyleSheet.create({
2024
+ var styles10 = reactNative.StyleSheet.create({
1855
2025
  backdrop: {
1856
2026
  flex: 1,
1857
2027
  backgroundColor: "rgba(0,0,0,0.35)",
@@ -1878,138 +2048,134 @@ var styles9 = reactNative.StyleSheet.create({
1878
2048
  fontSize: 14
1879
2049
  }
1880
2050
  });
2051
+ var AT_BOTTOM_THRESHOLD = 32;
1881
2052
  function MessageList({
1882
2053
  messages,
2054
+ items,
1883
2055
  loading = false,
1884
2056
  hasMore = false,
1885
2057
  onLoadMore,
1886
2058
  renderItem,
1887
2059
  ListHeader = null,
1888
2060
  ListFooter = null,
1889
- emptyState
2061
+ emptyState,
2062
+ meId = null
1890
2063
  }) {
1891
2064
  const theme = usePoolseTheme();
1892
- const data = [...messages].reverse();
2065
+ const flatListRef = react.useRef(null);
2066
+ const [atBottom, setAtBottom] = react.useState(true);
2067
+ const [newCount, setNewCount] = react.useState(0);
2068
+ const lastTailIdRef = react.useRef(null);
2069
+ const sourceItems = items ?? messages.map((msg) => ({ kind: "msg", msg }));
2070
+ const data = [...sourceItems].reverse();
2071
+ react.useEffect(() => {
2072
+ const tail = messages[messages.length - 1];
2073
+ if (!tail) return;
2074
+ const tailId = tail.id;
2075
+ if (tailId === lastTailIdRef.current) return;
2076
+ const isFirstSet = lastTailIdRef.current === null;
2077
+ lastTailIdRef.current = tailId;
2078
+ if (atBottom || isFirstSet) {
2079
+ setNewCount(0);
2080
+ return;
2081
+ }
2082
+ if (meId !== null && tail.sender_id === meId) return;
2083
+ setNewCount((n) => n + 1);
2084
+ }, [messages, atBottom, meId]);
2085
+ const handleScroll = react.useCallback((e) => {
2086
+ const y = e.nativeEvent.contentOffset.y;
2087
+ const next = y <= AT_BOTTOM_THRESHOLD;
2088
+ setAtBottom((prev) => prev === next ? prev : next);
2089
+ if (next) setNewCount(0);
2090
+ }, []);
2091
+ const scrollToBottom = react.useCallback(() => {
2092
+ flatListRef.current?.scrollToOffset({ offset: 0, animated: true });
2093
+ setNewCount(0);
2094
+ }, []);
1893
2095
  const handleRender = react.useCallback(
1894
- ({ item, index }) => renderItem(item, messages.length - 1 - index),
1895
- [renderItem, messages.length]
2096
+ ({ item, index }) => {
2097
+ if (item.kind === "separator") return item.node;
2098
+ const sourceIndex = data.length - 1 - index;
2099
+ return renderItem(item.msg, sourceIndex);
2100
+ },
2101
+ [renderItem, data.length]
1896
2102
  );
1897
2103
  if (loading && messages.length === 0) {
1898
- return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles10.placeholder, { backgroundColor: theme.colors.paper }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: theme.colors.brand }) });
2104
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles11.placeholder, { backgroundColor: theme.colors.paper }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: theme.colors.brand }) });
1899
2105
  }
1900
2106
  if (messages.length === 0) {
1901
- 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 });
2107
+ 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 });
1902
2108
  }
1903
- return /* @__PURE__ */ jsxRuntime.jsx(
1904
- reactNative.FlatList,
1905
- {
1906
- inverted: true,
1907
- data,
1908
- keyExtractor: (item) => item.id,
1909
- renderItem: handleRender,
1910
- onEndReached: hasMore ? onLoadMore : void 0,
1911
- onEndReachedThreshold: 0.3,
1912
- ListHeaderComponent: ListHeader,
1913
- ListFooterComponent: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { children: [
1914
- hasMore && loading ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: theme.colors.ink3, style: { marginVertical: 8 } }) : null,
1915
- ListFooter
1916
- ] }),
1917
- style: { backgroundColor: theme.colors.paper },
1918
- contentContainerStyle: { paddingVertical: 8 }
1919
- }
1920
- );
2109
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles11.root, children: [
2110
+ /* @__PURE__ */ jsxRuntime.jsx(
2111
+ reactNative.FlatList,
2112
+ {
2113
+ ref: flatListRef,
2114
+ inverted: true,
2115
+ data,
2116
+ keyExtractor: (item) => item.kind === "msg" ? item.msg.id : item.id,
2117
+ renderItem: handleRender,
2118
+ onEndReached: hasMore ? onLoadMore : void 0,
2119
+ onEndReachedThreshold: 0.3,
2120
+ onScroll: handleScroll,
2121
+ scrollEventThrottle: 32,
2122
+ ListHeaderComponent: ListHeader,
2123
+ ListFooterComponent: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { children: [
2124
+ hasMore && loading ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: theme.colors.ink3, style: { marginVertical: 8 } }) : null,
2125
+ ListFooter
2126
+ ] }),
2127
+ style: { backgroundColor: theme.colors.paper },
2128
+ contentContainerStyle: { paddingVertical: 8 }
2129
+ }
2130
+ ),
2131
+ !atBottom && newCount > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(
2132
+ reactNative.Pressable,
2133
+ {
2134
+ onPress: scrollToBottom,
2135
+ style: [
2136
+ styles11.jumpButton,
2137
+ {
2138
+ backgroundColor: theme.colors.brand
2139
+ },
2140
+ theme.shadows.sm
2141
+ ],
2142
+ accessibilityLabel: `${newCount} new ${newCount === 1 ? "message" : "messages"}, jump to latest`,
2143
+ children: [
2144
+ /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "chevron-down", size: 14, color: theme.colors.onBrand }),
2145
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { style: [styles11.jumpText, { color: theme.colors.onBrand }], children: [
2146
+ newCount,
2147
+ " new ",
2148
+ newCount === 1 ? "message" : "messages"
2149
+ ] })
2150
+ ]
2151
+ }
2152
+ ) : null
2153
+ ] });
1921
2154
  }
1922
- var styles10 = reactNative.StyleSheet.create({
2155
+ var styles11 = reactNative.StyleSheet.create({
2156
+ root: {
2157
+ flex: 1
2158
+ },
1923
2159
  placeholder: {
1924
2160
  flex: 1,
1925
2161
  alignItems: "center",
1926
2162
  justifyContent: "center",
1927
2163
  padding: 24
1928
- }
1929
- });
1930
- function EditableMessageBubble({
1931
- message,
1932
- currentUserId,
1933
- onCancel,
1934
- onSave
1935
- }) {
1936
- const theme = usePoolseTheme();
1937
- const isSelf = currentUserId !== null && message.sender_id === currentUserId;
1938
- const [value, setValue] = react.useState(message.body ?? "");
1939
- const [saving, setSaving] = react.useState(false);
1940
- const submit = async () => {
1941
- if (saving || !value.trim()) return;
1942
- setSaving(true);
1943
- try {
1944
- await onSave(value.trim());
1945
- } finally {
1946
- setSaving(false);
1947
- }
1948
- };
1949
- const bg = isSelf ? theme.colors.selfBubble : theme.colors.otherBubble;
1950
- const ink = isSelf ? theme.colors.selfBubbleText : theme.colors.otherBubbleText;
1951
- return /* @__PURE__ */ jsxRuntime.jsx(
1952
- reactNative.View,
1953
- {
1954
- style: [
1955
- styles11.wrap,
1956
- {
1957
- alignSelf: isSelf ? "flex-end" : "flex-start",
1958
- maxWidth: "80%"
1959
- }
1960
- ],
1961
- children: /* @__PURE__ */ jsxRuntime.jsxs(
1962
- reactNative.View,
1963
- {
1964
- style: [
1965
- styles11.bubble,
1966
- {
1967
- backgroundColor: bg,
1968
- borderRadius: theme.radii.bubble,
1969
- paddingHorizontal: theme.spacing.md,
1970
- paddingVertical: theme.spacing.sm
1971
- },
1972
- theme.shadows.sm
1973
- ],
1974
- children: [
1975
- /* @__PURE__ */ jsxRuntime.jsx(
1976
- reactNative.TextInput,
1977
- {
1978
- value,
1979
- onChangeText: setValue,
1980
- multiline: true,
1981
- autoFocus: true,
1982
- style: {
1983
- color: ink,
1984
- fontFamily: theme.type.fontBody,
1985
- fontSize: theme.type.bodySize,
1986
- minHeight: 24
1987
- }
1988
- }
1989
- ),
1990
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles11.actions, children: [
1991
- /* @__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" }) }),
1992
- /* @__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" }) })
1993
- ] })
1994
- ]
1995
- }
1996
- )
1997
- }
1998
- );
1999
- }
2000
- var styles11 = reactNative.StyleSheet.create({
2001
- wrap: {
2002
- marginBottom: 4
2003
2164
  },
2004
- bubble: {},
2005
- actions: {
2165
+ jumpButton: {
2166
+ position: "absolute",
2167
+ alignSelf: "center",
2168
+ bottom: 12,
2006
2169
  flexDirection: "row",
2007
- justifyContent: "flex-end",
2008
- gap: 12,
2009
- marginTop: 6
2170
+ alignItems: "center",
2171
+ gap: 6,
2172
+ paddingHorizontal: 14,
2173
+ paddingVertical: 8,
2174
+ borderRadius: 999
2010
2175
  },
2011
- btn: {
2012
- fontSize: 13
2176
+ jumpText: {
2177
+ fontSize: 13,
2178
+ fontWeight: "600"
2013
2179
  }
2014
2180
  });
2015
2181
  var DEFAULT_REACTION_EMOJIS = ["\u{1F44D}", "\u2764\uFE0F", "\u{1F602}", "\u{1F389}", "\u{1F64F}", "\u{1F525}"];
@@ -2472,6 +2638,53 @@ var styles14 = reactNative.StyleSheet.create({
2472
2638
  borderRadius: 20
2473
2639
  }
2474
2640
  });
2641
+ function renderMarkdown(body, args) {
2642
+ const baseText = {
2643
+ color: args.textColor,
2644
+ fontFamily: args.fontFamily,
2645
+ fontSize: args.fontSize,
2646
+ lineHeight: args.lineHeight
2647
+ };
2648
+ const style = {
2649
+ body: baseText,
2650
+ paragraph: { ...baseText, marginTop: 0, marginBottom: 0 },
2651
+ text: baseText,
2652
+ strong: { ...baseText, fontWeight: "700" },
2653
+ em: { ...baseText, fontStyle: "italic" },
2654
+ s: { ...baseText, textDecorationLine: "line-through" },
2655
+ link: { color: args.linkColor, textDecorationLine: "underline" },
2656
+ code_inline: {
2657
+ ...baseText,
2658
+ backgroundColor: "rgba(127,127,127,0.18)",
2659
+ borderRadius: 4,
2660
+ paddingHorizontal: 4
2661
+ },
2662
+ code_block: {
2663
+ ...baseText,
2664
+ backgroundColor: "rgba(127,127,127,0.18)",
2665
+ padding: 8,
2666
+ borderRadius: 6
2667
+ },
2668
+ fence: {
2669
+ ...baseText,
2670
+ backgroundColor: "rgba(127,127,127,0.18)",
2671
+ padding: 8,
2672
+ borderRadius: 6
2673
+ },
2674
+ bullet_list: { marginTop: 2, marginBottom: 2 },
2675
+ ordered_list: { marginTop: 2, marginBottom: 2 },
2676
+ list_item: baseText,
2677
+ blockquote: {
2678
+ ...baseText,
2679
+ borderLeftWidth: 3,
2680
+ borderLeftColor: args.linkColor,
2681
+ paddingLeft: 8,
2682
+ marginVertical: 4,
2683
+ opacity: 0.85
2684
+ }
2685
+ };
2686
+ return react.createElement(Markdown__default.default, { style, onLinkPress: args.onLinkPress }, body);
2687
+ }
2475
2688
  function MessageBubble({
2476
2689
  message,
2477
2690
  currentUserId,
@@ -2482,7 +2695,8 @@ function MessageBubble({
2482
2695
  maxBodyLength = 0,
2483
2696
  showSenderName = false,
2484
2697
  actionsTrigger,
2485
- showAttachments = true
2698
+ showAttachments = true,
2699
+ markdown = true
2486
2700
  }) {
2487
2701
  const theme = usePoolseTheme();
2488
2702
  const { width: screenWidth } = reactNative.useWindowDimensions();
@@ -2567,20 +2781,44 @@ function MessageBubble({
2567
2781
  ) : null,
2568
2782
  renderAttachments && images.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(ImageMosaic, { images, maxWidth: contentMaxWidth }) : null,
2569
2783
  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,
2570
- body.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(
2784
+ body.length > 0 ? isDeleted ? /* @__PURE__ */ jsxRuntime.jsx(
2571
2785
  reactNative.Text,
2572
2786
  {
2573
2787
  style: [
2574
2788
  styles15.body,
2575
2789
  {
2576
- color: isDeleted ? theme.colors.ink3 : textColor,
2790
+ color: theme.colors.ink3,
2577
2791
  fontFamily: theme.type.fontBody,
2578
2792
  fontSize: theme.type.bodySize,
2579
2793
  lineHeight: theme.type.lineHeight,
2580
- fontStyle: isDeleted ? "italic" : "normal"
2794
+ fontStyle: "italic"
2581
2795
  }
2582
2796
  ],
2583
- children: isDeleted ? "Message deleted" : truncated
2797
+ children: "Message deleted"
2798
+ }
2799
+ ) : markdown ? renderMarkdown(truncated, {
2800
+ textColor,
2801
+ linkColor: isSelf ? theme.colors.onBrand : theme.colors.brand,
2802
+ fontFamily: theme.type.fontBody,
2803
+ fontSize: theme.type.bodySize,
2804
+ lineHeight: theme.type.lineHeight,
2805
+ onLinkPress: (url) => {
2806
+ void reactNative.Linking.openURL(url).catch(() => void 0);
2807
+ return false;
2808
+ }
2809
+ }) : /* @__PURE__ */ jsxRuntime.jsx(
2810
+ reactNative.Text,
2811
+ {
2812
+ style: [
2813
+ styles15.body,
2814
+ {
2815
+ color: textColor,
2816
+ fontFamily: theme.type.fontBody,
2817
+ fontSize: theme.type.bodySize,
2818
+ lineHeight: theme.type.lineHeight
2819
+ }
2820
+ ],
2821
+ children: truncated
2584
2822
  }
2585
2823
  ) : null,
2586
2824
  maxBodyLength > 0 && body.length > maxBodyLength ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: () => setExpanded((e) => !e), children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -2675,9 +2913,9 @@ function formatTime(iso) {
2675
2913
  try {
2676
2914
  const d = new Date(iso);
2677
2915
  const now = /* @__PURE__ */ new Date();
2678
- const sameDay = isSameDay(d, now);
2916
+ const sameDay2 = isSameDay(d, now);
2679
2917
  const time = d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
2680
- if (sameDay) return time;
2918
+ if (sameDay2) return time;
2681
2919
  const yesterday = new Date(now);
2682
2920
  yesterday.setDate(now.getDate() - 1);
2683
2921
  if (isSameDay(d, yesterday)) return `Yesterday ${time}`;
@@ -2785,12 +3023,26 @@ function ReactionStrip({
2785
3023
  styles16.chip,
2786
3024
  {
2787
3025
  backgroundColor: meReacted ? theme.colors.brandSoft : theme.colors.surface2,
2788
- borderColor: meReacted ? theme.colors.brand : theme.colors.border
3026
+ borderColor: meReacted ? theme.colors.brand : theme.colors.border,
3027
+ borderWidth: meReacted ? 1.5 : 1,
3028
+ transform: meReacted ? [{ scale: 1.04 }] : void 0
2789
3029
  }
2790
3030
  ],
2791
3031
  children: [
2792
3032
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles16.emoji, children: emoji }),
2793
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles16.count, { color: theme.colors.ink2 }], children: userArr.length })
3033
+ /* @__PURE__ */ jsxRuntime.jsx(
3034
+ reactNative.Text,
3035
+ {
3036
+ style: [
3037
+ styles16.count,
3038
+ {
3039
+ color: meReacted ? theme.colors.brand : theme.colors.ink2,
3040
+ fontWeight: meReacted ? "700" : "600"
3041
+ }
3042
+ ],
3043
+ children: userArr.length
3044
+ }
3045
+ )
2794
3046
  ]
2795
3047
  },
2796
3048
  emoji
@@ -2843,7 +3095,6 @@ var styles16 = reactNative.StyleSheet.create({
2843
3095
  paddingHorizontal: 9,
2844
3096
  paddingVertical: 4,
2845
3097
  borderRadius: 14,
2846
- borderWidth: 1,
2847
3098
  gap: 5,
2848
3099
  minHeight: 26,
2849
3100
  // Subtle elevation so chips lift off the surface
@@ -2859,7 +3110,6 @@ var styles16 = reactNative.StyleSheet.create({
2859
3110
  },
2860
3111
  count: {
2861
3112
  fontSize: 12,
2862
- fontWeight: "600",
2863
3113
  fontVariant: ["tabular-nums"]
2864
3114
  },
2865
3115
  backdrop: {
@@ -2897,11 +3147,10 @@ function MessageRow({
2897
3147
  actions = true,
2898
3148
  threads = true,
2899
3149
  quotations = true,
3150
+ markdown = true,
2900
3151
  readState,
2901
- editing = false,
3152
+ isEditing = false,
2902
3153
  onStartEdit,
2903
- onCancelEdit,
2904
- onSaveEdit,
2905
3154
  onDelete,
2906
3155
  onOpenThread,
2907
3156
  onQuote,
@@ -2922,7 +3171,7 @@ function MessageRow({
2922
3171
  ...msg.reactions ? { initialReactions: msg.reactions } : {}
2923
3172
  });
2924
3173
  const translateX = react.useRef(new reactNative.Animated.Value(0)).current;
2925
- const swipeEnabled = quotations && !!onQuote && !editing;
3174
+ const swipeEnabled = quotations && !!onQuote && !isEditing;
2926
3175
  const panResponder = react.useMemo(
2927
3176
  () => reactNative.PanResponder.create({
2928
3177
  // Tap / long-press still go to the inner Pressable.
@@ -2997,137 +3246,149 @@ function MessageRow({
2997
3246
  const rowSender = react$1.useUser(showAvatarSlot ? msg.sender_external_id : null);
2998
3247
  const avatarUrl = msg.sender_external_id ? avatarFor?.(msg.sender_external_id) ?? rowSender.profile?.avatarUrl ?? null : null;
2999
3248
  const senderLabel = msg.sender_external_id ? labelFor?.(msg.sender_external_id) ?? rowSender.profile?.displayName ?? msg.sender_external_id : null;
3000
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles17.row, { alignSelf: isSelf ? "flex-end" : "flex-start" }], children: [
3001
- showAvatarSlot ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles17.avatarSlot, children: showAvatarRender ? /* @__PURE__ */ jsxRuntime.jsx(
3002
- Avatar,
3003
- {
3004
- src: avatarUrl ?? null,
3005
- name: senderLabel,
3006
- seed: msg.sender_external_id ?? null,
3007
- size: "sm"
3008
- }
3009
- ) : null }) : null,
3010
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles17.bubbleCol, children: [
3011
- swipeEnabled ? /* @__PURE__ */ jsxRuntime.jsx(
3012
- reactNative.Animated.View,
3013
- {
3014
- pointerEvents: "none",
3015
- style: [
3016
- styles17.replyHint,
3017
- isSelf ? styles17.replyHintRight : styles17.replyHintLeft,
3018
- {
3019
- opacity: replyIconOpacity,
3020
- transform: [{ scale: replyIconScale }],
3021
- backgroundColor: theme.colors.surface2
3022
- }
3023
- ],
3024
- children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "reply", size: 16, color: theme.colors.brand })
3025
- }
3026
- ) : null,
3027
- /* @__PURE__ */ jsxRuntime.jsx(
3028
- reactNative.Animated.View,
3029
- {
3030
- ...swipeEnabled ? panResponder.panHandlers : {},
3031
- style: { transform: [{ translateX }] },
3032
- children: /* @__PURE__ */ jsxRuntime.jsx(
3033
- reactNative.Pressable,
3249
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3250
+ reactNative.View,
3251
+ {
3252
+ style: [
3253
+ styles17.row,
3254
+ { alignSelf: isSelf ? "flex-end" : "flex-start" },
3255
+ // Lift this row above the dim backdrop the parent renders
3256
+ // during edit mode. zIndex covers iOS / web; elevation does
3257
+ // the equivalent on Android.
3258
+ isEditing ? styles17.rowEditing : null
3259
+ ],
3260
+ children: [
3261
+ showAvatarSlot ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles17.avatarSlot, children: showAvatarRender ? /* @__PURE__ */ jsxRuntime.jsx(
3262
+ Avatar,
3263
+ {
3264
+ src: avatarUrl ?? null,
3265
+ name: senderLabel,
3266
+ seed: msg.sender_external_id ?? null,
3267
+ size: "sm"
3268
+ }
3269
+ ) : null }) : null,
3270
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles17.bubbleCol, children: [
3271
+ swipeEnabled ? /* @__PURE__ */ jsxRuntime.jsx(
3272
+ reactNative.Animated.View,
3034
3273
  {
3035
- onLongPress: () => {
3036
- if (!actions) return;
3037
- setActionsOpen(true);
3038
- },
3039
- accessibilityRole: "button",
3040
- accessibilityLabel: "Message actions",
3041
- children: editing ? /* @__PURE__ */ jsxRuntime.jsx(
3042
- EditableMessageBubble,
3274
+ pointerEvents: "none",
3275
+ style: [
3276
+ styles17.replyHint,
3277
+ isSelf ? styles17.replyHintRight : styles17.replyHintLeft,
3043
3278
  {
3044
- message: msg,
3045
- currentUserId: meId,
3046
- onCancel: onCancelEdit ?? (() => void 0),
3047
- onSave: onSaveEdit ?? (() => void 0)
3279
+ opacity: replyIconOpacity,
3280
+ transform: [{ scale: replyIconScale }],
3281
+ backgroundColor: theme.colors.surface2
3048
3282
  }
3049
- ) : /* @__PURE__ */ jsxRuntime.jsx(
3050
- MessageBubble,
3283
+ ],
3284
+ children: /* @__PURE__ */ jsxRuntime.jsx(PoolseIcon, { name: "reply", size: 16, color: theme.colors.brand })
3285
+ }
3286
+ ) : null,
3287
+ /* @__PURE__ */ jsxRuntime.jsx(
3288
+ reactNative.Animated.View,
3289
+ {
3290
+ ...swipeEnabled ? panResponder.panHandlers : {},
3291
+ style: { transform: [{ translateX }] },
3292
+ children: /* @__PURE__ */ jsxRuntime.jsx(
3293
+ reactNative.Pressable,
3051
3294
  {
3052
- message: msg,
3053
- currentUserId: meId,
3054
- ...readState ? { readState } : {},
3055
- ...labelFor ? { labelFor } : {},
3056
- ...onQuotedClick ? { onQuotedClick } : {},
3057
- groupPosition,
3058
- maxBodyLength,
3059
- showSenderName,
3060
- showAttachments: attachments,
3061
- actionsTrigger: actions ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: () => setActionsOpen(true), hitSlop: 8, children: /* @__PURE__ */ jsxRuntime.jsx(
3062
- PoolseIcon,
3295
+ onLongPress: () => {
3296
+ if (!actions) return;
3297
+ setActionsOpen(true);
3298
+ },
3299
+ accessibilityRole: "button",
3300
+ accessibilityLabel: "Message actions",
3301
+ children: /* @__PURE__ */ jsxRuntime.jsx(
3302
+ MessageBubble,
3063
3303
  {
3064
- name: "chevron-down",
3065
- size: 14,
3066
- color: isSelf ? theme.colors.onBrand : theme.colors.ink3
3304
+ message: msg,
3305
+ currentUserId: meId,
3306
+ ...readState ? { readState } : {},
3307
+ ...labelFor ? { labelFor } : {},
3308
+ ...onQuotedClick ? { onQuotedClick } : {},
3309
+ groupPosition,
3310
+ maxBodyLength,
3311
+ showSenderName,
3312
+ showAttachments: attachments,
3313
+ markdown,
3314
+ actionsTrigger: actions ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: () => setActionsOpen(true), hitSlop: 8, children: /* @__PURE__ */ jsxRuntime.jsx(
3315
+ PoolseIcon,
3316
+ {
3317
+ name: "chevron-down",
3318
+ size: 14,
3319
+ color: isSelf ? theme.colors.onBrand : theme.colors.ink3
3320
+ }
3321
+ ) }) : null
3067
3322
  }
3068
- ) }) : null
3323
+ )
3069
3324
  }
3070
3325
  )
3071
3326
  }
3072
- )
3073
- }
3074
- ),
3075
- 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: [
3076
- "\u{1F4AC} ",
3077
- msg.reply_count,
3078
- " ",
3079
- msg.reply_count === 1 ? "reply" : "replies"
3080
- ] }) }) : null,
3081
- reactions ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles17.reactionsRow, { alignSelf: isSelf ? "flex-end" : "flex-start" }], children: /* @__PURE__ */ jsxRuntime.jsx(
3082
- ReactionStrip,
3083
- {
3084
- messageId: msg.id,
3085
- conversationId: msg.conversation_id,
3086
- meId,
3087
- ...msg.reactions ? { initialReactions: msg.reactions } : {}
3088
- }
3089
- ) }) : null
3090
- ] }),
3091
- actions && actionsOpen ? /* @__PURE__ */ jsxRuntime.jsx(
3092
- MessageActions,
3093
- {
3094
- visible: actionsOpen,
3095
- onClose: () => setActionsOpen(false),
3096
- isSelf,
3097
- ...reactions ? {
3098
- // Inline quick-react row at the top of the sheet —
3099
- // one tap = react + close, no second modal needed.
3100
- onPickReaction: (emoji) => {
3101
- void reactionsApi.addReaction(emoji);
3102
- }
3103
- } : {},
3104
- ...onStartEdit ? {
3105
- onEdit: () => {
3106
- setActionsOpen(false);
3107
- onStartEdit();
3108
- }
3109
- } : {},
3110
- ...onDelete ? {
3111
- onDelete: () => {
3112
- setActionsOpen(false);
3113
- onDelete();
3114
- }
3115
- } : {},
3116
- ...onOpenThread ? {
3117
- onReplyInThread: () => {
3118
- setActionsOpen(false);
3119
- onOpenThread();
3120
- }
3121
- } : {},
3122
- ...quotations && onQuote ? {
3123
- onQuote: () => {
3124
- setActionsOpen(false);
3125
- onQuote();
3327
+ ),
3328
+ 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: [
3329
+ "\u{1F4AC} ",
3330
+ msg.reply_count,
3331
+ " ",
3332
+ msg.reply_count === 1 ? "reply" : "replies"
3333
+ ] }) }) : null,
3334
+ reactions ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles17.reactionsRow, { alignSelf: isSelf ? "flex-end" : "flex-start" }], children: /* @__PURE__ */ jsxRuntime.jsx(
3335
+ ReactionStrip,
3336
+ {
3337
+ messageId: msg.id,
3338
+ conversationId: msg.conversation_id,
3339
+ meId,
3340
+ ...msg.reactions ? { initialReactions: msg.reactions } : {}
3341
+ }
3342
+ ) }) : null
3343
+ ] }),
3344
+ actions && actionsOpen ? /* @__PURE__ */ jsxRuntime.jsx(
3345
+ MessageActions,
3346
+ {
3347
+ visible: actionsOpen,
3348
+ onClose: () => setActionsOpen(false),
3349
+ isSelf,
3350
+ ...reactions ? {
3351
+ // Inline quick-react row at the top of the sheet —
3352
+ // one tap = react + close, no second modal needed.
3353
+ onPickReaction: (emoji) => {
3354
+ void reactionsApi.addReaction(emoji);
3355
+ }
3356
+ } : {},
3357
+ ...msg.body && !msg.deleted_at ? {
3358
+ onCopy: () => {
3359
+ setActionsOpen(false);
3360
+ void Clipboard__namespace.setStringAsync(msg.body ?? "");
3361
+ }
3362
+ } : {},
3363
+ ...onStartEdit ? {
3364
+ onEdit: () => {
3365
+ setActionsOpen(false);
3366
+ onStartEdit();
3367
+ }
3368
+ } : {},
3369
+ ...onDelete ? {
3370
+ onDelete: () => {
3371
+ setActionsOpen(false);
3372
+ onDelete();
3373
+ }
3374
+ } : {},
3375
+ ...onOpenThread ? {
3376
+ onReplyInThread: () => {
3377
+ setActionsOpen(false);
3378
+ onOpenThread();
3379
+ }
3380
+ } : {},
3381
+ ...quotations && onQuote ? {
3382
+ onQuote: () => {
3383
+ setActionsOpen(false);
3384
+ onQuote();
3385
+ }
3386
+ } : {}
3126
3387
  }
3127
- } : {}
3128
- }
3129
- ) : null
3130
- ] });
3388
+ ) : null
3389
+ ]
3390
+ }
3391
+ );
3131
3392
  }
3132
3393
  var styles17 = reactNative.StyleSheet.create({
3133
3394
  row: {
@@ -3143,6 +3404,12 @@ var styles17 = reactNative.StyleSheet.create({
3143
3404
  paddingHorizontal: 10,
3144
3405
  maxWidth: "95%"
3145
3406
  },
3407
+ rowEditing: {
3408
+ // Above ConversationView's editBackdrop (zIndex 5). Elevation
3409
+ // does the equivalent stacking on Android.
3410
+ zIndex: 10,
3411
+ elevation: 10
3412
+ },
3146
3413
  avatarSlot: {
3147
3414
  width: 36,
3148
3415
  marginRight: 6
@@ -3237,31 +3504,34 @@ function ThreadSheet({
3237
3504
  const [pickerOpen, setPickerOpen] = react.useState(false);
3238
3505
  const [editingId, setEditingId] = react.useState(null);
3239
3506
  const renderReply = react.useCallback(
3240
- (msg) => /* @__PURE__ */ jsxRuntime.jsx(
3241
- MessageRow,
3242
- {
3243
- msg,
3244
- meId,
3245
- reactions: true,
3246
- attachments: true,
3247
- actions: true,
3248
- threads: false,
3249
- quotations: false,
3250
- editing: editingId === msg.id,
3251
- onStartEdit: () => setEditingId(msg.id),
3252
- onCancelEdit: () => setEditingId(null),
3253
- onSaveEdit: async (body) => {
3254
- await edit(msg.id, body);
3255
- setEditingId(null);
3256
- },
3257
- onDelete: () => {
3258
- void deleteReply(msg.id);
3259
- },
3260
- ...labelFor ? { labelFor } : {},
3261
- ...avatarFor ? { avatarFor } : {}
3262
- }
3263
- ),
3264
- [meId, editingId, edit, deleteReply, labelFor, avatarFor]
3507
+ (msg) => {
3508
+ const isSelf = meId !== null && msg.sender_id === meId;
3509
+ const canEdit = isSelf && !msg.deleted_at;
3510
+ return /* @__PURE__ */ jsxRuntime.jsx(
3511
+ MessageRow,
3512
+ {
3513
+ msg,
3514
+ meId,
3515
+ reactions: true,
3516
+ attachments: true,
3517
+ actions: true,
3518
+ threads: false,
3519
+ quotations: false,
3520
+ isEditing: editingId === msg.id,
3521
+ ...canEdit ? { onStartEdit: () => setEditingId(msg.id) } : {},
3522
+ onDelete: () => {
3523
+ void deleteReply(msg.id);
3524
+ },
3525
+ ...labelFor ? { labelFor } : {},
3526
+ ...avatarFor ? { avatarFor } : {}
3527
+ }
3528
+ );
3529
+ },
3530
+ [meId, editingId, deleteReply, labelFor, avatarFor]
3531
+ );
3532
+ const editingMessage = react.useMemo(
3533
+ () => editingId ? replies.find((m) => m.id === editingId) ?? null : null,
3534
+ [editingId, replies]
3265
3535
  );
3266
3536
  const rootRow = react.useMemo(
3267
3537
  () => /* @__PURE__ */ jsxRuntime.jsx(
@@ -3347,7 +3617,14 @@ function ThreadSheet({
3347
3617
  },
3348
3618
  placeholder: "Reply to thread\u2026",
3349
3619
  attachments: true,
3350
- onAttachPress: () => setPickerOpen(true)
3620
+ onAttachPress: () => setPickerOpen(true),
3621
+ editingMessage,
3622
+ onCancelEdit: () => setEditingId(null),
3623
+ onSaveEdit: async (body) => {
3624
+ if (!editingId) return;
3625
+ await edit(editingId, body);
3626
+ setEditingId(null);
3627
+ }
3351
3628
  }
3352
3629
  )
3353
3630
  ]
@@ -3511,6 +3788,7 @@ var styles19 = reactNative.StyleSheet.create({
3511
3788
  fontSize: 12
3512
3789
  }
3513
3790
  });
3791
+ var GROUPING_WINDOW_MS = 5 * 60 * 1e3;
3514
3792
  function ConversationView({
3515
3793
  conversationId,
3516
3794
  emptyState,
@@ -3527,6 +3805,7 @@ function ConversationView({
3527
3805
  threads = true,
3528
3806
  quotations = true,
3529
3807
  readReceipts = true,
3808
+ markdown = true,
3530
3809
  maxBodyLength = 200,
3531
3810
  senderLabels = "auto",
3532
3811
  avatars = "auto",
@@ -3545,9 +3824,25 @@ function ConversationView({
3545
3824
  send,
3546
3825
  edit,
3547
3826
  delete: deleteMsg,
3548
- markReadUpTo
3827
+ markReadUpTo,
3828
+ refetch
3549
3829
  } = react$1.useMessages(conversationId);
3550
3830
  const { typing, signalTyping } = react$1.useTyping(conversationId);
3831
+ const realtimeStatus = react$1.useRealtimeStatus();
3832
+ react.useEffect(() => {
3833
+ const sub = reactNative.AppState.addEventListener("change", (next) => {
3834
+ if (next === "active") void refetch();
3835
+ });
3836
+ return () => sub.remove();
3837
+ }, [refetch]);
3838
+ const prevStatusRef = react.useRef(realtimeStatus);
3839
+ react.useEffect(() => {
3840
+ const prev = prevStatusRef.current;
3841
+ prevStatusRef.current = realtimeStatus;
3842
+ if (realtimeStatus === "connected" && (prev === "reconnecting" || prev === "closed" || prev === "connecting")) {
3843
+ void refetch();
3844
+ }
3845
+ }, [realtimeStatus, refetch]);
3551
3846
  const wantsMembers = mentions || senderLabels === "auto" || avatars === "auto";
3552
3847
  const { members } = react$1.useMembers(wantsMembers ? conversationId : "");
3553
3848
  const isGroupChat = members.length > 2;
@@ -3578,10 +3873,24 @@ function ConversationView({
3578
3873
  });
3579
3874
  onMarkedRead?.(conversationId, latest.id);
3580
3875
  }, [messages, meId, readReceipts, markReadUpTo, onMarkedRead, conversationId]);
3876
+ const groupPositionById = react.useMemo(() => {
3877
+ const map = /* @__PURE__ */ new Map();
3878
+ for (let i = 0; i < messages.length; i++) {
3879
+ const msg = messages[i];
3880
+ if (!msg) continue;
3881
+ const prev = i > 0 ? messages[i - 1] ?? null : null;
3882
+ const next = i < messages.length - 1 ? messages[i + 1] ?? null : null;
3883
+ map.set(msg.id, react$1.computeGroupPosition(msg, prev, next, GROUPING_WINDOW_MS));
3884
+ }
3885
+ return map;
3886
+ }, [messages]);
3581
3887
  const renderItem = react.useCallback(
3582
3888
  (msg) => {
3583
3889
  if (renderMessage) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderMessage(msg, meId) });
3584
3890
  const readState = readReceipts && meId !== null && msg.sender_id === meId ? readStateForMessage(msg, members, meId, sequenceByMessageId) : void 0;
3891
+ const isSelf = meId !== null && msg.sender_id === meId;
3892
+ const canEdit = isSelf && !msg.deleted_at && (readState ?? "sent") === "sent";
3893
+ const groupPosition = groupPositionById.get(msg.id) ?? "standalone";
3585
3894
  return /* @__PURE__ */ jsxRuntime.jsx(
3586
3895
  MessageRow,
3587
3896
  {
@@ -3593,13 +3902,8 @@ function ConversationView({
3593
3902
  threads,
3594
3903
  quotations,
3595
3904
  ...readState ? { readState } : {},
3596
- editing: editingId === msg.id,
3597
- onStartEdit: () => setEditingId(msg.id),
3598
- onCancelEdit: () => setEditingId(null),
3599
- onSaveEdit: async (body) => {
3600
- await edit(msg.id, body);
3601
- setEditingId(null);
3602
- },
3905
+ isEditing: editingId === msg.id,
3906
+ ...canEdit ? { onStartEdit: () => setEditingId(msg.id) } : {},
3603
3907
  onDelete: () => deleteMsg(msg.id),
3604
3908
  ...threads ? { onOpenThread: () => setThreadRoot(msg) } : {},
3605
3909
  ...quotations ? { onQuote: () => setReplyingTo(msg) } : {},
@@ -3607,7 +3911,9 @@ function ConversationView({
3607
3911
  ...avatarFor ? { avatarFor } : {},
3608
3912
  showSenderName: showSenderLabels,
3609
3913
  showAvatar: showAvatars,
3610
- maxBodyLength
3914
+ maxBodyLength,
3915
+ groupPosition,
3916
+ markdown
3611
3917
  }
3612
3918
  );
3613
3919
  },
@@ -3622,16 +3928,38 @@ function ConversationView({
3622
3928
  threads,
3623
3929
  quotations,
3624
3930
  editingId,
3625
- edit,
3626
3931
  deleteMsg,
3627
3932
  labelFor,
3628
3933
  avatarFor,
3629
3934
  showSenderLabels,
3630
3935
  showAvatars,
3631
3936
  maxBodyLength,
3632
- sequenceByMessageId
3937
+ sequenceByMessageId,
3938
+ groupPositionById,
3939
+ markdown
3633
3940
  ]
3634
3941
  );
3942
+ const editingMessage = react.useMemo(
3943
+ () => editingId ? messages.find((m) => m.id === editingId) ?? null : null,
3944
+ [editingId, messages]
3945
+ );
3946
+ const listItems = react.useMemo(() => {
3947
+ const out = [];
3948
+ let prev = null;
3949
+ for (const msg of messages) {
3950
+ if (prev === null || !react$1.sameDay(msg.inserted_at, prev.inserted_at)) {
3951
+ const label = react$1.formatDayLabel(msg.inserted_at);
3952
+ out.push({
3953
+ kind: "separator",
3954
+ id: `sep-${msg.id}`,
3955
+ node: /* @__PURE__ */ jsxRuntime.jsx(DaySeparator, { label })
3956
+ });
3957
+ }
3958
+ out.push({ kind: "msg", msg });
3959
+ prev = msg;
3960
+ }
3961
+ return out;
3962
+ }, [messages]);
3635
3963
  const composerProps = react.useMemo(
3636
3964
  () => ({
3637
3965
  onSend: async (body, opts) => {
@@ -3645,9 +3973,28 @@ function ConversationView({
3645
3973
  onCancelReply: () => setReplyingTo(null),
3646
3974
  ...labelFor ? { labelFor } : {},
3647
3975
  attachments,
3648
- keyboardOffset
3976
+ keyboardOffset,
3977
+ editingMessage,
3978
+ onCancelEdit: () => setEditingId(null),
3979
+ onSaveEdit: async (body) => {
3980
+ if (!editingId) return;
3981
+ await edit(editingId, body);
3982
+ setEditingId(null);
3983
+ }
3649
3984
  }),
3650
- [send, signalTyping, attachments, replyingTo, labelFor, keyboardOffset, onSent, conversationId]
3985
+ [
3986
+ send,
3987
+ signalTyping,
3988
+ attachments,
3989
+ replyingTo,
3990
+ labelFor,
3991
+ keyboardOffset,
3992
+ onSent,
3993
+ conversationId,
3994
+ editingMessage,
3995
+ editingId,
3996
+ edit
3997
+ ]
3651
3998
  );
3652
3999
  const headerNode = header === false ? null : header === true ? /* @__PURE__ */ jsxRuntime.jsx(
3653
4000
  ChatHeader,
@@ -3667,18 +4014,30 @@ function ConversationView({
3667
4014
  keyboardVerticalOffset: keyboardOffset,
3668
4015
  children: [
3669
4016
  headerNode,
3670
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles20.listWrap, children: /* @__PURE__ */ jsxRuntime.jsx(
3671
- MessageList,
3672
- {
3673
- messages,
3674
- loading,
3675
- hasMore,
3676
- onLoadMore: loadMore,
3677
- renderItem,
3678
- ListHeader: /* @__PURE__ */ jsxRuntime.jsx(TypingIndicator, { typing, ...labelFor ? { labelFor } : {} }),
3679
- ...emptyState !== void 0 ? { emptyState } : {}
3680
- }
3681
- ) }),
4017
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles20.listWrap, children: [
4018
+ /* @__PURE__ */ jsxRuntime.jsx(
4019
+ MessageList,
4020
+ {
4021
+ messages,
4022
+ items: listItems,
4023
+ meId,
4024
+ loading,
4025
+ hasMore,
4026
+ onLoadMore: loadMore,
4027
+ renderItem,
4028
+ ListHeader: /* @__PURE__ */ jsxRuntime.jsx(TypingIndicator, { typing, ...labelFor ? { labelFor } : {} }),
4029
+ ...emptyState !== void 0 ? { emptyState } : {}
4030
+ }
4031
+ ),
4032
+ editingId ? /* @__PURE__ */ jsxRuntime.jsx(
4033
+ reactNative.Pressable,
4034
+ {
4035
+ style: styles20.editBackdrop,
4036
+ onPress: () => setEditingId(null),
4037
+ accessibilityLabel: "Cancel editing"
4038
+ }
4039
+ ) : null
4040
+ ] }),
3682
4041
  mentions ? /* @__PURE__ */ jsxRuntime.jsx(MentionInput, { conversationId, ...composerProps }) : /* @__PURE__ */ jsxRuntime.jsx(MessageComposer, { ...composerProps }),
3683
4042
  attachments ? /* @__PURE__ */ jsxRuntime.jsx(AttachmentPicker, { visible: pickerOpen, onClose: () => setPickerOpen(false) }) : null,
3684
4043
  threads && threadRoot ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -3714,6 +4073,14 @@ var styles20 = reactNative.StyleSheet.create({
3714
4073
  },
3715
4074
  listWrap: {
3716
4075
  flex: 1
4076
+ },
4077
+ editBackdrop: {
4078
+ ...reactNative.StyleSheet.absoluteFillObject,
4079
+ backgroundColor: "rgba(0,0,0,0.45)",
4080
+ // Sit above message rows by default but below any row that
4081
+ // explicitly elevates itself (MessageRow bumps zIndex/elevation
4082
+ // when isEditing is true).
4083
+ zIndex: 5
3717
4084
  }
3718
4085
  });
3719
4086
  function UserPickerSheet({
@@ -4578,7 +4945,7 @@ exports.Avatar = Avatar;
4578
4945
  exports.ChatHeader = ChatHeader;
4579
4946
  exports.ConversationList = ConversationList;
4580
4947
  exports.ConversationView = ConversationView;
4581
- exports.EditableMessageBubble = EditableMessageBubble;
4948
+ exports.DaySeparator = DaySeparator;
4582
4949
  exports.GroupDetailsSheet = GroupDetailsSheet;
4583
4950
  exports.ImageMosaic = ImageMosaic;
4584
4951
  exports.MemberList = MemberList;