@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.
@@ -1,7 +1,7 @@
1
- import { u as PalbeConfig, J as PalbeRuntime } from './analytics-facade-Ct3A1zop.cjs';
2
- export { K as buildRuntime } from './analytics-facade-Ct3A1zop.cjs';
1
+ import { u as PalbeConfig, K as PalbeRuntime } from './analytics-facade-B8UATgS4.cjs';
2
+ export { L as buildRuntime } from './analytics-facade-B8UATgS4.cjs';
3
3
  import { B as BackendError } from './errors-fDoNdTrJ.cjs';
4
- export { P as PB, c as createBoundClient } from './pb-BlIfgBG-.cjs';
4
+ export { P as PB, c as createBoundClient } from './pb-DivIiUGR.cjs';
5
5
  import 'livekit-client';
6
6
  import './storage-BPaeSG8K.cjs';
7
7
  import './pooled-flags-Bwq4usn0.js';
@@ -1,7 +1,7 @@
1
- import { u as PalbeConfig, J as PalbeRuntime } from './analytics-facade-C93tr7dA.js';
2
- export { K as buildRuntime } from './analytics-facade-C93tr7dA.js';
1
+ import { u as PalbeConfig, K as PalbeRuntime } from './analytics-facade-Cq7R-VH_.js';
2
+ export { L as buildRuntime } from './analytics-facade-Cq7R-VH_.js';
3
3
  import { B as BackendError } from './errors-fDoNdTrJ.js';
4
- export { P as PB, c as createBoundClient } from './pb-BtYdWClg.js';
4
+ export { P as PB, c as createBoundClient } from './pb-DSJiH1uV.js';
5
5
  import 'livekit-client';
6
6
  import './storage-BPaeSG8K.js';
7
7
  import './pooled-flags-Bwq4usn0.js';
package/dist/internal.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  createBoundClient,
7
7
  getRuntime,
8
8
  onConfigured
9
- } from "./chunk-OZLVL7G2.js";
9
+ } from "./chunk-UX43AB4W.js";
10
10
  export {
11
11
  __configure,
12
12
  __registerNamespaces,
@@ -2413,6 +2413,88 @@ var PalbeFlags = class {
2413
2413
  }
2414
2414
  };
2415
2415
 
2416
+ // src/messaging/edit-fold.ts
2417
+ function orderLt(aEpoch, aSeq, bEpoch, bSeq) {
2418
+ if (aEpoch !== bEpoch) return aEpoch < bEpoch;
2419
+ return aSeq < bSeq;
2420
+ }
2421
+ function orderEq(aEpoch, aSeq, bEpoch, bSeq) {
2422
+ return aEpoch === bEpoch && aSeq === bSeq;
2423
+ }
2424
+ var EditFold = class {
2425
+ // target → winning edit state
2426
+ states = /* @__PURE__ */ new Map();
2427
+ // dedup of real wire events that reached (and were resolvable enough to evaluate at) the fold
2428
+ seenEvents = /* @__PURE__ */ new Set();
2429
+ // events parked because target/author or sender was unresolved at ingest time
2430
+ held = [];
2431
+ // targets that have had ≥1 valid edit applied (write-once)
2432
+ editedTargets = /* @__PURE__ */ new Set();
2433
+ /**
2434
+ * Ingest one edit. `authorOfTarget` resolves the target message's AUTHOR userId
2435
+ * (null = target unknown/dangling → HOLD).
2436
+ */
2437
+ ingest(e, authorOfTarget) {
2438
+ const author = authorOfTarget(e.targetClientMsgId);
2439
+ if (author === null) {
2440
+ this.holdIfNew(e);
2441
+ return;
2442
+ }
2443
+ if (e.editorUserId === null) {
2444
+ this.holdIfNew(e);
2445
+ return;
2446
+ }
2447
+ if (e.editorUserId !== author) return;
2448
+ if (this.seenEvents.has(e.eventClientMsgId)) return;
2449
+ this.seenEvents.add(e.eventClientMsgId);
2450
+ const prev = this.states.get(e.targetClientMsgId);
2451
+ if (prev !== void 0) {
2452
+ if (orderLt(e.epoch, e.serverSeq, prev.orderEpoch, prev.orderSeq)) return;
2453
+ if (orderEq(e.epoch, e.serverSeq, prev.orderEpoch, prev.orderSeq) && e.eventClientMsgId <= prev.lastEventId) {
2454
+ return;
2455
+ }
2456
+ }
2457
+ this.states.set(e.targetClientMsgId, {
2458
+ orderEpoch: e.epoch,
2459
+ orderSeq: e.serverSeq,
2460
+ lastEventId: e.eventClientMsgId,
2461
+ text: e.newText
2462
+ });
2463
+ this.editedTargets.add(e.targetClientMsgId);
2464
+ }
2465
+ /**
2466
+ * Park an event for later re-attempt, deduping held re-deliveries by
2467
+ * eventClientMsgId so a repeatedly-delivered unresolvable edit is held exactly
2468
+ * once (and never double-applies when it finally resolves on reevaluate).
2469
+ */
2470
+ holdIfNew(e) {
2471
+ if (this.held.some((h) => h.eventClientMsgId === e.eventClientMsgId)) return;
2472
+ this.held.push(e);
2473
+ }
2474
+ /** The winning edit text for a target, or null if no valid edit has applied. */
2475
+ text(targetClientMsgId) {
2476
+ return this.states.get(targetClientMsgId)?.text ?? null;
2477
+ }
2478
+ /** Write-once: true once any valid edit applied to the target. */
2479
+ isEdited(targetClientMsgId) {
2480
+ return this.editedTargets.has(targetClientMsgId);
2481
+ }
2482
+ /**
2483
+ * Re-run HELD edits when the roster/target newly resolves (call on member/roster
2484
+ * change and when a target message arrives). Clears `held` and re-ingests each
2485
+ * event with the fresh `authorOfTarget` — events that still don't resolve are
2486
+ * simply re-held; events that now resolve fold via the normal LWW path.
2487
+ * Idempotent: re-ingest is deduped by `seenEvents` (applied events) and by
2488
+ * `holdIfNew` (still-held events), so reevaluating repeatedly can neither
2489
+ * double-apply nor lose an edit.
2490
+ */
2491
+ reevaluateHeld(authorOfTarget) {
2492
+ const pending = this.held;
2493
+ this.held = [];
2494
+ for (const e of pending) this.ingest(e, authorOfTarget);
2495
+ }
2496
+ };
2497
+
2416
2498
  // src/messaging/util.ts
2417
2499
  function toBase64(bytes) {
2418
2500
  if (typeof Buffer !== "undefined") {
@@ -2552,6 +2634,17 @@ async function listDevices(rt, userId) {
2552
2634
  }
2553
2635
 
2554
2636
  // src/messaging/group-messaging.ts
2637
+ function encodeEdit(args) {
2638
+ return encodeUtf8(
2639
+ JSON.stringify({
2640
+ v: 1,
2641
+ type: "edit",
2642
+ client_msg_id: args.clientMsgId,
2643
+ target_client_msg_id: args.targetClientMsgId,
2644
+ new_text: args.newText
2645
+ })
2646
+ );
2647
+ }
2555
2648
  function encodeReaction(args) {
2556
2649
  return encodeUtf8(
2557
2650
  JSON.stringify({
@@ -2591,6 +2684,18 @@ function decodeEnvelope(bytes) {
2591
2684
  }
2592
2685
  };
2593
2686
  }
2687
+ if (typeof o === "object" && o !== null && o.type === "edit") {
2688
+ return {
2689
+ type: "edit",
2690
+ text: null,
2691
+ clientMsgId: o.client_msg_id ?? "",
2692
+ replyTo: null,
2693
+ edit: {
2694
+ targetClientMsgId: o.target_client_msg_id ?? "",
2695
+ newText: o.new_text ?? ""
2696
+ }
2697
+ };
2698
+ }
2594
2699
  if (typeof o === "object" && o !== null && o.type === "text" && typeof o.v === "number") {
2595
2700
  return {
2596
2701
  type: "text",
@@ -2905,6 +3010,54 @@ var GroupMessaging = class {
2905
3010
  clientMsgId: args.clientMsgId
2906
3011
  };
2907
3012
  }
3013
+ /** Send an edit (edit-by-supersession on a target message). Encrypts a
3014
+ * `type:'edit'` envelope at the current epoch and sends through the SAME MLS
3015
+ * application path as `sendText` (the server stays blind — an edit is just
3016
+ * another application message). Persists the outgoing edit row so it re-folds
3017
+ * onto its target's text after a reload (the own-send half of the reload
3018
+ * parity). NEVER rebases (epoch-bound like any application message). */
3019
+ async sendEdit(group, args) {
3020
+ const plaintext = encodeEdit({
3021
+ clientMsgId: args.clientMsgId,
3022
+ targetClientMsgId: args.targetClientMsgId,
3023
+ newText: args.newText
3024
+ });
3025
+ const ct = await this.engine.encryptApplication(fromBase64(group.rfcGroupId), plaintext);
3026
+ const body = {
3027
+ ciphertext_b64: toBase64(ct),
3028
+ client_idem_key: randomId()
3029
+ };
3030
+ const wire = await palbeRequest(
3031
+ this.rt,
3032
+ "POST",
3033
+ MessagingPaths.groupMessages(group.displayId),
3034
+ { body }
3035
+ );
3036
+ const stored = {
3037
+ id: `${group.rfcGroupId}#${wire.server_seq}`,
3038
+ direction: "outgoing",
3039
+ text: null,
3040
+ senderDeviceId: this.selfDeviceId,
3041
+ epoch: wire.epoch,
3042
+ serverSeq: wire.server_seq,
3043
+ at: Date.now(),
3044
+ clientMsgId: args.clientMsgId,
3045
+ replyTo: null,
3046
+ envelopeType: "edit",
3047
+ edit: {
3048
+ targetClientMsgId: args.targetClientMsgId,
3049
+ newText: args.newText
3050
+ }
3051
+ };
3052
+ try {
3053
+ await this.messageStore.append(group.rfcGroupId, stored);
3054
+ } catch {
3055
+ }
3056
+ return {
3057
+ receipt: { serverSeq: wire.server_seq, epoch: wire.epoch },
3058
+ clientMsgId: args.clientMsgId
3059
+ };
3060
+ }
2908
3061
  // ── The rebase loop ──
2909
3062
  async commitWithRebase(rfcGroupId, build) {
2910
3063
  const gidBytes = fromBase64(rfcGroupId);
@@ -3041,6 +3194,14 @@ var Chat = class {
3041
3194
  byClientMsgId = /* @__PURE__ */ new Map();
3042
3195
  /** The single authoritative reaction fold for this chat (live + own-send + history). */
3043
3196
  reactionFold = new ReactionFold();
3197
+ /** The single authoritative edit fold for this chat (live + own-send + history). */
3198
+ editFold = new EditFold();
3199
+ /** Per-target BASE (original) text, so the rendered text is `edit ?? original`.
3200
+ * Seeded once per target (write-once base) so a 2nd own edit can't corrupt it. */
3201
+ originalTextByClientMsgId = /* @__PURE__ */ new Map();
3202
+ /** Per-target AUTHOR userId — the EditFold author-gate input (filled at bubble
3203
+ * projection time from senderUserId; '' = resolved-but-unknown peer). */
3204
+ authorByClientMsgId = /* @__PURE__ */ new Map();
3044
3205
  loadedEarliestSeq = null;
3045
3206
  historyLoaded = false;
3046
3207
  wired = false;
@@ -3141,13 +3302,17 @@ var Chat = class {
3141
3302
  senderUserId: m.senderUserId ?? ""
3142
3303
  });
3143
3304
  }
3305
+ if (m.clientMsgId) {
3306
+ this.seedEditBase(m.clientMsgId, m.text, m.senderUserId ?? "");
3307
+ }
3144
3308
  }
3309
+ this.editFold.reevaluateHeld(this.authorOfTarget);
3145
3310
  for (const m of incoming) {
3146
3311
  if (m.serverSeq <= 0) continue;
3147
3312
  const key = this.internalKey(m.serverSeq);
3148
3313
  if (this.seenKeys.has(key)) continue;
3149
3314
  this.seenKeys.add(key);
3150
- this.messageList.push(this.applyReactionTally(m));
3315
+ this.messageList.push(this.applyEditOverlay(this.applyReactionTally(m)));
3151
3316
  changed = true;
3152
3317
  this.loadedEarliestSeq = Math.min(this.loadedEarliestSeq ?? m.serverSeq, m.serverSeq);
3153
3318
  }
@@ -3188,6 +3353,22 @@ var Chat = class {
3188
3353
  }
3189
3354
  return;
3190
3355
  }
3356
+ if (incoming.envelopeType === "edit" && incoming.edit) {
3357
+ const editorUserId = direction === "outgoing" ? this.backend.selfUserId : senderUser;
3358
+ this.editFold.ingest(
3359
+ {
3360
+ targetClientMsgId: incoming.edit.targetClientMsgId,
3361
+ editorUserId,
3362
+ newText: incoming.edit.newText,
3363
+ epoch: incoming.epoch,
3364
+ serverSeq: incoming.serverSeq,
3365
+ eventClientMsgId: incoming.clientMsgId
3366
+ },
3367
+ this.authorOfTarget
3368
+ );
3369
+ this.recomputeEdit(incoming.edit.targetClientMsgId);
3370
+ return;
3371
+ }
3191
3372
  const incomingClientMsgId = incoming.clientMsgId;
3192
3373
  const incomingReplyRef = incoming.replyRef;
3193
3374
  let resolvedReplyTo = null;
@@ -3206,7 +3387,9 @@ var Chat = class {
3206
3387
  replyTo: resolvedReplyTo,
3207
3388
  // Attach any tally already folded for this message (a reaction that arrived
3208
3389
  // BEFORE its target — the dangling case — renders the moment the target lands).
3209
- reactions: incomingClientMsgId ? this.reactionFold.tally(incomingClientMsgId) : {}
3390
+ reactions: incomingClientMsgId ? this.reactionFold.tally(incomingClientMsgId) : {},
3391
+ // Default false; applyEditOverlay below folds any edit that arrived first.
3392
+ edited: false
3210
3393
  };
3211
3394
  if (incomingClientMsgId && incoming.text !== null) {
3212
3395
  this.byClientMsgId.set(incomingClientMsgId, {
@@ -3214,7 +3397,11 @@ var Chat = class {
3214
3397
  senderUserId: senderUser ?? ""
3215
3398
  });
3216
3399
  }
3217
- this.messageList.push(msg);
3400
+ if (incomingClientMsgId) {
3401
+ this.seedEditBase(incomingClientMsgId, incoming.text, senderUser);
3402
+ this.editFold.reevaluateHeld(this.authorOfTarget);
3403
+ }
3404
+ this.messageList.push(this.applyEditOverlay(msg));
3218
3405
  this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
3219
3406
  this.loadedEarliestSeq = Math.min(
3220
3407
  this.loadedEarliestSeq ?? incoming.serverSeq,
@@ -3222,6 +3409,19 @@ var Chat = class {
3222
3409
  );
3223
3410
  this.emit();
3224
3411
  }
3412
+ /** The EditFold author-gate input: the target message's resolved author userId
3413
+ * (null = target unknown/dangling → the fold HOLDs). Captured as a bound arrow
3414
+ * so it can be passed to the pure EditFold. */
3415
+ authorOfTarget = (targetClientMsgId) => this.authorByClientMsgId.get(targetClientMsgId) ?? null;
3416
+ /** Seed the per-target base text + author for the edit fold. Base is write-once
3417
+ * (a later own/peer edit must not overwrite the original we render against). The
3418
+ * author is (re)recorded whenever a non-empty resolution is available. */
3419
+ seedEditBase(clientMsgId, text, author) {
3420
+ if (!this.originalTextByClientMsgId.has(clientMsgId)) {
3421
+ this.originalTextByClientMsgId.set(clientMsgId, text);
3422
+ }
3423
+ if (author !== null) this.authorByClientMsgId.set(clientMsgId, author);
3424
+ }
3225
3425
  /**
3226
3426
  * Rebuild the target message's `reactions` from the authoritative fold and
3227
3427
  * re-emit. No-op when the target isn't present yet (its tally is attached the
@@ -3251,6 +3451,46 @@ var Chat = class {
3251
3451
  if (sameReactions(m.reactions, tally)) return m;
3252
3452
  return { ...m, reactions: tally };
3253
3453
  }
3454
+ /**
3455
+ * Rebuild the target message's rendered `text` + `edited` flag from the
3456
+ * authoritative edit fold and re-emit, PRESERVING `.reactions` and `.replyTo`
3457
+ * (the reaction-polish lesson — never clobber). text = `editFold.text(cid) ??
3458
+ * base`; base is the seeded original so a forged/ignored edit leaves it intact.
3459
+ * No-op when the target isn't present yet (the fold already recorded it; the
3460
+ * overlay applies the moment the target lands) or when unchanged.
3461
+ */
3462
+ recomputeEdit(targetClientMsgId) {
3463
+ if (!targetClientMsgId) return;
3464
+ const editText = this.editFold.text(targetClientMsgId);
3465
+ const foldEdited = this.editFold.isEdited(targetClientMsgId);
3466
+ let changed = false;
3467
+ this.messageList = this.messageList.map((m) => {
3468
+ if (m.clientMsgId !== targetClientMsgId) return m;
3469
+ const base = this.originalTextByClientMsgId.has(targetClientMsgId) ? this.originalTextByClientMsgId.get(targetClientMsgId) ?? null : m.text;
3470
+ const text = editText ?? base;
3471
+ const edited = foldEdited || m.edited;
3472
+ if (m.text === text && m.edited === edited) return m;
3473
+ changed = true;
3474
+ return { ...m, text, edited };
3475
+ });
3476
+ if (changed) this.emit();
3477
+ }
3478
+ /**
3479
+ * Overlay the authoritative edit fold's winning text + flag onto a message as it
3480
+ * is appended/merged. The fold WINS when it has an edit for this target;
3481
+ * otherwise the upstream `text`/`edited` (e.g. the coordinator's page-local
3482
+ * history fold) is preserved. PRESERVES reactions + replyTo.
3483
+ */
3484
+ applyEditOverlay(m) {
3485
+ if (!m.clientMsgId) return m;
3486
+ const editText = this.editFold.text(m.clientMsgId);
3487
+ const foldEdited = this.editFold.isEdited(m.clientMsgId);
3488
+ if (editText === null && !foldEdited) return m;
3489
+ const text = editText ?? m.text;
3490
+ const edited = foldEdited || m.edited;
3491
+ if (m.text === text && m.edited === edited) return m;
3492
+ return { ...m, text, edited };
3493
+ }
3254
3494
  /** @internal — called by the backend's conv subscription. */
3255
3495
  applyConv(event, payload) {
3256
3496
  const userId = typeof payload.user_id === "string" ? payload.user_id : null;
@@ -3305,6 +3545,8 @@ var Chat = class {
3305
3545
  this.memberCache = m;
3306
3546
  this.emit();
3307
3547
  }
3548
+ this.editFold.reevaluateHeld(this.authorOfTarget);
3549
+ for (const cid of this.originalTextByClientMsgId.keys()) this.recomputeEdit(cid);
3308
3550
  }
3309
3551
  seedMembersFromGroup(group) {
3310
3552
  const seed = [
@@ -3383,6 +3625,7 @@ var Chat = class {
3383
3625
  this.seenKeys.add(key);
3384
3626
  if (clientMsgId) {
3385
3627
  this.byClientMsgId.set(clientMsgId, { text, senderUserId: this.backend.selfUserId });
3628
+ this.seedEditBase(clientMsgId, text, this.backend.selfUserId);
3386
3629
  }
3387
3630
  this.messageList.push({
3388
3631
  id: this.publicId(receipt.serverSeq),
@@ -3396,7 +3639,9 @@ var Chat = class {
3396
3639
  replyTo: resolvedReplyTo,
3397
3640
  // Attach any tally already folded for this own-sent message (rare, but keeps
3398
3641
  // the dangling-target invariant uniform across every append path).
3399
- reactions: clientMsgId ? this.reactionFold.tally(clientMsgId) : {}
3642
+ reactions: clientMsgId ? this.reactionFold.tally(clientMsgId) : {},
3643
+ // Own-sent edits fold via edit() after the fact; new sends start unedited.
3644
+ edited: false
3400
3645
  });
3401
3646
  this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
3402
3647
  this.emit();
@@ -3477,6 +3722,37 @@ var Chat = class {
3477
3722
  });
3478
3723
  this.recomputeReactions(message.clientMsgId);
3479
3724
  }
3725
+ // ── Edit ──
3726
+ /** Edit an own text message (edit-by-supersession). No-op if the message isn't
3727
+ * editable (empty clientMsgId, or not a `text` kind). The edit folds locally
3728
+ * with the server receipt's `(epoch, serverSeq)` so the target's text updates
3729
+ * instantly; the durable echo on the next pump is a fold no-op (dedup on the
3730
+ * SAME wire clientMsgId). Never appends a bubble; PRESERVES the target's
3731
+ * reactions + reply context. Only the original author's edits count — for an own
3732
+ * message self IS the author, so the author-gate passes. */
3733
+ async edit(message, newText) {
3734
+ if (!message.clientMsgId || message.kind !== "text") return;
3735
+ const group = await this.materializeIfNeeded();
3736
+ const clientMsgId = mintClientMsgId();
3737
+ const { receipt } = await this.backend.sendEdit(group, {
3738
+ clientMsgId,
3739
+ targetClientMsgId: message.clientMsgId,
3740
+ newText
3741
+ });
3742
+ this.seedEditBase(message.clientMsgId, message.text, this.backend.selfUserId);
3743
+ this.editFold.ingest(
3744
+ {
3745
+ targetClientMsgId: message.clientMsgId,
3746
+ editorUserId: this.backend.selfUserId,
3747
+ newText,
3748
+ epoch: receipt.epoch,
3749
+ serverSeq: receipt.serverSeq,
3750
+ eventClientMsgId: clientMsgId
3751
+ },
3752
+ this.authorOfTarget
3753
+ );
3754
+ this.recomputeEdit(message.clientMsgId);
3755
+ }
3480
3756
  };
3481
3757
  function sameReactions(a, b) {
3482
3758
  const ak = Object.keys(a);
@@ -3519,6 +3795,11 @@ var MessageHub = class {
3519
3795
  };
3520
3796
  }
3521
3797
  };
3798
+ function decodeSenderDeviceId(sender) {
3799
+ if (sender.length === 0) return null;
3800
+ const id = decodeUtf8(sender);
3801
+ return id.length > 0 ? id : null;
3802
+ }
3522
3803
  var MessageDeliverySource = class {
3523
3804
  constructor(rt, engine, hub, registry, messageStore, deviceId, selfUserId) {
3524
3805
  this.rt = rt;
@@ -3651,11 +3932,13 @@ var MessageDeliverySource = class {
3651
3932
  const decoded = decodeEnvelope(received.data);
3652
3933
  const { text, clientMsgId, replyTo } = decoded;
3653
3934
  const isReaction = decoded.type === "reaction" && decoded.reaction != null;
3935
+ const isEdit = decoded.type === "edit" && decoded.edit != null;
3936
+ const senderDeviceId = row.sender_device_id ?? decodeSenderDeviceId(received.sender);
3654
3937
  const stored = {
3655
3938
  id: `${group.rfcGroupId}#${row.server_seq}`,
3656
3939
  direction: "incoming",
3657
3940
  text,
3658
- senderDeviceId: row.sender_device_id ?? null,
3941
+ senderDeviceId,
3659
3942
  epoch: row.epoch,
3660
3943
  serverSeq: row.server_seq,
3661
3944
  at: Date.now(),
@@ -3677,6 +3960,17 @@ var MessageDeliverySource = class {
3677
3960
  emoji: decoded.reaction.emoji,
3678
3961
  op: decoded.reaction.op
3679
3962
  }
3963
+ } : {},
3964
+ // Thread the edit discriminator + new text through the persisted row so an
3965
+ // edit folded LIVE re-folds onto its target after a reload (the reload-parity
3966
+ // boundary — mirrors iOS T3). Omitted for non-edits → old rows hydrate as
3967
+ // `'text'`/no-edit (backward-compat).
3968
+ ...isEdit && decoded.edit ? {
3969
+ envelopeType: "edit",
3970
+ edit: {
3971
+ targetClientMsgId: decoded.edit.targetClientMsgId,
3972
+ newText: decoded.edit.newText
3973
+ }
3680
3974
  } : {}
3681
3975
  };
3682
3976
  try {
@@ -3688,14 +3982,15 @@ var MessageDeliverySource = class {
3688
3982
  kind: "application",
3689
3983
  group,
3690
3984
  text,
3691
- senderDeviceId: row.sender_device_id ?? null,
3985
+ senderDeviceId,
3692
3986
  epoch: row.epoch,
3693
3987
  serverSeq: row.server_seq,
3694
3988
  receivedAt: /* @__PURE__ */ new Date(),
3695
3989
  clientMsgId,
3696
3990
  replyRef: replyTo,
3697
3991
  envelopeType: decoded.type ?? "text",
3698
- reaction: isReaction ? decoded.reaction : null
3992
+ reaction: isReaction ? decoded.reaction : null,
3993
+ edit: isEdit ? decoded.edit : null
3699
3994
  });
3700
3995
  return true;
3701
3996
  }
@@ -5703,6 +5998,10 @@ var MessagingCoordinator = class {
5703
5998
  const r = await this.resolve();
5704
5999
  return r.groups.sendReaction(group, args);
5705
6000
  }
6001
+ async sendEdit(group, args) {
6002
+ const r = await this.resolve();
6003
+ return r.groups.sendEdit(group, args);
6004
+ }
5706
6005
  async history(group, limit, before) {
5707
6006
  const r = await this.resolve();
5708
6007
  const rows = await r.messageStore.history(group.rfcGroupId, limit, before);
@@ -5799,6 +6098,32 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
5799
6098
  eventClientMsgId: s.clientMsgId ?? `${s.id}`
5800
6099
  });
5801
6100
  }
6101
+ const editFold = new EditFold();
6102
+ const authorByClientMsgId = /* @__PURE__ */ new Map();
6103
+ for (const s of rows) {
6104
+ if (s.envelopeType === "reaction" || s.envelopeType === "edit") continue;
6105
+ const cid = s.clientMsgId ?? "";
6106
+ if (!cid) continue;
6107
+ const author = s.direction === "outgoing" ? selfUserId : resolveActor?.(s.senderDeviceId ?? null);
6108
+ if (author != null) authorByClientMsgId.set(cid, author);
6109
+ }
6110
+ const authorOfTarget = (cid) => authorByClientMsgId.get(cid) ?? null;
6111
+ for (const s of rows) {
6112
+ if (s.envelopeType !== "edit" || !s.edit) continue;
6113
+ const editor = s.direction === "outgoing" ? selfUserId : resolveActor?.(s.senderDeviceId ?? null) ?? null;
6114
+ editFold.ingest(
6115
+ {
6116
+ targetClientMsgId: s.edit.targetClientMsgId,
6117
+ editorUserId: editor,
6118
+ newText: s.edit.newText,
6119
+ epoch: s.epoch,
6120
+ serverSeq: s.serverSeq,
6121
+ eventClientMsgId: s.clientMsgId ?? `${s.id}`
6122
+ },
6123
+ authorOfTarget
6124
+ );
6125
+ }
6126
+ editFold.reevaluateHeld(authorOfTarget);
5802
6127
  const lookup = /* @__PURE__ */ new Map();
5803
6128
  for (const s of rows) {
5804
6129
  if (s.envelopeType === "reaction") continue;
@@ -5810,7 +6135,7 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
5810
6135
  }
5811
6136
  const out = [];
5812
6137
  for (const s of rows) {
5813
- if (s.envelopeType === "reaction") continue;
6138
+ if (s.envelopeType === "reaction" || s.envelopeType === "edit") continue;
5814
6139
  const clientMsgId = s.clientMsgId ?? "";
5815
6140
  let replyTo = null;
5816
6141
  if (s.replyTo) {
@@ -5826,17 +6151,20 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
5826
6151
  };
5827
6152
  replyTo = resolveReply(ref, (id) => lookup.get(id) ?? null);
5828
6153
  }
6154
+ const editText = clientMsgId ? editFold.text(clientMsgId) : null;
6155
+ const edited = clientMsgId ? editFold.isEdited(clientMsgId) : false;
5829
6156
  out.push({
5830
6157
  id: `${displayId}#${s.serverSeq}`,
5831
6158
  kind: s.text != null ? "text" : "system",
5832
6159
  direction: s.direction,
5833
6160
  senderUserId: s.direction === "outgoing" ? selfUserId : null,
5834
- text: s.text,
6161
+ text: editText ?? s.text,
5835
6162
  serverSeq: s.serverSeq,
5836
6163
  sentAt: new Date(s.at),
5837
6164
  clientMsgId,
5838
6165
  replyTo,
5839
- reactions: clientMsgId ? fold.tally(clientMsgId) : {}
6166
+ reactions: clientMsgId ? fold.tally(clientMsgId) : {},
6167
+ edited
5840
6168
  });
5841
6169
  }
5842
6170
  return out;
@@ -6574,7 +6902,7 @@ function defaultSessionStorage(key) {
6574
6902
  }
6575
6903
 
6576
6904
  // src/version.ts
6577
- var VERSION = "1.2.0";
6905
+ var VERSION = "1.3.0";
6578
6906
 
6579
6907
  // src/runtime.ts
6580
6908
  function buildRuntime(config) {