@palbase/web 1.2.1 → 1.3.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-Ct3A1zop.d.cts → analytics-facade-B8UATgS4.d.cts} +66 -1
- package/dist/{analytics-facade-C93tr7dA.d.ts → analytics-facade-Cq7R-VH_.d.ts} +66 -1
- package/dist/{chunk-KJXRY4S3.js → chunk-UX43AB4W.js} +332 -10
- package/dist/chunk-UX43AB4W.js.map +1 -0
- package/dist/index.cjs +331 -9
- 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 +331 -9
- 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 +331 -9
- package/dist/next/client.cjs.map +1 -1
- package/dist/next/client.js +1 -1
- package/dist/next/index.cjs +331 -9
- 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-BtYdWClg.d.ts → pb-DSJiH1uV.d.ts} +1 -1
- package/dist/{pb-BlIfgBG-.d.cts → pb-DivIiUGR.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-KJXRY4S3.js.map +0 -1
package/dist/next/client.js
CHANGED
package/dist/next/index.cjs
CHANGED
|
@@ -2628,6 +2628,88 @@ var PalbeFlags = class {
|
|
|
2628
2628
|
}
|
|
2629
2629
|
};
|
|
2630
2630
|
|
|
2631
|
+
// src/messaging/edit-fold.ts
|
|
2632
|
+
function orderLt(aEpoch, aSeq, bEpoch, bSeq) {
|
|
2633
|
+
if (aEpoch !== bEpoch) return aEpoch < bEpoch;
|
|
2634
|
+
return aSeq < bSeq;
|
|
2635
|
+
}
|
|
2636
|
+
function orderEq(aEpoch, aSeq, bEpoch, bSeq) {
|
|
2637
|
+
return aEpoch === bEpoch && aSeq === bSeq;
|
|
2638
|
+
}
|
|
2639
|
+
var EditFold = class {
|
|
2640
|
+
// target → winning edit state
|
|
2641
|
+
states = /* @__PURE__ */ new Map();
|
|
2642
|
+
// dedup of real wire events that reached (and were resolvable enough to evaluate at) the fold
|
|
2643
|
+
seenEvents = /* @__PURE__ */ new Set();
|
|
2644
|
+
// events parked because target/author or sender was unresolved at ingest time
|
|
2645
|
+
held = [];
|
|
2646
|
+
// targets that have had ≥1 valid edit applied (write-once)
|
|
2647
|
+
editedTargets = /* @__PURE__ */ new Set();
|
|
2648
|
+
/**
|
|
2649
|
+
* Ingest one edit. `authorOfTarget` resolves the target message's AUTHOR userId
|
|
2650
|
+
* (null = target unknown/dangling → HOLD).
|
|
2651
|
+
*/
|
|
2652
|
+
ingest(e, authorOfTarget) {
|
|
2653
|
+
const author = authorOfTarget(e.targetClientMsgId);
|
|
2654
|
+
if (author === null) {
|
|
2655
|
+
this.holdIfNew(e);
|
|
2656
|
+
return;
|
|
2657
|
+
}
|
|
2658
|
+
if (e.editorUserId === null) {
|
|
2659
|
+
this.holdIfNew(e);
|
|
2660
|
+
return;
|
|
2661
|
+
}
|
|
2662
|
+
if (e.editorUserId !== author) return;
|
|
2663
|
+
if (this.seenEvents.has(e.eventClientMsgId)) return;
|
|
2664
|
+
this.seenEvents.add(e.eventClientMsgId);
|
|
2665
|
+
const prev = this.states.get(e.targetClientMsgId);
|
|
2666
|
+
if (prev !== void 0) {
|
|
2667
|
+
if (orderLt(e.epoch, e.serverSeq, prev.orderEpoch, prev.orderSeq)) return;
|
|
2668
|
+
if (orderEq(e.epoch, e.serverSeq, prev.orderEpoch, prev.orderSeq) && e.eventClientMsgId <= prev.lastEventId) {
|
|
2669
|
+
return;
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
this.states.set(e.targetClientMsgId, {
|
|
2673
|
+
orderEpoch: e.epoch,
|
|
2674
|
+
orderSeq: e.serverSeq,
|
|
2675
|
+
lastEventId: e.eventClientMsgId,
|
|
2676
|
+
text: e.newText
|
|
2677
|
+
});
|
|
2678
|
+
this.editedTargets.add(e.targetClientMsgId);
|
|
2679
|
+
}
|
|
2680
|
+
/**
|
|
2681
|
+
* Park an event for later re-attempt, deduping held re-deliveries by
|
|
2682
|
+
* eventClientMsgId so a repeatedly-delivered unresolvable edit is held exactly
|
|
2683
|
+
* once (and never double-applies when it finally resolves on reevaluate).
|
|
2684
|
+
*/
|
|
2685
|
+
holdIfNew(e) {
|
|
2686
|
+
if (this.held.some((h) => h.eventClientMsgId === e.eventClientMsgId)) return;
|
|
2687
|
+
this.held.push(e);
|
|
2688
|
+
}
|
|
2689
|
+
/** The winning edit text for a target, or null if no valid edit has applied. */
|
|
2690
|
+
text(targetClientMsgId) {
|
|
2691
|
+
return this.states.get(targetClientMsgId)?.text ?? null;
|
|
2692
|
+
}
|
|
2693
|
+
/** Write-once: true once any valid edit applied to the target. */
|
|
2694
|
+
isEdited(targetClientMsgId) {
|
|
2695
|
+
return this.editedTargets.has(targetClientMsgId);
|
|
2696
|
+
}
|
|
2697
|
+
/**
|
|
2698
|
+
* Re-run HELD edits when the roster/target newly resolves (call on member/roster
|
|
2699
|
+
* change and when a target message arrives). Clears `held` and re-ingests each
|
|
2700
|
+
* event with the fresh `authorOfTarget` — events that still don't resolve are
|
|
2701
|
+
* simply re-held; events that now resolve fold via the normal LWW path.
|
|
2702
|
+
* Idempotent: re-ingest is deduped by `seenEvents` (applied events) and by
|
|
2703
|
+
* `holdIfNew` (still-held events), so reevaluating repeatedly can neither
|
|
2704
|
+
* double-apply nor lose an edit.
|
|
2705
|
+
*/
|
|
2706
|
+
reevaluateHeld(authorOfTarget) {
|
|
2707
|
+
const pending = this.held;
|
|
2708
|
+
this.held = [];
|
|
2709
|
+
for (const e of pending) this.ingest(e, authorOfTarget);
|
|
2710
|
+
}
|
|
2711
|
+
};
|
|
2712
|
+
|
|
2631
2713
|
// src/messaging/util.ts
|
|
2632
2714
|
function toBase64(bytes) {
|
|
2633
2715
|
if (typeof Buffer !== "undefined") {
|
|
@@ -2767,6 +2849,17 @@ async function listDevices(rt, userId) {
|
|
|
2767
2849
|
}
|
|
2768
2850
|
|
|
2769
2851
|
// src/messaging/group-messaging.ts
|
|
2852
|
+
function encodeEdit(args) {
|
|
2853
|
+
return encodeUtf8(
|
|
2854
|
+
JSON.stringify({
|
|
2855
|
+
v: 1,
|
|
2856
|
+
type: "edit",
|
|
2857
|
+
client_msg_id: args.clientMsgId,
|
|
2858
|
+
target_client_msg_id: args.targetClientMsgId,
|
|
2859
|
+
new_text: args.newText
|
|
2860
|
+
})
|
|
2861
|
+
);
|
|
2862
|
+
}
|
|
2770
2863
|
function encodeReaction(args) {
|
|
2771
2864
|
return encodeUtf8(
|
|
2772
2865
|
JSON.stringify({
|
|
@@ -2806,6 +2899,18 @@ function decodeEnvelope(bytes) {
|
|
|
2806
2899
|
}
|
|
2807
2900
|
};
|
|
2808
2901
|
}
|
|
2902
|
+
if (typeof o === "object" && o !== null && o.type === "edit") {
|
|
2903
|
+
return {
|
|
2904
|
+
type: "edit",
|
|
2905
|
+
text: null,
|
|
2906
|
+
clientMsgId: o.client_msg_id ?? "",
|
|
2907
|
+
replyTo: null,
|
|
2908
|
+
edit: {
|
|
2909
|
+
targetClientMsgId: o.target_client_msg_id ?? "",
|
|
2910
|
+
newText: o.new_text ?? ""
|
|
2911
|
+
}
|
|
2912
|
+
};
|
|
2913
|
+
}
|
|
2809
2914
|
if (typeof o === "object" && o !== null && o.type === "text" && typeof o.v === "number") {
|
|
2810
2915
|
return {
|
|
2811
2916
|
type: "text",
|
|
@@ -3120,6 +3225,54 @@ var GroupMessaging = class {
|
|
|
3120
3225
|
clientMsgId: args.clientMsgId
|
|
3121
3226
|
};
|
|
3122
3227
|
}
|
|
3228
|
+
/** Send an edit (edit-by-supersession on a target message). Encrypts a
|
|
3229
|
+
* `type:'edit'` envelope at the current epoch and sends through the SAME MLS
|
|
3230
|
+
* application path as `sendText` (the server stays blind — an edit is just
|
|
3231
|
+
* another application message). Persists the outgoing edit row so it re-folds
|
|
3232
|
+
* onto its target's text after a reload (the own-send half of the reload
|
|
3233
|
+
* parity). NEVER rebases (epoch-bound like any application message). */
|
|
3234
|
+
async sendEdit(group, args) {
|
|
3235
|
+
const plaintext = encodeEdit({
|
|
3236
|
+
clientMsgId: args.clientMsgId,
|
|
3237
|
+
targetClientMsgId: args.targetClientMsgId,
|
|
3238
|
+
newText: args.newText
|
|
3239
|
+
});
|
|
3240
|
+
const ct = await this.engine.encryptApplication(fromBase64(group.rfcGroupId), plaintext);
|
|
3241
|
+
const body = {
|
|
3242
|
+
ciphertext_b64: toBase64(ct),
|
|
3243
|
+
client_idem_key: randomId()
|
|
3244
|
+
};
|
|
3245
|
+
const wire = await palbeRequest(
|
|
3246
|
+
this.rt,
|
|
3247
|
+
"POST",
|
|
3248
|
+
MessagingPaths.groupMessages(group.displayId),
|
|
3249
|
+
{ body }
|
|
3250
|
+
);
|
|
3251
|
+
const stored = {
|
|
3252
|
+
id: `${group.rfcGroupId}#${wire.server_seq}`,
|
|
3253
|
+
direction: "outgoing",
|
|
3254
|
+
text: null,
|
|
3255
|
+
senderDeviceId: this.selfDeviceId,
|
|
3256
|
+
epoch: wire.epoch,
|
|
3257
|
+
serverSeq: wire.server_seq,
|
|
3258
|
+
at: Date.now(),
|
|
3259
|
+
clientMsgId: args.clientMsgId,
|
|
3260
|
+
replyTo: null,
|
|
3261
|
+
envelopeType: "edit",
|
|
3262
|
+
edit: {
|
|
3263
|
+
targetClientMsgId: args.targetClientMsgId,
|
|
3264
|
+
newText: args.newText
|
|
3265
|
+
}
|
|
3266
|
+
};
|
|
3267
|
+
try {
|
|
3268
|
+
await this.messageStore.append(group.rfcGroupId, stored);
|
|
3269
|
+
} catch {
|
|
3270
|
+
}
|
|
3271
|
+
return {
|
|
3272
|
+
receipt: { serverSeq: wire.server_seq, epoch: wire.epoch },
|
|
3273
|
+
clientMsgId: args.clientMsgId
|
|
3274
|
+
};
|
|
3275
|
+
}
|
|
3123
3276
|
// ── The rebase loop ──
|
|
3124
3277
|
async commitWithRebase(rfcGroupId, build) {
|
|
3125
3278
|
const gidBytes = fromBase64(rfcGroupId);
|
|
@@ -3256,6 +3409,14 @@ var Chat = class {
|
|
|
3256
3409
|
byClientMsgId = /* @__PURE__ */ new Map();
|
|
3257
3410
|
/** The single authoritative reaction fold for this chat (live + own-send + history). */
|
|
3258
3411
|
reactionFold = new ReactionFold();
|
|
3412
|
+
/** The single authoritative edit fold for this chat (live + own-send + history). */
|
|
3413
|
+
editFold = new EditFold();
|
|
3414
|
+
/** Per-target BASE (original) text, so the rendered text is `edit ?? original`.
|
|
3415
|
+
* Seeded once per target (write-once base) so a 2nd own edit can't corrupt it. */
|
|
3416
|
+
originalTextByClientMsgId = /* @__PURE__ */ new Map();
|
|
3417
|
+
/** Per-target AUTHOR userId — the EditFold author-gate input (filled at bubble
|
|
3418
|
+
* projection time from senderUserId; '' = resolved-but-unknown peer). */
|
|
3419
|
+
authorByClientMsgId = /* @__PURE__ */ new Map();
|
|
3259
3420
|
loadedEarliestSeq = null;
|
|
3260
3421
|
historyLoaded = false;
|
|
3261
3422
|
wired = false;
|
|
@@ -3356,13 +3517,17 @@ var Chat = class {
|
|
|
3356
3517
|
senderUserId: m.senderUserId ?? ""
|
|
3357
3518
|
});
|
|
3358
3519
|
}
|
|
3520
|
+
if (m.clientMsgId) {
|
|
3521
|
+
this.seedEditBase(m.clientMsgId, m.text, m.senderUserId ?? "");
|
|
3522
|
+
}
|
|
3359
3523
|
}
|
|
3524
|
+
this.editFold.reevaluateHeld(this.authorOfTarget);
|
|
3360
3525
|
for (const m of incoming) {
|
|
3361
3526
|
if (m.serverSeq <= 0) continue;
|
|
3362
3527
|
const key = this.internalKey(m.serverSeq);
|
|
3363
3528
|
if (this.seenKeys.has(key)) continue;
|
|
3364
3529
|
this.seenKeys.add(key);
|
|
3365
|
-
this.messageList.push(this.applyReactionTally(m));
|
|
3530
|
+
this.messageList.push(this.applyEditOverlay(this.applyReactionTally(m)));
|
|
3366
3531
|
changed = true;
|
|
3367
3532
|
this.loadedEarliestSeq = Math.min(this.loadedEarliestSeq ?? m.serverSeq, m.serverSeq);
|
|
3368
3533
|
}
|
|
@@ -3403,6 +3568,22 @@ var Chat = class {
|
|
|
3403
3568
|
}
|
|
3404
3569
|
return;
|
|
3405
3570
|
}
|
|
3571
|
+
if (incoming.envelopeType === "edit" && incoming.edit) {
|
|
3572
|
+
const editorUserId = direction === "outgoing" ? this.backend.selfUserId : senderUser;
|
|
3573
|
+
this.editFold.ingest(
|
|
3574
|
+
{
|
|
3575
|
+
targetClientMsgId: incoming.edit.targetClientMsgId,
|
|
3576
|
+
editorUserId,
|
|
3577
|
+
newText: incoming.edit.newText,
|
|
3578
|
+
epoch: incoming.epoch,
|
|
3579
|
+
serverSeq: incoming.serverSeq,
|
|
3580
|
+
eventClientMsgId: incoming.clientMsgId
|
|
3581
|
+
},
|
|
3582
|
+
this.authorOfTarget
|
|
3583
|
+
);
|
|
3584
|
+
this.recomputeEdit(incoming.edit.targetClientMsgId);
|
|
3585
|
+
return;
|
|
3586
|
+
}
|
|
3406
3587
|
const incomingClientMsgId = incoming.clientMsgId;
|
|
3407
3588
|
const incomingReplyRef = incoming.replyRef;
|
|
3408
3589
|
let resolvedReplyTo = null;
|
|
@@ -3421,7 +3602,9 @@ var Chat = class {
|
|
|
3421
3602
|
replyTo: resolvedReplyTo,
|
|
3422
3603
|
// Attach any tally already folded for this message (a reaction that arrived
|
|
3423
3604
|
// BEFORE its target — the dangling case — renders the moment the target lands).
|
|
3424
|
-
reactions: incomingClientMsgId ? this.reactionFold.tally(incomingClientMsgId) : {}
|
|
3605
|
+
reactions: incomingClientMsgId ? this.reactionFold.tally(incomingClientMsgId) : {},
|
|
3606
|
+
// Default false; applyEditOverlay below folds any edit that arrived first.
|
|
3607
|
+
edited: false
|
|
3425
3608
|
};
|
|
3426
3609
|
if (incomingClientMsgId && incoming.text !== null) {
|
|
3427
3610
|
this.byClientMsgId.set(incomingClientMsgId, {
|
|
@@ -3429,7 +3612,11 @@ var Chat = class {
|
|
|
3429
3612
|
senderUserId: senderUser ?? ""
|
|
3430
3613
|
});
|
|
3431
3614
|
}
|
|
3432
|
-
|
|
3615
|
+
if (incomingClientMsgId) {
|
|
3616
|
+
this.seedEditBase(incomingClientMsgId, incoming.text, senderUser);
|
|
3617
|
+
this.editFold.reevaluateHeld(this.authorOfTarget);
|
|
3618
|
+
}
|
|
3619
|
+
this.messageList.push(this.applyEditOverlay(msg));
|
|
3433
3620
|
this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
|
|
3434
3621
|
this.loadedEarliestSeq = Math.min(
|
|
3435
3622
|
this.loadedEarliestSeq ?? incoming.serverSeq,
|
|
@@ -3437,6 +3624,19 @@ var Chat = class {
|
|
|
3437
3624
|
);
|
|
3438
3625
|
this.emit();
|
|
3439
3626
|
}
|
|
3627
|
+
/** The EditFold author-gate input: the target message's resolved author userId
|
|
3628
|
+
* (null = target unknown/dangling → the fold HOLDs). Captured as a bound arrow
|
|
3629
|
+
* so it can be passed to the pure EditFold. */
|
|
3630
|
+
authorOfTarget = (targetClientMsgId) => this.authorByClientMsgId.get(targetClientMsgId) ?? null;
|
|
3631
|
+
/** Seed the per-target base text + author for the edit fold. Base is write-once
|
|
3632
|
+
* (a later own/peer edit must not overwrite the original we render against). The
|
|
3633
|
+
* author is (re)recorded whenever a non-empty resolution is available. */
|
|
3634
|
+
seedEditBase(clientMsgId, text, author) {
|
|
3635
|
+
if (!this.originalTextByClientMsgId.has(clientMsgId)) {
|
|
3636
|
+
this.originalTextByClientMsgId.set(clientMsgId, text);
|
|
3637
|
+
}
|
|
3638
|
+
if (author !== null) this.authorByClientMsgId.set(clientMsgId, author);
|
|
3639
|
+
}
|
|
3440
3640
|
/**
|
|
3441
3641
|
* Rebuild the target message's `reactions` from the authoritative fold and
|
|
3442
3642
|
* re-emit. No-op when the target isn't present yet (its tally is attached the
|
|
@@ -3466,6 +3666,46 @@ var Chat = class {
|
|
|
3466
3666
|
if (sameReactions(m.reactions, tally)) return m;
|
|
3467
3667
|
return { ...m, reactions: tally };
|
|
3468
3668
|
}
|
|
3669
|
+
/**
|
|
3670
|
+
* Rebuild the target message's rendered `text` + `edited` flag from the
|
|
3671
|
+
* authoritative edit fold and re-emit, PRESERVING `.reactions` and `.replyTo`
|
|
3672
|
+
* (the reaction-polish lesson — never clobber). text = `editFold.text(cid) ??
|
|
3673
|
+
* base`; base is the seeded original so a forged/ignored edit leaves it intact.
|
|
3674
|
+
* No-op when the target isn't present yet (the fold already recorded it; the
|
|
3675
|
+
* overlay applies the moment the target lands) or when unchanged.
|
|
3676
|
+
*/
|
|
3677
|
+
recomputeEdit(targetClientMsgId) {
|
|
3678
|
+
if (!targetClientMsgId) return;
|
|
3679
|
+
const editText = this.editFold.text(targetClientMsgId);
|
|
3680
|
+
const foldEdited = this.editFold.isEdited(targetClientMsgId);
|
|
3681
|
+
let changed = false;
|
|
3682
|
+
this.messageList = this.messageList.map((m) => {
|
|
3683
|
+
if (m.clientMsgId !== targetClientMsgId) return m;
|
|
3684
|
+
const base = this.originalTextByClientMsgId.has(targetClientMsgId) ? this.originalTextByClientMsgId.get(targetClientMsgId) ?? null : m.text;
|
|
3685
|
+
const text = editText ?? base;
|
|
3686
|
+
const edited = foldEdited || m.edited;
|
|
3687
|
+
if (m.text === text && m.edited === edited) return m;
|
|
3688
|
+
changed = true;
|
|
3689
|
+
return { ...m, text, edited };
|
|
3690
|
+
});
|
|
3691
|
+
if (changed) this.emit();
|
|
3692
|
+
}
|
|
3693
|
+
/**
|
|
3694
|
+
* Overlay the authoritative edit fold's winning text + flag onto a message as it
|
|
3695
|
+
* is appended/merged. The fold WINS when it has an edit for this target;
|
|
3696
|
+
* otherwise the upstream `text`/`edited` (e.g. the coordinator's page-local
|
|
3697
|
+
* history fold) is preserved. PRESERVES reactions + replyTo.
|
|
3698
|
+
*/
|
|
3699
|
+
applyEditOverlay(m) {
|
|
3700
|
+
if (!m.clientMsgId) return m;
|
|
3701
|
+
const editText = this.editFold.text(m.clientMsgId);
|
|
3702
|
+
const foldEdited = this.editFold.isEdited(m.clientMsgId);
|
|
3703
|
+
if (editText === null && !foldEdited) return m;
|
|
3704
|
+
const text = editText ?? m.text;
|
|
3705
|
+
const edited = foldEdited || m.edited;
|
|
3706
|
+
if (m.text === text && m.edited === edited) return m;
|
|
3707
|
+
return { ...m, text, edited };
|
|
3708
|
+
}
|
|
3469
3709
|
/** @internal — called by the backend's conv subscription. */
|
|
3470
3710
|
applyConv(event, payload) {
|
|
3471
3711
|
const userId = typeof payload.user_id === "string" ? payload.user_id : null;
|
|
@@ -3520,6 +3760,8 @@ var Chat = class {
|
|
|
3520
3760
|
this.memberCache = m;
|
|
3521
3761
|
this.emit();
|
|
3522
3762
|
}
|
|
3763
|
+
this.editFold.reevaluateHeld(this.authorOfTarget);
|
|
3764
|
+
for (const cid of this.originalTextByClientMsgId.keys()) this.recomputeEdit(cid);
|
|
3523
3765
|
}
|
|
3524
3766
|
seedMembersFromGroup(group) {
|
|
3525
3767
|
const seed = [
|
|
@@ -3598,6 +3840,7 @@ var Chat = class {
|
|
|
3598
3840
|
this.seenKeys.add(key);
|
|
3599
3841
|
if (clientMsgId) {
|
|
3600
3842
|
this.byClientMsgId.set(clientMsgId, { text, senderUserId: this.backend.selfUserId });
|
|
3843
|
+
this.seedEditBase(clientMsgId, text, this.backend.selfUserId);
|
|
3601
3844
|
}
|
|
3602
3845
|
this.messageList.push({
|
|
3603
3846
|
id: this.publicId(receipt.serverSeq),
|
|
@@ -3611,7 +3854,9 @@ var Chat = class {
|
|
|
3611
3854
|
replyTo: resolvedReplyTo,
|
|
3612
3855
|
// Attach any tally already folded for this own-sent message (rare, but keeps
|
|
3613
3856
|
// the dangling-target invariant uniform across every append path).
|
|
3614
|
-
reactions: clientMsgId ? this.reactionFold.tally(clientMsgId) : {}
|
|
3857
|
+
reactions: clientMsgId ? this.reactionFold.tally(clientMsgId) : {},
|
|
3858
|
+
// Own-sent edits fold via edit() after the fact; new sends start unedited.
|
|
3859
|
+
edited: false
|
|
3615
3860
|
});
|
|
3616
3861
|
this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
|
|
3617
3862
|
this.emit();
|
|
@@ -3692,6 +3937,37 @@ var Chat = class {
|
|
|
3692
3937
|
});
|
|
3693
3938
|
this.recomputeReactions(message.clientMsgId);
|
|
3694
3939
|
}
|
|
3940
|
+
// ── Edit ──
|
|
3941
|
+
/** Edit an own text message (edit-by-supersession). No-op if the message isn't
|
|
3942
|
+
* editable (empty clientMsgId, or not a `text` kind). The edit folds locally
|
|
3943
|
+
* with the server receipt's `(epoch, serverSeq)` so the target's text updates
|
|
3944
|
+
* instantly; the durable echo on the next pump is a fold no-op (dedup on the
|
|
3945
|
+
* SAME wire clientMsgId). Never appends a bubble; PRESERVES the target's
|
|
3946
|
+
* reactions + reply context. Only the original author's edits count — for an own
|
|
3947
|
+
* message self IS the author, so the author-gate passes. */
|
|
3948
|
+
async edit(message, newText) {
|
|
3949
|
+
if (!message.clientMsgId || message.kind !== "text") return;
|
|
3950
|
+
const group = await this.materializeIfNeeded();
|
|
3951
|
+
const clientMsgId = mintClientMsgId();
|
|
3952
|
+
const { receipt } = await this.backend.sendEdit(group, {
|
|
3953
|
+
clientMsgId,
|
|
3954
|
+
targetClientMsgId: message.clientMsgId,
|
|
3955
|
+
newText
|
|
3956
|
+
});
|
|
3957
|
+
this.seedEditBase(message.clientMsgId, message.text, this.backend.selfUserId);
|
|
3958
|
+
this.editFold.ingest(
|
|
3959
|
+
{
|
|
3960
|
+
targetClientMsgId: message.clientMsgId,
|
|
3961
|
+
editorUserId: this.backend.selfUserId,
|
|
3962
|
+
newText,
|
|
3963
|
+
epoch: receipt.epoch,
|
|
3964
|
+
serverSeq: receipt.serverSeq,
|
|
3965
|
+
eventClientMsgId: clientMsgId
|
|
3966
|
+
},
|
|
3967
|
+
this.authorOfTarget
|
|
3968
|
+
);
|
|
3969
|
+
this.recomputeEdit(message.clientMsgId);
|
|
3970
|
+
}
|
|
3695
3971
|
};
|
|
3696
3972
|
function sameReactions(a, b) {
|
|
3697
3973
|
const ak = Object.keys(a);
|
|
@@ -3871,6 +4147,7 @@ var MessageDeliverySource = class {
|
|
|
3871
4147
|
const decoded = decodeEnvelope(received.data);
|
|
3872
4148
|
const { text, clientMsgId, replyTo } = decoded;
|
|
3873
4149
|
const isReaction = decoded.type === "reaction" && decoded.reaction != null;
|
|
4150
|
+
const isEdit = decoded.type === "edit" && decoded.edit != null;
|
|
3874
4151
|
const senderDeviceId = row.sender_device_id ?? decodeSenderDeviceId(received.sender);
|
|
3875
4152
|
const stored = {
|
|
3876
4153
|
id: `${group.rfcGroupId}#${row.server_seq}`,
|
|
@@ -3898,6 +4175,17 @@ var MessageDeliverySource = class {
|
|
|
3898
4175
|
emoji: decoded.reaction.emoji,
|
|
3899
4176
|
op: decoded.reaction.op
|
|
3900
4177
|
}
|
|
4178
|
+
} : {},
|
|
4179
|
+
// Thread the edit discriminator + new text through the persisted row so an
|
|
4180
|
+
// edit folded LIVE re-folds onto its target after a reload (the reload-parity
|
|
4181
|
+
// boundary — mirrors iOS T3). Omitted for non-edits → old rows hydrate as
|
|
4182
|
+
// `'text'`/no-edit (backward-compat).
|
|
4183
|
+
...isEdit && decoded.edit ? {
|
|
4184
|
+
envelopeType: "edit",
|
|
4185
|
+
edit: {
|
|
4186
|
+
targetClientMsgId: decoded.edit.targetClientMsgId,
|
|
4187
|
+
newText: decoded.edit.newText
|
|
4188
|
+
}
|
|
3901
4189
|
} : {}
|
|
3902
4190
|
};
|
|
3903
4191
|
try {
|
|
@@ -3916,7 +4204,8 @@ var MessageDeliverySource = class {
|
|
|
3916
4204
|
clientMsgId,
|
|
3917
4205
|
replyRef: replyTo,
|
|
3918
4206
|
envelopeType: decoded.type ?? "text",
|
|
3919
|
-
reaction: isReaction ? decoded.reaction : null
|
|
4207
|
+
reaction: isReaction ? decoded.reaction : null,
|
|
4208
|
+
edit: isEdit ? decoded.edit : null
|
|
3920
4209
|
});
|
|
3921
4210
|
return true;
|
|
3922
4211
|
}
|
|
@@ -5924,6 +6213,10 @@ var MessagingCoordinator = class {
|
|
|
5924
6213
|
const r = await this.resolve();
|
|
5925
6214
|
return r.groups.sendReaction(group, args);
|
|
5926
6215
|
}
|
|
6216
|
+
async sendEdit(group, args) {
|
|
6217
|
+
const r = await this.resolve();
|
|
6218
|
+
return r.groups.sendEdit(group, args);
|
|
6219
|
+
}
|
|
5927
6220
|
async history(group, limit, before) {
|
|
5928
6221
|
const r = await this.resolve();
|
|
5929
6222
|
const rows = await r.messageStore.history(group.rfcGroupId, limit, before);
|
|
@@ -6020,6 +6313,32 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
6020
6313
|
eventClientMsgId: s.clientMsgId ?? `${s.id}`
|
|
6021
6314
|
});
|
|
6022
6315
|
}
|
|
6316
|
+
const editFold = new EditFold();
|
|
6317
|
+
const authorByClientMsgId = /* @__PURE__ */ new Map();
|
|
6318
|
+
for (const s of rows) {
|
|
6319
|
+
if (s.envelopeType === "reaction" || s.envelopeType === "edit") continue;
|
|
6320
|
+
const cid = s.clientMsgId ?? "";
|
|
6321
|
+
if (!cid) continue;
|
|
6322
|
+
const author = s.direction === "outgoing" ? selfUserId : resolveActor?.(s.senderDeviceId ?? null);
|
|
6323
|
+
if (author != null) authorByClientMsgId.set(cid, author);
|
|
6324
|
+
}
|
|
6325
|
+
const authorOfTarget = (cid) => authorByClientMsgId.get(cid) ?? null;
|
|
6326
|
+
for (const s of rows) {
|
|
6327
|
+
if (s.envelopeType !== "edit" || !s.edit) continue;
|
|
6328
|
+
const editor = s.direction === "outgoing" ? selfUserId : resolveActor?.(s.senderDeviceId ?? null) ?? null;
|
|
6329
|
+
editFold.ingest(
|
|
6330
|
+
{
|
|
6331
|
+
targetClientMsgId: s.edit.targetClientMsgId,
|
|
6332
|
+
editorUserId: editor,
|
|
6333
|
+
newText: s.edit.newText,
|
|
6334
|
+
epoch: s.epoch,
|
|
6335
|
+
serverSeq: s.serverSeq,
|
|
6336
|
+
eventClientMsgId: s.clientMsgId ?? `${s.id}`
|
|
6337
|
+
},
|
|
6338
|
+
authorOfTarget
|
|
6339
|
+
);
|
|
6340
|
+
}
|
|
6341
|
+
editFold.reevaluateHeld(authorOfTarget);
|
|
6023
6342
|
const lookup = /* @__PURE__ */ new Map();
|
|
6024
6343
|
for (const s of rows) {
|
|
6025
6344
|
if (s.envelopeType === "reaction") continue;
|
|
@@ -6031,7 +6350,7 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
6031
6350
|
}
|
|
6032
6351
|
const out = [];
|
|
6033
6352
|
for (const s of rows) {
|
|
6034
|
-
if (s.envelopeType === "reaction") continue;
|
|
6353
|
+
if (s.envelopeType === "reaction" || s.envelopeType === "edit") continue;
|
|
6035
6354
|
const clientMsgId = s.clientMsgId ?? "";
|
|
6036
6355
|
let replyTo = null;
|
|
6037
6356
|
if (s.replyTo) {
|
|
@@ -6047,17 +6366,20 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
6047
6366
|
};
|
|
6048
6367
|
replyTo = resolveReply(ref, (id) => lookup.get(id) ?? null);
|
|
6049
6368
|
}
|
|
6369
|
+
const editText = clientMsgId ? editFold.text(clientMsgId) : null;
|
|
6370
|
+
const edited = clientMsgId ? editFold.isEdited(clientMsgId) : false;
|
|
6050
6371
|
out.push({
|
|
6051
6372
|
id: `${displayId}#${s.serverSeq}`,
|
|
6052
6373
|
kind: s.text != null ? "text" : "system",
|
|
6053
6374
|
direction: s.direction,
|
|
6054
6375
|
senderUserId: s.direction === "outgoing" ? selfUserId : null,
|
|
6055
|
-
text: s.text,
|
|
6376
|
+
text: editText ?? s.text,
|
|
6056
6377
|
serverSeq: s.serverSeq,
|
|
6057
6378
|
sentAt: new Date(s.at),
|
|
6058
6379
|
clientMsgId,
|
|
6059
6380
|
replyTo,
|
|
6060
|
-
reactions: clientMsgId ? fold.tally(clientMsgId) : {}
|
|
6381
|
+
reactions: clientMsgId ? fold.tally(clientMsgId) : {},
|
|
6382
|
+
edited
|
|
6061
6383
|
});
|
|
6062
6384
|
}
|
|
6063
6385
|
return out;
|
|
@@ -6795,7 +7117,7 @@ function defaultSessionStorage(key) {
|
|
|
6795
7117
|
}
|
|
6796
7118
|
|
|
6797
7119
|
// src/version.ts
|
|
6798
|
-
var VERSION = "1.
|
|
7120
|
+
var VERSION = "1.3.0";
|
|
6799
7121
|
|
|
6800
7122
|
// src/runtime.ts
|
|
6801
7123
|
function buildRuntime(config) {
|