@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.d.cts CHANGED
@@ -1,6 +1,6 @@
1
- export { A as AnalyticsProperties, a as AuthChangeEvent, b as AuthState, c as AuthSuccess, d as AuthUser, C as Call, e as CallChangeCallback, f as CallParticipant, g as CallState, h as Chat, i as ChatBackend, j as ChatDraft, k as ChatKind, l as ChatMember, m as ChatMessage, n as ChatMessageKind, o as ChatRole, p as ChatState, q as CommsPrefs, F as FlagsView, M as MagicLinkResult, r as MessageDirection, O as OAuthExchangeResult, P as PalbeAnalytics, s as PalbeAuth, t as PalbeCalls, u as PalbeConfig, v as PalbeFlags, w as PalbeMessaging, x as PalbeOAuthConfig, y as PalbeRealtime, z as PresenceState, R as RealtimeChannel, B as RealtimeConnectionState, D as RealtimeHandler, E as RealtimePayload, G as RealtimeStatus, H as RealtimeStatusSnapshot, I as RealtimeSubscription, S as SentReceipt, U as Unsubscribe } from './analytics-facade-Ct3A1zop.cjs';
1
+ export { A as AnalyticsProperties, a as AuthChangeEvent, b as AuthState, c as AuthSuccess, d as AuthUser, C as Call, e as CallChangeCallback, f as CallParticipant, g as CallState, h as Chat, i as ChatBackend, j as ChatDraft, k as ChatKind, l as ChatMember, m as ChatMessage, n as ChatMessageKind, o as ChatRole, p as ChatState, q as CommsPrefs, F as FlagsView, M as MagicLinkResult, r as MessageDirection, O as OAuthExchangeResult, P as PalbeAnalytics, s as PalbeAuth, t as PalbeCalls, u as PalbeConfig, v as PalbeFlags, w as PalbeMessaging, x as PalbeOAuthConfig, y as PalbeRealtime, z as PresenceState, R as RealtimeChannel, B as RealtimeConnectionState, D as RealtimeHandler, E as RealtimePayload, G as RealtimeStatus, H as RealtimeStatusSnapshot, I as RealtimeSubscription, J as ResolvedReply, S as SentReceipt, U as Unsubscribe } from './analytics-facade-B8UATgS4.cjs';
2
2
  export { B as BackendError, a as BackendErrorKind, F as FieldError, i as isBackendError } from './errors-fDoNdTrJ.cjs';
3
- export { C as CallOptions, P as PB, U as UploadOptions, a as UploadProgress, p as pb } from './pb-BlIfgBG-.cjs';
3
+ export { C as CallOptions, P as PB, U as UploadOptions, a as UploadProgress, p as pb } from './pb-DivIiUGR.cjs';
4
4
  export { P as PersistedSession, S as SessionStorageAdapter, l as localStorageSessionStorage, m as memorySessionStorage } from './storage-BPaeSG8K.cjs';
5
5
  export { F as FlagValue } from './pooled-flags-Bwq4usn0.js';
6
6
  import 'livekit-client';
@@ -207,6 +207,6 @@ interface MlsGlue {
207
207
  */
208
208
  declare function initMls(): Promise<MlsGlue>;
209
209
 
210
- declare const VERSION = "1.2.1";
210
+ declare const VERSION = "1.3.0";
211
211
 
212
212
  export { VERSION, initMls };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- export { A as AnalyticsProperties, a as AuthChangeEvent, b as AuthState, c as AuthSuccess, d as AuthUser, C as Call, e as CallChangeCallback, f as CallParticipant, g as CallState, h as Chat, i as ChatBackend, j as ChatDraft, k as ChatKind, l as ChatMember, m as ChatMessage, n as ChatMessageKind, o as ChatRole, p as ChatState, q as CommsPrefs, F as FlagsView, M as MagicLinkResult, r as MessageDirection, O as OAuthExchangeResult, P as PalbeAnalytics, s as PalbeAuth, t as PalbeCalls, u as PalbeConfig, v as PalbeFlags, w as PalbeMessaging, x as PalbeOAuthConfig, y as PalbeRealtime, z as PresenceState, R as RealtimeChannel, B as RealtimeConnectionState, D as RealtimeHandler, E as RealtimePayload, G as RealtimeStatus, H as RealtimeStatusSnapshot, I as RealtimeSubscription, S as SentReceipt, U as Unsubscribe } from './analytics-facade-C93tr7dA.js';
1
+ export { A as AnalyticsProperties, a as AuthChangeEvent, b as AuthState, c as AuthSuccess, d as AuthUser, C as Call, e as CallChangeCallback, f as CallParticipant, g as CallState, h as Chat, i as ChatBackend, j as ChatDraft, k as ChatKind, l as ChatMember, m as ChatMessage, n as ChatMessageKind, o as ChatRole, p as ChatState, q as CommsPrefs, F as FlagsView, M as MagicLinkResult, r as MessageDirection, O as OAuthExchangeResult, P as PalbeAnalytics, s as PalbeAuth, t as PalbeCalls, u as PalbeConfig, v as PalbeFlags, w as PalbeMessaging, x as PalbeOAuthConfig, y as PalbeRealtime, z as PresenceState, R as RealtimeChannel, B as RealtimeConnectionState, D as RealtimeHandler, E as RealtimePayload, G as RealtimeStatus, H as RealtimeStatusSnapshot, I as RealtimeSubscription, J as ResolvedReply, S as SentReceipt, U as Unsubscribe } from './analytics-facade-Cq7R-VH_.js';
2
2
  export { B as BackendError, a as BackendErrorKind, F as FieldError, i as isBackendError } from './errors-fDoNdTrJ.js';
3
- export { C as CallOptions, P as PB, U as UploadOptions, a as UploadProgress, p as pb } from './pb-BtYdWClg.js';
3
+ export { C as CallOptions, P as PB, U as UploadOptions, a as UploadProgress, p as pb } from './pb-DSJiH1uV.js';
4
4
  export { P as PersistedSession, S as SessionStorageAdapter, l as localStorageSessionStorage, m as memorySessionStorage } from './storage-BPaeSG8K.js';
5
5
  export { F as FlagValue } from './pooled-flags-Bwq4usn0.js';
6
6
  import 'livekit-client';
@@ -207,6 +207,6 @@ interface MlsGlue {
207
207
  */
208
208
  declare function initMls(): Promise<MlsGlue>;
209
209
 
210
- declare const VERSION = "1.2.1";
210
+ declare const VERSION = "1.3.0";
211
211
 
212
212
  export { VERSION, initMls };
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  localStorageSessionStorage,
14
14
  memorySessionStorage,
15
15
  pb
16
- } from "./chunk-KJXRY4S3.js";
16
+ } from "./chunk-UX43AB4W.js";
17
17
  export {
18
18
  BackendError,
19
19
  PalbeAnalytics,
package/dist/internal.cjs CHANGED
@@ -2612,6 +2612,88 @@ var PalbeFlags = class {
2612
2612
  }
2613
2613
  };
2614
2614
 
2615
+ // src/messaging/edit-fold.ts
2616
+ function orderLt(aEpoch, aSeq, bEpoch, bSeq) {
2617
+ if (aEpoch !== bEpoch) return aEpoch < bEpoch;
2618
+ return aSeq < bSeq;
2619
+ }
2620
+ function orderEq(aEpoch, aSeq, bEpoch, bSeq) {
2621
+ return aEpoch === bEpoch && aSeq === bSeq;
2622
+ }
2623
+ var EditFold = class {
2624
+ // target → winning edit state
2625
+ states = /* @__PURE__ */ new Map();
2626
+ // dedup of real wire events that reached (and were resolvable enough to evaluate at) the fold
2627
+ seenEvents = /* @__PURE__ */ new Set();
2628
+ // events parked because target/author or sender was unresolved at ingest time
2629
+ held = [];
2630
+ // targets that have had ≥1 valid edit applied (write-once)
2631
+ editedTargets = /* @__PURE__ */ new Set();
2632
+ /**
2633
+ * Ingest one edit. `authorOfTarget` resolves the target message's AUTHOR userId
2634
+ * (null = target unknown/dangling → HOLD).
2635
+ */
2636
+ ingest(e, authorOfTarget) {
2637
+ const author = authorOfTarget(e.targetClientMsgId);
2638
+ if (author === null) {
2639
+ this.holdIfNew(e);
2640
+ return;
2641
+ }
2642
+ if (e.editorUserId === null) {
2643
+ this.holdIfNew(e);
2644
+ return;
2645
+ }
2646
+ if (e.editorUserId !== author) return;
2647
+ if (this.seenEvents.has(e.eventClientMsgId)) return;
2648
+ this.seenEvents.add(e.eventClientMsgId);
2649
+ const prev = this.states.get(e.targetClientMsgId);
2650
+ if (prev !== void 0) {
2651
+ if (orderLt(e.epoch, e.serverSeq, prev.orderEpoch, prev.orderSeq)) return;
2652
+ if (orderEq(e.epoch, e.serverSeq, prev.orderEpoch, prev.orderSeq) && e.eventClientMsgId <= prev.lastEventId) {
2653
+ return;
2654
+ }
2655
+ }
2656
+ this.states.set(e.targetClientMsgId, {
2657
+ orderEpoch: e.epoch,
2658
+ orderSeq: e.serverSeq,
2659
+ lastEventId: e.eventClientMsgId,
2660
+ text: e.newText
2661
+ });
2662
+ this.editedTargets.add(e.targetClientMsgId);
2663
+ }
2664
+ /**
2665
+ * Park an event for later re-attempt, deduping held re-deliveries by
2666
+ * eventClientMsgId so a repeatedly-delivered unresolvable edit is held exactly
2667
+ * once (and never double-applies when it finally resolves on reevaluate).
2668
+ */
2669
+ holdIfNew(e) {
2670
+ if (this.held.some((h) => h.eventClientMsgId === e.eventClientMsgId)) return;
2671
+ this.held.push(e);
2672
+ }
2673
+ /** The winning edit text for a target, or null if no valid edit has applied. */
2674
+ text(targetClientMsgId) {
2675
+ return this.states.get(targetClientMsgId)?.text ?? null;
2676
+ }
2677
+ /** Write-once: true once any valid edit applied to the target. */
2678
+ isEdited(targetClientMsgId) {
2679
+ return this.editedTargets.has(targetClientMsgId);
2680
+ }
2681
+ /**
2682
+ * Re-run HELD edits when the roster/target newly resolves (call on member/roster
2683
+ * change and when a target message arrives). Clears `held` and re-ingests each
2684
+ * event with the fresh `authorOfTarget` — events that still don't resolve are
2685
+ * simply re-held; events that now resolve fold via the normal LWW path.
2686
+ * Idempotent: re-ingest is deduped by `seenEvents` (applied events) and by
2687
+ * `holdIfNew` (still-held events), so reevaluating repeatedly can neither
2688
+ * double-apply nor lose an edit.
2689
+ */
2690
+ reevaluateHeld(authorOfTarget) {
2691
+ const pending = this.held;
2692
+ this.held = [];
2693
+ for (const e of pending) this.ingest(e, authorOfTarget);
2694
+ }
2695
+ };
2696
+
2615
2697
  // src/messaging/util.ts
2616
2698
  function toBase64(bytes) {
2617
2699
  if (typeof Buffer !== "undefined") {
@@ -2751,6 +2833,17 @@ async function listDevices(rt, userId) {
2751
2833
  }
2752
2834
 
2753
2835
  // src/messaging/group-messaging.ts
2836
+ function encodeEdit(args) {
2837
+ return encodeUtf8(
2838
+ JSON.stringify({
2839
+ v: 1,
2840
+ type: "edit",
2841
+ client_msg_id: args.clientMsgId,
2842
+ target_client_msg_id: args.targetClientMsgId,
2843
+ new_text: args.newText
2844
+ })
2845
+ );
2846
+ }
2754
2847
  function encodeReaction(args) {
2755
2848
  return encodeUtf8(
2756
2849
  JSON.stringify({
@@ -2790,6 +2883,18 @@ function decodeEnvelope(bytes) {
2790
2883
  }
2791
2884
  };
2792
2885
  }
2886
+ if (typeof o === "object" && o !== null && o.type === "edit") {
2887
+ return {
2888
+ type: "edit",
2889
+ text: null,
2890
+ clientMsgId: o.client_msg_id ?? "",
2891
+ replyTo: null,
2892
+ edit: {
2893
+ targetClientMsgId: o.target_client_msg_id ?? "",
2894
+ newText: o.new_text ?? ""
2895
+ }
2896
+ };
2897
+ }
2793
2898
  if (typeof o === "object" && o !== null && o.type === "text" && typeof o.v === "number") {
2794
2899
  return {
2795
2900
  type: "text",
@@ -3104,6 +3209,54 @@ var GroupMessaging = class {
3104
3209
  clientMsgId: args.clientMsgId
3105
3210
  };
3106
3211
  }
3212
+ /** Send an edit (edit-by-supersession on a target message). Encrypts a
3213
+ * `type:'edit'` envelope at the current epoch and sends through the SAME MLS
3214
+ * application path as `sendText` (the server stays blind — an edit is just
3215
+ * another application message). Persists the outgoing edit row so it re-folds
3216
+ * onto its target's text after a reload (the own-send half of the reload
3217
+ * parity). NEVER rebases (epoch-bound like any application message). */
3218
+ async sendEdit(group, args) {
3219
+ const plaintext = encodeEdit({
3220
+ clientMsgId: args.clientMsgId,
3221
+ targetClientMsgId: args.targetClientMsgId,
3222
+ newText: args.newText
3223
+ });
3224
+ const ct = await this.engine.encryptApplication(fromBase64(group.rfcGroupId), plaintext);
3225
+ const body = {
3226
+ ciphertext_b64: toBase64(ct),
3227
+ client_idem_key: randomId()
3228
+ };
3229
+ const wire = await palbeRequest(
3230
+ this.rt,
3231
+ "POST",
3232
+ MessagingPaths.groupMessages(group.displayId),
3233
+ { body }
3234
+ );
3235
+ const stored = {
3236
+ id: `${group.rfcGroupId}#${wire.server_seq}`,
3237
+ direction: "outgoing",
3238
+ text: null,
3239
+ senderDeviceId: this.selfDeviceId,
3240
+ epoch: wire.epoch,
3241
+ serverSeq: wire.server_seq,
3242
+ at: Date.now(),
3243
+ clientMsgId: args.clientMsgId,
3244
+ replyTo: null,
3245
+ envelopeType: "edit",
3246
+ edit: {
3247
+ targetClientMsgId: args.targetClientMsgId,
3248
+ newText: args.newText
3249
+ }
3250
+ };
3251
+ try {
3252
+ await this.messageStore.append(group.rfcGroupId, stored);
3253
+ } catch {
3254
+ }
3255
+ return {
3256
+ receipt: { serverSeq: wire.server_seq, epoch: wire.epoch },
3257
+ clientMsgId: args.clientMsgId
3258
+ };
3259
+ }
3107
3260
  // ── The rebase loop ──
3108
3261
  async commitWithRebase(rfcGroupId, build) {
3109
3262
  const gidBytes = fromBase64(rfcGroupId);
@@ -3240,6 +3393,14 @@ var Chat = class {
3240
3393
  byClientMsgId = /* @__PURE__ */ new Map();
3241
3394
  /** The single authoritative reaction fold for this chat (live + own-send + history). */
3242
3395
  reactionFold = new ReactionFold();
3396
+ /** The single authoritative edit fold for this chat (live + own-send + history). */
3397
+ editFold = new EditFold();
3398
+ /** Per-target BASE (original) text, so the rendered text is `edit ?? original`.
3399
+ * Seeded once per target (write-once base) so a 2nd own edit can't corrupt it. */
3400
+ originalTextByClientMsgId = /* @__PURE__ */ new Map();
3401
+ /** Per-target AUTHOR userId — the EditFold author-gate input (filled at bubble
3402
+ * projection time from senderUserId; '' = resolved-but-unknown peer). */
3403
+ authorByClientMsgId = /* @__PURE__ */ new Map();
3243
3404
  loadedEarliestSeq = null;
3244
3405
  historyLoaded = false;
3245
3406
  wired = false;
@@ -3340,13 +3501,17 @@ var Chat = class {
3340
3501
  senderUserId: m.senderUserId ?? ""
3341
3502
  });
3342
3503
  }
3504
+ if (m.clientMsgId) {
3505
+ this.seedEditBase(m.clientMsgId, m.text, m.senderUserId ?? "");
3506
+ }
3343
3507
  }
3508
+ this.editFold.reevaluateHeld(this.authorOfTarget);
3344
3509
  for (const m of incoming) {
3345
3510
  if (m.serverSeq <= 0) continue;
3346
3511
  const key = this.internalKey(m.serverSeq);
3347
3512
  if (this.seenKeys.has(key)) continue;
3348
3513
  this.seenKeys.add(key);
3349
- this.messageList.push(this.applyReactionTally(m));
3514
+ this.messageList.push(this.applyEditOverlay(this.applyReactionTally(m)));
3350
3515
  changed = true;
3351
3516
  this.loadedEarliestSeq = Math.min(this.loadedEarliestSeq ?? m.serverSeq, m.serverSeq);
3352
3517
  }
@@ -3387,6 +3552,22 @@ var Chat = class {
3387
3552
  }
3388
3553
  return;
3389
3554
  }
3555
+ if (incoming.envelopeType === "edit" && incoming.edit) {
3556
+ const editorUserId = direction === "outgoing" ? this.backend.selfUserId : senderUser;
3557
+ this.editFold.ingest(
3558
+ {
3559
+ targetClientMsgId: incoming.edit.targetClientMsgId,
3560
+ editorUserId,
3561
+ newText: incoming.edit.newText,
3562
+ epoch: incoming.epoch,
3563
+ serverSeq: incoming.serverSeq,
3564
+ eventClientMsgId: incoming.clientMsgId
3565
+ },
3566
+ this.authorOfTarget
3567
+ );
3568
+ this.recomputeEdit(incoming.edit.targetClientMsgId);
3569
+ return;
3570
+ }
3390
3571
  const incomingClientMsgId = incoming.clientMsgId;
3391
3572
  const incomingReplyRef = incoming.replyRef;
3392
3573
  let resolvedReplyTo = null;
@@ -3405,7 +3586,9 @@ var Chat = class {
3405
3586
  replyTo: resolvedReplyTo,
3406
3587
  // Attach any tally already folded for this message (a reaction that arrived
3407
3588
  // BEFORE its target — the dangling case — renders the moment the target lands).
3408
- reactions: incomingClientMsgId ? this.reactionFold.tally(incomingClientMsgId) : {}
3589
+ reactions: incomingClientMsgId ? this.reactionFold.tally(incomingClientMsgId) : {},
3590
+ // Default false; applyEditOverlay below folds any edit that arrived first.
3591
+ edited: false
3409
3592
  };
3410
3593
  if (incomingClientMsgId && incoming.text !== null) {
3411
3594
  this.byClientMsgId.set(incomingClientMsgId, {
@@ -3413,7 +3596,11 @@ var Chat = class {
3413
3596
  senderUserId: senderUser ?? ""
3414
3597
  });
3415
3598
  }
3416
- this.messageList.push(msg);
3599
+ if (incomingClientMsgId) {
3600
+ this.seedEditBase(incomingClientMsgId, incoming.text, senderUser);
3601
+ this.editFold.reevaluateHeld(this.authorOfTarget);
3602
+ }
3603
+ this.messageList.push(this.applyEditOverlay(msg));
3417
3604
  this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
3418
3605
  this.loadedEarliestSeq = Math.min(
3419
3606
  this.loadedEarliestSeq ?? incoming.serverSeq,
@@ -3421,6 +3608,19 @@ var Chat = class {
3421
3608
  );
3422
3609
  this.emit();
3423
3610
  }
3611
+ /** The EditFold author-gate input: the target message's resolved author userId
3612
+ * (null = target unknown/dangling → the fold HOLDs). Captured as a bound arrow
3613
+ * so it can be passed to the pure EditFold. */
3614
+ authorOfTarget = (targetClientMsgId) => this.authorByClientMsgId.get(targetClientMsgId) ?? null;
3615
+ /** Seed the per-target base text + author for the edit fold. Base is write-once
3616
+ * (a later own/peer edit must not overwrite the original we render against). The
3617
+ * author is (re)recorded whenever a non-empty resolution is available. */
3618
+ seedEditBase(clientMsgId, text, author) {
3619
+ if (!this.originalTextByClientMsgId.has(clientMsgId)) {
3620
+ this.originalTextByClientMsgId.set(clientMsgId, text);
3621
+ }
3622
+ if (author !== null) this.authorByClientMsgId.set(clientMsgId, author);
3623
+ }
3424
3624
  /**
3425
3625
  * Rebuild the target message's `reactions` from the authoritative fold and
3426
3626
  * re-emit. No-op when the target isn't present yet (its tally is attached the
@@ -3450,6 +3650,46 @@ var Chat = class {
3450
3650
  if (sameReactions(m.reactions, tally)) return m;
3451
3651
  return { ...m, reactions: tally };
3452
3652
  }
3653
+ /**
3654
+ * Rebuild the target message's rendered `text` + `edited` flag from the
3655
+ * authoritative edit fold and re-emit, PRESERVING `.reactions` and `.replyTo`
3656
+ * (the reaction-polish lesson — never clobber). text = `editFold.text(cid) ??
3657
+ * base`; base is the seeded original so a forged/ignored edit leaves it intact.
3658
+ * No-op when the target isn't present yet (the fold already recorded it; the
3659
+ * overlay applies the moment the target lands) or when unchanged.
3660
+ */
3661
+ recomputeEdit(targetClientMsgId) {
3662
+ if (!targetClientMsgId) return;
3663
+ const editText = this.editFold.text(targetClientMsgId);
3664
+ const foldEdited = this.editFold.isEdited(targetClientMsgId);
3665
+ let changed = false;
3666
+ this.messageList = this.messageList.map((m) => {
3667
+ if (m.clientMsgId !== targetClientMsgId) return m;
3668
+ const base = this.originalTextByClientMsgId.has(targetClientMsgId) ? this.originalTextByClientMsgId.get(targetClientMsgId) ?? null : m.text;
3669
+ const text = editText ?? base;
3670
+ const edited = foldEdited || m.edited;
3671
+ if (m.text === text && m.edited === edited) return m;
3672
+ changed = true;
3673
+ return { ...m, text, edited };
3674
+ });
3675
+ if (changed) this.emit();
3676
+ }
3677
+ /**
3678
+ * Overlay the authoritative edit fold's winning text + flag onto a message as it
3679
+ * is appended/merged. The fold WINS when it has an edit for this target;
3680
+ * otherwise the upstream `text`/`edited` (e.g. the coordinator's page-local
3681
+ * history fold) is preserved. PRESERVES reactions + replyTo.
3682
+ */
3683
+ applyEditOverlay(m) {
3684
+ if (!m.clientMsgId) return m;
3685
+ const editText = this.editFold.text(m.clientMsgId);
3686
+ const foldEdited = this.editFold.isEdited(m.clientMsgId);
3687
+ if (editText === null && !foldEdited) return m;
3688
+ const text = editText ?? m.text;
3689
+ const edited = foldEdited || m.edited;
3690
+ if (m.text === text && m.edited === edited) return m;
3691
+ return { ...m, text, edited };
3692
+ }
3453
3693
  /** @internal — called by the backend's conv subscription. */
3454
3694
  applyConv(event, payload) {
3455
3695
  const userId = typeof payload.user_id === "string" ? payload.user_id : null;
@@ -3504,6 +3744,8 @@ var Chat = class {
3504
3744
  this.memberCache = m;
3505
3745
  this.emit();
3506
3746
  }
3747
+ this.editFold.reevaluateHeld(this.authorOfTarget);
3748
+ for (const cid of this.originalTextByClientMsgId.keys()) this.recomputeEdit(cid);
3507
3749
  }
3508
3750
  seedMembersFromGroup(group) {
3509
3751
  const seed = [
@@ -3582,6 +3824,7 @@ var Chat = class {
3582
3824
  this.seenKeys.add(key);
3583
3825
  if (clientMsgId) {
3584
3826
  this.byClientMsgId.set(clientMsgId, { text, senderUserId: this.backend.selfUserId });
3827
+ this.seedEditBase(clientMsgId, text, this.backend.selfUserId);
3585
3828
  }
3586
3829
  this.messageList.push({
3587
3830
  id: this.publicId(receipt.serverSeq),
@@ -3595,7 +3838,9 @@ var Chat = class {
3595
3838
  replyTo: resolvedReplyTo,
3596
3839
  // Attach any tally already folded for this own-sent message (rare, but keeps
3597
3840
  // the dangling-target invariant uniform across every append path).
3598
- reactions: clientMsgId ? this.reactionFold.tally(clientMsgId) : {}
3841
+ reactions: clientMsgId ? this.reactionFold.tally(clientMsgId) : {},
3842
+ // Own-sent edits fold via edit() after the fact; new sends start unedited.
3843
+ edited: false
3599
3844
  });
3600
3845
  this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
3601
3846
  this.emit();
@@ -3676,6 +3921,37 @@ var Chat = class {
3676
3921
  });
3677
3922
  this.recomputeReactions(message.clientMsgId);
3678
3923
  }
3924
+ // ── Edit ──
3925
+ /** Edit an own text message (edit-by-supersession). No-op if the message isn't
3926
+ * editable (empty clientMsgId, or not a `text` kind). The edit folds locally
3927
+ * with the server receipt's `(epoch, serverSeq)` so the target's text updates
3928
+ * instantly; the durable echo on the next pump is a fold no-op (dedup on the
3929
+ * SAME wire clientMsgId). Never appends a bubble; PRESERVES the target's
3930
+ * reactions + reply context. Only the original author's edits count — for an own
3931
+ * message self IS the author, so the author-gate passes. */
3932
+ async edit(message, newText) {
3933
+ if (!message.clientMsgId || message.kind !== "text") return;
3934
+ const group = await this.materializeIfNeeded();
3935
+ const clientMsgId = mintClientMsgId();
3936
+ const { receipt } = await this.backend.sendEdit(group, {
3937
+ clientMsgId,
3938
+ targetClientMsgId: message.clientMsgId,
3939
+ newText
3940
+ });
3941
+ this.seedEditBase(message.clientMsgId, message.text, this.backend.selfUserId);
3942
+ this.editFold.ingest(
3943
+ {
3944
+ targetClientMsgId: message.clientMsgId,
3945
+ editorUserId: this.backend.selfUserId,
3946
+ newText,
3947
+ epoch: receipt.epoch,
3948
+ serverSeq: receipt.serverSeq,
3949
+ eventClientMsgId: clientMsgId
3950
+ },
3951
+ this.authorOfTarget
3952
+ );
3953
+ this.recomputeEdit(message.clientMsgId);
3954
+ }
3679
3955
  };
3680
3956
  function sameReactions(a, b) {
3681
3957
  const ak = Object.keys(a);
@@ -3855,6 +4131,7 @@ var MessageDeliverySource = class {
3855
4131
  const decoded = decodeEnvelope(received.data);
3856
4132
  const { text, clientMsgId, replyTo } = decoded;
3857
4133
  const isReaction = decoded.type === "reaction" && decoded.reaction != null;
4134
+ const isEdit = decoded.type === "edit" && decoded.edit != null;
3858
4135
  const senderDeviceId = row.sender_device_id ?? decodeSenderDeviceId(received.sender);
3859
4136
  const stored = {
3860
4137
  id: `${group.rfcGroupId}#${row.server_seq}`,
@@ -3882,6 +4159,17 @@ var MessageDeliverySource = class {
3882
4159
  emoji: decoded.reaction.emoji,
3883
4160
  op: decoded.reaction.op
3884
4161
  }
4162
+ } : {},
4163
+ // Thread the edit discriminator + new text through the persisted row so an
4164
+ // edit folded LIVE re-folds onto its target after a reload (the reload-parity
4165
+ // boundary — mirrors iOS T3). Omitted for non-edits → old rows hydrate as
4166
+ // `'text'`/no-edit (backward-compat).
4167
+ ...isEdit && decoded.edit ? {
4168
+ envelopeType: "edit",
4169
+ edit: {
4170
+ targetClientMsgId: decoded.edit.targetClientMsgId,
4171
+ newText: decoded.edit.newText
4172
+ }
3885
4173
  } : {}
3886
4174
  };
3887
4175
  try {
@@ -3900,7 +4188,8 @@ var MessageDeliverySource = class {
3900
4188
  clientMsgId,
3901
4189
  replyRef: replyTo,
3902
4190
  envelopeType: decoded.type ?? "text",
3903
- reaction: isReaction ? decoded.reaction : null
4191
+ reaction: isReaction ? decoded.reaction : null,
4192
+ edit: isEdit ? decoded.edit : null
3904
4193
  });
3905
4194
  return true;
3906
4195
  }
@@ -5908,6 +6197,10 @@ var MessagingCoordinator = class {
5908
6197
  const r = await this.resolve();
5909
6198
  return r.groups.sendReaction(group, args);
5910
6199
  }
6200
+ async sendEdit(group, args) {
6201
+ const r = await this.resolve();
6202
+ return r.groups.sendEdit(group, args);
6203
+ }
5911
6204
  async history(group, limit, before) {
5912
6205
  const r = await this.resolve();
5913
6206
  const rows = await r.messageStore.history(group.rfcGroupId, limit, before);
@@ -6004,6 +6297,32 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
6004
6297
  eventClientMsgId: s.clientMsgId ?? `${s.id}`
6005
6298
  });
6006
6299
  }
6300
+ const editFold = new EditFold();
6301
+ const authorByClientMsgId = /* @__PURE__ */ new Map();
6302
+ for (const s of rows) {
6303
+ if (s.envelopeType === "reaction" || s.envelopeType === "edit") continue;
6304
+ const cid = s.clientMsgId ?? "";
6305
+ if (!cid) continue;
6306
+ const author = s.direction === "outgoing" ? selfUserId : resolveActor?.(s.senderDeviceId ?? null);
6307
+ if (author != null) authorByClientMsgId.set(cid, author);
6308
+ }
6309
+ const authorOfTarget = (cid) => authorByClientMsgId.get(cid) ?? null;
6310
+ for (const s of rows) {
6311
+ if (s.envelopeType !== "edit" || !s.edit) continue;
6312
+ const editor = s.direction === "outgoing" ? selfUserId : resolveActor?.(s.senderDeviceId ?? null) ?? null;
6313
+ editFold.ingest(
6314
+ {
6315
+ targetClientMsgId: s.edit.targetClientMsgId,
6316
+ editorUserId: editor,
6317
+ newText: s.edit.newText,
6318
+ epoch: s.epoch,
6319
+ serverSeq: s.serverSeq,
6320
+ eventClientMsgId: s.clientMsgId ?? `${s.id}`
6321
+ },
6322
+ authorOfTarget
6323
+ );
6324
+ }
6325
+ editFold.reevaluateHeld(authorOfTarget);
6007
6326
  const lookup = /* @__PURE__ */ new Map();
6008
6327
  for (const s of rows) {
6009
6328
  if (s.envelopeType === "reaction") continue;
@@ -6015,7 +6334,7 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
6015
6334
  }
6016
6335
  const out = [];
6017
6336
  for (const s of rows) {
6018
- if (s.envelopeType === "reaction") continue;
6337
+ if (s.envelopeType === "reaction" || s.envelopeType === "edit") continue;
6019
6338
  const clientMsgId = s.clientMsgId ?? "";
6020
6339
  let replyTo = null;
6021
6340
  if (s.replyTo) {
@@ -6031,17 +6350,20 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
6031
6350
  };
6032
6351
  replyTo = resolveReply(ref, (id) => lookup.get(id) ?? null);
6033
6352
  }
6353
+ const editText = clientMsgId ? editFold.text(clientMsgId) : null;
6354
+ const edited = clientMsgId ? editFold.isEdited(clientMsgId) : false;
6034
6355
  out.push({
6035
6356
  id: `${displayId}#${s.serverSeq}`,
6036
6357
  kind: s.text != null ? "text" : "system",
6037
6358
  direction: s.direction,
6038
6359
  senderUserId: s.direction === "outgoing" ? selfUserId : null,
6039
- text: s.text,
6360
+ text: editText ?? s.text,
6040
6361
  serverSeq: s.serverSeq,
6041
6362
  sentAt: new Date(s.at),
6042
6363
  clientMsgId,
6043
6364
  replyTo,
6044
- reactions: clientMsgId ? fold.tally(clientMsgId) : {}
6365
+ reactions: clientMsgId ? fold.tally(clientMsgId) : {},
6366
+ edited
6045
6367
  });
6046
6368
  }
6047
6369
  return out;
@@ -6779,7 +7101,7 @@ function defaultSessionStorage(key) {
6779
7101
  }
6780
7102
 
6781
7103
  // src/version.ts
6782
- var VERSION = "1.2.1";
7104
+ var VERSION = "1.3.0";
6783
7105
 
6784
7106
  // src/runtime.ts
6785
7107
  function buildRuntime(config) {