@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.js CHANGED
@@ -1,12 +1,18 @@
1
- import { useUser, useMe, useConversations, useAttachmentUpload, useConversation, useMembers, usePresence, useMessages, useTyping, useReactions, useThread, useAttachmentUrl, usePoolse } from '@poolse/react';
2
- import { createContext, forwardRef, useRef, useState, useImperativeHandle, useEffect, useMemo, useCallback, useContext } from 'react';
3
- import { StyleSheet, View, Pressable, TextInput, Text, useWindowDimensions, Animated, Easing, PanResponder, Image, ScrollView, ActivityIndicator, FlatList, KeyboardAvoidingView, Platform, Modal, Dimensions, AppState } from 'react-native';
1
+ import { useUser, useMe, useConversations, useAttachmentUpload, useConversation, useMembers, usePresence, useMessages, useTyping, useRealtimeStatus, computeGroupPosition, formatDayLabel, useReactions, sameDay, useThread, useAttachmentUrl, usePoolse } from '@poolse/react';
2
+ import { createContext, forwardRef, useRef, useState, useEffect, useImperativeHandle, useMemo, useCallback, useContext, createElement } from 'react';
3
+ import { StyleSheet, Platform, Keyboard, View, Pressable, TextInput, Text, useWindowDimensions, Animated, Easing, PanResponder, Image, ScrollView, ActivityIndicator, FlatList, AppState, KeyboardAvoidingView, Modal, Linking, Dimensions } from 'react-native';
4
4
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
5
  import { SvgXml, Svg, Path, Circle, G } from 'react-native-svg';
6
6
  import * as ImagePicker from 'expo-image-picker';
7
7
  import * as DocumentPicker from 'expo-document-picker';
8
+ import { manipulateAsync, SaveFormat } from 'expo-image-manipulator';
8
9
 
9
- // src/PoolseInbox.tsx
10
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
11
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
12
+ }) : x)(function(x) {
13
+ if (typeof require !== "undefined") return require.apply(this, arguments);
14
+ throw Error('Dynamic require of "' + x + '" is not supported');
15
+ });
10
16
 
11
17
  // src/theme/default.ts
12
18
  var lightColors = {
@@ -1160,24 +1166,84 @@ async function uriToBlob(uri) {
1160
1166
  }
1161
1167
  return res.blob();
1162
1168
  }
1169
+ var COMPRESSION_LONG_SIDE_PX = 1600;
1170
+ var COMPRESSION_QUALITY = 0.75;
1171
+ function getImageSize(uri) {
1172
+ return new Promise((resolve, reject) => {
1173
+ Image.getSize(uri, (width, height) => resolve({ width, height }), reject);
1174
+ });
1175
+ }
1176
+ async function compressImage({
1177
+ uri,
1178
+ width,
1179
+ height,
1180
+ filename
1181
+ }) {
1182
+ let w = width;
1183
+ let h = height;
1184
+ if (!w || !h) {
1185
+ try {
1186
+ const size = await getImageSize(uri);
1187
+ w = size.width;
1188
+ h = size.height;
1189
+ } catch {
1190
+ w = 0;
1191
+ h = 0;
1192
+ }
1193
+ }
1194
+ const longSide = Math.max(w ?? 0, h ?? 0);
1195
+ const scale = longSide > COMPRESSION_LONG_SIDE_PX ? COMPRESSION_LONG_SIDE_PX / longSide : 1;
1196
+ const actions = scale < 1 && w && h ? [{ resize: { width: Math.round(w * scale), height: Math.round(h * scale) } }] : [];
1197
+ const result = await manipulateAsync(uri, actions, {
1198
+ compress: COMPRESSION_QUALITY,
1199
+ format: SaveFormat.WEBP
1200
+ });
1201
+ const blob = await uriToBlob(result.uri);
1202
+ const base = (filename ?? `image-${Date.now()}`).replace(/\.[^.]+$/, "");
1203
+ return {
1204
+ blob,
1205
+ contentType: "image/webp",
1206
+ filename: `${base}.webp`
1207
+ };
1208
+ }
1163
1209
  async function defaultAdapter(kind) {
1164
1210
  if (kind === "image") {
1165
1211
  const res2 = await ImagePicker.launchImageLibraryAsync({
1166
1212
  mediaTypes: "images",
1167
- quality: 0.85
1213
+ // Quality 1 — picker hands back the source as-is and we do the
1214
+ // compression ourselves below. Letting the picker re-encode
1215
+ // first just chains two lossy passes.
1216
+ quality: 1
1168
1217
  });
1169
1218
  if (res2.canceled) return [];
1170
1219
  return Promise.all(
1171
1220
  res2.assets.map(async (asset) => {
1172
- const blob = await uriToBlob(asset.uri);
1173
- const contentType = asset.mimeType ?? blob.type ?? "image/jpeg";
1174
- return {
1175
- body: blob,
1176
- contentType,
1177
- byteSize: asset.fileSize ?? blob.size,
1178
- filename: asset.fileName ?? `image-${Date.now()}.jpg`,
1179
- _previewUri: asset.uri
1180
- };
1221
+ try {
1222
+ const compressed = await compressImage({
1223
+ uri: asset.uri,
1224
+ width: asset.width,
1225
+ height: asset.height,
1226
+ filename: asset.fileName ?? `image-${Date.now()}`
1227
+ });
1228
+ return {
1229
+ body: compressed.blob,
1230
+ contentType: compressed.contentType,
1231
+ byteSize: compressed.blob.size,
1232
+ filename: compressed.filename,
1233
+ _previewUri: asset.uri
1234
+ };
1235
+ } catch (err) {
1236
+ console.warn("[poolse/picker] image compression failed, sending original:", err);
1237
+ const blob = await uriToBlob(asset.uri);
1238
+ const contentType = asset.mimeType ?? blob.type ?? "image/jpeg";
1239
+ return {
1240
+ body: blob,
1241
+ contentType,
1242
+ byteSize: asset.fileSize ?? blob.size,
1243
+ filename: asset.fileName ?? `image-${Date.now()}.jpg`,
1244
+ _previewUri: asset.uri
1245
+ };
1246
+ }
1181
1247
  })
1182
1248
  );
1183
1249
  }
@@ -1188,6 +1254,24 @@ async function defaultAdapter(kind) {
1188
1254
  if (res.canceled) return [];
1189
1255
  return Promise.all(
1190
1256
  res.assets.map(async (asset) => {
1257
+ const isImage = (asset.mimeType ?? "").startsWith("image/");
1258
+ if (isImage) {
1259
+ try {
1260
+ const compressed = await compressImage({
1261
+ uri: asset.uri,
1262
+ filename: asset.name
1263
+ });
1264
+ return {
1265
+ body: compressed.blob,
1266
+ contentType: compressed.contentType,
1267
+ byteSize: compressed.blob.size,
1268
+ filename: compressed.filename,
1269
+ _previewUri: asset.uri
1270
+ };
1271
+ } catch (err) {
1272
+ console.warn("[poolse/picker] image compression failed, sending original:", err);
1273
+ }
1274
+ }
1191
1275
  const blob = await uriToBlob(asset.uri);
1192
1276
  const contentType = asset.mimeType ?? blob.type ?? "application/octet-stream";
1193
1277
  return {
@@ -1228,6 +1312,51 @@ var styles6 = StyleSheet.create({
1228
1312
  fontWeight: "500"
1229
1313
  }
1230
1314
  });
1315
+ function DaySeparator({ label }) {
1316
+ const theme = usePoolseTheme();
1317
+ return /* @__PURE__ */ jsx(View, { style: styles7.wrap, children: /* @__PURE__ */ jsx(
1318
+ View,
1319
+ {
1320
+ style: [
1321
+ styles7.chip,
1322
+ {
1323
+ backgroundColor: theme.colors.surface2,
1324
+ borderColor: theme.colors.border
1325
+ }
1326
+ ],
1327
+ children: /* @__PURE__ */ jsx(
1328
+ Text,
1329
+ {
1330
+ style: [
1331
+ styles7.label,
1332
+ {
1333
+ color: theme.colors.ink2,
1334
+ fontFamily: theme.type.fontBody
1335
+ }
1336
+ ],
1337
+ children: label
1338
+ }
1339
+ )
1340
+ }
1341
+ ) });
1342
+ }
1343
+ var styles7 = StyleSheet.create({
1344
+ wrap: {
1345
+ alignItems: "center",
1346
+ marginVertical: 8
1347
+ },
1348
+ chip: {
1349
+ paddingHorizontal: 12,
1350
+ paddingVertical: 4,
1351
+ borderRadius: 999,
1352
+ borderWidth: StyleSheet.hairlineWidth
1353
+ },
1354
+ label: {
1355
+ fontSize: 11,
1356
+ fontWeight: "600",
1357
+ letterSpacing: 0.3
1358
+ }
1359
+ });
1231
1360
  function UploadQueueStrip() {
1232
1361
  const theme = usePoolseTheme();
1233
1362
  const upload = useSharedUpload();
@@ -1238,8 +1367,8 @@ function UploadQueueStrip() {
1238
1367
  {
1239
1368
  horizontal: true,
1240
1369
  showsHorizontalScrollIndicator: false,
1241
- contentContainerStyle: styles7.scroll,
1242
- style: [styles7.scrollStyle, { marginBottom: 8 }],
1370
+ contentContainerStyle: styles8.scroll,
1371
+ style: [styles8.scrollStyle, { marginBottom: 8 }],
1243
1372
  children: upload.queue.map((item) => {
1244
1373
  const isError = item.status === "error";
1245
1374
  const isUploading = item.status === "uploading" || item.status === "pending";
@@ -1250,7 +1379,7 @@ function UploadQueueStrip() {
1250
1379
  if (item.status === "ready" || isError) upload.remove(item.localId);
1251
1380
  else upload.cancel(item.localId);
1252
1381
  };
1253
- return /* @__PURE__ */ jsxs(View, { style: styles7.tileWrap, children: [
1382
+ return /* @__PURE__ */ jsxs(View, { style: styles8.tileWrap, children: [
1254
1383
  isImage && previewUri ? /* @__PURE__ */ jsx(ImageTile, { uri: previewUri, uploading: isUploading, error: isError }) : /* @__PURE__ */ jsx(
1255
1384
  FileTile,
1256
1385
  {
@@ -1265,13 +1394,13 @@ function UploadQueueStrip() {
1265
1394
  {
1266
1395
  onPress: handleClose,
1267
1396
  hitSlop: 8,
1268
- style: [styles7.closeBtn, { backgroundColor: theme.colors.ink }],
1397
+ style: [styles8.closeBtn, { backgroundColor: theme.colors.ink }],
1269
1398
  accessibilityLabel: isError ? "Remove failed upload" : "Cancel upload",
1270
1399
  children: /* @__PURE__ */ jsx(PoolseIcon, { name: "close", size: 11, color: theme.colors.onBrand })
1271
1400
  }
1272
1401
  ) : null,
1273
- isUploading ? /* @__PURE__ */ jsx(View, { style: styles7.progressOverlay, children: /* @__PURE__ */ jsx(View, { style: [styles7.spinner, { borderColor: theme.colors.onBrand }] }) }) : null,
1274
- isError ? /* @__PURE__ */ jsx(View, { style: [styles7.errorOverlay, { backgroundColor: "rgba(229,72,77,0.85)" }], children: /* @__PURE__ */ jsx(Text, { style: [styles7.errorText, { color: "#fff" }], children: "Failed" }) }) : null
1402
+ isUploading ? /* @__PURE__ */ jsx(View, { style: styles8.progressOverlay, children: /* @__PURE__ */ jsx(View, { style: [styles8.spinner, { borderColor: theme.colors.onBrand }] }) }) : null,
1403
+ isError ? /* @__PURE__ */ jsx(View, { style: [styles8.errorOverlay, { backgroundColor: "rgba(229,72,77,0.85)" }], children: /* @__PURE__ */ jsx(Text, { style: [styles8.errorText, { color: "#fff" }], children: "Failed" }) }) : null
1275
1404
  ] }, item.localId);
1276
1405
  })
1277
1406
  }
@@ -1284,7 +1413,7 @@ function ImageTile({ uri, uploading, error }) {
1284
1413
  {
1285
1414
  source: { uri },
1286
1415
  style: [
1287
- styles7.imageTile,
1416
+ styles8.imageTile,
1288
1417
  {
1289
1418
  backgroundColor: theme.colors.surface2,
1290
1419
  borderColor: error ? theme.colors.error : "transparent",
@@ -1306,24 +1435,24 @@ function FileTile({
1306
1435
  View,
1307
1436
  {
1308
1437
  style: [
1309
- styles7.fileTile,
1438
+ styles8.fileTile,
1310
1439
  {
1311
1440
  backgroundColor: theme.colors.surface2,
1312
1441
  borderColor: error ? theme.colors.error : uploading ? theme.colors.brand : theme.colors.border
1313
1442
  }
1314
1443
  ],
1315
1444
  children: [
1316
- /* @__PURE__ */ jsx(View, { style: [styles7.fileIconBox, { backgroundColor: theme.colors.brandSoft }], children: /* @__PURE__ */ jsx(PoolseIcon, { name: "attachment", size: 18, color: theme.colors.brand }) }),
1317
- /* @__PURE__ */ jsxs(View, { style: styles7.fileInfo, children: [
1445
+ /* @__PURE__ */ jsx(View, { style: [styles8.fileIconBox, { backgroundColor: theme.colors.brandSoft }], children: /* @__PURE__ */ jsx(PoolseIcon, { name: "attachment", size: 18, color: theme.colors.brand }) }),
1446
+ /* @__PURE__ */ jsxs(View, { style: styles8.fileInfo, children: [
1318
1447
  /* @__PURE__ */ jsx(
1319
1448
  Text,
1320
1449
  {
1321
- style: [styles7.fileName, { color: theme.colors.ink, fontFamily: theme.type.fontBody }],
1450
+ style: [styles8.fileName, { color: theme.colors.ink, fontFamily: theme.type.fontBody }],
1322
1451
  numberOfLines: 1,
1323
1452
  children: filename
1324
1453
  }
1325
1454
  ),
1326
- /* @__PURE__ */ jsx(Text, { style: [styles7.fileSize, { color: theme.colors.ink3 }], children: formatBytes(size) })
1455
+ /* @__PURE__ */ jsx(Text, { style: [styles8.fileSize, { color: theme.colors.ink3 }], children: formatBytes(size) })
1327
1456
  ] })
1328
1457
  ]
1329
1458
  }
@@ -1337,7 +1466,7 @@ function formatBytes(n) {
1337
1466
  return `${(n / 1024 / 1024 / 1024).toFixed(1)} GB`;
1338
1467
  }
1339
1468
  var TILE = 64;
1340
- var styles7 = StyleSheet.create({
1469
+ var styles8 = StyleSheet.create({
1341
1470
  scrollStyle: {
1342
1471
  overflow: "visible"
1343
1472
  },
@@ -1437,6 +1566,22 @@ var styles7 = StyleSheet.create({
1437
1566
  fontWeight: "700"
1438
1567
  }
1439
1568
  });
1569
+ var FALLBACK = {
1570
+ top: Platform.OS === "ios" ? 50 : 24,
1571
+ bottom: Platform.OS === "ios" ? 34 : 0,
1572
+ left: 0,
1573
+ right: 0
1574
+ };
1575
+ var realHook = null;
1576
+ try {
1577
+ const mod = __require("react-native-safe-area-context");
1578
+ if (mod && typeof mod.useSafeAreaInsets === "function") {
1579
+ realHook = mod.useSafeAreaInsets;
1580
+ }
1581
+ } catch {
1582
+ }
1583
+ var useFallback = () => FALLBACK;
1584
+ var useSafeInsets = realHook ?? useFallback;
1440
1585
  var MessageComposer = forwardRef(
1441
1586
  function MessageComposer2({
1442
1587
  onSend,
@@ -1448,13 +1593,32 @@ var MessageComposer = forwardRef(
1448
1593
  labelFor,
1449
1594
  attachments = true,
1450
1595
  onAttachPress,
1451
- keyboardOffset: _keyboardOffset = 0
1596
+ keyboardOffset: _keyboardOffset = 0,
1597
+ editingMessage = null,
1598
+ onCancelEdit,
1599
+ onSaveEdit
1452
1600
  }, ref) {
1453
1601
  const theme = usePoolseTheme();
1454
1602
  const inputRef = useRef(null);
1455
1603
  const [value, setValue] = useState("");
1456
1604
  const [sending, setSending] = useState(false);
1605
+ const [keyboardOpen, setKeyboardOpen] = useState(false);
1457
1606
  const upload = useSharedUpload();
1607
+ const insets = useSafeInsets();
1608
+ useEffect(() => {
1609
+ const show = Keyboard.addListener(
1610
+ Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow",
1611
+ () => setKeyboardOpen(true)
1612
+ );
1613
+ const hide = Keyboard.addListener(
1614
+ Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide",
1615
+ () => setKeyboardOpen(false)
1616
+ );
1617
+ return () => {
1618
+ show.remove();
1619
+ hide.remove();
1620
+ };
1621
+ }, []);
1458
1622
  useImperativeHandle(
1459
1623
  ref,
1460
1624
  () => ({
@@ -1473,36 +1637,65 @@ var MessageComposer = forwardRef(
1473
1637
  const isUploading = upload.queue.some(
1474
1638
  (item) => item.status === "uploading" || item.status === "pending"
1475
1639
  );
1476
- const submitDisabled = disabled || sending || isUploading || value.trim().length === 0 && readyAttachmentIds.length === 0;
1640
+ const previousDraftRef = useRef(null);
1641
+ const editingIdRef = useRef(null);
1642
+ useEffect(() => {
1643
+ const id = editingMessage?.id ?? null;
1644
+ if (id === editingIdRef.current) return;
1645
+ if (id) {
1646
+ previousDraftRef.current = value;
1647
+ setValue(editingMessage?.body ?? "");
1648
+ inputRef.current?.focus();
1649
+ } else {
1650
+ setValue(previousDraftRef.current ?? "");
1651
+ previousDraftRef.current = null;
1652
+ }
1653
+ editingIdRef.current = id;
1654
+ }, [editingMessage?.id, editingMessage?.body]);
1655
+ const isEditing = !!editingMessage;
1656
+ const editChanged = isEditing && value.trim() !== (editingMessage?.body?.trim() ?? "");
1657
+ const submitDisabled = disabled || sending || isUploading || value.trim().length === 0 || (isEditing ? !editChanged : value.trim().length === 0 && readyAttachmentIds.length === 0);
1477
1658
  const handleSend = async () => {
1478
1659
  if (submitDisabled) return;
1479
1660
  const body = value.trim();
1480
- const replyId = replyingTo?.id;
1481
- const attachmentIds = readyAttachmentIds;
1482
- setValue("");
1483
- upload.reset();
1484
1661
  setSending(true);
1485
1662
  try {
1486
- await onSend(body, {
1487
- ...replyId ? { quoted_message_id: replyId } : {},
1488
- ...attachmentIds.length > 0 ? { attachment_ids: attachmentIds } : {}
1489
- });
1663
+ if (isEditing && onSaveEdit) {
1664
+ await onSaveEdit(body);
1665
+ } else {
1666
+ const replyId = replyingTo?.id;
1667
+ const attachmentIds = readyAttachmentIds;
1668
+ setValue("");
1669
+ upload.reset();
1670
+ await onSend(body, {
1671
+ ...replyId ? { quoted_message_id: replyId } : {},
1672
+ ...attachmentIds.length > 0 ? { attachment_ids: attachmentIds } : {}
1673
+ });
1674
+ }
1490
1675
  } finally {
1491
1676
  setSending(false);
1492
1677
  }
1493
1678
  };
1679
+ const paddingBottom = keyboardOpen ? 10 : Math.max(10, insets.bottom);
1494
1680
  return /* @__PURE__ */ jsxs(
1495
1681
  View,
1496
1682
  {
1497
1683
  style: [
1498
- styles8.wrap,
1684
+ styles9.wrap,
1499
1685
  {
1500
1686
  backgroundColor: theme.colors.surface,
1501
- borderTopColor: theme.colors.border
1687
+ borderTopColor: theme.colors.border,
1688
+ paddingBottom
1502
1689
  }
1503
1690
  ],
1504
1691
  children: [
1505
- replyingTo ? /* @__PURE__ */ jsx(
1692
+ isEditing ? /* @__PURE__ */ jsx(
1693
+ EditChip,
1694
+ {
1695
+ message: editingMessage,
1696
+ ...onCancelEdit ? { onCancel: onCancelEdit } : {}
1697
+ }
1698
+ ) : replyingTo ? /* @__PURE__ */ jsx(
1506
1699
  ReplyChip,
1507
1700
  {
1508
1701
  message: replyingTo,
@@ -1510,14 +1703,14 @@ var MessageComposer = forwardRef(
1510
1703
  ...onCancelReply ? { onCancel: onCancelReply } : {}
1511
1704
  }
1512
1705
  ) : null,
1513
- attachments ? /* @__PURE__ */ jsx(UploadQueueStrip, {}) : null,
1514
- /* @__PURE__ */ jsxs(View, { style: styles8.row, children: [
1706
+ attachments && !isEditing ? /* @__PURE__ */ jsx(UploadQueueStrip, {}) : null,
1707
+ /* @__PURE__ */ jsxs(View, { style: styles9.row, children: [
1515
1708
  attachments && onAttachPress ? /* @__PURE__ */ jsx(
1516
1709
  Pressable,
1517
1710
  {
1518
1711
  onPress: onAttachPress,
1519
1712
  hitSlop: 8,
1520
- style: styles8.iconBtn,
1713
+ style: styles9.iconBtn,
1521
1714
  accessibilityLabel: "Attach file",
1522
1715
  children: /* @__PURE__ */ jsx(PoolseIcon, { name: "attachment", size: 22, color: theme.colors.ink2 })
1523
1716
  }
@@ -1536,7 +1729,7 @@ var MessageComposer = forwardRef(
1536
1729
  multiline: true,
1537
1730
  editable: !disabled,
1538
1731
  style: [
1539
- styles8.input,
1732
+ styles9.input,
1540
1733
  {
1541
1734
  color: theme.colors.ink,
1542
1735
  backgroundColor: theme.colors.surface2,
@@ -1553,7 +1746,7 @@ var MessageComposer = forwardRef(
1553
1746
  onPress: handleSend,
1554
1747
  disabled: submitDisabled,
1555
1748
  style: [
1556
- styles8.sendBtn,
1749
+ styles9.sendBtn,
1557
1750
  {
1558
1751
  backgroundColor: submitDisabled ? theme.colors.ink3 : theme.colors.brand,
1559
1752
  opacity: submitDisabled ? 0.6 : 1
@@ -1569,6 +1762,28 @@ var MessageComposer = forwardRef(
1569
1762
  );
1570
1763
  }
1571
1764
  );
1765
+ function EditChip({ message, onCancel }) {
1766
+ const theme = usePoolseTheme();
1767
+ return /* @__PURE__ */ jsxs(
1768
+ View,
1769
+ {
1770
+ style: [
1771
+ styles9.replyChip,
1772
+ {
1773
+ backgroundColor: theme.colors.surface2,
1774
+ borderLeftColor: theme.colors.brand
1775
+ }
1776
+ ],
1777
+ children: [
1778
+ /* @__PURE__ */ jsxs(View, { style: { flex: 1 }, children: [
1779
+ /* @__PURE__ */ jsx(Text, { style: [styles9.replyName, { color: theme.colors.brand }], children: "Editing message" }),
1780
+ /* @__PURE__ */ jsx(Text, { style: [styles9.replyBody, { color: theme.colors.ink2 }], numberOfLines: 1, children: message.body ?? "\u2026" })
1781
+ ] }),
1782
+ onCancel ? /* @__PURE__ */ jsx(Pressable, { onPress: onCancel, hitSlop: 8, style: styles9.replyCancel, children: /* @__PURE__ */ jsx(PoolseIcon, { name: "close", size: 16, color: theme.colors.ink2 }) }) : null
1783
+ ]
1784
+ }
1785
+ );
1786
+ }
1572
1787
  function ReplyChip({
1573
1788
  message,
1574
1789
  labelFor,
@@ -1581,7 +1796,7 @@ function ReplyChip({
1581
1796
  View,
1582
1797
  {
1583
1798
  style: [
1584
- styles8.replyChip,
1799
+ styles9.replyChip,
1585
1800
  {
1586
1801
  backgroundColor: theme.colors.surface2,
1587
1802
  borderLeftColor: theme.colors.brand
@@ -1589,27 +1804,26 @@ function ReplyChip({
1589
1804
  ],
1590
1805
  children: [
1591
1806
  /* @__PURE__ */ jsxs(View, { style: { flex: 1 }, children: [
1592
- /* @__PURE__ */ jsxs(Text, { style: [styles8.replyName, { color: theme.colors.brand }], children: [
1807
+ /* @__PURE__ */ jsxs(Text, { style: [styles9.replyName, { color: theme.colors.brand }], children: [
1593
1808
  "Replying to ",
1594
1809
  senderName
1595
1810
  ] }),
1596
- /* @__PURE__ */ jsx(Text, { style: [styles8.replyBody, { color: theme.colors.ink2 }], numberOfLines: 1, children: message.body ?? "\u2026" })
1811
+ /* @__PURE__ */ jsx(Text, { style: [styles9.replyBody, { color: theme.colors.ink2 }], numberOfLines: 1, children: message.body ?? "\u2026" })
1597
1812
  ] }),
1598
- onCancel ? /* @__PURE__ */ jsx(Pressable, { onPress: onCancel, hitSlop: 8, style: styles8.replyCancel, children: /* @__PURE__ */ jsx(PoolseIcon, { name: "close", size: 16, color: theme.colors.ink2 }) }) : null
1813
+ onCancel ? /* @__PURE__ */ jsx(Pressable, { onPress: onCancel, hitSlop: 8, style: styles9.replyCancel, children: /* @__PURE__ */ jsx(PoolseIcon, { name: "close", size: 16, color: theme.colors.ink2 }) }) : null
1599
1814
  ]
1600
1815
  }
1601
1816
  );
1602
1817
  }
1603
- var styles8 = StyleSheet.create({
1818
+ var styles9 = StyleSheet.create({
1604
1819
  wrap: {
1605
1820
  borderTopWidth: StyleSheet.hairlineWidth,
1606
1821
  paddingTop: 8,
1607
- paddingBottom: 10,
1608
1822
  paddingHorizontal: 12
1609
- // Note: no safe-area inset here on purpose. Wrap your chat
1610
- // surface in <SafeAreaView> (or <SafeAreaView edges={['bottom']}>)
1611
- // at the app root so the home-indicator clearance is handled
1612
- // once. Doing it here too produces double padding.
1823
+ // paddingBottom is applied inline — it follows the keyboard state
1824
+ // (10 px while open, max(10, safeBottomInset) while closed) so the
1825
+ // send/attach buttons stay clear of the home indicator without
1826
+ // double-padding above the keyboard.
1613
1827
  },
1614
1828
  row: {
1615
1829
  flexDirection: "row",
@@ -1698,11 +1912,11 @@ function MentionInput({ conversationId, onSend, ...composerProps }) {
1698
1912
  transparent: true,
1699
1913
  animationType: "slide",
1700
1914
  onRequestClose: () => setPickerOpen(false),
1701
- children: /* @__PURE__ */ jsx(Pressable, { style: styles9.backdrop, onPress: () => setPickerOpen(false), children: /* @__PURE__ */ jsxs(
1915
+ children: /* @__PURE__ */ jsx(Pressable, { style: styles10.backdrop, onPress: () => setPickerOpen(false), children: /* @__PURE__ */ jsxs(
1702
1916
  View,
1703
1917
  {
1704
1918
  style: [
1705
- styles9.sheet,
1919
+ styles10.sheet,
1706
1920
  {
1707
1921
  backgroundColor: theme.colors.surface,
1708
1922
  borderTopLeftRadius: theme.radii.lg,
@@ -1713,7 +1927,7 @@ function MentionInput({ conversationId, onSend, ...composerProps }) {
1713
1927
  /* @__PURE__ */ jsx(
1714
1928
  Text,
1715
1929
  {
1716
- style: [styles9.title, { color: theme.colors.ink, fontFamily: theme.type.fontBody }],
1930
+ style: [styles10.title, { color: theme.colors.ink, fontFamily: theme.type.fontBody }],
1717
1931
  children: "Mention a member"
1718
1932
  }
1719
1933
  ),
@@ -1744,12 +1958,12 @@ function MentionInput({ conversationId, onSend, ...composerProps }) {
1744
1958
  }
1745
1959
  function MentionRow({ member, onPress }) {
1746
1960
  const theme = usePoolseTheme();
1747
- return /* @__PURE__ */ jsxs(Pressable, { onPress, style: styles9.row, children: [
1961
+ return /* @__PURE__ */ jsxs(Pressable, { onPress, style: styles10.row, children: [
1748
1962
  /* @__PURE__ */ jsx(Avatar, { src: null, name: member.external_id, seed: member.external_id, size: "sm" }),
1749
- /* @__PURE__ */ jsx(Text, { style: [styles9.rowText, { color: theme.colors.ink, fontFamily: theme.type.fontBody }], children: member.external_id })
1963
+ /* @__PURE__ */ jsx(Text, { style: [styles10.rowText, { color: theme.colors.ink, fontFamily: theme.type.fontBody }], children: member.external_id })
1750
1964
  ] });
1751
1965
  }
1752
- var styles9 = StyleSheet.create({
1966
+ var styles10 = StyleSheet.create({
1753
1967
  backdrop: {
1754
1968
  flex: 1,
1755
1969
  backgroundColor: "rgba(0,0,0,0.35)",
@@ -1776,138 +1990,134 @@ var styles9 = StyleSheet.create({
1776
1990
  fontSize: 14
1777
1991
  }
1778
1992
  });
1993
+ var AT_BOTTOM_THRESHOLD = 32;
1779
1994
  function MessageList({
1780
1995
  messages,
1996
+ items,
1781
1997
  loading = false,
1782
1998
  hasMore = false,
1783
1999
  onLoadMore,
1784
2000
  renderItem,
1785
2001
  ListHeader = null,
1786
2002
  ListFooter = null,
1787
- emptyState
2003
+ emptyState,
2004
+ meId = null
1788
2005
  }) {
1789
2006
  const theme = usePoolseTheme();
1790
- const data = [...messages].reverse();
2007
+ const flatListRef = useRef(null);
2008
+ const [atBottom, setAtBottom] = useState(true);
2009
+ const [newCount, setNewCount] = useState(0);
2010
+ const lastTailIdRef = useRef(null);
2011
+ const sourceItems = items ?? messages.map((msg) => ({ kind: "msg", msg }));
2012
+ const data = [...sourceItems].reverse();
2013
+ useEffect(() => {
2014
+ const tail = messages[messages.length - 1];
2015
+ if (!tail) return;
2016
+ const tailId = tail.id;
2017
+ if (tailId === lastTailIdRef.current) return;
2018
+ const isFirstSet = lastTailIdRef.current === null;
2019
+ lastTailIdRef.current = tailId;
2020
+ if (atBottom || isFirstSet) {
2021
+ setNewCount(0);
2022
+ return;
2023
+ }
2024
+ if (meId !== null && tail.sender_id === meId) return;
2025
+ setNewCount((n) => n + 1);
2026
+ }, [messages, atBottom, meId]);
2027
+ const handleScroll = useCallback((e) => {
2028
+ const y = e.nativeEvent.contentOffset.y;
2029
+ const next = y <= AT_BOTTOM_THRESHOLD;
2030
+ setAtBottom((prev) => prev === next ? prev : next);
2031
+ if (next) setNewCount(0);
2032
+ }, []);
2033
+ const scrollToBottom = useCallback(() => {
2034
+ flatListRef.current?.scrollToOffset({ offset: 0, animated: true });
2035
+ setNewCount(0);
2036
+ }, []);
1791
2037
  const handleRender = useCallback(
1792
- ({ item, index }) => renderItem(item, messages.length - 1 - index),
1793
- [renderItem, messages.length]
2038
+ ({ item, index }) => {
2039
+ if (item.kind === "separator") return item.node;
2040
+ const sourceIndex = data.length - 1 - index;
2041
+ return renderItem(item.msg, sourceIndex);
2042
+ },
2043
+ [renderItem, data.length]
1794
2044
  );
1795
2045
  if (loading && messages.length === 0) {
1796
- return /* @__PURE__ */ jsx(View, { style: [styles10.placeholder, { backgroundColor: theme.colors.paper }], children: /* @__PURE__ */ jsx(ActivityIndicator, { color: theme.colors.brand }) });
2046
+ return /* @__PURE__ */ jsx(View, { style: [styles11.placeholder, { backgroundColor: theme.colors.paper }], children: /* @__PURE__ */ jsx(ActivityIndicator, { color: theme.colors.brand }) });
1797
2047
  }
1798
2048
  if (messages.length === 0) {
1799
- return /* @__PURE__ */ jsx(View, { style: [styles10.placeholder, { backgroundColor: theme.colors.paper }], children: typeof emptyState === "string" ? /* @__PURE__ */ jsx(Text, { style: { color: theme.colors.ink3, fontFamily: theme.type.fontBody }, children: emptyState }) : emptyState ?? null });
2049
+ return /* @__PURE__ */ jsx(View, { style: [styles11.placeholder, { backgroundColor: theme.colors.paper }], children: typeof emptyState === "string" ? /* @__PURE__ */ jsx(Text, { style: { color: theme.colors.ink3, fontFamily: theme.type.fontBody }, children: emptyState }) : emptyState ?? null });
1800
2050
  }
1801
- return /* @__PURE__ */ jsx(
1802
- FlatList,
1803
- {
1804
- inverted: true,
1805
- data,
1806
- keyExtractor: (item) => item.id,
1807
- renderItem: handleRender,
1808
- onEndReached: hasMore ? onLoadMore : void 0,
1809
- onEndReachedThreshold: 0.3,
1810
- ListHeaderComponent: ListHeader,
1811
- ListFooterComponent: /* @__PURE__ */ jsxs(View, { children: [
1812
- hasMore && loading ? /* @__PURE__ */ jsx(ActivityIndicator, { color: theme.colors.ink3, style: { marginVertical: 8 } }) : null,
1813
- ListFooter
1814
- ] }),
1815
- style: { backgroundColor: theme.colors.paper },
1816
- contentContainerStyle: { paddingVertical: 8 }
1817
- }
1818
- );
2051
+ return /* @__PURE__ */ jsxs(View, { style: styles11.root, children: [
2052
+ /* @__PURE__ */ jsx(
2053
+ FlatList,
2054
+ {
2055
+ ref: flatListRef,
2056
+ inverted: true,
2057
+ data,
2058
+ keyExtractor: (item) => item.kind === "msg" ? item.msg.id : item.id,
2059
+ renderItem: handleRender,
2060
+ onEndReached: hasMore ? onLoadMore : void 0,
2061
+ onEndReachedThreshold: 0.3,
2062
+ onScroll: handleScroll,
2063
+ scrollEventThrottle: 32,
2064
+ ListHeaderComponent: ListHeader,
2065
+ ListFooterComponent: /* @__PURE__ */ jsxs(View, { children: [
2066
+ hasMore && loading ? /* @__PURE__ */ jsx(ActivityIndicator, { color: theme.colors.ink3, style: { marginVertical: 8 } }) : null,
2067
+ ListFooter
2068
+ ] }),
2069
+ style: { backgroundColor: theme.colors.paper },
2070
+ contentContainerStyle: { paddingVertical: 8 }
2071
+ }
2072
+ ),
2073
+ !atBottom && newCount > 0 ? /* @__PURE__ */ jsxs(
2074
+ Pressable,
2075
+ {
2076
+ onPress: scrollToBottom,
2077
+ style: [
2078
+ styles11.jumpButton,
2079
+ {
2080
+ backgroundColor: theme.colors.brand
2081
+ },
2082
+ theme.shadows.sm
2083
+ ],
2084
+ accessibilityLabel: `${newCount} new ${newCount === 1 ? "message" : "messages"}, jump to latest`,
2085
+ children: [
2086
+ /* @__PURE__ */ jsx(PoolseIcon, { name: "chevron-down", size: 14, color: theme.colors.onBrand }),
2087
+ /* @__PURE__ */ jsxs(Text, { style: [styles11.jumpText, { color: theme.colors.onBrand }], children: [
2088
+ newCount,
2089
+ " new ",
2090
+ newCount === 1 ? "message" : "messages"
2091
+ ] })
2092
+ ]
2093
+ }
2094
+ ) : null
2095
+ ] });
1819
2096
  }
1820
- var styles10 = StyleSheet.create({
2097
+ var styles11 = StyleSheet.create({
2098
+ root: {
2099
+ flex: 1
2100
+ },
1821
2101
  placeholder: {
1822
2102
  flex: 1,
1823
2103
  alignItems: "center",
1824
2104
  justifyContent: "center",
1825
2105
  padding: 24
1826
- }
1827
- });
1828
- function EditableMessageBubble({
1829
- message,
1830
- currentUserId,
1831
- onCancel,
1832
- onSave
1833
- }) {
1834
- const theme = usePoolseTheme();
1835
- const isSelf = currentUserId !== null && message.sender_id === currentUserId;
1836
- const [value, setValue] = useState(message.body ?? "");
1837
- const [saving, setSaving] = useState(false);
1838
- const submit = async () => {
1839
- if (saving || !value.trim()) return;
1840
- setSaving(true);
1841
- try {
1842
- await onSave(value.trim());
1843
- } finally {
1844
- setSaving(false);
1845
- }
1846
- };
1847
- const bg = isSelf ? theme.colors.selfBubble : theme.colors.otherBubble;
1848
- const ink = isSelf ? theme.colors.selfBubbleText : theme.colors.otherBubbleText;
1849
- return /* @__PURE__ */ jsx(
1850
- View,
1851
- {
1852
- style: [
1853
- styles11.wrap,
1854
- {
1855
- alignSelf: isSelf ? "flex-end" : "flex-start",
1856
- maxWidth: "80%"
1857
- }
1858
- ],
1859
- children: /* @__PURE__ */ jsxs(
1860
- View,
1861
- {
1862
- style: [
1863
- styles11.bubble,
1864
- {
1865
- backgroundColor: bg,
1866
- borderRadius: theme.radii.bubble,
1867
- paddingHorizontal: theme.spacing.md,
1868
- paddingVertical: theme.spacing.sm
1869
- },
1870
- theme.shadows.sm
1871
- ],
1872
- children: [
1873
- /* @__PURE__ */ jsx(
1874
- TextInput,
1875
- {
1876
- value,
1877
- onChangeText: setValue,
1878
- multiline: true,
1879
- autoFocus: true,
1880
- style: {
1881
- color: ink,
1882
- fontFamily: theme.type.fontBody,
1883
- fontSize: theme.type.bodySize,
1884
- minHeight: 24
1885
- }
1886
- }
1887
- ),
1888
- /* @__PURE__ */ jsxs(View, { style: styles11.actions, children: [
1889
- /* @__PURE__ */ jsx(Pressable, { onPress: onCancel, hitSlop: 6, children: /* @__PURE__ */ jsx(Text, { style: [styles11.btn, { color: ink, opacity: 0.7 }], children: "Cancel" }) }),
1890
- /* @__PURE__ */ jsx(Pressable, { onPress: submit, disabled: saving, hitSlop: 6, children: /* @__PURE__ */ jsx(Text, { style: [styles11.btn, { color: ink, fontWeight: "700" }], children: saving ? "Saving\u2026" : "Save" }) })
1891
- ] })
1892
- ]
1893
- }
1894
- )
1895
- }
1896
- );
1897
- }
1898
- var styles11 = StyleSheet.create({
1899
- wrap: {
1900
- marginBottom: 4
1901
2106
  },
1902
- bubble: {},
1903
- actions: {
2107
+ jumpButton: {
2108
+ position: "absolute",
2109
+ alignSelf: "center",
2110
+ bottom: 12,
1904
2111
  flexDirection: "row",
1905
- justifyContent: "flex-end",
1906
- gap: 12,
1907
- marginTop: 6
2112
+ alignItems: "center",
2113
+ gap: 6,
2114
+ paddingHorizontal: 14,
2115
+ paddingVertical: 8,
2116
+ borderRadius: 999
1908
2117
  },
1909
- btn: {
1910
- fontSize: 13
2118
+ jumpText: {
2119
+ fontSize: 13,
2120
+ fontWeight: "600"
1911
2121
  }
1912
2122
  });
1913
2123
  var DEFAULT_REACTION_EMOJIS = ["\u{1F44D}", "\u2764\uFE0F", "\u{1F602}", "\u{1F389}", "\u{1F64F}", "\u{1F525}"];
@@ -2370,6 +2580,77 @@ var styles14 = StyleSheet.create({
2370
2580
  borderRadius: 20
2371
2581
  }
2372
2582
  });
2583
+ var MarkdownModule;
2584
+ var warned = false;
2585
+ function loadMarkdown() {
2586
+ if (MarkdownModule === void 0) {
2587
+ try {
2588
+ MarkdownModule = __require("react-native-markdown-display");
2589
+ } catch {
2590
+ MarkdownModule = null;
2591
+ }
2592
+ }
2593
+ const fn = MarkdownModule?.default;
2594
+ if (!fn) {
2595
+ if (!warned) {
2596
+ warned = true;
2597
+ console.warn(
2598
+ "[@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."
2599
+ );
2600
+ }
2601
+ return null;
2602
+ }
2603
+ return fn;
2604
+ }
2605
+ function renderMarkdown(body, args) {
2606
+ const Markdown = loadMarkdown();
2607
+ if (!Markdown) return null;
2608
+ const baseText = {
2609
+ color: args.textColor,
2610
+ fontFamily: args.fontFamily,
2611
+ fontSize: args.fontSize,
2612
+ lineHeight: args.lineHeight
2613
+ };
2614
+ const style = {
2615
+ body: baseText,
2616
+ paragraph: { ...baseText, marginTop: 0, marginBottom: 0 },
2617
+ text: baseText,
2618
+ strong: { ...baseText, fontWeight: "700" },
2619
+ em: { ...baseText, fontStyle: "italic" },
2620
+ s: { ...baseText, textDecorationLine: "line-through" },
2621
+ link: { color: args.linkColor, textDecorationLine: "underline" },
2622
+ code_inline: {
2623
+ ...baseText,
2624
+ backgroundColor: "rgba(127,127,127,0.18)",
2625
+ borderRadius: 4,
2626
+ paddingHorizontal: 4
2627
+ },
2628
+ code_block: {
2629
+ ...baseText,
2630
+ backgroundColor: "rgba(127,127,127,0.18)",
2631
+ padding: 8,
2632
+ borderRadius: 6
2633
+ },
2634
+ fence: {
2635
+ ...baseText,
2636
+ backgroundColor: "rgba(127,127,127,0.18)",
2637
+ padding: 8,
2638
+ borderRadius: 6
2639
+ },
2640
+ bullet_list: { marginTop: 2, marginBottom: 2 },
2641
+ ordered_list: { marginTop: 2, marginBottom: 2 },
2642
+ list_item: baseText,
2643
+ blockquote: {
2644
+ ...baseText,
2645
+ borderLeftWidth: 3,
2646
+ borderLeftColor: args.linkColor,
2647
+ paddingLeft: 8,
2648
+ marginVertical: 4,
2649
+ opacity: 0.85
2650
+ }
2651
+ };
2652
+ return createElement(Markdown, { style, onLinkPress: args.onLinkPress }, body);
2653
+ }
2373
2654
  function MessageBubble({
2374
2655
  message,
2375
2656
  currentUserId,
@@ -2380,7 +2661,8 @@ function MessageBubble({
2380
2661
  maxBodyLength = 0,
2381
2662
  showSenderName = false,
2382
2663
  actionsTrigger,
2383
- showAttachments = true
2664
+ showAttachments = true,
2665
+ markdown = true
2384
2666
  }) {
2385
2667
  const theme = usePoolseTheme();
2386
2668
  const { width: screenWidth } = useWindowDimensions();
@@ -2465,22 +2747,53 @@ function MessageBubble({
2465
2747
  ) : null,
2466
2748
  renderAttachments && images.length > 0 ? /* @__PURE__ */ jsx(ImageMosaic, { images, maxWidth: contentMaxWidth }) : null,
2467
2749
  renderAttachments && files.length > 0 ? /* @__PURE__ */ jsx(View, { style: { gap: theme.spacing.xs, marginBottom: theme.spacing.xs }, children: files.map((att) => /* @__PURE__ */ jsx(AttachmentPreview, { attachment: att, maxWidth: contentMaxWidth }, att.id)) }) : null,
2468
- body.length > 0 ? /* @__PURE__ */ jsx(
2469
- Text,
2470
- {
2471
- style: [
2472
- styles15.body,
2750
+ body.length > 0 ? (() => {
2751
+ if (isDeleted) {
2752
+ return /* @__PURE__ */ jsx(
2753
+ Text,
2473
2754
  {
2474
- color: isDeleted ? theme.colors.ink3 : textColor,
2475
- fontFamily: theme.type.fontBody,
2476
- fontSize: theme.type.bodySize,
2477
- lineHeight: theme.type.lineHeight,
2478
- fontStyle: isDeleted ? "italic" : "normal"
2755
+ style: [
2756
+ styles15.body,
2757
+ {
2758
+ color: theme.colors.ink3,
2759
+ fontFamily: theme.type.fontBody,
2760
+ fontSize: theme.type.bodySize,
2761
+ lineHeight: theme.type.lineHeight,
2762
+ fontStyle: "italic"
2763
+ }
2764
+ ],
2765
+ children: "Message deleted"
2479
2766
  }
2480
- ],
2481
- children: isDeleted ? "Message deleted" : truncated
2767
+ );
2482
2768
  }
2483
- ) : null,
2769
+ const md = markdown ? renderMarkdown(truncated, {
2770
+ textColor,
2771
+ linkColor: isSelf ? theme.colors.onBrand : theme.colors.brand,
2772
+ fontFamily: theme.type.fontBody,
2773
+ fontSize: theme.type.bodySize,
2774
+ lineHeight: theme.type.lineHeight,
2775
+ onLinkPress: (url) => {
2776
+ void Linking.openURL(url).catch(() => void 0);
2777
+ return false;
2778
+ }
2779
+ }) : null;
2780
+ if (md) return md;
2781
+ return /* @__PURE__ */ jsx(
2782
+ Text,
2783
+ {
2784
+ style: [
2785
+ styles15.body,
2786
+ {
2787
+ color: textColor,
2788
+ fontFamily: theme.type.fontBody,
2789
+ fontSize: theme.type.bodySize,
2790
+ lineHeight: theme.type.lineHeight
2791
+ }
2792
+ ],
2793
+ children: truncated
2794
+ }
2795
+ );
2796
+ })() : null,
2484
2797
  maxBodyLength > 0 && body.length > maxBodyLength ? /* @__PURE__ */ jsx(Pressable, { onPress: () => setExpanded((e) => !e), children: /* @__PURE__ */ jsx(
2485
2798
  Text,
2486
2799
  {
@@ -2573,9 +2886,9 @@ function formatTime(iso) {
2573
2886
  try {
2574
2887
  const d = new Date(iso);
2575
2888
  const now = /* @__PURE__ */ new Date();
2576
- const sameDay = isSameDay(d, now);
2889
+ const sameDay2 = isSameDay(d, now);
2577
2890
  const time = d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
2578
- if (sameDay) return time;
2891
+ if (sameDay2) return time;
2579
2892
  const yesterday = new Date(now);
2580
2893
  yesterday.setDate(now.getDate() - 1);
2581
2894
  if (isSameDay(d, yesterday)) return `Yesterday ${time}`;
@@ -2683,12 +2996,26 @@ function ReactionStrip({
2683
2996
  styles16.chip,
2684
2997
  {
2685
2998
  backgroundColor: meReacted ? theme.colors.brandSoft : theme.colors.surface2,
2686
- borderColor: meReacted ? theme.colors.brand : theme.colors.border
2999
+ borderColor: meReacted ? theme.colors.brand : theme.colors.border,
3000
+ borderWidth: meReacted ? 1.5 : 1,
3001
+ transform: meReacted ? [{ scale: 1.04 }] : void 0
2687
3002
  }
2688
3003
  ],
2689
3004
  children: [
2690
3005
  /* @__PURE__ */ jsx(Text, { style: styles16.emoji, children: emoji }),
2691
- /* @__PURE__ */ jsx(Text, { style: [styles16.count, { color: theme.colors.ink2 }], children: userArr.length })
3006
+ /* @__PURE__ */ jsx(
3007
+ Text,
3008
+ {
3009
+ style: [
3010
+ styles16.count,
3011
+ {
3012
+ color: meReacted ? theme.colors.brand : theme.colors.ink2,
3013
+ fontWeight: meReacted ? "700" : "600"
3014
+ }
3015
+ ],
3016
+ children: userArr.length
3017
+ }
3018
+ )
2692
3019
  ]
2693
3020
  },
2694
3021
  emoji
@@ -2741,7 +3068,6 @@ var styles16 = StyleSheet.create({
2741
3068
  paddingHorizontal: 9,
2742
3069
  paddingVertical: 4,
2743
3070
  borderRadius: 14,
2744
- borderWidth: 1,
2745
3071
  gap: 5,
2746
3072
  minHeight: 26,
2747
3073
  // Subtle elevation so chips lift off the surface
@@ -2757,7 +3083,6 @@ var styles16 = StyleSheet.create({
2757
3083
  },
2758
3084
  count: {
2759
3085
  fontSize: 12,
2760
- fontWeight: "600",
2761
3086
  fontVariant: ["tabular-nums"]
2762
3087
  },
2763
3088
  backdrop: {
@@ -2795,11 +3120,10 @@ function MessageRow({
2795
3120
  actions = true,
2796
3121
  threads = true,
2797
3122
  quotations = true,
3123
+ markdown = true,
2798
3124
  readState,
2799
- editing = false,
3125
+ isEditing = false,
2800
3126
  onStartEdit,
2801
- onCancelEdit,
2802
- onSaveEdit,
2803
3127
  onDelete,
2804
3128
  onOpenThread,
2805
3129
  onQuote,
@@ -2820,7 +3144,7 @@ function MessageRow({
2820
3144
  ...msg.reactions ? { initialReactions: msg.reactions } : {}
2821
3145
  });
2822
3146
  const translateX = useRef(new Animated.Value(0)).current;
2823
- const swipeEnabled = quotations && !!onQuote && !editing;
3147
+ const swipeEnabled = quotations && !!onQuote && !isEditing;
2824
3148
  const panResponder = useMemo(
2825
3149
  () => PanResponder.create({
2826
3150
  // Tap / long-press still go to the inner Pressable.
@@ -2895,137 +3219,149 @@ function MessageRow({
2895
3219
  const rowSender = useUser(showAvatarSlot ? msg.sender_external_id : null);
2896
3220
  const avatarUrl = msg.sender_external_id ? avatarFor?.(msg.sender_external_id) ?? rowSender.profile?.avatarUrl ?? null : null;
2897
3221
  const senderLabel = msg.sender_external_id ? labelFor?.(msg.sender_external_id) ?? rowSender.profile?.displayName ?? msg.sender_external_id : null;
2898
- return /* @__PURE__ */ jsxs(View, { style: [styles17.row, { alignSelf: isSelf ? "flex-end" : "flex-start" }], children: [
2899
- showAvatarSlot ? /* @__PURE__ */ jsx(View, { style: styles17.avatarSlot, children: showAvatarRender ? /* @__PURE__ */ jsx(
2900
- Avatar,
2901
- {
2902
- src: avatarUrl ?? null,
2903
- name: senderLabel,
2904
- seed: msg.sender_external_id ?? null,
2905
- size: "sm"
2906
- }
2907
- ) : null }) : null,
2908
- /* @__PURE__ */ jsxs(View, { style: styles17.bubbleCol, children: [
2909
- swipeEnabled ? /* @__PURE__ */ jsx(
2910
- Animated.View,
2911
- {
2912
- pointerEvents: "none",
2913
- style: [
2914
- styles17.replyHint,
2915
- isSelf ? styles17.replyHintRight : styles17.replyHintLeft,
2916
- {
2917
- opacity: replyIconOpacity,
2918
- transform: [{ scale: replyIconScale }],
2919
- backgroundColor: theme.colors.surface2
2920
- }
2921
- ],
2922
- children: /* @__PURE__ */ jsx(PoolseIcon, { name: "reply", size: 16, color: theme.colors.brand })
2923
- }
2924
- ) : null,
2925
- /* @__PURE__ */ jsx(
2926
- Animated.View,
2927
- {
2928
- ...swipeEnabled ? panResponder.panHandlers : {},
2929
- style: { transform: [{ translateX }] },
2930
- children: /* @__PURE__ */ jsx(
2931
- Pressable,
3222
+ return /* @__PURE__ */ jsxs(
3223
+ View,
3224
+ {
3225
+ style: [
3226
+ styles17.row,
3227
+ { alignSelf: isSelf ? "flex-end" : "flex-start" },
3228
+ // Lift this row above the dim backdrop the parent renders
3229
+ // during edit mode. zIndex covers iOS / web; elevation does
3230
+ // the equivalent on Android.
3231
+ isEditing ? styles17.rowEditing : null
3232
+ ],
3233
+ children: [
3234
+ showAvatarSlot ? /* @__PURE__ */ jsx(View, { style: styles17.avatarSlot, children: showAvatarRender ? /* @__PURE__ */ jsx(
3235
+ Avatar,
3236
+ {
3237
+ src: avatarUrl ?? null,
3238
+ name: senderLabel,
3239
+ seed: msg.sender_external_id ?? null,
3240
+ size: "sm"
3241
+ }
3242
+ ) : null }) : null,
3243
+ /* @__PURE__ */ jsxs(View, { style: styles17.bubbleCol, children: [
3244
+ swipeEnabled ? /* @__PURE__ */ jsx(
3245
+ Animated.View,
2932
3246
  {
2933
- onLongPress: () => {
2934
- if (!actions) return;
2935
- setActionsOpen(true);
2936
- },
2937
- accessibilityRole: "button",
2938
- accessibilityLabel: "Message actions",
2939
- children: editing ? /* @__PURE__ */ jsx(
2940
- EditableMessageBubble,
3247
+ pointerEvents: "none",
3248
+ style: [
3249
+ styles17.replyHint,
3250
+ isSelf ? styles17.replyHintRight : styles17.replyHintLeft,
2941
3251
  {
2942
- message: msg,
2943
- currentUserId: meId,
2944
- onCancel: onCancelEdit ?? (() => void 0),
2945
- onSave: onSaveEdit ?? (() => void 0)
3252
+ opacity: replyIconOpacity,
3253
+ transform: [{ scale: replyIconScale }],
3254
+ backgroundColor: theme.colors.surface2
2946
3255
  }
2947
- ) : /* @__PURE__ */ jsx(
2948
- MessageBubble,
3256
+ ],
3257
+ children: /* @__PURE__ */ jsx(PoolseIcon, { name: "reply", size: 16, color: theme.colors.brand })
3258
+ }
3259
+ ) : null,
3260
+ /* @__PURE__ */ jsx(
3261
+ Animated.View,
3262
+ {
3263
+ ...swipeEnabled ? panResponder.panHandlers : {},
3264
+ style: { transform: [{ translateX }] },
3265
+ children: /* @__PURE__ */ jsx(
3266
+ Pressable,
2949
3267
  {
2950
- message: msg,
2951
- currentUserId: meId,
2952
- ...readState ? { readState } : {},
2953
- ...labelFor ? { labelFor } : {},
2954
- ...onQuotedClick ? { onQuotedClick } : {},
2955
- groupPosition,
2956
- maxBodyLength,
2957
- showSenderName,
2958
- showAttachments: attachments,
2959
- actionsTrigger: actions ? /* @__PURE__ */ jsx(Pressable, { onPress: () => setActionsOpen(true), hitSlop: 8, children: /* @__PURE__ */ jsx(
2960
- PoolseIcon,
3268
+ onLongPress: () => {
3269
+ if (!actions) return;
3270
+ setActionsOpen(true);
3271
+ },
3272
+ accessibilityRole: "button",
3273
+ accessibilityLabel: "Message actions",
3274
+ children: /* @__PURE__ */ jsx(
3275
+ MessageBubble,
2961
3276
  {
2962
- name: "chevron-down",
2963
- size: 14,
2964
- color: isSelf ? theme.colors.onBrand : theme.colors.ink3
3277
+ message: msg,
3278
+ currentUserId: meId,
3279
+ ...readState ? { readState } : {},
3280
+ ...labelFor ? { labelFor } : {},
3281
+ ...onQuotedClick ? { onQuotedClick } : {},
3282
+ groupPosition,
3283
+ maxBodyLength,
3284
+ showSenderName,
3285
+ showAttachments: attachments,
3286
+ markdown,
3287
+ actionsTrigger: actions ? /* @__PURE__ */ jsx(Pressable, { onPress: () => setActionsOpen(true), hitSlop: 8, children: /* @__PURE__ */ jsx(
3288
+ PoolseIcon,
3289
+ {
3290
+ name: "chevron-down",
3291
+ size: 14,
3292
+ color: isSelf ? theme.colors.onBrand : theme.colors.ink3
3293
+ }
3294
+ ) }) : null
2965
3295
  }
2966
- ) }) : null
3296
+ )
2967
3297
  }
2968
3298
  )
2969
3299
  }
2970
- )
2971
- }
2972
- ),
2973
- threads && (msg.reply_count ?? 0) > 0 && onOpenThread ? /* @__PURE__ */ jsx(Pressable, { onPress: onOpenThread, style: styles17.threadPill, children: /* @__PURE__ */ jsxs(Text, { style: [styles17.threadPillText, { color: theme.colors.brand }], children: [
2974
- "\u{1F4AC} ",
2975
- msg.reply_count,
2976
- " ",
2977
- msg.reply_count === 1 ? "reply" : "replies"
2978
- ] }) }) : null,
2979
- reactions ? /* @__PURE__ */ jsx(View, { style: [styles17.reactionsRow, { alignSelf: isSelf ? "flex-end" : "flex-start" }], children: /* @__PURE__ */ jsx(
2980
- ReactionStrip,
2981
- {
2982
- messageId: msg.id,
2983
- conversationId: msg.conversation_id,
2984
- meId,
2985
- ...msg.reactions ? { initialReactions: msg.reactions } : {}
2986
- }
2987
- ) }) : null
2988
- ] }),
2989
- actions && actionsOpen ? /* @__PURE__ */ jsx(
2990
- MessageActions,
2991
- {
2992
- visible: actionsOpen,
2993
- onClose: () => setActionsOpen(false),
2994
- isSelf,
2995
- ...reactions ? {
2996
- // Inline quick-react row at the top of the sheet —
2997
- // one tap = react + close, no second modal needed.
2998
- onPickReaction: (emoji) => {
2999
- void reactionsApi.addReaction(emoji);
3000
- }
3001
- } : {},
3002
- ...onStartEdit ? {
3003
- onEdit: () => {
3004
- setActionsOpen(false);
3005
- onStartEdit();
3006
- }
3007
- } : {},
3008
- ...onDelete ? {
3009
- onDelete: () => {
3010
- setActionsOpen(false);
3011
- onDelete();
3012
- }
3013
- } : {},
3014
- ...onOpenThread ? {
3015
- onReplyInThread: () => {
3016
- setActionsOpen(false);
3017
- onOpenThread();
3018
- }
3019
- } : {},
3020
- ...quotations && onQuote ? {
3021
- onQuote: () => {
3022
- setActionsOpen(false);
3023
- onQuote();
3300
+ ),
3301
+ threads && (msg.reply_count ?? 0) > 0 && onOpenThread ? /* @__PURE__ */ jsx(Pressable, { onPress: onOpenThread, style: styles17.threadPill, children: /* @__PURE__ */ jsxs(Text, { style: [styles17.threadPillText, { color: theme.colors.brand }], children: [
3302
+ "\u{1F4AC} ",
3303
+ msg.reply_count,
3304
+ " ",
3305
+ msg.reply_count === 1 ? "reply" : "replies"
3306
+ ] }) }) : null,
3307
+ reactions ? /* @__PURE__ */ jsx(View, { style: [styles17.reactionsRow, { alignSelf: isSelf ? "flex-end" : "flex-start" }], children: /* @__PURE__ */ jsx(
3308
+ ReactionStrip,
3309
+ {
3310
+ messageId: msg.id,
3311
+ conversationId: msg.conversation_id,
3312
+ meId,
3313
+ ...msg.reactions ? { initialReactions: msg.reactions } : {}
3314
+ }
3315
+ ) }) : null
3316
+ ] }),
3317
+ actions && actionsOpen ? /* @__PURE__ */ jsx(
3318
+ MessageActions,
3319
+ {
3320
+ visible: actionsOpen,
3321
+ onClose: () => setActionsOpen(false),
3322
+ isSelf,
3323
+ ...reactions ? {
3324
+ // Inline quick-react row at the top of the sheet —
3325
+ // one tap = react + close, no second modal needed.
3326
+ onPickReaction: (emoji) => {
3327
+ void reactionsApi.addReaction(emoji);
3328
+ }
3329
+ } : {},
3330
+ ...msg.body && !msg.deleted_at ? {
3331
+ onCopy: () => {
3332
+ setActionsOpen(false);
3333
+ void copyToClipboard(msg.body ?? "");
3334
+ }
3335
+ } : {},
3336
+ ...onStartEdit ? {
3337
+ onEdit: () => {
3338
+ setActionsOpen(false);
3339
+ onStartEdit();
3340
+ }
3341
+ } : {},
3342
+ ...onDelete ? {
3343
+ onDelete: () => {
3344
+ setActionsOpen(false);
3345
+ onDelete();
3346
+ }
3347
+ } : {},
3348
+ ...onOpenThread ? {
3349
+ onReplyInThread: () => {
3350
+ setActionsOpen(false);
3351
+ onOpenThread();
3352
+ }
3353
+ } : {},
3354
+ ...quotations && onQuote ? {
3355
+ onQuote: () => {
3356
+ setActionsOpen(false);
3357
+ onQuote();
3358
+ }
3359
+ } : {}
3024
3360
  }
3025
- } : {}
3026
- }
3027
- ) : null
3028
- ] });
3361
+ ) : null
3362
+ ]
3363
+ }
3364
+ );
3029
3365
  }
3030
3366
  var styles17 = StyleSheet.create({
3031
3367
  row: {
@@ -3041,6 +3377,12 @@ var styles17 = StyleSheet.create({
3041
3377
  paddingHorizontal: 10,
3042
3378
  maxWidth: "95%"
3043
3379
  },
3380
+ rowEditing: {
3381
+ // Above ConversationView's editBackdrop (zIndex 5). Elevation
3382
+ // does the equivalent stacking on Android.
3383
+ zIndex: 10,
3384
+ elevation: 10
3385
+ },
3044
3386
  avatarSlot: {
3045
3387
  width: 36,
3046
3388
  marginRight: 6
@@ -3080,6 +3422,27 @@ var styles17 = StyleSheet.create({
3080
3422
  marginTop: 2
3081
3423
  }
3082
3424
  });
3425
+ var clipboardModule;
3426
+ var clipboardWarned = false;
3427
+ async function copyToClipboard(text) {
3428
+ if (clipboardModule === void 0) {
3429
+ try {
3430
+ clipboardModule = __require("expo-clipboard");
3431
+ } catch {
3432
+ clipboardModule = null;
3433
+ }
3434
+ }
3435
+ if (!clipboardModule?.setStringAsync) {
3436
+ if (!clipboardWarned) {
3437
+ clipboardWarned = true;
3438
+ console.warn(
3439
+ "[@poolse/react-native] Copy action requires `expo-clipboard`. Install it to enable copy-to-clipboard."
3440
+ );
3441
+ }
3442
+ return;
3443
+ }
3444
+ await clipboardModule.setStringAsync(text);
3445
+ }
3083
3446
  var SHEET_HEIGHT_FRACTION = 0.88;
3084
3447
  function ThreadView({
3085
3448
  visible,
@@ -3135,31 +3498,34 @@ function ThreadSheet({
3135
3498
  const [pickerOpen, setPickerOpen] = useState(false);
3136
3499
  const [editingId, setEditingId] = useState(null);
3137
3500
  const renderReply = useCallback(
3138
- (msg) => /* @__PURE__ */ jsx(
3139
- MessageRow,
3140
- {
3141
- msg,
3142
- meId,
3143
- reactions: true,
3144
- attachments: true,
3145
- actions: true,
3146
- threads: false,
3147
- quotations: false,
3148
- editing: editingId === msg.id,
3149
- onStartEdit: () => setEditingId(msg.id),
3150
- onCancelEdit: () => setEditingId(null),
3151
- onSaveEdit: async (body) => {
3152
- await edit(msg.id, body);
3153
- setEditingId(null);
3154
- },
3155
- onDelete: () => {
3156
- void deleteReply(msg.id);
3157
- },
3158
- ...labelFor ? { labelFor } : {},
3159
- ...avatarFor ? { avatarFor } : {}
3160
- }
3161
- ),
3162
- [meId, editingId, edit, deleteReply, labelFor, avatarFor]
3501
+ (msg) => {
3502
+ const isSelf = meId !== null && msg.sender_id === meId;
3503
+ const canEdit = isSelf && !msg.deleted_at;
3504
+ return /* @__PURE__ */ jsx(
3505
+ MessageRow,
3506
+ {
3507
+ msg,
3508
+ meId,
3509
+ reactions: true,
3510
+ attachments: true,
3511
+ actions: true,
3512
+ threads: false,
3513
+ quotations: false,
3514
+ isEditing: editingId === msg.id,
3515
+ ...canEdit ? { onStartEdit: () => setEditingId(msg.id) } : {},
3516
+ onDelete: () => {
3517
+ void deleteReply(msg.id);
3518
+ },
3519
+ ...labelFor ? { labelFor } : {},
3520
+ ...avatarFor ? { avatarFor } : {}
3521
+ }
3522
+ );
3523
+ },
3524
+ [meId, editingId, deleteReply, labelFor, avatarFor]
3525
+ );
3526
+ const editingMessage = useMemo(
3527
+ () => editingId ? replies.find((m) => m.id === editingId) ?? null : null,
3528
+ [editingId, replies]
3163
3529
  );
3164
3530
  const rootRow = useMemo(
3165
3531
  () => /* @__PURE__ */ jsx(
@@ -3245,7 +3611,14 @@ function ThreadSheet({
3245
3611
  },
3246
3612
  placeholder: "Reply to thread\u2026",
3247
3613
  attachments: true,
3248
- onAttachPress: () => setPickerOpen(true)
3614
+ onAttachPress: () => setPickerOpen(true),
3615
+ editingMessage,
3616
+ onCancelEdit: () => setEditingId(null),
3617
+ onSaveEdit: async (body) => {
3618
+ if (!editingId) return;
3619
+ await edit(editingId, body);
3620
+ setEditingId(null);
3621
+ }
3249
3622
  }
3250
3623
  )
3251
3624
  ]
@@ -3409,6 +3782,7 @@ var styles19 = StyleSheet.create({
3409
3782
  fontSize: 12
3410
3783
  }
3411
3784
  });
3785
+ var GROUPING_WINDOW_MS = 5 * 60 * 1e3;
3412
3786
  function ConversationView({
3413
3787
  conversationId,
3414
3788
  emptyState,
@@ -3425,6 +3799,7 @@ function ConversationView({
3425
3799
  threads = true,
3426
3800
  quotations = true,
3427
3801
  readReceipts = true,
3802
+ markdown = true,
3428
3803
  maxBodyLength = 200,
3429
3804
  senderLabels = "auto",
3430
3805
  avatars = "auto",
@@ -3443,9 +3818,25 @@ function ConversationView({
3443
3818
  send,
3444
3819
  edit,
3445
3820
  delete: deleteMsg,
3446
- markReadUpTo
3821
+ markReadUpTo,
3822
+ refetch
3447
3823
  } = useMessages(conversationId);
3448
3824
  const { typing, signalTyping } = useTyping(conversationId);
3825
+ const realtimeStatus = useRealtimeStatus();
3826
+ useEffect(() => {
3827
+ const sub = AppState.addEventListener("change", (next) => {
3828
+ if (next === "active") void refetch();
3829
+ });
3830
+ return () => sub.remove();
3831
+ }, [refetch]);
3832
+ const prevStatusRef = useRef(realtimeStatus);
3833
+ useEffect(() => {
3834
+ const prev = prevStatusRef.current;
3835
+ prevStatusRef.current = realtimeStatus;
3836
+ if (realtimeStatus === "connected" && (prev === "reconnecting" || prev === "closed" || prev === "connecting")) {
3837
+ void refetch();
3838
+ }
3839
+ }, [realtimeStatus, refetch]);
3449
3840
  const wantsMembers = mentions || senderLabels === "auto" || avatars === "auto";
3450
3841
  const { members } = useMembers(wantsMembers ? conversationId : "");
3451
3842
  const isGroupChat = members.length > 2;
@@ -3476,10 +3867,24 @@ function ConversationView({
3476
3867
  });
3477
3868
  onMarkedRead?.(conversationId, latest.id);
3478
3869
  }, [messages, meId, readReceipts, markReadUpTo, onMarkedRead, conversationId]);
3870
+ const groupPositionById = useMemo(() => {
3871
+ const map = /* @__PURE__ */ new Map();
3872
+ for (let i = 0; i < messages.length; i++) {
3873
+ const msg = messages[i];
3874
+ if (!msg) continue;
3875
+ const prev = i > 0 ? messages[i - 1] ?? null : null;
3876
+ const next = i < messages.length - 1 ? messages[i + 1] ?? null : null;
3877
+ map.set(msg.id, computeGroupPosition(msg, prev, next, GROUPING_WINDOW_MS));
3878
+ }
3879
+ return map;
3880
+ }, [messages]);
3479
3881
  const renderItem = useCallback(
3480
3882
  (msg) => {
3481
3883
  if (renderMessage) return /* @__PURE__ */ jsx(Fragment, { children: renderMessage(msg, meId) });
3482
3884
  const readState = readReceipts && meId !== null && msg.sender_id === meId ? readStateForMessage(msg, members, meId, sequenceByMessageId) : void 0;
3885
+ const isSelf = meId !== null && msg.sender_id === meId;
3886
+ const canEdit = isSelf && !msg.deleted_at && (readState ?? "sent") === "sent";
3887
+ const groupPosition = groupPositionById.get(msg.id) ?? "standalone";
3483
3888
  return /* @__PURE__ */ jsx(
3484
3889
  MessageRow,
3485
3890
  {
@@ -3491,13 +3896,8 @@ function ConversationView({
3491
3896
  threads,
3492
3897
  quotations,
3493
3898
  ...readState ? { readState } : {},
3494
- editing: editingId === msg.id,
3495
- onStartEdit: () => setEditingId(msg.id),
3496
- onCancelEdit: () => setEditingId(null),
3497
- onSaveEdit: async (body) => {
3498
- await edit(msg.id, body);
3499
- setEditingId(null);
3500
- },
3899
+ isEditing: editingId === msg.id,
3900
+ ...canEdit ? { onStartEdit: () => setEditingId(msg.id) } : {},
3501
3901
  onDelete: () => deleteMsg(msg.id),
3502
3902
  ...threads ? { onOpenThread: () => setThreadRoot(msg) } : {},
3503
3903
  ...quotations ? { onQuote: () => setReplyingTo(msg) } : {},
@@ -3505,7 +3905,9 @@ function ConversationView({
3505
3905
  ...avatarFor ? { avatarFor } : {},
3506
3906
  showSenderName: showSenderLabels,
3507
3907
  showAvatar: showAvatars,
3508
- maxBodyLength
3908
+ maxBodyLength,
3909
+ groupPosition,
3910
+ markdown
3509
3911
  }
3510
3912
  );
3511
3913
  },
@@ -3520,16 +3922,38 @@ function ConversationView({
3520
3922
  threads,
3521
3923
  quotations,
3522
3924
  editingId,
3523
- edit,
3524
3925
  deleteMsg,
3525
3926
  labelFor,
3526
3927
  avatarFor,
3527
3928
  showSenderLabels,
3528
3929
  showAvatars,
3529
3930
  maxBodyLength,
3530
- sequenceByMessageId
3931
+ sequenceByMessageId,
3932
+ groupPositionById,
3933
+ markdown
3531
3934
  ]
3532
3935
  );
3936
+ const editingMessage = useMemo(
3937
+ () => editingId ? messages.find((m) => m.id === editingId) ?? null : null,
3938
+ [editingId, messages]
3939
+ );
3940
+ const listItems = useMemo(() => {
3941
+ const out = [];
3942
+ let prev = null;
3943
+ for (const msg of messages) {
3944
+ if (prev === null || !sameDay(msg.inserted_at, prev.inserted_at)) {
3945
+ const label = formatDayLabel(msg.inserted_at);
3946
+ out.push({
3947
+ kind: "separator",
3948
+ id: `sep-${msg.id}`,
3949
+ node: /* @__PURE__ */ jsx(DaySeparator, { label })
3950
+ });
3951
+ }
3952
+ out.push({ kind: "msg", msg });
3953
+ prev = msg;
3954
+ }
3955
+ return out;
3956
+ }, [messages]);
3533
3957
  const composerProps = useMemo(
3534
3958
  () => ({
3535
3959
  onSend: async (body, opts) => {
@@ -3543,9 +3967,28 @@ function ConversationView({
3543
3967
  onCancelReply: () => setReplyingTo(null),
3544
3968
  ...labelFor ? { labelFor } : {},
3545
3969
  attachments,
3546
- keyboardOffset
3970
+ keyboardOffset,
3971
+ editingMessage,
3972
+ onCancelEdit: () => setEditingId(null),
3973
+ onSaveEdit: async (body) => {
3974
+ if (!editingId) return;
3975
+ await edit(editingId, body);
3976
+ setEditingId(null);
3977
+ }
3547
3978
  }),
3548
- [send, signalTyping, attachments, replyingTo, labelFor, keyboardOffset, onSent, conversationId]
3979
+ [
3980
+ send,
3981
+ signalTyping,
3982
+ attachments,
3983
+ replyingTo,
3984
+ labelFor,
3985
+ keyboardOffset,
3986
+ onSent,
3987
+ conversationId,
3988
+ editingMessage,
3989
+ editingId,
3990
+ edit
3991
+ ]
3549
3992
  );
3550
3993
  const headerNode = header === false ? null : header === true ? /* @__PURE__ */ jsx(
3551
3994
  ChatHeader,
@@ -3565,18 +4008,30 @@ function ConversationView({
3565
4008
  keyboardVerticalOffset: keyboardOffset,
3566
4009
  children: [
3567
4010
  headerNode,
3568
- /* @__PURE__ */ jsx(View, { style: styles20.listWrap, children: /* @__PURE__ */ jsx(
3569
- MessageList,
3570
- {
3571
- messages,
3572
- loading,
3573
- hasMore,
3574
- onLoadMore: loadMore,
3575
- renderItem,
3576
- ListHeader: /* @__PURE__ */ jsx(TypingIndicator, { typing, ...labelFor ? { labelFor } : {} }),
3577
- ...emptyState !== void 0 ? { emptyState } : {}
3578
- }
3579
- ) }),
4011
+ /* @__PURE__ */ jsxs(View, { style: styles20.listWrap, children: [
4012
+ /* @__PURE__ */ jsx(
4013
+ MessageList,
4014
+ {
4015
+ messages,
4016
+ items: listItems,
4017
+ meId,
4018
+ loading,
4019
+ hasMore,
4020
+ onLoadMore: loadMore,
4021
+ renderItem,
4022
+ ListHeader: /* @__PURE__ */ jsx(TypingIndicator, { typing, ...labelFor ? { labelFor } : {} }),
4023
+ ...emptyState !== void 0 ? { emptyState } : {}
4024
+ }
4025
+ ),
4026
+ editingId ? /* @__PURE__ */ jsx(
4027
+ Pressable,
4028
+ {
4029
+ style: styles20.editBackdrop,
4030
+ onPress: () => setEditingId(null),
4031
+ accessibilityLabel: "Cancel editing"
4032
+ }
4033
+ ) : null
4034
+ ] }),
3580
4035
  mentions ? /* @__PURE__ */ jsx(MentionInput, { conversationId, ...composerProps }) : /* @__PURE__ */ jsx(MessageComposer, { ...composerProps }),
3581
4036
  attachments ? /* @__PURE__ */ jsx(AttachmentPicker, { visible: pickerOpen, onClose: () => setPickerOpen(false) }) : null,
3582
4037
  threads && threadRoot ? /* @__PURE__ */ jsx(
@@ -3612,6 +4067,14 @@ var styles20 = StyleSheet.create({
3612
4067
  },
3613
4068
  listWrap: {
3614
4069
  flex: 1
4070
+ },
4071
+ editBackdrop: {
4072
+ ...StyleSheet.absoluteFillObject,
4073
+ backgroundColor: "rgba(0,0,0,0.45)",
4074
+ // Sit above message rows by default but below any row that
4075
+ // explicitly elevates itself (MessageRow bumps zIndex/elevation
4076
+ // when isEditing is true).
4077
+ zIndex: 5
3615
4078
  }
3616
4079
  });
3617
4080
  function UserPickerSheet({
@@ -4470,6 +4933,6 @@ function useAppStateLifecycle() {
4470
4933
  }, [poolse]);
4471
4934
  }
4472
4935
 
4473
- export { AttachmentPicker, AttachmentPreview, Avatar, ChatHeader, ConversationList, ConversationView, EditableMessageBubble, GroupDetailsSheet, ImageMosaic, MemberList, MentionInput, MessageActions, MessageBubble, MessageComposer, MessageList, MessageRow, PoolseIcon, PoolseInbox, PoolseLogo, PoolseTheme, primitives as Primitives, ReactionPicker, ReactionStrip, ThreadView, TypingIndicator, UploadQueueStrip, UserName, defaultDarkTheme, defaultLightTheme, useAppStateLifecycle, useDisplayName, usePoolseTheme };
4936
+ export { AttachmentPicker, AttachmentPreview, Avatar, ChatHeader, ConversationList, ConversationView, DaySeparator, GroupDetailsSheet, ImageMosaic, MemberList, MentionInput, MessageActions, MessageBubble, MessageComposer, MessageList, MessageRow, PoolseIcon, PoolseInbox, PoolseLogo, PoolseTheme, primitives as Primitives, ReactionPicker, ReactionStrip, ThreadView, TypingIndicator, UploadQueueStrip, UserName, defaultDarkTheme, defaultLightTheme, useAppStateLifecycle, useDisplayName, usePoolseTheme };
4474
4937
  //# sourceMappingURL=index.js.map
4475
4938
  //# sourceMappingURL=index.js.map