@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
package/dist/index.cjs
CHANGED
|
@@ -1607,6 +1607,69 @@ var PalbeFlags = class {
|
|
|
1607
1607
|
}
|
|
1608
1608
|
};
|
|
1609
1609
|
|
|
1610
|
+
// src/messaging/delete-fold.ts
|
|
1611
|
+
var DeleteFold = class {
|
|
1612
|
+
// targets with a confirmed valid tombstone — MONOTONIC (never removed within retention).
|
|
1613
|
+
tombstoned = /* @__PURE__ */ new Set();
|
|
1614
|
+
// target → the tombstone's authenticated actor userId, awaiting the target's arrival.
|
|
1615
|
+
pending = /* @__PURE__ */ new Map();
|
|
1616
|
+
// dedup of real wire events the fold could evaluate (tombstoned or parked in pending).
|
|
1617
|
+
seen = /* @__PURE__ */ new Set();
|
|
1618
|
+
// events parked because NEITHER the actor NOR the target's author was resolvable at ingest;
|
|
1619
|
+
// keyed by eventClientMsgId so a re-delivered unverifiable event is held exactly once.
|
|
1620
|
+
held = [];
|
|
1621
|
+
/**
|
|
1622
|
+
* Ingest one tombstone. `authorOfTarget` resolves the target message's AUTHOR
|
|
1623
|
+
* userId (null = target absent locally → defer).
|
|
1624
|
+
*/
|
|
1625
|
+
ingest(e, authorOfTarget) {
|
|
1626
|
+
if (this.tombstoned.has(e.targetClientMsgId)) return;
|
|
1627
|
+
if (this.seen.has(e.eventClientMsgId)) return;
|
|
1628
|
+
if (this.heldContains(e.eventClientMsgId)) return;
|
|
1629
|
+
const author = authorOfTarget(e.targetClientMsgId);
|
|
1630
|
+
if (author !== null) {
|
|
1631
|
+
this.seen.add(e.eventClientMsgId);
|
|
1632
|
+
if (e.actorUserId === null || e.actorUserId !== author) return;
|
|
1633
|
+
this.tombstoned.add(e.targetClientMsgId);
|
|
1634
|
+
} else if (e.actorUserId !== null) {
|
|
1635
|
+
this.seen.add(e.eventClientMsgId);
|
|
1636
|
+
this.pending.set(e.targetClientMsgId, e.actorUserId);
|
|
1637
|
+
} else {
|
|
1638
|
+
this.held.push(e);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
/** True once a valid tombstone has absorbed this target. */
|
|
1642
|
+
isTombstoned(targetClientMsgId) {
|
|
1643
|
+
return this.tombstoned.has(targetClientMsgId);
|
|
1644
|
+
}
|
|
1645
|
+
/**
|
|
1646
|
+
* When a target message newly arrives with a resolved `author`, re-check any
|
|
1647
|
+
* pending tombstone for it AND re-attempt any held (unverifiable) tombstones
|
|
1648
|
+
* whose target is now resolvable. The deferred gate is the SAME comparison as
|
|
1649
|
+
* the in-order path.
|
|
1650
|
+
*/
|
|
1651
|
+
reevaluatePending(target, author) {
|
|
1652
|
+
const actor = this.pending.get(target);
|
|
1653
|
+
if (actor !== void 0) {
|
|
1654
|
+
if (author !== null && actor === author) {
|
|
1655
|
+
this.tombstoned.add(target);
|
|
1656
|
+
this.pending.delete(target);
|
|
1657
|
+
} else if (author !== null) {
|
|
1658
|
+
this.pending.delete(target);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
if (this.held.length === 0) return;
|
|
1662
|
+
const pendingHeld = this.held;
|
|
1663
|
+
this.held = [];
|
|
1664
|
+
for (const e of pendingHeld) {
|
|
1665
|
+
this.ingest(e, (t) => t === target ? author : null);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
heldContains(eventClientMsgId) {
|
|
1669
|
+
return this.held.some((h) => h.eventClientMsgId === eventClientMsgId);
|
|
1670
|
+
}
|
|
1671
|
+
};
|
|
1672
|
+
|
|
1610
1673
|
// src/messaging/edit-fold.ts
|
|
1611
1674
|
function orderLt(aEpoch, aSeq, bEpoch, bSeq) {
|
|
1612
1675
|
if (aEpoch !== bEpoch) return aEpoch < bEpoch;
|
|
@@ -1828,6 +1891,17 @@ async function listDevices(rt, userId) {
|
|
|
1828
1891
|
}
|
|
1829
1892
|
|
|
1830
1893
|
// src/messaging/group-messaging.ts
|
|
1894
|
+
function encodeDelete(args) {
|
|
1895
|
+
return encodeUtf8(
|
|
1896
|
+
JSON.stringify({
|
|
1897
|
+
v: 1,
|
|
1898
|
+
type: "delete",
|
|
1899
|
+
client_msg_id: args.clientMsgId,
|
|
1900
|
+
target_client_msg_id: args.targetClientMsgId,
|
|
1901
|
+
scope: "everyone"
|
|
1902
|
+
})
|
|
1903
|
+
);
|
|
1904
|
+
}
|
|
1831
1905
|
function encodeEdit(args) {
|
|
1832
1906
|
return encodeUtf8(
|
|
1833
1907
|
JSON.stringify({
|
|
@@ -1865,6 +1939,18 @@ function decodeEnvelope(bytes) {
|
|
|
1865
1939
|
const s = decodeUtf8(bytes);
|
|
1866
1940
|
try {
|
|
1867
1941
|
const o = JSON.parse(s);
|
|
1942
|
+
if (typeof o === "object" && o !== null && o.type === "delete") {
|
|
1943
|
+
return {
|
|
1944
|
+
type: "delete",
|
|
1945
|
+
text: null,
|
|
1946
|
+
clientMsgId: o.client_msg_id ?? "",
|
|
1947
|
+
replyTo: null,
|
|
1948
|
+
delete: {
|
|
1949
|
+
targetClientMsgId: o.target_client_msg_id ?? "",
|
|
1950
|
+
scope: o.scope ?? "everyone"
|
|
1951
|
+
}
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1868
1954
|
if (typeof o === "object" && o !== null && o.type === "reaction") {
|
|
1869
1955
|
return {
|
|
1870
1956
|
type: "reaction",
|
|
@@ -2252,6 +2338,55 @@ var GroupMessaging = class {
|
|
|
2252
2338
|
clientMsgId: args.clientMsgId
|
|
2253
2339
|
};
|
|
2254
2340
|
}
|
|
2341
|
+
/** Send a delete-for-everyone tombstone on a target message. Encrypts a
|
|
2342
|
+
* `type:'delete'` envelope at the current epoch and sends through the SAME MLS
|
|
2343
|
+
* application path as `sendText` (the server stays blind — a delete is just
|
|
2344
|
+
* another opaque application message; the original ciphertext row is NOT
|
|
2345
|
+
* removed). Persists the outgoing delete row so the tombstone re-folds onto its
|
|
2346
|
+
* target after a reload (the own-send half of the reload parity — the iOS-review
|
|
2347
|
+
* CRITICAL boundary; the projection's `.delete` branch re-folds it). NEVER
|
|
2348
|
+
* rebases (epoch-bound like any application message). */
|
|
2349
|
+
async sendDelete(group, args) {
|
|
2350
|
+
const plaintext = encodeDelete({
|
|
2351
|
+
clientMsgId: args.clientMsgId,
|
|
2352
|
+
targetClientMsgId: args.targetClientMsgId
|
|
2353
|
+
});
|
|
2354
|
+
const ct = await this.engine.encryptApplication(fromBase64(group.rfcGroupId), plaintext);
|
|
2355
|
+
const body = {
|
|
2356
|
+
ciphertext_b64: toBase64(ct),
|
|
2357
|
+
client_idem_key: randomId()
|
|
2358
|
+
};
|
|
2359
|
+
const wire = await palbeRequest(
|
|
2360
|
+
this.rt,
|
|
2361
|
+
"POST",
|
|
2362
|
+
MessagingPaths.groupMessages(group.displayId),
|
|
2363
|
+
{ body }
|
|
2364
|
+
);
|
|
2365
|
+
const stored = {
|
|
2366
|
+
id: `${group.rfcGroupId}#${wire.server_seq}`,
|
|
2367
|
+
direction: "outgoing",
|
|
2368
|
+
text: null,
|
|
2369
|
+
senderDeviceId: this.selfDeviceId,
|
|
2370
|
+
epoch: wire.epoch,
|
|
2371
|
+
serverSeq: wire.server_seq,
|
|
2372
|
+
at: Date.now(),
|
|
2373
|
+
clientMsgId: args.clientMsgId,
|
|
2374
|
+
replyTo: null,
|
|
2375
|
+
envelopeType: "delete",
|
|
2376
|
+
delete: {
|
|
2377
|
+
targetClientMsgId: args.targetClientMsgId,
|
|
2378
|
+
scope: "everyone"
|
|
2379
|
+
}
|
|
2380
|
+
};
|
|
2381
|
+
try {
|
|
2382
|
+
await this.messageStore.append(group.rfcGroupId, stored);
|
|
2383
|
+
} catch {
|
|
2384
|
+
}
|
|
2385
|
+
return {
|
|
2386
|
+
receipt: { serverSeq: wire.server_seq, epoch: wire.epoch },
|
|
2387
|
+
clientMsgId: args.clientMsgId
|
|
2388
|
+
};
|
|
2389
|
+
}
|
|
2255
2390
|
// ── The rebase loop ──
|
|
2256
2391
|
async commitWithRebase(rfcGroupId, build) {
|
|
2257
2392
|
const gidBytes = fromBase64(rfcGroupId);
|
|
@@ -2369,6 +2504,7 @@ var ReactionFold = class {
|
|
|
2369
2504
|
};
|
|
2370
2505
|
|
|
2371
2506
|
// src/messaging/chat.ts
|
|
2507
|
+
var DELETED_DESCRIPTOR = "\u{1F6AB} This message was deleted";
|
|
2372
2508
|
var Chat = class {
|
|
2373
2509
|
/** Stable, URL/log-safe id (the grp_ display id once active; a reserved local id while draft). */
|
|
2374
2510
|
id;
|
|
@@ -2390,6 +2526,15 @@ var Chat = class {
|
|
|
2390
2526
|
reactionFold = new ReactionFold();
|
|
2391
2527
|
/** The single authoritative edit fold for this chat (live + own-send + history). */
|
|
2392
2528
|
editFold = new EditFold();
|
|
2529
|
+
/** The single authoritative delete-for-everyone fold (live + own-send + history).
|
|
2530
|
+
* A tombstone scrubs its target in place (delete DOMINATES edit at render). */
|
|
2531
|
+
deleteFold = new DeleteFold();
|
|
2532
|
+
/** delete-for-me suppression keys (clientMsgId, or `seq:<serverSeq>` for legacy)
|
|
2533
|
+
* — the message is OMITTED from this view. Local + persisted per chat, NO wire. */
|
|
2534
|
+
suppressed = /* @__PURE__ */ new Set();
|
|
2535
|
+
/** True once the persisted suppression set has been loaded (so the omit applies
|
|
2536
|
+
* even on the cold-launch hydrate path before a fresh deleteForMe). */
|
|
2537
|
+
suppressedLoaded = false;
|
|
2393
2538
|
/** Per-target BASE (original) text, so the rendered text is `edit ?? original`.
|
|
2394
2539
|
* Seeded once per target (write-once base) so a 2nd own edit can't corrupt it. */
|
|
2395
2540
|
originalTextByClientMsgId = /* @__PURE__ */ new Map();
|
|
@@ -2438,7 +2583,7 @@ var Chat = class {
|
|
|
2438
2583
|
return this.kind === "direct";
|
|
2439
2584
|
}
|
|
2440
2585
|
get messages() {
|
|
2441
|
-
return this.
|
|
2586
|
+
return this.surfaced();
|
|
2442
2587
|
}
|
|
2443
2588
|
get members() {
|
|
2444
2589
|
return this.memberCache;
|
|
@@ -2447,13 +2592,49 @@ var Chat = class {
|
|
|
2447
2592
|
return this.typingList;
|
|
2448
2593
|
}
|
|
2449
2594
|
get lastMessage() {
|
|
2450
|
-
return this.
|
|
2595
|
+
return this.surfaced().at(-1) ?? null;
|
|
2451
2596
|
}
|
|
2452
2597
|
get unreadCount() {
|
|
2453
|
-
return this.
|
|
2454
|
-
(m) => m.direction === "incoming" && m.serverSeq > this.readWatermark
|
|
2598
|
+
return this.surfaced().filter(
|
|
2599
|
+
(m) => m.direction === "incoming" && !m.isDeleted && m.serverSeq > this.readWatermark
|
|
2455
2600
|
).length;
|
|
2456
2601
|
}
|
|
2602
|
+
/**
|
|
2603
|
+
* The RENDER PRECEDENCE — the single composition point (live AND history project
|
|
2604
|
+
* through it identically). Over the raw `messageList` (which already carries the
|
|
2605
|
+
* folded edit text + reactions + reply):
|
|
2606
|
+
* (1) in the delete-for-me suppression set → OMIT the message entirely;
|
|
2607
|
+
* (2) else tombstoned (delete-for-everyone) → the neutral "deleted" descriptor
|
|
2608
|
+
* with reactions/reply/edit HIDDEN (delete DOMINATES edit — short-circuit);
|
|
2609
|
+
* (3) else the row as-is (edit overlay + reactions + reply already applied).
|
|
2610
|
+
* Pure over (messageList, deleteFold, suppressed) — recomputed on every read so a
|
|
2611
|
+
* just-folded delete / just-suppressed key takes effect without rewriting rows.
|
|
2612
|
+
*/
|
|
2613
|
+
surfaced() {
|
|
2614
|
+
const out = [];
|
|
2615
|
+
for (const m of this.messageList) {
|
|
2616
|
+
const key = this.suppressionKey(m);
|
|
2617
|
+
if (this.suppressed.has(key)) continue;
|
|
2618
|
+
const tombstoned = m.clientMsgId && this.deleteFold.isTombstoned(m.clientMsgId) || m.isDeleted;
|
|
2619
|
+
if (tombstoned) {
|
|
2620
|
+
out.push({
|
|
2621
|
+
...m,
|
|
2622
|
+
text: DELETED_DESCRIPTOR,
|
|
2623
|
+
reactions: {},
|
|
2624
|
+
replyTo: null,
|
|
2625
|
+
edited: false,
|
|
2626
|
+
isDeleted: true
|
|
2627
|
+
});
|
|
2628
|
+
continue;
|
|
2629
|
+
}
|
|
2630
|
+
out.push(m);
|
|
2631
|
+
}
|
|
2632
|
+
return out;
|
|
2633
|
+
}
|
|
2634
|
+
/** The delete-for-me suppression key: clientMsgId when present, else `seq:<n>`. */
|
|
2635
|
+
suppressionKey(m) {
|
|
2636
|
+
return m.clientMsgId ? m.clientMsgId : `seq:${m.serverSeq}`;
|
|
2637
|
+
}
|
|
2457
2638
|
get title() {
|
|
2458
2639
|
if (this.titleOverride) return this.titleOverride;
|
|
2459
2640
|
if (this._group?.name) return this._group.name;
|
|
@@ -2477,9 +2658,28 @@ var Chat = class {
|
|
|
2477
2658
|
if (this.wired || this._state !== "active" || !this._group) return;
|
|
2478
2659
|
this.wired = true;
|
|
2479
2660
|
this.liveUnsub = this.backend.subscribeLive(this._group, this);
|
|
2661
|
+
void this.loadSuppressed();
|
|
2480
2662
|
void this.hydrateHistory();
|
|
2481
2663
|
void this.refreshMembers();
|
|
2482
2664
|
}
|
|
2665
|
+
/** Hydrate the persisted delete-for-me suppression keys (once), then re-emit so
|
|
2666
|
+
* any already-surfaced suppressed message is omitted (cold-launch parity). */
|
|
2667
|
+
async loadSuppressed() {
|
|
2668
|
+
if (this.suppressedLoaded || !this._group) return;
|
|
2669
|
+
this.suppressedLoaded = true;
|
|
2670
|
+
try {
|
|
2671
|
+
const keys = await this.backend.loadSuppressed(this._group);
|
|
2672
|
+
let changed = false;
|
|
2673
|
+
for (const k of keys) {
|
|
2674
|
+
if (!this.suppressed.has(k)) {
|
|
2675
|
+
this.suppressed.add(k);
|
|
2676
|
+
changed = true;
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
if (changed) this.emit();
|
|
2680
|
+
} catch {
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2483
2683
|
async hydrateHistory() {
|
|
2484
2684
|
if (this.historyLoaded || !this._group) return;
|
|
2485
2685
|
this.historyLoaded = true;
|
|
@@ -2490,13 +2690,13 @@ var Chat = class {
|
|
|
2490
2690
|
let changed = false;
|
|
2491
2691
|
for (const m of incoming) {
|
|
2492
2692
|
if (m.serverSeq <= 0) continue;
|
|
2493
|
-
if (m.clientMsgId && m.text !== null) {
|
|
2693
|
+
if (m.clientMsgId && m.text !== null && !m.isDeleted) {
|
|
2494
2694
|
this.byClientMsgId.set(m.clientMsgId, {
|
|
2495
2695
|
text: m.text,
|
|
2496
2696
|
senderUserId: m.senderUserId ?? ""
|
|
2497
2697
|
});
|
|
2498
2698
|
}
|
|
2499
|
-
if (m.clientMsgId) {
|
|
2699
|
+
if (m.clientMsgId && !m.isDeleted) {
|
|
2500
2700
|
this.seedEditBase(m.clientMsgId, m.text, m.senderUserId ?? "");
|
|
2501
2701
|
}
|
|
2502
2702
|
}
|
|
@@ -2506,6 +2706,9 @@ var Chat = class {
|
|
|
2506
2706
|
const key = this.internalKey(m.serverSeq);
|
|
2507
2707
|
if (this.seenKeys.has(key)) continue;
|
|
2508
2708
|
this.seenKeys.add(key);
|
|
2709
|
+
if (m.clientMsgId && !m.isDeleted) {
|
|
2710
|
+
this.deleteFold.reevaluatePending(m.clientMsgId, m.senderUserId);
|
|
2711
|
+
}
|
|
2509
2712
|
this.messageList.push(this.applyEditOverlay(this.applyReactionTally(m)));
|
|
2510
2713
|
changed = true;
|
|
2511
2714
|
this.loadedEarliestSeq = Math.min(this.loadedEarliestSeq ?? m.serverSeq, m.serverSeq);
|
|
@@ -2563,6 +2766,21 @@ var Chat = class {
|
|
|
2563
2766
|
this.recomputeEdit(incoming.edit.targetClientMsgId);
|
|
2564
2767
|
return;
|
|
2565
2768
|
}
|
|
2769
|
+
if (incoming.envelopeType === "delete" && incoming.delete) {
|
|
2770
|
+
const actorUserId = direction === "outgoing" ? this.backend.selfUserId : senderUser;
|
|
2771
|
+
this.deleteFold.ingest(
|
|
2772
|
+
{
|
|
2773
|
+
targetClientMsgId: incoming.delete.targetClientMsgId,
|
|
2774
|
+
actorUserId,
|
|
2775
|
+
epoch: incoming.epoch,
|
|
2776
|
+
serverSeq: incoming.serverSeq,
|
|
2777
|
+
eventClientMsgId: incoming.clientMsgId
|
|
2778
|
+
},
|
|
2779
|
+
this.authorOfTarget
|
|
2780
|
+
);
|
|
2781
|
+
this.emit();
|
|
2782
|
+
return;
|
|
2783
|
+
}
|
|
2566
2784
|
const incomingClientMsgId = incoming.clientMsgId;
|
|
2567
2785
|
const incomingReplyRef = incoming.replyRef;
|
|
2568
2786
|
let resolvedReplyTo = null;
|
|
@@ -2583,7 +2801,9 @@ var Chat = class {
|
|
|
2583
2801
|
// BEFORE its target — the dangling case — renders the moment the target lands).
|
|
2584
2802
|
reactions: incomingClientMsgId ? this.reactionFold.tally(incomingClientMsgId) : {},
|
|
2585
2803
|
// Default false; applyEditOverlay below folds any edit that arrived first.
|
|
2586
|
-
edited: false
|
|
2804
|
+
edited: false,
|
|
2805
|
+
// Default false; surfaced() applies the tombstone scrub if a delete folded.
|
|
2806
|
+
isDeleted: false
|
|
2587
2807
|
};
|
|
2588
2808
|
if (incomingClientMsgId && incoming.text !== null) {
|
|
2589
2809
|
this.byClientMsgId.set(incomingClientMsgId, {
|
|
@@ -2594,6 +2814,7 @@ var Chat = class {
|
|
|
2594
2814
|
if (incomingClientMsgId) {
|
|
2595
2815
|
this.seedEditBase(incomingClientMsgId, incoming.text, senderUser);
|
|
2596
2816
|
this.editFold.reevaluateHeld(this.authorOfTarget);
|
|
2817
|
+
this.deleteFold.reevaluatePending(incomingClientMsgId, senderUser);
|
|
2597
2818
|
}
|
|
2598
2819
|
this.messageList.push(this.applyEditOverlay(msg));
|
|
2599
2820
|
this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
|
|
@@ -2835,7 +3056,9 @@ var Chat = class {
|
|
|
2835
3056
|
// the dangling-target invariant uniform across every append path).
|
|
2836
3057
|
reactions: clientMsgId ? this.reactionFold.tally(clientMsgId) : {},
|
|
2837
3058
|
// Own-sent edits fold via edit() after the fact; new sends start unedited.
|
|
2838
|
-
edited: false
|
|
3059
|
+
edited: false,
|
|
3060
|
+
// Own-sent deletes fold via deleteForEveryone() after the fact; start live.
|
|
3061
|
+
isDeleted: false
|
|
2839
3062
|
});
|
|
2840
3063
|
this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
|
|
2841
3064
|
this.emit();
|
|
@@ -2947,6 +3170,52 @@ var Chat = class {
|
|
|
2947
3170
|
);
|
|
2948
3171
|
this.recomputeEdit(message.clientMsgId);
|
|
2949
3172
|
}
|
|
3173
|
+
// ── Delete ──
|
|
3174
|
+
/** Delete a message for EVERYONE (a cooperative encrypted tombstone). Only the
|
|
3175
|
+
* ORIGINAL SENDER can do this — for an own message self IS the author, so the
|
|
3176
|
+
* author-gate passes. Legacy messages (empty clientMsgId) are DISABLED (a
|
|
3177
|
+
* tombstone keys on the target's clientMsgId, which they lack) — no-op. Sends a
|
|
3178
|
+
* `type:'delete'` envelope through the SAME MLS path as a text message (the
|
|
3179
|
+
* server stays blind), folds the own delete locally so the target scrubs in
|
|
3180
|
+
* place instantly (the durable echo dedups on the SAME wire clientMsgId), and
|
|
3181
|
+
* re-emits. NEVER appends a bubble. delete-for-me'ing the target becomes moot. */
|
|
3182
|
+
async deleteForEveryone(message) {
|
|
3183
|
+
if (!message.clientMsgId) return;
|
|
3184
|
+
const group = await this.materializeIfNeeded();
|
|
3185
|
+
const clientMsgId = mintClientMsgId();
|
|
3186
|
+
const { receipt } = await this.backend.sendDelete(group, {
|
|
3187
|
+
clientMsgId,
|
|
3188
|
+
targetClientMsgId: message.clientMsgId
|
|
3189
|
+
});
|
|
3190
|
+
this.seedEditBase(message.clientMsgId, message.text, this.backend.selfUserId);
|
|
3191
|
+
this.deleteFold.ingest(
|
|
3192
|
+
{
|
|
3193
|
+
targetClientMsgId: message.clientMsgId,
|
|
3194
|
+
actorUserId: this.backend.selfUserId,
|
|
3195
|
+
epoch: receipt.epoch,
|
|
3196
|
+
serverSeq: receipt.serverSeq,
|
|
3197
|
+
eventClientMsgId: clientMsgId
|
|
3198
|
+
},
|
|
3199
|
+
this.authorOfTarget
|
|
3200
|
+
);
|
|
3201
|
+
this.emit();
|
|
3202
|
+
}
|
|
3203
|
+
/** Delete a message for ME only — a LOCAL, per-device suppression. NO wire, NO
|
|
3204
|
+
* attribution, no server contact: the message is OMITTED from THIS view and the
|
|
3205
|
+
* suppression key persists per chat (survives reload). The key is the message's
|
|
3206
|
+
* clientMsgId when present, else `seq:<serverSeq>` for legacy messages. */
|
|
3207
|
+
async deleteForMe(message) {
|
|
3208
|
+
const key = message.clientMsgId ? message.clientMsgId : `seq:${message.serverSeq}`;
|
|
3209
|
+
if (this.suppressed.has(key)) return;
|
|
3210
|
+
this.suppressed.add(key);
|
|
3211
|
+
this.emit();
|
|
3212
|
+
if (this._group) {
|
|
3213
|
+
try {
|
|
3214
|
+
await this.backend.saveSuppressed(this._group, [...this.suppressed]);
|
|
3215
|
+
} catch {
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
2950
3219
|
};
|
|
2951
3220
|
function sameReactions(a, b) {
|
|
2952
3221
|
const ak = Object.keys(a);
|
|
@@ -3127,6 +3396,7 @@ var MessageDeliverySource = class {
|
|
|
3127
3396
|
const { text, clientMsgId, replyTo } = decoded;
|
|
3128
3397
|
const isReaction = decoded.type === "reaction" && decoded.reaction != null;
|
|
3129
3398
|
const isEdit = decoded.type === "edit" && decoded.edit != null;
|
|
3399
|
+
const isDelete = decoded.type === "delete" && decoded.delete != null;
|
|
3130
3400
|
const senderDeviceId = row.sender_device_id ?? decodeSenderDeviceId(received.sender);
|
|
3131
3401
|
const stored = {
|
|
3132
3402
|
id: `${group.rfcGroupId}#${row.server_seq}`,
|
|
@@ -3165,6 +3435,18 @@ var MessageDeliverySource = class {
|
|
|
3165
3435
|
targetClientMsgId: decoded.edit.targetClientMsgId,
|
|
3166
3436
|
newText: decoded.edit.newText
|
|
3167
3437
|
}
|
|
3438
|
+
} : {},
|
|
3439
|
+
// Thread the delete discriminator + target through the persisted row so a
|
|
3440
|
+
// delete-for-everyone tombstone folded LIVE re-folds onto its target after
|
|
3441
|
+
// a reload (the reload-parity boundary — the iOS-review CRITICAL lesson;
|
|
3442
|
+
// the projection's `.delete` branch re-folds it so it never leaks a blank
|
|
3443
|
+
// bubble). Omitted for non-deletes → old rows hydrate as `'text'`/no-delete.
|
|
3444
|
+
...isDelete && decoded.delete ? {
|
|
3445
|
+
envelopeType: "delete",
|
|
3446
|
+
delete: {
|
|
3447
|
+
targetClientMsgId: decoded.delete.targetClientMsgId,
|
|
3448
|
+
scope: decoded.delete.scope
|
|
3449
|
+
}
|
|
3168
3450
|
} : {}
|
|
3169
3451
|
};
|
|
3170
3452
|
try {
|
|
@@ -3184,7 +3466,8 @@ var MessageDeliverySource = class {
|
|
|
3184
3466
|
replyRef: replyTo,
|
|
3185
3467
|
envelopeType: decoded.type ?? "text",
|
|
3186
3468
|
reaction: isReaction ? decoded.reaction : null,
|
|
3187
|
-
edit: isEdit ? decoded.edit : null
|
|
3469
|
+
edit: isEdit ? decoded.edit : null,
|
|
3470
|
+
delete: isDelete ? decoded.delete : null
|
|
3188
3471
|
});
|
|
3189
3472
|
return true;
|
|
3190
3473
|
}
|
|
@@ -5006,6 +5289,36 @@ var SignatureKeyStore = class {
|
|
|
5006
5289
|
}
|
|
5007
5290
|
};
|
|
5008
5291
|
|
|
5292
|
+
// src/messaging/suppression.ts
|
|
5293
|
+
var SuppressionStore = class {
|
|
5294
|
+
constructor(kv) {
|
|
5295
|
+
this.kv = kv;
|
|
5296
|
+
}
|
|
5297
|
+
kv;
|
|
5298
|
+
key(rfcGroupId) {
|
|
5299
|
+
return `supp:${rfcGroupId}`;
|
|
5300
|
+
}
|
|
5301
|
+
/** Load the persisted suppression keys for a chat (empty array if none). */
|
|
5302
|
+
async load(rfcGroupId) {
|
|
5303
|
+
const raw = await this.kv.get(this.key(rfcGroupId));
|
|
5304
|
+
if (!raw) return [];
|
|
5305
|
+
try {
|
|
5306
|
+
const parsed = JSON.parse(decodeUtf8(raw));
|
|
5307
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
5308
|
+
} catch {
|
|
5309
|
+
return [];
|
|
5310
|
+
}
|
|
5311
|
+
}
|
|
5312
|
+
/** Persist the full suppression key set for a chat (deterministic order). */
|
|
5313
|
+
async save(rfcGroupId, keys) {
|
|
5314
|
+
const sorted = [...new Set(keys)].sort();
|
|
5315
|
+
await this.kv.set(this.key(rfcGroupId), encodeUtf8(JSON.stringify(sorted)));
|
|
5316
|
+
}
|
|
5317
|
+
async wipe() {
|
|
5318
|
+
for (const k of await this.kv.keys("supp:")) await this.kv.delete(k);
|
|
5319
|
+
}
|
|
5320
|
+
};
|
|
5321
|
+
|
|
5009
5322
|
// src/messaging/coordinator.ts
|
|
5010
5323
|
var MessagingCoordinator = class {
|
|
5011
5324
|
constructor(rt) {
|
|
@@ -5015,6 +5328,7 @@ var MessagingCoordinator = class {
|
|
|
5015
5328
|
this.sigStore = new SignatureKeyStore(this.kv);
|
|
5016
5329
|
this.groupStore = new GroupStateStorage(this.kv);
|
|
5017
5330
|
this.kpStore = new KeyPackageStorage(this.kv);
|
|
5331
|
+
this.suppressionStore = new SuppressionStore(this.kv);
|
|
5018
5332
|
this.registry.attachChatList(
|
|
5019
5333
|
(chats) => {
|
|
5020
5334
|
this.chatList = chats;
|
|
@@ -5029,6 +5343,7 @@ var MessagingCoordinator = class {
|
|
|
5029
5343
|
sigStore;
|
|
5030
5344
|
groupStore;
|
|
5031
5345
|
kpStore;
|
|
5346
|
+
suppressionStore;
|
|
5032
5347
|
registry = new GroupRegistry();
|
|
5033
5348
|
resolved = null;
|
|
5034
5349
|
resolvePromise = null;
|
|
@@ -5196,6 +5511,18 @@ var MessagingCoordinator = class {
|
|
|
5196
5511
|
const r = await this.resolve();
|
|
5197
5512
|
return r.groups.sendEdit(group, args);
|
|
5198
5513
|
}
|
|
5514
|
+
async sendDelete(group, args) {
|
|
5515
|
+
const r = await this.resolve();
|
|
5516
|
+
return r.groups.sendDelete(group, args);
|
|
5517
|
+
}
|
|
5518
|
+
/** Load this chat's persisted delete-for-me suppression keys (durable-only). */
|
|
5519
|
+
loadSuppressed(group) {
|
|
5520
|
+
return this.suppressionStore.load(group.rfcGroupId);
|
|
5521
|
+
}
|
|
5522
|
+
/** Persist this chat's delete-for-me suppression keys (durable-only, no wire). */
|
|
5523
|
+
saveSuppressed(group, keys) {
|
|
5524
|
+
return this.suppressionStore.save(group.rfcGroupId, keys);
|
|
5525
|
+
}
|
|
5199
5526
|
async history(group, limit, before) {
|
|
5200
5527
|
const r = await this.resolve();
|
|
5201
5528
|
const rows = await r.messageStore.history(group.rfcGroupId, limit, before);
|
|
@@ -5293,9 +5620,11 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
5293
5620
|
});
|
|
5294
5621
|
}
|
|
5295
5622
|
const editFold = new EditFold();
|
|
5623
|
+
const deleteFold = new DeleteFold();
|
|
5296
5624
|
const authorByClientMsgId = /* @__PURE__ */ new Map();
|
|
5297
5625
|
for (const s of rows) {
|
|
5298
|
-
if (s.envelopeType === "reaction" || s.envelopeType === "edit")
|
|
5626
|
+
if (s.envelopeType === "reaction" || s.envelopeType === "edit" || s.envelopeType === "delete")
|
|
5627
|
+
continue;
|
|
5299
5628
|
const cid = s.clientMsgId ?? "";
|
|
5300
5629
|
if (!cid) continue;
|
|
5301
5630
|
const author = s.direction === "outgoing" ? selfUserId : resolveActor?.(s.senderDeviceId ?? null);
|
|
@@ -5318,9 +5647,25 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
5318
5647
|
);
|
|
5319
5648
|
}
|
|
5320
5649
|
editFold.reevaluateHeld(authorOfTarget);
|
|
5650
|
+
for (const s of rows) {
|
|
5651
|
+
if (s.envelopeType !== "delete" || !s.delete) continue;
|
|
5652
|
+
const actor = s.direction === "outgoing" ? selfUserId : resolveActor?.(s.senderDeviceId ?? null) ?? null;
|
|
5653
|
+
deleteFold.ingest(
|
|
5654
|
+
{
|
|
5655
|
+
targetClientMsgId: s.delete.targetClientMsgId,
|
|
5656
|
+
actorUserId: actor,
|
|
5657
|
+
epoch: s.epoch,
|
|
5658
|
+
serverSeq: s.serverSeq,
|
|
5659
|
+
eventClientMsgId: s.clientMsgId ?? `${s.id}`
|
|
5660
|
+
},
|
|
5661
|
+
authorOfTarget
|
|
5662
|
+
);
|
|
5663
|
+
}
|
|
5664
|
+
for (const [cid, author] of authorByClientMsgId) deleteFold.reevaluatePending(cid, author);
|
|
5321
5665
|
const lookup = /* @__PURE__ */ new Map();
|
|
5322
5666
|
for (const s of rows) {
|
|
5323
|
-
if (s.envelopeType === "reaction")
|
|
5667
|
+
if (s.envelopeType === "reaction" || s.envelopeType === "edit" || s.envelopeType === "delete")
|
|
5668
|
+
continue;
|
|
5324
5669
|
const cid = s.clientMsgId ?? "";
|
|
5325
5670
|
if (cid && s.text !== null) {
|
|
5326
5671
|
const senderUserId = s.direction === "outgoing" ? selfUserId : "";
|
|
@@ -5329,8 +5674,27 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
5329
5674
|
}
|
|
5330
5675
|
const out = [];
|
|
5331
5676
|
for (const s of rows) {
|
|
5332
|
-
if (s.envelopeType === "reaction" || s.envelopeType === "edit")
|
|
5677
|
+
if (s.envelopeType === "reaction" || s.envelopeType === "edit" || s.envelopeType === "delete")
|
|
5678
|
+
continue;
|
|
5333
5679
|
const clientMsgId = s.clientMsgId ?? "";
|
|
5680
|
+
const isDeleted = clientMsgId ? deleteFold.isTombstoned(clientMsgId) : false;
|
|
5681
|
+
if (isDeleted) {
|
|
5682
|
+
out.push({
|
|
5683
|
+
id: `${displayId}#${s.serverSeq}`,
|
|
5684
|
+
kind: "text",
|
|
5685
|
+
direction: s.direction,
|
|
5686
|
+
senderUserId: s.direction === "outgoing" ? selfUserId : null,
|
|
5687
|
+
text: DELETED_DESCRIPTOR,
|
|
5688
|
+
serverSeq: s.serverSeq,
|
|
5689
|
+
sentAt: new Date(s.at),
|
|
5690
|
+
clientMsgId,
|
|
5691
|
+
replyTo: null,
|
|
5692
|
+
reactions: {},
|
|
5693
|
+
edited: false,
|
|
5694
|
+
isDeleted: true
|
|
5695
|
+
});
|
|
5696
|
+
continue;
|
|
5697
|
+
}
|
|
5334
5698
|
let replyTo = null;
|
|
5335
5699
|
if (s.replyTo) {
|
|
5336
5700
|
const ref = {
|
|
@@ -5358,7 +5722,8 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
5358
5722
|
clientMsgId,
|
|
5359
5723
|
replyTo,
|
|
5360
5724
|
reactions: clientMsgId ? fold.tally(clientMsgId) : {},
|
|
5361
|
-
edited
|
|
5725
|
+
edited,
|
|
5726
|
+
isDeleted: false
|
|
5362
5727
|
});
|
|
5363
5728
|
}
|
|
5364
5729
|
return out;
|
|
@@ -6215,7 +6580,7 @@ function localStorageSessionStorage(key = DEFAULT_KEY) {
|
|
|
6215
6580
|
}
|
|
6216
6581
|
|
|
6217
6582
|
// src/version.ts
|
|
6218
|
-
var VERSION = "1.
|
|
6583
|
+
var VERSION = "1.4.0";
|
|
6219
6584
|
|
|
6220
6585
|
// src/internal.ts
|
|
6221
6586
|
function getRuntime() {
|