@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
|
@@ -2591,6 +2591,88 @@ var PalbeFlags = class {
|
|
|
2591
2591
|
}
|
|
2592
2592
|
};
|
|
2593
2593
|
|
|
2594
|
+
// src/messaging/edit-fold.ts
|
|
2595
|
+
function orderLt(aEpoch, aSeq, bEpoch, bSeq) {
|
|
2596
|
+
if (aEpoch !== bEpoch) return aEpoch < bEpoch;
|
|
2597
|
+
return aSeq < bSeq;
|
|
2598
|
+
}
|
|
2599
|
+
function orderEq(aEpoch, aSeq, bEpoch, bSeq) {
|
|
2600
|
+
return aEpoch === bEpoch && aSeq === bSeq;
|
|
2601
|
+
}
|
|
2602
|
+
var EditFold = class {
|
|
2603
|
+
// target → winning edit state
|
|
2604
|
+
states = /* @__PURE__ */ new Map();
|
|
2605
|
+
// dedup of real wire events that reached (and were resolvable enough to evaluate at) the fold
|
|
2606
|
+
seenEvents = /* @__PURE__ */ new Set();
|
|
2607
|
+
// events parked because target/author or sender was unresolved at ingest time
|
|
2608
|
+
held = [];
|
|
2609
|
+
// targets that have had ≥1 valid edit applied (write-once)
|
|
2610
|
+
editedTargets = /* @__PURE__ */ new Set();
|
|
2611
|
+
/**
|
|
2612
|
+
* Ingest one edit. `authorOfTarget` resolves the target message's AUTHOR userId
|
|
2613
|
+
* (null = target unknown/dangling → HOLD).
|
|
2614
|
+
*/
|
|
2615
|
+
ingest(e, authorOfTarget) {
|
|
2616
|
+
const author = authorOfTarget(e.targetClientMsgId);
|
|
2617
|
+
if (author === null) {
|
|
2618
|
+
this.holdIfNew(e);
|
|
2619
|
+
return;
|
|
2620
|
+
}
|
|
2621
|
+
if (e.editorUserId === null) {
|
|
2622
|
+
this.holdIfNew(e);
|
|
2623
|
+
return;
|
|
2624
|
+
}
|
|
2625
|
+
if (e.editorUserId !== author) return;
|
|
2626
|
+
if (this.seenEvents.has(e.eventClientMsgId)) return;
|
|
2627
|
+
this.seenEvents.add(e.eventClientMsgId);
|
|
2628
|
+
const prev = this.states.get(e.targetClientMsgId);
|
|
2629
|
+
if (prev !== void 0) {
|
|
2630
|
+
if (orderLt(e.epoch, e.serverSeq, prev.orderEpoch, prev.orderSeq)) return;
|
|
2631
|
+
if (orderEq(e.epoch, e.serverSeq, prev.orderEpoch, prev.orderSeq) && e.eventClientMsgId <= prev.lastEventId) {
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
this.states.set(e.targetClientMsgId, {
|
|
2636
|
+
orderEpoch: e.epoch,
|
|
2637
|
+
orderSeq: e.serverSeq,
|
|
2638
|
+
lastEventId: e.eventClientMsgId,
|
|
2639
|
+
text: e.newText
|
|
2640
|
+
});
|
|
2641
|
+
this.editedTargets.add(e.targetClientMsgId);
|
|
2642
|
+
}
|
|
2643
|
+
/**
|
|
2644
|
+
* Park an event for later re-attempt, deduping held re-deliveries by
|
|
2645
|
+
* eventClientMsgId so a repeatedly-delivered unresolvable edit is held exactly
|
|
2646
|
+
* once (and never double-applies when it finally resolves on reevaluate).
|
|
2647
|
+
*/
|
|
2648
|
+
holdIfNew(e) {
|
|
2649
|
+
if (this.held.some((h) => h.eventClientMsgId === e.eventClientMsgId)) return;
|
|
2650
|
+
this.held.push(e);
|
|
2651
|
+
}
|
|
2652
|
+
/** The winning edit text for a target, or null if no valid edit has applied. */
|
|
2653
|
+
text(targetClientMsgId) {
|
|
2654
|
+
return this.states.get(targetClientMsgId)?.text ?? null;
|
|
2655
|
+
}
|
|
2656
|
+
/** Write-once: true once any valid edit applied to the target. */
|
|
2657
|
+
isEdited(targetClientMsgId) {
|
|
2658
|
+
return this.editedTargets.has(targetClientMsgId);
|
|
2659
|
+
}
|
|
2660
|
+
/**
|
|
2661
|
+
* Re-run HELD edits when the roster/target newly resolves (call on member/roster
|
|
2662
|
+
* change and when a target message arrives). Clears `held` and re-ingests each
|
|
2663
|
+
* event with the fresh `authorOfTarget` — events that still don't resolve are
|
|
2664
|
+
* simply re-held; events that now resolve fold via the normal LWW path.
|
|
2665
|
+
* Idempotent: re-ingest is deduped by `seenEvents` (applied events) and by
|
|
2666
|
+
* `holdIfNew` (still-held events), so reevaluating repeatedly can neither
|
|
2667
|
+
* double-apply nor lose an edit.
|
|
2668
|
+
*/
|
|
2669
|
+
reevaluateHeld(authorOfTarget) {
|
|
2670
|
+
const pending = this.held;
|
|
2671
|
+
this.held = [];
|
|
2672
|
+
for (const e of pending) this.ingest(e, authorOfTarget);
|
|
2673
|
+
}
|
|
2674
|
+
};
|
|
2675
|
+
|
|
2594
2676
|
// src/messaging/util.ts
|
|
2595
2677
|
function toBase64(bytes) {
|
|
2596
2678
|
if (typeof Buffer !== "undefined") {
|
|
@@ -2730,6 +2812,17 @@ async function listDevices(rt, userId) {
|
|
|
2730
2812
|
}
|
|
2731
2813
|
|
|
2732
2814
|
// src/messaging/group-messaging.ts
|
|
2815
|
+
function encodeEdit(args) {
|
|
2816
|
+
return encodeUtf8(
|
|
2817
|
+
JSON.stringify({
|
|
2818
|
+
v: 1,
|
|
2819
|
+
type: "edit",
|
|
2820
|
+
client_msg_id: args.clientMsgId,
|
|
2821
|
+
target_client_msg_id: args.targetClientMsgId,
|
|
2822
|
+
new_text: args.newText
|
|
2823
|
+
})
|
|
2824
|
+
);
|
|
2825
|
+
}
|
|
2733
2826
|
function encodeReaction(args) {
|
|
2734
2827
|
return encodeUtf8(
|
|
2735
2828
|
JSON.stringify({
|
|
@@ -2769,6 +2862,18 @@ function decodeEnvelope(bytes) {
|
|
|
2769
2862
|
}
|
|
2770
2863
|
};
|
|
2771
2864
|
}
|
|
2865
|
+
if (typeof o === "object" && o !== null && o.type === "edit") {
|
|
2866
|
+
return {
|
|
2867
|
+
type: "edit",
|
|
2868
|
+
text: null,
|
|
2869
|
+
clientMsgId: o.client_msg_id ?? "",
|
|
2870
|
+
replyTo: null,
|
|
2871
|
+
edit: {
|
|
2872
|
+
targetClientMsgId: o.target_client_msg_id ?? "",
|
|
2873
|
+
newText: o.new_text ?? ""
|
|
2874
|
+
}
|
|
2875
|
+
};
|
|
2876
|
+
}
|
|
2772
2877
|
if (typeof o === "object" && o !== null && o.type === "text" && typeof o.v === "number") {
|
|
2773
2878
|
return {
|
|
2774
2879
|
type: "text",
|
|
@@ -3083,6 +3188,54 @@ var GroupMessaging = class {
|
|
|
3083
3188
|
clientMsgId: args.clientMsgId
|
|
3084
3189
|
};
|
|
3085
3190
|
}
|
|
3191
|
+
/** Send an edit (edit-by-supersession on a target message). Encrypts a
|
|
3192
|
+
* `type:'edit'` envelope at the current epoch and sends through the SAME MLS
|
|
3193
|
+
* application path as `sendText` (the server stays blind — an edit is just
|
|
3194
|
+
* another application message). Persists the outgoing edit row so it re-folds
|
|
3195
|
+
* onto its target's text after a reload (the own-send half of the reload
|
|
3196
|
+
* parity). NEVER rebases (epoch-bound like any application message). */
|
|
3197
|
+
async sendEdit(group, args) {
|
|
3198
|
+
const plaintext = encodeEdit({
|
|
3199
|
+
clientMsgId: args.clientMsgId,
|
|
3200
|
+
targetClientMsgId: args.targetClientMsgId,
|
|
3201
|
+
newText: args.newText
|
|
3202
|
+
});
|
|
3203
|
+
const ct = await this.engine.encryptApplication(fromBase64(group.rfcGroupId), plaintext);
|
|
3204
|
+
const body = {
|
|
3205
|
+
ciphertext_b64: toBase64(ct),
|
|
3206
|
+
client_idem_key: randomId()
|
|
3207
|
+
};
|
|
3208
|
+
const wire = await palbeRequest(
|
|
3209
|
+
this.rt,
|
|
3210
|
+
"POST",
|
|
3211
|
+
MessagingPaths.groupMessages(group.displayId),
|
|
3212
|
+
{ body }
|
|
3213
|
+
);
|
|
3214
|
+
const stored = {
|
|
3215
|
+
id: `${group.rfcGroupId}#${wire.server_seq}`,
|
|
3216
|
+
direction: "outgoing",
|
|
3217
|
+
text: null,
|
|
3218
|
+
senderDeviceId: this.selfDeviceId,
|
|
3219
|
+
epoch: wire.epoch,
|
|
3220
|
+
serverSeq: wire.server_seq,
|
|
3221
|
+
at: Date.now(),
|
|
3222
|
+
clientMsgId: args.clientMsgId,
|
|
3223
|
+
replyTo: null,
|
|
3224
|
+
envelopeType: "edit",
|
|
3225
|
+
edit: {
|
|
3226
|
+
targetClientMsgId: args.targetClientMsgId,
|
|
3227
|
+
newText: args.newText
|
|
3228
|
+
}
|
|
3229
|
+
};
|
|
3230
|
+
try {
|
|
3231
|
+
await this.messageStore.append(group.rfcGroupId, stored);
|
|
3232
|
+
} catch {
|
|
3233
|
+
}
|
|
3234
|
+
return {
|
|
3235
|
+
receipt: { serverSeq: wire.server_seq, epoch: wire.epoch },
|
|
3236
|
+
clientMsgId: args.clientMsgId
|
|
3237
|
+
};
|
|
3238
|
+
}
|
|
3086
3239
|
// ── The rebase loop ──
|
|
3087
3240
|
async commitWithRebase(rfcGroupId, build) {
|
|
3088
3241
|
const gidBytes = fromBase64(rfcGroupId);
|
|
@@ -3219,6 +3372,14 @@ var Chat = class {
|
|
|
3219
3372
|
byClientMsgId = /* @__PURE__ */ new Map();
|
|
3220
3373
|
/** The single authoritative reaction fold for this chat (live + own-send + history). */
|
|
3221
3374
|
reactionFold = new ReactionFold();
|
|
3375
|
+
/** The single authoritative edit fold for this chat (live + own-send + history). */
|
|
3376
|
+
editFold = new EditFold();
|
|
3377
|
+
/** Per-target BASE (original) text, so the rendered text is `edit ?? original`.
|
|
3378
|
+
* Seeded once per target (write-once base) so a 2nd own edit can't corrupt it. */
|
|
3379
|
+
originalTextByClientMsgId = /* @__PURE__ */ new Map();
|
|
3380
|
+
/** Per-target AUTHOR userId — the EditFold author-gate input (filled at bubble
|
|
3381
|
+
* projection time from senderUserId; '' = resolved-but-unknown peer). */
|
|
3382
|
+
authorByClientMsgId = /* @__PURE__ */ new Map();
|
|
3222
3383
|
loadedEarliestSeq = null;
|
|
3223
3384
|
historyLoaded = false;
|
|
3224
3385
|
wired = false;
|
|
@@ -3319,13 +3480,17 @@ var Chat = class {
|
|
|
3319
3480
|
senderUserId: m.senderUserId ?? ""
|
|
3320
3481
|
});
|
|
3321
3482
|
}
|
|
3483
|
+
if (m.clientMsgId) {
|
|
3484
|
+
this.seedEditBase(m.clientMsgId, m.text, m.senderUserId ?? "");
|
|
3485
|
+
}
|
|
3322
3486
|
}
|
|
3487
|
+
this.editFold.reevaluateHeld(this.authorOfTarget);
|
|
3323
3488
|
for (const m of incoming) {
|
|
3324
3489
|
if (m.serverSeq <= 0) continue;
|
|
3325
3490
|
const key = this.internalKey(m.serverSeq);
|
|
3326
3491
|
if (this.seenKeys.has(key)) continue;
|
|
3327
3492
|
this.seenKeys.add(key);
|
|
3328
|
-
this.messageList.push(this.applyReactionTally(m));
|
|
3493
|
+
this.messageList.push(this.applyEditOverlay(this.applyReactionTally(m)));
|
|
3329
3494
|
changed = true;
|
|
3330
3495
|
this.loadedEarliestSeq = Math.min(this.loadedEarliestSeq ?? m.serverSeq, m.serverSeq);
|
|
3331
3496
|
}
|
|
@@ -3366,6 +3531,22 @@ var Chat = class {
|
|
|
3366
3531
|
}
|
|
3367
3532
|
return;
|
|
3368
3533
|
}
|
|
3534
|
+
if (incoming.envelopeType === "edit" && incoming.edit) {
|
|
3535
|
+
const editorUserId = direction === "outgoing" ? this.backend.selfUserId : senderUser;
|
|
3536
|
+
this.editFold.ingest(
|
|
3537
|
+
{
|
|
3538
|
+
targetClientMsgId: incoming.edit.targetClientMsgId,
|
|
3539
|
+
editorUserId,
|
|
3540
|
+
newText: incoming.edit.newText,
|
|
3541
|
+
epoch: incoming.epoch,
|
|
3542
|
+
serverSeq: incoming.serverSeq,
|
|
3543
|
+
eventClientMsgId: incoming.clientMsgId
|
|
3544
|
+
},
|
|
3545
|
+
this.authorOfTarget
|
|
3546
|
+
);
|
|
3547
|
+
this.recomputeEdit(incoming.edit.targetClientMsgId);
|
|
3548
|
+
return;
|
|
3549
|
+
}
|
|
3369
3550
|
const incomingClientMsgId = incoming.clientMsgId;
|
|
3370
3551
|
const incomingReplyRef = incoming.replyRef;
|
|
3371
3552
|
let resolvedReplyTo = null;
|
|
@@ -3384,7 +3565,9 @@ var Chat = class {
|
|
|
3384
3565
|
replyTo: resolvedReplyTo,
|
|
3385
3566
|
// Attach any tally already folded for this message (a reaction that arrived
|
|
3386
3567
|
// BEFORE its target — the dangling case — renders the moment the target lands).
|
|
3387
|
-
reactions: incomingClientMsgId ? this.reactionFold.tally(incomingClientMsgId) : {}
|
|
3568
|
+
reactions: incomingClientMsgId ? this.reactionFold.tally(incomingClientMsgId) : {},
|
|
3569
|
+
// Default false; applyEditOverlay below folds any edit that arrived first.
|
|
3570
|
+
edited: false
|
|
3388
3571
|
};
|
|
3389
3572
|
if (incomingClientMsgId && incoming.text !== null) {
|
|
3390
3573
|
this.byClientMsgId.set(incomingClientMsgId, {
|
|
@@ -3392,7 +3575,11 @@ var Chat = class {
|
|
|
3392
3575
|
senderUserId: senderUser ?? ""
|
|
3393
3576
|
});
|
|
3394
3577
|
}
|
|
3395
|
-
|
|
3578
|
+
if (incomingClientMsgId) {
|
|
3579
|
+
this.seedEditBase(incomingClientMsgId, incoming.text, senderUser);
|
|
3580
|
+
this.editFold.reevaluateHeld(this.authorOfTarget);
|
|
3581
|
+
}
|
|
3582
|
+
this.messageList.push(this.applyEditOverlay(msg));
|
|
3396
3583
|
this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
|
|
3397
3584
|
this.loadedEarliestSeq = Math.min(
|
|
3398
3585
|
this.loadedEarliestSeq ?? incoming.serverSeq,
|
|
@@ -3400,6 +3587,19 @@ var Chat = class {
|
|
|
3400
3587
|
);
|
|
3401
3588
|
this.emit();
|
|
3402
3589
|
}
|
|
3590
|
+
/** The EditFold author-gate input: the target message's resolved author userId
|
|
3591
|
+
* (null = target unknown/dangling → the fold HOLDs). Captured as a bound arrow
|
|
3592
|
+
* so it can be passed to the pure EditFold. */
|
|
3593
|
+
authorOfTarget = (targetClientMsgId) => this.authorByClientMsgId.get(targetClientMsgId) ?? null;
|
|
3594
|
+
/** Seed the per-target base text + author for the edit fold. Base is write-once
|
|
3595
|
+
* (a later own/peer edit must not overwrite the original we render against). The
|
|
3596
|
+
* author is (re)recorded whenever a non-empty resolution is available. */
|
|
3597
|
+
seedEditBase(clientMsgId, text, author) {
|
|
3598
|
+
if (!this.originalTextByClientMsgId.has(clientMsgId)) {
|
|
3599
|
+
this.originalTextByClientMsgId.set(clientMsgId, text);
|
|
3600
|
+
}
|
|
3601
|
+
if (author !== null) this.authorByClientMsgId.set(clientMsgId, author);
|
|
3602
|
+
}
|
|
3403
3603
|
/**
|
|
3404
3604
|
* Rebuild the target message's `reactions` from the authoritative fold and
|
|
3405
3605
|
* re-emit. No-op when the target isn't present yet (its tally is attached the
|
|
@@ -3429,6 +3629,46 @@ var Chat = class {
|
|
|
3429
3629
|
if (sameReactions(m.reactions, tally)) return m;
|
|
3430
3630
|
return { ...m, reactions: tally };
|
|
3431
3631
|
}
|
|
3632
|
+
/**
|
|
3633
|
+
* Rebuild the target message's rendered `text` + `edited` flag from the
|
|
3634
|
+
* authoritative edit fold and re-emit, PRESERVING `.reactions` and `.replyTo`
|
|
3635
|
+
* (the reaction-polish lesson — never clobber). text = `editFold.text(cid) ??
|
|
3636
|
+
* base`; base is the seeded original so a forged/ignored edit leaves it intact.
|
|
3637
|
+
* No-op when the target isn't present yet (the fold already recorded it; the
|
|
3638
|
+
* overlay applies the moment the target lands) or when unchanged.
|
|
3639
|
+
*/
|
|
3640
|
+
recomputeEdit(targetClientMsgId) {
|
|
3641
|
+
if (!targetClientMsgId) return;
|
|
3642
|
+
const editText = this.editFold.text(targetClientMsgId);
|
|
3643
|
+
const foldEdited = this.editFold.isEdited(targetClientMsgId);
|
|
3644
|
+
let changed = false;
|
|
3645
|
+
this.messageList = this.messageList.map((m) => {
|
|
3646
|
+
if (m.clientMsgId !== targetClientMsgId) return m;
|
|
3647
|
+
const base = this.originalTextByClientMsgId.has(targetClientMsgId) ? this.originalTextByClientMsgId.get(targetClientMsgId) ?? null : m.text;
|
|
3648
|
+
const text = editText ?? base;
|
|
3649
|
+
const edited = foldEdited || m.edited;
|
|
3650
|
+
if (m.text === text && m.edited === edited) return m;
|
|
3651
|
+
changed = true;
|
|
3652
|
+
return { ...m, text, edited };
|
|
3653
|
+
});
|
|
3654
|
+
if (changed) this.emit();
|
|
3655
|
+
}
|
|
3656
|
+
/**
|
|
3657
|
+
* Overlay the authoritative edit fold's winning text + flag onto a message as it
|
|
3658
|
+
* is appended/merged. The fold WINS when it has an edit for this target;
|
|
3659
|
+
* otherwise the upstream `text`/`edited` (e.g. the coordinator's page-local
|
|
3660
|
+
* history fold) is preserved. PRESERVES reactions + replyTo.
|
|
3661
|
+
*/
|
|
3662
|
+
applyEditOverlay(m) {
|
|
3663
|
+
if (!m.clientMsgId) return m;
|
|
3664
|
+
const editText = this.editFold.text(m.clientMsgId);
|
|
3665
|
+
const foldEdited = this.editFold.isEdited(m.clientMsgId);
|
|
3666
|
+
if (editText === null && !foldEdited) return m;
|
|
3667
|
+
const text = editText ?? m.text;
|
|
3668
|
+
const edited = foldEdited || m.edited;
|
|
3669
|
+
if (m.text === text && m.edited === edited) return m;
|
|
3670
|
+
return { ...m, text, edited };
|
|
3671
|
+
}
|
|
3432
3672
|
/** @internal — called by the backend's conv subscription. */
|
|
3433
3673
|
applyConv(event, payload) {
|
|
3434
3674
|
const userId = typeof payload.user_id === "string" ? payload.user_id : null;
|
|
@@ -3483,6 +3723,8 @@ var Chat = class {
|
|
|
3483
3723
|
this.memberCache = m;
|
|
3484
3724
|
this.emit();
|
|
3485
3725
|
}
|
|
3726
|
+
this.editFold.reevaluateHeld(this.authorOfTarget);
|
|
3727
|
+
for (const cid of this.originalTextByClientMsgId.keys()) this.recomputeEdit(cid);
|
|
3486
3728
|
}
|
|
3487
3729
|
seedMembersFromGroup(group) {
|
|
3488
3730
|
const seed = [
|
|
@@ -3561,6 +3803,7 @@ var Chat = class {
|
|
|
3561
3803
|
this.seenKeys.add(key);
|
|
3562
3804
|
if (clientMsgId) {
|
|
3563
3805
|
this.byClientMsgId.set(clientMsgId, { text, senderUserId: this.backend.selfUserId });
|
|
3806
|
+
this.seedEditBase(clientMsgId, text, this.backend.selfUserId);
|
|
3564
3807
|
}
|
|
3565
3808
|
this.messageList.push({
|
|
3566
3809
|
id: this.publicId(receipt.serverSeq),
|
|
@@ -3574,7 +3817,9 @@ var Chat = class {
|
|
|
3574
3817
|
replyTo: resolvedReplyTo,
|
|
3575
3818
|
// Attach any tally already folded for this own-sent message (rare, but keeps
|
|
3576
3819
|
// the dangling-target invariant uniform across every append path).
|
|
3577
|
-
reactions: clientMsgId ? this.reactionFold.tally(clientMsgId) : {}
|
|
3820
|
+
reactions: clientMsgId ? this.reactionFold.tally(clientMsgId) : {},
|
|
3821
|
+
// Own-sent edits fold via edit() after the fact; new sends start unedited.
|
|
3822
|
+
edited: false
|
|
3578
3823
|
});
|
|
3579
3824
|
this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
|
|
3580
3825
|
this.emit();
|
|
@@ -3655,6 +3900,37 @@ var Chat = class {
|
|
|
3655
3900
|
});
|
|
3656
3901
|
this.recomputeReactions(message.clientMsgId);
|
|
3657
3902
|
}
|
|
3903
|
+
// ── Edit ──
|
|
3904
|
+
/** Edit an own text message (edit-by-supersession). No-op if the message isn't
|
|
3905
|
+
* editable (empty clientMsgId, or not a `text` kind). The edit folds locally
|
|
3906
|
+
* with the server receipt's `(epoch, serverSeq)` so the target's text updates
|
|
3907
|
+
* instantly; the durable echo on the next pump is a fold no-op (dedup on the
|
|
3908
|
+
* SAME wire clientMsgId). Never appends a bubble; PRESERVES the target's
|
|
3909
|
+
* reactions + reply context. Only the original author's edits count — for an own
|
|
3910
|
+
* message self IS the author, so the author-gate passes. */
|
|
3911
|
+
async edit(message, newText) {
|
|
3912
|
+
if (!message.clientMsgId || message.kind !== "text") return;
|
|
3913
|
+
const group = await this.materializeIfNeeded();
|
|
3914
|
+
const clientMsgId = mintClientMsgId();
|
|
3915
|
+
const { receipt } = await this.backend.sendEdit(group, {
|
|
3916
|
+
clientMsgId,
|
|
3917
|
+
targetClientMsgId: message.clientMsgId,
|
|
3918
|
+
newText
|
|
3919
|
+
});
|
|
3920
|
+
this.seedEditBase(message.clientMsgId, message.text, this.backend.selfUserId);
|
|
3921
|
+
this.editFold.ingest(
|
|
3922
|
+
{
|
|
3923
|
+
targetClientMsgId: message.clientMsgId,
|
|
3924
|
+
editorUserId: this.backend.selfUserId,
|
|
3925
|
+
newText,
|
|
3926
|
+
epoch: receipt.epoch,
|
|
3927
|
+
serverSeq: receipt.serverSeq,
|
|
3928
|
+
eventClientMsgId: clientMsgId
|
|
3929
|
+
},
|
|
3930
|
+
this.authorOfTarget
|
|
3931
|
+
);
|
|
3932
|
+
this.recomputeEdit(message.clientMsgId);
|
|
3933
|
+
}
|
|
3658
3934
|
};
|
|
3659
3935
|
function sameReactions(a, b) {
|
|
3660
3936
|
const ak = Object.keys(a);
|
|
@@ -3834,6 +4110,7 @@ var MessageDeliverySource = class {
|
|
|
3834
4110
|
const decoded = decodeEnvelope(received.data);
|
|
3835
4111
|
const { text, clientMsgId, replyTo } = decoded;
|
|
3836
4112
|
const isReaction = decoded.type === "reaction" && decoded.reaction != null;
|
|
4113
|
+
const isEdit = decoded.type === "edit" && decoded.edit != null;
|
|
3837
4114
|
const senderDeviceId = row.sender_device_id ?? decodeSenderDeviceId(received.sender);
|
|
3838
4115
|
const stored = {
|
|
3839
4116
|
id: `${group.rfcGroupId}#${row.server_seq}`,
|
|
@@ -3861,6 +4138,17 @@ var MessageDeliverySource = class {
|
|
|
3861
4138
|
emoji: decoded.reaction.emoji,
|
|
3862
4139
|
op: decoded.reaction.op
|
|
3863
4140
|
}
|
|
4141
|
+
} : {},
|
|
4142
|
+
// Thread the edit discriminator + new text through the persisted row so an
|
|
4143
|
+
// edit folded LIVE re-folds onto its target after a reload (the reload-parity
|
|
4144
|
+
// boundary — mirrors iOS T3). Omitted for non-edits → old rows hydrate as
|
|
4145
|
+
// `'text'`/no-edit (backward-compat).
|
|
4146
|
+
...isEdit && decoded.edit ? {
|
|
4147
|
+
envelopeType: "edit",
|
|
4148
|
+
edit: {
|
|
4149
|
+
targetClientMsgId: decoded.edit.targetClientMsgId,
|
|
4150
|
+
newText: decoded.edit.newText
|
|
4151
|
+
}
|
|
3864
4152
|
} : {}
|
|
3865
4153
|
};
|
|
3866
4154
|
try {
|
|
@@ -3879,7 +4167,8 @@ var MessageDeliverySource = class {
|
|
|
3879
4167
|
clientMsgId,
|
|
3880
4168
|
replyRef: replyTo,
|
|
3881
4169
|
envelopeType: decoded.type ?? "text",
|
|
3882
|
-
reaction: isReaction ? decoded.reaction : null
|
|
4170
|
+
reaction: isReaction ? decoded.reaction : null,
|
|
4171
|
+
edit: isEdit ? decoded.edit : null
|
|
3883
4172
|
});
|
|
3884
4173
|
return true;
|
|
3885
4174
|
}
|
|
@@ -5886,6 +6175,10 @@ var MessagingCoordinator = class {
|
|
|
5886
6175
|
const r = await this.resolve();
|
|
5887
6176
|
return r.groups.sendReaction(group, args);
|
|
5888
6177
|
}
|
|
6178
|
+
async sendEdit(group, args) {
|
|
6179
|
+
const r = await this.resolve();
|
|
6180
|
+
return r.groups.sendEdit(group, args);
|
|
6181
|
+
}
|
|
5889
6182
|
async history(group, limit, before) {
|
|
5890
6183
|
const r = await this.resolve();
|
|
5891
6184
|
const rows = await r.messageStore.history(group.rfcGroupId, limit, before);
|
|
@@ -5982,6 +6275,32 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
5982
6275
|
eventClientMsgId: s.clientMsgId ?? `${s.id}`
|
|
5983
6276
|
});
|
|
5984
6277
|
}
|
|
6278
|
+
const editFold = new EditFold();
|
|
6279
|
+
const authorByClientMsgId = /* @__PURE__ */ new Map();
|
|
6280
|
+
for (const s of rows) {
|
|
6281
|
+
if (s.envelopeType === "reaction" || s.envelopeType === "edit") continue;
|
|
6282
|
+
const cid = s.clientMsgId ?? "";
|
|
6283
|
+
if (!cid) continue;
|
|
6284
|
+
const author = s.direction === "outgoing" ? selfUserId : resolveActor?.(s.senderDeviceId ?? null);
|
|
6285
|
+
if (author != null) authorByClientMsgId.set(cid, author);
|
|
6286
|
+
}
|
|
6287
|
+
const authorOfTarget = (cid) => authorByClientMsgId.get(cid) ?? null;
|
|
6288
|
+
for (const s of rows) {
|
|
6289
|
+
if (s.envelopeType !== "edit" || !s.edit) continue;
|
|
6290
|
+
const editor = s.direction === "outgoing" ? selfUserId : resolveActor?.(s.senderDeviceId ?? null) ?? null;
|
|
6291
|
+
editFold.ingest(
|
|
6292
|
+
{
|
|
6293
|
+
targetClientMsgId: s.edit.targetClientMsgId,
|
|
6294
|
+
editorUserId: editor,
|
|
6295
|
+
newText: s.edit.newText,
|
|
6296
|
+
epoch: s.epoch,
|
|
6297
|
+
serverSeq: s.serverSeq,
|
|
6298
|
+
eventClientMsgId: s.clientMsgId ?? `${s.id}`
|
|
6299
|
+
},
|
|
6300
|
+
authorOfTarget
|
|
6301
|
+
);
|
|
6302
|
+
}
|
|
6303
|
+
editFold.reevaluateHeld(authorOfTarget);
|
|
5985
6304
|
const lookup = /* @__PURE__ */ new Map();
|
|
5986
6305
|
for (const s of rows) {
|
|
5987
6306
|
if (s.envelopeType === "reaction") continue;
|
|
@@ -5993,7 +6312,7 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
5993
6312
|
}
|
|
5994
6313
|
const out = [];
|
|
5995
6314
|
for (const s of rows) {
|
|
5996
|
-
if (s.envelopeType === "reaction") continue;
|
|
6315
|
+
if (s.envelopeType === "reaction" || s.envelopeType === "edit") continue;
|
|
5997
6316
|
const clientMsgId = s.clientMsgId ?? "";
|
|
5998
6317
|
let replyTo = null;
|
|
5999
6318
|
if (s.replyTo) {
|
|
@@ -6009,17 +6328,20 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
6009
6328
|
};
|
|
6010
6329
|
replyTo = resolveReply(ref, (id) => lookup.get(id) ?? null);
|
|
6011
6330
|
}
|
|
6331
|
+
const editText = clientMsgId ? editFold.text(clientMsgId) : null;
|
|
6332
|
+
const edited = clientMsgId ? editFold.isEdited(clientMsgId) : false;
|
|
6012
6333
|
out.push({
|
|
6013
6334
|
id: `${displayId}#${s.serverSeq}`,
|
|
6014
6335
|
kind: s.text != null ? "text" : "system",
|
|
6015
6336
|
direction: s.direction,
|
|
6016
6337
|
senderUserId: s.direction === "outgoing" ? selfUserId : null,
|
|
6017
|
-
text: s.text,
|
|
6338
|
+
text: editText ?? s.text,
|
|
6018
6339
|
serverSeq: s.serverSeq,
|
|
6019
6340
|
sentAt: new Date(s.at),
|
|
6020
6341
|
clientMsgId,
|
|
6021
6342
|
replyTo,
|
|
6022
|
-
reactions: clientMsgId ? fold.tally(clientMsgId) : {}
|
|
6343
|
+
reactions: clientMsgId ? fold.tally(clientMsgId) : {},
|
|
6344
|
+
edited
|
|
6023
6345
|
});
|
|
6024
6346
|
}
|
|
6025
6347
|
return out;
|
|
@@ -6757,7 +7079,7 @@ function defaultSessionStorage(key) {
|
|
|
6757
7079
|
}
|
|
6758
7080
|
|
|
6759
7081
|
// src/version.ts
|
|
6760
|
-
var VERSION = "1.
|
|
7082
|
+
var VERSION = "1.3.0";
|
|
6761
7083
|
|
|
6762
7084
|
// src/runtime.ts
|
|
6763
7085
|
function buildRuntime(config) {
|
|
@@ -7117,4 +7439,4 @@ export {
|
|
|
7117
7439
|
pb,
|
|
7118
7440
|
createBoundClient
|
|
7119
7441
|
};
|
|
7120
|
-
//# sourceMappingURL=chunk-
|
|
7442
|
+
//# sourceMappingURL=chunk-UX43AB4W.js.map
|