@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.
@@ -7,7 +7,7 @@ import {
7
7
  __configure,
8
8
  endpointRefFromApiKey,
9
9
  getRuntime
10
- } from "../chunk-OZLVL7G2.js";
10
+ } from "../chunk-UX43AB4W.js";
11
11
 
12
12
  // src/next/client.ts
13
13
  var SESSION_MAX_AGE_S = 2592e3;
@@ -2628,6 +2628,88 @@ var PalbeFlags = class {
2628
2628
  }
2629
2629
  };
2630
2630
 
2631
+ // src/messaging/edit-fold.ts
2632
+ function orderLt(aEpoch, aSeq, bEpoch, bSeq) {
2633
+ if (aEpoch !== bEpoch) return aEpoch < bEpoch;
2634
+ return aSeq < bSeq;
2635
+ }
2636
+ function orderEq(aEpoch, aSeq, bEpoch, bSeq) {
2637
+ return aEpoch === bEpoch && aSeq === bSeq;
2638
+ }
2639
+ var EditFold = class {
2640
+ // target → winning edit state
2641
+ states = /* @__PURE__ */ new Map();
2642
+ // dedup of real wire events that reached (and were resolvable enough to evaluate at) the fold
2643
+ seenEvents = /* @__PURE__ */ new Set();
2644
+ // events parked because target/author or sender was unresolved at ingest time
2645
+ held = [];
2646
+ // targets that have had ≥1 valid edit applied (write-once)
2647
+ editedTargets = /* @__PURE__ */ new Set();
2648
+ /**
2649
+ * Ingest one edit. `authorOfTarget` resolves the target message's AUTHOR userId
2650
+ * (null = target unknown/dangling → HOLD).
2651
+ */
2652
+ ingest(e, authorOfTarget) {
2653
+ const author = authorOfTarget(e.targetClientMsgId);
2654
+ if (author === null) {
2655
+ this.holdIfNew(e);
2656
+ return;
2657
+ }
2658
+ if (e.editorUserId === null) {
2659
+ this.holdIfNew(e);
2660
+ return;
2661
+ }
2662
+ if (e.editorUserId !== author) return;
2663
+ if (this.seenEvents.has(e.eventClientMsgId)) return;
2664
+ this.seenEvents.add(e.eventClientMsgId);
2665
+ const prev = this.states.get(e.targetClientMsgId);
2666
+ if (prev !== void 0) {
2667
+ if (orderLt(e.epoch, e.serverSeq, prev.orderEpoch, prev.orderSeq)) return;
2668
+ if (orderEq(e.epoch, e.serverSeq, prev.orderEpoch, prev.orderSeq) && e.eventClientMsgId <= prev.lastEventId) {
2669
+ return;
2670
+ }
2671
+ }
2672
+ this.states.set(e.targetClientMsgId, {
2673
+ orderEpoch: e.epoch,
2674
+ orderSeq: e.serverSeq,
2675
+ lastEventId: e.eventClientMsgId,
2676
+ text: e.newText
2677
+ });
2678
+ this.editedTargets.add(e.targetClientMsgId);
2679
+ }
2680
+ /**
2681
+ * Park an event for later re-attempt, deduping held re-deliveries by
2682
+ * eventClientMsgId so a repeatedly-delivered unresolvable edit is held exactly
2683
+ * once (and never double-applies when it finally resolves on reevaluate).
2684
+ */
2685
+ holdIfNew(e) {
2686
+ if (this.held.some((h) => h.eventClientMsgId === e.eventClientMsgId)) return;
2687
+ this.held.push(e);
2688
+ }
2689
+ /** The winning edit text for a target, or null if no valid edit has applied. */
2690
+ text(targetClientMsgId) {
2691
+ return this.states.get(targetClientMsgId)?.text ?? null;
2692
+ }
2693
+ /** Write-once: true once any valid edit applied to the target. */
2694
+ isEdited(targetClientMsgId) {
2695
+ return this.editedTargets.has(targetClientMsgId);
2696
+ }
2697
+ /**
2698
+ * Re-run HELD edits when the roster/target newly resolves (call on member/roster
2699
+ * change and when a target message arrives). Clears `held` and re-ingests each
2700
+ * event with the fresh `authorOfTarget` — events that still don't resolve are
2701
+ * simply re-held; events that now resolve fold via the normal LWW path.
2702
+ * Idempotent: re-ingest is deduped by `seenEvents` (applied events) and by
2703
+ * `holdIfNew` (still-held events), so reevaluating repeatedly can neither
2704
+ * double-apply nor lose an edit.
2705
+ */
2706
+ reevaluateHeld(authorOfTarget) {
2707
+ const pending = this.held;
2708
+ this.held = [];
2709
+ for (const e of pending) this.ingest(e, authorOfTarget);
2710
+ }
2711
+ };
2712
+
2631
2713
  // src/messaging/util.ts
2632
2714
  function toBase64(bytes) {
2633
2715
  if (typeof Buffer !== "undefined") {
@@ -2767,6 +2849,17 @@ async function listDevices(rt, userId) {
2767
2849
  }
2768
2850
 
2769
2851
  // src/messaging/group-messaging.ts
2852
+ function encodeEdit(args) {
2853
+ return encodeUtf8(
2854
+ JSON.stringify({
2855
+ v: 1,
2856
+ type: "edit",
2857
+ client_msg_id: args.clientMsgId,
2858
+ target_client_msg_id: args.targetClientMsgId,
2859
+ new_text: args.newText
2860
+ })
2861
+ );
2862
+ }
2770
2863
  function encodeReaction(args) {
2771
2864
  return encodeUtf8(
2772
2865
  JSON.stringify({
@@ -2806,6 +2899,18 @@ function decodeEnvelope(bytes) {
2806
2899
  }
2807
2900
  };
2808
2901
  }
2902
+ if (typeof o === "object" && o !== null && o.type === "edit") {
2903
+ return {
2904
+ type: "edit",
2905
+ text: null,
2906
+ clientMsgId: o.client_msg_id ?? "",
2907
+ replyTo: null,
2908
+ edit: {
2909
+ targetClientMsgId: o.target_client_msg_id ?? "",
2910
+ newText: o.new_text ?? ""
2911
+ }
2912
+ };
2913
+ }
2809
2914
  if (typeof o === "object" && o !== null && o.type === "text" && typeof o.v === "number") {
2810
2915
  return {
2811
2916
  type: "text",
@@ -3120,6 +3225,54 @@ var GroupMessaging = class {
3120
3225
  clientMsgId: args.clientMsgId
3121
3226
  };
3122
3227
  }
3228
+ /** Send an edit (edit-by-supersession on a target message). Encrypts a
3229
+ * `type:'edit'` envelope at the current epoch and sends through the SAME MLS
3230
+ * application path as `sendText` (the server stays blind — an edit is just
3231
+ * another application message). Persists the outgoing edit row so it re-folds
3232
+ * onto its target's text after a reload (the own-send half of the reload
3233
+ * parity). NEVER rebases (epoch-bound like any application message). */
3234
+ async sendEdit(group, args) {
3235
+ const plaintext = encodeEdit({
3236
+ clientMsgId: args.clientMsgId,
3237
+ targetClientMsgId: args.targetClientMsgId,
3238
+ newText: args.newText
3239
+ });
3240
+ const ct = await this.engine.encryptApplication(fromBase64(group.rfcGroupId), plaintext);
3241
+ const body = {
3242
+ ciphertext_b64: toBase64(ct),
3243
+ client_idem_key: randomId()
3244
+ };
3245
+ const wire = await palbeRequest(
3246
+ this.rt,
3247
+ "POST",
3248
+ MessagingPaths.groupMessages(group.displayId),
3249
+ { body }
3250
+ );
3251
+ const stored = {
3252
+ id: `${group.rfcGroupId}#${wire.server_seq}`,
3253
+ direction: "outgoing",
3254
+ text: null,
3255
+ senderDeviceId: this.selfDeviceId,
3256
+ epoch: wire.epoch,
3257
+ serverSeq: wire.server_seq,
3258
+ at: Date.now(),
3259
+ clientMsgId: args.clientMsgId,
3260
+ replyTo: null,
3261
+ envelopeType: "edit",
3262
+ edit: {
3263
+ targetClientMsgId: args.targetClientMsgId,
3264
+ newText: args.newText
3265
+ }
3266
+ };
3267
+ try {
3268
+ await this.messageStore.append(group.rfcGroupId, stored);
3269
+ } catch {
3270
+ }
3271
+ return {
3272
+ receipt: { serverSeq: wire.server_seq, epoch: wire.epoch },
3273
+ clientMsgId: args.clientMsgId
3274
+ };
3275
+ }
3123
3276
  // ── The rebase loop ──
3124
3277
  async commitWithRebase(rfcGroupId, build) {
3125
3278
  const gidBytes = fromBase64(rfcGroupId);
@@ -3256,6 +3409,14 @@ var Chat = class {
3256
3409
  byClientMsgId = /* @__PURE__ */ new Map();
3257
3410
  /** The single authoritative reaction fold for this chat (live + own-send + history). */
3258
3411
  reactionFold = new ReactionFold();
3412
+ /** The single authoritative edit fold for this chat (live + own-send + history). */
3413
+ editFold = new EditFold();
3414
+ /** Per-target BASE (original) text, so the rendered text is `edit ?? original`.
3415
+ * Seeded once per target (write-once base) so a 2nd own edit can't corrupt it. */
3416
+ originalTextByClientMsgId = /* @__PURE__ */ new Map();
3417
+ /** Per-target AUTHOR userId — the EditFold author-gate input (filled at bubble
3418
+ * projection time from senderUserId; '' = resolved-but-unknown peer). */
3419
+ authorByClientMsgId = /* @__PURE__ */ new Map();
3259
3420
  loadedEarliestSeq = null;
3260
3421
  historyLoaded = false;
3261
3422
  wired = false;
@@ -3356,13 +3517,17 @@ var Chat = class {
3356
3517
  senderUserId: m.senderUserId ?? ""
3357
3518
  });
3358
3519
  }
3520
+ if (m.clientMsgId) {
3521
+ this.seedEditBase(m.clientMsgId, m.text, m.senderUserId ?? "");
3522
+ }
3359
3523
  }
3524
+ this.editFold.reevaluateHeld(this.authorOfTarget);
3360
3525
  for (const m of incoming) {
3361
3526
  if (m.serverSeq <= 0) continue;
3362
3527
  const key = this.internalKey(m.serverSeq);
3363
3528
  if (this.seenKeys.has(key)) continue;
3364
3529
  this.seenKeys.add(key);
3365
- this.messageList.push(this.applyReactionTally(m));
3530
+ this.messageList.push(this.applyEditOverlay(this.applyReactionTally(m)));
3366
3531
  changed = true;
3367
3532
  this.loadedEarliestSeq = Math.min(this.loadedEarliestSeq ?? m.serverSeq, m.serverSeq);
3368
3533
  }
@@ -3403,6 +3568,22 @@ var Chat = class {
3403
3568
  }
3404
3569
  return;
3405
3570
  }
3571
+ if (incoming.envelopeType === "edit" && incoming.edit) {
3572
+ const editorUserId = direction === "outgoing" ? this.backend.selfUserId : senderUser;
3573
+ this.editFold.ingest(
3574
+ {
3575
+ targetClientMsgId: incoming.edit.targetClientMsgId,
3576
+ editorUserId,
3577
+ newText: incoming.edit.newText,
3578
+ epoch: incoming.epoch,
3579
+ serverSeq: incoming.serverSeq,
3580
+ eventClientMsgId: incoming.clientMsgId
3581
+ },
3582
+ this.authorOfTarget
3583
+ );
3584
+ this.recomputeEdit(incoming.edit.targetClientMsgId);
3585
+ return;
3586
+ }
3406
3587
  const incomingClientMsgId = incoming.clientMsgId;
3407
3588
  const incomingReplyRef = incoming.replyRef;
3408
3589
  let resolvedReplyTo = null;
@@ -3421,7 +3602,9 @@ var Chat = class {
3421
3602
  replyTo: resolvedReplyTo,
3422
3603
  // Attach any tally already folded for this message (a reaction that arrived
3423
3604
  // BEFORE its target — the dangling case — renders the moment the target lands).
3424
- reactions: incomingClientMsgId ? this.reactionFold.tally(incomingClientMsgId) : {}
3605
+ reactions: incomingClientMsgId ? this.reactionFold.tally(incomingClientMsgId) : {},
3606
+ // Default false; applyEditOverlay below folds any edit that arrived first.
3607
+ edited: false
3425
3608
  };
3426
3609
  if (incomingClientMsgId && incoming.text !== null) {
3427
3610
  this.byClientMsgId.set(incomingClientMsgId, {
@@ -3429,7 +3612,11 @@ var Chat = class {
3429
3612
  senderUserId: senderUser ?? ""
3430
3613
  });
3431
3614
  }
3432
- this.messageList.push(msg);
3615
+ if (incomingClientMsgId) {
3616
+ this.seedEditBase(incomingClientMsgId, incoming.text, senderUser);
3617
+ this.editFold.reevaluateHeld(this.authorOfTarget);
3618
+ }
3619
+ this.messageList.push(this.applyEditOverlay(msg));
3433
3620
  this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
3434
3621
  this.loadedEarliestSeq = Math.min(
3435
3622
  this.loadedEarliestSeq ?? incoming.serverSeq,
@@ -3437,6 +3624,19 @@ var Chat = class {
3437
3624
  );
3438
3625
  this.emit();
3439
3626
  }
3627
+ /** The EditFold author-gate input: the target message's resolved author userId
3628
+ * (null = target unknown/dangling → the fold HOLDs). Captured as a bound arrow
3629
+ * so it can be passed to the pure EditFold. */
3630
+ authorOfTarget = (targetClientMsgId) => this.authorByClientMsgId.get(targetClientMsgId) ?? null;
3631
+ /** Seed the per-target base text + author for the edit fold. Base is write-once
3632
+ * (a later own/peer edit must not overwrite the original we render against). The
3633
+ * author is (re)recorded whenever a non-empty resolution is available. */
3634
+ seedEditBase(clientMsgId, text, author) {
3635
+ if (!this.originalTextByClientMsgId.has(clientMsgId)) {
3636
+ this.originalTextByClientMsgId.set(clientMsgId, text);
3637
+ }
3638
+ if (author !== null) this.authorByClientMsgId.set(clientMsgId, author);
3639
+ }
3440
3640
  /**
3441
3641
  * Rebuild the target message's `reactions` from the authoritative fold and
3442
3642
  * re-emit. No-op when the target isn't present yet (its tally is attached the
@@ -3466,6 +3666,46 @@ var Chat = class {
3466
3666
  if (sameReactions(m.reactions, tally)) return m;
3467
3667
  return { ...m, reactions: tally };
3468
3668
  }
3669
+ /**
3670
+ * Rebuild the target message's rendered `text` + `edited` flag from the
3671
+ * authoritative edit fold and re-emit, PRESERVING `.reactions` and `.replyTo`
3672
+ * (the reaction-polish lesson — never clobber). text = `editFold.text(cid) ??
3673
+ * base`; base is the seeded original so a forged/ignored edit leaves it intact.
3674
+ * No-op when the target isn't present yet (the fold already recorded it; the
3675
+ * overlay applies the moment the target lands) or when unchanged.
3676
+ */
3677
+ recomputeEdit(targetClientMsgId) {
3678
+ if (!targetClientMsgId) return;
3679
+ const editText = this.editFold.text(targetClientMsgId);
3680
+ const foldEdited = this.editFold.isEdited(targetClientMsgId);
3681
+ let changed = false;
3682
+ this.messageList = this.messageList.map((m) => {
3683
+ if (m.clientMsgId !== targetClientMsgId) return m;
3684
+ const base = this.originalTextByClientMsgId.has(targetClientMsgId) ? this.originalTextByClientMsgId.get(targetClientMsgId) ?? null : m.text;
3685
+ const text = editText ?? base;
3686
+ const edited = foldEdited || m.edited;
3687
+ if (m.text === text && m.edited === edited) return m;
3688
+ changed = true;
3689
+ return { ...m, text, edited };
3690
+ });
3691
+ if (changed) this.emit();
3692
+ }
3693
+ /**
3694
+ * Overlay the authoritative edit fold's winning text + flag onto a message as it
3695
+ * is appended/merged. The fold WINS when it has an edit for this target;
3696
+ * otherwise the upstream `text`/`edited` (e.g. the coordinator's page-local
3697
+ * history fold) is preserved. PRESERVES reactions + replyTo.
3698
+ */
3699
+ applyEditOverlay(m) {
3700
+ if (!m.clientMsgId) return m;
3701
+ const editText = this.editFold.text(m.clientMsgId);
3702
+ const foldEdited = this.editFold.isEdited(m.clientMsgId);
3703
+ if (editText === null && !foldEdited) return m;
3704
+ const text = editText ?? m.text;
3705
+ const edited = foldEdited || m.edited;
3706
+ if (m.text === text && m.edited === edited) return m;
3707
+ return { ...m, text, edited };
3708
+ }
3469
3709
  /** @internal — called by the backend's conv subscription. */
3470
3710
  applyConv(event, payload) {
3471
3711
  const userId = typeof payload.user_id === "string" ? payload.user_id : null;
@@ -3520,6 +3760,8 @@ var Chat = class {
3520
3760
  this.memberCache = m;
3521
3761
  this.emit();
3522
3762
  }
3763
+ this.editFold.reevaluateHeld(this.authorOfTarget);
3764
+ for (const cid of this.originalTextByClientMsgId.keys()) this.recomputeEdit(cid);
3523
3765
  }
3524
3766
  seedMembersFromGroup(group) {
3525
3767
  const seed = [
@@ -3598,6 +3840,7 @@ var Chat = class {
3598
3840
  this.seenKeys.add(key);
3599
3841
  if (clientMsgId) {
3600
3842
  this.byClientMsgId.set(clientMsgId, { text, senderUserId: this.backend.selfUserId });
3843
+ this.seedEditBase(clientMsgId, text, this.backend.selfUserId);
3601
3844
  }
3602
3845
  this.messageList.push({
3603
3846
  id: this.publicId(receipt.serverSeq),
@@ -3611,7 +3854,9 @@ var Chat = class {
3611
3854
  replyTo: resolvedReplyTo,
3612
3855
  // Attach any tally already folded for this own-sent message (rare, but keeps
3613
3856
  // the dangling-target invariant uniform across every append path).
3614
- reactions: clientMsgId ? this.reactionFold.tally(clientMsgId) : {}
3857
+ reactions: clientMsgId ? this.reactionFold.tally(clientMsgId) : {},
3858
+ // Own-sent edits fold via edit() after the fact; new sends start unedited.
3859
+ edited: false
3615
3860
  });
3616
3861
  this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
3617
3862
  this.emit();
@@ -3692,6 +3937,37 @@ var Chat = class {
3692
3937
  });
3693
3938
  this.recomputeReactions(message.clientMsgId);
3694
3939
  }
3940
+ // ── Edit ──
3941
+ /** Edit an own text message (edit-by-supersession). No-op if the message isn't
3942
+ * editable (empty clientMsgId, or not a `text` kind). The edit folds locally
3943
+ * with the server receipt's `(epoch, serverSeq)` so the target's text updates
3944
+ * instantly; the durable echo on the next pump is a fold no-op (dedup on the
3945
+ * SAME wire clientMsgId). Never appends a bubble; PRESERVES the target's
3946
+ * reactions + reply context. Only the original author's edits count — for an own
3947
+ * message self IS the author, so the author-gate passes. */
3948
+ async edit(message, newText) {
3949
+ if (!message.clientMsgId || message.kind !== "text") return;
3950
+ const group = await this.materializeIfNeeded();
3951
+ const clientMsgId = mintClientMsgId();
3952
+ const { receipt } = await this.backend.sendEdit(group, {
3953
+ clientMsgId,
3954
+ targetClientMsgId: message.clientMsgId,
3955
+ newText
3956
+ });
3957
+ this.seedEditBase(message.clientMsgId, message.text, this.backend.selfUserId);
3958
+ this.editFold.ingest(
3959
+ {
3960
+ targetClientMsgId: message.clientMsgId,
3961
+ editorUserId: this.backend.selfUserId,
3962
+ newText,
3963
+ epoch: receipt.epoch,
3964
+ serverSeq: receipt.serverSeq,
3965
+ eventClientMsgId: clientMsgId
3966
+ },
3967
+ this.authorOfTarget
3968
+ );
3969
+ this.recomputeEdit(message.clientMsgId);
3970
+ }
3695
3971
  };
3696
3972
  function sameReactions(a, b) {
3697
3973
  const ak = Object.keys(a);
@@ -3734,6 +4010,11 @@ var MessageHub = class {
3734
4010
  };
3735
4011
  }
3736
4012
  };
4013
+ function decodeSenderDeviceId(sender) {
4014
+ if (sender.length === 0) return null;
4015
+ const id = decodeUtf8(sender);
4016
+ return id.length > 0 ? id : null;
4017
+ }
3737
4018
  var MessageDeliverySource = class {
3738
4019
  constructor(rt, engine, hub, registry, messageStore, deviceId, selfUserId) {
3739
4020
  this.rt = rt;
@@ -3866,11 +4147,13 @@ var MessageDeliverySource = class {
3866
4147
  const decoded = decodeEnvelope(received.data);
3867
4148
  const { text, clientMsgId, replyTo } = decoded;
3868
4149
  const isReaction = decoded.type === "reaction" && decoded.reaction != null;
4150
+ const isEdit = decoded.type === "edit" && decoded.edit != null;
4151
+ const senderDeviceId = row.sender_device_id ?? decodeSenderDeviceId(received.sender);
3869
4152
  const stored = {
3870
4153
  id: `${group.rfcGroupId}#${row.server_seq}`,
3871
4154
  direction: "incoming",
3872
4155
  text,
3873
- senderDeviceId: row.sender_device_id ?? null,
4156
+ senderDeviceId,
3874
4157
  epoch: row.epoch,
3875
4158
  serverSeq: row.server_seq,
3876
4159
  at: Date.now(),
@@ -3892,6 +4175,17 @@ var MessageDeliverySource = class {
3892
4175
  emoji: decoded.reaction.emoji,
3893
4176
  op: decoded.reaction.op
3894
4177
  }
4178
+ } : {},
4179
+ // Thread the edit discriminator + new text through the persisted row so an
4180
+ // edit folded LIVE re-folds onto its target after a reload (the reload-parity
4181
+ // boundary — mirrors iOS T3). Omitted for non-edits → old rows hydrate as
4182
+ // `'text'`/no-edit (backward-compat).
4183
+ ...isEdit && decoded.edit ? {
4184
+ envelopeType: "edit",
4185
+ edit: {
4186
+ targetClientMsgId: decoded.edit.targetClientMsgId,
4187
+ newText: decoded.edit.newText
4188
+ }
3895
4189
  } : {}
3896
4190
  };
3897
4191
  try {
@@ -3903,14 +4197,15 @@ var MessageDeliverySource = class {
3903
4197
  kind: "application",
3904
4198
  group,
3905
4199
  text,
3906
- senderDeviceId: row.sender_device_id ?? null,
4200
+ senderDeviceId,
3907
4201
  epoch: row.epoch,
3908
4202
  serverSeq: row.server_seq,
3909
4203
  receivedAt: /* @__PURE__ */ new Date(),
3910
4204
  clientMsgId,
3911
4205
  replyRef: replyTo,
3912
4206
  envelopeType: decoded.type ?? "text",
3913
- reaction: isReaction ? decoded.reaction : null
4207
+ reaction: isReaction ? decoded.reaction : null,
4208
+ edit: isEdit ? decoded.edit : null
3914
4209
  });
3915
4210
  return true;
3916
4211
  }
@@ -5918,6 +6213,10 @@ var MessagingCoordinator = class {
5918
6213
  const r = await this.resolve();
5919
6214
  return r.groups.sendReaction(group, args);
5920
6215
  }
6216
+ async sendEdit(group, args) {
6217
+ const r = await this.resolve();
6218
+ return r.groups.sendEdit(group, args);
6219
+ }
5921
6220
  async history(group, limit, before) {
5922
6221
  const r = await this.resolve();
5923
6222
  const rows = await r.messageStore.history(group.rfcGroupId, limit, before);
@@ -6014,6 +6313,32 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
6014
6313
  eventClientMsgId: s.clientMsgId ?? `${s.id}`
6015
6314
  });
6016
6315
  }
6316
+ const editFold = new EditFold();
6317
+ const authorByClientMsgId = /* @__PURE__ */ new Map();
6318
+ for (const s of rows) {
6319
+ if (s.envelopeType === "reaction" || s.envelopeType === "edit") continue;
6320
+ const cid = s.clientMsgId ?? "";
6321
+ if (!cid) continue;
6322
+ const author = s.direction === "outgoing" ? selfUserId : resolveActor?.(s.senderDeviceId ?? null);
6323
+ if (author != null) authorByClientMsgId.set(cid, author);
6324
+ }
6325
+ const authorOfTarget = (cid) => authorByClientMsgId.get(cid) ?? null;
6326
+ for (const s of rows) {
6327
+ if (s.envelopeType !== "edit" || !s.edit) continue;
6328
+ const editor = s.direction === "outgoing" ? selfUserId : resolveActor?.(s.senderDeviceId ?? null) ?? null;
6329
+ editFold.ingest(
6330
+ {
6331
+ targetClientMsgId: s.edit.targetClientMsgId,
6332
+ editorUserId: editor,
6333
+ newText: s.edit.newText,
6334
+ epoch: s.epoch,
6335
+ serverSeq: s.serverSeq,
6336
+ eventClientMsgId: s.clientMsgId ?? `${s.id}`
6337
+ },
6338
+ authorOfTarget
6339
+ );
6340
+ }
6341
+ editFold.reevaluateHeld(authorOfTarget);
6017
6342
  const lookup = /* @__PURE__ */ new Map();
6018
6343
  for (const s of rows) {
6019
6344
  if (s.envelopeType === "reaction") continue;
@@ -6025,7 +6350,7 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
6025
6350
  }
6026
6351
  const out = [];
6027
6352
  for (const s of rows) {
6028
- if (s.envelopeType === "reaction") continue;
6353
+ if (s.envelopeType === "reaction" || s.envelopeType === "edit") continue;
6029
6354
  const clientMsgId = s.clientMsgId ?? "";
6030
6355
  let replyTo = null;
6031
6356
  if (s.replyTo) {
@@ -6041,17 +6366,20 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
6041
6366
  };
6042
6367
  replyTo = resolveReply(ref, (id) => lookup.get(id) ?? null);
6043
6368
  }
6369
+ const editText = clientMsgId ? editFold.text(clientMsgId) : null;
6370
+ const edited = clientMsgId ? editFold.isEdited(clientMsgId) : false;
6044
6371
  out.push({
6045
6372
  id: `${displayId}#${s.serverSeq}`,
6046
6373
  kind: s.text != null ? "text" : "system",
6047
6374
  direction: s.direction,
6048
6375
  senderUserId: s.direction === "outgoing" ? selfUserId : null,
6049
- text: s.text,
6376
+ text: editText ?? s.text,
6050
6377
  serverSeq: s.serverSeq,
6051
6378
  sentAt: new Date(s.at),
6052
6379
  clientMsgId,
6053
6380
  replyTo,
6054
- reactions: clientMsgId ? fold.tally(clientMsgId) : {}
6381
+ reactions: clientMsgId ? fold.tally(clientMsgId) : {},
6382
+ edited
6055
6383
  });
6056
6384
  }
6057
6385
  return out;
@@ -6789,7 +7117,7 @@ function defaultSessionStorage(key) {
6789
7117
  }
6790
7118
 
6791
7119
  // src/version.ts
6792
- var VERSION = "1.2.0";
7120
+ var VERSION = "1.3.0";
6793
7121
 
6794
7122
  // src/runtime.ts
6795
7123
  function buildRuntime(config) {