@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/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);
|
|
@@ -3656,6 +3932,7 @@ var MessageDeliverySource = class {
|
|
|
3656
3932
|
const decoded = decodeEnvelope(received.data);
|
|
3657
3933
|
const { text, clientMsgId, replyTo } = decoded;
|
|
3658
3934
|
const isReaction = decoded.type === "reaction" && decoded.reaction != null;
|
|
3935
|
+
const isEdit = decoded.type === "edit" && decoded.edit != null;
|
|
3659
3936
|
const senderDeviceId = row.sender_device_id ?? decodeSenderDeviceId(received.sender);
|
|
3660
3937
|
const stored = {
|
|
3661
3938
|
id: `${group.rfcGroupId}#${row.server_seq}`,
|
|
@@ -3683,6 +3960,17 @@ var MessageDeliverySource = class {
|
|
|
3683
3960
|
emoji: decoded.reaction.emoji,
|
|
3684
3961
|
op: decoded.reaction.op
|
|
3685
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
|
+
}
|
|
3686
3974
|
} : {}
|
|
3687
3975
|
};
|
|
3688
3976
|
try {
|
|
@@ -3701,7 +3989,8 @@ var MessageDeliverySource = class {
|
|
|
3701
3989
|
clientMsgId,
|
|
3702
3990
|
replyRef: replyTo,
|
|
3703
3991
|
envelopeType: decoded.type ?? "text",
|
|
3704
|
-
reaction: isReaction ? decoded.reaction : null
|
|
3992
|
+
reaction: isReaction ? decoded.reaction : null,
|
|
3993
|
+
edit: isEdit ? decoded.edit : null
|
|
3705
3994
|
});
|
|
3706
3995
|
return true;
|
|
3707
3996
|
}
|
|
@@ -5709,6 +5998,10 @@ var MessagingCoordinator = class {
|
|
|
5709
5998
|
const r = await this.resolve();
|
|
5710
5999
|
return r.groups.sendReaction(group, args);
|
|
5711
6000
|
}
|
|
6001
|
+
async sendEdit(group, args) {
|
|
6002
|
+
const r = await this.resolve();
|
|
6003
|
+
return r.groups.sendEdit(group, args);
|
|
6004
|
+
}
|
|
5712
6005
|
async history(group, limit, before) {
|
|
5713
6006
|
const r = await this.resolve();
|
|
5714
6007
|
const rows = await r.messageStore.history(group.rfcGroupId, limit, before);
|
|
@@ -5805,6 +6098,32 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
5805
6098
|
eventClientMsgId: s.clientMsgId ?? `${s.id}`
|
|
5806
6099
|
});
|
|
5807
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);
|
|
5808
6127
|
const lookup = /* @__PURE__ */ new Map();
|
|
5809
6128
|
for (const s of rows) {
|
|
5810
6129
|
if (s.envelopeType === "reaction") continue;
|
|
@@ -5816,7 +6135,7 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
5816
6135
|
}
|
|
5817
6136
|
const out = [];
|
|
5818
6137
|
for (const s of rows) {
|
|
5819
|
-
if (s.envelopeType === "reaction") continue;
|
|
6138
|
+
if (s.envelopeType === "reaction" || s.envelopeType === "edit") continue;
|
|
5820
6139
|
const clientMsgId = s.clientMsgId ?? "";
|
|
5821
6140
|
let replyTo = null;
|
|
5822
6141
|
if (s.replyTo) {
|
|
@@ -5832,17 +6151,20 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
5832
6151
|
};
|
|
5833
6152
|
replyTo = resolveReply(ref, (id) => lookup.get(id) ?? null);
|
|
5834
6153
|
}
|
|
6154
|
+
const editText = clientMsgId ? editFold.text(clientMsgId) : null;
|
|
6155
|
+
const edited = clientMsgId ? editFold.isEdited(clientMsgId) : false;
|
|
5835
6156
|
out.push({
|
|
5836
6157
|
id: `${displayId}#${s.serverSeq}`,
|
|
5837
6158
|
kind: s.text != null ? "text" : "system",
|
|
5838
6159
|
direction: s.direction,
|
|
5839
6160
|
senderUserId: s.direction === "outgoing" ? selfUserId : null,
|
|
5840
|
-
text: s.text,
|
|
6161
|
+
text: editText ?? s.text,
|
|
5841
6162
|
serverSeq: s.serverSeq,
|
|
5842
6163
|
sentAt: new Date(s.at),
|
|
5843
6164
|
clientMsgId,
|
|
5844
6165
|
replyTo,
|
|
5845
|
-
reactions: clientMsgId ? fold.tally(clientMsgId) : {}
|
|
6166
|
+
reactions: clientMsgId ? fold.tally(clientMsgId) : {},
|
|
6167
|
+
edited
|
|
5846
6168
|
});
|
|
5847
6169
|
}
|
|
5848
6170
|
return out;
|
|
@@ -6580,7 +6902,7 @@ function defaultSessionStorage(key) {
|
|
|
6580
6902
|
}
|
|
6581
6903
|
|
|
6582
6904
|
// src/version.ts
|
|
6583
|
-
var VERSION = "1.
|
|
6905
|
+
var VERSION = "1.3.0";
|
|
6584
6906
|
|
|
6585
6907
|
// src/runtime.ts
|
|
6586
6908
|
function buildRuntime(config) {
|