@palbase/web 1.2.0 → 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-OZLVL7G2.js → chunk-UX43AB4W.js} +340 -12
- package/dist/chunk-UX43AB4W.js.map +1 -0
- package/dist/index.cjs +339 -11
- 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 +339 -11
- 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 +339 -11
- package/dist/next/client.cjs.map +1 -1
- package/dist/next/client.js +1 -1
- package/dist/next/index.cjs +339 -11
- 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-OZLVL7G2.js.map +0 -1
package/dist/internal.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { u as PalbeConfig,
|
|
2
|
-
export {
|
|
1
|
+
import { u as PalbeConfig, K as PalbeRuntime } from './analytics-facade-B8UATgS4.cjs';
|
|
2
|
+
export { L as buildRuntime } from './analytics-facade-B8UATgS4.cjs';
|
|
3
3
|
import { B as BackendError } from './errors-fDoNdTrJ.cjs';
|
|
4
|
-
export { P as PB, c as createBoundClient } from './pb-
|
|
4
|
+
export { P as PB, c as createBoundClient } from './pb-DivIiUGR.cjs';
|
|
5
5
|
import 'livekit-client';
|
|
6
6
|
import './storage-BPaeSG8K.cjs';
|
|
7
7
|
import './pooled-flags-Bwq4usn0.js';
|
package/dist/internal.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { u as PalbeConfig,
|
|
2
|
-
export {
|
|
1
|
+
import { u as PalbeConfig, K as PalbeRuntime } from './analytics-facade-Cq7R-VH_.js';
|
|
2
|
+
export { L as buildRuntime } from './analytics-facade-Cq7R-VH_.js';
|
|
3
3
|
import { B as BackendError } from './errors-fDoNdTrJ.js';
|
|
4
|
-
export { P as PB, c as createBoundClient } from './pb-
|
|
4
|
+
export { P as PB, c as createBoundClient } from './pb-DSJiH1uV.js';
|
|
5
5
|
import 'livekit-client';
|
|
6
6
|
import './storage-BPaeSG8K.js';
|
|
7
7
|
import './pooled-flags-Bwq4usn0.js';
|
package/dist/internal.js
CHANGED
package/dist/next/client.cjs
CHANGED
|
@@ -2413,6 +2413,88 @@ var PalbeFlags = class {
|
|
|
2413
2413
|
}
|
|
2414
2414
|
};
|
|
2415
2415
|
|
|
2416
|
+
// src/messaging/edit-fold.ts
|
|
2417
|
+
function orderLt(aEpoch, aSeq, bEpoch, bSeq) {
|
|
2418
|
+
if (aEpoch !== bEpoch) return aEpoch < bEpoch;
|
|
2419
|
+
return aSeq < bSeq;
|
|
2420
|
+
}
|
|
2421
|
+
function orderEq(aEpoch, aSeq, bEpoch, bSeq) {
|
|
2422
|
+
return aEpoch === bEpoch && aSeq === bSeq;
|
|
2423
|
+
}
|
|
2424
|
+
var EditFold = class {
|
|
2425
|
+
// target → winning edit state
|
|
2426
|
+
states = /* @__PURE__ */ new Map();
|
|
2427
|
+
// dedup of real wire events that reached (and were resolvable enough to evaluate at) the fold
|
|
2428
|
+
seenEvents = /* @__PURE__ */ new Set();
|
|
2429
|
+
// events parked because target/author or sender was unresolved at ingest time
|
|
2430
|
+
held = [];
|
|
2431
|
+
// targets that have had ≥1 valid edit applied (write-once)
|
|
2432
|
+
editedTargets = /* @__PURE__ */ new Set();
|
|
2433
|
+
/**
|
|
2434
|
+
* Ingest one edit. `authorOfTarget` resolves the target message's AUTHOR userId
|
|
2435
|
+
* (null = target unknown/dangling → HOLD).
|
|
2436
|
+
*/
|
|
2437
|
+
ingest(e, authorOfTarget) {
|
|
2438
|
+
const author = authorOfTarget(e.targetClientMsgId);
|
|
2439
|
+
if (author === null) {
|
|
2440
|
+
this.holdIfNew(e);
|
|
2441
|
+
return;
|
|
2442
|
+
}
|
|
2443
|
+
if (e.editorUserId === null) {
|
|
2444
|
+
this.holdIfNew(e);
|
|
2445
|
+
return;
|
|
2446
|
+
}
|
|
2447
|
+
if (e.editorUserId !== author) return;
|
|
2448
|
+
if (this.seenEvents.has(e.eventClientMsgId)) return;
|
|
2449
|
+
this.seenEvents.add(e.eventClientMsgId);
|
|
2450
|
+
const prev = this.states.get(e.targetClientMsgId);
|
|
2451
|
+
if (prev !== void 0) {
|
|
2452
|
+
if (orderLt(e.epoch, e.serverSeq, prev.orderEpoch, prev.orderSeq)) return;
|
|
2453
|
+
if (orderEq(e.epoch, e.serverSeq, prev.orderEpoch, prev.orderSeq) && e.eventClientMsgId <= prev.lastEventId) {
|
|
2454
|
+
return;
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
this.states.set(e.targetClientMsgId, {
|
|
2458
|
+
orderEpoch: e.epoch,
|
|
2459
|
+
orderSeq: e.serverSeq,
|
|
2460
|
+
lastEventId: e.eventClientMsgId,
|
|
2461
|
+
text: e.newText
|
|
2462
|
+
});
|
|
2463
|
+
this.editedTargets.add(e.targetClientMsgId);
|
|
2464
|
+
}
|
|
2465
|
+
/**
|
|
2466
|
+
* Park an event for later re-attempt, deduping held re-deliveries by
|
|
2467
|
+
* eventClientMsgId so a repeatedly-delivered unresolvable edit is held exactly
|
|
2468
|
+
* once (and never double-applies when it finally resolves on reevaluate).
|
|
2469
|
+
*/
|
|
2470
|
+
holdIfNew(e) {
|
|
2471
|
+
if (this.held.some((h) => h.eventClientMsgId === e.eventClientMsgId)) return;
|
|
2472
|
+
this.held.push(e);
|
|
2473
|
+
}
|
|
2474
|
+
/** The winning edit text for a target, or null if no valid edit has applied. */
|
|
2475
|
+
text(targetClientMsgId) {
|
|
2476
|
+
return this.states.get(targetClientMsgId)?.text ?? null;
|
|
2477
|
+
}
|
|
2478
|
+
/** Write-once: true once any valid edit applied to the target. */
|
|
2479
|
+
isEdited(targetClientMsgId) {
|
|
2480
|
+
return this.editedTargets.has(targetClientMsgId);
|
|
2481
|
+
}
|
|
2482
|
+
/**
|
|
2483
|
+
* Re-run HELD edits when the roster/target newly resolves (call on member/roster
|
|
2484
|
+
* change and when a target message arrives). Clears `held` and re-ingests each
|
|
2485
|
+
* event with the fresh `authorOfTarget` — events that still don't resolve are
|
|
2486
|
+
* simply re-held; events that now resolve fold via the normal LWW path.
|
|
2487
|
+
* Idempotent: re-ingest is deduped by `seenEvents` (applied events) and by
|
|
2488
|
+
* `holdIfNew` (still-held events), so reevaluating repeatedly can neither
|
|
2489
|
+
* double-apply nor lose an edit.
|
|
2490
|
+
*/
|
|
2491
|
+
reevaluateHeld(authorOfTarget) {
|
|
2492
|
+
const pending = this.held;
|
|
2493
|
+
this.held = [];
|
|
2494
|
+
for (const e of pending) this.ingest(e, authorOfTarget);
|
|
2495
|
+
}
|
|
2496
|
+
};
|
|
2497
|
+
|
|
2416
2498
|
// src/messaging/util.ts
|
|
2417
2499
|
function toBase64(bytes) {
|
|
2418
2500
|
if (typeof Buffer !== "undefined") {
|
|
@@ -2552,6 +2634,17 @@ async function listDevices(rt, userId) {
|
|
|
2552
2634
|
}
|
|
2553
2635
|
|
|
2554
2636
|
// src/messaging/group-messaging.ts
|
|
2637
|
+
function encodeEdit(args) {
|
|
2638
|
+
return encodeUtf8(
|
|
2639
|
+
JSON.stringify({
|
|
2640
|
+
v: 1,
|
|
2641
|
+
type: "edit",
|
|
2642
|
+
client_msg_id: args.clientMsgId,
|
|
2643
|
+
target_client_msg_id: args.targetClientMsgId,
|
|
2644
|
+
new_text: args.newText
|
|
2645
|
+
})
|
|
2646
|
+
);
|
|
2647
|
+
}
|
|
2555
2648
|
function encodeReaction(args) {
|
|
2556
2649
|
return encodeUtf8(
|
|
2557
2650
|
JSON.stringify({
|
|
@@ -2591,6 +2684,18 @@ function decodeEnvelope(bytes) {
|
|
|
2591
2684
|
}
|
|
2592
2685
|
};
|
|
2593
2686
|
}
|
|
2687
|
+
if (typeof o === "object" && o !== null && o.type === "edit") {
|
|
2688
|
+
return {
|
|
2689
|
+
type: "edit",
|
|
2690
|
+
text: null,
|
|
2691
|
+
clientMsgId: o.client_msg_id ?? "",
|
|
2692
|
+
replyTo: null,
|
|
2693
|
+
edit: {
|
|
2694
|
+
targetClientMsgId: o.target_client_msg_id ?? "",
|
|
2695
|
+
newText: o.new_text ?? ""
|
|
2696
|
+
}
|
|
2697
|
+
};
|
|
2698
|
+
}
|
|
2594
2699
|
if (typeof o === "object" && o !== null && o.type === "text" && typeof o.v === "number") {
|
|
2595
2700
|
return {
|
|
2596
2701
|
type: "text",
|
|
@@ -2905,6 +3010,54 @@ var GroupMessaging = class {
|
|
|
2905
3010
|
clientMsgId: args.clientMsgId
|
|
2906
3011
|
};
|
|
2907
3012
|
}
|
|
3013
|
+
/** Send an edit (edit-by-supersession on a target message). Encrypts a
|
|
3014
|
+
* `type:'edit'` envelope at the current epoch and sends through the SAME MLS
|
|
3015
|
+
* application path as `sendText` (the server stays blind — an edit is just
|
|
3016
|
+
* another application message). Persists the outgoing edit row so it re-folds
|
|
3017
|
+
* onto its target's text after a reload (the own-send half of the reload
|
|
3018
|
+
* parity). NEVER rebases (epoch-bound like any application message). */
|
|
3019
|
+
async sendEdit(group, args) {
|
|
3020
|
+
const plaintext = encodeEdit({
|
|
3021
|
+
clientMsgId: args.clientMsgId,
|
|
3022
|
+
targetClientMsgId: args.targetClientMsgId,
|
|
3023
|
+
newText: args.newText
|
|
3024
|
+
});
|
|
3025
|
+
const ct = await this.engine.encryptApplication(fromBase64(group.rfcGroupId), plaintext);
|
|
3026
|
+
const body = {
|
|
3027
|
+
ciphertext_b64: toBase64(ct),
|
|
3028
|
+
client_idem_key: randomId()
|
|
3029
|
+
};
|
|
3030
|
+
const wire = await palbeRequest(
|
|
3031
|
+
this.rt,
|
|
3032
|
+
"POST",
|
|
3033
|
+
MessagingPaths.groupMessages(group.displayId),
|
|
3034
|
+
{ body }
|
|
3035
|
+
);
|
|
3036
|
+
const stored = {
|
|
3037
|
+
id: `${group.rfcGroupId}#${wire.server_seq}`,
|
|
3038
|
+
direction: "outgoing",
|
|
3039
|
+
text: null,
|
|
3040
|
+
senderDeviceId: this.selfDeviceId,
|
|
3041
|
+
epoch: wire.epoch,
|
|
3042
|
+
serverSeq: wire.server_seq,
|
|
3043
|
+
at: Date.now(),
|
|
3044
|
+
clientMsgId: args.clientMsgId,
|
|
3045
|
+
replyTo: null,
|
|
3046
|
+
envelopeType: "edit",
|
|
3047
|
+
edit: {
|
|
3048
|
+
targetClientMsgId: args.targetClientMsgId,
|
|
3049
|
+
newText: args.newText
|
|
3050
|
+
}
|
|
3051
|
+
};
|
|
3052
|
+
try {
|
|
3053
|
+
await this.messageStore.append(group.rfcGroupId, stored);
|
|
3054
|
+
} catch {
|
|
3055
|
+
}
|
|
3056
|
+
return {
|
|
3057
|
+
receipt: { serverSeq: wire.server_seq, epoch: wire.epoch },
|
|
3058
|
+
clientMsgId: args.clientMsgId
|
|
3059
|
+
};
|
|
3060
|
+
}
|
|
2908
3061
|
// ── The rebase loop ──
|
|
2909
3062
|
async commitWithRebase(rfcGroupId, build) {
|
|
2910
3063
|
const gidBytes = fromBase64(rfcGroupId);
|
|
@@ -3041,6 +3194,14 @@ var Chat = class {
|
|
|
3041
3194
|
byClientMsgId = /* @__PURE__ */ new Map();
|
|
3042
3195
|
/** The single authoritative reaction fold for this chat (live + own-send + history). */
|
|
3043
3196
|
reactionFold = new ReactionFold();
|
|
3197
|
+
/** The single authoritative edit fold for this chat (live + own-send + history). */
|
|
3198
|
+
editFold = new EditFold();
|
|
3199
|
+
/** Per-target BASE (original) text, so the rendered text is `edit ?? original`.
|
|
3200
|
+
* Seeded once per target (write-once base) so a 2nd own edit can't corrupt it. */
|
|
3201
|
+
originalTextByClientMsgId = /* @__PURE__ */ new Map();
|
|
3202
|
+
/** Per-target AUTHOR userId — the EditFold author-gate input (filled at bubble
|
|
3203
|
+
* projection time from senderUserId; '' = resolved-but-unknown peer). */
|
|
3204
|
+
authorByClientMsgId = /* @__PURE__ */ new Map();
|
|
3044
3205
|
loadedEarliestSeq = null;
|
|
3045
3206
|
historyLoaded = false;
|
|
3046
3207
|
wired = false;
|
|
@@ -3141,13 +3302,17 @@ var Chat = class {
|
|
|
3141
3302
|
senderUserId: m.senderUserId ?? ""
|
|
3142
3303
|
});
|
|
3143
3304
|
}
|
|
3305
|
+
if (m.clientMsgId) {
|
|
3306
|
+
this.seedEditBase(m.clientMsgId, m.text, m.senderUserId ?? "");
|
|
3307
|
+
}
|
|
3144
3308
|
}
|
|
3309
|
+
this.editFold.reevaluateHeld(this.authorOfTarget);
|
|
3145
3310
|
for (const m of incoming) {
|
|
3146
3311
|
if (m.serverSeq <= 0) continue;
|
|
3147
3312
|
const key = this.internalKey(m.serverSeq);
|
|
3148
3313
|
if (this.seenKeys.has(key)) continue;
|
|
3149
3314
|
this.seenKeys.add(key);
|
|
3150
|
-
this.messageList.push(this.applyReactionTally(m));
|
|
3315
|
+
this.messageList.push(this.applyEditOverlay(this.applyReactionTally(m)));
|
|
3151
3316
|
changed = true;
|
|
3152
3317
|
this.loadedEarliestSeq = Math.min(this.loadedEarliestSeq ?? m.serverSeq, m.serverSeq);
|
|
3153
3318
|
}
|
|
@@ -3188,6 +3353,22 @@ var Chat = class {
|
|
|
3188
3353
|
}
|
|
3189
3354
|
return;
|
|
3190
3355
|
}
|
|
3356
|
+
if (incoming.envelopeType === "edit" && incoming.edit) {
|
|
3357
|
+
const editorUserId = direction === "outgoing" ? this.backend.selfUserId : senderUser;
|
|
3358
|
+
this.editFold.ingest(
|
|
3359
|
+
{
|
|
3360
|
+
targetClientMsgId: incoming.edit.targetClientMsgId,
|
|
3361
|
+
editorUserId,
|
|
3362
|
+
newText: incoming.edit.newText,
|
|
3363
|
+
epoch: incoming.epoch,
|
|
3364
|
+
serverSeq: incoming.serverSeq,
|
|
3365
|
+
eventClientMsgId: incoming.clientMsgId
|
|
3366
|
+
},
|
|
3367
|
+
this.authorOfTarget
|
|
3368
|
+
);
|
|
3369
|
+
this.recomputeEdit(incoming.edit.targetClientMsgId);
|
|
3370
|
+
return;
|
|
3371
|
+
}
|
|
3191
3372
|
const incomingClientMsgId = incoming.clientMsgId;
|
|
3192
3373
|
const incomingReplyRef = incoming.replyRef;
|
|
3193
3374
|
let resolvedReplyTo = null;
|
|
@@ -3206,7 +3387,9 @@ var Chat = class {
|
|
|
3206
3387
|
replyTo: resolvedReplyTo,
|
|
3207
3388
|
// Attach any tally already folded for this message (a reaction that arrived
|
|
3208
3389
|
// BEFORE its target — the dangling case — renders the moment the target lands).
|
|
3209
|
-
reactions: incomingClientMsgId ? this.reactionFold.tally(incomingClientMsgId) : {}
|
|
3390
|
+
reactions: incomingClientMsgId ? this.reactionFold.tally(incomingClientMsgId) : {},
|
|
3391
|
+
// Default false; applyEditOverlay below folds any edit that arrived first.
|
|
3392
|
+
edited: false
|
|
3210
3393
|
};
|
|
3211
3394
|
if (incomingClientMsgId && incoming.text !== null) {
|
|
3212
3395
|
this.byClientMsgId.set(incomingClientMsgId, {
|
|
@@ -3214,7 +3397,11 @@ var Chat = class {
|
|
|
3214
3397
|
senderUserId: senderUser ?? ""
|
|
3215
3398
|
});
|
|
3216
3399
|
}
|
|
3217
|
-
|
|
3400
|
+
if (incomingClientMsgId) {
|
|
3401
|
+
this.seedEditBase(incomingClientMsgId, incoming.text, senderUser);
|
|
3402
|
+
this.editFold.reevaluateHeld(this.authorOfTarget);
|
|
3403
|
+
}
|
|
3404
|
+
this.messageList.push(this.applyEditOverlay(msg));
|
|
3218
3405
|
this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
|
|
3219
3406
|
this.loadedEarliestSeq = Math.min(
|
|
3220
3407
|
this.loadedEarliestSeq ?? incoming.serverSeq,
|
|
@@ -3222,6 +3409,19 @@ var Chat = class {
|
|
|
3222
3409
|
);
|
|
3223
3410
|
this.emit();
|
|
3224
3411
|
}
|
|
3412
|
+
/** The EditFold author-gate input: the target message's resolved author userId
|
|
3413
|
+
* (null = target unknown/dangling → the fold HOLDs). Captured as a bound arrow
|
|
3414
|
+
* so it can be passed to the pure EditFold. */
|
|
3415
|
+
authorOfTarget = (targetClientMsgId) => this.authorByClientMsgId.get(targetClientMsgId) ?? null;
|
|
3416
|
+
/** Seed the per-target base text + author for the edit fold. Base is write-once
|
|
3417
|
+
* (a later own/peer edit must not overwrite the original we render against). The
|
|
3418
|
+
* author is (re)recorded whenever a non-empty resolution is available. */
|
|
3419
|
+
seedEditBase(clientMsgId, text, author) {
|
|
3420
|
+
if (!this.originalTextByClientMsgId.has(clientMsgId)) {
|
|
3421
|
+
this.originalTextByClientMsgId.set(clientMsgId, text);
|
|
3422
|
+
}
|
|
3423
|
+
if (author !== null) this.authorByClientMsgId.set(clientMsgId, author);
|
|
3424
|
+
}
|
|
3225
3425
|
/**
|
|
3226
3426
|
* Rebuild the target message's `reactions` from the authoritative fold and
|
|
3227
3427
|
* re-emit. No-op when the target isn't present yet (its tally is attached the
|
|
@@ -3251,6 +3451,46 @@ var Chat = class {
|
|
|
3251
3451
|
if (sameReactions(m.reactions, tally)) return m;
|
|
3252
3452
|
return { ...m, reactions: tally };
|
|
3253
3453
|
}
|
|
3454
|
+
/**
|
|
3455
|
+
* Rebuild the target message's rendered `text` + `edited` flag from the
|
|
3456
|
+
* authoritative edit fold and re-emit, PRESERVING `.reactions` and `.replyTo`
|
|
3457
|
+
* (the reaction-polish lesson — never clobber). text = `editFold.text(cid) ??
|
|
3458
|
+
* base`; base is the seeded original so a forged/ignored edit leaves it intact.
|
|
3459
|
+
* No-op when the target isn't present yet (the fold already recorded it; the
|
|
3460
|
+
* overlay applies the moment the target lands) or when unchanged.
|
|
3461
|
+
*/
|
|
3462
|
+
recomputeEdit(targetClientMsgId) {
|
|
3463
|
+
if (!targetClientMsgId) return;
|
|
3464
|
+
const editText = this.editFold.text(targetClientMsgId);
|
|
3465
|
+
const foldEdited = this.editFold.isEdited(targetClientMsgId);
|
|
3466
|
+
let changed = false;
|
|
3467
|
+
this.messageList = this.messageList.map((m) => {
|
|
3468
|
+
if (m.clientMsgId !== targetClientMsgId) return m;
|
|
3469
|
+
const base = this.originalTextByClientMsgId.has(targetClientMsgId) ? this.originalTextByClientMsgId.get(targetClientMsgId) ?? null : m.text;
|
|
3470
|
+
const text = editText ?? base;
|
|
3471
|
+
const edited = foldEdited || m.edited;
|
|
3472
|
+
if (m.text === text && m.edited === edited) return m;
|
|
3473
|
+
changed = true;
|
|
3474
|
+
return { ...m, text, edited };
|
|
3475
|
+
});
|
|
3476
|
+
if (changed) this.emit();
|
|
3477
|
+
}
|
|
3478
|
+
/**
|
|
3479
|
+
* Overlay the authoritative edit fold's winning text + flag onto a message as it
|
|
3480
|
+
* is appended/merged. The fold WINS when it has an edit for this target;
|
|
3481
|
+
* otherwise the upstream `text`/`edited` (e.g. the coordinator's page-local
|
|
3482
|
+
* history fold) is preserved. PRESERVES reactions + replyTo.
|
|
3483
|
+
*/
|
|
3484
|
+
applyEditOverlay(m) {
|
|
3485
|
+
if (!m.clientMsgId) return m;
|
|
3486
|
+
const editText = this.editFold.text(m.clientMsgId);
|
|
3487
|
+
const foldEdited = this.editFold.isEdited(m.clientMsgId);
|
|
3488
|
+
if (editText === null && !foldEdited) return m;
|
|
3489
|
+
const text = editText ?? m.text;
|
|
3490
|
+
const edited = foldEdited || m.edited;
|
|
3491
|
+
if (m.text === text && m.edited === edited) return m;
|
|
3492
|
+
return { ...m, text, edited };
|
|
3493
|
+
}
|
|
3254
3494
|
/** @internal — called by the backend's conv subscription. */
|
|
3255
3495
|
applyConv(event, payload) {
|
|
3256
3496
|
const userId = typeof payload.user_id === "string" ? payload.user_id : null;
|
|
@@ -3305,6 +3545,8 @@ var Chat = class {
|
|
|
3305
3545
|
this.memberCache = m;
|
|
3306
3546
|
this.emit();
|
|
3307
3547
|
}
|
|
3548
|
+
this.editFold.reevaluateHeld(this.authorOfTarget);
|
|
3549
|
+
for (const cid of this.originalTextByClientMsgId.keys()) this.recomputeEdit(cid);
|
|
3308
3550
|
}
|
|
3309
3551
|
seedMembersFromGroup(group) {
|
|
3310
3552
|
const seed = [
|
|
@@ -3383,6 +3625,7 @@ var Chat = class {
|
|
|
3383
3625
|
this.seenKeys.add(key);
|
|
3384
3626
|
if (clientMsgId) {
|
|
3385
3627
|
this.byClientMsgId.set(clientMsgId, { text, senderUserId: this.backend.selfUserId });
|
|
3628
|
+
this.seedEditBase(clientMsgId, text, this.backend.selfUserId);
|
|
3386
3629
|
}
|
|
3387
3630
|
this.messageList.push({
|
|
3388
3631
|
id: this.publicId(receipt.serverSeq),
|
|
@@ -3396,7 +3639,9 @@ var Chat = class {
|
|
|
3396
3639
|
replyTo: resolvedReplyTo,
|
|
3397
3640
|
// Attach any tally already folded for this own-sent message (rare, but keeps
|
|
3398
3641
|
// the dangling-target invariant uniform across every append path).
|
|
3399
|
-
reactions: clientMsgId ? this.reactionFold.tally(clientMsgId) : {}
|
|
3642
|
+
reactions: clientMsgId ? this.reactionFold.tally(clientMsgId) : {},
|
|
3643
|
+
// Own-sent edits fold via edit() after the fact; new sends start unedited.
|
|
3644
|
+
edited: false
|
|
3400
3645
|
});
|
|
3401
3646
|
this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
|
|
3402
3647
|
this.emit();
|
|
@@ -3477,6 +3722,37 @@ var Chat = class {
|
|
|
3477
3722
|
});
|
|
3478
3723
|
this.recomputeReactions(message.clientMsgId);
|
|
3479
3724
|
}
|
|
3725
|
+
// ── Edit ──
|
|
3726
|
+
/** Edit an own text message (edit-by-supersession). No-op if the message isn't
|
|
3727
|
+
* editable (empty clientMsgId, or not a `text` kind). The edit folds locally
|
|
3728
|
+
* with the server receipt's `(epoch, serverSeq)` so the target's text updates
|
|
3729
|
+
* instantly; the durable echo on the next pump is a fold no-op (dedup on the
|
|
3730
|
+
* SAME wire clientMsgId). Never appends a bubble; PRESERVES the target's
|
|
3731
|
+
* reactions + reply context. Only the original author's edits count — for an own
|
|
3732
|
+
* message self IS the author, so the author-gate passes. */
|
|
3733
|
+
async edit(message, newText) {
|
|
3734
|
+
if (!message.clientMsgId || message.kind !== "text") return;
|
|
3735
|
+
const group = await this.materializeIfNeeded();
|
|
3736
|
+
const clientMsgId = mintClientMsgId();
|
|
3737
|
+
const { receipt } = await this.backend.sendEdit(group, {
|
|
3738
|
+
clientMsgId,
|
|
3739
|
+
targetClientMsgId: message.clientMsgId,
|
|
3740
|
+
newText
|
|
3741
|
+
});
|
|
3742
|
+
this.seedEditBase(message.clientMsgId, message.text, this.backend.selfUserId);
|
|
3743
|
+
this.editFold.ingest(
|
|
3744
|
+
{
|
|
3745
|
+
targetClientMsgId: message.clientMsgId,
|
|
3746
|
+
editorUserId: this.backend.selfUserId,
|
|
3747
|
+
newText,
|
|
3748
|
+
epoch: receipt.epoch,
|
|
3749
|
+
serverSeq: receipt.serverSeq,
|
|
3750
|
+
eventClientMsgId: clientMsgId
|
|
3751
|
+
},
|
|
3752
|
+
this.authorOfTarget
|
|
3753
|
+
);
|
|
3754
|
+
this.recomputeEdit(message.clientMsgId);
|
|
3755
|
+
}
|
|
3480
3756
|
};
|
|
3481
3757
|
function sameReactions(a, b) {
|
|
3482
3758
|
const ak = Object.keys(a);
|
|
@@ -3519,6 +3795,11 @@ var MessageHub = class {
|
|
|
3519
3795
|
};
|
|
3520
3796
|
}
|
|
3521
3797
|
};
|
|
3798
|
+
function decodeSenderDeviceId(sender) {
|
|
3799
|
+
if (sender.length === 0) return null;
|
|
3800
|
+
const id = decodeUtf8(sender);
|
|
3801
|
+
return id.length > 0 ? id : null;
|
|
3802
|
+
}
|
|
3522
3803
|
var MessageDeliverySource = class {
|
|
3523
3804
|
constructor(rt, engine, hub, registry, messageStore, deviceId, selfUserId) {
|
|
3524
3805
|
this.rt = rt;
|
|
@@ -3651,11 +3932,13 @@ var MessageDeliverySource = class {
|
|
|
3651
3932
|
const decoded = decodeEnvelope(received.data);
|
|
3652
3933
|
const { text, clientMsgId, replyTo } = decoded;
|
|
3653
3934
|
const isReaction = decoded.type === "reaction" && decoded.reaction != null;
|
|
3935
|
+
const isEdit = decoded.type === "edit" && decoded.edit != null;
|
|
3936
|
+
const senderDeviceId = row.sender_device_id ?? decodeSenderDeviceId(received.sender);
|
|
3654
3937
|
const stored = {
|
|
3655
3938
|
id: `${group.rfcGroupId}#${row.server_seq}`,
|
|
3656
3939
|
direction: "incoming",
|
|
3657
3940
|
text,
|
|
3658
|
-
senderDeviceId
|
|
3941
|
+
senderDeviceId,
|
|
3659
3942
|
epoch: row.epoch,
|
|
3660
3943
|
serverSeq: row.server_seq,
|
|
3661
3944
|
at: Date.now(),
|
|
@@ -3677,6 +3960,17 @@ var MessageDeliverySource = class {
|
|
|
3677
3960
|
emoji: decoded.reaction.emoji,
|
|
3678
3961
|
op: decoded.reaction.op
|
|
3679
3962
|
}
|
|
3963
|
+
} : {},
|
|
3964
|
+
// Thread the edit discriminator + new text through the persisted row so an
|
|
3965
|
+
// edit folded LIVE re-folds onto its target after a reload (the reload-parity
|
|
3966
|
+
// boundary — mirrors iOS T3). Omitted for non-edits → old rows hydrate as
|
|
3967
|
+
// `'text'`/no-edit (backward-compat).
|
|
3968
|
+
...isEdit && decoded.edit ? {
|
|
3969
|
+
envelopeType: "edit",
|
|
3970
|
+
edit: {
|
|
3971
|
+
targetClientMsgId: decoded.edit.targetClientMsgId,
|
|
3972
|
+
newText: decoded.edit.newText
|
|
3973
|
+
}
|
|
3680
3974
|
} : {}
|
|
3681
3975
|
};
|
|
3682
3976
|
try {
|
|
@@ -3688,14 +3982,15 @@ var MessageDeliverySource = class {
|
|
|
3688
3982
|
kind: "application",
|
|
3689
3983
|
group,
|
|
3690
3984
|
text,
|
|
3691
|
-
senderDeviceId
|
|
3985
|
+
senderDeviceId,
|
|
3692
3986
|
epoch: row.epoch,
|
|
3693
3987
|
serverSeq: row.server_seq,
|
|
3694
3988
|
receivedAt: /* @__PURE__ */ new Date(),
|
|
3695
3989
|
clientMsgId,
|
|
3696
3990
|
replyRef: replyTo,
|
|
3697
3991
|
envelopeType: decoded.type ?? "text",
|
|
3698
|
-
reaction: isReaction ? decoded.reaction : null
|
|
3992
|
+
reaction: isReaction ? decoded.reaction : null,
|
|
3993
|
+
edit: isEdit ? decoded.edit : null
|
|
3699
3994
|
});
|
|
3700
3995
|
return true;
|
|
3701
3996
|
}
|
|
@@ -5703,6 +5998,10 @@ var MessagingCoordinator = class {
|
|
|
5703
5998
|
const r = await this.resolve();
|
|
5704
5999
|
return r.groups.sendReaction(group, args);
|
|
5705
6000
|
}
|
|
6001
|
+
async sendEdit(group, args) {
|
|
6002
|
+
const r = await this.resolve();
|
|
6003
|
+
return r.groups.sendEdit(group, args);
|
|
6004
|
+
}
|
|
5706
6005
|
async history(group, limit, before) {
|
|
5707
6006
|
const r = await this.resolve();
|
|
5708
6007
|
const rows = await r.messageStore.history(group.rfcGroupId, limit, before);
|
|
@@ -5799,6 +6098,32 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
5799
6098
|
eventClientMsgId: s.clientMsgId ?? `${s.id}`
|
|
5800
6099
|
});
|
|
5801
6100
|
}
|
|
6101
|
+
const editFold = new EditFold();
|
|
6102
|
+
const authorByClientMsgId = /* @__PURE__ */ new Map();
|
|
6103
|
+
for (const s of rows) {
|
|
6104
|
+
if (s.envelopeType === "reaction" || s.envelopeType === "edit") continue;
|
|
6105
|
+
const cid = s.clientMsgId ?? "";
|
|
6106
|
+
if (!cid) continue;
|
|
6107
|
+
const author = s.direction === "outgoing" ? selfUserId : resolveActor?.(s.senderDeviceId ?? null);
|
|
6108
|
+
if (author != null) authorByClientMsgId.set(cid, author);
|
|
6109
|
+
}
|
|
6110
|
+
const authorOfTarget = (cid) => authorByClientMsgId.get(cid) ?? null;
|
|
6111
|
+
for (const s of rows) {
|
|
6112
|
+
if (s.envelopeType !== "edit" || !s.edit) continue;
|
|
6113
|
+
const editor = s.direction === "outgoing" ? selfUserId : resolveActor?.(s.senderDeviceId ?? null) ?? null;
|
|
6114
|
+
editFold.ingest(
|
|
6115
|
+
{
|
|
6116
|
+
targetClientMsgId: s.edit.targetClientMsgId,
|
|
6117
|
+
editorUserId: editor,
|
|
6118
|
+
newText: s.edit.newText,
|
|
6119
|
+
epoch: s.epoch,
|
|
6120
|
+
serverSeq: s.serverSeq,
|
|
6121
|
+
eventClientMsgId: s.clientMsgId ?? `${s.id}`
|
|
6122
|
+
},
|
|
6123
|
+
authorOfTarget
|
|
6124
|
+
);
|
|
6125
|
+
}
|
|
6126
|
+
editFold.reevaluateHeld(authorOfTarget);
|
|
5802
6127
|
const lookup = /* @__PURE__ */ new Map();
|
|
5803
6128
|
for (const s of rows) {
|
|
5804
6129
|
if (s.envelopeType === "reaction") continue;
|
|
@@ -5810,7 +6135,7 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
5810
6135
|
}
|
|
5811
6136
|
const out = [];
|
|
5812
6137
|
for (const s of rows) {
|
|
5813
|
-
if (s.envelopeType === "reaction") continue;
|
|
6138
|
+
if (s.envelopeType === "reaction" || s.envelopeType === "edit") continue;
|
|
5814
6139
|
const clientMsgId = s.clientMsgId ?? "";
|
|
5815
6140
|
let replyTo = null;
|
|
5816
6141
|
if (s.replyTo) {
|
|
@@ -5826,17 +6151,20 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
5826
6151
|
};
|
|
5827
6152
|
replyTo = resolveReply(ref, (id) => lookup.get(id) ?? null);
|
|
5828
6153
|
}
|
|
6154
|
+
const editText = clientMsgId ? editFold.text(clientMsgId) : null;
|
|
6155
|
+
const edited = clientMsgId ? editFold.isEdited(clientMsgId) : false;
|
|
5829
6156
|
out.push({
|
|
5830
6157
|
id: `${displayId}#${s.serverSeq}`,
|
|
5831
6158
|
kind: s.text != null ? "text" : "system",
|
|
5832
6159
|
direction: s.direction,
|
|
5833
6160
|
senderUserId: s.direction === "outgoing" ? selfUserId : null,
|
|
5834
|
-
text: s.text,
|
|
6161
|
+
text: editText ?? s.text,
|
|
5835
6162
|
serverSeq: s.serverSeq,
|
|
5836
6163
|
sentAt: new Date(s.at),
|
|
5837
6164
|
clientMsgId,
|
|
5838
6165
|
replyTo,
|
|
5839
|
-
reactions: clientMsgId ? fold.tally(clientMsgId) : {}
|
|
6166
|
+
reactions: clientMsgId ? fold.tally(clientMsgId) : {},
|
|
6167
|
+
edited
|
|
5840
6168
|
});
|
|
5841
6169
|
}
|
|
5842
6170
|
return out;
|
|
@@ -6574,7 +6902,7 @@ function defaultSessionStorage(key) {
|
|
|
6574
6902
|
}
|
|
6575
6903
|
|
|
6576
6904
|
// src/version.ts
|
|
6577
|
-
var VERSION = "1.
|
|
6905
|
+
var VERSION = "1.3.0";
|
|
6578
6906
|
|
|
6579
6907
|
// src/runtime.ts
|
|
6580
6908
|
function buildRuntime(config) {
|