@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.
@@ -2591,6 +2591,88 @@ var PalbeFlags = class {
2591
2591
  }
2592
2592
  };
2593
2593
 
2594
+ // src/messaging/edit-fold.ts
2595
+ function orderLt(aEpoch, aSeq, bEpoch, bSeq) {
2596
+ if (aEpoch !== bEpoch) return aEpoch < bEpoch;
2597
+ return aSeq < bSeq;
2598
+ }
2599
+ function orderEq(aEpoch, aSeq, bEpoch, bSeq) {
2600
+ return aEpoch === bEpoch && aSeq === bSeq;
2601
+ }
2602
+ var EditFold = class {
2603
+ // target → winning edit state
2604
+ states = /* @__PURE__ */ new Map();
2605
+ // dedup of real wire events that reached (and were resolvable enough to evaluate at) the fold
2606
+ seenEvents = /* @__PURE__ */ new Set();
2607
+ // events parked because target/author or sender was unresolved at ingest time
2608
+ held = [];
2609
+ // targets that have had ≥1 valid edit applied (write-once)
2610
+ editedTargets = /* @__PURE__ */ new Set();
2611
+ /**
2612
+ * Ingest one edit. `authorOfTarget` resolves the target message's AUTHOR userId
2613
+ * (null = target unknown/dangling → HOLD).
2614
+ */
2615
+ ingest(e, authorOfTarget) {
2616
+ const author = authorOfTarget(e.targetClientMsgId);
2617
+ if (author === null) {
2618
+ this.holdIfNew(e);
2619
+ return;
2620
+ }
2621
+ if (e.editorUserId === null) {
2622
+ this.holdIfNew(e);
2623
+ return;
2624
+ }
2625
+ if (e.editorUserId !== author) return;
2626
+ if (this.seenEvents.has(e.eventClientMsgId)) return;
2627
+ this.seenEvents.add(e.eventClientMsgId);
2628
+ const prev = this.states.get(e.targetClientMsgId);
2629
+ if (prev !== void 0) {
2630
+ if (orderLt(e.epoch, e.serverSeq, prev.orderEpoch, prev.orderSeq)) return;
2631
+ if (orderEq(e.epoch, e.serverSeq, prev.orderEpoch, prev.orderSeq) && e.eventClientMsgId <= prev.lastEventId) {
2632
+ return;
2633
+ }
2634
+ }
2635
+ this.states.set(e.targetClientMsgId, {
2636
+ orderEpoch: e.epoch,
2637
+ orderSeq: e.serverSeq,
2638
+ lastEventId: e.eventClientMsgId,
2639
+ text: e.newText
2640
+ });
2641
+ this.editedTargets.add(e.targetClientMsgId);
2642
+ }
2643
+ /**
2644
+ * Park an event for later re-attempt, deduping held re-deliveries by
2645
+ * eventClientMsgId so a repeatedly-delivered unresolvable edit is held exactly
2646
+ * once (and never double-applies when it finally resolves on reevaluate).
2647
+ */
2648
+ holdIfNew(e) {
2649
+ if (this.held.some((h) => h.eventClientMsgId === e.eventClientMsgId)) return;
2650
+ this.held.push(e);
2651
+ }
2652
+ /** The winning edit text for a target, or null if no valid edit has applied. */
2653
+ text(targetClientMsgId) {
2654
+ return this.states.get(targetClientMsgId)?.text ?? null;
2655
+ }
2656
+ /** Write-once: true once any valid edit applied to the target. */
2657
+ isEdited(targetClientMsgId) {
2658
+ return this.editedTargets.has(targetClientMsgId);
2659
+ }
2660
+ /**
2661
+ * Re-run HELD edits when the roster/target newly resolves (call on member/roster
2662
+ * change and when a target message arrives). Clears `held` and re-ingests each
2663
+ * event with the fresh `authorOfTarget` — events that still don't resolve are
2664
+ * simply re-held; events that now resolve fold via the normal LWW path.
2665
+ * Idempotent: re-ingest is deduped by `seenEvents` (applied events) and by
2666
+ * `holdIfNew` (still-held events), so reevaluating repeatedly can neither
2667
+ * double-apply nor lose an edit.
2668
+ */
2669
+ reevaluateHeld(authorOfTarget) {
2670
+ const pending = this.held;
2671
+ this.held = [];
2672
+ for (const e of pending) this.ingest(e, authorOfTarget);
2673
+ }
2674
+ };
2675
+
2594
2676
  // src/messaging/util.ts
2595
2677
  function toBase64(bytes) {
2596
2678
  if (typeof Buffer !== "undefined") {
@@ -2730,6 +2812,17 @@ async function listDevices(rt, userId) {
2730
2812
  }
2731
2813
 
2732
2814
  // src/messaging/group-messaging.ts
2815
+ function encodeEdit(args) {
2816
+ return encodeUtf8(
2817
+ JSON.stringify({
2818
+ v: 1,
2819
+ type: "edit",
2820
+ client_msg_id: args.clientMsgId,
2821
+ target_client_msg_id: args.targetClientMsgId,
2822
+ new_text: args.newText
2823
+ })
2824
+ );
2825
+ }
2733
2826
  function encodeReaction(args) {
2734
2827
  return encodeUtf8(
2735
2828
  JSON.stringify({
@@ -2769,6 +2862,18 @@ function decodeEnvelope(bytes) {
2769
2862
  }
2770
2863
  };
2771
2864
  }
2865
+ if (typeof o === "object" && o !== null && o.type === "edit") {
2866
+ return {
2867
+ type: "edit",
2868
+ text: null,
2869
+ clientMsgId: o.client_msg_id ?? "",
2870
+ replyTo: null,
2871
+ edit: {
2872
+ targetClientMsgId: o.target_client_msg_id ?? "",
2873
+ newText: o.new_text ?? ""
2874
+ }
2875
+ };
2876
+ }
2772
2877
  if (typeof o === "object" && o !== null && o.type === "text" && typeof o.v === "number") {
2773
2878
  return {
2774
2879
  type: "text",
@@ -3083,6 +3188,54 @@ var GroupMessaging = class {
3083
3188
  clientMsgId: args.clientMsgId
3084
3189
  };
3085
3190
  }
3191
+ /** Send an edit (edit-by-supersession on a target message). Encrypts a
3192
+ * `type:'edit'` envelope at the current epoch and sends through the SAME MLS
3193
+ * application path as `sendText` (the server stays blind — an edit is just
3194
+ * another application message). Persists the outgoing edit row so it re-folds
3195
+ * onto its target's text after a reload (the own-send half of the reload
3196
+ * parity). NEVER rebases (epoch-bound like any application message). */
3197
+ async sendEdit(group, args) {
3198
+ const plaintext = encodeEdit({
3199
+ clientMsgId: args.clientMsgId,
3200
+ targetClientMsgId: args.targetClientMsgId,
3201
+ newText: args.newText
3202
+ });
3203
+ const ct = await this.engine.encryptApplication(fromBase64(group.rfcGroupId), plaintext);
3204
+ const body = {
3205
+ ciphertext_b64: toBase64(ct),
3206
+ client_idem_key: randomId()
3207
+ };
3208
+ const wire = await palbeRequest(
3209
+ this.rt,
3210
+ "POST",
3211
+ MessagingPaths.groupMessages(group.displayId),
3212
+ { body }
3213
+ );
3214
+ const stored = {
3215
+ id: `${group.rfcGroupId}#${wire.server_seq}`,
3216
+ direction: "outgoing",
3217
+ text: null,
3218
+ senderDeviceId: this.selfDeviceId,
3219
+ epoch: wire.epoch,
3220
+ serverSeq: wire.server_seq,
3221
+ at: Date.now(),
3222
+ clientMsgId: args.clientMsgId,
3223
+ replyTo: null,
3224
+ envelopeType: "edit",
3225
+ edit: {
3226
+ targetClientMsgId: args.targetClientMsgId,
3227
+ newText: args.newText
3228
+ }
3229
+ };
3230
+ try {
3231
+ await this.messageStore.append(group.rfcGroupId, stored);
3232
+ } catch {
3233
+ }
3234
+ return {
3235
+ receipt: { serverSeq: wire.server_seq, epoch: wire.epoch },
3236
+ clientMsgId: args.clientMsgId
3237
+ };
3238
+ }
3086
3239
  // ── The rebase loop ──
3087
3240
  async commitWithRebase(rfcGroupId, build) {
3088
3241
  const gidBytes = fromBase64(rfcGroupId);
@@ -3219,6 +3372,14 @@ var Chat = class {
3219
3372
  byClientMsgId = /* @__PURE__ */ new Map();
3220
3373
  /** The single authoritative reaction fold for this chat (live + own-send + history). */
3221
3374
  reactionFold = new ReactionFold();
3375
+ /** The single authoritative edit fold for this chat (live + own-send + history). */
3376
+ editFold = new EditFold();
3377
+ /** Per-target BASE (original) text, so the rendered text is `edit ?? original`.
3378
+ * Seeded once per target (write-once base) so a 2nd own edit can't corrupt it. */
3379
+ originalTextByClientMsgId = /* @__PURE__ */ new Map();
3380
+ /** Per-target AUTHOR userId — the EditFold author-gate input (filled at bubble
3381
+ * projection time from senderUserId; '' = resolved-but-unknown peer). */
3382
+ authorByClientMsgId = /* @__PURE__ */ new Map();
3222
3383
  loadedEarliestSeq = null;
3223
3384
  historyLoaded = false;
3224
3385
  wired = false;
@@ -3319,13 +3480,17 @@ var Chat = class {
3319
3480
  senderUserId: m.senderUserId ?? ""
3320
3481
  });
3321
3482
  }
3483
+ if (m.clientMsgId) {
3484
+ this.seedEditBase(m.clientMsgId, m.text, m.senderUserId ?? "");
3485
+ }
3322
3486
  }
3487
+ this.editFold.reevaluateHeld(this.authorOfTarget);
3323
3488
  for (const m of incoming) {
3324
3489
  if (m.serverSeq <= 0) continue;
3325
3490
  const key = this.internalKey(m.serverSeq);
3326
3491
  if (this.seenKeys.has(key)) continue;
3327
3492
  this.seenKeys.add(key);
3328
- this.messageList.push(this.applyReactionTally(m));
3493
+ this.messageList.push(this.applyEditOverlay(this.applyReactionTally(m)));
3329
3494
  changed = true;
3330
3495
  this.loadedEarliestSeq = Math.min(this.loadedEarliestSeq ?? m.serverSeq, m.serverSeq);
3331
3496
  }
@@ -3366,6 +3531,22 @@ var Chat = class {
3366
3531
  }
3367
3532
  return;
3368
3533
  }
3534
+ if (incoming.envelopeType === "edit" && incoming.edit) {
3535
+ const editorUserId = direction === "outgoing" ? this.backend.selfUserId : senderUser;
3536
+ this.editFold.ingest(
3537
+ {
3538
+ targetClientMsgId: incoming.edit.targetClientMsgId,
3539
+ editorUserId,
3540
+ newText: incoming.edit.newText,
3541
+ epoch: incoming.epoch,
3542
+ serverSeq: incoming.serverSeq,
3543
+ eventClientMsgId: incoming.clientMsgId
3544
+ },
3545
+ this.authorOfTarget
3546
+ );
3547
+ this.recomputeEdit(incoming.edit.targetClientMsgId);
3548
+ return;
3549
+ }
3369
3550
  const incomingClientMsgId = incoming.clientMsgId;
3370
3551
  const incomingReplyRef = incoming.replyRef;
3371
3552
  let resolvedReplyTo = null;
@@ -3384,7 +3565,9 @@ var Chat = class {
3384
3565
  replyTo: resolvedReplyTo,
3385
3566
  // Attach any tally already folded for this message (a reaction that arrived
3386
3567
  // BEFORE its target — the dangling case — renders the moment the target lands).
3387
- reactions: incomingClientMsgId ? this.reactionFold.tally(incomingClientMsgId) : {}
3568
+ reactions: incomingClientMsgId ? this.reactionFold.tally(incomingClientMsgId) : {},
3569
+ // Default false; applyEditOverlay below folds any edit that arrived first.
3570
+ edited: false
3388
3571
  };
3389
3572
  if (incomingClientMsgId && incoming.text !== null) {
3390
3573
  this.byClientMsgId.set(incomingClientMsgId, {
@@ -3392,7 +3575,11 @@ var Chat = class {
3392
3575
  senderUserId: senderUser ?? ""
3393
3576
  });
3394
3577
  }
3395
- this.messageList.push(msg);
3578
+ if (incomingClientMsgId) {
3579
+ this.seedEditBase(incomingClientMsgId, incoming.text, senderUser);
3580
+ this.editFold.reevaluateHeld(this.authorOfTarget);
3581
+ }
3582
+ this.messageList.push(this.applyEditOverlay(msg));
3396
3583
  this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
3397
3584
  this.loadedEarliestSeq = Math.min(
3398
3585
  this.loadedEarliestSeq ?? incoming.serverSeq,
@@ -3400,6 +3587,19 @@ var Chat = class {
3400
3587
  );
3401
3588
  this.emit();
3402
3589
  }
3590
+ /** The EditFold author-gate input: the target message's resolved author userId
3591
+ * (null = target unknown/dangling → the fold HOLDs). Captured as a bound arrow
3592
+ * so it can be passed to the pure EditFold. */
3593
+ authorOfTarget = (targetClientMsgId) => this.authorByClientMsgId.get(targetClientMsgId) ?? null;
3594
+ /** Seed the per-target base text + author for the edit fold. Base is write-once
3595
+ * (a later own/peer edit must not overwrite the original we render against). The
3596
+ * author is (re)recorded whenever a non-empty resolution is available. */
3597
+ seedEditBase(clientMsgId, text, author) {
3598
+ if (!this.originalTextByClientMsgId.has(clientMsgId)) {
3599
+ this.originalTextByClientMsgId.set(clientMsgId, text);
3600
+ }
3601
+ if (author !== null) this.authorByClientMsgId.set(clientMsgId, author);
3602
+ }
3403
3603
  /**
3404
3604
  * Rebuild the target message's `reactions` from the authoritative fold and
3405
3605
  * re-emit. No-op when the target isn't present yet (its tally is attached the
@@ -3429,6 +3629,46 @@ var Chat = class {
3429
3629
  if (sameReactions(m.reactions, tally)) return m;
3430
3630
  return { ...m, reactions: tally };
3431
3631
  }
3632
+ /**
3633
+ * Rebuild the target message's rendered `text` + `edited` flag from the
3634
+ * authoritative edit fold and re-emit, PRESERVING `.reactions` and `.replyTo`
3635
+ * (the reaction-polish lesson — never clobber). text = `editFold.text(cid) ??
3636
+ * base`; base is the seeded original so a forged/ignored edit leaves it intact.
3637
+ * No-op when the target isn't present yet (the fold already recorded it; the
3638
+ * overlay applies the moment the target lands) or when unchanged.
3639
+ */
3640
+ recomputeEdit(targetClientMsgId) {
3641
+ if (!targetClientMsgId) return;
3642
+ const editText = this.editFold.text(targetClientMsgId);
3643
+ const foldEdited = this.editFold.isEdited(targetClientMsgId);
3644
+ let changed = false;
3645
+ this.messageList = this.messageList.map((m) => {
3646
+ if (m.clientMsgId !== targetClientMsgId) return m;
3647
+ const base = this.originalTextByClientMsgId.has(targetClientMsgId) ? this.originalTextByClientMsgId.get(targetClientMsgId) ?? null : m.text;
3648
+ const text = editText ?? base;
3649
+ const edited = foldEdited || m.edited;
3650
+ if (m.text === text && m.edited === edited) return m;
3651
+ changed = true;
3652
+ return { ...m, text, edited };
3653
+ });
3654
+ if (changed) this.emit();
3655
+ }
3656
+ /**
3657
+ * Overlay the authoritative edit fold's winning text + flag onto a message as it
3658
+ * is appended/merged. The fold WINS when it has an edit for this target;
3659
+ * otherwise the upstream `text`/`edited` (e.g. the coordinator's page-local
3660
+ * history fold) is preserved. PRESERVES reactions + replyTo.
3661
+ */
3662
+ applyEditOverlay(m) {
3663
+ if (!m.clientMsgId) return m;
3664
+ const editText = this.editFold.text(m.clientMsgId);
3665
+ const foldEdited = this.editFold.isEdited(m.clientMsgId);
3666
+ if (editText === null && !foldEdited) return m;
3667
+ const text = editText ?? m.text;
3668
+ const edited = foldEdited || m.edited;
3669
+ if (m.text === text && m.edited === edited) return m;
3670
+ return { ...m, text, edited };
3671
+ }
3432
3672
  /** @internal — called by the backend's conv subscription. */
3433
3673
  applyConv(event, payload) {
3434
3674
  const userId = typeof payload.user_id === "string" ? payload.user_id : null;
@@ -3483,6 +3723,8 @@ var Chat = class {
3483
3723
  this.memberCache = m;
3484
3724
  this.emit();
3485
3725
  }
3726
+ this.editFold.reevaluateHeld(this.authorOfTarget);
3727
+ for (const cid of this.originalTextByClientMsgId.keys()) this.recomputeEdit(cid);
3486
3728
  }
3487
3729
  seedMembersFromGroup(group) {
3488
3730
  const seed = [
@@ -3561,6 +3803,7 @@ var Chat = class {
3561
3803
  this.seenKeys.add(key);
3562
3804
  if (clientMsgId) {
3563
3805
  this.byClientMsgId.set(clientMsgId, { text, senderUserId: this.backend.selfUserId });
3806
+ this.seedEditBase(clientMsgId, text, this.backend.selfUserId);
3564
3807
  }
3565
3808
  this.messageList.push({
3566
3809
  id: this.publicId(receipt.serverSeq),
@@ -3574,7 +3817,9 @@ var Chat = class {
3574
3817
  replyTo: resolvedReplyTo,
3575
3818
  // Attach any tally already folded for this own-sent message (rare, but keeps
3576
3819
  // the dangling-target invariant uniform across every append path).
3577
- reactions: clientMsgId ? this.reactionFold.tally(clientMsgId) : {}
3820
+ reactions: clientMsgId ? this.reactionFold.tally(clientMsgId) : {},
3821
+ // Own-sent edits fold via edit() after the fact; new sends start unedited.
3822
+ edited: false
3578
3823
  });
3579
3824
  this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
3580
3825
  this.emit();
@@ -3655,6 +3900,37 @@ var Chat = class {
3655
3900
  });
3656
3901
  this.recomputeReactions(message.clientMsgId);
3657
3902
  }
3903
+ // ── Edit ──
3904
+ /** Edit an own text message (edit-by-supersession). No-op if the message isn't
3905
+ * editable (empty clientMsgId, or not a `text` kind). The edit folds locally
3906
+ * with the server receipt's `(epoch, serverSeq)` so the target's text updates
3907
+ * instantly; the durable echo on the next pump is a fold no-op (dedup on the
3908
+ * SAME wire clientMsgId). Never appends a bubble; PRESERVES the target's
3909
+ * reactions + reply context. Only the original author's edits count — for an own
3910
+ * message self IS the author, so the author-gate passes. */
3911
+ async edit(message, newText) {
3912
+ if (!message.clientMsgId || message.kind !== "text") return;
3913
+ const group = await this.materializeIfNeeded();
3914
+ const clientMsgId = mintClientMsgId();
3915
+ const { receipt } = await this.backend.sendEdit(group, {
3916
+ clientMsgId,
3917
+ targetClientMsgId: message.clientMsgId,
3918
+ newText
3919
+ });
3920
+ this.seedEditBase(message.clientMsgId, message.text, this.backend.selfUserId);
3921
+ this.editFold.ingest(
3922
+ {
3923
+ targetClientMsgId: message.clientMsgId,
3924
+ editorUserId: this.backend.selfUserId,
3925
+ newText,
3926
+ epoch: receipt.epoch,
3927
+ serverSeq: receipt.serverSeq,
3928
+ eventClientMsgId: clientMsgId
3929
+ },
3930
+ this.authorOfTarget
3931
+ );
3932
+ this.recomputeEdit(message.clientMsgId);
3933
+ }
3658
3934
  };
3659
3935
  function sameReactions(a, b) {
3660
3936
  const ak = Object.keys(a);
@@ -3834,6 +4110,7 @@ var MessageDeliverySource = class {
3834
4110
  const decoded = decodeEnvelope(received.data);
3835
4111
  const { text, clientMsgId, replyTo } = decoded;
3836
4112
  const isReaction = decoded.type === "reaction" && decoded.reaction != null;
4113
+ const isEdit = decoded.type === "edit" && decoded.edit != null;
3837
4114
  const senderDeviceId = row.sender_device_id ?? decodeSenderDeviceId(received.sender);
3838
4115
  const stored = {
3839
4116
  id: `${group.rfcGroupId}#${row.server_seq}`,
@@ -3861,6 +4138,17 @@ var MessageDeliverySource = class {
3861
4138
  emoji: decoded.reaction.emoji,
3862
4139
  op: decoded.reaction.op
3863
4140
  }
4141
+ } : {},
4142
+ // Thread the edit discriminator + new text through the persisted row so an
4143
+ // edit folded LIVE re-folds onto its target after a reload (the reload-parity
4144
+ // boundary — mirrors iOS T3). Omitted for non-edits → old rows hydrate as
4145
+ // `'text'`/no-edit (backward-compat).
4146
+ ...isEdit && decoded.edit ? {
4147
+ envelopeType: "edit",
4148
+ edit: {
4149
+ targetClientMsgId: decoded.edit.targetClientMsgId,
4150
+ newText: decoded.edit.newText
4151
+ }
3864
4152
  } : {}
3865
4153
  };
3866
4154
  try {
@@ -3879,7 +4167,8 @@ var MessageDeliverySource = class {
3879
4167
  clientMsgId,
3880
4168
  replyRef: replyTo,
3881
4169
  envelopeType: decoded.type ?? "text",
3882
- reaction: isReaction ? decoded.reaction : null
4170
+ reaction: isReaction ? decoded.reaction : null,
4171
+ edit: isEdit ? decoded.edit : null
3883
4172
  });
3884
4173
  return true;
3885
4174
  }
@@ -5886,6 +6175,10 @@ var MessagingCoordinator = class {
5886
6175
  const r = await this.resolve();
5887
6176
  return r.groups.sendReaction(group, args);
5888
6177
  }
6178
+ async sendEdit(group, args) {
6179
+ const r = await this.resolve();
6180
+ return r.groups.sendEdit(group, args);
6181
+ }
5889
6182
  async history(group, limit, before) {
5890
6183
  const r = await this.resolve();
5891
6184
  const rows = await r.messageStore.history(group.rfcGroupId, limit, before);
@@ -5982,6 +6275,32 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
5982
6275
  eventClientMsgId: s.clientMsgId ?? `${s.id}`
5983
6276
  });
5984
6277
  }
6278
+ const editFold = new EditFold();
6279
+ const authorByClientMsgId = /* @__PURE__ */ new Map();
6280
+ for (const s of rows) {
6281
+ if (s.envelopeType === "reaction" || s.envelopeType === "edit") continue;
6282
+ const cid = s.clientMsgId ?? "";
6283
+ if (!cid) continue;
6284
+ const author = s.direction === "outgoing" ? selfUserId : resolveActor?.(s.senderDeviceId ?? null);
6285
+ if (author != null) authorByClientMsgId.set(cid, author);
6286
+ }
6287
+ const authorOfTarget = (cid) => authorByClientMsgId.get(cid) ?? null;
6288
+ for (const s of rows) {
6289
+ if (s.envelopeType !== "edit" || !s.edit) continue;
6290
+ const editor = s.direction === "outgoing" ? selfUserId : resolveActor?.(s.senderDeviceId ?? null) ?? null;
6291
+ editFold.ingest(
6292
+ {
6293
+ targetClientMsgId: s.edit.targetClientMsgId,
6294
+ editorUserId: editor,
6295
+ newText: s.edit.newText,
6296
+ epoch: s.epoch,
6297
+ serverSeq: s.serverSeq,
6298
+ eventClientMsgId: s.clientMsgId ?? `${s.id}`
6299
+ },
6300
+ authorOfTarget
6301
+ );
6302
+ }
6303
+ editFold.reevaluateHeld(authorOfTarget);
5985
6304
  const lookup = /* @__PURE__ */ new Map();
5986
6305
  for (const s of rows) {
5987
6306
  if (s.envelopeType === "reaction") continue;
@@ -5993,7 +6312,7 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
5993
6312
  }
5994
6313
  const out = [];
5995
6314
  for (const s of rows) {
5996
- if (s.envelopeType === "reaction") continue;
6315
+ if (s.envelopeType === "reaction" || s.envelopeType === "edit") continue;
5997
6316
  const clientMsgId = s.clientMsgId ?? "";
5998
6317
  let replyTo = null;
5999
6318
  if (s.replyTo) {
@@ -6009,17 +6328,20 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
6009
6328
  };
6010
6329
  replyTo = resolveReply(ref, (id) => lookup.get(id) ?? null);
6011
6330
  }
6331
+ const editText = clientMsgId ? editFold.text(clientMsgId) : null;
6332
+ const edited = clientMsgId ? editFold.isEdited(clientMsgId) : false;
6012
6333
  out.push({
6013
6334
  id: `${displayId}#${s.serverSeq}`,
6014
6335
  kind: s.text != null ? "text" : "system",
6015
6336
  direction: s.direction,
6016
6337
  senderUserId: s.direction === "outgoing" ? selfUserId : null,
6017
- text: s.text,
6338
+ text: editText ?? s.text,
6018
6339
  serverSeq: s.serverSeq,
6019
6340
  sentAt: new Date(s.at),
6020
6341
  clientMsgId,
6021
6342
  replyTo,
6022
- reactions: clientMsgId ? fold.tally(clientMsgId) : {}
6343
+ reactions: clientMsgId ? fold.tally(clientMsgId) : {},
6344
+ edited
6023
6345
  });
6024
6346
  }
6025
6347
  return out;
@@ -6757,7 +7079,7 @@ function defaultSessionStorage(key) {
6757
7079
  }
6758
7080
 
6759
7081
  // src/version.ts
6760
- var VERSION = "1.2.1";
7082
+ var VERSION = "1.3.0";
6761
7083
 
6762
7084
  // src/runtime.ts
6763
7085
  function buildRuntime(config) {
@@ -7117,4 +7439,4 @@ export {
7117
7439
  pb,
7118
7440
  createBoundClient
7119
7441
  };
7120
- //# sourceMappingURL=chunk-KJXRY4S3.js.map
7442
+ //# sourceMappingURL=chunk-UX43AB4W.js.map