@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/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { A as AnalyticsProperties, a as AuthChangeEvent, b as AuthState, c as AuthSuccess, d as AuthUser, C as Call, e as CallChangeCallback, f as CallParticipant, g as CallState, h as Chat, i as ChatBackend, j as ChatDraft, k as ChatKind, l as ChatMember, m as ChatMessage, n as ChatMessageKind, o as ChatRole, p as ChatState, q as CommsPrefs, F as FlagsView, M as MagicLinkResult, r as MessageDirection, O as OAuthExchangeResult, P as PalbeAnalytics, s as PalbeAuth, t as PalbeCalls, u as PalbeConfig, v as PalbeFlags, w as PalbeMessaging, x as PalbeOAuthConfig, y as PalbeRealtime, z as PresenceState, R as RealtimeChannel, B as RealtimeConnectionState, D as RealtimeHandler, E as RealtimePayload, G as RealtimeStatus, H as RealtimeStatusSnapshot, I as RealtimeSubscription, S as SentReceipt, U as Unsubscribe } from './analytics-facade-
|
|
1
|
+
export { A as AnalyticsProperties, a as AuthChangeEvent, b as AuthState, c as AuthSuccess, d as AuthUser, C as Call, e as CallChangeCallback, f as CallParticipant, g as CallState, h as Chat, i as ChatBackend, j as ChatDraft, k as ChatKind, l as ChatMember, m as ChatMessage, n as ChatMessageKind, o as ChatRole, p as ChatState, q as CommsPrefs, F as FlagsView, M as MagicLinkResult, r as MessageDirection, O as OAuthExchangeResult, P as PalbeAnalytics, s as PalbeAuth, t as PalbeCalls, u as PalbeConfig, v as PalbeFlags, w as PalbeMessaging, x as PalbeOAuthConfig, y as PalbeRealtime, z as PresenceState, R as RealtimeChannel, B as RealtimeConnectionState, D as RealtimeHandler, E as RealtimePayload, G as RealtimeStatus, H as RealtimeStatusSnapshot, I as RealtimeSubscription, J as ResolvedReply, S as SentReceipt, U as Unsubscribe } from './analytics-facade-B8UATgS4.cjs';
|
|
2
2
|
export { B as BackendError, a as BackendErrorKind, F as FieldError, i as isBackendError } from './errors-fDoNdTrJ.cjs';
|
|
3
|
-
export { C as CallOptions, P as PB, U as UploadOptions, a as UploadProgress, p as pb } from './pb-
|
|
3
|
+
export { C as CallOptions, P as PB, U as UploadOptions, a as UploadProgress, p as pb } from './pb-DivIiUGR.cjs';
|
|
4
4
|
export { P as PersistedSession, S as SessionStorageAdapter, l as localStorageSessionStorage, m as memorySessionStorage } from './storage-BPaeSG8K.cjs';
|
|
5
5
|
export { F as FlagValue } from './pooled-flags-Bwq4usn0.js';
|
|
6
6
|
import 'livekit-client';
|
|
@@ -207,6 +207,6 @@ interface MlsGlue {
|
|
|
207
207
|
*/
|
|
208
208
|
declare function initMls(): Promise<MlsGlue>;
|
|
209
209
|
|
|
210
|
-
declare const VERSION = "1.
|
|
210
|
+
declare const VERSION = "1.3.0";
|
|
211
211
|
|
|
212
212
|
export { VERSION, initMls };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { A as AnalyticsProperties, a as AuthChangeEvent, b as AuthState, c as AuthSuccess, d as AuthUser, C as Call, e as CallChangeCallback, f as CallParticipant, g as CallState, h as Chat, i as ChatBackend, j as ChatDraft, k as ChatKind, l as ChatMember, m as ChatMessage, n as ChatMessageKind, o as ChatRole, p as ChatState, q as CommsPrefs, F as FlagsView, M as MagicLinkResult, r as MessageDirection, O as OAuthExchangeResult, P as PalbeAnalytics, s as PalbeAuth, t as PalbeCalls, u as PalbeConfig, v as PalbeFlags, w as PalbeMessaging, x as PalbeOAuthConfig, y as PalbeRealtime, z as PresenceState, R as RealtimeChannel, B as RealtimeConnectionState, D as RealtimeHandler, E as RealtimePayload, G as RealtimeStatus, H as RealtimeStatusSnapshot, I as RealtimeSubscription, S as SentReceipt, U as Unsubscribe } from './analytics-facade-
|
|
1
|
+
export { A as AnalyticsProperties, a as AuthChangeEvent, b as AuthState, c as AuthSuccess, d as AuthUser, C as Call, e as CallChangeCallback, f as CallParticipant, g as CallState, h as Chat, i as ChatBackend, j as ChatDraft, k as ChatKind, l as ChatMember, m as ChatMessage, n as ChatMessageKind, o as ChatRole, p as ChatState, q as CommsPrefs, F as FlagsView, M as MagicLinkResult, r as MessageDirection, O as OAuthExchangeResult, P as PalbeAnalytics, s as PalbeAuth, t as PalbeCalls, u as PalbeConfig, v as PalbeFlags, w as PalbeMessaging, x as PalbeOAuthConfig, y as PalbeRealtime, z as PresenceState, R as RealtimeChannel, B as RealtimeConnectionState, D as RealtimeHandler, E as RealtimePayload, G as RealtimeStatus, H as RealtimeStatusSnapshot, I as RealtimeSubscription, J as ResolvedReply, S as SentReceipt, U as Unsubscribe } from './analytics-facade-Cq7R-VH_.js';
|
|
2
2
|
export { B as BackendError, a as BackendErrorKind, F as FieldError, i as isBackendError } from './errors-fDoNdTrJ.js';
|
|
3
|
-
export { C as CallOptions, P as PB, U as UploadOptions, a as UploadProgress, p as pb } from './pb-
|
|
3
|
+
export { C as CallOptions, P as PB, U as UploadOptions, a as UploadProgress, p as pb } from './pb-DSJiH1uV.js';
|
|
4
4
|
export { P as PersistedSession, S as SessionStorageAdapter, l as localStorageSessionStorage, m as memorySessionStorage } from './storage-BPaeSG8K.js';
|
|
5
5
|
export { F as FlagValue } from './pooled-flags-Bwq4usn0.js';
|
|
6
6
|
import 'livekit-client';
|
|
@@ -207,6 +207,6 @@ interface MlsGlue {
|
|
|
207
207
|
*/
|
|
208
208
|
declare function initMls(): Promise<MlsGlue>;
|
|
209
209
|
|
|
210
|
-
declare const VERSION = "1.
|
|
210
|
+
declare const VERSION = "1.3.0";
|
|
211
211
|
|
|
212
212
|
export { VERSION, initMls };
|
package/dist/index.js
CHANGED
package/dist/internal.cjs
CHANGED
|
@@ -2612,6 +2612,88 @@ var PalbeFlags = class {
|
|
|
2612
2612
|
}
|
|
2613
2613
|
};
|
|
2614
2614
|
|
|
2615
|
+
// src/messaging/edit-fold.ts
|
|
2616
|
+
function orderLt(aEpoch, aSeq, bEpoch, bSeq) {
|
|
2617
|
+
if (aEpoch !== bEpoch) return aEpoch < bEpoch;
|
|
2618
|
+
return aSeq < bSeq;
|
|
2619
|
+
}
|
|
2620
|
+
function orderEq(aEpoch, aSeq, bEpoch, bSeq) {
|
|
2621
|
+
return aEpoch === bEpoch && aSeq === bSeq;
|
|
2622
|
+
}
|
|
2623
|
+
var EditFold = class {
|
|
2624
|
+
// target → winning edit state
|
|
2625
|
+
states = /* @__PURE__ */ new Map();
|
|
2626
|
+
// dedup of real wire events that reached (and were resolvable enough to evaluate at) the fold
|
|
2627
|
+
seenEvents = /* @__PURE__ */ new Set();
|
|
2628
|
+
// events parked because target/author or sender was unresolved at ingest time
|
|
2629
|
+
held = [];
|
|
2630
|
+
// targets that have had ≥1 valid edit applied (write-once)
|
|
2631
|
+
editedTargets = /* @__PURE__ */ new Set();
|
|
2632
|
+
/**
|
|
2633
|
+
* Ingest one edit. `authorOfTarget` resolves the target message's AUTHOR userId
|
|
2634
|
+
* (null = target unknown/dangling → HOLD).
|
|
2635
|
+
*/
|
|
2636
|
+
ingest(e, authorOfTarget) {
|
|
2637
|
+
const author = authorOfTarget(e.targetClientMsgId);
|
|
2638
|
+
if (author === null) {
|
|
2639
|
+
this.holdIfNew(e);
|
|
2640
|
+
return;
|
|
2641
|
+
}
|
|
2642
|
+
if (e.editorUserId === null) {
|
|
2643
|
+
this.holdIfNew(e);
|
|
2644
|
+
return;
|
|
2645
|
+
}
|
|
2646
|
+
if (e.editorUserId !== author) return;
|
|
2647
|
+
if (this.seenEvents.has(e.eventClientMsgId)) return;
|
|
2648
|
+
this.seenEvents.add(e.eventClientMsgId);
|
|
2649
|
+
const prev = this.states.get(e.targetClientMsgId);
|
|
2650
|
+
if (prev !== void 0) {
|
|
2651
|
+
if (orderLt(e.epoch, e.serverSeq, prev.orderEpoch, prev.orderSeq)) return;
|
|
2652
|
+
if (orderEq(e.epoch, e.serverSeq, prev.orderEpoch, prev.orderSeq) && e.eventClientMsgId <= prev.lastEventId) {
|
|
2653
|
+
return;
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
this.states.set(e.targetClientMsgId, {
|
|
2657
|
+
orderEpoch: e.epoch,
|
|
2658
|
+
orderSeq: e.serverSeq,
|
|
2659
|
+
lastEventId: e.eventClientMsgId,
|
|
2660
|
+
text: e.newText
|
|
2661
|
+
});
|
|
2662
|
+
this.editedTargets.add(e.targetClientMsgId);
|
|
2663
|
+
}
|
|
2664
|
+
/**
|
|
2665
|
+
* Park an event for later re-attempt, deduping held re-deliveries by
|
|
2666
|
+
* eventClientMsgId so a repeatedly-delivered unresolvable edit is held exactly
|
|
2667
|
+
* once (and never double-applies when it finally resolves on reevaluate).
|
|
2668
|
+
*/
|
|
2669
|
+
holdIfNew(e) {
|
|
2670
|
+
if (this.held.some((h) => h.eventClientMsgId === e.eventClientMsgId)) return;
|
|
2671
|
+
this.held.push(e);
|
|
2672
|
+
}
|
|
2673
|
+
/** The winning edit text for a target, or null if no valid edit has applied. */
|
|
2674
|
+
text(targetClientMsgId) {
|
|
2675
|
+
return this.states.get(targetClientMsgId)?.text ?? null;
|
|
2676
|
+
}
|
|
2677
|
+
/** Write-once: true once any valid edit applied to the target. */
|
|
2678
|
+
isEdited(targetClientMsgId) {
|
|
2679
|
+
return this.editedTargets.has(targetClientMsgId);
|
|
2680
|
+
}
|
|
2681
|
+
/**
|
|
2682
|
+
* Re-run HELD edits when the roster/target newly resolves (call on member/roster
|
|
2683
|
+
* change and when a target message arrives). Clears `held` and re-ingests each
|
|
2684
|
+
* event with the fresh `authorOfTarget` — events that still don't resolve are
|
|
2685
|
+
* simply re-held; events that now resolve fold via the normal LWW path.
|
|
2686
|
+
* Idempotent: re-ingest is deduped by `seenEvents` (applied events) and by
|
|
2687
|
+
* `holdIfNew` (still-held events), so reevaluating repeatedly can neither
|
|
2688
|
+
* double-apply nor lose an edit.
|
|
2689
|
+
*/
|
|
2690
|
+
reevaluateHeld(authorOfTarget) {
|
|
2691
|
+
const pending = this.held;
|
|
2692
|
+
this.held = [];
|
|
2693
|
+
for (const e of pending) this.ingest(e, authorOfTarget);
|
|
2694
|
+
}
|
|
2695
|
+
};
|
|
2696
|
+
|
|
2615
2697
|
// src/messaging/util.ts
|
|
2616
2698
|
function toBase64(bytes) {
|
|
2617
2699
|
if (typeof Buffer !== "undefined") {
|
|
@@ -2751,6 +2833,17 @@ async function listDevices(rt, userId) {
|
|
|
2751
2833
|
}
|
|
2752
2834
|
|
|
2753
2835
|
// src/messaging/group-messaging.ts
|
|
2836
|
+
function encodeEdit(args) {
|
|
2837
|
+
return encodeUtf8(
|
|
2838
|
+
JSON.stringify({
|
|
2839
|
+
v: 1,
|
|
2840
|
+
type: "edit",
|
|
2841
|
+
client_msg_id: args.clientMsgId,
|
|
2842
|
+
target_client_msg_id: args.targetClientMsgId,
|
|
2843
|
+
new_text: args.newText
|
|
2844
|
+
})
|
|
2845
|
+
);
|
|
2846
|
+
}
|
|
2754
2847
|
function encodeReaction(args) {
|
|
2755
2848
|
return encodeUtf8(
|
|
2756
2849
|
JSON.stringify({
|
|
@@ -2790,6 +2883,18 @@ function decodeEnvelope(bytes) {
|
|
|
2790
2883
|
}
|
|
2791
2884
|
};
|
|
2792
2885
|
}
|
|
2886
|
+
if (typeof o === "object" && o !== null && o.type === "edit") {
|
|
2887
|
+
return {
|
|
2888
|
+
type: "edit",
|
|
2889
|
+
text: null,
|
|
2890
|
+
clientMsgId: o.client_msg_id ?? "",
|
|
2891
|
+
replyTo: null,
|
|
2892
|
+
edit: {
|
|
2893
|
+
targetClientMsgId: o.target_client_msg_id ?? "",
|
|
2894
|
+
newText: o.new_text ?? ""
|
|
2895
|
+
}
|
|
2896
|
+
};
|
|
2897
|
+
}
|
|
2793
2898
|
if (typeof o === "object" && o !== null && o.type === "text" && typeof o.v === "number") {
|
|
2794
2899
|
return {
|
|
2795
2900
|
type: "text",
|
|
@@ -3104,6 +3209,54 @@ var GroupMessaging = class {
|
|
|
3104
3209
|
clientMsgId: args.clientMsgId
|
|
3105
3210
|
};
|
|
3106
3211
|
}
|
|
3212
|
+
/** Send an edit (edit-by-supersession on a target message). Encrypts a
|
|
3213
|
+
* `type:'edit'` envelope at the current epoch and sends through the SAME MLS
|
|
3214
|
+
* application path as `sendText` (the server stays blind — an edit is just
|
|
3215
|
+
* another application message). Persists the outgoing edit row so it re-folds
|
|
3216
|
+
* onto its target's text after a reload (the own-send half of the reload
|
|
3217
|
+
* parity). NEVER rebases (epoch-bound like any application message). */
|
|
3218
|
+
async sendEdit(group, args) {
|
|
3219
|
+
const plaintext = encodeEdit({
|
|
3220
|
+
clientMsgId: args.clientMsgId,
|
|
3221
|
+
targetClientMsgId: args.targetClientMsgId,
|
|
3222
|
+
newText: args.newText
|
|
3223
|
+
});
|
|
3224
|
+
const ct = await this.engine.encryptApplication(fromBase64(group.rfcGroupId), plaintext);
|
|
3225
|
+
const body = {
|
|
3226
|
+
ciphertext_b64: toBase64(ct),
|
|
3227
|
+
client_idem_key: randomId()
|
|
3228
|
+
};
|
|
3229
|
+
const wire = await palbeRequest(
|
|
3230
|
+
this.rt,
|
|
3231
|
+
"POST",
|
|
3232
|
+
MessagingPaths.groupMessages(group.displayId),
|
|
3233
|
+
{ body }
|
|
3234
|
+
);
|
|
3235
|
+
const stored = {
|
|
3236
|
+
id: `${group.rfcGroupId}#${wire.server_seq}`,
|
|
3237
|
+
direction: "outgoing",
|
|
3238
|
+
text: null,
|
|
3239
|
+
senderDeviceId: this.selfDeviceId,
|
|
3240
|
+
epoch: wire.epoch,
|
|
3241
|
+
serverSeq: wire.server_seq,
|
|
3242
|
+
at: Date.now(),
|
|
3243
|
+
clientMsgId: args.clientMsgId,
|
|
3244
|
+
replyTo: null,
|
|
3245
|
+
envelopeType: "edit",
|
|
3246
|
+
edit: {
|
|
3247
|
+
targetClientMsgId: args.targetClientMsgId,
|
|
3248
|
+
newText: args.newText
|
|
3249
|
+
}
|
|
3250
|
+
};
|
|
3251
|
+
try {
|
|
3252
|
+
await this.messageStore.append(group.rfcGroupId, stored);
|
|
3253
|
+
} catch {
|
|
3254
|
+
}
|
|
3255
|
+
return {
|
|
3256
|
+
receipt: { serverSeq: wire.server_seq, epoch: wire.epoch },
|
|
3257
|
+
clientMsgId: args.clientMsgId
|
|
3258
|
+
};
|
|
3259
|
+
}
|
|
3107
3260
|
// ── The rebase loop ──
|
|
3108
3261
|
async commitWithRebase(rfcGroupId, build) {
|
|
3109
3262
|
const gidBytes = fromBase64(rfcGroupId);
|
|
@@ -3240,6 +3393,14 @@ var Chat = class {
|
|
|
3240
3393
|
byClientMsgId = /* @__PURE__ */ new Map();
|
|
3241
3394
|
/** The single authoritative reaction fold for this chat (live + own-send + history). */
|
|
3242
3395
|
reactionFold = new ReactionFold();
|
|
3396
|
+
/** The single authoritative edit fold for this chat (live + own-send + history). */
|
|
3397
|
+
editFold = new EditFold();
|
|
3398
|
+
/** Per-target BASE (original) text, so the rendered text is `edit ?? original`.
|
|
3399
|
+
* Seeded once per target (write-once base) so a 2nd own edit can't corrupt it. */
|
|
3400
|
+
originalTextByClientMsgId = /* @__PURE__ */ new Map();
|
|
3401
|
+
/** Per-target AUTHOR userId — the EditFold author-gate input (filled at bubble
|
|
3402
|
+
* projection time from senderUserId; '' = resolved-but-unknown peer). */
|
|
3403
|
+
authorByClientMsgId = /* @__PURE__ */ new Map();
|
|
3243
3404
|
loadedEarliestSeq = null;
|
|
3244
3405
|
historyLoaded = false;
|
|
3245
3406
|
wired = false;
|
|
@@ -3340,13 +3501,17 @@ var Chat = class {
|
|
|
3340
3501
|
senderUserId: m.senderUserId ?? ""
|
|
3341
3502
|
});
|
|
3342
3503
|
}
|
|
3504
|
+
if (m.clientMsgId) {
|
|
3505
|
+
this.seedEditBase(m.clientMsgId, m.text, m.senderUserId ?? "");
|
|
3506
|
+
}
|
|
3343
3507
|
}
|
|
3508
|
+
this.editFold.reevaluateHeld(this.authorOfTarget);
|
|
3344
3509
|
for (const m of incoming) {
|
|
3345
3510
|
if (m.serverSeq <= 0) continue;
|
|
3346
3511
|
const key = this.internalKey(m.serverSeq);
|
|
3347
3512
|
if (this.seenKeys.has(key)) continue;
|
|
3348
3513
|
this.seenKeys.add(key);
|
|
3349
|
-
this.messageList.push(this.applyReactionTally(m));
|
|
3514
|
+
this.messageList.push(this.applyEditOverlay(this.applyReactionTally(m)));
|
|
3350
3515
|
changed = true;
|
|
3351
3516
|
this.loadedEarliestSeq = Math.min(this.loadedEarliestSeq ?? m.serverSeq, m.serverSeq);
|
|
3352
3517
|
}
|
|
@@ -3387,6 +3552,22 @@ var Chat = class {
|
|
|
3387
3552
|
}
|
|
3388
3553
|
return;
|
|
3389
3554
|
}
|
|
3555
|
+
if (incoming.envelopeType === "edit" && incoming.edit) {
|
|
3556
|
+
const editorUserId = direction === "outgoing" ? this.backend.selfUserId : senderUser;
|
|
3557
|
+
this.editFold.ingest(
|
|
3558
|
+
{
|
|
3559
|
+
targetClientMsgId: incoming.edit.targetClientMsgId,
|
|
3560
|
+
editorUserId,
|
|
3561
|
+
newText: incoming.edit.newText,
|
|
3562
|
+
epoch: incoming.epoch,
|
|
3563
|
+
serverSeq: incoming.serverSeq,
|
|
3564
|
+
eventClientMsgId: incoming.clientMsgId
|
|
3565
|
+
},
|
|
3566
|
+
this.authorOfTarget
|
|
3567
|
+
);
|
|
3568
|
+
this.recomputeEdit(incoming.edit.targetClientMsgId);
|
|
3569
|
+
return;
|
|
3570
|
+
}
|
|
3390
3571
|
const incomingClientMsgId = incoming.clientMsgId;
|
|
3391
3572
|
const incomingReplyRef = incoming.replyRef;
|
|
3392
3573
|
let resolvedReplyTo = null;
|
|
@@ -3405,7 +3586,9 @@ var Chat = class {
|
|
|
3405
3586
|
replyTo: resolvedReplyTo,
|
|
3406
3587
|
// Attach any tally already folded for this message (a reaction that arrived
|
|
3407
3588
|
// BEFORE its target — the dangling case — renders the moment the target lands).
|
|
3408
|
-
reactions: incomingClientMsgId ? this.reactionFold.tally(incomingClientMsgId) : {}
|
|
3589
|
+
reactions: incomingClientMsgId ? this.reactionFold.tally(incomingClientMsgId) : {},
|
|
3590
|
+
// Default false; applyEditOverlay below folds any edit that arrived first.
|
|
3591
|
+
edited: false
|
|
3409
3592
|
};
|
|
3410
3593
|
if (incomingClientMsgId && incoming.text !== null) {
|
|
3411
3594
|
this.byClientMsgId.set(incomingClientMsgId, {
|
|
@@ -3413,7 +3596,11 @@ var Chat = class {
|
|
|
3413
3596
|
senderUserId: senderUser ?? ""
|
|
3414
3597
|
});
|
|
3415
3598
|
}
|
|
3416
|
-
|
|
3599
|
+
if (incomingClientMsgId) {
|
|
3600
|
+
this.seedEditBase(incomingClientMsgId, incoming.text, senderUser);
|
|
3601
|
+
this.editFold.reevaluateHeld(this.authorOfTarget);
|
|
3602
|
+
}
|
|
3603
|
+
this.messageList.push(this.applyEditOverlay(msg));
|
|
3417
3604
|
this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
|
|
3418
3605
|
this.loadedEarliestSeq = Math.min(
|
|
3419
3606
|
this.loadedEarliestSeq ?? incoming.serverSeq,
|
|
@@ -3421,6 +3608,19 @@ var Chat = class {
|
|
|
3421
3608
|
);
|
|
3422
3609
|
this.emit();
|
|
3423
3610
|
}
|
|
3611
|
+
/** The EditFold author-gate input: the target message's resolved author userId
|
|
3612
|
+
* (null = target unknown/dangling → the fold HOLDs). Captured as a bound arrow
|
|
3613
|
+
* so it can be passed to the pure EditFold. */
|
|
3614
|
+
authorOfTarget = (targetClientMsgId) => this.authorByClientMsgId.get(targetClientMsgId) ?? null;
|
|
3615
|
+
/** Seed the per-target base text + author for the edit fold. Base is write-once
|
|
3616
|
+
* (a later own/peer edit must not overwrite the original we render against). The
|
|
3617
|
+
* author is (re)recorded whenever a non-empty resolution is available. */
|
|
3618
|
+
seedEditBase(clientMsgId, text, author) {
|
|
3619
|
+
if (!this.originalTextByClientMsgId.has(clientMsgId)) {
|
|
3620
|
+
this.originalTextByClientMsgId.set(clientMsgId, text);
|
|
3621
|
+
}
|
|
3622
|
+
if (author !== null) this.authorByClientMsgId.set(clientMsgId, author);
|
|
3623
|
+
}
|
|
3424
3624
|
/**
|
|
3425
3625
|
* Rebuild the target message's `reactions` from the authoritative fold and
|
|
3426
3626
|
* re-emit. No-op when the target isn't present yet (its tally is attached the
|
|
@@ -3450,6 +3650,46 @@ var Chat = class {
|
|
|
3450
3650
|
if (sameReactions(m.reactions, tally)) return m;
|
|
3451
3651
|
return { ...m, reactions: tally };
|
|
3452
3652
|
}
|
|
3653
|
+
/**
|
|
3654
|
+
* Rebuild the target message's rendered `text` + `edited` flag from the
|
|
3655
|
+
* authoritative edit fold and re-emit, PRESERVING `.reactions` and `.replyTo`
|
|
3656
|
+
* (the reaction-polish lesson — never clobber). text = `editFold.text(cid) ??
|
|
3657
|
+
* base`; base is the seeded original so a forged/ignored edit leaves it intact.
|
|
3658
|
+
* No-op when the target isn't present yet (the fold already recorded it; the
|
|
3659
|
+
* overlay applies the moment the target lands) or when unchanged.
|
|
3660
|
+
*/
|
|
3661
|
+
recomputeEdit(targetClientMsgId) {
|
|
3662
|
+
if (!targetClientMsgId) return;
|
|
3663
|
+
const editText = this.editFold.text(targetClientMsgId);
|
|
3664
|
+
const foldEdited = this.editFold.isEdited(targetClientMsgId);
|
|
3665
|
+
let changed = false;
|
|
3666
|
+
this.messageList = this.messageList.map((m) => {
|
|
3667
|
+
if (m.clientMsgId !== targetClientMsgId) return m;
|
|
3668
|
+
const base = this.originalTextByClientMsgId.has(targetClientMsgId) ? this.originalTextByClientMsgId.get(targetClientMsgId) ?? null : m.text;
|
|
3669
|
+
const text = editText ?? base;
|
|
3670
|
+
const edited = foldEdited || m.edited;
|
|
3671
|
+
if (m.text === text && m.edited === edited) return m;
|
|
3672
|
+
changed = true;
|
|
3673
|
+
return { ...m, text, edited };
|
|
3674
|
+
});
|
|
3675
|
+
if (changed) this.emit();
|
|
3676
|
+
}
|
|
3677
|
+
/**
|
|
3678
|
+
* Overlay the authoritative edit fold's winning text + flag onto a message as it
|
|
3679
|
+
* is appended/merged. The fold WINS when it has an edit for this target;
|
|
3680
|
+
* otherwise the upstream `text`/`edited` (e.g. the coordinator's page-local
|
|
3681
|
+
* history fold) is preserved. PRESERVES reactions + replyTo.
|
|
3682
|
+
*/
|
|
3683
|
+
applyEditOverlay(m) {
|
|
3684
|
+
if (!m.clientMsgId) return m;
|
|
3685
|
+
const editText = this.editFold.text(m.clientMsgId);
|
|
3686
|
+
const foldEdited = this.editFold.isEdited(m.clientMsgId);
|
|
3687
|
+
if (editText === null && !foldEdited) return m;
|
|
3688
|
+
const text = editText ?? m.text;
|
|
3689
|
+
const edited = foldEdited || m.edited;
|
|
3690
|
+
if (m.text === text && m.edited === edited) return m;
|
|
3691
|
+
return { ...m, text, edited };
|
|
3692
|
+
}
|
|
3453
3693
|
/** @internal — called by the backend's conv subscription. */
|
|
3454
3694
|
applyConv(event, payload) {
|
|
3455
3695
|
const userId = typeof payload.user_id === "string" ? payload.user_id : null;
|
|
@@ -3504,6 +3744,8 @@ var Chat = class {
|
|
|
3504
3744
|
this.memberCache = m;
|
|
3505
3745
|
this.emit();
|
|
3506
3746
|
}
|
|
3747
|
+
this.editFold.reevaluateHeld(this.authorOfTarget);
|
|
3748
|
+
for (const cid of this.originalTextByClientMsgId.keys()) this.recomputeEdit(cid);
|
|
3507
3749
|
}
|
|
3508
3750
|
seedMembersFromGroup(group) {
|
|
3509
3751
|
const seed = [
|
|
@@ -3582,6 +3824,7 @@ var Chat = class {
|
|
|
3582
3824
|
this.seenKeys.add(key);
|
|
3583
3825
|
if (clientMsgId) {
|
|
3584
3826
|
this.byClientMsgId.set(clientMsgId, { text, senderUserId: this.backend.selfUserId });
|
|
3827
|
+
this.seedEditBase(clientMsgId, text, this.backend.selfUserId);
|
|
3585
3828
|
}
|
|
3586
3829
|
this.messageList.push({
|
|
3587
3830
|
id: this.publicId(receipt.serverSeq),
|
|
@@ -3595,7 +3838,9 @@ var Chat = class {
|
|
|
3595
3838
|
replyTo: resolvedReplyTo,
|
|
3596
3839
|
// Attach any tally already folded for this own-sent message (rare, but keeps
|
|
3597
3840
|
// the dangling-target invariant uniform across every append path).
|
|
3598
|
-
reactions: clientMsgId ? this.reactionFold.tally(clientMsgId) : {}
|
|
3841
|
+
reactions: clientMsgId ? this.reactionFold.tally(clientMsgId) : {},
|
|
3842
|
+
// Own-sent edits fold via edit() after the fact; new sends start unedited.
|
|
3843
|
+
edited: false
|
|
3599
3844
|
});
|
|
3600
3845
|
this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
|
|
3601
3846
|
this.emit();
|
|
@@ -3676,6 +3921,37 @@ var Chat = class {
|
|
|
3676
3921
|
});
|
|
3677
3922
|
this.recomputeReactions(message.clientMsgId);
|
|
3678
3923
|
}
|
|
3924
|
+
// ── Edit ──
|
|
3925
|
+
/** Edit an own text message (edit-by-supersession). No-op if the message isn't
|
|
3926
|
+
* editable (empty clientMsgId, or not a `text` kind). The edit folds locally
|
|
3927
|
+
* with the server receipt's `(epoch, serverSeq)` so the target's text updates
|
|
3928
|
+
* instantly; the durable echo on the next pump is a fold no-op (dedup on the
|
|
3929
|
+
* SAME wire clientMsgId). Never appends a bubble; PRESERVES the target's
|
|
3930
|
+
* reactions + reply context. Only the original author's edits count — for an own
|
|
3931
|
+
* message self IS the author, so the author-gate passes. */
|
|
3932
|
+
async edit(message, newText) {
|
|
3933
|
+
if (!message.clientMsgId || message.kind !== "text") return;
|
|
3934
|
+
const group = await this.materializeIfNeeded();
|
|
3935
|
+
const clientMsgId = mintClientMsgId();
|
|
3936
|
+
const { receipt } = await this.backend.sendEdit(group, {
|
|
3937
|
+
clientMsgId,
|
|
3938
|
+
targetClientMsgId: message.clientMsgId,
|
|
3939
|
+
newText
|
|
3940
|
+
});
|
|
3941
|
+
this.seedEditBase(message.clientMsgId, message.text, this.backend.selfUserId);
|
|
3942
|
+
this.editFold.ingest(
|
|
3943
|
+
{
|
|
3944
|
+
targetClientMsgId: message.clientMsgId,
|
|
3945
|
+
editorUserId: this.backend.selfUserId,
|
|
3946
|
+
newText,
|
|
3947
|
+
epoch: receipt.epoch,
|
|
3948
|
+
serverSeq: receipt.serverSeq,
|
|
3949
|
+
eventClientMsgId: clientMsgId
|
|
3950
|
+
},
|
|
3951
|
+
this.authorOfTarget
|
|
3952
|
+
);
|
|
3953
|
+
this.recomputeEdit(message.clientMsgId);
|
|
3954
|
+
}
|
|
3679
3955
|
};
|
|
3680
3956
|
function sameReactions(a, b) {
|
|
3681
3957
|
const ak = Object.keys(a);
|
|
@@ -3855,6 +4131,7 @@ var MessageDeliverySource = class {
|
|
|
3855
4131
|
const decoded = decodeEnvelope(received.data);
|
|
3856
4132
|
const { text, clientMsgId, replyTo } = decoded;
|
|
3857
4133
|
const isReaction = decoded.type === "reaction" && decoded.reaction != null;
|
|
4134
|
+
const isEdit = decoded.type === "edit" && decoded.edit != null;
|
|
3858
4135
|
const senderDeviceId = row.sender_device_id ?? decodeSenderDeviceId(received.sender);
|
|
3859
4136
|
const stored = {
|
|
3860
4137
|
id: `${group.rfcGroupId}#${row.server_seq}`,
|
|
@@ -3882,6 +4159,17 @@ var MessageDeliverySource = class {
|
|
|
3882
4159
|
emoji: decoded.reaction.emoji,
|
|
3883
4160
|
op: decoded.reaction.op
|
|
3884
4161
|
}
|
|
4162
|
+
} : {},
|
|
4163
|
+
// Thread the edit discriminator + new text through the persisted row so an
|
|
4164
|
+
// edit folded LIVE re-folds onto its target after a reload (the reload-parity
|
|
4165
|
+
// boundary — mirrors iOS T3). Omitted for non-edits → old rows hydrate as
|
|
4166
|
+
// `'text'`/no-edit (backward-compat).
|
|
4167
|
+
...isEdit && decoded.edit ? {
|
|
4168
|
+
envelopeType: "edit",
|
|
4169
|
+
edit: {
|
|
4170
|
+
targetClientMsgId: decoded.edit.targetClientMsgId,
|
|
4171
|
+
newText: decoded.edit.newText
|
|
4172
|
+
}
|
|
3885
4173
|
} : {}
|
|
3886
4174
|
};
|
|
3887
4175
|
try {
|
|
@@ -3900,7 +4188,8 @@ var MessageDeliverySource = class {
|
|
|
3900
4188
|
clientMsgId,
|
|
3901
4189
|
replyRef: replyTo,
|
|
3902
4190
|
envelopeType: decoded.type ?? "text",
|
|
3903
|
-
reaction: isReaction ? decoded.reaction : null
|
|
4191
|
+
reaction: isReaction ? decoded.reaction : null,
|
|
4192
|
+
edit: isEdit ? decoded.edit : null
|
|
3904
4193
|
});
|
|
3905
4194
|
return true;
|
|
3906
4195
|
}
|
|
@@ -5908,6 +6197,10 @@ var MessagingCoordinator = class {
|
|
|
5908
6197
|
const r = await this.resolve();
|
|
5909
6198
|
return r.groups.sendReaction(group, args);
|
|
5910
6199
|
}
|
|
6200
|
+
async sendEdit(group, args) {
|
|
6201
|
+
const r = await this.resolve();
|
|
6202
|
+
return r.groups.sendEdit(group, args);
|
|
6203
|
+
}
|
|
5911
6204
|
async history(group, limit, before) {
|
|
5912
6205
|
const r = await this.resolve();
|
|
5913
6206
|
const rows = await r.messageStore.history(group.rfcGroupId, limit, before);
|
|
@@ -6004,6 +6297,32 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
6004
6297
|
eventClientMsgId: s.clientMsgId ?? `${s.id}`
|
|
6005
6298
|
});
|
|
6006
6299
|
}
|
|
6300
|
+
const editFold = new EditFold();
|
|
6301
|
+
const authorByClientMsgId = /* @__PURE__ */ new Map();
|
|
6302
|
+
for (const s of rows) {
|
|
6303
|
+
if (s.envelopeType === "reaction" || s.envelopeType === "edit") continue;
|
|
6304
|
+
const cid = s.clientMsgId ?? "";
|
|
6305
|
+
if (!cid) continue;
|
|
6306
|
+
const author = s.direction === "outgoing" ? selfUserId : resolveActor?.(s.senderDeviceId ?? null);
|
|
6307
|
+
if (author != null) authorByClientMsgId.set(cid, author);
|
|
6308
|
+
}
|
|
6309
|
+
const authorOfTarget = (cid) => authorByClientMsgId.get(cid) ?? null;
|
|
6310
|
+
for (const s of rows) {
|
|
6311
|
+
if (s.envelopeType !== "edit" || !s.edit) continue;
|
|
6312
|
+
const editor = s.direction === "outgoing" ? selfUserId : resolveActor?.(s.senderDeviceId ?? null) ?? null;
|
|
6313
|
+
editFold.ingest(
|
|
6314
|
+
{
|
|
6315
|
+
targetClientMsgId: s.edit.targetClientMsgId,
|
|
6316
|
+
editorUserId: editor,
|
|
6317
|
+
newText: s.edit.newText,
|
|
6318
|
+
epoch: s.epoch,
|
|
6319
|
+
serverSeq: s.serverSeq,
|
|
6320
|
+
eventClientMsgId: s.clientMsgId ?? `${s.id}`
|
|
6321
|
+
},
|
|
6322
|
+
authorOfTarget
|
|
6323
|
+
);
|
|
6324
|
+
}
|
|
6325
|
+
editFold.reevaluateHeld(authorOfTarget);
|
|
6007
6326
|
const lookup = /* @__PURE__ */ new Map();
|
|
6008
6327
|
for (const s of rows) {
|
|
6009
6328
|
if (s.envelopeType === "reaction") continue;
|
|
@@ -6015,7 +6334,7 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
6015
6334
|
}
|
|
6016
6335
|
const out = [];
|
|
6017
6336
|
for (const s of rows) {
|
|
6018
|
-
if (s.envelopeType === "reaction") continue;
|
|
6337
|
+
if (s.envelopeType === "reaction" || s.envelopeType === "edit") continue;
|
|
6019
6338
|
const clientMsgId = s.clientMsgId ?? "";
|
|
6020
6339
|
let replyTo = null;
|
|
6021
6340
|
if (s.replyTo) {
|
|
@@ -6031,17 +6350,20 @@ function projectHistory(displayId, rows, selfUserId, resolveActor) {
|
|
|
6031
6350
|
};
|
|
6032
6351
|
replyTo = resolveReply(ref, (id) => lookup.get(id) ?? null);
|
|
6033
6352
|
}
|
|
6353
|
+
const editText = clientMsgId ? editFold.text(clientMsgId) : null;
|
|
6354
|
+
const edited = clientMsgId ? editFold.isEdited(clientMsgId) : false;
|
|
6034
6355
|
out.push({
|
|
6035
6356
|
id: `${displayId}#${s.serverSeq}`,
|
|
6036
6357
|
kind: s.text != null ? "text" : "system",
|
|
6037
6358
|
direction: s.direction,
|
|
6038
6359
|
senderUserId: s.direction === "outgoing" ? selfUserId : null,
|
|
6039
|
-
text: s.text,
|
|
6360
|
+
text: editText ?? s.text,
|
|
6040
6361
|
serverSeq: s.serverSeq,
|
|
6041
6362
|
sentAt: new Date(s.at),
|
|
6042
6363
|
clientMsgId,
|
|
6043
6364
|
replyTo,
|
|
6044
|
-
reactions: clientMsgId ? fold.tally(clientMsgId) : {}
|
|
6365
|
+
reactions: clientMsgId ? fold.tally(clientMsgId) : {},
|
|
6366
|
+
edited
|
|
6045
6367
|
});
|
|
6046
6368
|
}
|
|
6047
6369
|
return out;
|
|
@@ -6779,7 +7101,7 @@ function defaultSessionStorage(key) {
|
|
|
6779
7101
|
}
|
|
6780
7102
|
|
|
6781
7103
|
// src/version.ts
|
|
6782
|
-
var VERSION = "1.
|
|
7104
|
+
var VERSION = "1.3.0";
|
|
6783
7105
|
|
|
6784
7106
|
// src/runtime.ts
|
|
6785
7107
|
function buildRuntime(config) {
|