@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
package/dist/internal.cjs
CHANGED
|
@@ -2720,7 +2720,8 @@ var EditFold = class {
|
|
|
2720
2720
|
orderEpoch: e.epoch,
|
|
2721
2721
|
orderSeq: e.serverSeq,
|
|
2722
2722
|
lastEventId: e.eventClientMsgId,
|
|
2723
|
-
text: e.newText
|
|
2723
|
+
text: e.newText,
|
|
2724
|
+
bodyRanges: e.bodyRanges ?? null
|
|
2724
2725
|
});
|
|
2725
2726
|
this.editedTargets.add(e.targetClientMsgId);
|
|
2726
2727
|
}
|
|
@@ -2741,6 +2742,15 @@ var EditFold = class {
|
|
|
2741
2742
|
isEdited(targetClientMsgId) {
|
|
2742
2743
|
return this.editedTargets.has(targetClientMsgId);
|
|
2743
2744
|
}
|
|
2745
|
+
/**
|
|
2746
|
+
* The WINNING edit's replacement mention ranges for a target (raw, un-normalized),
|
|
2747
|
+
* or null when no valid edit applied or the winning edit carried none. The Chat
|
|
2748
|
+
* normalizes these against the edited text to compute the edited message's mentions
|
|
2749
|
+
* (mentions T6). LWW-consistent: always the same edit that `text(...)` returns.
|
|
2750
|
+
*/
|
|
2751
|
+
bodyRanges(targetClientMsgId) {
|
|
2752
|
+
return this.states.get(targetClientMsgId)?.bodyRanges ?? null;
|
|
2753
|
+
}
|
|
2744
2754
|
/**
|
|
2745
2755
|
* Re-run HELD edits when the roster/target newly resolves (call on member/roster
|
|
2746
2756
|
* change and when a target message arrives). Clears `held` and re-ingests each
|
|
@@ -2914,7 +2924,14 @@ function encodeEdit(args) {
|
|
|
2914
2924
|
type: "edit",
|
|
2915
2925
|
client_msg_id: args.clientMsgId,
|
|
2916
2926
|
target_client_msg_id: args.targetClientMsgId,
|
|
2917
|
-
new_text: args.newText
|
|
2927
|
+
new_text: args.newText,
|
|
2928
|
+
...args.bodyRanges && args.bodyRanges.length > 0 ? {
|
|
2929
|
+
body_ranges: args.bodyRanges.map((r) => ({
|
|
2930
|
+
start: r.start,
|
|
2931
|
+
length: r.length,
|
|
2932
|
+
mentioned_user_id: r.mentionedUserId
|
|
2933
|
+
}))
|
|
2934
|
+
} : {}
|
|
2918
2935
|
})
|
|
2919
2936
|
);
|
|
2920
2937
|
}
|
|
@@ -2936,7 +2953,14 @@ function encodeEnvelope(args) {
|
|
|
2936
2953
|
type: "text",
|
|
2937
2954
|
client_msg_id: args.clientMsgId,
|
|
2938
2955
|
text: args.text,
|
|
2939
|
-
...args.replyTo ? { reply_to: args.replyTo } : {}
|
|
2956
|
+
...args.replyTo ? { reply_to: args.replyTo } : {},
|
|
2957
|
+
...args.bodyRanges && args.bodyRanges.length > 0 ? {
|
|
2958
|
+
body_ranges: args.bodyRanges.map((r) => ({
|
|
2959
|
+
start: r.start,
|
|
2960
|
+
length: r.length,
|
|
2961
|
+
mentioned_user_id: r.mentionedUserId
|
|
2962
|
+
}))
|
|
2963
|
+
} : {}
|
|
2940
2964
|
};
|
|
2941
2965
|
return encodeUtf8(JSON.stringify(env));
|
|
2942
2966
|
}
|
|
@@ -2970,6 +2994,7 @@ function decodeEnvelope(bytes) {
|
|
|
2970
2994
|
};
|
|
2971
2995
|
}
|
|
2972
2996
|
if (typeof o === "object" && o !== null && o.type === "edit") {
|
|
2997
|
+
const editRanges = decodeBodyRanges(o.body_ranges);
|
|
2973
2998
|
return {
|
|
2974
2999
|
type: "edit",
|
|
2975
3000
|
text: null,
|
|
@@ -2978,15 +3003,18 @@ function decodeEnvelope(bytes) {
|
|
|
2978
3003
|
edit: {
|
|
2979
3004
|
targetClientMsgId: o.target_client_msg_id ?? "",
|
|
2980
3005
|
newText: o.new_text ?? ""
|
|
2981
|
-
}
|
|
3006
|
+
},
|
|
3007
|
+
...editRanges ? { bodyRanges: editRanges } : {}
|
|
2982
3008
|
};
|
|
2983
3009
|
}
|
|
2984
3010
|
if (typeof o === "object" && o !== null && o.type === "text" && typeof o.v === "number") {
|
|
3011
|
+
const textRanges = decodeBodyRanges(o.body_ranges);
|
|
2985
3012
|
return {
|
|
2986
3013
|
type: "text",
|
|
2987
3014
|
text: o.text ?? null,
|
|
2988
3015
|
clientMsgId: o.client_msg_id ?? "",
|
|
2989
|
-
replyTo: o.reply_to ?? null
|
|
3016
|
+
replyTo: o.reply_to ?? null,
|
|
3017
|
+
...textRanges ? { bodyRanges: textRanges } : {}
|
|
2990
3018
|
};
|
|
2991
3019
|
}
|
|
2992
3020
|
if (typeof o === "object" && o !== null && o.type === "text") {
|
|
@@ -2998,6 +3026,14 @@ function decodeEnvelope(bytes) {
|
|
|
2998
3026
|
}
|
|
2999
3027
|
return { text: s, clientMsgId: "", replyTo: null };
|
|
3000
3028
|
}
|
|
3029
|
+
function decodeBodyRanges(raw) {
|
|
3030
|
+
if (!raw || raw.length === 0) return void 0;
|
|
3031
|
+
return raw.map((r) => ({
|
|
3032
|
+
start: r.start,
|
|
3033
|
+
length: r.length,
|
|
3034
|
+
mentionedUserId: r.mentioned_user_id
|
|
3035
|
+
}));
|
|
3036
|
+
}
|
|
3001
3037
|
function resolveReply(ref, lookup) {
|
|
3002
3038
|
const parent = lookup(ref.client_msg_id);
|
|
3003
3039
|
if (parent !== null) {
|
|
@@ -3209,9 +3245,9 @@ var GroupMessaging = class {
|
|
|
3209
3245
|
/** Send a text message. Encrypt at the current epoch, POST, return the receipt +
|
|
3210
3246
|
* the client-minted `clientMsgId` (needed by the Chat layer for reply indexing).
|
|
3211
3247
|
* NEVER rebases (a 422 stale_application surfaces to the caller). */
|
|
3212
|
-
async sendText(group, text, replyTo) {
|
|
3248
|
+
async sendText(group, text, replyTo, bodyRanges) {
|
|
3213
3249
|
const clientMsgId = mintClientMsgId();
|
|
3214
|
-
const plaintext = encodeEnvelope({ text, clientMsgId, replyTo });
|
|
3250
|
+
const plaintext = encodeEnvelope({ text, clientMsgId, replyTo, bodyRanges });
|
|
3215
3251
|
const ct = await this.engine.encryptApplication(fromBase64(group.rfcGroupId), plaintext);
|
|
3216
3252
|
const body = {
|
|
3217
3253
|
ciphertext_b64: toBase64(ct),
|
|
@@ -3237,7 +3273,10 @@ var GroupMessaging = class {
|
|
|
3237
3273
|
previewBody: replyTo.preview?.body ?? null,
|
|
3238
3274
|
previewAuthorUserId: replyTo.preview?.author_user_id ?? null,
|
|
3239
3275
|
previewKind: replyTo.preview?.kind ?? "text"
|
|
3240
|
-
} : null
|
|
3276
|
+
} : null,
|
|
3277
|
+
// Persist the OUTGOING bubble's mention ranges so an own-sent mention re-resolves
|
|
3278
|
+
// onto its bubble after a reload (own-send reload parity — the T4-reviewer Minor).
|
|
3279
|
+
...bodyRanges && bodyRanges.length > 0 ? { bodyRanges } : {}
|
|
3241
3280
|
};
|
|
3242
3281
|
try {
|
|
3243
3282
|
await this.messageStore.append(group.rfcGroupId, stored);
|
|
@@ -3305,7 +3344,8 @@ var GroupMessaging = class {
|
|
|
3305
3344
|
const plaintext = encodeEdit({
|
|
3306
3345
|
clientMsgId: args.clientMsgId,
|
|
3307
3346
|
targetClientMsgId: args.targetClientMsgId,
|
|
3308
|
-
newText: args.newText
|
|
3347
|
+
newText: args.newText,
|
|
3348
|
+
bodyRanges: args.bodyRanges
|
|
3309
3349
|
});
|
|
3310
3350
|
const ct = await this.engine.encryptApplication(fromBase64(group.rfcGroupId), plaintext);
|
|
3311
3351
|
const body = {
|
|
@@ -3331,7 +3371,10 @@ var GroupMessaging = class {
|
|
|
3331
3371
|
envelopeType: "edit",
|
|
3332
3372
|
edit: {
|
|
3333
3373
|
targetClientMsgId: args.targetClientMsgId,
|
|
3334
|
-
newText: args.newText
|
|
3374
|
+
newText: args.newText,
|
|
3375
|
+
// Persist the edit's REPLACEMENT ranges so the edited message's mentions
|
|
3376
|
+
// re-resolve from this edit after a reload (own-send reload parity — T6).
|
|
3377
|
+
...args.bodyRanges && args.bodyRanges.length > 0 ? { bodyRanges: args.bodyRanges } : {}
|
|
3335
3378
|
}
|
|
3336
3379
|
};
|
|
3337
3380
|
try {
|
|
@@ -3453,6 +3496,41 @@ var GroupMessaging = class {
|
|
|
3453
3496
|
}
|
|
3454
3497
|
};
|
|
3455
3498
|
|
|
3499
|
+
// src/messaging/mention-ranges.ts
|
|
3500
|
+
function normalizeMentionRangesUtf16(ranges, text) {
|
|
3501
|
+
const n = text.length;
|
|
3502
|
+
function splitsSurrogatePair(index) {
|
|
3503
|
+
if (index <= 0 || index >= n) return false;
|
|
3504
|
+
const before = text.charCodeAt(index - 1);
|
|
3505
|
+
const at = text.charCodeAt(index);
|
|
3506
|
+
const beforeIsHigh = before >= 55296 && before <= 56319;
|
|
3507
|
+
const atIsLow = at >= 56320 && at <= 57343;
|
|
3508
|
+
return beforeIsHigh && atIsLow;
|
|
3509
|
+
}
|
|
3510
|
+
const survivors = [];
|
|
3511
|
+
for (let idx = 0; idx < ranges.length; idx++) {
|
|
3512
|
+
const r = ranges[idx];
|
|
3513
|
+
if (r === void 0) continue;
|
|
3514
|
+
if (r.start < 0 || r.length <= 0 || r.start + r.length > n) continue;
|
|
3515
|
+
if (splitsSurrogatePair(r.start) || splitsSurrogatePair(r.start + r.length)) continue;
|
|
3516
|
+
survivors.push({ idx, range: r });
|
|
3517
|
+
}
|
|
3518
|
+
survivors.sort((lhs, rhs) => {
|
|
3519
|
+
if (lhs.range.start !== rhs.range.start) return lhs.range.start - rhs.range.start;
|
|
3520
|
+
if (lhs.range.length !== rhs.range.length) return rhs.range.length - lhs.range.length;
|
|
3521
|
+
return lhs.idx - rhs.idx;
|
|
3522
|
+
});
|
|
3523
|
+
const kept = [];
|
|
3524
|
+
let prevEnd = Number.NEGATIVE_INFINITY;
|
|
3525
|
+
for (const s of survivors) {
|
|
3526
|
+
if (s.range.start >= prevEnd) {
|
|
3527
|
+
kept.push(s.range);
|
|
3528
|
+
prevEnd = s.range.start + s.range.length;
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
return kept;
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3456
3534
|
// src/messaging/reaction-fold.ts
|
|
3457
3535
|
function orderLte(aEpoch, aSeq, bEpoch, bSeq) {
|
|
3458
3536
|
if (aEpoch !== bEpoch) return aEpoch < bEpoch;
|
|
@@ -3540,6 +3618,13 @@ var Chat = class {
|
|
|
3540
3618
|
/** True once the persisted suppression set has been loaded (so the omit applies
|
|
3541
3619
|
* even on the cold-launch hydrate path before a fresh deleteForMe). */
|
|
3542
3620
|
suppressedLoaded = false;
|
|
3621
|
+
/** Self-elevation dedup keys (`<selfUserId>|<clientMsgId or seq:n>`). Once a
|
|
3622
|
+
* mention of me from another sender fires `onMentionElevation`, its key lands here
|
|
3623
|
+
* + is persisted, so a re-delivery / cold-launch re-hydrate never re-fires. */
|
|
3624
|
+
elevated = /* @__PURE__ */ new Set();
|
|
3625
|
+
/** True once the persisted elevation set has been loaded (so a re-delivered mention
|
|
3626
|
+
* on the cold-launch hydrate path dedups against the persisted decision). */
|
|
3627
|
+
elevatedLoaded = false;
|
|
3543
3628
|
/** Per-target BASE (original) text, so the rendered text is `edit ?? original`.
|
|
3544
3629
|
* Seeded once per target (write-once base) so a 2nd own edit can't corrupt it. */
|
|
3545
3630
|
originalTextByClientMsgId = /* @__PURE__ */ new Map();
|
|
@@ -3551,6 +3636,14 @@ var Chat = class {
|
|
|
3551
3636
|
wired = false;
|
|
3552
3637
|
liveUnsub = null;
|
|
3553
3638
|
listeners = /* @__PURE__ */ new Set();
|
|
3639
|
+
/**
|
|
3640
|
+
* Fires ONCE per `(selfUserId, clientMsgId)` when an INCOMING message mentions THIS
|
|
3641
|
+
* user from ANOTHER sender (not an edit). The dedup survives re-delivery + reload
|
|
3642
|
+
* via the persisted elevation set, so this never double-fires for one mention. The
|
|
3643
|
+
* app wires it to a buzz/badge (e.g. an in-app banner). Best-effort cooperative —
|
|
3644
|
+
* the SDK guarantees the DECISION, not the buzz. Mirrors iOS `Chat.onMentionElevation`.
|
|
3645
|
+
*/
|
|
3646
|
+
onMentionElevation;
|
|
3554
3647
|
/** @internal — obtain via pb.messaging.directChat / groupChat / chat(id). */
|
|
3555
3648
|
constructor(args) {
|
|
3556
3649
|
this.backend = args.backend;
|
|
@@ -3628,7 +3721,8 @@ var Chat = class {
|
|
|
3628
3721
|
reactions: {},
|
|
3629
3722
|
replyTo: null,
|
|
3630
3723
|
edited: false,
|
|
3631
|
-
isDeleted: true
|
|
3724
|
+
isDeleted: true,
|
|
3725
|
+
mentions: []
|
|
3632
3726
|
});
|
|
3633
3727
|
continue;
|
|
3634
3728
|
}
|
|
@@ -3664,6 +3758,7 @@ var Chat = class {
|
|
|
3664
3758
|
this.wired = true;
|
|
3665
3759
|
this.liveUnsub = this.backend.subscribeLive(this._group, this);
|
|
3666
3760
|
void this.loadSuppressed();
|
|
3761
|
+
void this.loadElevated();
|
|
3667
3762
|
void this.hydrateHistory();
|
|
3668
3763
|
void this.refreshMembers();
|
|
3669
3764
|
}
|
|
@@ -3685,6 +3780,17 @@ var Chat = class {
|
|
|
3685
3780
|
} catch {
|
|
3686
3781
|
}
|
|
3687
3782
|
}
|
|
3783
|
+
/** Hydrate the persisted self-elevation dedup keys (once). No re-emit: the set only
|
|
3784
|
+
* gates the elevation DECISION, it does not change what renders. */
|
|
3785
|
+
async loadElevated() {
|
|
3786
|
+
if (this.elevatedLoaded || !this._group) return;
|
|
3787
|
+
this.elevatedLoaded = true;
|
|
3788
|
+
try {
|
|
3789
|
+
const keys = await this.backend.loadElevated(this._group);
|
|
3790
|
+
for (const k of keys) this.elevated.add(k);
|
|
3791
|
+
} catch {
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3688
3794
|
async hydrateHistory() {
|
|
3689
3795
|
if (this.historyLoaded || !this._group) return;
|
|
3690
3796
|
this.historyLoaded = true;
|
|
@@ -3714,7 +3820,9 @@ var Chat = class {
|
|
|
3714
3820
|
if (m.clientMsgId && !m.isDeleted) {
|
|
3715
3821
|
this.deleteFold.reevaluatePending(m.clientMsgId, m.senderUserId);
|
|
3716
3822
|
}
|
|
3717
|
-
this.messageList.push(
|
|
3823
|
+
this.messageList.push(
|
|
3824
|
+
this.applyEditOverlay(this.applyReactionTally(this.resolveMentionNames(m)))
|
|
3825
|
+
);
|
|
3718
3826
|
changed = true;
|
|
3719
3827
|
this.loadedEarliestSeq = Math.min(this.loadedEarliestSeq ?? m.serverSeq, m.serverSeq);
|
|
3720
3828
|
}
|
|
@@ -3764,7 +3872,10 @@ var Chat = class {
|
|
|
3764
3872
|
newText: incoming.edit.newText,
|
|
3765
3873
|
epoch: incoming.epoch,
|
|
3766
3874
|
serverSeq: incoming.serverSeq,
|
|
3767
|
-
eventClientMsgId: incoming.clientMsgId
|
|
3875
|
+
eventClientMsgId: incoming.clientMsgId,
|
|
3876
|
+
// Mentions T6: carry the edit's REPLACEMENT ranges so the edited message's
|
|
3877
|
+
// mentions reflect them (recomputed against the new text on recomputeEdit).
|
|
3878
|
+
bodyRanges: incoming.bodyRanges
|
|
3768
3879
|
},
|
|
3769
3880
|
this.authorOfTarget
|
|
3770
3881
|
);
|
|
@@ -3792,6 +3903,7 @@ var Chat = class {
|
|
|
3792
3903
|
if (incomingReplyRef) {
|
|
3793
3904
|
resolvedReplyTo = resolveReply(incomingReplyRef, (id) => this.byClientMsgId.get(id) ?? null);
|
|
3794
3905
|
}
|
|
3906
|
+
const mentions = this.resolveMentions(incoming.text, incoming.bodyRanges);
|
|
3795
3907
|
const msg = {
|
|
3796
3908
|
id: this.publicId(incoming.serverSeq),
|
|
3797
3909
|
kind: this.kindOf(incoming),
|
|
@@ -3808,8 +3920,10 @@ var Chat = class {
|
|
|
3808
3920
|
// Default false; applyEditOverlay below folds any edit that arrived first.
|
|
3809
3921
|
edited: false,
|
|
3810
3922
|
// Default false; surfaced() applies the tombstone scrub if a delete folded.
|
|
3811
|
-
isDeleted: false
|
|
3923
|
+
isDeleted: false,
|
|
3924
|
+
mentions
|
|
3812
3925
|
};
|
|
3926
|
+
this.elevateIfMentioned(msg, mentions, senderUser, incoming.envelopeType);
|
|
3813
3927
|
if (incomingClientMsgId && incoming.text !== null) {
|
|
3814
3928
|
this.byClientMsgId.set(incomingClientMsgId, {
|
|
3815
3929
|
text: incoming.text,
|
|
@@ -3833,6 +3947,75 @@ var Chat = class {
|
|
|
3833
3947
|
* (null = target unknown/dangling → the fold HOLDs). Captured as a bound arrow
|
|
3834
3948
|
* so it can be passed to the pure EditFold. */
|
|
3835
3949
|
authorOfTarget = (targetClientMsgId) => this.authorByClientMsgId.get(targetClientMsgId) ?? null;
|
|
3950
|
+
// ── Mentions (mentions T6) ──
|
|
3951
|
+
/** Resolve a message's decode-time mention spans: NORMALIZE the raw `body_ranges`
|
|
3952
|
+
* against `text` (the pinned `normalizeMentionRangesUtf16` cross-SDK contract) then
|
|
3953
|
+
* resolve each surviving range's `mentionedUserId` to a roster display name. An id
|
|
3954
|
+
* not in the roster resolves to `null` (the renderer falls back to the `text` slice).
|
|
3955
|
+
* Pure over (text, bodyRanges, memberCache); never throws. Mirrors iOS T3. */
|
|
3956
|
+
resolveMentions(text, bodyRanges) {
|
|
3957
|
+
if (text === null || !bodyRanges || bodyRanges.length === 0) return [];
|
|
3958
|
+
const normalized = normalizeMentionRangesUtf16(bodyRanges, text);
|
|
3959
|
+
if (normalized.length === 0) return [];
|
|
3960
|
+
return normalized.map((r) => ({
|
|
3961
|
+
start: r.start,
|
|
3962
|
+
length: r.length,
|
|
3963
|
+
mentionedUserId: r.mentionedUserId,
|
|
3964
|
+
displayName: this.displayNameOf(r.mentionedUserId)
|
|
3965
|
+
}));
|
|
3966
|
+
}
|
|
3967
|
+
/** Re-resolve the roster display name on already-NORMALIZED spans (the history
|
|
3968
|
+
* projection produces them with null names — resolution is LIVE, not snapshotted).
|
|
3969
|
+
* A member rename then reflects on old messages. Returns the message unchanged when
|
|
3970
|
+
* it has no mentions (the common case) or no name changed. Mirrors iOS T3. */
|
|
3971
|
+
resolveMentionNames(m) {
|
|
3972
|
+
if (!m.mentions || m.mentions.length === 0) {
|
|
3973
|
+
return m.mentions ? m : { ...m, mentions: [] };
|
|
3974
|
+
}
|
|
3975
|
+
let changed = false;
|
|
3976
|
+
const reresolved = m.mentions.map((span) => {
|
|
3977
|
+
const name = this.displayNameOf(span.mentionedUserId);
|
|
3978
|
+
if (name === span.displayName) return span;
|
|
3979
|
+
changed = true;
|
|
3980
|
+
return { ...span, displayName: name };
|
|
3981
|
+
});
|
|
3982
|
+
if (!changed) return m;
|
|
3983
|
+
return { ...m, mentions: reresolved };
|
|
3984
|
+
}
|
|
3985
|
+
/** The WINNING edit's resolved mentions for a target (normalize its replacement
|
|
3986
|
+
* ranges against the new text + roster names), or `[]` if no winning edit / no
|
|
3987
|
+
* ranges. The edited message's mentions reflect the EDIT's ranges (mirrors iOS T3). */
|
|
3988
|
+
editMentions(targetClientMsgId, newText) {
|
|
3989
|
+
const ranges = this.editFold.bodyRanges(targetClientMsgId);
|
|
3990
|
+
if (!ranges) return [];
|
|
3991
|
+
return this.resolveMentions(newText, ranges);
|
|
3992
|
+
}
|
|
3993
|
+
/** Resolve a userId → its roster display name (null if not a known member). */
|
|
3994
|
+
displayNameOf(userId) {
|
|
3995
|
+
return this.memberCache.find((mm) => mm.userId === userId)?.displayName ?? null;
|
|
3996
|
+
}
|
|
3997
|
+
/** Compute the SELF-ELEVATION decision for a freshly-ingested INCOMING bubble and,
|
|
3998
|
+
* when it fires, record the dedup key (persisted) + invoke `onMentionElevation`.
|
|
3999
|
+
* Gate (mirrors iOS T3): a surviving mention targets THIS user AND the sender is not
|
|
4000
|
+
* me AND it's NOT an edit AND the `(selfUserId, clientMsgId|seq)` key isn't already
|
|
4001
|
+
* elevated. Dedup-once: the in-memory set gates the session, the persisted set
|
|
4002
|
+
* survives reload. An EDIT never reaches here (it folds, not a bubble) — the
|
|
4003
|
+
* `envelopeType !== 'edit'` guard is belt-and-braces. */
|
|
4004
|
+
elevateIfMentioned(message, mentions, senderUserId, envelopeType) {
|
|
4005
|
+
const me = this.backend.selfUserId;
|
|
4006
|
+
if (envelopeType === "edit") return;
|
|
4007
|
+
if (senderUserId === me) return;
|
|
4008
|
+
if (!mentions.some((mm) => mm.mentionedUserId === me)) return;
|
|
4009
|
+
const idPart = message.clientMsgId ? message.clientMsgId : `seq:${message.serverSeq}`;
|
|
4010
|
+
const key = `${me}|${idPart}`;
|
|
4011
|
+
if (this.elevated.has(key)) return;
|
|
4012
|
+
this.elevated.add(key);
|
|
4013
|
+
if (this._group) {
|
|
4014
|
+
void this.backend.saveElevated(this._group, [...this.elevated]).catch(() => {
|
|
4015
|
+
});
|
|
4016
|
+
}
|
|
4017
|
+
this.onMentionElevation?.(message);
|
|
4018
|
+
}
|
|
3836
4019
|
/** Seed the per-target base text + author for the edit fold. Base is write-once
|
|
3837
4020
|
* (a later own/peer edit must not overwrite the original we render against). The
|
|
3838
4021
|
* author is (re)recorded whenever a non-empty resolution is available. */
|
|
@@ -3889,9 +4072,10 @@ var Chat = class {
|
|
|
3889
4072
|
const base = this.originalTextByClientMsgId.has(targetClientMsgId) ? this.originalTextByClientMsgId.get(targetClientMsgId) ?? null : m.text;
|
|
3890
4073
|
const text = editText ?? base;
|
|
3891
4074
|
const edited = foldEdited || m.edited;
|
|
3892
|
-
|
|
4075
|
+
const mentions = editText !== null ? this.editMentions(targetClientMsgId, text) : m.mentions;
|
|
4076
|
+
if (m.text === text && m.edited === edited && sameMentions(m.mentions, mentions)) return m;
|
|
3893
4077
|
changed = true;
|
|
3894
|
-
return { ...m, text, edited };
|
|
4078
|
+
return { ...m, text, edited, mentions };
|
|
3895
4079
|
});
|
|
3896
4080
|
if (changed) this.emit();
|
|
3897
4081
|
}
|
|
@@ -3908,8 +4092,9 @@ var Chat = class {
|
|
|
3908
4092
|
if (editText === null && !foldEdited) return m;
|
|
3909
4093
|
const text = editText ?? m.text;
|
|
3910
4094
|
const edited = foldEdited || m.edited;
|
|
3911
|
-
|
|
3912
|
-
|
|
4095
|
+
const mentions = editText !== null ? this.editMentions(m.clientMsgId, text) : m.mentions;
|
|
4096
|
+
if (m.text === text && m.edited === edited && sameMentions(m.mentions, mentions)) return m;
|
|
4097
|
+
return { ...m, text, edited, mentions };
|
|
3913
4098
|
}
|
|
3914
4099
|
/** @internal — called by the backend's conv subscription. */
|
|
3915
4100
|
applyConv(event, payload) {
|
|
@@ -3967,6 +4152,18 @@ var Chat = class {
|
|
|
3967
4152
|
}
|
|
3968
4153
|
this.editFold.reevaluateHeld(this.authorOfTarget);
|
|
3969
4154
|
for (const cid of this.originalTextByClientMsgId.keys()) this.recomputeEdit(cid);
|
|
4155
|
+
this.reresolveAllMentionNames();
|
|
4156
|
+
}
|
|
4157
|
+
/** Re-resolve roster display names across the whole transcript (called on a roster
|
|
4158
|
+
* change). Re-emits only if any name actually changed. */
|
|
4159
|
+
reresolveAllMentionNames() {
|
|
4160
|
+
let changed = false;
|
|
4161
|
+
this.messageList = this.messageList.map((m) => {
|
|
4162
|
+
const reresolved = this.resolveMentionNames(m);
|
|
4163
|
+
if (reresolved !== m) changed = true;
|
|
4164
|
+
return reresolved;
|
|
4165
|
+
});
|
|
4166
|
+
if (changed) this.emit();
|
|
3970
4167
|
}
|
|
3971
4168
|
seedMembersFromGroup(group) {
|
|
3972
4169
|
const seed = [
|
|
@@ -4034,11 +4231,12 @@ var Chat = class {
|
|
|
4034
4231
|
};
|
|
4035
4232
|
resolvedReplyTo = resolveReply(replyRef, (id) => this.byClientMsgId.get(id) ?? null);
|
|
4036
4233
|
}
|
|
4037
|
-
const
|
|
4038
|
-
this.
|
|
4234
|
+
const bodyRanges = opts?.mentions ?? null;
|
|
4235
|
+
const { receipt, clientMsgId } = await this.backend.sendText(group, text, replyRef, bodyRanges);
|
|
4236
|
+
this.appendOwnSend(text, receipt, clientMsgId, resolvedReplyTo, bodyRanges);
|
|
4039
4237
|
return receipt;
|
|
4040
4238
|
}
|
|
4041
|
-
appendOwnSend(text, receipt, clientMsgId, resolvedReplyTo) {
|
|
4239
|
+
appendOwnSend(text, receipt, clientMsgId, resolvedReplyTo, bodyRanges) {
|
|
4042
4240
|
if (receipt.serverSeq <= 0) return;
|
|
4043
4241
|
const key = this.internalKey(receipt.serverSeq);
|
|
4044
4242
|
if (this.seenKeys.has(key)) return;
|
|
@@ -4063,7 +4261,10 @@ var Chat = class {
|
|
|
4063
4261
|
// Own-sent edits fold via edit() after the fact; new sends start unedited.
|
|
4064
4262
|
edited: false,
|
|
4065
4263
|
// Own-sent deletes fold via deleteForEveryone() after the fact; start live.
|
|
4066
|
-
isDeleted: false
|
|
4264
|
+
isDeleted: false,
|
|
4265
|
+
// Mentions T6: resolve the own-sent bubble's mentions for its own render (the
|
|
4266
|
+
// sender never gets a wire echo of its own message — this is the only local copy).
|
|
4267
|
+
mentions: this.resolveMentions(text, bodyRanges)
|
|
4067
4268
|
});
|
|
4068
4269
|
this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
|
|
4069
4270
|
this.emit();
|
|
@@ -4151,15 +4352,18 @@ var Chat = class {
|
|
|
4151
4352
|
* instantly; the durable echo on the next pump is a fold no-op (dedup on the
|
|
4152
4353
|
* SAME wire clientMsgId). Never appends a bubble; PRESERVES the target's
|
|
4153
4354
|
* reactions + reply context. Only the original author's edits count — for an own
|
|
4154
|
-
* message self IS the author, so the author-gate passes.
|
|
4155
|
-
|
|
4355
|
+
* message self IS the author, so the author-gate passes. `opts.mentions` carries the
|
|
4356
|
+
* edit's REPLACEMENT mention ranges → the edited message's mentions reflect them. */
|
|
4357
|
+
async edit(message, newText, opts) {
|
|
4156
4358
|
if (!message.clientMsgId || message.kind !== "text") return;
|
|
4157
4359
|
const group = await this.materializeIfNeeded();
|
|
4158
4360
|
const clientMsgId = mintClientMsgId();
|
|
4361
|
+
const bodyRanges = opts?.mentions ?? null;
|
|
4159
4362
|
const { receipt } = await this.backend.sendEdit(group, {
|
|
4160
4363
|
clientMsgId,
|
|
4161
4364
|
targetClientMsgId: message.clientMsgId,
|
|
4162
|
-
newText
|
|
4365
|
+
newText,
|
|
4366
|
+
bodyRanges
|
|
4163
4367
|
});
|
|
4164
4368
|
this.seedEditBase(message.clientMsgId, message.text, this.backend.selfUserId);
|
|
4165
4369
|
this.editFold.ingest(
|
|
@@ -4169,7 +4373,8 @@ var Chat = class {
|
|
|
4169
4373
|
newText,
|
|
4170
4374
|
epoch: receipt.epoch,
|
|
4171
4375
|
serverSeq: receipt.serverSeq,
|
|
4172
|
-
eventClientMsgId: clientMsgId
|
|
4376
|
+
eventClientMsgId: clientMsgId,
|
|
4377
|
+
bodyRanges
|
|
4173
4378
|
},
|
|
4174
4379
|
this.authorOfTarget
|
|
4175
4380
|
);
|
|
@@ -4222,6 +4427,18 @@ var Chat = class {
|
|
|
4222
4427
|
}
|
|
4223
4428
|
}
|
|
4224
4429
|
};
|
|
4430
|
+
function sameMentions(a, b) {
|
|
4431
|
+
if (a.length !== b.length) return false;
|
|
4432
|
+
for (let i = 0; i < a.length; i++) {
|
|
4433
|
+
const x = a[i];
|
|
4434
|
+
const y = b[i];
|
|
4435
|
+
if (!x || !y) return false;
|
|
4436
|
+
if (x.start !== y.start || x.length !== y.length || x.mentionedUserId !== y.mentionedUserId || x.displayName !== y.displayName) {
|
|
4437
|
+
return false;
|
|
4438
|
+
}
|
|
4439
|
+
}
|
|
4440
|
+
return true;
|
|
4441
|
+
}
|
|
4225
4442
|
function sameReactions(a, b) {
|
|
4226
4443
|
const ak = Object.keys(a);
|
|
4227
4444
|
const bk = Object.keys(b);
|
|
@@ -4433,14 +4650,21 @@ var MessageDeliverySource = class {
|
|
|
4433
4650
|
// Thread the edit discriminator + new text through the persisted row so an
|
|
4434
4651
|
// edit folded LIVE re-folds onto its target after a reload (the reload-parity
|
|
4435
4652
|
// boundary — mirrors iOS T3). Omitted for non-edits → old rows hydrate as
|
|
4436
|
-
// `'text'`/no-edit (backward-compat).
|
|
4653
|
+
// `'text'`/no-edit (backward-compat). The edit's replacement body_ranges ride
|
|
4654
|
+
// along so the edited message's mentions re-resolve on cold launch (T6).
|
|
4437
4655
|
...isEdit && decoded.edit ? {
|
|
4438
4656
|
envelopeType: "edit",
|
|
4439
4657
|
edit: {
|
|
4440
4658
|
targetClientMsgId: decoded.edit.targetClientMsgId,
|
|
4441
|
-
newText: decoded.edit.newText
|
|
4659
|
+
newText: decoded.edit.newText,
|
|
4660
|
+
...decoded.bodyRanges ? { bodyRanges: decoded.bodyRanges } : {}
|
|
4442
4661
|
}
|
|
4443
4662
|
} : {},
|
|
4663
|
+
// Thread the TEXT bubble's mention ranges (raw) through the persisted row so a
|
|
4664
|
+
// mention surfaced LIVE re-resolves onto its bubble after a reload (the
|
|
4665
|
+
// reload-parity boundary for mentions — T6, mirrors iOS T3). Only on a text
|
|
4666
|
+
// bubble (not a reaction/edit/delete row); omitted when absent (backward-compat).
|
|
4667
|
+
...!isReaction && !isEdit && !isDelete && decoded.bodyRanges ? { bodyRanges: decoded.bodyRanges } : {},
|
|
4444
4668
|
// Thread the delete discriminator + target through the persisted row so a
|
|
4445
4669
|
// delete-for-everyone tombstone folded LIVE re-folds onto its target after
|
|
4446
4670
|
// a reload (the reload-parity boundary — the iOS-review CRITICAL lesson;
|
|
@@ -4472,7 +4696,10 @@ var MessageDeliverySource = class {
|
|
|
4472
4696
|
envelopeType: decoded.type ?? "text",
|
|
4473
4697
|
reaction: isReaction ? decoded.reaction : null,
|
|
4474
4698
|
edit: isEdit ? decoded.edit : null,
|
|
4475
|
-
delete: isDelete ? decoded.delete : null
|
|
4699
|
+
delete: isDelete ? decoded.delete : null,
|
|
4700
|
+
// The raw mention ranges (text bubble or the edit's replacement ranges); the
|
|
4701
|
+
// Chat normalizes + resolves names → ChatMessage.mentions (T6).
|
|
4702
|
+
bodyRanges: decoded.bodyRanges ?? null
|
|
4476
4703
|
});
|
|
4477
4704
|
return true;
|
|
4478
4705
|
}
|
|
@@ -4627,6 +4854,36 @@ var GroupCatalog = class {
|
|
|
4627
4854
|
}
|
|
4628
4855
|
};
|
|
4629
4856
|
|
|
4857
|
+
// src/messaging/mention-elevation.ts
|
|
4858
|
+
var MentionElevationStore = class {
|
|
4859
|
+
constructor(kv) {
|
|
4860
|
+
this.kv = kv;
|
|
4861
|
+
}
|
|
4862
|
+
kv;
|
|
4863
|
+
key(rfcGroupId) {
|
|
4864
|
+
return `elev:${rfcGroupId}`;
|
|
4865
|
+
}
|
|
4866
|
+
/** Load the persisted elevation keys for a chat (empty array if none). */
|
|
4867
|
+
async load(rfcGroupId) {
|
|
4868
|
+
const raw = await this.kv.get(this.key(rfcGroupId));
|
|
4869
|
+
if (!raw) return [];
|
|
4870
|
+
try {
|
|
4871
|
+
const parsed = JSON.parse(decodeUtf8(raw));
|
|
4872
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
4873
|
+
} catch {
|
|
4874
|
+
return [];
|
|
4875
|
+
}
|
|
4876
|
+
}
|
|
4877
|
+
/** Persist the full elevation key set for a chat (deterministic, deduped order). */
|
|
4878
|
+
async save(rfcGroupId, keys) {
|
|
4879
|
+
const sorted = [...new Set(keys)].sort();
|
|
4880
|
+
await this.kv.set(this.key(rfcGroupId), encodeUtf8(JSON.stringify(sorted)));
|
|
4881
|
+
}
|
|
4882
|
+
async wipe() {
|
|
4883
|
+
for (const k of await this.kv.keys("elev:")) await this.kv.delete(k);
|
|
4884
|
+
}
|
|
4885
|
+
};
|
|
4886
|
+
|
|
4630
4887
|
// src/messaging/wasm/pkg/palbe_mls_bg.js
|
|
4631
4888
|
var palbe_mls_bg_exports = {};
|
|
4632
4889
|
__export(palbe_mls_bg_exports, {
|
|
@@ -6334,6 +6591,7 @@ var MessagingCoordinator = class {
|
|
|
6334
6591
|
this.groupStore = new GroupStateStorage(this.kv);
|
|
6335
6592
|
this.kpStore = new KeyPackageStorage(this.kv);
|
|
6336
6593
|
this.suppressionStore = new SuppressionStore(this.kv);
|
|
6594
|
+
this.elevationStore = new MentionElevationStore(this.kv);
|
|
6337
6595
|
this.registry.attachChatList(
|
|
6338
6596
|
(chats) => {
|
|
6339
6597
|
this.chatList = chats;
|
|
@@ -6349,6 +6607,7 @@ var MessagingCoordinator = class {
|
|
|
6349
6607
|
groupStore;
|
|
6350
6608
|
kpStore;
|
|
6351
6609
|
suppressionStore;
|
|
6610
|
+
elevationStore;
|
|
6352
6611
|
registry = new GroupRegistry();
|
|
6353
6612
|
resolved = null;
|
|
6354
6613
|
resolvePromise = null;
|
|
@@ -6504,9 +6763,9 @@ var MessagingCoordinator = class {
|
|
|
6504
6763
|
});
|
|
6505
6764
|
return group;
|
|
6506
6765
|
}
|
|
6507
|
-
async sendText(group, text, replyTo) {
|
|
6766
|
+
async sendText(group, text, replyTo, bodyRanges) {
|
|
6508
6767
|
const r = await this.resolve();
|
|
6509
|
-
return r.groups.sendText(group, text, replyTo);
|
|
6768
|
+
return r.groups.sendText(group, text, replyTo, bodyRanges);
|
|
6510
6769
|
}
|
|
6511
6770
|
async sendReaction(group, args) {
|
|
6512
6771
|
const r = await this.resolve();
|
|
@@ -6528,6 +6787,14 @@ var MessagingCoordinator = class {
|
|
|
6528
6787
|
saveSuppressed(group, keys) {
|
|
6529
6788
|
return this.suppressionStore.save(group.rfcGroupId, keys);
|
|
6530
6789
|
}
|
|
6790
|
+
/** Load this chat's persisted self-elevation dedup keys (durable-only). */
|
|
6791
|
+
loadElevated(group) {
|
|
6792
|
+
return this.elevationStore.load(group.rfcGroupId);
|
|
6793
|
+
}
|
|
6794
|
+
/** Persist this chat's self-elevation dedup keys (durable-only, no wire). */
|
|
6795
|
+
saveElevated(group, keys) {
|
|
6796
|
+
return this.elevationStore.save(group.rfcGroupId, keys);
|
|
6797
|
+
}
|
|
6531
6798
|
async history(group, limit, before) {
|
|
6532
6799
|
const r = await this.resolve();
|
|
6533
6800
|
const rows = await r.messageStore.history(group.rfcGroupId, limit, before);
|
|
@@ -6646,7 +6913,10 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
6646
6913
|
newText: s.edit.newText,
|
|
6647
6914
|
epoch: s.epoch,
|
|
6648
6915
|
serverSeq: s.serverSeq,
|
|
6649
|
-
eventClientMsgId: s.clientMsgId ?? `${s.id}
|
|
6916
|
+
eventClientMsgId: s.clientMsgId ?? `${s.id}`,
|
|
6917
|
+
// Mentions T6: the edit's replacement ranges ride the fold so the WINNING
|
|
6918
|
+
// edit's ranges drive the edited message's mentions on cold launch.
|
|
6919
|
+
bodyRanges: s.edit.bodyRanges ?? null
|
|
6650
6920
|
},
|
|
6651
6921
|
authorOfTarget
|
|
6652
6922
|
);
|
|
@@ -6696,7 +6966,9 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
6696
6966
|
replyTo: null,
|
|
6697
6967
|
reactions: {},
|
|
6698
6968
|
edited: false,
|
|
6699
|
-
isDeleted: true
|
|
6969
|
+
isDeleted: true,
|
|
6970
|
+
// Mentions T6: delete DOMINATES mentions too — a tombstoned message has none.
|
|
6971
|
+
mentions: []
|
|
6700
6972
|
});
|
|
6701
6973
|
continue;
|
|
6702
6974
|
}
|
|
@@ -6716,23 +6988,36 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
6716
6988
|
}
|
|
6717
6989
|
const editText = clientMsgId ? editFold.text(clientMsgId) : null;
|
|
6718
6990
|
const edited = clientMsgId ? editFold.isEdited(clientMsgId) : false;
|
|
6991
|
+
const text = editText ?? s.text;
|
|
6992
|
+
const rawRanges = editText !== null ? editFold.bodyRanges(clientMsgId) : s.bodyRanges;
|
|
6993
|
+
const mentions = normalizeMentionsNullNames(rawRanges, text);
|
|
6719
6994
|
out.push({
|
|
6720
6995
|
id: `${displayId}#${s.serverSeq}`,
|
|
6721
6996
|
kind: s.text != null ? "text" : "system",
|
|
6722
6997
|
direction: s.direction,
|
|
6723
6998
|
senderUserId: s.direction === "outgoing" ? selfUserId : null,
|
|
6724
|
-
text
|
|
6999
|
+
text,
|
|
6725
7000
|
serverSeq: s.serverSeq,
|
|
6726
7001
|
sentAt: new Date(s.at),
|
|
6727
7002
|
clientMsgId,
|
|
6728
7003
|
replyTo,
|
|
6729
7004
|
reactions: clientMsgId ? fold.tally(clientMsgId) : {},
|
|
6730
7005
|
edited,
|
|
6731
|
-
isDeleted: false
|
|
7006
|
+
isDeleted: false,
|
|
7007
|
+
mentions
|
|
6732
7008
|
});
|
|
6733
7009
|
}
|
|
6734
7010
|
return out;
|
|
6735
7011
|
}
|
|
7012
|
+
function normalizeMentionsNullNames(raw, text) {
|
|
7013
|
+
if (text === null || !raw || raw.length === 0) return [];
|
|
7014
|
+
return normalizeMentionRangesUtf16(raw, text).map((r) => ({
|
|
7015
|
+
start: r.start,
|
|
7016
|
+
length: r.length,
|
|
7017
|
+
mentionedUserId: r.mentionedUserId,
|
|
7018
|
+
displayName: null
|
|
7019
|
+
}));
|
|
7020
|
+
}
|
|
6736
7021
|
|
|
6737
7022
|
// src/messaging/facade.ts
|
|
6738
7023
|
var PalbeMessaging = class {
|
|
@@ -7466,7 +7751,7 @@ function defaultSessionStorage(key) {
|
|
|
7466
7751
|
}
|
|
7467
7752
|
|
|
7468
7753
|
// src/version.ts
|
|
7469
|
-
var VERSION = "1.
|
|
7754
|
+
var VERSION = "1.5.0";
|
|
7470
7755
|
|
|
7471
7756
|
// src/runtime.ts
|
|
7472
7757
|
function buildRuntime(config) {
|