@palbase/web 1.2.0 → 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);
@@ -2713,6 +2989,11 @@ var MessageHub = class {
2713
2989
  };
2714
2990
  }
2715
2991
  };
2992
+ function decodeSenderDeviceId(sender) {
2993
+ if (sender.length === 0) return null;
2994
+ const id = decodeUtf8(sender);
2995
+ return id.length > 0 ? id : null;
2996
+ }
2716
2997
  var MessageDeliverySource = class {
2717
2998
  constructor(rt, engine, hub, registry, messageStore, deviceId, selfUserId) {
2718
2999
  this.rt = rt;
@@ -2845,11 +3126,13 @@ var MessageDeliverySource = class {
2845
3126
  const decoded = decodeEnvelope(received.data);
2846
3127
  const { text, clientMsgId, replyTo } = decoded;
2847
3128
  const isReaction = decoded.type === "reaction" && decoded.reaction != null;
3129
+ const isEdit = decoded.type === "edit" && decoded.edit != null;
3130
+ const senderDeviceId = row.sender_device_id ?? decodeSenderDeviceId(received.sender);
2848
3131
  const stored = {
2849
3132
  id: `${group.rfcGroupId}#${row.server_seq}`,
2850
3133
  direction: "incoming",
2851
3134
  text,
2852
- senderDeviceId: row.sender_device_id ?? null,
3135
+ senderDeviceId,
2853
3136
  epoch: row.epoch,
2854
3137
  serverSeq: row.server_seq,
2855
3138
  at: Date.now(),
@@ -2871,6 +3154,17 @@ var MessageDeliverySource = class {
2871
3154
  emoji: decoded.reaction.emoji,
2872
3155
  op: decoded.reaction.op
2873
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
+ }
2874
3168
  } : {}
2875
3169
  };
2876
3170
  try {
@@ -2882,14 +3176,15 @@ var MessageDeliverySource = class {
2882
3176
  kind: "application",
2883
3177
  group,
2884
3178
  text,
2885
- senderDeviceId: row.sender_device_id ?? null,
3179
+ senderDeviceId,
2886
3180
  epoch: row.epoch,
2887
3181
  serverSeq: row.server_seq,
2888
3182
  receivedAt: /* @__PURE__ */ new Date(),
2889
3183
  clientMsgId,
2890
3184
  replyRef: replyTo,
2891
3185
  envelopeType: decoded.type ?? "text",
2892
- reaction: isReaction ? decoded.reaction : null
3186
+ reaction: isReaction ? decoded.reaction : null,
3187
+ edit: isEdit ? decoded.edit : null
2893
3188
  });
2894
3189
  return true;
2895
3190
  }
@@ -4897,6 +5192,10 @@ var MessagingCoordinator = class {
4897
5192
  const r = await this.resolve();
4898
5193
  return r.groups.sendReaction(group, args);
4899
5194
  }
5195
+ async sendEdit(group, args) {
5196
+ const r = await this.resolve();
5197
+ return r.groups.sendEdit(group, args);
5198
+ }
4900
5199
  async history(group, limit, before) {
4901
5200
  const r = await this.resolve();
4902
5201
  const rows = await r.messageStore.history(group.rfcGroupId, limit, before);
@@ -4993,6 +5292,32 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
4993
5292
  eventClientMsgId: s.clientMsgId ?? `${s.id}`
4994
5293
  });
4995
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);
4996
5321
  const lookup = /* @__PURE__ */ new Map();
4997
5322
  for (const s of rows) {
4998
5323
  if (s.envelopeType === "reaction") continue;
@@ -5004,7 +5329,7 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
5004
5329
  }
5005
5330
  const out = [];
5006
5331
  for (const s of rows) {
5007
- if (s.envelopeType === "reaction") continue;
5332
+ if (s.envelopeType === "reaction" || s.envelopeType === "edit") continue;
5008
5333
  const clientMsgId = s.clientMsgId ?? "";
5009
5334
  let replyTo = null;
5010
5335
  if (s.replyTo) {
@@ -5020,17 +5345,20 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
5020
5345
  };
5021
5346
  replyTo = resolveReply(ref, (id) => lookup.get(id) ?? null);
5022
5347
  }
5348
+ const editText = clientMsgId ? editFold.text(clientMsgId) : null;
5349
+ const edited = clientMsgId ? editFold.isEdited(clientMsgId) : false;
5023
5350
  out.push({
5024
5351
  id: `${displayId}#${s.serverSeq}`,
5025
5352
  kind: s.text != null ? "text" : "system",
5026
5353
  direction: s.direction,
5027
5354
  senderUserId: s.direction === "outgoing" ? selfUserId : null,
5028
- text: s.text,
5355
+ text: editText ?? s.text,
5029
5356
  serverSeq: s.serverSeq,
5030
5357
  sentAt: new Date(s.at),
5031
5358
  clientMsgId,
5032
5359
  replyTo,
5033
- reactions: clientMsgId ? fold.tally(clientMsgId) : {}
5360
+ reactions: clientMsgId ? fold.tally(clientMsgId) : {},
5361
+ edited
5034
5362
  });
5035
5363
  }
5036
5364
  return out;
@@ -5887,7 +6215,7 @@ function localStorageSessionStorage(key = DEFAULT_KEY) {
5887
6215
  }
5888
6216
 
5889
6217
  // src/version.ts
5890
- var VERSION = "1.2.0";
6218
+ var VERSION = "1.3.0";
5891
6219
 
5892
6220
  // src/internal.ts
5893
6221
  function getRuntime() {