@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.
- package/dist/{analytics-facade-Cq7R-VH_.d.ts → analytics-facade-CbXprYrS.d.ts} +72 -0
- package/dist/{analytics-facade-B8UATgS4.d.cts → analytics-facade-DA9TGKLF.d.cts} +72 -0
- package/dist/{chunk-UX43AB4W.js → chunk-3EVGYJ5F.js} +380 -15
- package/dist/chunk-3EVGYJ5F.js.map +1 -0
- package/dist/index.cjs +379 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/internal.cjs +379 -14
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +3 -3
- package/dist/internal.d.ts +3 -3
- package/dist/internal.js +1 -1
- package/dist/next/client.cjs +379 -14
- package/dist/next/client.cjs.map +1 -1
- package/dist/next/client.js +1 -1
- package/dist/next/index.cjs +379 -14
- package/dist/next/index.cjs.map +1 -1
- package/dist/next/index.d.cts +2 -2
- package/dist/next/index.d.ts +2 -2
- package/dist/next/index.js +1 -1
- package/dist/{pb-DSJiH1uV.d.ts → pb-CLfTLQzH.d.ts} +1 -1
- package/dist/{pb-DivIiUGR.d.cts → pb-SVs7vTOp.d.cts} +1 -1
- package/dist/react/index.cjs +1 -1
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +1 -1
- package/package.json +3 -3
- package/dist/chunk-UX43AB4W.js.map +0 -1
|
@@ -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.
|
|
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.
|
|
3579
|
+
return this.surfaced().at(-1) ?? null;
|
|
3435
3580
|
}
|
|
3436
3581
|
get unreadCount() {
|
|
3437
|
-
return this.
|
|
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")
|
|
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")
|
|
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")
|
|
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.
|
|
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-
|
|
7807
|
+
//# sourceMappingURL=chunk-3EVGYJ5F.js.map
|