@palbase/web 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1607,6 +1607,88 @@ var PalbeFlags = class {
1607
1607
  }
1608
1608
  };
1609
1609
 
1610
+ // src/messaging/edit-fold.ts
1611
+ function orderLt(aEpoch, aSeq, bEpoch, bSeq) {
1612
+ if (aEpoch !== bEpoch) return aEpoch < bEpoch;
1613
+ return aSeq < bSeq;
1614
+ }
1615
+ function orderEq(aEpoch, aSeq, bEpoch, bSeq) {
1616
+ return aEpoch === bEpoch && aSeq === bSeq;
1617
+ }
1618
+ var EditFold = class {
1619
+ // target → winning edit state
1620
+ states = /* @__PURE__ */ new Map();
1621
+ // dedup of real wire events that reached (and were resolvable enough to evaluate at) the fold
1622
+ seenEvents = /* @__PURE__ */ new Set();
1623
+ // events parked because target/author or sender was unresolved at ingest time
1624
+ held = [];
1625
+ // targets that have had ≥1 valid edit applied (write-once)
1626
+ editedTargets = /* @__PURE__ */ new Set();
1627
+ /**
1628
+ * Ingest one edit. `authorOfTarget` resolves the target message's AUTHOR userId
1629
+ * (null = target unknown/dangling → HOLD).
1630
+ */
1631
+ ingest(e, authorOfTarget) {
1632
+ const author = authorOfTarget(e.targetClientMsgId);
1633
+ if (author === null) {
1634
+ this.holdIfNew(e);
1635
+ return;
1636
+ }
1637
+ if (e.editorUserId === null) {
1638
+ this.holdIfNew(e);
1639
+ return;
1640
+ }
1641
+ if (e.editorUserId !== author) return;
1642
+ if (this.seenEvents.has(e.eventClientMsgId)) return;
1643
+ this.seenEvents.add(e.eventClientMsgId);
1644
+ const prev = this.states.get(e.targetClientMsgId);
1645
+ if (prev !== void 0) {
1646
+ if (orderLt(e.epoch, e.serverSeq, prev.orderEpoch, prev.orderSeq)) return;
1647
+ if (orderEq(e.epoch, e.serverSeq, prev.orderEpoch, prev.orderSeq) && e.eventClientMsgId <= prev.lastEventId) {
1648
+ return;
1649
+ }
1650
+ }
1651
+ this.states.set(e.targetClientMsgId, {
1652
+ orderEpoch: e.epoch,
1653
+ orderSeq: e.serverSeq,
1654
+ lastEventId: e.eventClientMsgId,
1655
+ text: e.newText
1656
+ });
1657
+ this.editedTargets.add(e.targetClientMsgId);
1658
+ }
1659
+ /**
1660
+ * Park an event for later re-attempt, deduping held re-deliveries by
1661
+ * eventClientMsgId so a repeatedly-delivered unresolvable edit is held exactly
1662
+ * once (and never double-applies when it finally resolves on reevaluate).
1663
+ */
1664
+ holdIfNew(e) {
1665
+ if (this.held.some((h) => h.eventClientMsgId === e.eventClientMsgId)) return;
1666
+ this.held.push(e);
1667
+ }
1668
+ /** The winning edit text for a target, or null if no valid edit has applied. */
1669
+ text(targetClientMsgId) {
1670
+ return this.states.get(targetClientMsgId)?.text ?? null;
1671
+ }
1672
+ /** Write-once: true once any valid edit applied to the target. */
1673
+ isEdited(targetClientMsgId) {
1674
+ return this.editedTargets.has(targetClientMsgId);
1675
+ }
1676
+ /**
1677
+ * Re-run HELD edits when the roster/target newly resolves (call on member/roster
1678
+ * change and when a target message arrives). Clears `held` and re-ingests each
1679
+ * event with the fresh `authorOfTarget` — events that still don't resolve are
1680
+ * simply re-held; events that now resolve fold via the normal LWW path.
1681
+ * Idempotent: re-ingest is deduped by `seenEvents` (applied events) and by
1682
+ * `holdIfNew` (still-held events), so reevaluating repeatedly can neither
1683
+ * double-apply nor lose an edit.
1684
+ */
1685
+ reevaluateHeld(authorOfTarget) {
1686
+ const pending = this.held;
1687
+ this.held = [];
1688
+ for (const e of pending) this.ingest(e, authorOfTarget);
1689
+ }
1690
+ };
1691
+
1610
1692
  // src/messaging/util.ts
1611
1693
  function toBase64(bytes) {
1612
1694
  if (typeof Buffer !== "undefined") {
@@ -1746,6 +1828,17 @@ async function listDevices(rt, userId) {
1746
1828
  }
1747
1829
 
1748
1830
  // src/messaging/group-messaging.ts
1831
+ function encodeEdit(args) {
1832
+ return encodeUtf8(
1833
+ JSON.stringify({
1834
+ v: 1,
1835
+ type: "edit",
1836
+ client_msg_id: args.clientMsgId,
1837
+ target_client_msg_id: args.targetClientMsgId,
1838
+ new_text: args.newText
1839
+ })
1840
+ );
1841
+ }
1749
1842
  function encodeReaction(args) {
1750
1843
  return encodeUtf8(
1751
1844
  JSON.stringify({
@@ -1785,6 +1878,18 @@ function decodeEnvelope(bytes) {
1785
1878
  }
1786
1879
  };
1787
1880
  }
1881
+ if (typeof o === "object" && o !== null && o.type === "edit") {
1882
+ return {
1883
+ type: "edit",
1884
+ text: null,
1885
+ clientMsgId: o.client_msg_id ?? "",
1886
+ replyTo: null,
1887
+ edit: {
1888
+ targetClientMsgId: o.target_client_msg_id ?? "",
1889
+ newText: o.new_text ?? ""
1890
+ }
1891
+ };
1892
+ }
1788
1893
  if (typeof o === "object" && o !== null && o.type === "text" && typeof o.v === "number") {
1789
1894
  return {
1790
1895
  type: "text",
@@ -2099,6 +2204,54 @@ var GroupMessaging = class {
2099
2204
  clientMsgId: args.clientMsgId
2100
2205
  };
2101
2206
  }
2207
+ /** Send an edit (edit-by-supersession on a target message). Encrypts a
2208
+ * `type:'edit'` envelope at the current epoch and sends through the SAME MLS
2209
+ * application path as `sendText` (the server stays blind — an edit is just
2210
+ * another application message). Persists the outgoing edit row so it re-folds
2211
+ * onto its target's text after a reload (the own-send half of the reload
2212
+ * parity). NEVER rebases (epoch-bound like any application message). */
2213
+ async sendEdit(group, args) {
2214
+ const plaintext = encodeEdit({
2215
+ clientMsgId: args.clientMsgId,
2216
+ targetClientMsgId: args.targetClientMsgId,
2217
+ newText: args.newText
2218
+ });
2219
+ const ct = await this.engine.encryptApplication(fromBase64(group.rfcGroupId), plaintext);
2220
+ const body = {
2221
+ ciphertext_b64: toBase64(ct),
2222
+ client_idem_key: randomId()
2223
+ };
2224
+ const wire = await palbeRequest(
2225
+ this.rt,
2226
+ "POST",
2227
+ MessagingPaths.groupMessages(group.displayId),
2228
+ { body }
2229
+ );
2230
+ const stored = {
2231
+ id: `${group.rfcGroupId}#${wire.server_seq}`,
2232
+ direction: "outgoing",
2233
+ text: null,
2234
+ senderDeviceId: this.selfDeviceId,
2235
+ epoch: wire.epoch,
2236
+ serverSeq: wire.server_seq,
2237
+ at: Date.now(),
2238
+ clientMsgId: args.clientMsgId,
2239
+ replyTo: null,
2240
+ envelopeType: "edit",
2241
+ edit: {
2242
+ targetClientMsgId: args.targetClientMsgId,
2243
+ newText: args.newText
2244
+ }
2245
+ };
2246
+ try {
2247
+ await this.messageStore.append(group.rfcGroupId, stored);
2248
+ } catch {
2249
+ }
2250
+ return {
2251
+ receipt: { serverSeq: wire.server_seq, epoch: wire.epoch },
2252
+ clientMsgId: args.clientMsgId
2253
+ };
2254
+ }
2102
2255
  // ── The rebase loop ──
2103
2256
  async commitWithRebase(rfcGroupId, build) {
2104
2257
  const gidBytes = fromBase64(rfcGroupId);
@@ -2235,6 +2388,14 @@ var Chat = class {
2235
2388
  byClientMsgId = /* @__PURE__ */ new Map();
2236
2389
  /** The single authoritative reaction fold for this chat (live + own-send + history). */
2237
2390
  reactionFold = new ReactionFold();
2391
+ /** The single authoritative edit fold for this chat (live + own-send + history). */
2392
+ editFold = new EditFold();
2393
+ /** Per-target BASE (original) text, so the rendered text is `edit ?? original`.
2394
+ * Seeded once per target (write-once base) so a 2nd own edit can't corrupt it. */
2395
+ originalTextByClientMsgId = /* @__PURE__ */ new Map();
2396
+ /** Per-target AUTHOR userId — the EditFold author-gate input (filled at bubble
2397
+ * projection time from senderUserId; '' = resolved-but-unknown peer). */
2398
+ authorByClientMsgId = /* @__PURE__ */ new Map();
2238
2399
  loadedEarliestSeq = null;
2239
2400
  historyLoaded = false;
2240
2401
  wired = false;
@@ -2335,13 +2496,17 @@ var Chat = class {
2335
2496
  senderUserId: m.senderUserId ?? ""
2336
2497
  });
2337
2498
  }
2499
+ if (m.clientMsgId) {
2500
+ this.seedEditBase(m.clientMsgId, m.text, m.senderUserId ?? "");
2501
+ }
2338
2502
  }
2503
+ this.editFold.reevaluateHeld(this.authorOfTarget);
2339
2504
  for (const m of incoming) {
2340
2505
  if (m.serverSeq <= 0) continue;
2341
2506
  const key = this.internalKey(m.serverSeq);
2342
2507
  if (this.seenKeys.has(key)) continue;
2343
2508
  this.seenKeys.add(key);
2344
- this.messageList.push(this.applyReactionTally(m));
2509
+ this.messageList.push(this.applyEditOverlay(this.applyReactionTally(m)));
2345
2510
  changed = true;
2346
2511
  this.loadedEarliestSeq = Math.min(this.loadedEarliestSeq ?? m.serverSeq, m.serverSeq);
2347
2512
  }
@@ -2382,6 +2547,22 @@ var Chat = class {
2382
2547
  }
2383
2548
  return;
2384
2549
  }
2550
+ if (incoming.envelopeType === "edit" && incoming.edit) {
2551
+ const editorUserId = direction === "outgoing" ? this.backend.selfUserId : senderUser;
2552
+ this.editFold.ingest(
2553
+ {
2554
+ targetClientMsgId: incoming.edit.targetClientMsgId,
2555
+ editorUserId,
2556
+ newText: incoming.edit.newText,
2557
+ epoch: incoming.epoch,
2558
+ serverSeq: incoming.serverSeq,
2559
+ eventClientMsgId: incoming.clientMsgId
2560
+ },
2561
+ this.authorOfTarget
2562
+ );
2563
+ this.recomputeEdit(incoming.edit.targetClientMsgId);
2564
+ return;
2565
+ }
2385
2566
  const incomingClientMsgId = incoming.clientMsgId;
2386
2567
  const incomingReplyRef = incoming.replyRef;
2387
2568
  let resolvedReplyTo = null;
@@ -2400,7 +2581,9 @@ var Chat = class {
2400
2581
  replyTo: resolvedReplyTo,
2401
2582
  // Attach any tally already folded for this message (a reaction that arrived
2402
2583
  // BEFORE its target — the dangling case — renders the moment the target lands).
2403
- reactions: incomingClientMsgId ? this.reactionFold.tally(incomingClientMsgId) : {}
2584
+ reactions: incomingClientMsgId ? this.reactionFold.tally(incomingClientMsgId) : {},
2585
+ // Default false; applyEditOverlay below folds any edit that arrived first.
2586
+ edited: false
2404
2587
  };
2405
2588
  if (incomingClientMsgId && incoming.text !== null) {
2406
2589
  this.byClientMsgId.set(incomingClientMsgId, {
@@ -2408,7 +2591,11 @@ var Chat = class {
2408
2591
  senderUserId: senderUser ?? ""
2409
2592
  });
2410
2593
  }
2411
- this.messageList.push(msg);
2594
+ if (incomingClientMsgId) {
2595
+ this.seedEditBase(incomingClientMsgId, incoming.text, senderUser);
2596
+ this.editFold.reevaluateHeld(this.authorOfTarget);
2597
+ }
2598
+ this.messageList.push(this.applyEditOverlay(msg));
2412
2599
  this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
2413
2600
  this.loadedEarliestSeq = Math.min(
2414
2601
  this.loadedEarliestSeq ?? incoming.serverSeq,
@@ -2416,6 +2603,19 @@ var Chat = class {
2416
2603
  );
2417
2604
  this.emit();
2418
2605
  }
2606
+ /** The EditFold author-gate input: the target message's resolved author userId
2607
+ * (null = target unknown/dangling → the fold HOLDs). Captured as a bound arrow
2608
+ * so it can be passed to the pure EditFold. */
2609
+ authorOfTarget = (targetClientMsgId) => this.authorByClientMsgId.get(targetClientMsgId) ?? null;
2610
+ /** Seed the per-target base text + author for the edit fold. Base is write-once
2611
+ * (a later own/peer edit must not overwrite the original we render against). The
2612
+ * author is (re)recorded whenever a non-empty resolution is available. */
2613
+ seedEditBase(clientMsgId, text, author) {
2614
+ if (!this.originalTextByClientMsgId.has(clientMsgId)) {
2615
+ this.originalTextByClientMsgId.set(clientMsgId, text);
2616
+ }
2617
+ if (author !== null) this.authorByClientMsgId.set(clientMsgId, author);
2618
+ }
2419
2619
  /**
2420
2620
  * Rebuild the target message's `reactions` from the authoritative fold and
2421
2621
  * re-emit. No-op when the target isn't present yet (its tally is attached the
@@ -2445,6 +2645,46 @@ var Chat = class {
2445
2645
  if (sameReactions(m.reactions, tally)) return m;
2446
2646
  return { ...m, reactions: tally };
2447
2647
  }
2648
+ /**
2649
+ * Rebuild the target message's rendered `text` + `edited` flag from the
2650
+ * authoritative edit fold and re-emit, PRESERVING `.reactions` and `.replyTo`
2651
+ * (the reaction-polish lesson — never clobber). text = `editFold.text(cid) ??
2652
+ * base`; base is the seeded original so a forged/ignored edit leaves it intact.
2653
+ * No-op when the target isn't present yet (the fold already recorded it; the
2654
+ * overlay applies the moment the target lands) or when unchanged.
2655
+ */
2656
+ recomputeEdit(targetClientMsgId) {
2657
+ if (!targetClientMsgId) return;
2658
+ const editText = this.editFold.text(targetClientMsgId);
2659
+ const foldEdited = this.editFold.isEdited(targetClientMsgId);
2660
+ let changed = false;
2661
+ this.messageList = this.messageList.map((m) => {
2662
+ if (m.clientMsgId !== targetClientMsgId) return m;
2663
+ const base = this.originalTextByClientMsgId.has(targetClientMsgId) ? this.originalTextByClientMsgId.get(targetClientMsgId) ?? null : m.text;
2664
+ const text = editText ?? base;
2665
+ const edited = foldEdited || m.edited;
2666
+ if (m.text === text && m.edited === edited) return m;
2667
+ changed = true;
2668
+ return { ...m, text, edited };
2669
+ });
2670
+ if (changed) this.emit();
2671
+ }
2672
+ /**
2673
+ * Overlay the authoritative edit fold's winning text + flag onto a message as it
2674
+ * is appended/merged. The fold WINS when it has an edit for this target;
2675
+ * otherwise the upstream `text`/`edited` (e.g. the coordinator's page-local
2676
+ * history fold) is preserved. PRESERVES reactions + replyTo.
2677
+ */
2678
+ applyEditOverlay(m) {
2679
+ if (!m.clientMsgId) return m;
2680
+ const editText = this.editFold.text(m.clientMsgId);
2681
+ const foldEdited = this.editFold.isEdited(m.clientMsgId);
2682
+ if (editText === null && !foldEdited) return m;
2683
+ const text = editText ?? m.text;
2684
+ const edited = foldEdited || m.edited;
2685
+ if (m.text === text && m.edited === edited) return m;
2686
+ return { ...m, text, edited };
2687
+ }
2448
2688
  /** @internal — called by the backend's conv subscription. */
2449
2689
  applyConv(event, payload) {
2450
2690
  const userId = typeof payload.user_id === "string" ? payload.user_id : null;
@@ -2499,6 +2739,8 @@ var Chat = class {
2499
2739
  this.memberCache = m;
2500
2740
  this.emit();
2501
2741
  }
2742
+ this.editFold.reevaluateHeld(this.authorOfTarget);
2743
+ for (const cid of this.originalTextByClientMsgId.keys()) this.recomputeEdit(cid);
2502
2744
  }
2503
2745
  seedMembersFromGroup(group) {
2504
2746
  const seed = [
@@ -2577,6 +2819,7 @@ var Chat = class {
2577
2819
  this.seenKeys.add(key);
2578
2820
  if (clientMsgId) {
2579
2821
  this.byClientMsgId.set(clientMsgId, { text, senderUserId: this.backend.selfUserId });
2822
+ this.seedEditBase(clientMsgId, text, this.backend.selfUserId);
2580
2823
  }
2581
2824
  this.messageList.push({
2582
2825
  id: this.publicId(receipt.serverSeq),
@@ -2590,7 +2833,9 @@ var Chat = class {
2590
2833
  replyTo: resolvedReplyTo,
2591
2834
  // Attach any tally already folded for this own-sent message (rare, but keeps
2592
2835
  // the dangling-target invariant uniform across every append path).
2593
- reactions: clientMsgId ? this.reactionFold.tally(clientMsgId) : {}
2836
+ reactions: clientMsgId ? this.reactionFold.tally(clientMsgId) : {},
2837
+ // Own-sent edits fold via edit() after the fact; new sends start unedited.
2838
+ edited: false
2594
2839
  });
2595
2840
  this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
2596
2841
  this.emit();
@@ -2671,6 +2916,37 @@ var Chat = class {
2671
2916
  });
2672
2917
  this.recomputeReactions(message.clientMsgId);
2673
2918
  }
2919
+ // ── Edit ──
2920
+ /** Edit an own text message (edit-by-supersession). No-op if the message isn't
2921
+ * editable (empty clientMsgId, or not a `text` kind). The edit folds locally
2922
+ * with the server receipt's `(epoch, serverSeq)` so the target's text updates
2923
+ * instantly; the durable echo on the next pump is a fold no-op (dedup on the
2924
+ * SAME wire clientMsgId). Never appends a bubble; PRESERVES the target's
2925
+ * reactions + reply context. Only the original author's edits count — for an own
2926
+ * message self IS the author, so the author-gate passes. */
2927
+ async edit(message, newText) {
2928
+ if (!message.clientMsgId || message.kind !== "text") return;
2929
+ const group = await this.materializeIfNeeded();
2930
+ const clientMsgId = mintClientMsgId();
2931
+ const { receipt } = await this.backend.sendEdit(group, {
2932
+ clientMsgId,
2933
+ targetClientMsgId: message.clientMsgId,
2934
+ newText
2935
+ });
2936
+ this.seedEditBase(message.clientMsgId, message.text, this.backend.selfUserId);
2937
+ this.editFold.ingest(
2938
+ {
2939
+ targetClientMsgId: message.clientMsgId,
2940
+ editorUserId: this.backend.selfUserId,
2941
+ newText,
2942
+ epoch: receipt.epoch,
2943
+ serverSeq: receipt.serverSeq,
2944
+ eventClientMsgId: clientMsgId
2945
+ },
2946
+ this.authorOfTarget
2947
+ );
2948
+ this.recomputeEdit(message.clientMsgId);
2949
+ }
2674
2950
  };
2675
2951
  function sameReactions(a, b) {
2676
2952
  const ak = Object.keys(a);
@@ -2850,6 +3126,7 @@ var MessageDeliverySource = class {
2850
3126
  const decoded = decodeEnvelope(received.data);
2851
3127
  const { text, clientMsgId, replyTo } = decoded;
2852
3128
  const isReaction = decoded.type === "reaction" && decoded.reaction != null;
3129
+ const isEdit = decoded.type === "edit" && decoded.edit != null;
2853
3130
  const senderDeviceId = row.sender_device_id ?? decodeSenderDeviceId(received.sender);
2854
3131
  const stored = {
2855
3132
  id: `${group.rfcGroupId}#${row.server_seq}`,
@@ -2877,6 +3154,17 @@ var MessageDeliverySource = class {
2877
3154
  emoji: decoded.reaction.emoji,
2878
3155
  op: decoded.reaction.op
2879
3156
  }
3157
+ } : {},
3158
+ // Thread the edit discriminator + new text through the persisted row so an
3159
+ // edit folded LIVE re-folds onto its target after a reload (the reload-parity
3160
+ // boundary — mirrors iOS T3). Omitted for non-edits → old rows hydrate as
3161
+ // `'text'`/no-edit (backward-compat).
3162
+ ...isEdit && decoded.edit ? {
3163
+ envelopeType: "edit",
3164
+ edit: {
3165
+ targetClientMsgId: decoded.edit.targetClientMsgId,
3166
+ newText: decoded.edit.newText
3167
+ }
2880
3168
  } : {}
2881
3169
  };
2882
3170
  try {
@@ -2895,7 +3183,8 @@ var MessageDeliverySource = class {
2895
3183
  clientMsgId,
2896
3184
  replyRef: replyTo,
2897
3185
  envelopeType: decoded.type ?? "text",
2898
- reaction: isReaction ? decoded.reaction : null
3186
+ reaction: isReaction ? decoded.reaction : null,
3187
+ edit: isEdit ? decoded.edit : null
2899
3188
  });
2900
3189
  return true;
2901
3190
  }
@@ -4903,6 +5192,10 @@ var MessagingCoordinator = class {
4903
5192
  const r = await this.resolve();
4904
5193
  return r.groups.sendReaction(group, args);
4905
5194
  }
5195
+ async sendEdit(group, args) {
5196
+ const r = await this.resolve();
5197
+ return r.groups.sendEdit(group, args);
5198
+ }
4906
5199
  async history(group, limit, before) {
4907
5200
  const r = await this.resolve();
4908
5201
  const rows = await r.messageStore.history(group.rfcGroupId, limit, before);
@@ -4999,6 +5292,32 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
4999
5292
  eventClientMsgId: s.clientMsgId ?? `${s.id}`
5000
5293
  });
5001
5294
  }
5295
+ const editFold = new EditFold();
5296
+ const authorByClientMsgId = /* @__PURE__ */ new Map();
5297
+ for (const s of rows) {
5298
+ if (s.envelopeType === "reaction" || s.envelopeType === "edit") continue;
5299
+ const cid = s.clientMsgId ?? "";
5300
+ if (!cid) continue;
5301
+ const author = s.direction === "outgoing" ? selfUserId : resolveActor?.(s.senderDeviceId ?? null);
5302
+ if (author != null) authorByClientMsgId.set(cid, author);
5303
+ }
5304
+ const authorOfTarget = (cid) => authorByClientMsgId.get(cid) ?? null;
5305
+ for (const s of rows) {
5306
+ if (s.envelopeType !== "edit" || !s.edit) continue;
5307
+ const editor = s.direction === "outgoing" ? selfUserId : resolveActor?.(s.senderDeviceId ?? null) ?? null;
5308
+ editFold.ingest(
5309
+ {
5310
+ targetClientMsgId: s.edit.targetClientMsgId,
5311
+ editorUserId: editor,
5312
+ newText: s.edit.newText,
5313
+ epoch: s.epoch,
5314
+ serverSeq: s.serverSeq,
5315
+ eventClientMsgId: s.clientMsgId ?? `${s.id}`
5316
+ },
5317
+ authorOfTarget
5318
+ );
5319
+ }
5320
+ editFold.reevaluateHeld(authorOfTarget);
5002
5321
  const lookup = /* @__PURE__ */ new Map();
5003
5322
  for (const s of rows) {
5004
5323
  if (s.envelopeType === "reaction") continue;
@@ -5010,7 +5329,7 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
5010
5329
  }
5011
5330
  const out = [];
5012
5331
  for (const s of rows) {
5013
- if (s.envelopeType === "reaction") continue;
5332
+ if (s.envelopeType === "reaction" || s.envelopeType === "edit") continue;
5014
5333
  const clientMsgId = s.clientMsgId ?? "";
5015
5334
  let replyTo = null;
5016
5335
  if (s.replyTo) {
@@ -5026,17 +5345,20 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
5026
5345
  };
5027
5346
  replyTo = resolveReply(ref, (id) => lookup.get(id) ?? null);
5028
5347
  }
5348
+ const editText = clientMsgId ? editFold.text(clientMsgId) : null;
5349
+ const edited = clientMsgId ? editFold.isEdited(clientMsgId) : false;
5029
5350
  out.push({
5030
5351
  id: `${displayId}#${s.serverSeq}`,
5031
5352
  kind: s.text != null ? "text" : "system",
5032
5353
  direction: s.direction,
5033
5354
  senderUserId: s.direction === "outgoing" ? selfUserId : null,
5034
- text: s.text,
5355
+ text: editText ?? s.text,
5035
5356
  serverSeq: s.serverSeq,
5036
5357
  sentAt: new Date(s.at),
5037
5358
  clientMsgId,
5038
5359
  replyTo,
5039
- reactions: clientMsgId ? fold.tally(clientMsgId) : {}
5360
+ reactions: clientMsgId ? fold.tally(clientMsgId) : {},
5361
+ edited
5040
5362
  });
5041
5363
  }
5042
5364
  return out;
@@ -5893,7 +6215,7 @@ function localStorageSessionStorage(key = DEFAULT_KEY) {
5893
6215
  }
5894
6216
 
5895
6217
  // src/version.ts
5896
- var VERSION = "1.2.1";
6218
+ var VERSION = "1.3.0";
5897
6219
 
5898
6220
  // src/internal.ts
5899
6221
  function getRuntime() {