@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/index.cjs
CHANGED
|
@@ -1607,6 +1607,88 @@ var PalbeFlags = class {
|
|
|
1607
1607
|
}
|
|
1608
1608
|
};
|
|
1609
1609
|
|
|
1610
|
+
// src/messaging/edit-fold.ts
|
|
1611
|
+
function orderLt(aEpoch, aSeq, bEpoch, bSeq) {
|
|
1612
|
+
if (aEpoch !== bEpoch) return aEpoch < bEpoch;
|
|
1613
|
+
return aSeq < bSeq;
|
|
1614
|
+
}
|
|
1615
|
+
function orderEq(aEpoch, aSeq, bEpoch, bSeq) {
|
|
1616
|
+
return aEpoch === bEpoch && aSeq === bSeq;
|
|
1617
|
+
}
|
|
1618
|
+
var EditFold = class {
|
|
1619
|
+
// target → winning edit state
|
|
1620
|
+
states = /* @__PURE__ */ new Map();
|
|
1621
|
+
// dedup of real wire events that reached (and were resolvable enough to evaluate at) the fold
|
|
1622
|
+
seenEvents = /* @__PURE__ */ new Set();
|
|
1623
|
+
// events parked because target/author or sender was unresolved at ingest time
|
|
1624
|
+
held = [];
|
|
1625
|
+
// targets that have had ≥1 valid edit applied (write-once)
|
|
1626
|
+
editedTargets = /* @__PURE__ */ new Set();
|
|
1627
|
+
/**
|
|
1628
|
+
* Ingest one edit. `authorOfTarget` resolves the target message's AUTHOR userId
|
|
1629
|
+
* (null = target unknown/dangling → HOLD).
|
|
1630
|
+
*/
|
|
1631
|
+
ingest(e, authorOfTarget) {
|
|
1632
|
+
const author = authorOfTarget(e.targetClientMsgId);
|
|
1633
|
+
if (author === null) {
|
|
1634
|
+
this.holdIfNew(e);
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
if (e.editorUserId === null) {
|
|
1638
|
+
this.holdIfNew(e);
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
if (e.editorUserId !== author) return;
|
|
1642
|
+
if (this.seenEvents.has(e.eventClientMsgId)) return;
|
|
1643
|
+
this.seenEvents.add(e.eventClientMsgId);
|
|
1644
|
+
const prev = this.states.get(e.targetClientMsgId);
|
|
1645
|
+
if (prev !== void 0) {
|
|
1646
|
+
if (orderLt(e.epoch, e.serverSeq, prev.orderEpoch, prev.orderSeq)) return;
|
|
1647
|
+
if (orderEq(e.epoch, e.serverSeq, prev.orderEpoch, prev.orderSeq) && e.eventClientMsgId <= prev.lastEventId) {
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
this.states.set(e.targetClientMsgId, {
|
|
1652
|
+
orderEpoch: e.epoch,
|
|
1653
|
+
orderSeq: e.serverSeq,
|
|
1654
|
+
lastEventId: e.eventClientMsgId,
|
|
1655
|
+
text: e.newText
|
|
1656
|
+
});
|
|
1657
|
+
this.editedTargets.add(e.targetClientMsgId);
|
|
1658
|
+
}
|
|
1659
|
+
/**
|
|
1660
|
+
* Park an event for later re-attempt, deduping held re-deliveries by
|
|
1661
|
+
* eventClientMsgId so a repeatedly-delivered unresolvable edit is held exactly
|
|
1662
|
+
* once (and never double-applies when it finally resolves on reevaluate).
|
|
1663
|
+
*/
|
|
1664
|
+
holdIfNew(e) {
|
|
1665
|
+
if (this.held.some((h) => h.eventClientMsgId === e.eventClientMsgId)) return;
|
|
1666
|
+
this.held.push(e);
|
|
1667
|
+
}
|
|
1668
|
+
/** The winning edit text for a target, or null if no valid edit has applied. */
|
|
1669
|
+
text(targetClientMsgId) {
|
|
1670
|
+
return this.states.get(targetClientMsgId)?.text ?? null;
|
|
1671
|
+
}
|
|
1672
|
+
/** Write-once: true once any valid edit applied to the target. */
|
|
1673
|
+
isEdited(targetClientMsgId) {
|
|
1674
|
+
return this.editedTargets.has(targetClientMsgId);
|
|
1675
|
+
}
|
|
1676
|
+
/**
|
|
1677
|
+
* Re-run HELD edits when the roster/target newly resolves (call on member/roster
|
|
1678
|
+
* change and when a target message arrives). Clears `held` and re-ingests each
|
|
1679
|
+
* event with the fresh `authorOfTarget` — events that still don't resolve are
|
|
1680
|
+
* simply re-held; events that now resolve fold via the normal LWW path.
|
|
1681
|
+
* Idempotent: re-ingest is deduped by `seenEvents` (applied events) and by
|
|
1682
|
+
* `holdIfNew` (still-held events), so reevaluating repeatedly can neither
|
|
1683
|
+
* double-apply nor lose an edit.
|
|
1684
|
+
*/
|
|
1685
|
+
reevaluateHeld(authorOfTarget) {
|
|
1686
|
+
const pending = this.held;
|
|
1687
|
+
this.held = [];
|
|
1688
|
+
for (const e of pending) this.ingest(e, authorOfTarget);
|
|
1689
|
+
}
|
|
1690
|
+
};
|
|
1691
|
+
|
|
1610
1692
|
// src/messaging/util.ts
|
|
1611
1693
|
function toBase64(bytes) {
|
|
1612
1694
|
if (typeof Buffer !== "undefined") {
|
|
@@ -1746,6 +1828,17 @@ async function listDevices(rt, userId) {
|
|
|
1746
1828
|
}
|
|
1747
1829
|
|
|
1748
1830
|
// src/messaging/group-messaging.ts
|
|
1831
|
+
function encodeEdit(args) {
|
|
1832
|
+
return encodeUtf8(
|
|
1833
|
+
JSON.stringify({
|
|
1834
|
+
v: 1,
|
|
1835
|
+
type: "edit",
|
|
1836
|
+
client_msg_id: args.clientMsgId,
|
|
1837
|
+
target_client_msg_id: args.targetClientMsgId,
|
|
1838
|
+
new_text: args.newText
|
|
1839
|
+
})
|
|
1840
|
+
);
|
|
1841
|
+
}
|
|
1749
1842
|
function encodeReaction(args) {
|
|
1750
1843
|
return encodeUtf8(
|
|
1751
1844
|
JSON.stringify({
|
|
@@ -1785,6 +1878,18 @@ function decodeEnvelope(bytes) {
|
|
|
1785
1878
|
}
|
|
1786
1879
|
};
|
|
1787
1880
|
}
|
|
1881
|
+
if (typeof o === "object" && o !== null && o.type === "edit") {
|
|
1882
|
+
return {
|
|
1883
|
+
type: "edit",
|
|
1884
|
+
text: null,
|
|
1885
|
+
clientMsgId: o.client_msg_id ?? "",
|
|
1886
|
+
replyTo: null,
|
|
1887
|
+
edit: {
|
|
1888
|
+
targetClientMsgId: o.target_client_msg_id ?? "",
|
|
1889
|
+
newText: o.new_text ?? ""
|
|
1890
|
+
}
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1788
1893
|
if (typeof o === "object" && o !== null && o.type === "text" && typeof o.v === "number") {
|
|
1789
1894
|
return {
|
|
1790
1895
|
type: "text",
|
|
@@ -2099,6 +2204,54 @@ var GroupMessaging = class {
|
|
|
2099
2204
|
clientMsgId: args.clientMsgId
|
|
2100
2205
|
};
|
|
2101
2206
|
}
|
|
2207
|
+
/** Send an edit (edit-by-supersession on a target message). Encrypts a
|
|
2208
|
+
* `type:'edit'` envelope at the current epoch and sends through the SAME MLS
|
|
2209
|
+
* application path as `sendText` (the server stays blind — an edit is just
|
|
2210
|
+
* another application message). Persists the outgoing edit row so it re-folds
|
|
2211
|
+
* onto its target's text after a reload (the own-send half of the reload
|
|
2212
|
+
* parity). NEVER rebases (epoch-bound like any application message). */
|
|
2213
|
+
async sendEdit(group, args) {
|
|
2214
|
+
const plaintext = encodeEdit({
|
|
2215
|
+
clientMsgId: args.clientMsgId,
|
|
2216
|
+
targetClientMsgId: args.targetClientMsgId,
|
|
2217
|
+
newText: args.newText
|
|
2218
|
+
});
|
|
2219
|
+
const ct = await this.engine.encryptApplication(fromBase64(group.rfcGroupId), plaintext);
|
|
2220
|
+
const body = {
|
|
2221
|
+
ciphertext_b64: toBase64(ct),
|
|
2222
|
+
client_idem_key: randomId()
|
|
2223
|
+
};
|
|
2224
|
+
const wire = await palbeRequest(
|
|
2225
|
+
this.rt,
|
|
2226
|
+
"POST",
|
|
2227
|
+
MessagingPaths.groupMessages(group.displayId),
|
|
2228
|
+
{ body }
|
|
2229
|
+
);
|
|
2230
|
+
const stored = {
|
|
2231
|
+
id: `${group.rfcGroupId}#${wire.server_seq}`,
|
|
2232
|
+
direction: "outgoing",
|
|
2233
|
+
text: null,
|
|
2234
|
+
senderDeviceId: this.selfDeviceId,
|
|
2235
|
+
epoch: wire.epoch,
|
|
2236
|
+
serverSeq: wire.server_seq,
|
|
2237
|
+
at: Date.now(),
|
|
2238
|
+
clientMsgId: args.clientMsgId,
|
|
2239
|
+
replyTo: null,
|
|
2240
|
+
envelopeType: "edit",
|
|
2241
|
+
edit: {
|
|
2242
|
+
targetClientMsgId: args.targetClientMsgId,
|
|
2243
|
+
newText: args.newText
|
|
2244
|
+
}
|
|
2245
|
+
};
|
|
2246
|
+
try {
|
|
2247
|
+
await this.messageStore.append(group.rfcGroupId, stored);
|
|
2248
|
+
} catch {
|
|
2249
|
+
}
|
|
2250
|
+
return {
|
|
2251
|
+
receipt: { serverSeq: wire.server_seq, epoch: wire.epoch },
|
|
2252
|
+
clientMsgId: args.clientMsgId
|
|
2253
|
+
};
|
|
2254
|
+
}
|
|
2102
2255
|
// ── The rebase loop ──
|
|
2103
2256
|
async commitWithRebase(rfcGroupId, build) {
|
|
2104
2257
|
const gidBytes = fromBase64(rfcGroupId);
|
|
@@ -2235,6 +2388,14 @@ var Chat = class {
|
|
|
2235
2388
|
byClientMsgId = /* @__PURE__ */ new Map();
|
|
2236
2389
|
/** The single authoritative reaction fold for this chat (live + own-send + history). */
|
|
2237
2390
|
reactionFold = new ReactionFold();
|
|
2391
|
+
/** The single authoritative edit fold for this chat (live + own-send + history). */
|
|
2392
|
+
editFold = new EditFold();
|
|
2393
|
+
/** Per-target BASE (original) text, so the rendered text is `edit ?? original`.
|
|
2394
|
+
* Seeded once per target (write-once base) so a 2nd own edit can't corrupt it. */
|
|
2395
|
+
originalTextByClientMsgId = /* @__PURE__ */ new Map();
|
|
2396
|
+
/** Per-target AUTHOR userId — the EditFold author-gate input (filled at bubble
|
|
2397
|
+
* projection time from senderUserId; '' = resolved-but-unknown peer). */
|
|
2398
|
+
authorByClientMsgId = /* @__PURE__ */ new Map();
|
|
2238
2399
|
loadedEarliestSeq = null;
|
|
2239
2400
|
historyLoaded = false;
|
|
2240
2401
|
wired = false;
|
|
@@ -2335,13 +2496,17 @@ var Chat = class {
|
|
|
2335
2496
|
senderUserId: m.senderUserId ?? ""
|
|
2336
2497
|
});
|
|
2337
2498
|
}
|
|
2499
|
+
if (m.clientMsgId) {
|
|
2500
|
+
this.seedEditBase(m.clientMsgId, m.text, m.senderUserId ?? "");
|
|
2501
|
+
}
|
|
2338
2502
|
}
|
|
2503
|
+
this.editFold.reevaluateHeld(this.authorOfTarget);
|
|
2339
2504
|
for (const m of incoming) {
|
|
2340
2505
|
if (m.serverSeq <= 0) continue;
|
|
2341
2506
|
const key = this.internalKey(m.serverSeq);
|
|
2342
2507
|
if (this.seenKeys.has(key)) continue;
|
|
2343
2508
|
this.seenKeys.add(key);
|
|
2344
|
-
this.messageList.push(this.applyReactionTally(m));
|
|
2509
|
+
this.messageList.push(this.applyEditOverlay(this.applyReactionTally(m)));
|
|
2345
2510
|
changed = true;
|
|
2346
2511
|
this.loadedEarliestSeq = Math.min(this.loadedEarliestSeq ?? m.serverSeq, m.serverSeq);
|
|
2347
2512
|
}
|
|
@@ -2382,6 +2547,22 @@ var Chat = class {
|
|
|
2382
2547
|
}
|
|
2383
2548
|
return;
|
|
2384
2549
|
}
|
|
2550
|
+
if (incoming.envelopeType === "edit" && incoming.edit) {
|
|
2551
|
+
const editorUserId = direction === "outgoing" ? this.backend.selfUserId : senderUser;
|
|
2552
|
+
this.editFold.ingest(
|
|
2553
|
+
{
|
|
2554
|
+
targetClientMsgId: incoming.edit.targetClientMsgId,
|
|
2555
|
+
editorUserId,
|
|
2556
|
+
newText: incoming.edit.newText,
|
|
2557
|
+
epoch: incoming.epoch,
|
|
2558
|
+
serverSeq: incoming.serverSeq,
|
|
2559
|
+
eventClientMsgId: incoming.clientMsgId
|
|
2560
|
+
},
|
|
2561
|
+
this.authorOfTarget
|
|
2562
|
+
);
|
|
2563
|
+
this.recomputeEdit(incoming.edit.targetClientMsgId);
|
|
2564
|
+
return;
|
|
2565
|
+
}
|
|
2385
2566
|
const incomingClientMsgId = incoming.clientMsgId;
|
|
2386
2567
|
const incomingReplyRef = incoming.replyRef;
|
|
2387
2568
|
let resolvedReplyTo = null;
|
|
@@ -2400,7 +2581,9 @@ var Chat = class {
|
|
|
2400
2581
|
replyTo: resolvedReplyTo,
|
|
2401
2582
|
// Attach any tally already folded for this message (a reaction that arrived
|
|
2402
2583
|
// BEFORE its target — the dangling case — renders the moment the target lands).
|
|
2403
|
-
reactions: incomingClientMsgId ? this.reactionFold.tally(incomingClientMsgId) : {}
|
|
2584
|
+
reactions: incomingClientMsgId ? this.reactionFold.tally(incomingClientMsgId) : {},
|
|
2585
|
+
// Default false; applyEditOverlay below folds any edit that arrived first.
|
|
2586
|
+
edited: false
|
|
2404
2587
|
};
|
|
2405
2588
|
if (incomingClientMsgId && incoming.text !== null) {
|
|
2406
2589
|
this.byClientMsgId.set(incomingClientMsgId, {
|
|
@@ -2408,7 +2591,11 @@ var Chat = class {
|
|
|
2408
2591
|
senderUserId: senderUser ?? ""
|
|
2409
2592
|
});
|
|
2410
2593
|
}
|
|
2411
|
-
|
|
2594
|
+
if (incomingClientMsgId) {
|
|
2595
|
+
this.seedEditBase(incomingClientMsgId, incoming.text, senderUser);
|
|
2596
|
+
this.editFold.reevaluateHeld(this.authorOfTarget);
|
|
2597
|
+
}
|
|
2598
|
+
this.messageList.push(this.applyEditOverlay(msg));
|
|
2412
2599
|
this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
|
|
2413
2600
|
this.loadedEarliestSeq = Math.min(
|
|
2414
2601
|
this.loadedEarliestSeq ?? incoming.serverSeq,
|
|
@@ -2416,6 +2603,19 @@ var Chat = class {
|
|
|
2416
2603
|
);
|
|
2417
2604
|
this.emit();
|
|
2418
2605
|
}
|
|
2606
|
+
/** The EditFold author-gate input: the target message's resolved author userId
|
|
2607
|
+
* (null = target unknown/dangling → the fold HOLDs). Captured as a bound arrow
|
|
2608
|
+
* so it can be passed to the pure EditFold. */
|
|
2609
|
+
authorOfTarget = (targetClientMsgId) => this.authorByClientMsgId.get(targetClientMsgId) ?? null;
|
|
2610
|
+
/** Seed the per-target base text + author for the edit fold. Base is write-once
|
|
2611
|
+
* (a later own/peer edit must not overwrite the original we render against). The
|
|
2612
|
+
* author is (re)recorded whenever a non-empty resolution is available. */
|
|
2613
|
+
seedEditBase(clientMsgId, text, author) {
|
|
2614
|
+
if (!this.originalTextByClientMsgId.has(clientMsgId)) {
|
|
2615
|
+
this.originalTextByClientMsgId.set(clientMsgId, text);
|
|
2616
|
+
}
|
|
2617
|
+
if (author !== null) this.authorByClientMsgId.set(clientMsgId, author);
|
|
2618
|
+
}
|
|
2419
2619
|
/**
|
|
2420
2620
|
* Rebuild the target message's `reactions` from the authoritative fold and
|
|
2421
2621
|
* re-emit. No-op when the target isn't present yet (its tally is attached the
|
|
@@ -2445,6 +2645,46 @@ var Chat = class {
|
|
|
2445
2645
|
if (sameReactions(m.reactions, tally)) return m;
|
|
2446
2646
|
return { ...m, reactions: tally };
|
|
2447
2647
|
}
|
|
2648
|
+
/**
|
|
2649
|
+
* Rebuild the target message's rendered `text` + `edited` flag from the
|
|
2650
|
+
* authoritative edit fold and re-emit, PRESERVING `.reactions` and `.replyTo`
|
|
2651
|
+
* (the reaction-polish lesson — never clobber). text = `editFold.text(cid) ??
|
|
2652
|
+
* base`; base is the seeded original so a forged/ignored edit leaves it intact.
|
|
2653
|
+
* No-op when the target isn't present yet (the fold already recorded it; the
|
|
2654
|
+
* overlay applies the moment the target lands) or when unchanged.
|
|
2655
|
+
*/
|
|
2656
|
+
recomputeEdit(targetClientMsgId) {
|
|
2657
|
+
if (!targetClientMsgId) return;
|
|
2658
|
+
const editText = this.editFold.text(targetClientMsgId);
|
|
2659
|
+
const foldEdited = this.editFold.isEdited(targetClientMsgId);
|
|
2660
|
+
let changed = false;
|
|
2661
|
+
this.messageList = this.messageList.map((m) => {
|
|
2662
|
+
if (m.clientMsgId !== targetClientMsgId) return m;
|
|
2663
|
+
const base = this.originalTextByClientMsgId.has(targetClientMsgId) ? this.originalTextByClientMsgId.get(targetClientMsgId) ?? null : m.text;
|
|
2664
|
+
const text = editText ?? base;
|
|
2665
|
+
const edited = foldEdited || m.edited;
|
|
2666
|
+
if (m.text === text && m.edited === edited) return m;
|
|
2667
|
+
changed = true;
|
|
2668
|
+
return { ...m, text, edited };
|
|
2669
|
+
});
|
|
2670
|
+
if (changed) this.emit();
|
|
2671
|
+
}
|
|
2672
|
+
/**
|
|
2673
|
+
* Overlay the authoritative edit fold's winning text + flag onto a message as it
|
|
2674
|
+
* is appended/merged. The fold WINS when it has an edit for this target;
|
|
2675
|
+
* otherwise the upstream `text`/`edited` (e.g. the coordinator's page-local
|
|
2676
|
+
* history fold) is preserved. PRESERVES reactions + replyTo.
|
|
2677
|
+
*/
|
|
2678
|
+
applyEditOverlay(m) {
|
|
2679
|
+
if (!m.clientMsgId) return m;
|
|
2680
|
+
const editText = this.editFold.text(m.clientMsgId);
|
|
2681
|
+
const foldEdited = this.editFold.isEdited(m.clientMsgId);
|
|
2682
|
+
if (editText === null && !foldEdited) return m;
|
|
2683
|
+
const text = editText ?? m.text;
|
|
2684
|
+
const edited = foldEdited || m.edited;
|
|
2685
|
+
if (m.text === text && m.edited === edited) return m;
|
|
2686
|
+
return { ...m, text, edited };
|
|
2687
|
+
}
|
|
2448
2688
|
/** @internal — called by the backend's conv subscription. */
|
|
2449
2689
|
applyConv(event, payload) {
|
|
2450
2690
|
const userId = typeof payload.user_id === "string" ? payload.user_id : null;
|
|
@@ -2499,6 +2739,8 @@ var Chat = class {
|
|
|
2499
2739
|
this.memberCache = m;
|
|
2500
2740
|
this.emit();
|
|
2501
2741
|
}
|
|
2742
|
+
this.editFold.reevaluateHeld(this.authorOfTarget);
|
|
2743
|
+
for (const cid of this.originalTextByClientMsgId.keys()) this.recomputeEdit(cid);
|
|
2502
2744
|
}
|
|
2503
2745
|
seedMembersFromGroup(group) {
|
|
2504
2746
|
const seed = [
|
|
@@ -2577,6 +2819,7 @@ var Chat = class {
|
|
|
2577
2819
|
this.seenKeys.add(key);
|
|
2578
2820
|
if (clientMsgId) {
|
|
2579
2821
|
this.byClientMsgId.set(clientMsgId, { text, senderUserId: this.backend.selfUserId });
|
|
2822
|
+
this.seedEditBase(clientMsgId, text, this.backend.selfUserId);
|
|
2580
2823
|
}
|
|
2581
2824
|
this.messageList.push({
|
|
2582
2825
|
id: this.publicId(receipt.serverSeq),
|
|
@@ -2590,7 +2833,9 @@ var Chat = class {
|
|
|
2590
2833
|
replyTo: resolvedReplyTo,
|
|
2591
2834
|
// Attach any tally already folded for this own-sent message (rare, but keeps
|
|
2592
2835
|
// the dangling-target invariant uniform across every append path).
|
|
2593
|
-
reactions: clientMsgId ? this.reactionFold.tally(clientMsgId) : {}
|
|
2836
|
+
reactions: clientMsgId ? this.reactionFold.tally(clientMsgId) : {},
|
|
2837
|
+
// Own-sent edits fold via edit() after the fact; new sends start unedited.
|
|
2838
|
+
edited: false
|
|
2594
2839
|
});
|
|
2595
2840
|
this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
|
|
2596
2841
|
this.emit();
|
|
@@ -2671,6 +2916,37 @@ var Chat = class {
|
|
|
2671
2916
|
});
|
|
2672
2917
|
this.recomputeReactions(message.clientMsgId);
|
|
2673
2918
|
}
|
|
2919
|
+
// ── Edit ──
|
|
2920
|
+
/** Edit an own text message (edit-by-supersession). No-op if the message isn't
|
|
2921
|
+
* editable (empty clientMsgId, or not a `text` kind). The edit folds locally
|
|
2922
|
+
* with the server receipt's `(epoch, serverSeq)` so the target's text updates
|
|
2923
|
+
* instantly; the durable echo on the next pump is a fold no-op (dedup on the
|
|
2924
|
+
* SAME wire clientMsgId). Never appends a bubble; PRESERVES the target's
|
|
2925
|
+
* reactions + reply context. Only the original author's edits count — for an own
|
|
2926
|
+
* message self IS the author, so the author-gate passes. */
|
|
2927
|
+
async edit(message, newText) {
|
|
2928
|
+
if (!message.clientMsgId || message.kind !== "text") return;
|
|
2929
|
+
const group = await this.materializeIfNeeded();
|
|
2930
|
+
const clientMsgId = mintClientMsgId();
|
|
2931
|
+
const { receipt } = await this.backend.sendEdit(group, {
|
|
2932
|
+
clientMsgId,
|
|
2933
|
+
targetClientMsgId: message.clientMsgId,
|
|
2934
|
+
newText
|
|
2935
|
+
});
|
|
2936
|
+
this.seedEditBase(message.clientMsgId, message.text, this.backend.selfUserId);
|
|
2937
|
+
this.editFold.ingest(
|
|
2938
|
+
{
|
|
2939
|
+
targetClientMsgId: message.clientMsgId,
|
|
2940
|
+
editorUserId: this.backend.selfUserId,
|
|
2941
|
+
newText,
|
|
2942
|
+
epoch: receipt.epoch,
|
|
2943
|
+
serverSeq: receipt.serverSeq,
|
|
2944
|
+
eventClientMsgId: clientMsgId
|
|
2945
|
+
},
|
|
2946
|
+
this.authorOfTarget
|
|
2947
|
+
);
|
|
2948
|
+
this.recomputeEdit(message.clientMsgId);
|
|
2949
|
+
}
|
|
2674
2950
|
};
|
|
2675
2951
|
function sameReactions(a, b) {
|
|
2676
2952
|
const ak = Object.keys(a);
|
|
@@ -2713,6 +2989,11 @@ var MessageHub = class {
|
|
|
2713
2989
|
};
|
|
2714
2990
|
}
|
|
2715
2991
|
};
|
|
2992
|
+
function decodeSenderDeviceId(sender) {
|
|
2993
|
+
if (sender.length === 0) return null;
|
|
2994
|
+
const id = decodeUtf8(sender);
|
|
2995
|
+
return id.length > 0 ? id : null;
|
|
2996
|
+
}
|
|
2716
2997
|
var MessageDeliverySource = class {
|
|
2717
2998
|
constructor(rt, engine, hub, registry, messageStore, deviceId, selfUserId) {
|
|
2718
2999
|
this.rt = rt;
|
|
@@ -2845,11 +3126,13 @@ var MessageDeliverySource = class {
|
|
|
2845
3126
|
const decoded = decodeEnvelope(received.data);
|
|
2846
3127
|
const { text, clientMsgId, replyTo } = decoded;
|
|
2847
3128
|
const isReaction = decoded.type === "reaction" && decoded.reaction != null;
|
|
3129
|
+
const isEdit = decoded.type === "edit" && decoded.edit != null;
|
|
3130
|
+
const senderDeviceId = row.sender_device_id ?? decodeSenderDeviceId(received.sender);
|
|
2848
3131
|
const stored = {
|
|
2849
3132
|
id: `${group.rfcGroupId}#${row.server_seq}`,
|
|
2850
3133
|
direction: "incoming",
|
|
2851
3134
|
text,
|
|
2852
|
-
senderDeviceId
|
|
3135
|
+
senderDeviceId,
|
|
2853
3136
|
epoch: row.epoch,
|
|
2854
3137
|
serverSeq: row.server_seq,
|
|
2855
3138
|
at: Date.now(),
|
|
@@ -2871,6 +3154,17 @@ var MessageDeliverySource = class {
|
|
|
2871
3154
|
emoji: decoded.reaction.emoji,
|
|
2872
3155
|
op: decoded.reaction.op
|
|
2873
3156
|
}
|
|
3157
|
+
} : {},
|
|
3158
|
+
// Thread the edit discriminator + new text through the persisted row so an
|
|
3159
|
+
// edit folded LIVE re-folds onto its target after a reload (the reload-parity
|
|
3160
|
+
// boundary — mirrors iOS T3). Omitted for non-edits → old rows hydrate as
|
|
3161
|
+
// `'text'`/no-edit (backward-compat).
|
|
3162
|
+
...isEdit && decoded.edit ? {
|
|
3163
|
+
envelopeType: "edit",
|
|
3164
|
+
edit: {
|
|
3165
|
+
targetClientMsgId: decoded.edit.targetClientMsgId,
|
|
3166
|
+
newText: decoded.edit.newText
|
|
3167
|
+
}
|
|
2874
3168
|
} : {}
|
|
2875
3169
|
};
|
|
2876
3170
|
try {
|
|
@@ -2882,14 +3176,15 @@ var MessageDeliverySource = class {
|
|
|
2882
3176
|
kind: "application",
|
|
2883
3177
|
group,
|
|
2884
3178
|
text,
|
|
2885
|
-
senderDeviceId
|
|
3179
|
+
senderDeviceId,
|
|
2886
3180
|
epoch: row.epoch,
|
|
2887
3181
|
serverSeq: row.server_seq,
|
|
2888
3182
|
receivedAt: /* @__PURE__ */ new Date(),
|
|
2889
3183
|
clientMsgId,
|
|
2890
3184
|
replyRef: replyTo,
|
|
2891
3185
|
envelopeType: decoded.type ?? "text",
|
|
2892
|
-
reaction: isReaction ? decoded.reaction : null
|
|
3186
|
+
reaction: isReaction ? decoded.reaction : null,
|
|
3187
|
+
edit: isEdit ? decoded.edit : null
|
|
2893
3188
|
});
|
|
2894
3189
|
return true;
|
|
2895
3190
|
}
|
|
@@ -4897,6 +5192,10 @@ var MessagingCoordinator = class {
|
|
|
4897
5192
|
const r = await this.resolve();
|
|
4898
5193
|
return r.groups.sendReaction(group, args);
|
|
4899
5194
|
}
|
|
5195
|
+
async sendEdit(group, args) {
|
|
5196
|
+
const r = await this.resolve();
|
|
5197
|
+
return r.groups.sendEdit(group, args);
|
|
5198
|
+
}
|
|
4900
5199
|
async history(group, limit, before) {
|
|
4901
5200
|
const r = await this.resolve();
|
|
4902
5201
|
const rows = await r.messageStore.history(group.rfcGroupId, limit, before);
|
|
@@ -4993,6 +5292,32 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
4993
5292
|
eventClientMsgId: s.clientMsgId ?? `${s.id}`
|
|
4994
5293
|
});
|
|
4995
5294
|
}
|
|
5295
|
+
const editFold = new EditFold();
|
|
5296
|
+
const authorByClientMsgId = /* @__PURE__ */ new Map();
|
|
5297
|
+
for (const s of rows) {
|
|
5298
|
+
if (s.envelopeType === "reaction" || s.envelopeType === "edit") continue;
|
|
5299
|
+
const cid = s.clientMsgId ?? "";
|
|
5300
|
+
if (!cid) continue;
|
|
5301
|
+
const author = s.direction === "outgoing" ? selfUserId : resolveActor?.(s.senderDeviceId ?? null);
|
|
5302
|
+
if (author != null) authorByClientMsgId.set(cid, author);
|
|
5303
|
+
}
|
|
5304
|
+
const authorOfTarget = (cid) => authorByClientMsgId.get(cid) ?? null;
|
|
5305
|
+
for (const s of rows) {
|
|
5306
|
+
if (s.envelopeType !== "edit" || !s.edit) continue;
|
|
5307
|
+
const editor = s.direction === "outgoing" ? selfUserId : resolveActor?.(s.senderDeviceId ?? null) ?? null;
|
|
5308
|
+
editFold.ingest(
|
|
5309
|
+
{
|
|
5310
|
+
targetClientMsgId: s.edit.targetClientMsgId,
|
|
5311
|
+
editorUserId: editor,
|
|
5312
|
+
newText: s.edit.newText,
|
|
5313
|
+
epoch: s.epoch,
|
|
5314
|
+
serverSeq: s.serverSeq,
|
|
5315
|
+
eventClientMsgId: s.clientMsgId ?? `${s.id}`
|
|
5316
|
+
},
|
|
5317
|
+
authorOfTarget
|
|
5318
|
+
);
|
|
5319
|
+
}
|
|
5320
|
+
editFold.reevaluateHeld(authorOfTarget);
|
|
4996
5321
|
const lookup = /* @__PURE__ */ new Map();
|
|
4997
5322
|
for (const s of rows) {
|
|
4998
5323
|
if (s.envelopeType === "reaction") continue;
|
|
@@ -5004,7 +5329,7 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
5004
5329
|
}
|
|
5005
5330
|
const out = [];
|
|
5006
5331
|
for (const s of rows) {
|
|
5007
|
-
if (s.envelopeType === "reaction") continue;
|
|
5332
|
+
if (s.envelopeType === "reaction" || s.envelopeType === "edit") continue;
|
|
5008
5333
|
const clientMsgId = s.clientMsgId ?? "";
|
|
5009
5334
|
let replyTo = null;
|
|
5010
5335
|
if (s.replyTo) {
|
|
@@ -5020,17 +5345,20 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
5020
5345
|
};
|
|
5021
5346
|
replyTo = resolveReply(ref, (id) => lookup.get(id) ?? null);
|
|
5022
5347
|
}
|
|
5348
|
+
const editText = clientMsgId ? editFold.text(clientMsgId) : null;
|
|
5349
|
+
const edited = clientMsgId ? editFold.isEdited(clientMsgId) : false;
|
|
5023
5350
|
out.push({
|
|
5024
5351
|
id: `${displayId}#${s.serverSeq}`,
|
|
5025
5352
|
kind: s.text != null ? "text" : "system",
|
|
5026
5353
|
direction: s.direction,
|
|
5027
5354
|
senderUserId: s.direction === "outgoing" ? selfUserId : null,
|
|
5028
|
-
text: s.text,
|
|
5355
|
+
text: editText ?? s.text,
|
|
5029
5356
|
serverSeq: s.serverSeq,
|
|
5030
5357
|
sentAt: new Date(s.at),
|
|
5031
5358
|
clientMsgId,
|
|
5032
5359
|
replyTo,
|
|
5033
|
-
reactions: clientMsgId ? fold.tally(clientMsgId) : {}
|
|
5360
|
+
reactions: clientMsgId ? fold.tally(clientMsgId) : {},
|
|
5361
|
+
edited
|
|
5034
5362
|
});
|
|
5035
5363
|
}
|
|
5036
5364
|
return out;
|
|
@@ -5887,7 +6215,7 @@ function localStorageSessionStorage(key = DEFAULT_KEY) {
|
|
|
5887
6215
|
}
|
|
5888
6216
|
|
|
5889
6217
|
// src/version.ts
|
|
5890
|
-
var VERSION = "1.
|
|
6218
|
+
var VERSION = "1.3.0";
|
|
5891
6219
|
|
|
5892
6220
|
// src/internal.ts
|
|
5893
6221
|
function getRuntime() {
|