@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.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { A as AnalyticsProperties, a as AuthChangeEvent, b as AuthState, c as AuthSuccess, d as AuthUser, C as Call, e as CallChangeCallback, f as CallParticipant, g as CallState, h as Chat, i as ChatBackend, j as ChatDraft, k as ChatKind, l as ChatMember, m as ChatMessage, n as ChatMessageKind, o as ChatRole, p as ChatState, q as CommsPrefs, F as FlagsView, M as MagicLinkResult, r as MessageDirection, O as OAuthExchangeResult, P as PalbeAnalytics, s as PalbeAuth, t as PalbeCalls, u as PalbeConfig, v as PalbeFlags, w as PalbeMessaging, x as PalbeOAuthConfig, y as PalbeRealtime, z as PresenceState, R as RealtimeChannel, B as RealtimeConnectionState, D as RealtimeHandler, E as RealtimePayload, G as RealtimeStatus, H as RealtimeStatusSnapshot, I as RealtimeSubscription, J as ResolvedReply, S as SentReceipt, U as Unsubscribe } from './analytics-facade-
|
|
1
|
+
export { A as AnalyticsProperties, a as AuthChangeEvent, b as AuthState, c as AuthSuccess, d as AuthUser, C as Call, e as CallChangeCallback, f as CallParticipant, g as CallState, h as Chat, i as ChatBackend, j as ChatDraft, k as ChatKind, l as ChatMember, m as ChatMessage, n as ChatMessageKind, o as ChatRole, p as ChatState, q as CommsPrefs, F as FlagsView, M as MagicLinkResult, r as MessageDirection, O as OAuthExchangeResult, P as PalbeAnalytics, s as PalbeAuth, t as PalbeCalls, u as PalbeConfig, v as PalbeFlags, w as PalbeMessaging, x as PalbeOAuthConfig, y as PalbeRealtime, z as PresenceState, R as RealtimeChannel, B as RealtimeConnectionState, D as RealtimeHandler, E as RealtimePayload, G as RealtimeStatus, H as RealtimeStatusSnapshot, I as RealtimeSubscription, J as ResolvedReply, S as SentReceipt, U as Unsubscribe } from './analytics-facade-DA9TGKLF.cjs';
|
|
2
2
|
export { B as BackendError, a as BackendErrorKind, F as FieldError, i as isBackendError } from './errors-fDoNdTrJ.cjs';
|
|
3
|
-
export { C as CallOptions, P as PB, U as UploadOptions, a as UploadProgress, p as pb } from './pb-
|
|
3
|
+
export { C as CallOptions, P as PB, U as UploadOptions, a as UploadProgress, p as pb } from './pb-SVs7vTOp.cjs';
|
|
4
4
|
export { P as PersistedSession, S as SessionStorageAdapter, l as localStorageSessionStorage, m as memorySessionStorage } from './storage-BPaeSG8K.cjs';
|
|
5
5
|
export { F as FlagValue } from './pooled-flags-Bwq4usn0.js';
|
|
6
6
|
import 'livekit-client';
|
|
@@ -207,6 +207,6 @@ interface MlsGlue {
|
|
|
207
207
|
*/
|
|
208
208
|
declare function initMls(): Promise<MlsGlue>;
|
|
209
209
|
|
|
210
|
-
declare const VERSION = "1.
|
|
210
|
+
declare const VERSION = "1.4.0";
|
|
211
211
|
|
|
212
212
|
export { VERSION, initMls };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { A as AnalyticsProperties, a as AuthChangeEvent, b as AuthState, c as AuthSuccess, d as AuthUser, C as Call, e as CallChangeCallback, f as CallParticipant, g as CallState, h as Chat, i as ChatBackend, j as ChatDraft, k as ChatKind, l as ChatMember, m as ChatMessage, n as ChatMessageKind, o as ChatRole, p as ChatState, q as CommsPrefs, F as FlagsView, M as MagicLinkResult, r as MessageDirection, O as OAuthExchangeResult, P as PalbeAnalytics, s as PalbeAuth, t as PalbeCalls, u as PalbeConfig, v as PalbeFlags, w as PalbeMessaging, x as PalbeOAuthConfig, y as PalbeRealtime, z as PresenceState, R as RealtimeChannel, B as RealtimeConnectionState, D as RealtimeHandler, E as RealtimePayload, G as RealtimeStatus, H as RealtimeStatusSnapshot, I as RealtimeSubscription, J as ResolvedReply, S as SentReceipt, U as Unsubscribe } from './analytics-facade-
|
|
1
|
+
export { A as AnalyticsProperties, a as AuthChangeEvent, b as AuthState, c as AuthSuccess, d as AuthUser, C as Call, e as CallChangeCallback, f as CallParticipant, g as CallState, h as Chat, i as ChatBackend, j as ChatDraft, k as ChatKind, l as ChatMember, m as ChatMessage, n as ChatMessageKind, o as ChatRole, p as ChatState, q as CommsPrefs, F as FlagsView, M as MagicLinkResult, r as MessageDirection, O as OAuthExchangeResult, P as PalbeAnalytics, s as PalbeAuth, t as PalbeCalls, u as PalbeConfig, v as PalbeFlags, w as PalbeMessaging, x as PalbeOAuthConfig, y as PalbeRealtime, z as PresenceState, R as RealtimeChannel, B as RealtimeConnectionState, D as RealtimeHandler, E as RealtimePayload, G as RealtimeStatus, H as RealtimeStatusSnapshot, I as RealtimeSubscription, J as ResolvedReply, S as SentReceipt, U as Unsubscribe } from './analytics-facade-CbXprYrS.js';
|
|
2
2
|
export { B as BackendError, a as BackendErrorKind, F as FieldError, i as isBackendError } from './errors-fDoNdTrJ.js';
|
|
3
|
-
export { C as CallOptions, P as PB, U as UploadOptions, a as UploadProgress, p as pb } from './pb-
|
|
3
|
+
export { C as CallOptions, P as PB, U as UploadOptions, a as UploadProgress, p as pb } from './pb-CLfTLQzH.js';
|
|
4
4
|
export { P as PersistedSession, S as SessionStorageAdapter, l as localStorageSessionStorage, m as memorySessionStorage } from './storage-BPaeSG8K.js';
|
|
5
5
|
export { F as FlagValue } from './pooled-flags-Bwq4usn0.js';
|
|
6
6
|
import 'livekit-client';
|
|
@@ -207,6 +207,6 @@ interface MlsGlue {
|
|
|
207
207
|
*/
|
|
208
208
|
declare function initMls(): Promise<MlsGlue>;
|
|
209
209
|
|
|
210
|
-
declare const VERSION = "1.
|
|
210
|
+
declare const VERSION = "1.4.0";
|
|
211
211
|
|
|
212
212
|
export { VERSION, initMls };
|
package/dist/index.js
CHANGED
package/dist/internal.cjs
CHANGED
|
@@ -2612,6 +2612,69 @@ var PalbeFlags = class {
|
|
|
2612
2612
|
}
|
|
2613
2613
|
};
|
|
2614
2614
|
|
|
2615
|
+
// src/messaging/delete-fold.ts
|
|
2616
|
+
var DeleteFold = class {
|
|
2617
|
+
// targets with a confirmed valid tombstone — MONOTONIC (never removed within retention).
|
|
2618
|
+
tombstoned = /* @__PURE__ */ new Set();
|
|
2619
|
+
// target → the tombstone's authenticated actor userId, awaiting the target's arrival.
|
|
2620
|
+
pending = /* @__PURE__ */ new Map();
|
|
2621
|
+
// dedup of real wire events the fold could evaluate (tombstoned or parked in pending).
|
|
2622
|
+
seen = /* @__PURE__ */ new Set();
|
|
2623
|
+
// events parked because NEITHER the actor NOR the target's author was resolvable at ingest;
|
|
2624
|
+
// keyed by eventClientMsgId so a re-delivered unverifiable event is held exactly once.
|
|
2625
|
+
held = [];
|
|
2626
|
+
/**
|
|
2627
|
+
* Ingest one tombstone. `authorOfTarget` resolves the target message's AUTHOR
|
|
2628
|
+
* userId (null = target absent locally → defer).
|
|
2629
|
+
*/
|
|
2630
|
+
ingest(e, authorOfTarget) {
|
|
2631
|
+
if (this.tombstoned.has(e.targetClientMsgId)) return;
|
|
2632
|
+
if (this.seen.has(e.eventClientMsgId)) return;
|
|
2633
|
+
if (this.heldContains(e.eventClientMsgId)) return;
|
|
2634
|
+
const author = authorOfTarget(e.targetClientMsgId);
|
|
2635
|
+
if (author !== null) {
|
|
2636
|
+
this.seen.add(e.eventClientMsgId);
|
|
2637
|
+
if (e.actorUserId === null || e.actorUserId !== author) return;
|
|
2638
|
+
this.tombstoned.add(e.targetClientMsgId);
|
|
2639
|
+
} else if (e.actorUserId !== null) {
|
|
2640
|
+
this.seen.add(e.eventClientMsgId);
|
|
2641
|
+
this.pending.set(e.targetClientMsgId, e.actorUserId);
|
|
2642
|
+
} else {
|
|
2643
|
+
this.held.push(e);
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
/** True once a valid tombstone has absorbed this target. */
|
|
2647
|
+
isTombstoned(targetClientMsgId) {
|
|
2648
|
+
return this.tombstoned.has(targetClientMsgId);
|
|
2649
|
+
}
|
|
2650
|
+
/**
|
|
2651
|
+
* When a target message newly arrives with a resolved `author`, re-check any
|
|
2652
|
+
* pending tombstone for it AND re-attempt any held (unverifiable) tombstones
|
|
2653
|
+
* whose target is now resolvable. The deferred gate is the SAME comparison as
|
|
2654
|
+
* the in-order path.
|
|
2655
|
+
*/
|
|
2656
|
+
reevaluatePending(target, author) {
|
|
2657
|
+
const actor = this.pending.get(target);
|
|
2658
|
+
if (actor !== void 0) {
|
|
2659
|
+
if (author !== null && actor === author) {
|
|
2660
|
+
this.tombstoned.add(target);
|
|
2661
|
+
this.pending.delete(target);
|
|
2662
|
+
} else if (author !== null) {
|
|
2663
|
+
this.pending.delete(target);
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
if (this.held.length === 0) return;
|
|
2667
|
+
const pendingHeld = this.held;
|
|
2668
|
+
this.held = [];
|
|
2669
|
+
for (const e of pendingHeld) {
|
|
2670
|
+
this.ingest(e, (t) => t === target ? author : null);
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
heldContains(eventClientMsgId) {
|
|
2674
|
+
return this.held.some((h) => h.eventClientMsgId === eventClientMsgId);
|
|
2675
|
+
}
|
|
2676
|
+
};
|
|
2677
|
+
|
|
2615
2678
|
// src/messaging/edit-fold.ts
|
|
2616
2679
|
function orderLt(aEpoch, aSeq, bEpoch, bSeq) {
|
|
2617
2680
|
if (aEpoch !== bEpoch) return aEpoch < bEpoch;
|
|
@@ -2833,6 +2896,17 @@ async function listDevices(rt, userId) {
|
|
|
2833
2896
|
}
|
|
2834
2897
|
|
|
2835
2898
|
// src/messaging/group-messaging.ts
|
|
2899
|
+
function encodeDelete(args) {
|
|
2900
|
+
return encodeUtf8(
|
|
2901
|
+
JSON.stringify({
|
|
2902
|
+
v: 1,
|
|
2903
|
+
type: "delete",
|
|
2904
|
+
client_msg_id: args.clientMsgId,
|
|
2905
|
+
target_client_msg_id: args.targetClientMsgId,
|
|
2906
|
+
scope: "everyone"
|
|
2907
|
+
})
|
|
2908
|
+
);
|
|
2909
|
+
}
|
|
2836
2910
|
function encodeEdit(args) {
|
|
2837
2911
|
return encodeUtf8(
|
|
2838
2912
|
JSON.stringify({
|
|
@@ -2870,6 +2944,18 @@ function decodeEnvelope(bytes) {
|
|
|
2870
2944
|
const s = decodeUtf8(bytes);
|
|
2871
2945
|
try {
|
|
2872
2946
|
const o = JSON.parse(s);
|
|
2947
|
+
if (typeof o === "object" && o !== null && o.type === "delete") {
|
|
2948
|
+
return {
|
|
2949
|
+
type: "delete",
|
|
2950
|
+
text: null,
|
|
2951
|
+
clientMsgId: o.client_msg_id ?? "",
|
|
2952
|
+
replyTo: null,
|
|
2953
|
+
delete: {
|
|
2954
|
+
targetClientMsgId: o.target_client_msg_id ?? "",
|
|
2955
|
+
scope: o.scope ?? "everyone"
|
|
2956
|
+
}
|
|
2957
|
+
};
|
|
2958
|
+
}
|
|
2873
2959
|
if (typeof o === "object" && o !== null && o.type === "reaction") {
|
|
2874
2960
|
return {
|
|
2875
2961
|
type: "reaction",
|
|
@@ -3257,6 +3343,55 @@ var GroupMessaging = class {
|
|
|
3257
3343
|
clientMsgId: args.clientMsgId
|
|
3258
3344
|
};
|
|
3259
3345
|
}
|
|
3346
|
+
/** Send a delete-for-everyone tombstone on a target message. Encrypts a
|
|
3347
|
+
* `type:'delete'` envelope at the current epoch and sends through the SAME MLS
|
|
3348
|
+
* application path as `sendText` (the server stays blind — a delete is just
|
|
3349
|
+
* another opaque application message; the original ciphertext row is NOT
|
|
3350
|
+
* removed). Persists the outgoing delete row so the tombstone re-folds onto its
|
|
3351
|
+
* target after a reload (the own-send half of the reload parity — the iOS-review
|
|
3352
|
+
* CRITICAL boundary; the projection's `.delete` branch re-folds it). NEVER
|
|
3353
|
+
* rebases (epoch-bound like any application message). */
|
|
3354
|
+
async sendDelete(group, args) {
|
|
3355
|
+
const plaintext = encodeDelete({
|
|
3356
|
+
clientMsgId: args.clientMsgId,
|
|
3357
|
+
targetClientMsgId: args.targetClientMsgId
|
|
3358
|
+
});
|
|
3359
|
+
const ct = await this.engine.encryptApplication(fromBase64(group.rfcGroupId), plaintext);
|
|
3360
|
+
const body = {
|
|
3361
|
+
ciphertext_b64: toBase64(ct),
|
|
3362
|
+
client_idem_key: randomId()
|
|
3363
|
+
};
|
|
3364
|
+
const wire = await palbeRequest(
|
|
3365
|
+
this.rt,
|
|
3366
|
+
"POST",
|
|
3367
|
+
MessagingPaths.groupMessages(group.displayId),
|
|
3368
|
+
{ body }
|
|
3369
|
+
);
|
|
3370
|
+
const stored = {
|
|
3371
|
+
id: `${group.rfcGroupId}#${wire.server_seq}`,
|
|
3372
|
+
direction: "outgoing",
|
|
3373
|
+
text: null,
|
|
3374
|
+
senderDeviceId: this.selfDeviceId,
|
|
3375
|
+
epoch: wire.epoch,
|
|
3376
|
+
serverSeq: wire.server_seq,
|
|
3377
|
+
at: Date.now(),
|
|
3378
|
+
clientMsgId: args.clientMsgId,
|
|
3379
|
+
replyTo: null,
|
|
3380
|
+
envelopeType: "delete",
|
|
3381
|
+
delete: {
|
|
3382
|
+
targetClientMsgId: args.targetClientMsgId,
|
|
3383
|
+
scope: "everyone"
|
|
3384
|
+
}
|
|
3385
|
+
};
|
|
3386
|
+
try {
|
|
3387
|
+
await this.messageStore.append(group.rfcGroupId, stored);
|
|
3388
|
+
} catch {
|
|
3389
|
+
}
|
|
3390
|
+
return {
|
|
3391
|
+
receipt: { serverSeq: wire.server_seq, epoch: wire.epoch },
|
|
3392
|
+
clientMsgId: args.clientMsgId
|
|
3393
|
+
};
|
|
3394
|
+
}
|
|
3260
3395
|
// ── The rebase loop ──
|
|
3261
3396
|
async commitWithRebase(rfcGroupId, build) {
|
|
3262
3397
|
const gidBytes = fromBase64(rfcGroupId);
|
|
@@ -3374,6 +3509,7 @@ var ReactionFold = class {
|
|
|
3374
3509
|
};
|
|
3375
3510
|
|
|
3376
3511
|
// src/messaging/chat.ts
|
|
3512
|
+
var DELETED_DESCRIPTOR = "\u{1F6AB} This message was deleted";
|
|
3377
3513
|
var Chat = class {
|
|
3378
3514
|
/** Stable, URL/log-safe id (the grp_ display id once active; a reserved local id while draft). */
|
|
3379
3515
|
id;
|
|
@@ -3395,6 +3531,15 @@ var Chat = class {
|
|
|
3395
3531
|
reactionFold = new ReactionFold();
|
|
3396
3532
|
/** The single authoritative edit fold for this chat (live + own-send + history). */
|
|
3397
3533
|
editFold = new EditFold();
|
|
3534
|
+
/** The single authoritative delete-for-everyone fold (live + own-send + history).
|
|
3535
|
+
* A tombstone scrubs its target in place (delete DOMINATES edit at render). */
|
|
3536
|
+
deleteFold = new DeleteFold();
|
|
3537
|
+
/** delete-for-me suppression keys (clientMsgId, or `seq:<serverSeq>` for legacy)
|
|
3538
|
+
* — the message is OMITTED from this view. Local + persisted per chat, NO wire. */
|
|
3539
|
+
suppressed = /* @__PURE__ */ new Set();
|
|
3540
|
+
/** True once the persisted suppression set has been loaded (so the omit applies
|
|
3541
|
+
* even on the cold-launch hydrate path before a fresh deleteForMe). */
|
|
3542
|
+
suppressedLoaded = false;
|
|
3398
3543
|
/** Per-target BASE (original) text, so the rendered text is `edit ?? original`.
|
|
3399
3544
|
* Seeded once per target (write-once base) so a 2nd own edit can't corrupt it. */
|
|
3400
3545
|
originalTextByClientMsgId = /* @__PURE__ */ new Map();
|
|
@@ -3443,7 +3588,7 @@ var Chat = class {
|
|
|
3443
3588
|
return this.kind === "direct";
|
|
3444
3589
|
}
|
|
3445
3590
|
get messages() {
|
|
3446
|
-
return this.
|
|
3591
|
+
return this.surfaced();
|
|
3447
3592
|
}
|
|
3448
3593
|
get members() {
|
|
3449
3594
|
return this.memberCache;
|
|
@@ -3452,13 +3597,49 @@ var Chat = class {
|
|
|
3452
3597
|
return this.typingList;
|
|
3453
3598
|
}
|
|
3454
3599
|
get lastMessage() {
|
|
3455
|
-
return this.
|
|
3600
|
+
return this.surfaced().at(-1) ?? null;
|
|
3456
3601
|
}
|
|
3457
3602
|
get unreadCount() {
|
|
3458
|
-
return this.
|
|
3459
|
-
(m) => m.direction === "incoming" && m.serverSeq > this.readWatermark
|
|
3603
|
+
return this.surfaced().filter(
|
|
3604
|
+
(m) => m.direction === "incoming" && !m.isDeleted && m.serverSeq > this.readWatermark
|
|
3460
3605
|
).length;
|
|
3461
3606
|
}
|
|
3607
|
+
/**
|
|
3608
|
+
* The RENDER PRECEDENCE — the single composition point (live AND history project
|
|
3609
|
+
* through it identically). Over the raw `messageList` (which already carries the
|
|
3610
|
+
* folded edit text + reactions + reply):
|
|
3611
|
+
* (1) in the delete-for-me suppression set → OMIT the message entirely;
|
|
3612
|
+
* (2) else tombstoned (delete-for-everyone) → the neutral "deleted" descriptor
|
|
3613
|
+
* with reactions/reply/edit HIDDEN (delete DOMINATES edit — short-circuit);
|
|
3614
|
+
* (3) else the row as-is (edit overlay + reactions + reply already applied).
|
|
3615
|
+
* Pure over (messageList, deleteFold, suppressed) — recomputed on every read so a
|
|
3616
|
+
* just-folded delete / just-suppressed key takes effect without rewriting rows.
|
|
3617
|
+
*/
|
|
3618
|
+
surfaced() {
|
|
3619
|
+
const out = [];
|
|
3620
|
+
for (const m of this.messageList) {
|
|
3621
|
+
const key = this.suppressionKey(m);
|
|
3622
|
+
if (this.suppressed.has(key)) continue;
|
|
3623
|
+
const tombstoned = m.clientMsgId && this.deleteFold.isTombstoned(m.clientMsgId) || m.isDeleted;
|
|
3624
|
+
if (tombstoned) {
|
|
3625
|
+
out.push({
|
|
3626
|
+
...m,
|
|
3627
|
+
text: DELETED_DESCRIPTOR,
|
|
3628
|
+
reactions: {},
|
|
3629
|
+
replyTo: null,
|
|
3630
|
+
edited: false,
|
|
3631
|
+
isDeleted: true
|
|
3632
|
+
});
|
|
3633
|
+
continue;
|
|
3634
|
+
}
|
|
3635
|
+
out.push(m);
|
|
3636
|
+
}
|
|
3637
|
+
return out;
|
|
3638
|
+
}
|
|
3639
|
+
/** The delete-for-me suppression key: clientMsgId when present, else `seq:<n>`. */
|
|
3640
|
+
suppressionKey(m) {
|
|
3641
|
+
return m.clientMsgId ? m.clientMsgId : `seq:${m.serverSeq}`;
|
|
3642
|
+
}
|
|
3462
3643
|
get title() {
|
|
3463
3644
|
if (this.titleOverride) return this.titleOverride;
|
|
3464
3645
|
if (this._group?.name) return this._group.name;
|
|
@@ -3482,9 +3663,28 @@ var Chat = class {
|
|
|
3482
3663
|
if (this.wired || this._state !== "active" || !this._group) return;
|
|
3483
3664
|
this.wired = true;
|
|
3484
3665
|
this.liveUnsub = this.backend.subscribeLive(this._group, this);
|
|
3666
|
+
void this.loadSuppressed();
|
|
3485
3667
|
void this.hydrateHistory();
|
|
3486
3668
|
void this.refreshMembers();
|
|
3487
3669
|
}
|
|
3670
|
+
/** Hydrate the persisted delete-for-me suppression keys (once), then re-emit so
|
|
3671
|
+
* any already-surfaced suppressed message is omitted (cold-launch parity). */
|
|
3672
|
+
async loadSuppressed() {
|
|
3673
|
+
if (this.suppressedLoaded || !this._group) return;
|
|
3674
|
+
this.suppressedLoaded = true;
|
|
3675
|
+
try {
|
|
3676
|
+
const keys = await this.backend.loadSuppressed(this._group);
|
|
3677
|
+
let changed = false;
|
|
3678
|
+
for (const k of keys) {
|
|
3679
|
+
if (!this.suppressed.has(k)) {
|
|
3680
|
+
this.suppressed.add(k);
|
|
3681
|
+
changed = true;
|
|
3682
|
+
}
|
|
3683
|
+
}
|
|
3684
|
+
if (changed) this.emit();
|
|
3685
|
+
} catch {
|
|
3686
|
+
}
|
|
3687
|
+
}
|
|
3488
3688
|
async hydrateHistory() {
|
|
3489
3689
|
if (this.historyLoaded || !this._group) return;
|
|
3490
3690
|
this.historyLoaded = true;
|
|
@@ -3495,13 +3695,13 @@ var Chat = class {
|
|
|
3495
3695
|
let changed = false;
|
|
3496
3696
|
for (const m of incoming) {
|
|
3497
3697
|
if (m.serverSeq <= 0) continue;
|
|
3498
|
-
if (m.clientMsgId && m.text !== null) {
|
|
3698
|
+
if (m.clientMsgId && m.text !== null && !m.isDeleted) {
|
|
3499
3699
|
this.byClientMsgId.set(m.clientMsgId, {
|
|
3500
3700
|
text: m.text,
|
|
3501
3701
|
senderUserId: m.senderUserId ?? ""
|
|
3502
3702
|
});
|
|
3503
3703
|
}
|
|
3504
|
-
if (m.clientMsgId) {
|
|
3704
|
+
if (m.clientMsgId && !m.isDeleted) {
|
|
3505
3705
|
this.seedEditBase(m.clientMsgId, m.text, m.senderUserId ?? "");
|
|
3506
3706
|
}
|
|
3507
3707
|
}
|
|
@@ -3511,6 +3711,9 @@ var Chat = class {
|
|
|
3511
3711
|
const key = this.internalKey(m.serverSeq);
|
|
3512
3712
|
if (this.seenKeys.has(key)) continue;
|
|
3513
3713
|
this.seenKeys.add(key);
|
|
3714
|
+
if (m.clientMsgId && !m.isDeleted) {
|
|
3715
|
+
this.deleteFold.reevaluatePending(m.clientMsgId, m.senderUserId);
|
|
3716
|
+
}
|
|
3514
3717
|
this.messageList.push(this.applyEditOverlay(this.applyReactionTally(m)));
|
|
3515
3718
|
changed = true;
|
|
3516
3719
|
this.loadedEarliestSeq = Math.min(this.loadedEarliestSeq ?? m.serverSeq, m.serverSeq);
|
|
@@ -3568,6 +3771,21 @@ var Chat = class {
|
|
|
3568
3771
|
this.recomputeEdit(incoming.edit.targetClientMsgId);
|
|
3569
3772
|
return;
|
|
3570
3773
|
}
|
|
3774
|
+
if (incoming.envelopeType === "delete" && incoming.delete) {
|
|
3775
|
+
const actorUserId = direction === "outgoing" ? this.backend.selfUserId : senderUser;
|
|
3776
|
+
this.deleteFold.ingest(
|
|
3777
|
+
{
|
|
3778
|
+
targetClientMsgId: incoming.delete.targetClientMsgId,
|
|
3779
|
+
actorUserId,
|
|
3780
|
+
epoch: incoming.epoch,
|
|
3781
|
+
serverSeq: incoming.serverSeq,
|
|
3782
|
+
eventClientMsgId: incoming.clientMsgId
|
|
3783
|
+
},
|
|
3784
|
+
this.authorOfTarget
|
|
3785
|
+
);
|
|
3786
|
+
this.emit();
|
|
3787
|
+
return;
|
|
3788
|
+
}
|
|
3571
3789
|
const incomingClientMsgId = incoming.clientMsgId;
|
|
3572
3790
|
const incomingReplyRef = incoming.replyRef;
|
|
3573
3791
|
let resolvedReplyTo = null;
|
|
@@ -3588,7 +3806,9 @@ var Chat = class {
|
|
|
3588
3806
|
// BEFORE its target — the dangling case — renders the moment the target lands).
|
|
3589
3807
|
reactions: incomingClientMsgId ? this.reactionFold.tally(incomingClientMsgId) : {},
|
|
3590
3808
|
// Default false; applyEditOverlay below folds any edit that arrived first.
|
|
3591
|
-
edited: false
|
|
3809
|
+
edited: false,
|
|
3810
|
+
// Default false; surfaced() applies the tombstone scrub if a delete folded.
|
|
3811
|
+
isDeleted: false
|
|
3592
3812
|
};
|
|
3593
3813
|
if (incomingClientMsgId && incoming.text !== null) {
|
|
3594
3814
|
this.byClientMsgId.set(incomingClientMsgId, {
|
|
@@ -3599,6 +3819,7 @@ var Chat = class {
|
|
|
3599
3819
|
if (incomingClientMsgId) {
|
|
3600
3820
|
this.seedEditBase(incomingClientMsgId, incoming.text, senderUser);
|
|
3601
3821
|
this.editFold.reevaluateHeld(this.authorOfTarget);
|
|
3822
|
+
this.deleteFold.reevaluatePending(incomingClientMsgId, senderUser);
|
|
3602
3823
|
}
|
|
3603
3824
|
this.messageList.push(this.applyEditOverlay(msg));
|
|
3604
3825
|
this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
|
|
@@ -3840,7 +4061,9 @@ var Chat = class {
|
|
|
3840
4061
|
// the dangling-target invariant uniform across every append path).
|
|
3841
4062
|
reactions: clientMsgId ? this.reactionFold.tally(clientMsgId) : {},
|
|
3842
4063
|
// Own-sent edits fold via edit() after the fact; new sends start unedited.
|
|
3843
|
-
edited: false
|
|
4064
|
+
edited: false,
|
|
4065
|
+
// Own-sent deletes fold via deleteForEveryone() after the fact; start live.
|
|
4066
|
+
isDeleted: false
|
|
3844
4067
|
});
|
|
3845
4068
|
this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
|
|
3846
4069
|
this.emit();
|
|
@@ -3952,6 +4175,52 @@ var Chat = class {
|
|
|
3952
4175
|
);
|
|
3953
4176
|
this.recomputeEdit(message.clientMsgId);
|
|
3954
4177
|
}
|
|
4178
|
+
// ── Delete ──
|
|
4179
|
+
/** Delete a message for EVERYONE (a cooperative encrypted tombstone). Only the
|
|
4180
|
+
* ORIGINAL SENDER can do this — for an own message self IS the author, so the
|
|
4181
|
+
* author-gate passes. Legacy messages (empty clientMsgId) are DISABLED (a
|
|
4182
|
+
* tombstone keys on the target's clientMsgId, which they lack) — no-op. Sends a
|
|
4183
|
+
* `type:'delete'` envelope through the SAME MLS path as a text message (the
|
|
4184
|
+
* server stays blind), folds the own delete locally so the target scrubs in
|
|
4185
|
+
* place instantly (the durable echo dedups on the SAME wire clientMsgId), and
|
|
4186
|
+
* re-emits. NEVER appends a bubble. delete-for-me'ing the target becomes moot. */
|
|
4187
|
+
async deleteForEveryone(message) {
|
|
4188
|
+
if (!message.clientMsgId) return;
|
|
4189
|
+
const group = await this.materializeIfNeeded();
|
|
4190
|
+
const clientMsgId = mintClientMsgId();
|
|
4191
|
+
const { receipt } = await this.backend.sendDelete(group, {
|
|
4192
|
+
clientMsgId,
|
|
4193
|
+
targetClientMsgId: message.clientMsgId
|
|
4194
|
+
});
|
|
4195
|
+
this.seedEditBase(message.clientMsgId, message.text, this.backend.selfUserId);
|
|
4196
|
+
this.deleteFold.ingest(
|
|
4197
|
+
{
|
|
4198
|
+
targetClientMsgId: message.clientMsgId,
|
|
4199
|
+
actorUserId: this.backend.selfUserId,
|
|
4200
|
+
epoch: receipt.epoch,
|
|
4201
|
+
serverSeq: receipt.serverSeq,
|
|
4202
|
+
eventClientMsgId: clientMsgId
|
|
4203
|
+
},
|
|
4204
|
+
this.authorOfTarget
|
|
4205
|
+
);
|
|
4206
|
+
this.emit();
|
|
4207
|
+
}
|
|
4208
|
+
/** Delete a message for ME only — a LOCAL, per-device suppression. NO wire, NO
|
|
4209
|
+
* attribution, no server contact: the message is OMITTED from THIS view and the
|
|
4210
|
+
* suppression key persists per chat (survives reload). The key is the message's
|
|
4211
|
+
* clientMsgId when present, else `seq:<serverSeq>` for legacy messages. */
|
|
4212
|
+
async deleteForMe(message) {
|
|
4213
|
+
const key = message.clientMsgId ? message.clientMsgId : `seq:${message.serverSeq}`;
|
|
4214
|
+
if (this.suppressed.has(key)) return;
|
|
4215
|
+
this.suppressed.add(key);
|
|
4216
|
+
this.emit();
|
|
4217
|
+
if (this._group) {
|
|
4218
|
+
try {
|
|
4219
|
+
await this.backend.saveSuppressed(this._group, [...this.suppressed]);
|
|
4220
|
+
} catch {
|
|
4221
|
+
}
|
|
4222
|
+
}
|
|
4223
|
+
}
|
|
3955
4224
|
};
|
|
3956
4225
|
function sameReactions(a, b) {
|
|
3957
4226
|
const ak = Object.keys(a);
|
|
@@ -4132,6 +4401,7 @@ var MessageDeliverySource = class {
|
|
|
4132
4401
|
const { text, clientMsgId, replyTo } = decoded;
|
|
4133
4402
|
const isReaction = decoded.type === "reaction" && decoded.reaction != null;
|
|
4134
4403
|
const isEdit = decoded.type === "edit" && decoded.edit != null;
|
|
4404
|
+
const isDelete = decoded.type === "delete" && decoded.delete != null;
|
|
4135
4405
|
const senderDeviceId = row.sender_device_id ?? decodeSenderDeviceId(received.sender);
|
|
4136
4406
|
const stored = {
|
|
4137
4407
|
id: `${group.rfcGroupId}#${row.server_seq}`,
|
|
@@ -4170,6 +4440,18 @@ var MessageDeliverySource = class {
|
|
|
4170
4440
|
targetClientMsgId: decoded.edit.targetClientMsgId,
|
|
4171
4441
|
newText: decoded.edit.newText
|
|
4172
4442
|
}
|
|
4443
|
+
} : {},
|
|
4444
|
+
// Thread the delete discriminator + target through the persisted row so a
|
|
4445
|
+
// delete-for-everyone tombstone folded LIVE re-folds onto its target after
|
|
4446
|
+
// a reload (the reload-parity boundary — the iOS-review CRITICAL lesson;
|
|
4447
|
+
// the projection's `.delete` branch re-folds it so it never leaks a blank
|
|
4448
|
+
// bubble). Omitted for non-deletes → old rows hydrate as `'text'`/no-delete.
|
|
4449
|
+
...isDelete && decoded.delete ? {
|
|
4450
|
+
envelopeType: "delete",
|
|
4451
|
+
delete: {
|
|
4452
|
+
targetClientMsgId: decoded.delete.targetClientMsgId,
|
|
4453
|
+
scope: decoded.delete.scope
|
|
4454
|
+
}
|
|
4173
4455
|
} : {}
|
|
4174
4456
|
};
|
|
4175
4457
|
try {
|
|
@@ -4189,7 +4471,8 @@ var MessageDeliverySource = class {
|
|
|
4189
4471
|
replyRef: replyTo,
|
|
4190
4472
|
envelopeType: decoded.type ?? "text",
|
|
4191
4473
|
reaction: isReaction ? decoded.reaction : null,
|
|
4192
|
-
edit: isEdit ? decoded.edit : null
|
|
4474
|
+
edit: isEdit ? decoded.edit : null,
|
|
4475
|
+
delete: isDelete ? decoded.delete : null
|
|
4193
4476
|
});
|
|
4194
4477
|
return true;
|
|
4195
4478
|
}
|
|
@@ -6011,6 +6294,36 @@ var SignatureKeyStore = class {
|
|
|
6011
6294
|
}
|
|
6012
6295
|
};
|
|
6013
6296
|
|
|
6297
|
+
// src/messaging/suppression.ts
|
|
6298
|
+
var SuppressionStore = class {
|
|
6299
|
+
constructor(kv) {
|
|
6300
|
+
this.kv = kv;
|
|
6301
|
+
}
|
|
6302
|
+
kv;
|
|
6303
|
+
key(rfcGroupId) {
|
|
6304
|
+
return `supp:${rfcGroupId}`;
|
|
6305
|
+
}
|
|
6306
|
+
/** Load the persisted suppression keys for a chat (empty array if none). */
|
|
6307
|
+
async load(rfcGroupId) {
|
|
6308
|
+
const raw = await this.kv.get(this.key(rfcGroupId));
|
|
6309
|
+
if (!raw) return [];
|
|
6310
|
+
try {
|
|
6311
|
+
const parsed = JSON.parse(decodeUtf8(raw));
|
|
6312
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
6313
|
+
} catch {
|
|
6314
|
+
return [];
|
|
6315
|
+
}
|
|
6316
|
+
}
|
|
6317
|
+
/** Persist the full suppression key set for a chat (deterministic order). */
|
|
6318
|
+
async save(rfcGroupId, keys) {
|
|
6319
|
+
const sorted = [...new Set(keys)].sort();
|
|
6320
|
+
await this.kv.set(this.key(rfcGroupId), encodeUtf8(JSON.stringify(sorted)));
|
|
6321
|
+
}
|
|
6322
|
+
async wipe() {
|
|
6323
|
+
for (const k of await this.kv.keys("supp:")) await this.kv.delete(k);
|
|
6324
|
+
}
|
|
6325
|
+
};
|
|
6326
|
+
|
|
6014
6327
|
// src/messaging/coordinator.ts
|
|
6015
6328
|
var MessagingCoordinator = class {
|
|
6016
6329
|
constructor(rt) {
|
|
@@ -6020,6 +6333,7 @@ var MessagingCoordinator = class {
|
|
|
6020
6333
|
this.sigStore = new SignatureKeyStore(this.kv);
|
|
6021
6334
|
this.groupStore = new GroupStateStorage(this.kv);
|
|
6022
6335
|
this.kpStore = new KeyPackageStorage(this.kv);
|
|
6336
|
+
this.suppressionStore = new SuppressionStore(this.kv);
|
|
6023
6337
|
this.registry.attachChatList(
|
|
6024
6338
|
(chats) => {
|
|
6025
6339
|
this.chatList = chats;
|
|
@@ -6034,6 +6348,7 @@ var MessagingCoordinator = class {
|
|
|
6034
6348
|
sigStore;
|
|
6035
6349
|
groupStore;
|
|
6036
6350
|
kpStore;
|
|
6351
|
+
suppressionStore;
|
|
6037
6352
|
registry = new GroupRegistry();
|
|
6038
6353
|
resolved = null;
|
|
6039
6354
|
resolvePromise = null;
|
|
@@ -6201,6 +6516,18 @@ var MessagingCoordinator = class {
|
|
|
6201
6516
|
const r = await this.resolve();
|
|
6202
6517
|
return r.groups.sendEdit(group, args);
|
|
6203
6518
|
}
|
|
6519
|
+
async sendDelete(group, args) {
|
|
6520
|
+
const r = await this.resolve();
|
|
6521
|
+
return r.groups.sendDelete(group, args);
|
|
6522
|
+
}
|
|
6523
|
+
/** Load this chat's persisted delete-for-me suppression keys (durable-only). */
|
|
6524
|
+
loadSuppressed(group) {
|
|
6525
|
+
return this.suppressionStore.load(group.rfcGroupId);
|
|
6526
|
+
}
|
|
6527
|
+
/** Persist this chat's delete-for-me suppression keys (durable-only, no wire). */
|
|
6528
|
+
saveSuppressed(group, keys) {
|
|
6529
|
+
return this.suppressionStore.save(group.rfcGroupId, keys);
|
|
6530
|
+
}
|
|
6204
6531
|
async history(group, limit, before) {
|
|
6205
6532
|
const r = await this.resolve();
|
|
6206
6533
|
const rows = await r.messageStore.history(group.rfcGroupId, limit, before);
|
|
@@ -6298,9 +6625,11 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
6298
6625
|
});
|
|
6299
6626
|
}
|
|
6300
6627
|
const editFold = new EditFold();
|
|
6628
|
+
const deleteFold = new DeleteFold();
|
|
6301
6629
|
const authorByClientMsgId = /* @__PURE__ */ new Map();
|
|
6302
6630
|
for (const s of rows) {
|
|
6303
|
-
if (s.envelopeType === "reaction" || s.envelopeType === "edit")
|
|
6631
|
+
if (s.envelopeType === "reaction" || s.envelopeType === "edit" || s.envelopeType === "delete")
|
|
6632
|
+
continue;
|
|
6304
6633
|
const cid = s.clientMsgId ?? "";
|
|
6305
6634
|
if (!cid) continue;
|
|
6306
6635
|
const author = s.direction === "outgoing" ? selfUserId : resolveActor?.(s.senderDeviceId ?? null);
|
|
@@ -6323,9 +6652,25 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
6323
6652
|
);
|
|
6324
6653
|
}
|
|
6325
6654
|
editFold.reevaluateHeld(authorOfTarget);
|
|
6655
|
+
for (const s of rows) {
|
|
6656
|
+
if (s.envelopeType !== "delete" || !s.delete) continue;
|
|
6657
|
+
const actor = s.direction === "outgoing" ? selfUserId : resolveActor?.(s.senderDeviceId ?? null) ?? null;
|
|
6658
|
+
deleteFold.ingest(
|
|
6659
|
+
{
|
|
6660
|
+
targetClientMsgId: s.delete.targetClientMsgId,
|
|
6661
|
+
actorUserId: actor,
|
|
6662
|
+
epoch: s.epoch,
|
|
6663
|
+
serverSeq: s.serverSeq,
|
|
6664
|
+
eventClientMsgId: s.clientMsgId ?? `${s.id}`
|
|
6665
|
+
},
|
|
6666
|
+
authorOfTarget
|
|
6667
|
+
);
|
|
6668
|
+
}
|
|
6669
|
+
for (const [cid, author] of authorByClientMsgId) deleteFold.reevaluatePending(cid, author);
|
|
6326
6670
|
const lookup = /* @__PURE__ */ new Map();
|
|
6327
6671
|
for (const s of rows) {
|
|
6328
|
-
if (s.envelopeType === "reaction")
|
|
6672
|
+
if (s.envelopeType === "reaction" || s.envelopeType === "edit" || s.envelopeType === "delete")
|
|
6673
|
+
continue;
|
|
6329
6674
|
const cid = s.clientMsgId ?? "";
|
|
6330
6675
|
if (cid && s.text !== null) {
|
|
6331
6676
|
const senderUserId = s.direction === "outgoing" ? selfUserId : "";
|
|
@@ -6334,8 +6679,27 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
6334
6679
|
}
|
|
6335
6680
|
const out = [];
|
|
6336
6681
|
for (const s of rows) {
|
|
6337
|
-
if (s.envelopeType === "reaction" || s.envelopeType === "edit")
|
|
6682
|
+
if (s.envelopeType === "reaction" || s.envelopeType === "edit" || s.envelopeType === "delete")
|
|
6683
|
+
continue;
|
|
6338
6684
|
const clientMsgId = s.clientMsgId ?? "";
|
|
6685
|
+
const isDeleted = clientMsgId ? deleteFold.isTombstoned(clientMsgId) : false;
|
|
6686
|
+
if (isDeleted) {
|
|
6687
|
+
out.push({
|
|
6688
|
+
id: `${displayId}#${s.serverSeq}`,
|
|
6689
|
+
kind: "text",
|
|
6690
|
+
direction: s.direction,
|
|
6691
|
+
senderUserId: s.direction === "outgoing" ? selfUserId : null,
|
|
6692
|
+
text: DELETED_DESCRIPTOR,
|
|
6693
|
+
serverSeq: s.serverSeq,
|
|
6694
|
+
sentAt: new Date(s.at),
|
|
6695
|
+
clientMsgId,
|
|
6696
|
+
replyTo: null,
|
|
6697
|
+
reactions: {},
|
|
6698
|
+
edited: false,
|
|
6699
|
+
isDeleted: true
|
|
6700
|
+
});
|
|
6701
|
+
continue;
|
|
6702
|
+
}
|
|
6339
6703
|
let replyTo = null;
|
|
6340
6704
|
if (s.replyTo) {
|
|
6341
6705
|
const ref = {
|
|
@@ -6363,7 +6727,8 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
6363
6727
|
clientMsgId,
|
|
6364
6728
|
replyTo,
|
|
6365
6729
|
reactions: clientMsgId ? fold.tally(clientMsgId) : {},
|
|
6366
|
-
edited
|
|
6730
|
+
edited,
|
|
6731
|
+
isDeleted: false
|
|
6367
6732
|
});
|
|
6368
6733
|
}
|
|
6369
6734
|
return out;
|
|
@@ -7101,7 +7466,7 @@ function defaultSessionStorage(key) {
|
|
|
7101
7466
|
}
|
|
7102
7467
|
|
|
7103
7468
|
// src/version.ts
|
|
7104
|
-
var VERSION = "1.
|
|
7469
|
+
var VERSION = "1.4.0";
|
|
7105
7470
|
|
|
7106
7471
|
// src/runtime.ts
|
|
7107
7472
|
function buildRuntime(config) {
|