@palbase/web 1.4.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-CbXprYrS.d.ts → analytics-facade-BQlTfsEm.d.ts} +96 -4
- package/dist/{analytics-facade-DA9TGKLF.d.cts → analytics-facade-DvsLE_qz.d.cts} +96 -4
- package/dist/{chunk-3EVGYJ5F.js → chunk-MBA2NAKS.js} +322 -37
- package/dist/chunk-MBA2NAKS.js.map +1 -0
- package/dist/index.cjs +321 -36
- 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 +321 -36
- 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 +321 -36
- package/dist/next/client.cjs.map +1 -1
- package/dist/next/client.js +1 -1
- package/dist/next/index.cjs +321 -36
- 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-CLfTLQzH.d.ts → pb-Cl7TeyCR.d.ts} +1 -1
- package/dist/{pb-SVs7vTOp.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 +2 -2
- package/dist/chunk-3EVGYJ5F.js.map +0 -1
|
@@ -2699,7 +2699,8 @@ var EditFold = class {
|
|
|
2699
2699
|
orderEpoch: e.epoch,
|
|
2700
2700
|
orderSeq: e.serverSeq,
|
|
2701
2701
|
lastEventId: e.eventClientMsgId,
|
|
2702
|
-
text: e.newText
|
|
2702
|
+
text: e.newText,
|
|
2703
|
+
bodyRanges: e.bodyRanges ?? null
|
|
2703
2704
|
});
|
|
2704
2705
|
this.editedTargets.add(e.targetClientMsgId);
|
|
2705
2706
|
}
|
|
@@ -2720,6 +2721,15 @@ var EditFold = class {
|
|
|
2720
2721
|
isEdited(targetClientMsgId) {
|
|
2721
2722
|
return this.editedTargets.has(targetClientMsgId);
|
|
2722
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
|
+
}
|
|
2723
2733
|
/**
|
|
2724
2734
|
* Re-run HELD edits when the roster/target newly resolves (call on member/roster
|
|
2725
2735
|
* change and when a target message arrives). Clears `held` and re-ingests each
|
|
@@ -2893,7 +2903,14 @@ function encodeEdit(args) {
|
|
|
2893
2903
|
type: "edit",
|
|
2894
2904
|
client_msg_id: args.clientMsgId,
|
|
2895
2905
|
target_client_msg_id: args.targetClientMsgId,
|
|
2896
|
-
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
|
+
} : {}
|
|
2897
2914
|
})
|
|
2898
2915
|
);
|
|
2899
2916
|
}
|
|
@@ -2915,7 +2932,14 @@ function encodeEnvelope(args) {
|
|
|
2915
2932
|
type: "text",
|
|
2916
2933
|
client_msg_id: args.clientMsgId,
|
|
2917
2934
|
text: args.text,
|
|
2918
|
-
...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
|
+
} : {}
|
|
2919
2943
|
};
|
|
2920
2944
|
return encodeUtf8(JSON.stringify(env));
|
|
2921
2945
|
}
|
|
@@ -2949,6 +2973,7 @@ function decodeEnvelope(bytes) {
|
|
|
2949
2973
|
};
|
|
2950
2974
|
}
|
|
2951
2975
|
if (typeof o === "object" && o !== null && o.type === "edit") {
|
|
2976
|
+
const editRanges = decodeBodyRanges(o.body_ranges);
|
|
2952
2977
|
return {
|
|
2953
2978
|
type: "edit",
|
|
2954
2979
|
text: null,
|
|
@@ -2957,15 +2982,18 @@ function decodeEnvelope(bytes) {
|
|
|
2957
2982
|
edit: {
|
|
2958
2983
|
targetClientMsgId: o.target_client_msg_id ?? "",
|
|
2959
2984
|
newText: o.new_text ?? ""
|
|
2960
|
-
}
|
|
2985
|
+
},
|
|
2986
|
+
...editRanges ? { bodyRanges: editRanges } : {}
|
|
2961
2987
|
};
|
|
2962
2988
|
}
|
|
2963
2989
|
if (typeof o === "object" && o !== null && o.type === "text" && typeof o.v === "number") {
|
|
2990
|
+
const textRanges = decodeBodyRanges(o.body_ranges);
|
|
2964
2991
|
return {
|
|
2965
2992
|
type: "text",
|
|
2966
2993
|
text: o.text ?? null,
|
|
2967
2994
|
clientMsgId: o.client_msg_id ?? "",
|
|
2968
|
-
replyTo: o.reply_to ?? null
|
|
2995
|
+
replyTo: o.reply_to ?? null,
|
|
2996
|
+
...textRanges ? { bodyRanges: textRanges } : {}
|
|
2969
2997
|
};
|
|
2970
2998
|
}
|
|
2971
2999
|
if (typeof o === "object" && o !== null && o.type === "text") {
|
|
@@ -2977,6 +3005,14 @@ function decodeEnvelope(bytes) {
|
|
|
2977
3005
|
}
|
|
2978
3006
|
return { text: s, clientMsgId: "", replyTo: null };
|
|
2979
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
|
+
}
|
|
2980
3016
|
function resolveReply(ref, lookup) {
|
|
2981
3017
|
const parent = lookup(ref.client_msg_id);
|
|
2982
3018
|
if (parent !== null) {
|
|
@@ -3188,9 +3224,9 @@ var GroupMessaging = class {
|
|
|
3188
3224
|
/** Send a text message. Encrypt at the current epoch, POST, return the receipt +
|
|
3189
3225
|
* the client-minted `clientMsgId` (needed by the Chat layer for reply indexing).
|
|
3190
3226
|
* NEVER rebases (a 422 stale_application surfaces to the caller). */
|
|
3191
|
-
async sendText(group, text, replyTo) {
|
|
3227
|
+
async sendText(group, text, replyTo, bodyRanges) {
|
|
3192
3228
|
const clientMsgId = mintClientMsgId();
|
|
3193
|
-
const plaintext = encodeEnvelope({ text, clientMsgId, replyTo });
|
|
3229
|
+
const plaintext = encodeEnvelope({ text, clientMsgId, replyTo, bodyRanges });
|
|
3194
3230
|
const ct = await this.engine.encryptApplication(fromBase64(group.rfcGroupId), plaintext);
|
|
3195
3231
|
const body = {
|
|
3196
3232
|
ciphertext_b64: toBase64(ct),
|
|
@@ -3216,7 +3252,10 @@ var GroupMessaging = class {
|
|
|
3216
3252
|
previewBody: replyTo.preview?.body ?? null,
|
|
3217
3253
|
previewAuthorUserId: replyTo.preview?.author_user_id ?? null,
|
|
3218
3254
|
previewKind: replyTo.preview?.kind ?? "text"
|
|
3219
|
-
} : 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 } : {}
|
|
3220
3259
|
};
|
|
3221
3260
|
try {
|
|
3222
3261
|
await this.messageStore.append(group.rfcGroupId, stored);
|
|
@@ -3284,7 +3323,8 @@ var GroupMessaging = class {
|
|
|
3284
3323
|
const plaintext = encodeEdit({
|
|
3285
3324
|
clientMsgId: args.clientMsgId,
|
|
3286
3325
|
targetClientMsgId: args.targetClientMsgId,
|
|
3287
|
-
newText: args.newText
|
|
3326
|
+
newText: args.newText,
|
|
3327
|
+
bodyRanges: args.bodyRanges
|
|
3288
3328
|
});
|
|
3289
3329
|
const ct = await this.engine.encryptApplication(fromBase64(group.rfcGroupId), plaintext);
|
|
3290
3330
|
const body = {
|
|
@@ -3310,7 +3350,10 @@ var GroupMessaging = class {
|
|
|
3310
3350
|
envelopeType: "edit",
|
|
3311
3351
|
edit: {
|
|
3312
3352
|
targetClientMsgId: args.targetClientMsgId,
|
|
3313
|
-
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 } : {}
|
|
3314
3357
|
}
|
|
3315
3358
|
};
|
|
3316
3359
|
try {
|
|
@@ -3432,6 +3475,41 @@ var GroupMessaging = class {
|
|
|
3432
3475
|
}
|
|
3433
3476
|
};
|
|
3434
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
|
+
|
|
3435
3513
|
// src/messaging/reaction-fold.ts
|
|
3436
3514
|
function orderLte(aEpoch, aSeq, bEpoch, bSeq) {
|
|
3437
3515
|
if (aEpoch !== bEpoch) return aEpoch < bEpoch;
|
|
@@ -3519,6 +3597,13 @@ var Chat = class {
|
|
|
3519
3597
|
/** True once the persisted suppression set has been loaded (so the omit applies
|
|
3520
3598
|
* even on the cold-launch hydrate path before a fresh deleteForMe). */
|
|
3521
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;
|
|
3522
3607
|
/** Per-target BASE (original) text, so the rendered text is `edit ?? original`.
|
|
3523
3608
|
* Seeded once per target (write-once base) so a 2nd own edit can't corrupt it. */
|
|
3524
3609
|
originalTextByClientMsgId = /* @__PURE__ */ new Map();
|
|
@@ -3530,6 +3615,14 @@ var Chat = class {
|
|
|
3530
3615
|
wired = false;
|
|
3531
3616
|
liveUnsub = null;
|
|
3532
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;
|
|
3533
3626
|
/** @internal — obtain via pb.messaging.directChat / groupChat / chat(id). */
|
|
3534
3627
|
constructor(args) {
|
|
3535
3628
|
this.backend = args.backend;
|
|
@@ -3607,7 +3700,8 @@ var Chat = class {
|
|
|
3607
3700
|
reactions: {},
|
|
3608
3701
|
replyTo: null,
|
|
3609
3702
|
edited: false,
|
|
3610
|
-
isDeleted: true
|
|
3703
|
+
isDeleted: true,
|
|
3704
|
+
mentions: []
|
|
3611
3705
|
});
|
|
3612
3706
|
continue;
|
|
3613
3707
|
}
|
|
@@ -3643,6 +3737,7 @@ var Chat = class {
|
|
|
3643
3737
|
this.wired = true;
|
|
3644
3738
|
this.liveUnsub = this.backend.subscribeLive(this._group, this);
|
|
3645
3739
|
void this.loadSuppressed();
|
|
3740
|
+
void this.loadElevated();
|
|
3646
3741
|
void this.hydrateHistory();
|
|
3647
3742
|
void this.refreshMembers();
|
|
3648
3743
|
}
|
|
@@ -3664,6 +3759,17 @@ var Chat = class {
|
|
|
3664
3759
|
} catch {
|
|
3665
3760
|
}
|
|
3666
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
|
+
}
|
|
3667
3773
|
async hydrateHistory() {
|
|
3668
3774
|
if (this.historyLoaded || !this._group) return;
|
|
3669
3775
|
this.historyLoaded = true;
|
|
@@ -3693,7 +3799,9 @@ var Chat = class {
|
|
|
3693
3799
|
if (m.clientMsgId && !m.isDeleted) {
|
|
3694
3800
|
this.deleteFold.reevaluatePending(m.clientMsgId, m.senderUserId);
|
|
3695
3801
|
}
|
|
3696
|
-
this.messageList.push(
|
|
3802
|
+
this.messageList.push(
|
|
3803
|
+
this.applyEditOverlay(this.applyReactionTally(this.resolveMentionNames(m)))
|
|
3804
|
+
);
|
|
3697
3805
|
changed = true;
|
|
3698
3806
|
this.loadedEarliestSeq = Math.min(this.loadedEarliestSeq ?? m.serverSeq, m.serverSeq);
|
|
3699
3807
|
}
|
|
@@ -3743,7 +3851,10 @@ var Chat = class {
|
|
|
3743
3851
|
newText: incoming.edit.newText,
|
|
3744
3852
|
epoch: incoming.epoch,
|
|
3745
3853
|
serverSeq: incoming.serverSeq,
|
|
3746
|
-
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
|
|
3747
3858
|
},
|
|
3748
3859
|
this.authorOfTarget
|
|
3749
3860
|
);
|
|
@@ -3771,6 +3882,7 @@ var Chat = class {
|
|
|
3771
3882
|
if (incomingReplyRef) {
|
|
3772
3883
|
resolvedReplyTo = resolveReply(incomingReplyRef, (id) => this.byClientMsgId.get(id) ?? null);
|
|
3773
3884
|
}
|
|
3885
|
+
const mentions = this.resolveMentions(incoming.text, incoming.bodyRanges);
|
|
3774
3886
|
const msg = {
|
|
3775
3887
|
id: this.publicId(incoming.serverSeq),
|
|
3776
3888
|
kind: this.kindOf(incoming),
|
|
@@ -3787,8 +3899,10 @@ var Chat = class {
|
|
|
3787
3899
|
// Default false; applyEditOverlay below folds any edit that arrived first.
|
|
3788
3900
|
edited: false,
|
|
3789
3901
|
// Default false; surfaced() applies the tombstone scrub if a delete folded.
|
|
3790
|
-
isDeleted: false
|
|
3902
|
+
isDeleted: false,
|
|
3903
|
+
mentions
|
|
3791
3904
|
};
|
|
3905
|
+
this.elevateIfMentioned(msg, mentions, senderUser, incoming.envelopeType);
|
|
3792
3906
|
if (incomingClientMsgId && incoming.text !== null) {
|
|
3793
3907
|
this.byClientMsgId.set(incomingClientMsgId, {
|
|
3794
3908
|
text: incoming.text,
|
|
@@ -3812,6 +3926,75 @@ var Chat = class {
|
|
|
3812
3926
|
* (null = target unknown/dangling → the fold HOLDs). Captured as a bound arrow
|
|
3813
3927
|
* so it can be passed to the pure EditFold. */
|
|
3814
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
|
+
}
|
|
3815
3998
|
/** Seed the per-target base text + author for the edit fold. Base is write-once
|
|
3816
3999
|
* (a later own/peer edit must not overwrite the original we render against). The
|
|
3817
4000
|
* author is (re)recorded whenever a non-empty resolution is available. */
|
|
@@ -3868,9 +4051,10 @@ var Chat = class {
|
|
|
3868
4051
|
const base = this.originalTextByClientMsgId.has(targetClientMsgId) ? this.originalTextByClientMsgId.get(targetClientMsgId) ?? null : m.text;
|
|
3869
4052
|
const text = editText ?? base;
|
|
3870
4053
|
const edited = foldEdited || m.edited;
|
|
3871
|
-
|
|
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;
|
|
3872
4056
|
changed = true;
|
|
3873
|
-
return { ...m, text, edited };
|
|
4057
|
+
return { ...m, text, edited, mentions };
|
|
3874
4058
|
});
|
|
3875
4059
|
if (changed) this.emit();
|
|
3876
4060
|
}
|
|
@@ -3887,8 +4071,9 @@ var Chat = class {
|
|
|
3887
4071
|
if (editText === null && !foldEdited) return m;
|
|
3888
4072
|
const text = editText ?? m.text;
|
|
3889
4073
|
const edited = foldEdited || m.edited;
|
|
3890
|
-
|
|
3891
|
-
|
|
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 };
|
|
3892
4077
|
}
|
|
3893
4078
|
/** @internal — called by the backend's conv subscription. */
|
|
3894
4079
|
applyConv(event, payload) {
|
|
@@ -3946,6 +4131,18 @@ var Chat = class {
|
|
|
3946
4131
|
}
|
|
3947
4132
|
this.editFold.reevaluateHeld(this.authorOfTarget);
|
|
3948
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();
|
|
3949
4146
|
}
|
|
3950
4147
|
seedMembersFromGroup(group) {
|
|
3951
4148
|
const seed = [
|
|
@@ -4013,11 +4210,12 @@ var Chat = class {
|
|
|
4013
4210
|
};
|
|
4014
4211
|
resolvedReplyTo = resolveReply(replyRef, (id) => this.byClientMsgId.get(id) ?? null);
|
|
4015
4212
|
}
|
|
4016
|
-
const
|
|
4017
|
-
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);
|
|
4018
4216
|
return receipt;
|
|
4019
4217
|
}
|
|
4020
|
-
appendOwnSend(text, receipt, clientMsgId, resolvedReplyTo) {
|
|
4218
|
+
appendOwnSend(text, receipt, clientMsgId, resolvedReplyTo, bodyRanges) {
|
|
4021
4219
|
if (receipt.serverSeq <= 0) return;
|
|
4022
4220
|
const key = this.internalKey(receipt.serverSeq);
|
|
4023
4221
|
if (this.seenKeys.has(key)) return;
|
|
@@ -4042,7 +4240,10 @@ var Chat = class {
|
|
|
4042
4240
|
// Own-sent edits fold via edit() after the fact; new sends start unedited.
|
|
4043
4241
|
edited: false,
|
|
4044
4242
|
// Own-sent deletes fold via deleteForEveryone() after the fact; start live.
|
|
4045
|
-
isDeleted: false
|
|
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)
|
|
4046
4247
|
});
|
|
4047
4248
|
this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
|
|
4048
4249
|
this.emit();
|
|
@@ -4130,15 +4331,18 @@ var Chat = class {
|
|
|
4130
4331
|
* instantly; the durable echo on the next pump is a fold no-op (dedup on the
|
|
4131
4332
|
* SAME wire clientMsgId). Never appends a bubble; PRESERVES the target's
|
|
4132
4333
|
* reactions + reply context. Only the original author's edits count — for an own
|
|
4133
|
-
* message self IS the author, so the author-gate passes.
|
|
4134
|
-
|
|
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) {
|
|
4135
4337
|
if (!message.clientMsgId || message.kind !== "text") return;
|
|
4136
4338
|
const group = await this.materializeIfNeeded();
|
|
4137
4339
|
const clientMsgId = mintClientMsgId();
|
|
4340
|
+
const bodyRanges = opts?.mentions ?? null;
|
|
4138
4341
|
const { receipt } = await this.backend.sendEdit(group, {
|
|
4139
4342
|
clientMsgId,
|
|
4140
4343
|
targetClientMsgId: message.clientMsgId,
|
|
4141
|
-
newText
|
|
4344
|
+
newText,
|
|
4345
|
+
bodyRanges
|
|
4142
4346
|
});
|
|
4143
4347
|
this.seedEditBase(message.clientMsgId, message.text, this.backend.selfUserId);
|
|
4144
4348
|
this.editFold.ingest(
|
|
@@ -4148,7 +4352,8 @@ var Chat = class {
|
|
|
4148
4352
|
newText,
|
|
4149
4353
|
epoch: receipt.epoch,
|
|
4150
4354
|
serverSeq: receipt.serverSeq,
|
|
4151
|
-
eventClientMsgId: clientMsgId
|
|
4355
|
+
eventClientMsgId: clientMsgId,
|
|
4356
|
+
bodyRanges
|
|
4152
4357
|
},
|
|
4153
4358
|
this.authorOfTarget
|
|
4154
4359
|
);
|
|
@@ -4201,6 +4406,18 @@ var Chat = class {
|
|
|
4201
4406
|
}
|
|
4202
4407
|
}
|
|
4203
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
|
+
}
|
|
4204
4421
|
function sameReactions(a, b) {
|
|
4205
4422
|
const ak = Object.keys(a);
|
|
4206
4423
|
const bk = Object.keys(b);
|
|
@@ -4412,14 +4629,21 @@ var MessageDeliverySource = class {
|
|
|
4412
4629
|
// Thread the edit discriminator + new text through the persisted row so an
|
|
4413
4630
|
// edit folded LIVE re-folds onto its target after a reload (the reload-parity
|
|
4414
4631
|
// boundary — mirrors iOS T3). Omitted for non-edits → old rows hydrate as
|
|
4415
|
-
// `'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).
|
|
4416
4634
|
...isEdit && decoded.edit ? {
|
|
4417
4635
|
envelopeType: "edit",
|
|
4418
4636
|
edit: {
|
|
4419
4637
|
targetClientMsgId: decoded.edit.targetClientMsgId,
|
|
4420
|
-
newText: decoded.edit.newText
|
|
4638
|
+
newText: decoded.edit.newText,
|
|
4639
|
+
...decoded.bodyRanges ? { bodyRanges: decoded.bodyRanges } : {}
|
|
4421
4640
|
}
|
|
4422
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 } : {},
|
|
4423
4647
|
// Thread the delete discriminator + target through the persisted row so a
|
|
4424
4648
|
// delete-for-everyone tombstone folded LIVE re-folds onto its target after
|
|
4425
4649
|
// a reload (the reload-parity boundary — the iOS-review CRITICAL lesson;
|
|
@@ -4451,7 +4675,10 @@ var MessageDeliverySource = class {
|
|
|
4451
4675
|
envelopeType: decoded.type ?? "text",
|
|
4452
4676
|
reaction: isReaction ? decoded.reaction : null,
|
|
4453
4677
|
edit: isEdit ? decoded.edit : null,
|
|
4454
|
-
delete: isDelete ? decoded.delete : 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
|
|
4455
4682
|
});
|
|
4456
4683
|
return true;
|
|
4457
4684
|
}
|
|
@@ -4606,6 +4833,36 @@ var GroupCatalog = class {
|
|
|
4606
4833
|
}
|
|
4607
4834
|
};
|
|
4608
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
|
+
|
|
4609
4866
|
// src/messaging/wasm/pkg/palbe_mls_bg.js
|
|
4610
4867
|
var palbe_mls_bg_exports = {};
|
|
4611
4868
|
__export(palbe_mls_bg_exports, {
|
|
@@ -6312,6 +6569,7 @@ var MessagingCoordinator = class {
|
|
|
6312
6569
|
this.groupStore = new GroupStateStorage(this.kv);
|
|
6313
6570
|
this.kpStore = new KeyPackageStorage(this.kv);
|
|
6314
6571
|
this.suppressionStore = new SuppressionStore(this.kv);
|
|
6572
|
+
this.elevationStore = new MentionElevationStore(this.kv);
|
|
6315
6573
|
this.registry.attachChatList(
|
|
6316
6574
|
(chats) => {
|
|
6317
6575
|
this.chatList = chats;
|
|
@@ -6327,6 +6585,7 @@ var MessagingCoordinator = class {
|
|
|
6327
6585
|
groupStore;
|
|
6328
6586
|
kpStore;
|
|
6329
6587
|
suppressionStore;
|
|
6588
|
+
elevationStore;
|
|
6330
6589
|
registry = new GroupRegistry();
|
|
6331
6590
|
resolved = null;
|
|
6332
6591
|
resolvePromise = null;
|
|
@@ -6482,9 +6741,9 @@ var MessagingCoordinator = class {
|
|
|
6482
6741
|
});
|
|
6483
6742
|
return group;
|
|
6484
6743
|
}
|
|
6485
|
-
async sendText(group, text, replyTo) {
|
|
6744
|
+
async sendText(group, text, replyTo, bodyRanges) {
|
|
6486
6745
|
const r = await this.resolve();
|
|
6487
|
-
return r.groups.sendText(group, text, replyTo);
|
|
6746
|
+
return r.groups.sendText(group, text, replyTo, bodyRanges);
|
|
6488
6747
|
}
|
|
6489
6748
|
async sendReaction(group, args) {
|
|
6490
6749
|
const r = await this.resolve();
|
|
@@ -6506,6 +6765,14 @@ var MessagingCoordinator = class {
|
|
|
6506
6765
|
saveSuppressed(group, keys) {
|
|
6507
6766
|
return this.suppressionStore.save(group.rfcGroupId, keys);
|
|
6508
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
|
+
}
|
|
6509
6776
|
async history(group, limit, before) {
|
|
6510
6777
|
const r = await this.resolve();
|
|
6511
6778
|
const rows = await r.messageStore.history(group.rfcGroupId, limit, before);
|
|
@@ -6624,7 +6891,10 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
6624
6891
|
newText: s.edit.newText,
|
|
6625
6892
|
epoch: s.epoch,
|
|
6626
6893
|
serverSeq: s.serverSeq,
|
|
6627
|
-
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
|
|
6628
6898
|
},
|
|
6629
6899
|
authorOfTarget
|
|
6630
6900
|
);
|
|
@@ -6674,7 +6944,9 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
6674
6944
|
replyTo: null,
|
|
6675
6945
|
reactions: {},
|
|
6676
6946
|
edited: false,
|
|
6677
|
-
isDeleted: true
|
|
6947
|
+
isDeleted: true,
|
|
6948
|
+
// Mentions T6: delete DOMINATES mentions too — a tombstoned message has none.
|
|
6949
|
+
mentions: []
|
|
6678
6950
|
});
|
|
6679
6951
|
continue;
|
|
6680
6952
|
}
|
|
@@ -6694,23 +6966,36 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
6694
6966
|
}
|
|
6695
6967
|
const editText = clientMsgId ? editFold.text(clientMsgId) : null;
|
|
6696
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);
|
|
6697
6972
|
out.push({
|
|
6698
6973
|
id: `${displayId}#${s.serverSeq}`,
|
|
6699
6974
|
kind: s.text != null ? "text" : "system",
|
|
6700
6975
|
direction: s.direction,
|
|
6701
6976
|
senderUserId: s.direction === "outgoing" ? selfUserId : null,
|
|
6702
|
-
text
|
|
6977
|
+
text,
|
|
6703
6978
|
serverSeq: s.serverSeq,
|
|
6704
6979
|
sentAt: new Date(s.at),
|
|
6705
6980
|
clientMsgId,
|
|
6706
6981
|
replyTo,
|
|
6707
6982
|
reactions: clientMsgId ? fold.tally(clientMsgId) : {},
|
|
6708
6983
|
edited,
|
|
6709
|
-
isDeleted: false
|
|
6984
|
+
isDeleted: false,
|
|
6985
|
+
mentions
|
|
6710
6986
|
});
|
|
6711
6987
|
}
|
|
6712
6988
|
return out;
|
|
6713
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
|
+
}
|
|
6714
6999
|
|
|
6715
7000
|
// src/messaging/facade.ts
|
|
6716
7001
|
var PalbeMessaging = class {
|
|
@@ -7444,7 +7729,7 @@ function defaultSessionStorage(key) {
|
|
|
7444
7729
|
}
|
|
7445
7730
|
|
|
7446
7731
|
// src/version.ts
|
|
7447
|
-
var VERSION = "1.
|
|
7732
|
+
var VERSION = "1.5.0";
|
|
7448
7733
|
|
|
7449
7734
|
// src/runtime.ts
|
|
7450
7735
|
function buildRuntime(config) {
|
|
@@ -7804,4 +8089,4 @@ export {
|
|
|
7804
8089
|
pb,
|
|
7805
8090
|
createBoundClient
|
|
7806
8091
|
};
|
|
7807
|
-
//# sourceMappingURL=chunk-
|
|
8092
|
+
//# sourceMappingURL=chunk-MBA2NAKS.js.map
|