@palbase/web 1.3.0 → 1.5.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-BQlTfsEm.d.ts} +168 -4
- package/dist/{analytics-facade-B8UATgS4.d.cts → analytics-facade-DvsLE_qz.d.cts} +168 -4
- package/dist/{chunk-UX43AB4W.js → chunk-MBA2NAKS.js} +694 -44
- package/dist/chunk-MBA2NAKS.js.map +1 -0
- package/dist/index.cjs +693 -43
- 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 +693 -43
- 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 +693 -43
- package/dist/next/client.cjs.map +1 -1
- package/dist/next/client.js +1 -1
- package/dist/next/index.cjs +693 -43
- 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-Cl7TeyCR.d.ts} +1 -1
- package/dist/{pb-DivIiUGR.d.cts → pb-a86jrG9h.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;
|
|
@@ -2636,7 +2699,8 @@ var EditFold = class {
|
|
|
2636
2699
|
orderEpoch: e.epoch,
|
|
2637
2700
|
orderSeq: e.serverSeq,
|
|
2638
2701
|
lastEventId: e.eventClientMsgId,
|
|
2639
|
-
text: e.newText
|
|
2702
|
+
text: e.newText,
|
|
2703
|
+
bodyRanges: e.bodyRanges ?? null
|
|
2640
2704
|
});
|
|
2641
2705
|
this.editedTargets.add(e.targetClientMsgId);
|
|
2642
2706
|
}
|
|
@@ -2657,6 +2721,15 @@ var EditFold = class {
|
|
|
2657
2721
|
isEdited(targetClientMsgId) {
|
|
2658
2722
|
return this.editedTargets.has(targetClientMsgId);
|
|
2659
2723
|
}
|
|
2724
|
+
/**
|
|
2725
|
+
* The WINNING edit's replacement mention ranges for a target (raw, un-normalized),
|
|
2726
|
+
* or null when no valid edit applied or the winning edit carried none. The Chat
|
|
2727
|
+
* normalizes these against the edited text to compute the edited message's mentions
|
|
2728
|
+
* (mentions T6). LWW-consistent: always the same edit that `text(...)` returns.
|
|
2729
|
+
*/
|
|
2730
|
+
bodyRanges(targetClientMsgId) {
|
|
2731
|
+
return this.states.get(targetClientMsgId)?.bodyRanges ?? null;
|
|
2732
|
+
}
|
|
2660
2733
|
/**
|
|
2661
2734
|
* Re-run HELD edits when the roster/target newly resolves (call on member/roster
|
|
2662
2735
|
* change and when a target message arrives). Clears `held` and re-ingests each
|
|
@@ -2812,6 +2885,17 @@ async function listDevices(rt, userId) {
|
|
|
2812
2885
|
}
|
|
2813
2886
|
|
|
2814
2887
|
// src/messaging/group-messaging.ts
|
|
2888
|
+
function encodeDelete(args) {
|
|
2889
|
+
return encodeUtf8(
|
|
2890
|
+
JSON.stringify({
|
|
2891
|
+
v: 1,
|
|
2892
|
+
type: "delete",
|
|
2893
|
+
client_msg_id: args.clientMsgId,
|
|
2894
|
+
target_client_msg_id: args.targetClientMsgId,
|
|
2895
|
+
scope: "everyone"
|
|
2896
|
+
})
|
|
2897
|
+
);
|
|
2898
|
+
}
|
|
2815
2899
|
function encodeEdit(args) {
|
|
2816
2900
|
return encodeUtf8(
|
|
2817
2901
|
JSON.stringify({
|
|
@@ -2819,7 +2903,14 @@ function encodeEdit(args) {
|
|
|
2819
2903
|
type: "edit",
|
|
2820
2904
|
client_msg_id: args.clientMsgId,
|
|
2821
2905
|
target_client_msg_id: args.targetClientMsgId,
|
|
2822
|
-
new_text: args.newText
|
|
2906
|
+
new_text: args.newText,
|
|
2907
|
+
...args.bodyRanges && args.bodyRanges.length > 0 ? {
|
|
2908
|
+
body_ranges: args.bodyRanges.map((r) => ({
|
|
2909
|
+
start: r.start,
|
|
2910
|
+
length: r.length,
|
|
2911
|
+
mentioned_user_id: r.mentionedUserId
|
|
2912
|
+
}))
|
|
2913
|
+
} : {}
|
|
2823
2914
|
})
|
|
2824
2915
|
);
|
|
2825
2916
|
}
|
|
@@ -2841,7 +2932,14 @@ function encodeEnvelope(args) {
|
|
|
2841
2932
|
type: "text",
|
|
2842
2933
|
client_msg_id: args.clientMsgId,
|
|
2843
2934
|
text: args.text,
|
|
2844
|
-
...args.replyTo ? { reply_to: args.replyTo } : {}
|
|
2935
|
+
...args.replyTo ? { reply_to: args.replyTo } : {},
|
|
2936
|
+
...args.bodyRanges && args.bodyRanges.length > 0 ? {
|
|
2937
|
+
body_ranges: args.bodyRanges.map((r) => ({
|
|
2938
|
+
start: r.start,
|
|
2939
|
+
length: r.length,
|
|
2940
|
+
mentioned_user_id: r.mentionedUserId
|
|
2941
|
+
}))
|
|
2942
|
+
} : {}
|
|
2845
2943
|
};
|
|
2846
2944
|
return encodeUtf8(JSON.stringify(env));
|
|
2847
2945
|
}
|
|
@@ -2849,6 +2947,18 @@ function decodeEnvelope(bytes) {
|
|
|
2849
2947
|
const s = decodeUtf8(bytes);
|
|
2850
2948
|
try {
|
|
2851
2949
|
const o = JSON.parse(s);
|
|
2950
|
+
if (typeof o === "object" && o !== null && o.type === "delete") {
|
|
2951
|
+
return {
|
|
2952
|
+
type: "delete",
|
|
2953
|
+
text: null,
|
|
2954
|
+
clientMsgId: o.client_msg_id ?? "",
|
|
2955
|
+
replyTo: null,
|
|
2956
|
+
delete: {
|
|
2957
|
+
targetClientMsgId: o.target_client_msg_id ?? "",
|
|
2958
|
+
scope: o.scope ?? "everyone"
|
|
2959
|
+
}
|
|
2960
|
+
};
|
|
2961
|
+
}
|
|
2852
2962
|
if (typeof o === "object" && o !== null && o.type === "reaction") {
|
|
2853
2963
|
return {
|
|
2854
2964
|
type: "reaction",
|
|
@@ -2863,6 +2973,7 @@ function decodeEnvelope(bytes) {
|
|
|
2863
2973
|
};
|
|
2864
2974
|
}
|
|
2865
2975
|
if (typeof o === "object" && o !== null && o.type === "edit") {
|
|
2976
|
+
const editRanges = decodeBodyRanges(o.body_ranges);
|
|
2866
2977
|
return {
|
|
2867
2978
|
type: "edit",
|
|
2868
2979
|
text: null,
|
|
@@ -2871,15 +2982,18 @@ function decodeEnvelope(bytes) {
|
|
|
2871
2982
|
edit: {
|
|
2872
2983
|
targetClientMsgId: o.target_client_msg_id ?? "",
|
|
2873
2984
|
newText: o.new_text ?? ""
|
|
2874
|
-
}
|
|
2985
|
+
},
|
|
2986
|
+
...editRanges ? { bodyRanges: editRanges } : {}
|
|
2875
2987
|
};
|
|
2876
2988
|
}
|
|
2877
2989
|
if (typeof o === "object" && o !== null && o.type === "text" && typeof o.v === "number") {
|
|
2990
|
+
const textRanges = decodeBodyRanges(o.body_ranges);
|
|
2878
2991
|
return {
|
|
2879
2992
|
type: "text",
|
|
2880
2993
|
text: o.text ?? null,
|
|
2881
2994
|
clientMsgId: o.client_msg_id ?? "",
|
|
2882
|
-
replyTo: o.reply_to ?? null
|
|
2995
|
+
replyTo: o.reply_to ?? null,
|
|
2996
|
+
...textRanges ? { bodyRanges: textRanges } : {}
|
|
2883
2997
|
};
|
|
2884
2998
|
}
|
|
2885
2999
|
if (typeof o === "object" && o !== null && o.type === "text") {
|
|
@@ -2891,6 +3005,14 @@ function decodeEnvelope(bytes) {
|
|
|
2891
3005
|
}
|
|
2892
3006
|
return { text: s, clientMsgId: "", replyTo: null };
|
|
2893
3007
|
}
|
|
3008
|
+
function decodeBodyRanges(raw) {
|
|
3009
|
+
if (!raw || raw.length === 0) return void 0;
|
|
3010
|
+
return raw.map((r) => ({
|
|
3011
|
+
start: r.start,
|
|
3012
|
+
length: r.length,
|
|
3013
|
+
mentionedUserId: r.mentioned_user_id
|
|
3014
|
+
}));
|
|
3015
|
+
}
|
|
2894
3016
|
function resolveReply(ref, lookup) {
|
|
2895
3017
|
const parent = lookup(ref.client_msg_id);
|
|
2896
3018
|
if (parent !== null) {
|
|
@@ -3102,9 +3224,9 @@ var GroupMessaging = class {
|
|
|
3102
3224
|
/** Send a text message. Encrypt at the current epoch, POST, return the receipt +
|
|
3103
3225
|
* the client-minted `clientMsgId` (needed by the Chat layer for reply indexing).
|
|
3104
3226
|
* NEVER rebases (a 422 stale_application surfaces to the caller). */
|
|
3105
|
-
async sendText(group, text, replyTo) {
|
|
3227
|
+
async sendText(group, text, replyTo, bodyRanges) {
|
|
3106
3228
|
const clientMsgId = mintClientMsgId();
|
|
3107
|
-
const plaintext = encodeEnvelope({ text, clientMsgId, replyTo });
|
|
3229
|
+
const plaintext = encodeEnvelope({ text, clientMsgId, replyTo, bodyRanges });
|
|
3108
3230
|
const ct = await this.engine.encryptApplication(fromBase64(group.rfcGroupId), plaintext);
|
|
3109
3231
|
const body = {
|
|
3110
3232
|
ciphertext_b64: toBase64(ct),
|
|
@@ -3130,7 +3252,10 @@ var GroupMessaging = class {
|
|
|
3130
3252
|
previewBody: replyTo.preview?.body ?? null,
|
|
3131
3253
|
previewAuthorUserId: replyTo.preview?.author_user_id ?? null,
|
|
3132
3254
|
previewKind: replyTo.preview?.kind ?? "text"
|
|
3133
|
-
} : null
|
|
3255
|
+
} : null,
|
|
3256
|
+
// Persist the OUTGOING bubble's mention ranges so an own-sent mention re-resolves
|
|
3257
|
+
// onto its bubble after a reload (own-send reload parity — the T4-reviewer Minor).
|
|
3258
|
+
...bodyRanges && bodyRanges.length > 0 ? { bodyRanges } : {}
|
|
3134
3259
|
};
|
|
3135
3260
|
try {
|
|
3136
3261
|
await this.messageStore.append(group.rfcGroupId, stored);
|
|
@@ -3198,7 +3323,8 @@ var GroupMessaging = class {
|
|
|
3198
3323
|
const plaintext = encodeEdit({
|
|
3199
3324
|
clientMsgId: args.clientMsgId,
|
|
3200
3325
|
targetClientMsgId: args.targetClientMsgId,
|
|
3201
|
-
newText: args.newText
|
|
3326
|
+
newText: args.newText,
|
|
3327
|
+
bodyRanges: args.bodyRanges
|
|
3202
3328
|
});
|
|
3203
3329
|
const ct = await this.engine.encryptApplication(fromBase64(group.rfcGroupId), plaintext);
|
|
3204
3330
|
const body = {
|
|
@@ -3224,7 +3350,59 @@ var GroupMessaging = class {
|
|
|
3224
3350
|
envelopeType: "edit",
|
|
3225
3351
|
edit: {
|
|
3226
3352
|
targetClientMsgId: args.targetClientMsgId,
|
|
3227
|
-
newText: args.newText
|
|
3353
|
+
newText: args.newText,
|
|
3354
|
+
// Persist the edit's REPLACEMENT ranges so the edited message's mentions
|
|
3355
|
+
// re-resolve from this edit after a reload (own-send reload parity — T6).
|
|
3356
|
+
...args.bodyRanges && args.bodyRanges.length > 0 ? { bodyRanges: args.bodyRanges } : {}
|
|
3357
|
+
}
|
|
3358
|
+
};
|
|
3359
|
+
try {
|
|
3360
|
+
await this.messageStore.append(group.rfcGroupId, stored);
|
|
3361
|
+
} catch {
|
|
3362
|
+
}
|
|
3363
|
+
return {
|
|
3364
|
+
receipt: { serverSeq: wire.server_seq, epoch: wire.epoch },
|
|
3365
|
+
clientMsgId: args.clientMsgId
|
|
3366
|
+
};
|
|
3367
|
+
}
|
|
3368
|
+
/** Send a delete-for-everyone tombstone on a target message. Encrypts a
|
|
3369
|
+
* `type:'delete'` envelope at the current epoch and sends through the SAME MLS
|
|
3370
|
+
* application path as `sendText` (the server stays blind — a delete is just
|
|
3371
|
+
* another opaque application message; the original ciphertext row is NOT
|
|
3372
|
+
* removed). Persists the outgoing delete row so the tombstone re-folds onto its
|
|
3373
|
+
* target after a reload (the own-send half of the reload parity — the iOS-review
|
|
3374
|
+
* CRITICAL boundary; the projection's `.delete` branch re-folds it). NEVER
|
|
3375
|
+
* rebases (epoch-bound like any application message). */
|
|
3376
|
+
async sendDelete(group, args) {
|
|
3377
|
+
const plaintext = encodeDelete({
|
|
3378
|
+
clientMsgId: args.clientMsgId,
|
|
3379
|
+
targetClientMsgId: args.targetClientMsgId
|
|
3380
|
+
});
|
|
3381
|
+
const ct = await this.engine.encryptApplication(fromBase64(group.rfcGroupId), plaintext);
|
|
3382
|
+
const body = {
|
|
3383
|
+
ciphertext_b64: toBase64(ct),
|
|
3384
|
+
client_idem_key: randomId()
|
|
3385
|
+
};
|
|
3386
|
+
const wire = await palbeRequest(
|
|
3387
|
+
this.rt,
|
|
3388
|
+
"POST",
|
|
3389
|
+
MessagingPaths.groupMessages(group.displayId),
|
|
3390
|
+
{ body }
|
|
3391
|
+
);
|
|
3392
|
+
const stored = {
|
|
3393
|
+
id: `${group.rfcGroupId}#${wire.server_seq}`,
|
|
3394
|
+
direction: "outgoing",
|
|
3395
|
+
text: null,
|
|
3396
|
+
senderDeviceId: this.selfDeviceId,
|
|
3397
|
+
epoch: wire.epoch,
|
|
3398
|
+
serverSeq: wire.server_seq,
|
|
3399
|
+
at: Date.now(),
|
|
3400
|
+
clientMsgId: args.clientMsgId,
|
|
3401
|
+
replyTo: null,
|
|
3402
|
+
envelopeType: "delete",
|
|
3403
|
+
delete: {
|
|
3404
|
+
targetClientMsgId: args.targetClientMsgId,
|
|
3405
|
+
scope: "everyone"
|
|
3228
3406
|
}
|
|
3229
3407
|
};
|
|
3230
3408
|
try {
|
|
@@ -3297,6 +3475,41 @@ var GroupMessaging = class {
|
|
|
3297
3475
|
}
|
|
3298
3476
|
};
|
|
3299
3477
|
|
|
3478
|
+
// src/messaging/mention-ranges.ts
|
|
3479
|
+
function normalizeMentionRangesUtf16(ranges, text) {
|
|
3480
|
+
const n = text.length;
|
|
3481
|
+
function splitsSurrogatePair(index) {
|
|
3482
|
+
if (index <= 0 || index >= n) return false;
|
|
3483
|
+
const before = text.charCodeAt(index - 1);
|
|
3484
|
+
const at = text.charCodeAt(index);
|
|
3485
|
+
const beforeIsHigh = before >= 55296 && before <= 56319;
|
|
3486
|
+
const atIsLow = at >= 56320 && at <= 57343;
|
|
3487
|
+
return beforeIsHigh && atIsLow;
|
|
3488
|
+
}
|
|
3489
|
+
const survivors = [];
|
|
3490
|
+
for (let idx = 0; idx < ranges.length; idx++) {
|
|
3491
|
+
const r = ranges[idx];
|
|
3492
|
+
if (r === void 0) continue;
|
|
3493
|
+
if (r.start < 0 || r.length <= 0 || r.start + r.length > n) continue;
|
|
3494
|
+
if (splitsSurrogatePair(r.start) || splitsSurrogatePair(r.start + r.length)) continue;
|
|
3495
|
+
survivors.push({ idx, range: r });
|
|
3496
|
+
}
|
|
3497
|
+
survivors.sort((lhs, rhs) => {
|
|
3498
|
+
if (lhs.range.start !== rhs.range.start) return lhs.range.start - rhs.range.start;
|
|
3499
|
+
if (lhs.range.length !== rhs.range.length) return rhs.range.length - lhs.range.length;
|
|
3500
|
+
return lhs.idx - rhs.idx;
|
|
3501
|
+
});
|
|
3502
|
+
const kept = [];
|
|
3503
|
+
let prevEnd = Number.NEGATIVE_INFINITY;
|
|
3504
|
+
for (const s of survivors) {
|
|
3505
|
+
if (s.range.start >= prevEnd) {
|
|
3506
|
+
kept.push(s.range);
|
|
3507
|
+
prevEnd = s.range.start + s.range.length;
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
return kept;
|
|
3511
|
+
}
|
|
3512
|
+
|
|
3300
3513
|
// src/messaging/reaction-fold.ts
|
|
3301
3514
|
function orderLte(aEpoch, aSeq, bEpoch, bSeq) {
|
|
3302
3515
|
if (aEpoch !== bEpoch) return aEpoch < bEpoch;
|
|
@@ -3353,6 +3566,7 @@ var ReactionFold = class {
|
|
|
3353
3566
|
};
|
|
3354
3567
|
|
|
3355
3568
|
// src/messaging/chat.ts
|
|
3569
|
+
var DELETED_DESCRIPTOR = "\u{1F6AB} This message was deleted";
|
|
3356
3570
|
var Chat = class {
|
|
3357
3571
|
/** Stable, URL/log-safe id (the grp_ display id once active; a reserved local id while draft). */
|
|
3358
3572
|
id;
|
|
@@ -3374,6 +3588,22 @@ var Chat = class {
|
|
|
3374
3588
|
reactionFold = new ReactionFold();
|
|
3375
3589
|
/** The single authoritative edit fold for this chat (live + own-send + history). */
|
|
3376
3590
|
editFold = new EditFold();
|
|
3591
|
+
/** The single authoritative delete-for-everyone fold (live + own-send + history).
|
|
3592
|
+
* A tombstone scrubs its target in place (delete DOMINATES edit at render). */
|
|
3593
|
+
deleteFold = new DeleteFold();
|
|
3594
|
+
/** delete-for-me suppression keys (clientMsgId, or `seq:<serverSeq>` for legacy)
|
|
3595
|
+
* — the message is OMITTED from this view. Local + persisted per chat, NO wire. */
|
|
3596
|
+
suppressed = /* @__PURE__ */ new Set();
|
|
3597
|
+
/** True once the persisted suppression set has been loaded (so the omit applies
|
|
3598
|
+
* even on the cold-launch hydrate path before a fresh deleteForMe). */
|
|
3599
|
+
suppressedLoaded = false;
|
|
3600
|
+
/** Self-elevation dedup keys (`<selfUserId>|<clientMsgId or seq:n>`). Once a
|
|
3601
|
+
* mention of me from another sender fires `onMentionElevation`, its key lands here
|
|
3602
|
+
* + is persisted, so a re-delivery / cold-launch re-hydrate never re-fires. */
|
|
3603
|
+
elevated = /* @__PURE__ */ new Set();
|
|
3604
|
+
/** True once the persisted elevation set has been loaded (so a re-delivered mention
|
|
3605
|
+
* on the cold-launch hydrate path dedups against the persisted decision). */
|
|
3606
|
+
elevatedLoaded = false;
|
|
3377
3607
|
/** Per-target BASE (original) text, so the rendered text is `edit ?? original`.
|
|
3378
3608
|
* Seeded once per target (write-once base) so a 2nd own edit can't corrupt it. */
|
|
3379
3609
|
originalTextByClientMsgId = /* @__PURE__ */ new Map();
|
|
@@ -3385,6 +3615,14 @@ var Chat = class {
|
|
|
3385
3615
|
wired = false;
|
|
3386
3616
|
liveUnsub = null;
|
|
3387
3617
|
listeners = /* @__PURE__ */ new Set();
|
|
3618
|
+
/**
|
|
3619
|
+
* Fires ONCE per `(selfUserId, clientMsgId)` when an INCOMING message mentions THIS
|
|
3620
|
+
* user from ANOTHER sender (not an edit). The dedup survives re-delivery + reload
|
|
3621
|
+
* via the persisted elevation set, so this never double-fires for one mention. The
|
|
3622
|
+
* app wires it to a buzz/badge (e.g. an in-app banner). Best-effort cooperative —
|
|
3623
|
+
* the SDK guarantees the DECISION, not the buzz. Mirrors iOS `Chat.onMentionElevation`.
|
|
3624
|
+
*/
|
|
3625
|
+
onMentionElevation;
|
|
3388
3626
|
/** @internal — obtain via pb.messaging.directChat / groupChat / chat(id). */
|
|
3389
3627
|
constructor(args) {
|
|
3390
3628
|
this.backend = args.backend;
|
|
@@ -3422,7 +3660,7 @@ var Chat = class {
|
|
|
3422
3660
|
return this.kind === "direct";
|
|
3423
3661
|
}
|
|
3424
3662
|
get messages() {
|
|
3425
|
-
return this.
|
|
3663
|
+
return this.surfaced();
|
|
3426
3664
|
}
|
|
3427
3665
|
get members() {
|
|
3428
3666
|
return this.memberCache;
|
|
@@ -3431,13 +3669,50 @@ var Chat = class {
|
|
|
3431
3669
|
return this.typingList;
|
|
3432
3670
|
}
|
|
3433
3671
|
get lastMessage() {
|
|
3434
|
-
return this.
|
|
3672
|
+
return this.surfaced().at(-1) ?? null;
|
|
3435
3673
|
}
|
|
3436
3674
|
get unreadCount() {
|
|
3437
|
-
return this.
|
|
3438
|
-
(m) => m.direction === "incoming" && m.serverSeq > this.readWatermark
|
|
3675
|
+
return this.surfaced().filter(
|
|
3676
|
+
(m) => m.direction === "incoming" && !m.isDeleted && m.serverSeq > this.readWatermark
|
|
3439
3677
|
).length;
|
|
3440
3678
|
}
|
|
3679
|
+
/**
|
|
3680
|
+
* The RENDER PRECEDENCE — the single composition point (live AND history project
|
|
3681
|
+
* through it identically). Over the raw `messageList` (which already carries the
|
|
3682
|
+
* folded edit text + reactions + reply):
|
|
3683
|
+
* (1) in the delete-for-me suppression set → OMIT the message entirely;
|
|
3684
|
+
* (2) else tombstoned (delete-for-everyone) → the neutral "deleted" descriptor
|
|
3685
|
+
* with reactions/reply/edit HIDDEN (delete DOMINATES edit — short-circuit);
|
|
3686
|
+
* (3) else the row as-is (edit overlay + reactions + reply already applied).
|
|
3687
|
+
* Pure over (messageList, deleteFold, suppressed) — recomputed on every read so a
|
|
3688
|
+
* just-folded delete / just-suppressed key takes effect without rewriting rows.
|
|
3689
|
+
*/
|
|
3690
|
+
surfaced() {
|
|
3691
|
+
const out = [];
|
|
3692
|
+
for (const m of this.messageList) {
|
|
3693
|
+
const key = this.suppressionKey(m);
|
|
3694
|
+
if (this.suppressed.has(key)) continue;
|
|
3695
|
+
const tombstoned = m.clientMsgId && this.deleteFold.isTombstoned(m.clientMsgId) || m.isDeleted;
|
|
3696
|
+
if (tombstoned) {
|
|
3697
|
+
out.push({
|
|
3698
|
+
...m,
|
|
3699
|
+
text: DELETED_DESCRIPTOR,
|
|
3700
|
+
reactions: {},
|
|
3701
|
+
replyTo: null,
|
|
3702
|
+
edited: false,
|
|
3703
|
+
isDeleted: true,
|
|
3704
|
+
mentions: []
|
|
3705
|
+
});
|
|
3706
|
+
continue;
|
|
3707
|
+
}
|
|
3708
|
+
out.push(m);
|
|
3709
|
+
}
|
|
3710
|
+
return out;
|
|
3711
|
+
}
|
|
3712
|
+
/** The delete-for-me suppression key: clientMsgId when present, else `seq:<n>`. */
|
|
3713
|
+
suppressionKey(m) {
|
|
3714
|
+
return m.clientMsgId ? m.clientMsgId : `seq:${m.serverSeq}`;
|
|
3715
|
+
}
|
|
3441
3716
|
get title() {
|
|
3442
3717
|
if (this.titleOverride) return this.titleOverride;
|
|
3443
3718
|
if (this._group?.name) return this._group.name;
|
|
@@ -3461,9 +3736,40 @@ var Chat = class {
|
|
|
3461
3736
|
if (this.wired || this._state !== "active" || !this._group) return;
|
|
3462
3737
|
this.wired = true;
|
|
3463
3738
|
this.liveUnsub = this.backend.subscribeLive(this._group, this);
|
|
3739
|
+
void this.loadSuppressed();
|
|
3740
|
+
void this.loadElevated();
|
|
3464
3741
|
void this.hydrateHistory();
|
|
3465
3742
|
void this.refreshMembers();
|
|
3466
3743
|
}
|
|
3744
|
+
/** Hydrate the persisted delete-for-me suppression keys (once), then re-emit so
|
|
3745
|
+
* any already-surfaced suppressed message is omitted (cold-launch parity). */
|
|
3746
|
+
async loadSuppressed() {
|
|
3747
|
+
if (this.suppressedLoaded || !this._group) return;
|
|
3748
|
+
this.suppressedLoaded = true;
|
|
3749
|
+
try {
|
|
3750
|
+
const keys = await this.backend.loadSuppressed(this._group);
|
|
3751
|
+
let changed = false;
|
|
3752
|
+
for (const k of keys) {
|
|
3753
|
+
if (!this.suppressed.has(k)) {
|
|
3754
|
+
this.suppressed.add(k);
|
|
3755
|
+
changed = true;
|
|
3756
|
+
}
|
|
3757
|
+
}
|
|
3758
|
+
if (changed) this.emit();
|
|
3759
|
+
} catch {
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3762
|
+
/** Hydrate the persisted self-elevation dedup keys (once). No re-emit: the set only
|
|
3763
|
+
* gates the elevation DECISION, it does not change what renders. */
|
|
3764
|
+
async loadElevated() {
|
|
3765
|
+
if (this.elevatedLoaded || !this._group) return;
|
|
3766
|
+
this.elevatedLoaded = true;
|
|
3767
|
+
try {
|
|
3768
|
+
const keys = await this.backend.loadElevated(this._group);
|
|
3769
|
+
for (const k of keys) this.elevated.add(k);
|
|
3770
|
+
} catch {
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3467
3773
|
async hydrateHistory() {
|
|
3468
3774
|
if (this.historyLoaded || !this._group) return;
|
|
3469
3775
|
this.historyLoaded = true;
|
|
@@ -3474,13 +3780,13 @@ var Chat = class {
|
|
|
3474
3780
|
let changed = false;
|
|
3475
3781
|
for (const m of incoming) {
|
|
3476
3782
|
if (m.serverSeq <= 0) continue;
|
|
3477
|
-
if (m.clientMsgId && m.text !== null) {
|
|
3783
|
+
if (m.clientMsgId && m.text !== null && !m.isDeleted) {
|
|
3478
3784
|
this.byClientMsgId.set(m.clientMsgId, {
|
|
3479
3785
|
text: m.text,
|
|
3480
3786
|
senderUserId: m.senderUserId ?? ""
|
|
3481
3787
|
});
|
|
3482
3788
|
}
|
|
3483
|
-
if (m.clientMsgId) {
|
|
3789
|
+
if (m.clientMsgId && !m.isDeleted) {
|
|
3484
3790
|
this.seedEditBase(m.clientMsgId, m.text, m.senderUserId ?? "");
|
|
3485
3791
|
}
|
|
3486
3792
|
}
|
|
@@ -3490,7 +3796,12 @@ var Chat = class {
|
|
|
3490
3796
|
const key = this.internalKey(m.serverSeq);
|
|
3491
3797
|
if (this.seenKeys.has(key)) continue;
|
|
3492
3798
|
this.seenKeys.add(key);
|
|
3493
|
-
|
|
3799
|
+
if (m.clientMsgId && !m.isDeleted) {
|
|
3800
|
+
this.deleteFold.reevaluatePending(m.clientMsgId, m.senderUserId);
|
|
3801
|
+
}
|
|
3802
|
+
this.messageList.push(
|
|
3803
|
+
this.applyEditOverlay(this.applyReactionTally(this.resolveMentionNames(m)))
|
|
3804
|
+
);
|
|
3494
3805
|
changed = true;
|
|
3495
3806
|
this.loadedEarliestSeq = Math.min(this.loadedEarliestSeq ?? m.serverSeq, m.serverSeq);
|
|
3496
3807
|
}
|
|
@@ -3540,19 +3851,38 @@ var Chat = class {
|
|
|
3540
3851
|
newText: incoming.edit.newText,
|
|
3541
3852
|
epoch: incoming.epoch,
|
|
3542
3853
|
serverSeq: incoming.serverSeq,
|
|
3543
|
-
eventClientMsgId: incoming.clientMsgId
|
|
3854
|
+
eventClientMsgId: incoming.clientMsgId,
|
|
3855
|
+
// Mentions T6: carry the edit's REPLACEMENT ranges so the edited message's
|
|
3856
|
+
// mentions reflect them (recomputed against the new text on recomputeEdit).
|
|
3857
|
+
bodyRanges: incoming.bodyRanges
|
|
3544
3858
|
},
|
|
3545
3859
|
this.authorOfTarget
|
|
3546
3860
|
);
|
|
3547
3861
|
this.recomputeEdit(incoming.edit.targetClientMsgId);
|
|
3548
3862
|
return;
|
|
3549
3863
|
}
|
|
3864
|
+
if (incoming.envelopeType === "delete" && incoming.delete) {
|
|
3865
|
+
const actorUserId = direction === "outgoing" ? this.backend.selfUserId : senderUser;
|
|
3866
|
+
this.deleteFold.ingest(
|
|
3867
|
+
{
|
|
3868
|
+
targetClientMsgId: incoming.delete.targetClientMsgId,
|
|
3869
|
+
actorUserId,
|
|
3870
|
+
epoch: incoming.epoch,
|
|
3871
|
+
serverSeq: incoming.serverSeq,
|
|
3872
|
+
eventClientMsgId: incoming.clientMsgId
|
|
3873
|
+
},
|
|
3874
|
+
this.authorOfTarget
|
|
3875
|
+
);
|
|
3876
|
+
this.emit();
|
|
3877
|
+
return;
|
|
3878
|
+
}
|
|
3550
3879
|
const incomingClientMsgId = incoming.clientMsgId;
|
|
3551
3880
|
const incomingReplyRef = incoming.replyRef;
|
|
3552
3881
|
let resolvedReplyTo = null;
|
|
3553
3882
|
if (incomingReplyRef) {
|
|
3554
3883
|
resolvedReplyTo = resolveReply(incomingReplyRef, (id) => this.byClientMsgId.get(id) ?? null);
|
|
3555
3884
|
}
|
|
3885
|
+
const mentions = this.resolveMentions(incoming.text, incoming.bodyRanges);
|
|
3556
3886
|
const msg = {
|
|
3557
3887
|
id: this.publicId(incoming.serverSeq),
|
|
3558
3888
|
kind: this.kindOf(incoming),
|
|
@@ -3567,8 +3897,12 @@ var Chat = class {
|
|
|
3567
3897
|
// BEFORE its target — the dangling case — renders the moment the target lands).
|
|
3568
3898
|
reactions: incomingClientMsgId ? this.reactionFold.tally(incomingClientMsgId) : {},
|
|
3569
3899
|
// Default false; applyEditOverlay below folds any edit that arrived first.
|
|
3570
|
-
edited: false
|
|
3900
|
+
edited: false,
|
|
3901
|
+
// Default false; surfaced() applies the tombstone scrub if a delete folded.
|
|
3902
|
+
isDeleted: false,
|
|
3903
|
+
mentions
|
|
3571
3904
|
};
|
|
3905
|
+
this.elevateIfMentioned(msg, mentions, senderUser, incoming.envelopeType);
|
|
3572
3906
|
if (incomingClientMsgId && incoming.text !== null) {
|
|
3573
3907
|
this.byClientMsgId.set(incomingClientMsgId, {
|
|
3574
3908
|
text: incoming.text,
|
|
@@ -3578,6 +3912,7 @@ var Chat = class {
|
|
|
3578
3912
|
if (incomingClientMsgId) {
|
|
3579
3913
|
this.seedEditBase(incomingClientMsgId, incoming.text, senderUser);
|
|
3580
3914
|
this.editFold.reevaluateHeld(this.authorOfTarget);
|
|
3915
|
+
this.deleteFold.reevaluatePending(incomingClientMsgId, senderUser);
|
|
3581
3916
|
}
|
|
3582
3917
|
this.messageList.push(this.applyEditOverlay(msg));
|
|
3583
3918
|
this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
|
|
@@ -3591,6 +3926,75 @@ var Chat = class {
|
|
|
3591
3926
|
* (null = target unknown/dangling → the fold HOLDs). Captured as a bound arrow
|
|
3592
3927
|
* so it can be passed to the pure EditFold. */
|
|
3593
3928
|
authorOfTarget = (targetClientMsgId) => this.authorByClientMsgId.get(targetClientMsgId) ?? null;
|
|
3929
|
+
// ── Mentions (mentions T6) ──
|
|
3930
|
+
/** Resolve a message's decode-time mention spans: NORMALIZE the raw `body_ranges`
|
|
3931
|
+
* against `text` (the pinned `normalizeMentionRangesUtf16` cross-SDK contract) then
|
|
3932
|
+
* resolve each surviving range's `mentionedUserId` to a roster display name. An id
|
|
3933
|
+
* not in the roster resolves to `null` (the renderer falls back to the `text` slice).
|
|
3934
|
+
* Pure over (text, bodyRanges, memberCache); never throws. Mirrors iOS T3. */
|
|
3935
|
+
resolveMentions(text, bodyRanges) {
|
|
3936
|
+
if (text === null || !bodyRanges || bodyRanges.length === 0) return [];
|
|
3937
|
+
const normalized = normalizeMentionRangesUtf16(bodyRanges, text);
|
|
3938
|
+
if (normalized.length === 0) return [];
|
|
3939
|
+
return normalized.map((r) => ({
|
|
3940
|
+
start: r.start,
|
|
3941
|
+
length: r.length,
|
|
3942
|
+
mentionedUserId: r.mentionedUserId,
|
|
3943
|
+
displayName: this.displayNameOf(r.mentionedUserId)
|
|
3944
|
+
}));
|
|
3945
|
+
}
|
|
3946
|
+
/** Re-resolve the roster display name on already-NORMALIZED spans (the history
|
|
3947
|
+
* projection produces them with null names — resolution is LIVE, not snapshotted).
|
|
3948
|
+
* A member rename then reflects on old messages. Returns the message unchanged when
|
|
3949
|
+
* it has no mentions (the common case) or no name changed. Mirrors iOS T3. */
|
|
3950
|
+
resolveMentionNames(m) {
|
|
3951
|
+
if (!m.mentions || m.mentions.length === 0) {
|
|
3952
|
+
return m.mentions ? m : { ...m, mentions: [] };
|
|
3953
|
+
}
|
|
3954
|
+
let changed = false;
|
|
3955
|
+
const reresolved = m.mentions.map((span) => {
|
|
3956
|
+
const name = this.displayNameOf(span.mentionedUserId);
|
|
3957
|
+
if (name === span.displayName) return span;
|
|
3958
|
+
changed = true;
|
|
3959
|
+
return { ...span, displayName: name };
|
|
3960
|
+
});
|
|
3961
|
+
if (!changed) return m;
|
|
3962
|
+
return { ...m, mentions: reresolved };
|
|
3963
|
+
}
|
|
3964
|
+
/** The WINNING edit's resolved mentions for a target (normalize its replacement
|
|
3965
|
+
* ranges against the new text + roster names), or `[]` if no winning edit / no
|
|
3966
|
+
* ranges. The edited message's mentions reflect the EDIT's ranges (mirrors iOS T3). */
|
|
3967
|
+
editMentions(targetClientMsgId, newText) {
|
|
3968
|
+
const ranges = this.editFold.bodyRanges(targetClientMsgId);
|
|
3969
|
+
if (!ranges) return [];
|
|
3970
|
+
return this.resolveMentions(newText, ranges);
|
|
3971
|
+
}
|
|
3972
|
+
/** Resolve a userId → its roster display name (null if not a known member). */
|
|
3973
|
+
displayNameOf(userId) {
|
|
3974
|
+
return this.memberCache.find((mm) => mm.userId === userId)?.displayName ?? null;
|
|
3975
|
+
}
|
|
3976
|
+
/** Compute the SELF-ELEVATION decision for a freshly-ingested INCOMING bubble and,
|
|
3977
|
+
* when it fires, record the dedup key (persisted) + invoke `onMentionElevation`.
|
|
3978
|
+
* Gate (mirrors iOS T3): a surviving mention targets THIS user AND the sender is not
|
|
3979
|
+
* me AND it's NOT an edit AND the `(selfUserId, clientMsgId|seq)` key isn't already
|
|
3980
|
+
* elevated. Dedup-once: the in-memory set gates the session, the persisted set
|
|
3981
|
+
* survives reload. An EDIT never reaches here (it folds, not a bubble) — the
|
|
3982
|
+
* `envelopeType !== 'edit'` guard is belt-and-braces. */
|
|
3983
|
+
elevateIfMentioned(message, mentions, senderUserId, envelopeType) {
|
|
3984
|
+
const me = this.backend.selfUserId;
|
|
3985
|
+
if (envelopeType === "edit") return;
|
|
3986
|
+
if (senderUserId === me) return;
|
|
3987
|
+
if (!mentions.some((mm) => mm.mentionedUserId === me)) return;
|
|
3988
|
+
const idPart = message.clientMsgId ? message.clientMsgId : `seq:${message.serverSeq}`;
|
|
3989
|
+
const key = `${me}|${idPart}`;
|
|
3990
|
+
if (this.elevated.has(key)) return;
|
|
3991
|
+
this.elevated.add(key);
|
|
3992
|
+
if (this._group) {
|
|
3993
|
+
void this.backend.saveElevated(this._group, [...this.elevated]).catch(() => {
|
|
3994
|
+
});
|
|
3995
|
+
}
|
|
3996
|
+
this.onMentionElevation?.(message);
|
|
3997
|
+
}
|
|
3594
3998
|
/** Seed the per-target base text + author for the edit fold. Base is write-once
|
|
3595
3999
|
* (a later own/peer edit must not overwrite the original we render against). The
|
|
3596
4000
|
* author is (re)recorded whenever a non-empty resolution is available. */
|
|
@@ -3647,9 +4051,10 @@ var Chat = class {
|
|
|
3647
4051
|
const base = this.originalTextByClientMsgId.has(targetClientMsgId) ? this.originalTextByClientMsgId.get(targetClientMsgId) ?? null : m.text;
|
|
3648
4052
|
const text = editText ?? base;
|
|
3649
4053
|
const edited = foldEdited || m.edited;
|
|
3650
|
-
|
|
4054
|
+
const mentions = editText !== null ? this.editMentions(targetClientMsgId, text) : m.mentions;
|
|
4055
|
+
if (m.text === text && m.edited === edited && sameMentions(m.mentions, mentions)) return m;
|
|
3651
4056
|
changed = true;
|
|
3652
|
-
return { ...m, text, edited };
|
|
4057
|
+
return { ...m, text, edited, mentions };
|
|
3653
4058
|
});
|
|
3654
4059
|
if (changed) this.emit();
|
|
3655
4060
|
}
|
|
@@ -3666,8 +4071,9 @@ var Chat = class {
|
|
|
3666
4071
|
if (editText === null && !foldEdited) return m;
|
|
3667
4072
|
const text = editText ?? m.text;
|
|
3668
4073
|
const edited = foldEdited || m.edited;
|
|
3669
|
-
|
|
3670
|
-
|
|
4074
|
+
const mentions = editText !== null ? this.editMentions(m.clientMsgId, text) : m.mentions;
|
|
4075
|
+
if (m.text === text && m.edited === edited && sameMentions(m.mentions, mentions)) return m;
|
|
4076
|
+
return { ...m, text, edited, mentions };
|
|
3671
4077
|
}
|
|
3672
4078
|
/** @internal — called by the backend's conv subscription. */
|
|
3673
4079
|
applyConv(event, payload) {
|
|
@@ -3725,6 +4131,18 @@ var Chat = class {
|
|
|
3725
4131
|
}
|
|
3726
4132
|
this.editFold.reevaluateHeld(this.authorOfTarget);
|
|
3727
4133
|
for (const cid of this.originalTextByClientMsgId.keys()) this.recomputeEdit(cid);
|
|
4134
|
+
this.reresolveAllMentionNames();
|
|
4135
|
+
}
|
|
4136
|
+
/** Re-resolve roster display names across the whole transcript (called on a roster
|
|
4137
|
+
* change). Re-emits only if any name actually changed. */
|
|
4138
|
+
reresolveAllMentionNames() {
|
|
4139
|
+
let changed = false;
|
|
4140
|
+
this.messageList = this.messageList.map((m) => {
|
|
4141
|
+
const reresolved = this.resolveMentionNames(m);
|
|
4142
|
+
if (reresolved !== m) changed = true;
|
|
4143
|
+
return reresolved;
|
|
4144
|
+
});
|
|
4145
|
+
if (changed) this.emit();
|
|
3728
4146
|
}
|
|
3729
4147
|
seedMembersFromGroup(group) {
|
|
3730
4148
|
const seed = [
|
|
@@ -3792,11 +4210,12 @@ var Chat = class {
|
|
|
3792
4210
|
};
|
|
3793
4211
|
resolvedReplyTo = resolveReply(replyRef, (id) => this.byClientMsgId.get(id) ?? null);
|
|
3794
4212
|
}
|
|
3795
|
-
const
|
|
3796
|
-
this.
|
|
4213
|
+
const bodyRanges = opts?.mentions ?? null;
|
|
4214
|
+
const { receipt, clientMsgId } = await this.backend.sendText(group, text, replyRef, bodyRanges);
|
|
4215
|
+
this.appendOwnSend(text, receipt, clientMsgId, resolvedReplyTo, bodyRanges);
|
|
3797
4216
|
return receipt;
|
|
3798
4217
|
}
|
|
3799
|
-
appendOwnSend(text, receipt, clientMsgId, resolvedReplyTo) {
|
|
4218
|
+
appendOwnSend(text, receipt, clientMsgId, resolvedReplyTo, bodyRanges) {
|
|
3800
4219
|
if (receipt.serverSeq <= 0) return;
|
|
3801
4220
|
const key = this.internalKey(receipt.serverSeq);
|
|
3802
4221
|
if (this.seenKeys.has(key)) return;
|
|
@@ -3819,7 +4238,12 @@ var Chat = class {
|
|
|
3819
4238
|
// the dangling-target invariant uniform across every append path).
|
|
3820
4239
|
reactions: clientMsgId ? this.reactionFold.tally(clientMsgId) : {},
|
|
3821
4240
|
// Own-sent edits fold via edit() after the fact; new sends start unedited.
|
|
3822
|
-
edited: false
|
|
4241
|
+
edited: false,
|
|
4242
|
+
// Own-sent deletes fold via deleteForEveryone() after the fact; start live.
|
|
4243
|
+
isDeleted: false,
|
|
4244
|
+
// Mentions T6: resolve the own-sent bubble's mentions for its own render (the
|
|
4245
|
+
// sender never gets a wire echo of its own message — this is the only local copy).
|
|
4246
|
+
mentions: this.resolveMentions(text, bodyRanges)
|
|
3823
4247
|
});
|
|
3824
4248
|
this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
|
|
3825
4249
|
this.emit();
|
|
@@ -3907,15 +4331,18 @@ var Chat = class {
|
|
|
3907
4331
|
* instantly; the durable echo on the next pump is a fold no-op (dedup on the
|
|
3908
4332
|
* SAME wire clientMsgId). Never appends a bubble; PRESERVES the target's
|
|
3909
4333
|
* reactions + reply context. Only the original author's edits count — for an own
|
|
3910
|
-
* message self IS the author, so the author-gate passes.
|
|
3911
|
-
|
|
4334
|
+
* message self IS the author, so the author-gate passes. `opts.mentions` carries the
|
|
4335
|
+
* edit's REPLACEMENT mention ranges → the edited message's mentions reflect them. */
|
|
4336
|
+
async edit(message, newText, opts) {
|
|
3912
4337
|
if (!message.clientMsgId || message.kind !== "text") return;
|
|
3913
4338
|
const group = await this.materializeIfNeeded();
|
|
3914
4339
|
const clientMsgId = mintClientMsgId();
|
|
4340
|
+
const bodyRanges = opts?.mentions ?? null;
|
|
3915
4341
|
const { receipt } = await this.backend.sendEdit(group, {
|
|
3916
4342
|
clientMsgId,
|
|
3917
4343
|
targetClientMsgId: message.clientMsgId,
|
|
3918
|
-
newText
|
|
4344
|
+
newText,
|
|
4345
|
+
bodyRanges
|
|
3919
4346
|
});
|
|
3920
4347
|
this.seedEditBase(message.clientMsgId, message.text, this.backend.selfUserId);
|
|
3921
4348
|
this.editFold.ingest(
|
|
@@ -3925,13 +4352,72 @@ var Chat = class {
|
|
|
3925
4352
|
newText,
|
|
3926
4353
|
epoch: receipt.epoch,
|
|
3927
4354
|
serverSeq: receipt.serverSeq,
|
|
3928
|
-
eventClientMsgId: clientMsgId
|
|
4355
|
+
eventClientMsgId: clientMsgId,
|
|
4356
|
+
bodyRanges
|
|
3929
4357
|
},
|
|
3930
4358
|
this.authorOfTarget
|
|
3931
4359
|
);
|
|
3932
4360
|
this.recomputeEdit(message.clientMsgId);
|
|
3933
4361
|
}
|
|
4362
|
+
// ── Delete ──
|
|
4363
|
+
/** Delete a message for EVERYONE (a cooperative encrypted tombstone). Only the
|
|
4364
|
+
* ORIGINAL SENDER can do this — for an own message self IS the author, so the
|
|
4365
|
+
* author-gate passes. Legacy messages (empty clientMsgId) are DISABLED (a
|
|
4366
|
+
* tombstone keys on the target's clientMsgId, which they lack) — no-op. Sends a
|
|
4367
|
+
* `type:'delete'` envelope through the SAME MLS path as a text message (the
|
|
4368
|
+
* server stays blind), folds the own delete locally so the target scrubs in
|
|
4369
|
+
* place instantly (the durable echo dedups on the SAME wire clientMsgId), and
|
|
4370
|
+
* re-emits. NEVER appends a bubble. delete-for-me'ing the target becomes moot. */
|
|
4371
|
+
async deleteForEveryone(message) {
|
|
4372
|
+
if (!message.clientMsgId) return;
|
|
4373
|
+
const group = await this.materializeIfNeeded();
|
|
4374
|
+
const clientMsgId = mintClientMsgId();
|
|
4375
|
+
const { receipt } = await this.backend.sendDelete(group, {
|
|
4376
|
+
clientMsgId,
|
|
4377
|
+
targetClientMsgId: message.clientMsgId
|
|
4378
|
+
});
|
|
4379
|
+
this.seedEditBase(message.clientMsgId, message.text, this.backend.selfUserId);
|
|
4380
|
+
this.deleteFold.ingest(
|
|
4381
|
+
{
|
|
4382
|
+
targetClientMsgId: message.clientMsgId,
|
|
4383
|
+
actorUserId: this.backend.selfUserId,
|
|
4384
|
+
epoch: receipt.epoch,
|
|
4385
|
+
serverSeq: receipt.serverSeq,
|
|
4386
|
+
eventClientMsgId: clientMsgId
|
|
4387
|
+
},
|
|
4388
|
+
this.authorOfTarget
|
|
4389
|
+
);
|
|
4390
|
+
this.emit();
|
|
4391
|
+
}
|
|
4392
|
+
/** Delete a message for ME only — a LOCAL, per-device suppression. NO wire, NO
|
|
4393
|
+
* attribution, no server contact: the message is OMITTED from THIS view and the
|
|
4394
|
+
* suppression key persists per chat (survives reload). The key is the message's
|
|
4395
|
+
* clientMsgId when present, else `seq:<serverSeq>` for legacy messages. */
|
|
4396
|
+
async deleteForMe(message) {
|
|
4397
|
+
const key = message.clientMsgId ? message.clientMsgId : `seq:${message.serverSeq}`;
|
|
4398
|
+
if (this.suppressed.has(key)) return;
|
|
4399
|
+
this.suppressed.add(key);
|
|
4400
|
+
this.emit();
|
|
4401
|
+
if (this._group) {
|
|
4402
|
+
try {
|
|
4403
|
+
await this.backend.saveSuppressed(this._group, [...this.suppressed]);
|
|
4404
|
+
} catch {
|
|
4405
|
+
}
|
|
4406
|
+
}
|
|
4407
|
+
}
|
|
3934
4408
|
};
|
|
4409
|
+
function sameMentions(a, b) {
|
|
4410
|
+
if (a.length !== b.length) return false;
|
|
4411
|
+
for (let i = 0; i < a.length; i++) {
|
|
4412
|
+
const x = a[i];
|
|
4413
|
+
const y = b[i];
|
|
4414
|
+
if (!x || !y) return false;
|
|
4415
|
+
if (x.start !== y.start || x.length !== y.length || x.mentionedUserId !== y.mentionedUserId || x.displayName !== y.displayName) {
|
|
4416
|
+
return false;
|
|
4417
|
+
}
|
|
4418
|
+
}
|
|
4419
|
+
return true;
|
|
4420
|
+
}
|
|
3935
4421
|
function sameReactions(a, b) {
|
|
3936
4422
|
const ak = Object.keys(a);
|
|
3937
4423
|
const bk = Object.keys(b);
|
|
@@ -4111,6 +4597,7 @@ var MessageDeliverySource = class {
|
|
|
4111
4597
|
const { text, clientMsgId, replyTo } = decoded;
|
|
4112
4598
|
const isReaction = decoded.type === "reaction" && decoded.reaction != null;
|
|
4113
4599
|
const isEdit = decoded.type === "edit" && decoded.edit != null;
|
|
4600
|
+
const isDelete = decoded.type === "delete" && decoded.delete != null;
|
|
4114
4601
|
const senderDeviceId = row.sender_device_id ?? decodeSenderDeviceId(received.sender);
|
|
4115
4602
|
const stored = {
|
|
4116
4603
|
id: `${group.rfcGroupId}#${row.server_seq}`,
|
|
@@ -4142,12 +4629,31 @@ var MessageDeliverySource = class {
|
|
|
4142
4629
|
// Thread the edit discriminator + new text through the persisted row so an
|
|
4143
4630
|
// edit folded LIVE re-folds onto its target after a reload (the reload-parity
|
|
4144
4631
|
// boundary — mirrors iOS T3). Omitted for non-edits → old rows hydrate as
|
|
4145
|
-
// `'text'`/no-edit (backward-compat).
|
|
4632
|
+
// `'text'`/no-edit (backward-compat). The edit's replacement body_ranges ride
|
|
4633
|
+
// along so the edited message's mentions re-resolve on cold launch (T6).
|
|
4146
4634
|
...isEdit && decoded.edit ? {
|
|
4147
4635
|
envelopeType: "edit",
|
|
4148
4636
|
edit: {
|
|
4149
4637
|
targetClientMsgId: decoded.edit.targetClientMsgId,
|
|
4150
|
-
newText: decoded.edit.newText
|
|
4638
|
+
newText: decoded.edit.newText,
|
|
4639
|
+
...decoded.bodyRanges ? { bodyRanges: decoded.bodyRanges } : {}
|
|
4640
|
+
}
|
|
4641
|
+
} : {},
|
|
4642
|
+
// Thread the TEXT bubble's mention ranges (raw) through the persisted row so a
|
|
4643
|
+
// mention surfaced LIVE re-resolves onto its bubble after a reload (the
|
|
4644
|
+
// reload-parity boundary for mentions — T6, mirrors iOS T3). Only on a text
|
|
4645
|
+
// bubble (not a reaction/edit/delete row); omitted when absent (backward-compat).
|
|
4646
|
+
...!isReaction && !isEdit && !isDelete && decoded.bodyRanges ? { bodyRanges: decoded.bodyRanges } : {},
|
|
4647
|
+
// Thread the delete discriminator + target through the persisted row so a
|
|
4648
|
+
// delete-for-everyone tombstone folded LIVE re-folds onto its target after
|
|
4649
|
+
// a reload (the reload-parity boundary — the iOS-review CRITICAL lesson;
|
|
4650
|
+
// the projection's `.delete` branch re-folds it so it never leaks a blank
|
|
4651
|
+
// bubble). Omitted for non-deletes → old rows hydrate as `'text'`/no-delete.
|
|
4652
|
+
...isDelete && decoded.delete ? {
|
|
4653
|
+
envelopeType: "delete",
|
|
4654
|
+
delete: {
|
|
4655
|
+
targetClientMsgId: decoded.delete.targetClientMsgId,
|
|
4656
|
+
scope: decoded.delete.scope
|
|
4151
4657
|
}
|
|
4152
4658
|
} : {}
|
|
4153
4659
|
};
|
|
@@ -4168,7 +4674,11 @@ var MessageDeliverySource = class {
|
|
|
4168
4674
|
replyRef: replyTo,
|
|
4169
4675
|
envelopeType: decoded.type ?? "text",
|
|
4170
4676
|
reaction: isReaction ? decoded.reaction : null,
|
|
4171
|
-
edit: isEdit ? decoded.edit : null
|
|
4677
|
+
edit: isEdit ? decoded.edit : null,
|
|
4678
|
+
delete: isDelete ? decoded.delete : null,
|
|
4679
|
+
// The raw mention ranges (text bubble or the edit's replacement ranges); the
|
|
4680
|
+
// Chat normalizes + resolves names → ChatMessage.mentions (T6).
|
|
4681
|
+
bodyRanges: decoded.bodyRanges ?? null
|
|
4172
4682
|
});
|
|
4173
4683
|
return true;
|
|
4174
4684
|
}
|
|
@@ -4323,6 +4833,36 @@ var GroupCatalog = class {
|
|
|
4323
4833
|
}
|
|
4324
4834
|
};
|
|
4325
4835
|
|
|
4836
|
+
// src/messaging/mention-elevation.ts
|
|
4837
|
+
var MentionElevationStore = class {
|
|
4838
|
+
constructor(kv) {
|
|
4839
|
+
this.kv = kv;
|
|
4840
|
+
}
|
|
4841
|
+
kv;
|
|
4842
|
+
key(rfcGroupId) {
|
|
4843
|
+
return `elev:${rfcGroupId}`;
|
|
4844
|
+
}
|
|
4845
|
+
/** Load the persisted elevation keys for a chat (empty array if none). */
|
|
4846
|
+
async load(rfcGroupId) {
|
|
4847
|
+
const raw = await this.kv.get(this.key(rfcGroupId));
|
|
4848
|
+
if (!raw) return [];
|
|
4849
|
+
try {
|
|
4850
|
+
const parsed = JSON.parse(decodeUtf8(raw));
|
|
4851
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
4852
|
+
} catch {
|
|
4853
|
+
return [];
|
|
4854
|
+
}
|
|
4855
|
+
}
|
|
4856
|
+
/** Persist the full elevation key set for a chat (deterministic, deduped order). */
|
|
4857
|
+
async save(rfcGroupId, keys) {
|
|
4858
|
+
const sorted = [...new Set(keys)].sort();
|
|
4859
|
+
await this.kv.set(this.key(rfcGroupId), encodeUtf8(JSON.stringify(sorted)));
|
|
4860
|
+
}
|
|
4861
|
+
async wipe() {
|
|
4862
|
+
for (const k of await this.kv.keys("elev:")) await this.kv.delete(k);
|
|
4863
|
+
}
|
|
4864
|
+
};
|
|
4865
|
+
|
|
4326
4866
|
// src/messaging/wasm/pkg/palbe_mls_bg.js
|
|
4327
4867
|
var palbe_mls_bg_exports = {};
|
|
4328
4868
|
__export(palbe_mls_bg_exports, {
|
|
@@ -5989,6 +6529,36 @@ var SignatureKeyStore = class {
|
|
|
5989
6529
|
}
|
|
5990
6530
|
};
|
|
5991
6531
|
|
|
6532
|
+
// src/messaging/suppression.ts
|
|
6533
|
+
var SuppressionStore = class {
|
|
6534
|
+
constructor(kv) {
|
|
6535
|
+
this.kv = kv;
|
|
6536
|
+
}
|
|
6537
|
+
kv;
|
|
6538
|
+
key(rfcGroupId) {
|
|
6539
|
+
return `supp:${rfcGroupId}`;
|
|
6540
|
+
}
|
|
6541
|
+
/** Load the persisted suppression keys for a chat (empty array if none). */
|
|
6542
|
+
async load(rfcGroupId) {
|
|
6543
|
+
const raw = await this.kv.get(this.key(rfcGroupId));
|
|
6544
|
+
if (!raw) return [];
|
|
6545
|
+
try {
|
|
6546
|
+
const parsed = JSON.parse(decodeUtf8(raw));
|
|
6547
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
6548
|
+
} catch {
|
|
6549
|
+
return [];
|
|
6550
|
+
}
|
|
6551
|
+
}
|
|
6552
|
+
/** Persist the full suppression key set for a chat (deterministic order). */
|
|
6553
|
+
async save(rfcGroupId, keys) {
|
|
6554
|
+
const sorted = [...new Set(keys)].sort();
|
|
6555
|
+
await this.kv.set(this.key(rfcGroupId), encodeUtf8(JSON.stringify(sorted)));
|
|
6556
|
+
}
|
|
6557
|
+
async wipe() {
|
|
6558
|
+
for (const k of await this.kv.keys("supp:")) await this.kv.delete(k);
|
|
6559
|
+
}
|
|
6560
|
+
};
|
|
6561
|
+
|
|
5992
6562
|
// src/messaging/coordinator.ts
|
|
5993
6563
|
var MessagingCoordinator = class {
|
|
5994
6564
|
constructor(rt) {
|
|
@@ -5998,6 +6568,8 @@ var MessagingCoordinator = class {
|
|
|
5998
6568
|
this.sigStore = new SignatureKeyStore(this.kv);
|
|
5999
6569
|
this.groupStore = new GroupStateStorage(this.kv);
|
|
6000
6570
|
this.kpStore = new KeyPackageStorage(this.kv);
|
|
6571
|
+
this.suppressionStore = new SuppressionStore(this.kv);
|
|
6572
|
+
this.elevationStore = new MentionElevationStore(this.kv);
|
|
6001
6573
|
this.registry.attachChatList(
|
|
6002
6574
|
(chats) => {
|
|
6003
6575
|
this.chatList = chats;
|
|
@@ -6012,6 +6584,8 @@ var MessagingCoordinator = class {
|
|
|
6012
6584
|
sigStore;
|
|
6013
6585
|
groupStore;
|
|
6014
6586
|
kpStore;
|
|
6587
|
+
suppressionStore;
|
|
6588
|
+
elevationStore;
|
|
6015
6589
|
registry = new GroupRegistry();
|
|
6016
6590
|
resolved = null;
|
|
6017
6591
|
resolvePromise = null;
|
|
@@ -6167,9 +6741,9 @@ var MessagingCoordinator = class {
|
|
|
6167
6741
|
});
|
|
6168
6742
|
return group;
|
|
6169
6743
|
}
|
|
6170
|
-
async sendText(group, text, replyTo) {
|
|
6744
|
+
async sendText(group, text, replyTo, bodyRanges) {
|
|
6171
6745
|
const r = await this.resolve();
|
|
6172
|
-
return r.groups.sendText(group, text, replyTo);
|
|
6746
|
+
return r.groups.sendText(group, text, replyTo, bodyRanges);
|
|
6173
6747
|
}
|
|
6174
6748
|
async sendReaction(group, args) {
|
|
6175
6749
|
const r = await this.resolve();
|
|
@@ -6179,6 +6753,26 @@ var MessagingCoordinator = class {
|
|
|
6179
6753
|
const r = await this.resolve();
|
|
6180
6754
|
return r.groups.sendEdit(group, args);
|
|
6181
6755
|
}
|
|
6756
|
+
async sendDelete(group, args) {
|
|
6757
|
+
const r = await this.resolve();
|
|
6758
|
+
return r.groups.sendDelete(group, args);
|
|
6759
|
+
}
|
|
6760
|
+
/** Load this chat's persisted delete-for-me suppression keys (durable-only). */
|
|
6761
|
+
loadSuppressed(group) {
|
|
6762
|
+
return this.suppressionStore.load(group.rfcGroupId);
|
|
6763
|
+
}
|
|
6764
|
+
/** Persist this chat's delete-for-me suppression keys (durable-only, no wire). */
|
|
6765
|
+
saveSuppressed(group, keys) {
|
|
6766
|
+
return this.suppressionStore.save(group.rfcGroupId, keys);
|
|
6767
|
+
}
|
|
6768
|
+
/** Load this chat's persisted self-elevation dedup keys (durable-only). */
|
|
6769
|
+
loadElevated(group) {
|
|
6770
|
+
return this.elevationStore.load(group.rfcGroupId);
|
|
6771
|
+
}
|
|
6772
|
+
/** Persist this chat's self-elevation dedup keys (durable-only, no wire). */
|
|
6773
|
+
saveElevated(group, keys) {
|
|
6774
|
+
return this.elevationStore.save(group.rfcGroupId, keys);
|
|
6775
|
+
}
|
|
6182
6776
|
async history(group, limit, before) {
|
|
6183
6777
|
const r = await this.resolve();
|
|
6184
6778
|
const rows = await r.messageStore.history(group.rfcGroupId, limit, before);
|
|
@@ -6276,9 +6870,11 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
6276
6870
|
});
|
|
6277
6871
|
}
|
|
6278
6872
|
const editFold = new EditFold();
|
|
6873
|
+
const deleteFold = new DeleteFold();
|
|
6279
6874
|
const authorByClientMsgId = /* @__PURE__ */ new Map();
|
|
6280
6875
|
for (const s of rows) {
|
|
6281
|
-
if (s.envelopeType === "reaction" || s.envelopeType === "edit")
|
|
6876
|
+
if (s.envelopeType === "reaction" || s.envelopeType === "edit" || s.envelopeType === "delete")
|
|
6877
|
+
continue;
|
|
6282
6878
|
const cid = s.clientMsgId ?? "";
|
|
6283
6879
|
if (!cid) continue;
|
|
6284
6880
|
const author = s.direction === "outgoing" ? selfUserId : resolveActor?.(s.senderDeviceId ?? null);
|
|
@@ -6295,15 +6891,34 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
6295
6891
|
newText: s.edit.newText,
|
|
6296
6892
|
epoch: s.epoch,
|
|
6297
6893
|
serverSeq: s.serverSeq,
|
|
6298
|
-
eventClientMsgId: s.clientMsgId ?? `${s.id}
|
|
6894
|
+
eventClientMsgId: s.clientMsgId ?? `${s.id}`,
|
|
6895
|
+
// Mentions T6: the edit's replacement ranges ride the fold so the WINNING
|
|
6896
|
+
// edit's ranges drive the edited message's mentions on cold launch.
|
|
6897
|
+
bodyRanges: s.edit.bodyRanges ?? null
|
|
6299
6898
|
},
|
|
6300
6899
|
authorOfTarget
|
|
6301
6900
|
);
|
|
6302
6901
|
}
|
|
6303
6902
|
editFold.reevaluateHeld(authorOfTarget);
|
|
6903
|
+
for (const s of rows) {
|
|
6904
|
+
if (s.envelopeType !== "delete" || !s.delete) continue;
|
|
6905
|
+
const actor = s.direction === "outgoing" ? selfUserId : resolveActor?.(s.senderDeviceId ?? null) ?? null;
|
|
6906
|
+
deleteFold.ingest(
|
|
6907
|
+
{
|
|
6908
|
+
targetClientMsgId: s.delete.targetClientMsgId,
|
|
6909
|
+
actorUserId: actor,
|
|
6910
|
+
epoch: s.epoch,
|
|
6911
|
+
serverSeq: s.serverSeq,
|
|
6912
|
+
eventClientMsgId: s.clientMsgId ?? `${s.id}`
|
|
6913
|
+
},
|
|
6914
|
+
authorOfTarget
|
|
6915
|
+
);
|
|
6916
|
+
}
|
|
6917
|
+
for (const [cid, author] of authorByClientMsgId) deleteFold.reevaluatePending(cid, author);
|
|
6304
6918
|
const lookup = /* @__PURE__ */ new Map();
|
|
6305
6919
|
for (const s of rows) {
|
|
6306
|
-
if (s.envelopeType === "reaction")
|
|
6920
|
+
if (s.envelopeType === "reaction" || s.envelopeType === "edit" || s.envelopeType === "delete")
|
|
6921
|
+
continue;
|
|
6307
6922
|
const cid = s.clientMsgId ?? "";
|
|
6308
6923
|
if (cid && s.text !== null) {
|
|
6309
6924
|
const senderUserId = s.direction === "outgoing" ? selfUserId : "";
|
|
@@ -6312,8 +6927,29 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
6312
6927
|
}
|
|
6313
6928
|
const out = [];
|
|
6314
6929
|
for (const s of rows) {
|
|
6315
|
-
if (s.envelopeType === "reaction" || s.envelopeType === "edit")
|
|
6930
|
+
if (s.envelopeType === "reaction" || s.envelopeType === "edit" || s.envelopeType === "delete")
|
|
6931
|
+
continue;
|
|
6316
6932
|
const clientMsgId = s.clientMsgId ?? "";
|
|
6933
|
+
const isDeleted = clientMsgId ? deleteFold.isTombstoned(clientMsgId) : false;
|
|
6934
|
+
if (isDeleted) {
|
|
6935
|
+
out.push({
|
|
6936
|
+
id: `${displayId}#${s.serverSeq}`,
|
|
6937
|
+
kind: "text",
|
|
6938
|
+
direction: s.direction,
|
|
6939
|
+
senderUserId: s.direction === "outgoing" ? selfUserId : null,
|
|
6940
|
+
text: DELETED_DESCRIPTOR,
|
|
6941
|
+
serverSeq: s.serverSeq,
|
|
6942
|
+
sentAt: new Date(s.at),
|
|
6943
|
+
clientMsgId,
|
|
6944
|
+
replyTo: null,
|
|
6945
|
+
reactions: {},
|
|
6946
|
+
edited: false,
|
|
6947
|
+
isDeleted: true,
|
|
6948
|
+
// Mentions T6: delete DOMINATES mentions too — a tombstoned message has none.
|
|
6949
|
+
mentions: []
|
|
6950
|
+
});
|
|
6951
|
+
continue;
|
|
6952
|
+
}
|
|
6317
6953
|
let replyTo = null;
|
|
6318
6954
|
if (s.replyTo) {
|
|
6319
6955
|
const ref = {
|
|
@@ -6330,22 +6966,36 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
6330
6966
|
}
|
|
6331
6967
|
const editText = clientMsgId ? editFold.text(clientMsgId) : null;
|
|
6332
6968
|
const edited = clientMsgId ? editFold.isEdited(clientMsgId) : false;
|
|
6969
|
+
const text = editText ?? s.text;
|
|
6970
|
+
const rawRanges = editText !== null ? editFold.bodyRanges(clientMsgId) : s.bodyRanges;
|
|
6971
|
+
const mentions = normalizeMentionsNullNames(rawRanges, text);
|
|
6333
6972
|
out.push({
|
|
6334
6973
|
id: `${displayId}#${s.serverSeq}`,
|
|
6335
6974
|
kind: s.text != null ? "text" : "system",
|
|
6336
6975
|
direction: s.direction,
|
|
6337
6976
|
senderUserId: s.direction === "outgoing" ? selfUserId : null,
|
|
6338
|
-
text
|
|
6977
|
+
text,
|
|
6339
6978
|
serverSeq: s.serverSeq,
|
|
6340
6979
|
sentAt: new Date(s.at),
|
|
6341
6980
|
clientMsgId,
|
|
6342
6981
|
replyTo,
|
|
6343
6982
|
reactions: clientMsgId ? fold.tally(clientMsgId) : {},
|
|
6344
|
-
edited
|
|
6983
|
+
edited,
|
|
6984
|
+
isDeleted: false,
|
|
6985
|
+
mentions
|
|
6345
6986
|
});
|
|
6346
6987
|
}
|
|
6347
6988
|
return out;
|
|
6348
6989
|
}
|
|
6990
|
+
function normalizeMentionsNullNames(raw, text) {
|
|
6991
|
+
if (text === null || !raw || raw.length === 0) return [];
|
|
6992
|
+
return normalizeMentionRangesUtf16(raw, text).map((r) => ({
|
|
6993
|
+
start: r.start,
|
|
6994
|
+
length: r.length,
|
|
6995
|
+
mentionedUserId: r.mentionedUserId,
|
|
6996
|
+
displayName: null
|
|
6997
|
+
}));
|
|
6998
|
+
}
|
|
6349
6999
|
|
|
6350
7000
|
// src/messaging/facade.ts
|
|
6351
7001
|
var PalbeMessaging = class {
|
|
@@ -7079,7 +7729,7 @@ function defaultSessionStorage(key) {
|
|
|
7079
7729
|
}
|
|
7080
7730
|
|
|
7081
7731
|
// src/version.ts
|
|
7082
|
-
var VERSION = "1.
|
|
7732
|
+
var VERSION = "1.5.0";
|
|
7083
7733
|
|
|
7084
7734
|
// src/runtime.ts
|
|
7085
7735
|
function buildRuntime(config) {
|
|
@@ -7439,4 +8089,4 @@ export {
|
|
|
7439
8089
|
pb,
|
|
7440
8090
|
createBoundClient
|
|
7441
8091
|
};
|
|
7442
|
-
//# sourceMappingURL=chunk-
|
|
8092
|
+
//# sourceMappingURL=chunk-MBA2NAKS.js.map
|