@palbase/web 1.3.0 → 1.4.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,69 @@ var PalbeFlags = class {
2591
2591
  }
2592
2592
  };
2593
2593
 
2594
+ // src/messaging/delete-fold.ts
2595
+ var DeleteFold = class {
2596
+ // targets with a confirmed valid tombstone — MONOTONIC (never removed within retention).
2597
+ tombstoned = /* @__PURE__ */ new Set();
2598
+ // target → the tombstone's authenticated actor userId, awaiting the target's arrival.
2599
+ pending = /* @__PURE__ */ new Map();
2600
+ // dedup of real wire events the fold could evaluate (tombstoned or parked in pending).
2601
+ seen = /* @__PURE__ */ new Set();
2602
+ // events parked because NEITHER the actor NOR the target's author was resolvable at ingest;
2603
+ // keyed by eventClientMsgId so a re-delivered unverifiable event is held exactly once.
2604
+ held = [];
2605
+ /**
2606
+ * Ingest one tombstone. `authorOfTarget` resolves the target message's AUTHOR
2607
+ * userId (null = target absent locally → defer).
2608
+ */
2609
+ ingest(e, authorOfTarget) {
2610
+ if (this.tombstoned.has(e.targetClientMsgId)) return;
2611
+ if (this.seen.has(e.eventClientMsgId)) return;
2612
+ if (this.heldContains(e.eventClientMsgId)) return;
2613
+ const author = authorOfTarget(e.targetClientMsgId);
2614
+ if (author !== null) {
2615
+ this.seen.add(e.eventClientMsgId);
2616
+ if (e.actorUserId === null || e.actorUserId !== author) return;
2617
+ this.tombstoned.add(e.targetClientMsgId);
2618
+ } else if (e.actorUserId !== null) {
2619
+ this.seen.add(e.eventClientMsgId);
2620
+ this.pending.set(e.targetClientMsgId, e.actorUserId);
2621
+ } else {
2622
+ this.held.push(e);
2623
+ }
2624
+ }
2625
+ /** True once a valid tombstone has absorbed this target. */
2626
+ isTombstoned(targetClientMsgId) {
2627
+ return this.tombstoned.has(targetClientMsgId);
2628
+ }
2629
+ /**
2630
+ * When a target message newly arrives with a resolved `author`, re-check any
2631
+ * pending tombstone for it AND re-attempt any held (unverifiable) tombstones
2632
+ * whose target is now resolvable. The deferred gate is the SAME comparison as
2633
+ * the in-order path.
2634
+ */
2635
+ reevaluatePending(target, author) {
2636
+ const actor = this.pending.get(target);
2637
+ if (actor !== void 0) {
2638
+ if (author !== null && actor === author) {
2639
+ this.tombstoned.add(target);
2640
+ this.pending.delete(target);
2641
+ } else if (author !== null) {
2642
+ this.pending.delete(target);
2643
+ }
2644
+ }
2645
+ if (this.held.length === 0) return;
2646
+ const pendingHeld = this.held;
2647
+ this.held = [];
2648
+ for (const e of pendingHeld) {
2649
+ this.ingest(e, (t) => t === target ? author : null);
2650
+ }
2651
+ }
2652
+ heldContains(eventClientMsgId) {
2653
+ return this.held.some((h) => h.eventClientMsgId === eventClientMsgId);
2654
+ }
2655
+ };
2656
+
2594
2657
  // src/messaging/edit-fold.ts
2595
2658
  function orderLt(aEpoch, aSeq, bEpoch, bSeq) {
2596
2659
  if (aEpoch !== bEpoch) return aEpoch < bEpoch;
@@ -2812,6 +2875,17 @@ async function listDevices(rt, userId) {
2812
2875
  }
2813
2876
 
2814
2877
  // src/messaging/group-messaging.ts
2878
+ function encodeDelete(args) {
2879
+ return encodeUtf8(
2880
+ JSON.stringify({
2881
+ v: 1,
2882
+ type: "delete",
2883
+ client_msg_id: args.clientMsgId,
2884
+ target_client_msg_id: args.targetClientMsgId,
2885
+ scope: "everyone"
2886
+ })
2887
+ );
2888
+ }
2815
2889
  function encodeEdit(args) {
2816
2890
  return encodeUtf8(
2817
2891
  JSON.stringify({
@@ -2849,6 +2923,18 @@ function decodeEnvelope(bytes) {
2849
2923
  const s = decodeUtf8(bytes);
2850
2924
  try {
2851
2925
  const o = JSON.parse(s);
2926
+ if (typeof o === "object" && o !== null && o.type === "delete") {
2927
+ return {
2928
+ type: "delete",
2929
+ text: null,
2930
+ clientMsgId: o.client_msg_id ?? "",
2931
+ replyTo: null,
2932
+ delete: {
2933
+ targetClientMsgId: o.target_client_msg_id ?? "",
2934
+ scope: o.scope ?? "everyone"
2935
+ }
2936
+ };
2937
+ }
2852
2938
  if (typeof o === "object" && o !== null && o.type === "reaction") {
2853
2939
  return {
2854
2940
  type: "reaction",
@@ -3236,6 +3322,55 @@ var GroupMessaging = class {
3236
3322
  clientMsgId: args.clientMsgId
3237
3323
  };
3238
3324
  }
3325
+ /** Send a delete-for-everyone tombstone on a target message. Encrypts a
3326
+ * `type:'delete'` envelope at the current epoch and sends through the SAME MLS
3327
+ * application path as `sendText` (the server stays blind — a delete is just
3328
+ * another opaque application message; the original ciphertext row is NOT
3329
+ * removed). Persists the outgoing delete row so the tombstone re-folds onto its
3330
+ * target after a reload (the own-send half of the reload parity — the iOS-review
3331
+ * CRITICAL boundary; the projection's `.delete` branch re-folds it). NEVER
3332
+ * rebases (epoch-bound like any application message). */
3333
+ async sendDelete(group, args) {
3334
+ const plaintext = encodeDelete({
3335
+ clientMsgId: args.clientMsgId,
3336
+ targetClientMsgId: args.targetClientMsgId
3337
+ });
3338
+ const ct = await this.engine.encryptApplication(fromBase64(group.rfcGroupId), plaintext);
3339
+ const body = {
3340
+ ciphertext_b64: toBase64(ct),
3341
+ client_idem_key: randomId()
3342
+ };
3343
+ const wire = await palbeRequest(
3344
+ this.rt,
3345
+ "POST",
3346
+ MessagingPaths.groupMessages(group.displayId),
3347
+ { body }
3348
+ );
3349
+ const stored = {
3350
+ id: `${group.rfcGroupId}#${wire.server_seq}`,
3351
+ direction: "outgoing",
3352
+ text: null,
3353
+ senderDeviceId: this.selfDeviceId,
3354
+ epoch: wire.epoch,
3355
+ serverSeq: wire.server_seq,
3356
+ at: Date.now(),
3357
+ clientMsgId: args.clientMsgId,
3358
+ replyTo: null,
3359
+ envelopeType: "delete",
3360
+ delete: {
3361
+ targetClientMsgId: args.targetClientMsgId,
3362
+ scope: "everyone"
3363
+ }
3364
+ };
3365
+ try {
3366
+ await this.messageStore.append(group.rfcGroupId, stored);
3367
+ } catch {
3368
+ }
3369
+ return {
3370
+ receipt: { serverSeq: wire.server_seq, epoch: wire.epoch },
3371
+ clientMsgId: args.clientMsgId
3372
+ };
3373
+ }
3239
3374
  // ── The rebase loop ──
3240
3375
  async commitWithRebase(rfcGroupId, build) {
3241
3376
  const gidBytes = fromBase64(rfcGroupId);
@@ -3353,6 +3488,7 @@ var ReactionFold = class {
3353
3488
  };
3354
3489
 
3355
3490
  // src/messaging/chat.ts
3491
+ var DELETED_DESCRIPTOR = "\u{1F6AB} This message was deleted";
3356
3492
  var Chat = class {
3357
3493
  /** Stable, URL/log-safe id (the grp_ display id once active; a reserved local id while draft). */
3358
3494
  id;
@@ -3374,6 +3510,15 @@ var Chat = class {
3374
3510
  reactionFold = new ReactionFold();
3375
3511
  /** The single authoritative edit fold for this chat (live + own-send + history). */
3376
3512
  editFold = new EditFold();
3513
+ /** The single authoritative delete-for-everyone fold (live + own-send + history).
3514
+ * A tombstone scrubs its target in place (delete DOMINATES edit at render). */
3515
+ deleteFold = new DeleteFold();
3516
+ /** delete-for-me suppression keys (clientMsgId, or `seq:<serverSeq>` for legacy)
3517
+ * — the message is OMITTED from this view. Local + persisted per chat, NO wire. */
3518
+ suppressed = /* @__PURE__ */ new Set();
3519
+ /** True once the persisted suppression set has been loaded (so the omit applies
3520
+ * even on the cold-launch hydrate path before a fresh deleteForMe). */
3521
+ suppressedLoaded = false;
3377
3522
  /** Per-target BASE (original) text, so the rendered text is `edit ?? original`.
3378
3523
  * Seeded once per target (write-once base) so a 2nd own edit can't corrupt it. */
3379
3524
  originalTextByClientMsgId = /* @__PURE__ */ new Map();
@@ -3422,7 +3567,7 @@ var Chat = class {
3422
3567
  return this.kind === "direct";
3423
3568
  }
3424
3569
  get messages() {
3425
- return this.messageList;
3570
+ return this.surfaced();
3426
3571
  }
3427
3572
  get members() {
3428
3573
  return this.memberCache;
@@ -3431,13 +3576,49 @@ var Chat = class {
3431
3576
  return this.typingList;
3432
3577
  }
3433
3578
  get lastMessage() {
3434
- return this.messageList.at(-1) ?? null;
3579
+ return this.surfaced().at(-1) ?? null;
3435
3580
  }
3436
3581
  get unreadCount() {
3437
- return this.messageList.filter(
3438
- (m) => m.direction === "incoming" && m.serverSeq > this.readWatermark
3582
+ return this.surfaced().filter(
3583
+ (m) => m.direction === "incoming" && !m.isDeleted && m.serverSeq > this.readWatermark
3439
3584
  ).length;
3440
3585
  }
3586
+ /**
3587
+ * The RENDER PRECEDENCE — the single composition point (live AND history project
3588
+ * through it identically). Over the raw `messageList` (which already carries the
3589
+ * folded edit text + reactions + reply):
3590
+ * (1) in the delete-for-me suppression set → OMIT the message entirely;
3591
+ * (2) else tombstoned (delete-for-everyone) → the neutral "deleted" descriptor
3592
+ * with reactions/reply/edit HIDDEN (delete DOMINATES edit — short-circuit);
3593
+ * (3) else the row as-is (edit overlay + reactions + reply already applied).
3594
+ * Pure over (messageList, deleteFold, suppressed) — recomputed on every read so a
3595
+ * just-folded delete / just-suppressed key takes effect without rewriting rows.
3596
+ */
3597
+ surfaced() {
3598
+ const out = [];
3599
+ for (const m of this.messageList) {
3600
+ const key = this.suppressionKey(m);
3601
+ if (this.suppressed.has(key)) continue;
3602
+ const tombstoned = m.clientMsgId && this.deleteFold.isTombstoned(m.clientMsgId) || m.isDeleted;
3603
+ if (tombstoned) {
3604
+ out.push({
3605
+ ...m,
3606
+ text: DELETED_DESCRIPTOR,
3607
+ reactions: {},
3608
+ replyTo: null,
3609
+ edited: false,
3610
+ isDeleted: true
3611
+ });
3612
+ continue;
3613
+ }
3614
+ out.push(m);
3615
+ }
3616
+ return out;
3617
+ }
3618
+ /** The delete-for-me suppression key: clientMsgId when present, else `seq:<n>`. */
3619
+ suppressionKey(m) {
3620
+ return m.clientMsgId ? m.clientMsgId : `seq:${m.serverSeq}`;
3621
+ }
3441
3622
  get title() {
3442
3623
  if (this.titleOverride) return this.titleOverride;
3443
3624
  if (this._group?.name) return this._group.name;
@@ -3461,9 +3642,28 @@ var Chat = class {
3461
3642
  if (this.wired || this._state !== "active" || !this._group) return;
3462
3643
  this.wired = true;
3463
3644
  this.liveUnsub = this.backend.subscribeLive(this._group, this);
3645
+ void this.loadSuppressed();
3464
3646
  void this.hydrateHistory();
3465
3647
  void this.refreshMembers();
3466
3648
  }
3649
+ /** Hydrate the persisted delete-for-me suppression keys (once), then re-emit so
3650
+ * any already-surfaced suppressed message is omitted (cold-launch parity). */
3651
+ async loadSuppressed() {
3652
+ if (this.suppressedLoaded || !this._group) return;
3653
+ this.suppressedLoaded = true;
3654
+ try {
3655
+ const keys = await this.backend.loadSuppressed(this._group);
3656
+ let changed = false;
3657
+ for (const k of keys) {
3658
+ if (!this.suppressed.has(k)) {
3659
+ this.suppressed.add(k);
3660
+ changed = true;
3661
+ }
3662
+ }
3663
+ if (changed) this.emit();
3664
+ } catch {
3665
+ }
3666
+ }
3467
3667
  async hydrateHistory() {
3468
3668
  if (this.historyLoaded || !this._group) return;
3469
3669
  this.historyLoaded = true;
@@ -3474,13 +3674,13 @@ var Chat = class {
3474
3674
  let changed = false;
3475
3675
  for (const m of incoming) {
3476
3676
  if (m.serverSeq <= 0) continue;
3477
- if (m.clientMsgId && m.text !== null) {
3677
+ if (m.clientMsgId && m.text !== null && !m.isDeleted) {
3478
3678
  this.byClientMsgId.set(m.clientMsgId, {
3479
3679
  text: m.text,
3480
3680
  senderUserId: m.senderUserId ?? ""
3481
3681
  });
3482
3682
  }
3483
- if (m.clientMsgId) {
3683
+ if (m.clientMsgId && !m.isDeleted) {
3484
3684
  this.seedEditBase(m.clientMsgId, m.text, m.senderUserId ?? "");
3485
3685
  }
3486
3686
  }
@@ -3490,6 +3690,9 @@ var Chat = class {
3490
3690
  const key = this.internalKey(m.serverSeq);
3491
3691
  if (this.seenKeys.has(key)) continue;
3492
3692
  this.seenKeys.add(key);
3693
+ if (m.clientMsgId && !m.isDeleted) {
3694
+ this.deleteFold.reevaluatePending(m.clientMsgId, m.senderUserId);
3695
+ }
3493
3696
  this.messageList.push(this.applyEditOverlay(this.applyReactionTally(m)));
3494
3697
  changed = true;
3495
3698
  this.loadedEarliestSeq = Math.min(this.loadedEarliestSeq ?? m.serverSeq, m.serverSeq);
@@ -3547,6 +3750,21 @@ var Chat = class {
3547
3750
  this.recomputeEdit(incoming.edit.targetClientMsgId);
3548
3751
  return;
3549
3752
  }
3753
+ if (incoming.envelopeType === "delete" && incoming.delete) {
3754
+ const actorUserId = direction === "outgoing" ? this.backend.selfUserId : senderUser;
3755
+ this.deleteFold.ingest(
3756
+ {
3757
+ targetClientMsgId: incoming.delete.targetClientMsgId,
3758
+ actorUserId,
3759
+ epoch: incoming.epoch,
3760
+ serverSeq: incoming.serverSeq,
3761
+ eventClientMsgId: incoming.clientMsgId
3762
+ },
3763
+ this.authorOfTarget
3764
+ );
3765
+ this.emit();
3766
+ return;
3767
+ }
3550
3768
  const incomingClientMsgId = incoming.clientMsgId;
3551
3769
  const incomingReplyRef = incoming.replyRef;
3552
3770
  let resolvedReplyTo = null;
@@ -3567,7 +3785,9 @@ var Chat = class {
3567
3785
  // BEFORE its target — the dangling case — renders the moment the target lands).
3568
3786
  reactions: incomingClientMsgId ? this.reactionFold.tally(incomingClientMsgId) : {},
3569
3787
  // Default false; applyEditOverlay below folds any edit that arrived first.
3570
- edited: false
3788
+ edited: false,
3789
+ // Default false; surfaced() applies the tombstone scrub if a delete folded.
3790
+ isDeleted: false
3571
3791
  };
3572
3792
  if (incomingClientMsgId && incoming.text !== null) {
3573
3793
  this.byClientMsgId.set(incomingClientMsgId, {
@@ -3578,6 +3798,7 @@ var Chat = class {
3578
3798
  if (incomingClientMsgId) {
3579
3799
  this.seedEditBase(incomingClientMsgId, incoming.text, senderUser);
3580
3800
  this.editFold.reevaluateHeld(this.authorOfTarget);
3801
+ this.deleteFold.reevaluatePending(incomingClientMsgId, senderUser);
3581
3802
  }
3582
3803
  this.messageList.push(this.applyEditOverlay(msg));
3583
3804
  this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
@@ -3819,7 +4040,9 @@ var Chat = class {
3819
4040
  // the dangling-target invariant uniform across every append path).
3820
4041
  reactions: clientMsgId ? this.reactionFold.tally(clientMsgId) : {},
3821
4042
  // Own-sent edits fold via edit() after the fact; new sends start unedited.
3822
- edited: false
4043
+ edited: false,
4044
+ // Own-sent deletes fold via deleteForEveryone() after the fact; start live.
4045
+ isDeleted: false
3823
4046
  });
3824
4047
  this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
3825
4048
  this.emit();
@@ -3931,6 +4154,52 @@ var Chat = class {
3931
4154
  );
3932
4155
  this.recomputeEdit(message.clientMsgId);
3933
4156
  }
4157
+ // ── Delete ──
4158
+ /** Delete a message for EVERYONE (a cooperative encrypted tombstone). Only the
4159
+ * ORIGINAL SENDER can do this — for an own message self IS the author, so the
4160
+ * author-gate passes. Legacy messages (empty clientMsgId) are DISABLED (a
4161
+ * tombstone keys on the target's clientMsgId, which they lack) — no-op. Sends a
4162
+ * `type:'delete'` envelope through the SAME MLS path as a text message (the
4163
+ * server stays blind), folds the own delete locally so the target scrubs in
4164
+ * place instantly (the durable echo dedups on the SAME wire clientMsgId), and
4165
+ * re-emits. NEVER appends a bubble. delete-for-me'ing the target becomes moot. */
4166
+ async deleteForEveryone(message) {
4167
+ if (!message.clientMsgId) return;
4168
+ const group = await this.materializeIfNeeded();
4169
+ const clientMsgId = mintClientMsgId();
4170
+ const { receipt } = await this.backend.sendDelete(group, {
4171
+ clientMsgId,
4172
+ targetClientMsgId: message.clientMsgId
4173
+ });
4174
+ this.seedEditBase(message.clientMsgId, message.text, this.backend.selfUserId);
4175
+ this.deleteFold.ingest(
4176
+ {
4177
+ targetClientMsgId: message.clientMsgId,
4178
+ actorUserId: this.backend.selfUserId,
4179
+ epoch: receipt.epoch,
4180
+ serverSeq: receipt.serverSeq,
4181
+ eventClientMsgId: clientMsgId
4182
+ },
4183
+ this.authorOfTarget
4184
+ );
4185
+ this.emit();
4186
+ }
4187
+ /** Delete a message for ME only — a LOCAL, per-device suppression. NO wire, NO
4188
+ * attribution, no server contact: the message is OMITTED from THIS view and the
4189
+ * suppression key persists per chat (survives reload). The key is the message's
4190
+ * clientMsgId when present, else `seq:<serverSeq>` for legacy messages. */
4191
+ async deleteForMe(message) {
4192
+ const key = message.clientMsgId ? message.clientMsgId : `seq:${message.serverSeq}`;
4193
+ if (this.suppressed.has(key)) return;
4194
+ this.suppressed.add(key);
4195
+ this.emit();
4196
+ if (this._group) {
4197
+ try {
4198
+ await this.backend.saveSuppressed(this._group, [...this.suppressed]);
4199
+ } catch {
4200
+ }
4201
+ }
4202
+ }
3934
4203
  };
3935
4204
  function sameReactions(a, b) {
3936
4205
  const ak = Object.keys(a);
@@ -4111,6 +4380,7 @@ var MessageDeliverySource = class {
4111
4380
  const { text, clientMsgId, replyTo } = decoded;
4112
4381
  const isReaction = decoded.type === "reaction" && decoded.reaction != null;
4113
4382
  const isEdit = decoded.type === "edit" && decoded.edit != null;
4383
+ const isDelete = decoded.type === "delete" && decoded.delete != null;
4114
4384
  const senderDeviceId = row.sender_device_id ?? decodeSenderDeviceId(received.sender);
4115
4385
  const stored = {
4116
4386
  id: `${group.rfcGroupId}#${row.server_seq}`,
@@ -4149,6 +4419,18 @@ var MessageDeliverySource = class {
4149
4419
  targetClientMsgId: decoded.edit.targetClientMsgId,
4150
4420
  newText: decoded.edit.newText
4151
4421
  }
4422
+ } : {},
4423
+ // Thread the delete discriminator + target through the persisted row so a
4424
+ // delete-for-everyone tombstone folded LIVE re-folds onto its target after
4425
+ // a reload (the reload-parity boundary — the iOS-review CRITICAL lesson;
4426
+ // the projection's `.delete` branch re-folds it so it never leaks a blank
4427
+ // bubble). Omitted for non-deletes → old rows hydrate as `'text'`/no-delete.
4428
+ ...isDelete && decoded.delete ? {
4429
+ envelopeType: "delete",
4430
+ delete: {
4431
+ targetClientMsgId: decoded.delete.targetClientMsgId,
4432
+ scope: decoded.delete.scope
4433
+ }
4152
4434
  } : {}
4153
4435
  };
4154
4436
  try {
@@ -4168,7 +4450,8 @@ var MessageDeliverySource = class {
4168
4450
  replyRef: replyTo,
4169
4451
  envelopeType: decoded.type ?? "text",
4170
4452
  reaction: isReaction ? decoded.reaction : null,
4171
- edit: isEdit ? decoded.edit : null
4453
+ edit: isEdit ? decoded.edit : null,
4454
+ delete: isDelete ? decoded.delete : null
4172
4455
  });
4173
4456
  return true;
4174
4457
  }
@@ -5989,6 +6272,36 @@ var SignatureKeyStore = class {
5989
6272
  }
5990
6273
  };
5991
6274
 
6275
+ // src/messaging/suppression.ts
6276
+ var SuppressionStore = class {
6277
+ constructor(kv) {
6278
+ this.kv = kv;
6279
+ }
6280
+ kv;
6281
+ key(rfcGroupId) {
6282
+ return `supp:${rfcGroupId}`;
6283
+ }
6284
+ /** Load the persisted suppression keys for a chat (empty array if none). */
6285
+ async load(rfcGroupId) {
6286
+ const raw = await this.kv.get(this.key(rfcGroupId));
6287
+ if (!raw) return [];
6288
+ try {
6289
+ const parsed = JSON.parse(decodeUtf8(raw));
6290
+ return Array.isArray(parsed) ? parsed : [];
6291
+ } catch {
6292
+ return [];
6293
+ }
6294
+ }
6295
+ /** Persist the full suppression key set for a chat (deterministic order). */
6296
+ async save(rfcGroupId, keys) {
6297
+ const sorted = [...new Set(keys)].sort();
6298
+ await this.kv.set(this.key(rfcGroupId), encodeUtf8(JSON.stringify(sorted)));
6299
+ }
6300
+ async wipe() {
6301
+ for (const k of await this.kv.keys("supp:")) await this.kv.delete(k);
6302
+ }
6303
+ };
6304
+
5992
6305
  // src/messaging/coordinator.ts
5993
6306
  var MessagingCoordinator = class {
5994
6307
  constructor(rt) {
@@ -5998,6 +6311,7 @@ var MessagingCoordinator = class {
5998
6311
  this.sigStore = new SignatureKeyStore(this.kv);
5999
6312
  this.groupStore = new GroupStateStorage(this.kv);
6000
6313
  this.kpStore = new KeyPackageStorage(this.kv);
6314
+ this.suppressionStore = new SuppressionStore(this.kv);
6001
6315
  this.registry.attachChatList(
6002
6316
  (chats) => {
6003
6317
  this.chatList = chats;
@@ -6012,6 +6326,7 @@ var MessagingCoordinator = class {
6012
6326
  sigStore;
6013
6327
  groupStore;
6014
6328
  kpStore;
6329
+ suppressionStore;
6015
6330
  registry = new GroupRegistry();
6016
6331
  resolved = null;
6017
6332
  resolvePromise = null;
@@ -6179,6 +6494,18 @@ var MessagingCoordinator = class {
6179
6494
  const r = await this.resolve();
6180
6495
  return r.groups.sendEdit(group, args);
6181
6496
  }
6497
+ async sendDelete(group, args) {
6498
+ const r = await this.resolve();
6499
+ return r.groups.sendDelete(group, args);
6500
+ }
6501
+ /** Load this chat's persisted delete-for-me suppression keys (durable-only). */
6502
+ loadSuppressed(group) {
6503
+ return this.suppressionStore.load(group.rfcGroupId);
6504
+ }
6505
+ /** Persist this chat's delete-for-me suppression keys (durable-only, no wire). */
6506
+ saveSuppressed(group, keys) {
6507
+ return this.suppressionStore.save(group.rfcGroupId, keys);
6508
+ }
6182
6509
  async history(group, limit, before) {
6183
6510
  const r = await this.resolve();
6184
6511
  const rows = await r.messageStore.history(group.rfcGroupId, limit, before);
@@ -6276,9 +6603,11 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
6276
6603
  });
6277
6604
  }
6278
6605
  const editFold = new EditFold();
6606
+ const deleteFold = new DeleteFold();
6279
6607
  const authorByClientMsgId = /* @__PURE__ */ new Map();
6280
6608
  for (const s of rows) {
6281
- if (s.envelopeType === "reaction" || s.envelopeType === "edit") continue;
6609
+ if (s.envelopeType === "reaction" || s.envelopeType === "edit" || s.envelopeType === "delete")
6610
+ continue;
6282
6611
  const cid = s.clientMsgId ?? "";
6283
6612
  if (!cid) continue;
6284
6613
  const author = s.direction === "outgoing" ? selfUserId : resolveActor?.(s.senderDeviceId ?? null);
@@ -6301,9 +6630,25 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
6301
6630
  );
6302
6631
  }
6303
6632
  editFold.reevaluateHeld(authorOfTarget);
6633
+ for (const s of rows) {
6634
+ if (s.envelopeType !== "delete" || !s.delete) continue;
6635
+ const actor = s.direction === "outgoing" ? selfUserId : resolveActor?.(s.senderDeviceId ?? null) ?? null;
6636
+ deleteFold.ingest(
6637
+ {
6638
+ targetClientMsgId: s.delete.targetClientMsgId,
6639
+ actorUserId: actor,
6640
+ epoch: s.epoch,
6641
+ serverSeq: s.serverSeq,
6642
+ eventClientMsgId: s.clientMsgId ?? `${s.id}`
6643
+ },
6644
+ authorOfTarget
6645
+ );
6646
+ }
6647
+ for (const [cid, author] of authorByClientMsgId) deleteFold.reevaluatePending(cid, author);
6304
6648
  const lookup = /* @__PURE__ */ new Map();
6305
6649
  for (const s of rows) {
6306
- if (s.envelopeType === "reaction") continue;
6650
+ if (s.envelopeType === "reaction" || s.envelopeType === "edit" || s.envelopeType === "delete")
6651
+ continue;
6307
6652
  const cid = s.clientMsgId ?? "";
6308
6653
  if (cid && s.text !== null) {
6309
6654
  const senderUserId = s.direction === "outgoing" ? selfUserId : "";
@@ -6312,8 +6657,27 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
6312
6657
  }
6313
6658
  const out = [];
6314
6659
  for (const s of rows) {
6315
- if (s.envelopeType === "reaction" || s.envelopeType === "edit") continue;
6660
+ if (s.envelopeType === "reaction" || s.envelopeType === "edit" || s.envelopeType === "delete")
6661
+ continue;
6316
6662
  const clientMsgId = s.clientMsgId ?? "";
6663
+ const isDeleted = clientMsgId ? deleteFold.isTombstoned(clientMsgId) : false;
6664
+ if (isDeleted) {
6665
+ out.push({
6666
+ id: `${displayId}#${s.serverSeq}`,
6667
+ kind: "text",
6668
+ direction: s.direction,
6669
+ senderUserId: s.direction === "outgoing" ? selfUserId : null,
6670
+ text: DELETED_DESCRIPTOR,
6671
+ serverSeq: s.serverSeq,
6672
+ sentAt: new Date(s.at),
6673
+ clientMsgId,
6674
+ replyTo: null,
6675
+ reactions: {},
6676
+ edited: false,
6677
+ isDeleted: true
6678
+ });
6679
+ continue;
6680
+ }
6317
6681
  let replyTo = null;
6318
6682
  if (s.replyTo) {
6319
6683
  const ref = {
@@ -6341,7 +6705,8 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
6341
6705
  clientMsgId,
6342
6706
  replyTo,
6343
6707
  reactions: clientMsgId ? fold.tally(clientMsgId) : {},
6344
- edited
6708
+ edited,
6709
+ isDeleted: false
6345
6710
  });
6346
6711
  }
6347
6712
  return out;
@@ -7079,7 +7444,7 @@ function defaultSessionStorage(key) {
7079
7444
  }
7080
7445
 
7081
7446
  // src/version.ts
7082
- var VERSION = "1.3.0";
7447
+ var VERSION = "1.4.0";
7083
7448
 
7084
7449
  // src/runtime.ts
7085
7450
  function buildRuntime(config) {
@@ -7439,4 +7804,4 @@ export {
7439
7804
  pb,
7440
7805
  createBoundClient
7441
7806
  };
7442
- //# sourceMappingURL=chunk-UX43AB4W.js.map
7807
+ //# sourceMappingURL=chunk-3EVGYJ5F.js.map