@palbase/web 1.1.1 → 1.2.1

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/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-CAKBIH_U.cjs';
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-Ct3A1zop.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-DioxNuEV.cjs';
3
+ export { C as CallOptions, P as PB, U as UploadOptions, a as UploadProgress, p as pb } from './pb-BlIfgBG-.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.1.1";
210
+ declare const VERSION = "1.2.1";
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-DLH-KivI.js';
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-C93tr7dA.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-HegMSSk-.js';
3
+ export { C as CallOptions, P as PB, U as UploadOptions, a as UploadProgress, p as pb } from './pb-BtYdWClg.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.1.1";
210
+ declare const VERSION = "1.2.1";
211
211
 
212
212
  export { VERSION, initMls };
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  localStorageSessionStorage,
14
14
  memorySessionStorage,
15
15
  pb
16
- } from "./chunk-EMQGOKW6.js";
16
+ } from "./chunk-KJXRY4S3.js";
17
17
  export {
18
18
  BackendError,
19
19
  PalbeAnalytics,
package/dist/internal.cjs CHANGED
@@ -2751,6 +2751,18 @@ async function listDevices(rt, userId) {
2751
2751
  }
2752
2752
 
2753
2753
  // src/messaging/group-messaging.ts
2754
+ function encodeReaction(args) {
2755
+ return encodeUtf8(
2756
+ JSON.stringify({
2757
+ v: 1,
2758
+ type: "reaction",
2759
+ client_msg_id: args.clientMsgId,
2760
+ target_client_msg_id: args.targetClientMsgId,
2761
+ emoji: args.emoji,
2762
+ op: args.op
2763
+ })
2764
+ );
2765
+ }
2754
2766
  function encodeEnvelope(args) {
2755
2767
  const env = {
2756
2768
  v: 1,
@@ -2765,18 +2777,32 @@ function decodeEnvelope(bytes) {
2765
2777
  const s = decodeUtf8(bytes);
2766
2778
  try {
2767
2779
  const o = JSON.parse(s);
2780
+ if (typeof o === "object" && o !== null && o.type === "reaction") {
2781
+ return {
2782
+ type: "reaction",
2783
+ text: null,
2784
+ clientMsgId: o.client_msg_id ?? "",
2785
+ replyTo: null,
2786
+ reaction: {
2787
+ targetClientMsgId: o.target_client_msg_id ?? "",
2788
+ emoji: o.emoji ?? "",
2789
+ op: o.op ?? "add"
2790
+ }
2791
+ };
2792
+ }
2768
2793
  if (typeof o === "object" && o !== null && o.type === "text" && typeof o.v === "number") {
2769
2794
  return {
2795
+ type: "text",
2770
2796
  text: o.text ?? null,
2771
2797
  clientMsgId: o.client_msg_id ?? "",
2772
2798
  replyTo: o.reply_to ?? null
2773
2799
  };
2774
2800
  }
2775
2801
  if (typeof o === "object" && o !== null && o.type === "text") {
2776
- return { text: o.text ?? null, clientMsgId: "", replyTo: null };
2802
+ return { type: "text", text: o.text ?? null, clientMsgId: "", replyTo: null };
2777
2803
  }
2778
2804
  if (typeof o === "object" && o !== null && o.type === "media")
2779
- return { text: null, clientMsgId: "", replyTo: null };
2805
+ return { type: "media", text: null, clientMsgId: "", replyTo: null };
2780
2806
  } catch {
2781
2807
  }
2782
2808
  return { text: s, clientMsgId: "", replyTo: null };
@@ -3028,6 +3054,56 @@ var GroupMessaging = class {
3028
3054
  }
3029
3055
  return { receipt: { serverSeq: wire.server_seq, epoch: wire.epoch }, clientMsgId };
3030
3056
  }
3057
+ /** Send a reaction (add/remove of an emoji on a target message). Encrypts a
3058
+ * `type:'reaction'` envelope at the current epoch and sends through the SAME
3059
+ * MLS application path as `sendText` (the server stays blind — a reaction is
3060
+ * just another application message). Persists the outgoing reaction row so it
3061
+ * re-folds onto its target after a reload (the own-send half of the reload
3062
+ * parity). NEVER rebases (epoch-bound like any application message). */
3063
+ async sendReaction(group, args) {
3064
+ const plaintext = encodeReaction({
3065
+ clientMsgId: args.clientMsgId,
3066
+ targetClientMsgId: args.targetClientMsgId,
3067
+ emoji: args.emoji,
3068
+ op: args.op
3069
+ });
3070
+ const ct = await this.engine.encryptApplication(fromBase64(group.rfcGroupId), plaintext);
3071
+ const body = {
3072
+ ciphertext_b64: toBase64(ct),
3073
+ client_idem_key: randomId()
3074
+ };
3075
+ const wire = await palbeRequest(
3076
+ this.rt,
3077
+ "POST",
3078
+ MessagingPaths.groupMessages(group.displayId),
3079
+ { body }
3080
+ );
3081
+ const stored = {
3082
+ id: `${group.rfcGroupId}#${wire.server_seq}`,
3083
+ direction: "outgoing",
3084
+ text: null,
3085
+ senderDeviceId: this.selfDeviceId,
3086
+ epoch: wire.epoch,
3087
+ serverSeq: wire.server_seq,
3088
+ at: Date.now(),
3089
+ clientMsgId: args.clientMsgId,
3090
+ replyTo: null,
3091
+ envelopeType: "reaction",
3092
+ reaction: {
3093
+ targetClientMsgId: args.targetClientMsgId,
3094
+ emoji: args.emoji,
3095
+ op: args.op
3096
+ }
3097
+ };
3098
+ try {
3099
+ await this.messageStore.append(group.rfcGroupId, stored);
3100
+ } catch {
3101
+ }
3102
+ return {
3103
+ receipt: { serverSeq: wire.server_seq, epoch: wire.epoch },
3104
+ clientMsgId: args.clientMsgId
3105
+ };
3106
+ }
3031
3107
  // ── The rebase loop ──
3032
3108
  async commitWithRebase(rfcGroupId, build) {
3033
3109
  const gidBytes = fromBase64(rfcGroupId);
@@ -3089,6 +3165,61 @@ var GroupMessaging = class {
3089
3165
  }
3090
3166
  };
3091
3167
 
3168
+ // src/messaging/reaction-fold.ts
3169
+ function orderLte(aEpoch, aSeq, bEpoch, bSeq) {
3170
+ if (aEpoch !== bEpoch) return aEpoch < bEpoch;
3171
+ return aSeq <= bSeq;
3172
+ }
3173
+ var ReactionFold = class {
3174
+ // Map<targetClientMsgId, Map<actorUserId, UserState>>
3175
+ states = /* @__PURE__ */ new Map();
3176
+ // Dedup set: eventClientMsgId values already applied.
3177
+ seenEvents = /* @__PURE__ */ new Set();
3178
+ ingest(e) {
3179
+ if (this.seenEvents.has(e.eventClientMsgId)) return;
3180
+ this.seenEvents.add(e.eventClientMsgId);
3181
+ let byUser = this.states.get(e.targetClientMsgId);
3182
+ if (byUser === void 0) {
3183
+ byUser = /* @__PURE__ */ new Map();
3184
+ this.states.set(e.targetClientMsgId, byUser);
3185
+ }
3186
+ const prev = byUser.get(e.actorUserId);
3187
+ if (prev !== void 0) {
3188
+ if (orderLte(e.epoch, e.serverSeq, prev.orderEpoch, prev.orderSeq)) return;
3189
+ }
3190
+ byUser.set(e.actorUserId, {
3191
+ orderEpoch: e.epoch,
3192
+ orderSeq: e.serverSeq,
3193
+ emoji: e.op === "add" ? e.emoji : null
3194
+ });
3195
+ }
3196
+ /**
3197
+ * Returns a tally of `{ emoji → sorted userIds[] }` for the given target
3198
+ * message. Users whose last event was a `remove` (emoji === null) are
3199
+ * excluded. userId arrays are sorted for deterministic output (render
3200
+ * stability + golden tests).
3201
+ */
3202
+ tally(targetClientMsgId) {
3203
+ const byUser = this.states.get(targetClientMsgId);
3204
+ if (byUser === void 0) return {};
3205
+ const out = /* @__PURE__ */ new Map();
3206
+ for (const [userId, st] of byUser) {
3207
+ if (st.emoji === null) continue;
3208
+ let users = out.get(st.emoji);
3209
+ if (users === void 0) {
3210
+ users = [];
3211
+ out.set(st.emoji, users);
3212
+ }
3213
+ users.push(userId);
3214
+ }
3215
+ const result = {};
3216
+ for (const [emoji, users] of out) {
3217
+ result[emoji] = users.sort();
3218
+ }
3219
+ return result;
3220
+ }
3221
+ };
3222
+
3092
3223
  // src/messaging/chat.ts
3093
3224
  var Chat = class {
3094
3225
  /** Stable, URL/log-safe id (the grp_ display id once active; a reserved local id while draft). */
@@ -3107,6 +3238,8 @@ var Chat = class {
3107
3238
  seenKeys = /* @__PURE__ */ new Set();
3108
3239
  /** Index: clientMsgId → { text, senderUserId } for resolveReply lookups. */
3109
3240
  byClientMsgId = /* @__PURE__ */ new Map();
3241
+ /** The single authoritative reaction fold for this chat (live + own-send + history). */
3242
+ reactionFold = new ReactionFold();
3110
3243
  loadedEarliestSeq = null;
3111
3244
  historyLoaded = false;
3112
3245
  wired = false;
@@ -3213,7 +3346,7 @@ var Chat = class {
3213
3346
  const key = this.internalKey(m.serverSeq);
3214
3347
  if (this.seenKeys.has(key)) continue;
3215
3348
  this.seenKeys.add(key);
3216
- this.messageList.push(m);
3349
+ this.messageList.push(this.applyReactionTally(m));
3217
3350
  changed = true;
3218
3351
  this.loadedEarliestSeq = Math.min(this.loadedEarliestSeq ?? m.serverSeq, m.serverSeq);
3219
3352
  }
@@ -3238,6 +3371,22 @@ var Chat = class {
3238
3371
  senderUser = await this.backend.userIdForDevice(this._group, incoming.senderDeviceId);
3239
3372
  }
3240
3373
  const direction = senderUser !== null && senderUser === this.backend.selfUserId ? "outgoing" : "incoming";
3374
+ if (incoming.envelopeType === "reaction" && incoming.reaction) {
3375
+ const actorUserId = direction === "outgoing" ? this.backend.selfUserId : senderUser;
3376
+ if (actorUserId !== null) {
3377
+ this.reactionFold.ingest({
3378
+ targetClientMsgId: incoming.reaction.targetClientMsgId,
3379
+ actorUserId,
3380
+ emoji: incoming.reaction.emoji,
3381
+ op: incoming.reaction.op,
3382
+ epoch: incoming.epoch,
3383
+ serverSeq: incoming.serverSeq,
3384
+ eventClientMsgId: incoming.clientMsgId
3385
+ });
3386
+ this.recomputeReactions(incoming.reaction.targetClientMsgId);
3387
+ }
3388
+ return;
3389
+ }
3241
3390
  const incomingClientMsgId = incoming.clientMsgId;
3242
3391
  const incomingReplyRef = incoming.replyRef;
3243
3392
  let resolvedReplyTo = null;
@@ -3253,7 +3402,10 @@ var Chat = class {
3253
3402
  serverSeq: incoming.serverSeq,
3254
3403
  sentAt: incoming.receivedAt,
3255
3404
  clientMsgId: incomingClientMsgId,
3256
- replyTo: resolvedReplyTo
3405
+ replyTo: resolvedReplyTo,
3406
+ // Attach any tally already folded for this message (a reaction that arrived
3407
+ // BEFORE its target — the dangling case — renders the moment the target lands).
3408
+ reactions: incomingClientMsgId ? this.reactionFold.tally(incomingClientMsgId) : {}
3257
3409
  };
3258
3410
  if (incomingClientMsgId && incoming.text !== null) {
3259
3411
  this.byClientMsgId.set(incomingClientMsgId, {
@@ -3269,6 +3421,35 @@ var Chat = class {
3269
3421
  );
3270
3422
  this.emit();
3271
3423
  }
3424
+ /**
3425
+ * Rebuild the target message's `reactions` from the authoritative fold and
3426
+ * re-emit. No-op when the target isn't present yet (its tally is attached the
3427
+ * moment it lands, via the append/merge paths) or when the tally is unchanged.
3428
+ */
3429
+ recomputeReactions(targetClientMsgId) {
3430
+ if (!targetClientMsgId) return;
3431
+ const tally = this.reactionFold.tally(targetClientMsgId);
3432
+ let changed = false;
3433
+ this.messageList = this.messageList.map((m) => {
3434
+ if (m.clientMsgId !== targetClientMsgId) return m;
3435
+ if (sameReactions(m.reactions, tally)) return m;
3436
+ changed = true;
3437
+ return { ...m, reactions: tally };
3438
+ });
3439
+ if (changed) this.emit();
3440
+ }
3441
+ /**
3442
+ * Overlay the authoritative fold's tally onto a message as it is appended/merged.
3443
+ * The Chat fold WINS when it has a non-empty tally; otherwise the tally already
3444
+ * attached upstream (the coordinator's page-local history fold) is preserved.
3445
+ */
3446
+ applyReactionTally(m) {
3447
+ if (!m.clientMsgId) return m;
3448
+ const tally = this.reactionFold.tally(m.clientMsgId);
3449
+ if (Object.keys(tally).length === 0) return m;
3450
+ if (sameReactions(m.reactions, tally)) return m;
3451
+ return { ...m, reactions: tally };
3452
+ }
3272
3453
  /** @internal — called by the backend's conv subscription. */
3273
3454
  applyConv(event, payload) {
3274
3455
  const userId = typeof payload.user_id === "string" ? payload.user_id : null;
@@ -3411,7 +3592,10 @@ var Chat = class {
3411
3592
  serverSeq: receipt.serverSeq,
3412
3593
  sentAt: /* @__PURE__ */ new Date(),
3413
3594
  clientMsgId,
3414
- replyTo: resolvedReplyTo
3595
+ replyTo: resolvedReplyTo,
3596
+ // Attach any tally already folded for this own-sent message (rare, but keeps
3597
+ // the dangling-target invariant uniform across every append path).
3598
+ reactions: clientMsgId ? this.reactionFold.tally(clientMsgId) : {}
3415
3599
  });
3416
3600
  this.messageList.sort((a, b) => a.serverSeq - b.serverSeq);
3417
3601
  this.emit();
@@ -3458,7 +3642,53 @@ var Chat = class {
3458
3642
  this.readWatermark = Math.max(this.readWatermark, message.serverSeq);
3459
3643
  this.emit();
3460
3644
  }
3645
+ // ── Reactions ──
3646
+ /** Add an emoji reaction to a message. No-op if the message isn't reactable
3647
+ * (empty clientMsgId — a legacy/system row). The reaction folds locally with
3648
+ * the server receipt's `(epoch, serverSeq)` so the target's tally updates
3649
+ * instantly; the durable echo on the next pump is a fold no-op (dedup on the
3650
+ * SAME wire clientMsgId). Never appends a bubble. */
3651
+ async react(message, emoji) {
3652
+ await this.sendReaction(message, emoji, "add");
3653
+ }
3654
+ /** Remove this user's emoji reaction from a message (op:'remove'). */
3655
+ async unreact(message, emoji) {
3656
+ await this.sendReaction(message, emoji, "remove");
3657
+ }
3658
+ async sendReaction(message, emoji, op) {
3659
+ if (!message.clientMsgId) return;
3660
+ const group = await this.materializeIfNeeded();
3661
+ const clientMsgId = mintClientMsgId();
3662
+ const { receipt } = await this.backend.sendReaction(group, {
3663
+ clientMsgId,
3664
+ targetClientMsgId: message.clientMsgId,
3665
+ emoji,
3666
+ op
3667
+ });
3668
+ this.reactionFold.ingest({
3669
+ targetClientMsgId: message.clientMsgId,
3670
+ actorUserId: this.backend.selfUserId,
3671
+ emoji,
3672
+ op,
3673
+ epoch: receipt.epoch,
3674
+ serverSeq: receipt.serverSeq,
3675
+ eventClientMsgId: clientMsgId
3676
+ });
3677
+ this.recomputeReactions(message.clientMsgId);
3678
+ }
3461
3679
  };
3680
+ function sameReactions(a, b) {
3681
+ const ak = Object.keys(a);
3682
+ const bk = Object.keys(b);
3683
+ if (ak.length !== bk.length) return false;
3684
+ for (const k of ak) {
3685
+ const av = a[k];
3686
+ const bv = b[k];
3687
+ if (!bv || av === void 0 || av.length !== bv.length) return false;
3688
+ for (let i = 0; i < av.length; i++) if (av[i] !== bv[i]) return false;
3689
+ }
3690
+ return true;
3691
+ }
3462
3692
 
3463
3693
  // src/messaging/delivery-source.ts
3464
3694
  var MessageHub = class {
@@ -3488,6 +3718,11 @@ var MessageHub = class {
3488
3718
  };
3489
3719
  }
3490
3720
  };
3721
+ function decodeSenderDeviceId(sender) {
3722
+ if (sender.length === 0) return null;
3723
+ const id = decodeUtf8(sender);
3724
+ return id.length > 0 ? id : null;
3725
+ }
3491
3726
  var MessageDeliverySource = class {
3492
3727
  constructor(rt, engine, hub, registry, messageStore, deviceId, selfUserId) {
3493
3728
  this.rt = rt;
@@ -3617,12 +3852,15 @@ var MessageDeliverySource = class {
3617
3852
  try {
3618
3853
  const received = await this.engine.processIncoming(fromBase64(group.rfcGroupId), blob);
3619
3854
  if (received.type === "application") {
3620
- const { text, clientMsgId, replyTo } = decodeEnvelope(received.data);
3855
+ const decoded = decodeEnvelope(received.data);
3856
+ const { text, clientMsgId, replyTo } = decoded;
3857
+ const isReaction = decoded.type === "reaction" && decoded.reaction != null;
3858
+ const senderDeviceId = row.sender_device_id ?? decodeSenderDeviceId(received.sender);
3621
3859
  const stored = {
3622
3860
  id: `${group.rfcGroupId}#${row.server_seq}`,
3623
3861
  direction: "incoming",
3624
3862
  text,
3625
- senderDeviceId: row.sender_device_id ?? null,
3863
+ senderDeviceId,
3626
3864
  epoch: row.epoch,
3627
3865
  serverSeq: row.server_seq,
3628
3866
  at: Date.now(),
@@ -3632,7 +3870,19 @@ var MessageDeliverySource = class {
3632
3870
  previewBody: replyTo.preview?.body ?? null,
3633
3871
  previewAuthorUserId: replyTo.preview?.author_user_id ?? null,
3634
3872
  previewKind: replyTo.preview?.kind ?? "text"
3635
- } : null
3873
+ } : null,
3874
+ // Thread the reaction discriminator + fields through the persisted row so
3875
+ // a reaction folded LIVE re-folds onto its target after a reload (the
3876
+ // reload-parity boundary — mirrors iOS T3). Omitted for non-reactions →
3877
+ // old rows hydrate as `'text'`/no-reaction (backward-compat).
3878
+ ...isReaction && decoded.reaction ? {
3879
+ envelopeType: "reaction",
3880
+ reaction: {
3881
+ targetClientMsgId: decoded.reaction.targetClientMsgId,
3882
+ emoji: decoded.reaction.emoji,
3883
+ op: decoded.reaction.op
3884
+ }
3885
+ } : {}
3636
3886
  };
3637
3887
  try {
3638
3888
  await this.messageStore.append(group.rfcGroupId, stored);
@@ -3643,12 +3893,14 @@ var MessageDeliverySource = class {
3643
3893
  kind: "application",
3644
3894
  group,
3645
3895
  text,
3646
- senderDeviceId: row.sender_device_id ?? null,
3896
+ senderDeviceId,
3647
3897
  epoch: row.epoch,
3648
3898
  serverSeq: row.server_seq,
3649
3899
  receivedAt: /* @__PURE__ */ new Date(),
3650
3900
  clientMsgId,
3651
- replyRef: replyTo
3901
+ replyRef: replyTo,
3902
+ envelopeType: decoded.type ?? "text",
3903
+ reaction: isReaction ? decoded.reaction : null
3652
3904
  });
3653
3905
  return true;
3654
3906
  }
@@ -5652,46 +5904,14 @@ var MessagingCoordinator = class {
5652
5904
  const r = await this.resolve();
5653
5905
  return r.groups.sendText(group, text, replyTo);
5654
5906
  }
5907
+ async sendReaction(group, args) {
5908
+ const r = await this.resolve();
5909
+ return r.groups.sendReaction(group, args);
5910
+ }
5655
5911
  async history(group, limit, before) {
5656
5912
  const r = await this.resolve();
5657
5913
  const rows = await r.messageStore.history(group.rfcGroupId, limit, before);
5658
- const lookup = /* @__PURE__ */ new Map();
5659
- for (const s of rows) {
5660
- const cid = s.clientMsgId ?? "";
5661
- if (cid && s.text !== null) {
5662
- const senderUserId = s.direction === "outgoing" ? this.selfUserId : "";
5663
- lookup.set(cid, { text: s.text, senderUserId });
5664
- }
5665
- }
5666
- return rows.map((s) => this.toChatMessage(group, s, (id) => lookup.get(id) ?? null));
5667
- }
5668
- toChatMessage(group, s, lookup) {
5669
- const clientMsgId = s.clientMsgId ?? "";
5670
- let replyTo = null;
5671
- if (s.replyTo && lookup) {
5672
- const ref = {
5673
- v: 1,
5674
- client_msg_id: s.replyTo.clientMsgId,
5675
- preview: {
5676
- kind: s.replyTo.previewKind,
5677
- author_user_id: s.replyTo.previewAuthorUserId ?? "",
5678
- body: s.replyTo.previewBody ?? void 0,
5679
- body_truncated: false
5680
- }
5681
- };
5682
- replyTo = resolveReply(ref, lookup);
5683
- }
5684
- return {
5685
- id: `${group.displayId}#${s.serverSeq}`,
5686
- kind: s.text != null ? "text" : "system",
5687
- direction: s.direction,
5688
- senderUserId: s.direction === "outgoing" ? this.selfUserId : null,
5689
- text: s.text,
5690
- serverSeq: s.serverSeq,
5691
- sentAt: new Date(s.at),
5692
- clientMsgId,
5693
- replyTo
5694
- };
5914
+ return projectHistory(group.displayId, rows, this.selfUserId);
5695
5915
  }
5696
5916
  async members(group) {
5697
5917
  const r = await this.resolve();
@@ -5768,6 +5988,64 @@ var MessagingCoordinator = class {
5768
5988
  return res.devices.map((d) => d.device_id);
5769
5989
  }
5770
5990
  };
5991
+ function projectHistory(displayId, rows, selfUserId, resolveActor) {
5992
+ const fold = new ReactionFold();
5993
+ for (const s of rows) {
5994
+ if (s.envelopeType !== "reaction" || !s.reaction) continue;
5995
+ const actor = s.direction === "outgoing" ? selfUserId : resolveActor?.(s.senderDeviceId ?? null) ?? null;
5996
+ if (actor === null) continue;
5997
+ fold.ingest({
5998
+ targetClientMsgId: s.reaction.targetClientMsgId,
5999
+ actorUserId: actor,
6000
+ emoji: s.reaction.emoji,
6001
+ op: s.reaction.op,
6002
+ epoch: s.epoch,
6003
+ serverSeq: s.serverSeq,
6004
+ eventClientMsgId: s.clientMsgId ?? `${s.id}`
6005
+ });
6006
+ }
6007
+ const lookup = /* @__PURE__ */ new Map();
6008
+ for (const s of rows) {
6009
+ if (s.envelopeType === "reaction") continue;
6010
+ const cid = s.clientMsgId ?? "";
6011
+ if (cid && s.text !== null) {
6012
+ const senderUserId = s.direction === "outgoing" ? selfUserId : "";
6013
+ lookup.set(cid, { text: s.text, senderUserId });
6014
+ }
6015
+ }
6016
+ const out = [];
6017
+ for (const s of rows) {
6018
+ if (s.envelopeType === "reaction") continue;
6019
+ const clientMsgId = s.clientMsgId ?? "";
6020
+ let replyTo = null;
6021
+ if (s.replyTo) {
6022
+ const ref = {
6023
+ v: 1,
6024
+ client_msg_id: s.replyTo.clientMsgId,
6025
+ preview: {
6026
+ kind: s.replyTo.previewKind,
6027
+ author_user_id: s.replyTo.previewAuthorUserId ?? "",
6028
+ body: s.replyTo.previewBody ?? void 0,
6029
+ body_truncated: false
6030
+ }
6031
+ };
6032
+ replyTo = resolveReply(ref, (id) => lookup.get(id) ?? null);
6033
+ }
6034
+ out.push({
6035
+ id: `${displayId}#${s.serverSeq}`,
6036
+ kind: s.text != null ? "text" : "system",
6037
+ direction: s.direction,
6038
+ senderUserId: s.direction === "outgoing" ? selfUserId : null,
6039
+ text: s.text,
6040
+ serverSeq: s.serverSeq,
6041
+ sentAt: new Date(s.at),
6042
+ clientMsgId,
6043
+ replyTo,
6044
+ reactions: clientMsgId ? fold.tally(clientMsgId) : {}
6045
+ });
6046
+ }
6047
+ return out;
6048
+ }
5771
6049
 
5772
6050
  // src/messaging/facade.ts
5773
6051
  var PalbeMessaging = class {
@@ -6501,7 +6779,7 @@ function defaultSessionStorage(key) {
6501
6779
  }
6502
6780
 
6503
6781
  // src/version.ts
6504
- var VERSION = "1.1.1";
6782
+ var VERSION = "1.2.1";
6505
6783
 
6506
6784
  // src/runtime.ts
6507
6785
  function buildRuntime(config) {