stream-chat 10.0.0-rc.12 → 10.0.0-rc.14

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.
Files changed (106) hide show
  1. package/dist/cjs/index.browser.js +801 -652
  2. package/dist/cjs/index.browser.js.map +4 -4
  3. package/dist/cjs/index.node.js +807 -653
  4. package/dist/cjs/index.node.js.map +4 -4
  5. package/dist/esm/index.mjs +801 -652
  6. package/dist/esm/index.mjs.map +4 -4
  7. package/dist/types/CooldownTimer.d.ts +4 -3
  8. package/dist/types/channel.d.ts +44 -81
  9. package/dist/types/channel_state.d.ts +5 -5
  10. package/dist/types/client.d.ts +14 -3
  11. package/dist/types/entityStore/EntityStore.d.ts +7 -0
  12. package/dist/types/entityStore/index.d.ts +0 -1
  13. package/dist/types/formatMessageMemo.d.ts +5 -0
  14. package/dist/types/gen/chat/ChatApi.d.ts +2 -2
  15. package/dist/types/gen/models/index.d.ts +492 -369
  16. package/dist/types/gen/models/timestamp-guard.d.ts +22 -0
  17. package/dist/types/index.d.ts +2 -1
  18. package/dist/types/messageComposer/attachmentIdentity.d.ts +7 -0
  19. package/dist/types/messageComposer/attachmentUtils.d.ts +34 -0
  20. package/dist/types/messageComposer/configuration/types.d.ts +12 -0
  21. package/dist/types/messageComposer/index.d.ts +1 -0
  22. package/dist/types/messageComposer/linkPreviewsManager.d.ts +3 -3
  23. package/dist/types/messageComposer/messageComposer.d.ts +63 -19
  24. package/dist/types/messageComposer/middleware/attachmentManager/postUpload/attachmentEnrichment.d.ts +1 -1
  25. package/dist/types/messageComposer/middleware/messageComposer/MessageComposerMiddlewareExecutor.d.ts +1 -6
  26. package/dist/types/messageComposer/middleware/messageComposer/attachments.d.ts +7 -20
  27. package/dist/types/messageComposer/middleware/messageComposer/types.d.ts +1 -22
  28. package/dist/types/messageComposer/middleware/textComposer/commands.d.ts +2 -2
  29. package/dist/types/messageComposer/middleware/textComposer/mentions.d.ts +8 -0
  30. package/dist/types/messageDelivery/MessageReceiptsTracker.d.ts +42 -8
  31. package/dist/types/messageOperations/MessageOperations.d.ts +35 -4
  32. package/dist/types/messageOperations/WithMessageOperations.d.ts +35 -0
  33. package/dist/types/messageOperations/createMessageOperations.d.ts +11 -0
  34. package/dist/types/messageOperations/index.d.ts +6 -0
  35. package/dist/types/messageOperations/reflectReactionEvent.d.ts +24 -0
  36. package/dist/types/messageOperations/sendOrdering.d.ts +17 -0
  37. package/dist/types/messageOperations/settlePendingAttachmentUploads.d.ts +28 -0
  38. package/dist/types/messageOperations/types.d.ts +34 -0
  39. package/dist/types/notifications/types.d.ts +4 -0
  40. package/dist/types/offline-support/types.d.ts +2 -2
  41. package/dist/types/pagination/cursorDerivation/createdAtAroundPaginationFlags.d.ts +2 -1
  42. package/dist/types/pagination/cursorDerivation/linearPaginationFlags.d.ts +2 -1
  43. package/dist/types/pagination/paginators/BasePaginator.d.ts +22 -0
  44. package/dist/types/pagination/paginators/MessageIntervalPaginator.d.ts +10 -48
  45. package/dist/types/pagination/paginators/MessagePaginator.d.ts +5 -5
  46. package/dist/types/poll.d.ts +2 -2
  47. package/dist/types/reminders/Reminder.d.ts +6 -6
  48. package/dist/types/thread.d.ts +26 -46
  49. package/dist/types/types.d.ts +5 -5
  50. package/dist/types/uploadManager.d.ts +19 -1
  51. package/dist/types/utils/time.d.ts +23 -7
  52. package/dist/types/utils.d.ts +11 -13
  53. package/package.json +6 -4
  54. package/src/CooldownTimer.ts +5 -5
  55. package/src/api-client.ts +5 -2
  56. package/src/channel.ts +65 -300
  57. package/src/channel_state.ts +5 -4
  58. package/src/client.ts +18 -3
  59. package/src/configuration/shape.ts +12 -0
  60. package/src/entityStore/EntityStore.ts +22 -1
  61. package/src/entityStore/index.ts +0 -1
  62. package/src/formatMessageMemo.ts +23 -0
  63. package/src/gen/chat/ChatApi.ts +4 -2
  64. package/src/gen/models/index.ts +502 -3217
  65. package/src/gen/models/timestamp-guard.ts +27 -0
  66. package/src/gen/moderation/ModerationApi.ts +1 -0
  67. package/src/index.ts +6 -0
  68. package/src/messageComposer/attachmentIdentity.ts +12 -0
  69. package/src/messageComposer/attachmentUtils.ts +58 -0
  70. package/src/messageComposer/configuration/configuration.ts +2 -0
  71. package/src/messageComposer/configuration/types.ts +12 -0
  72. package/src/messageComposer/index.ts +1 -0
  73. package/src/messageComposer/messageComposer.ts +178 -28
  74. package/src/messageComposer/middleware/attachmentManager/postUpload/attachmentEnrichment.ts +1 -1
  75. package/src/messageComposer/middleware/messageComposer/MessageComposerMiddlewareExecutor.ts +0 -9
  76. package/src/messageComposer/middleware/messageComposer/attachments.ts +43 -56
  77. package/src/messageComposer/middleware/messageComposer/linkPreviews.ts +20 -10
  78. package/src/messageComposer/middleware/messageComposer/types.ts +1 -22
  79. package/src/messageDelivery/MessageDeliveryReporter.ts +3 -2
  80. package/src/messageDelivery/MessageReceiptsTracker.ts +83 -13
  81. package/src/messageOperations/MessageOperations.ts +140 -15
  82. package/src/messageOperations/WithMessageOperations.ts +60 -0
  83. package/src/messageOperations/createMessageOperations.ts +139 -0
  84. package/src/messageOperations/index.ts +6 -0
  85. package/src/messageOperations/optimistic.ts +1 -1
  86. package/src/messageOperations/reflectReactionEvent.ts +51 -0
  87. package/src/messageOperations/sendOrdering.ts +28 -0
  88. package/src/messageOperations/settlePendingAttachmentUploads.ts +110 -0
  89. package/src/messageOperations/types.ts +38 -0
  90. package/src/notifications/types.ts +4 -0
  91. package/src/offline-support/offline_support_api.ts +10 -1
  92. package/src/offline-support/types.ts +2 -1
  93. package/src/pagination/cursorDerivation/createdAtAroundPaginationFlags.ts +2 -1
  94. package/src/pagination/cursorDerivation/linearPaginationFlags.ts +2 -2
  95. package/src/pagination/paginators/BasePaginator.ts +73 -23
  96. package/src/pagination/paginators/MessageIntervalPaginator.ts +45 -102
  97. package/src/pagination/paginators/MessagePaginator.ts +7 -7
  98. package/src/poll.ts +6 -2
  99. package/src/reminders/Reminder.ts +10 -5
  100. package/src/thread.ts +24 -245
  101. package/src/types.ts +5 -4
  102. package/src/uploadManager.ts +37 -5
  103. package/src/utils/time.ts +27 -7
  104. package/src/utils.ts +12 -5
  105. /package/dist/types/{entityStore → messageOperations}/applyReactionLocally.d.ts +0 -0
  106. /package/src/{entityStore → messageOperations}/applyReactionLocally.ts +0 -0
@@ -137,6 +137,7 @@ __export(src_exports, {
137
137
  WS_NETWORK_RECOVERY_RETRY_MS: () => WS_NETWORK_RECOVERY_RETRY_MS,
138
138
  ZERO_PAGE_CURSOR: () => ZERO_PAGE_CURSOR,
139
139
  accentsMap: () => accentsMap,
140
+ asTimestampNS: () => asTimestampNS,
140
141
  browserNetworkStatusReporter: () => browserNetworkStatusReporter,
141
142
  calculateLevenshtein: () => calculateLevenshtein,
142
143
  chatCodes: () => chatCodes,
@@ -167,7 +168,6 @@ __export(src_exports, {
167
168
  createPollComposerStateMiddleware: () => createPollComposerStateMiddleware,
168
169
  createPostUploadAttachmentEnrichmentMiddleware: () => createPostUploadAttachmentEnrichmentMiddleware,
169
170
  createPriorityOwnershipResolver: () => createPriorityOwnershipResolver,
170
- createSendWithPendingUploadsAttachmentsMiddleware: () => createSendWithPendingUploadsAttachmentsMiddleware,
171
171
  createSharedLocationCompositionMiddleware: () => createSharedLocationCompositionMiddleware,
172
172
  createTextComposerCompositionMiddleware: () => createTextComposerCompositionMiddleware,
173
173
  createTextComposerPreValidationMiddleware: () => createTextComposerPreValidationMiddleware,
@@ -186,6 +186,7 @@ __export(src_exports, {
186
186
  formatMessage: () => formatMessage,
187
187
  generateFileName: () => generateFileName,
188
188
  getAllowedMentionTypesFromCapabilities: () => getAllowedMentionTypesFromCapabilities,
189
+ getAttachmentPreviewUrl: () => getAttachmentPreviewUrl,
189
190
  getAttachmentTypeFromMimeType: () => getAttachmentTypeFromMimeType,
190
191
  getCommandByName: () => getCommandByName,
191
192
  getCompleteCommandInString: () => getCompleteCommandInString,
@@ -200,6 +201,7 @@ __export(src_exports, {
200
201
  insertItemWithTrigger: () => insertItemWithTrigger,
201
202
  isAudioAttachment: () => isAudioAttachment,
202
203
  isBlobButNotFile: () => isBlobButNotFile,
204
+ isFailedUpload: () => isFailedUpload,
203
205
  isFile: () => isFile,
204
206
  isFileAttachment: () => isFileAttachment,
205
207
  isFileList: () => isFileList,
@@ -225,6 +227,7 @@ __export(src_exports, {
225
227
  isSharedLocationResponseData: () => isSharedLocationResponseData,
226
228
  isTargetedOptionTextUpdate: () => isTargetedOptionTextUpdate,
227
229
  isUploadCancellation: () => isUploadCancellation,
230
+ isUploadConfirmationPending: () => isUploadConfirmationPending,
228
231
  isUploadedAttachment: () => isUploadedAttachment,
229
232
  isVideoAttachment: () => isVideoAttachment,
230
233
  isVoiceRecordingAttachment: () => isVoiceRecordingAttachment,
@@ -247,6 +250,8 @@ __export(src_exports, {
247
250
  readFileAsArrayBuffer: () => readFileAsArrayBuffer,
248
251
  removeDiacritics: () => removeDiacritics,
249
252
  replaceWordWithEntity: () => replaceWordWithEntity,
253
+ resolveAttachmentFileSize: () => resolveAttachmentFileSize,
254
+ resolveAttachmentFullByteSize: () => resolveAttachmentFullByteSize,
250
255
  settled: () => settled,
251
256
  stripCommandFromText: () => stripCommandFromText,
252
257
  stripMentionTokens: () => stripMentionTokens,
@@ -389,6 +394,13 @@ var LOCAL_MESSAGE_FIELDS = Object.freeze({ error: true });
389
394
  var DEFAULT_QUERY_CHANNELS_RETRY_COUNT = 3;
390
395
  var DEFAULT_QUERY_CHANNELS_MS_BETWEEN_RETRIES = 1e3;
391
396
 
397
+ // src/formatMessageMemo.ts
398
+ var memo = /* @__PURE__ */ new WeakMap();
399
+ var getMemoizedFormat = (source) => memo.get(source);
400
+ var memoizeFormat = (source, formatted) => {
401
+ memo.set(source, formatted);
402
+ };
403
+
392
404
  // src/logger.ts
393
405
  var scopedLogger = __toESM(require("@stream-io/logger"));
394
406
  var import_logger = require("@stream-io/logger");
@@ -396,8 +408,9 @@ var chatLoggerSystem = scopedLogger.createLoggerSystem();
396
408
 
397
409
  // src/utils/time.ts
398
410
  var NS_PER_MS = 1e6;
411
+ var asTimestampNS = (ns) => ns;
399
412
  var nsToMs = (ns) => Math.floor(ns / NS_PER_MS);
400
- var msToNs = (ms) => ms * NS_PER_MS;
413
+ var msToNs = (ms) => asTimestampNS(ms * NS_PER_MS);
401
414
  var nowNs = () => msToNs(Date.now());
402
415
  var nsToDate = (ns) => new Date(nsToMs(ns));
403
416
  var nsToRfc3339 = (ns) => {
@@ -594,6 +607,8 @@ var axiosParamsSerializer = (params) => {
594
607
  return newParams.join("&");
595
608
  };
596
609
  function formatMessage(message) {
610
+ const memoized = getMemoizedFormat(message);
611
+ if (memoized) return memoized;
597
612
  const toLocalMessageBase = (msg) => {
598
613
  if (!msg) return null;
599
614
  return {
@@ -613,11 +628,13 @@ function formatMessage(message) {
613
628
  updated_at: msg.updated_at ?? nowNs()
614
629
  };
615
630
  };
616
- return {
631
+ const formatted = {
617
632
  ...toLocalMessageBase(message),
618
633
  error: message.error ?? void 0,
619
634
  quoted_message: toLocalMessageBase(message.quoted_message) ?? void 0
620
635
  };
636
+ memoizeFormat(message, formatted);
637
+ return formatted;
621
638
  }
622
639
  function computeOwnReactions({
623
640
  current,
@@ -1488,7 +1505,12 @@ var EntityStore = class {
1488
1505
  return fn();
1489
1506
  } finally {
1490
1507
  this.transactionDepth -= 1;
1491
- if (this.transactionDepth === 0) this.flush();
1508
+ if (this.transactionDepth === 0) {
1509
+ this.flush();
1510
+ const ids = this.pendingFlushIds;
1511
+ this.pendingFlushIds = void 0;
1512
+ if (ids) for (const id of ids) this.flushSubscribersNow(id);
1513
+ }
1492
1514
  }
1493
1515
  }
1494
1516
  /**
@@ -1496,8 +1518,19 @@ var EntityStore = class {
1496
1518
  * {@link EntityStoreSubscriber.flushState}). Called after an optimistic (local-user) write to
1497
1519
  * `id` so it renders without the throttle delay. Only that id's own subscribers are flushed — the
1498
1520
  * write touched no other id — and flushing a subscriber with nothing pending is a no-op.
1521
+ *
1522
+ * Inside a transaction it is deferred to the outermost exit — inline there it would be a no-op,
1523
+ * since `markDirty` has not notified anyone yet. Getting this wrong costs the siblings (pinned
1524
+ * list, open thread) up to 500ms on an optimistic write, while the writer updates inline.
1499
1525
  */
1500
1526
  flushSubscribers(id) {
1527
+ if (this.transactionDepth > 0) {
1528
+ (this.pendingFlushIds ?? (this.pendingFlushIds = /* @__PURE__ */ new Set())).add(id);
1529
+ return;
1530
+ }
1531
+ this.flushSubscribersNow(id);
1532
+ }
1533
+ flushSubscribersNow(id) {
1501
1534
  const subscribers = this.subscribers.get(id);
1502
1535
  if (!subscribers) return;
1503
1536
  for (const subscriber of subscribers) subscriber.flushState?.();
@@ -1596,11 +1629,11 @@ var isClassInstance = (value) => {
1596
1629
  if (proto === null || proto === Object.prototype) return false;
1597
1630
  return value.constructor && value.constructor !== Object;
1598
1631
  };
1599
- function memoHasOrAdd(memo, a, b) {
1600
- const set = memo.get(a);
1632
+ function memoHasOrAdd(memo2, a, b) {
1633
+ const set = memo2.get(a);
1601
1634
  if (set && set.has(b)) return true;
1602
1635
  if (set) set.add(b);
1603
- else memo.set(a, new WeakSet([b]));
1636
+ else memo2.set(a, new WeakSet([b]));
1604
1637
  return false;
1605
1638
  }
1606
1639
  var isEqual = (value1, value2, pairMemo = /* @__PURE__ */ new WeakMap()) => {
@@ -2671,6 +2704,41 @@ var BasePaginator = class {
2671
2704
  scheduleWindowPublish() {
2672
2705
  this._windowPublishThrottle?.throttledFn();
2673
2706
  }
2707
+ /**
2708
+ * Run `fn` with this paginator's window publishes suspended, emitting once on exit.
2709
+ *
2710
+ * A replace is internally remove-then-insert and each half changes the window, so without this one
2711
+ * logical `ingestItem` emits twice.
2712
+ *
2713
+ * `exit` decides how the single emit leaves the scope: `auto` rides the throttle when there is one
2714
+ * (live ops, where successive events should coalesce across the trailing edge), `sync` always
2715
+ * flushes ({@link batch}, whose contract is that a one-shot operation settles in a single update).
2716
+ *
2717
+ * **`lockItemOrder` lists are excluded.** Their emit is not a re-projection but a republish of the
2718
+ * last-published array with the item spliced back at its original index, and exiting through
2719
+ * {@link flushWindowPublish} re-derives from the active interval — reordering the list and dropping
2720
+ * whatever the interval no longer holds, which is the whole point of locking the order.
2721
+ *
2722
+ * Re-entrant: only the outermost scope emits, so an `ingestItem` inside a coalescing `batch`
2723
+ * defers to the batch.
2724
+ */
2725
+ withSingleWindowPublish(fn, { exit = "auto" } = {}) {
2726
+ if (this.config.lockItemOrder) {
2727
+ fn();
2728
+ return;
2729
+ }
2730
+ const isOutermost = this._windowPublishSuspendDepth === 0;
2731
+ this._windowPublishSuspendDepth += 1;
2732
+ try {
2733
+ fn();
2734
+ } finally {
2735
+ this._windowPublishSuspendDepth -= 1;
2736
+ }
2737
+ if (!isOutermost || !this._suspendedWindowDirty) return;
2738
+ this._suspendedWindowDirty = false;
2739
+ if (exit === "auto" && this.isStateThrottled) this.scheduleWindowPublish();
2740
+ else this.flushWindowPublish();
2741
+ }
2674
2742
  /**
2675
2743
  * Flush any pending throttled window + interval-view publishes immediately. No-op when nothing
2676
2744
  * is pending or throttling is off.
@@ -2899,8 +2967,18 @@ var BasePaginator = class {
2899
2967
  const activeInterval = this._itemIntervals.get(this._activeIntervalId);
2900
2968
  if (!activeInterval) return;
2901
2969
  if (this.isStateThrottled) {
2902
- for (const id of activeInterval.itemIds) {
2903
- if (changedIds.has(id)) {
2970
+ const currentItems2 = this.items;
2971
+ if (!currentItems2 || currentItems2.length !== activeInterval.itemIds.length) {
2972
+ if (this.intervalHoldsAnyChangedId(activeInterval, changedIds)) {
2973
+ this.scheduleWindowPublish();
2974
+ }
2975
+ return;
2976
+ }
2977
+ for (let i = 0; i < currentItems2.length; i++) {
2978
+ const id = this.getItemId(currentItems2[i]);
2979
+ if (!changedIds.has(id)) continue;
2980
+ const updated = this._itemIndex.get(id);
2981
+ if (!updated || updated !== currentItems2[i]) {
2904
2982
  this.scheduleWindowPublish();
2905
2983
  return;
2906
2984
  }
@@ -2929,11 +3007,8 @@ var BasePaginator = class {
2929
3007
  return;
2930
3008
  }
2931
3009
  }
2932
- for (const id of activeInterval.itemIds) {
2933
- if (changedIds.has(id)) {
2934
- this.state.partialNext({ items: this.intervalToItems(activeInterval) });
2935
- return;
2936
- }
3010
+ if (this.intervalHoldsAnyChangedId(activeInterval, changedIds)) {
3011
+ this.state.partialNext({ items: this.intervalToItems(activeInterval) });
2937
3012
  }
2938
3013
  }
2939
3014
  // ---------------------------------------------------------------------------
@@ -3603,6 +3678,13 @@ var BasePaginator = class {
3603
3678
  * - if this is the active interval, re-emit state.items from interval
3604
3679
  */
3605
3680
  ingestItem(ingestedItem) {
3681
+ let ingested = false;
3682
+ this.withSingleWindowPublish(() => {
3683
+ ingested = this.applyItemIngestion(ingestedItem);
3684
+ });
3685
+ return ingested;
3686
+ }
3687
+ applyItemIngestion(ingestedItem) {
3606
3688
  const id = this.getItemId(ingestedItem);
3607
3689
  const previousItem = this._itemIndex.get(id);
3608
3690
  const previousCoords = this.locateByItem(previousItem || ingestedItem);
@@ -3614,7 +3696,9 @@ var BasePaginator = class {
3614
3696
  removedItemCoordinates = this.removeItemAtCoordinates(previousCoords);
3615
3697
  }
3616
3698
  const itemHasBeenRemoved = !!removedItemCoordinates?.state && removedItemCoordinates.state.currentIndex > -1;
3617
- this._itemIndex.setOne(ingestedItem);
3699
+ if (this._itemIndex.has(id) || this.matchesFilter(ingestedItem)) {
3700
+ this._itemIndex.setOne(ingestedItem);
3701
+ }
3618
3702
  if (!this.matchesFilter(ingestedItem)) {
3619
3703
  this.shrinkOffsetAfterRemoval(removedItemCoordinates);
3620
3704
  if (!this.isWindowPublishSuspended && this.isStateThrottled && itemHasBeenRemoved)
@@ -3733,16 +3817,7 @@ var BasePaginator = class {
3733
3817
  this._itemIndex.batch(fn);
3734
3818
  return;
3735
3819
  }
3736
- this._windowPublishSuspendDepth += 1;
3737
- try {
3738
- this._itemIndex.batch(fn);
3739
- } finally {
3740
- this._windowPublishSuspendDepth -= 1;
3741
- }
3742
- if (this._windowPublishSuspendDepth === 0 && this._suspendedWindowDirty) {
3743
- this._suspendedWindowDirty = false;
3744
- this.flushWindowPublish();
3745
- }
3820
+ this.withSingleWindowPublish(() => this._itemIndex.batch(fn), { exit: "sync" });
3746
3821
  }
3747
3822
  // ---------------------------------------------------------------------------
3748
3823
  // Remove / contains
@@ -4359,6 +4434,10 @@ var CORE_NOTIFICATION_TYPE = {
4359
4434
  messageJumpFailed: "api:message:jump:failed",
4360
4435
  /** Jumping to the latest message failed. */
4361
4436
  messageJumpToLatestFailed: "api:message:jumpToLatest:failed",
4437
+ /** Sending a composed message failed. The message stays in the list, marked failed. */
4438
+ messageSendFailed: "api:message:send:failed",
4439
+ /** Saving an edit failed. The edit is kept on the message, marked failed. */
4440
+ messageUpdateFailed: "api:message:update:failed",
4362
4441
  pollCastVoteLimit: "validation:poll:castVote:limit",
4363
4442
  pollCreateFailed: "api:poll:create:failed"
4364
4443
  };
@@ -5021,34 +5100,37 @@ var MessageIntervalPaginator = class extends BasePaginator {
5021
5100
  deletedAt
5022
5101
  }) => {
5023
5102
  const loadedMessages = this.items ?? [];
5024
- this.batch(() => {
5025
- for (const message of loadedMessages) {
5026
- if (message.user?.id === userId) {
5027
- if (hardDelete) {
5028
- this.removeItem({ id: message.id });
5029
- } else {
5030
- this.ingestItem(
5031
- toDeletedMessage({
5032
- message,
5103
+ this.batch(
5104
+ () => {
5105
+ for (const message of loadedMessages) {
5106
+ if (message.user?.id === userId) {
5107
+ if (hardDelete) {
5108
+ this.removeItem({ id: message.id });
5109
+ } else {
5110
+ this.ingestItem(
5111
+ toDeletedMessage({
5112
+ message,
5113
+ hardDelete,
5114
+ deletedAt
5115
+ })
5116
+ );
5117
+ }
5118
+ continue;
5119
+ }
5120
+ if (message.quoted_message?.user?.id === userId && message.quoted_message.type !== "deleted") {
5121
+ this.ingestItem({
5122
+ ...message,
5123
+ quoted_message: toDeletedMessage({
5124
+ message: formatMessage(message.quoted_message),
5033
5125
  hardDelete,
5034
5126
  deletedAt
5035
5127
  })
5036
- );
5128
+ });
5037
5129
  }
5038
- continue;
5039
- }
5040
- if (message.quoted_message?.user?.id === userId && message.quoted_message.type !== "deleted") {
5041
- this.ingestItem({
5042
- ...message,
5043
- quoted_message: toDeletedMessage({
5044
- message: formatMessage(message.quoted_message),
5045
- hardDelete,
5046
- deletedAt
5047
- })
5048
- });
5049
5130
  }
5050
- }
5051
- });
5131
+ },
5132
+ { coalesce: true }
5133
+ );
5052
5134
  };
5053
5135
  /**
5054
5136
  * Ensures quoted-message snapshots across loaded paginator cache are in sync
@@ -5084,6 +5166,7 @@ var MessageIntervalPaginator = class extends BasePaginator {
5084
5166
  this.batch(() => {
5085
5167
  for (const message of this._itemIndex.values()) {
5086
5168
  if (message.user?.id !== user.id) continue;
5169
+ if (message.user === user) continue;
5087
5170
  this._itemIndex.setOne({ ...message, user });
5088
5171
  if (activeIds.has(this.getItemId(message))) activeAffected = true;
5089
5172
  }
@@ -5094,58 +5177,6 @@ var MessageIntervalPaginator = class extends BasePaginator {
5094
5177
  });
5095
5178
  }
5096
5179
  };
5097
- /**
5098
- * Apply a reaction WS event (`reaction.new` / `reaction.updated` / `reaction.deleted`) to the
5099
- * cached message. The event's `message` already carries the server-updated
5100
- * `reaction_groups` / `latest_reactions`; only `own_reactions` needs local preservation so a
5101
- * cross-user reaction does not wipe the current user's reactions. This re-homes what
5102
- * `ChannelState.addReaction` / `removeReaction` used to do off the now-removed
5103
- * `channel.state.messages` / `channel.state.threads` caches (the same logic backs the thread
5104
- * paginator via `Thread.messagePaginator`).
5105
- *
5106
- * `own_reactions` is seeded from the currently cached item (so another user's reaction keeps ours),
5107
- * falling back to the event's own_reactions when the message is not loaded — matching the legacy
5108
- * behavior where `_updateMessage` only mutated a message that existed locally.
5109
- *
5110
- * @param params - The reaction event payload.
5111
- * @param params.message - The reaction event's message, carrying the
5112
- * server-computed `reaction_groups` / `latest_reactions`. Ingested as-is except for `own_reactions`.
5113
- * @param params.reaction - The reaction from the event. Only added to/removed from
5114
- * `own_reactions` when its `user_id` is the current user; otherwise the current user's
5115
- * `own_reactions` are left untouched.
5116
- * @param [params.removed=false] - `true` for `reaction.deleted` (remove the reaction from
5117
- * `own_reactions`); `false` for `reaction.new` / `reaction.updated` (add it).
5118
- * @param [params.enforceUnique=false] - When adding, first clear the current user's existing
5119
- * `own_reactions` so only the incoming one remains (used by `reaction.updated`, where a user's
5120
- * reaction replaces their previous one).
5121
- *
5122
- * TODO(reactive-store): reflect reactions ONCE at the store level, not per-paginator. Both the
5123
- * channel handler (channel.ts) and the thread handler (thread.ts) call this on every reaction.*
5124
- * event, so a message held in more than one collection (a show_in_channel reply, or the thread
5125
- * parent) is reflected TWICE: two writes to the same canonical slot, each fanning out to the
5126
- * other holder (double re-projection) and minting a fresh ref that defeats the reconcile
5127
- * ref-equality bail. Idempotent (counts come wholesale from the event) so the result is correct,
5128
- * just wasteful. The store already fans out to every holder, so reflect once (by id, if held) and
5129
- * retire the per-collection reflect calls + parent path + enforce_unique branch.
5130
- */
5131
- this.reflectReaction = ({
5132
- enforceUnique = false,
5133
- message,
5134
- reaction,
5135
- removed = false
5136
- }) => {
5137
- const formatted = formatMessage(message);
5138
- const existing = this.getItem(formatted.id);
5139
- const baseOwnReactions = existing?.own_reactions ?? formatted.own_reactions ?? [];
5140
- const own_reactions = computeOwnReactions({
5141
- current: baseOwnReactions,
5142
- enforceUnique,
5143
- reaction,
5144
- removed,
5145
- userId: this.channel.getClient().userId
5146
- });
5147
- this.ingestItem({ ...formatted, own_reactions });
5148
- };
5149
5180
  /**
5150
5181
  * Map a timestamp to a loaded message — the first message in the latest (head) window whose
5151
5182
  * `created_at` is >= `timestamp` (mirrors the legacy `ChannelState.findMessageByTimestamp`
@@ -5883,6 +5914,7 @@ var PENDING_UPLOAD_STATES = [
5883
5914
  ];
5884
5915
  var isPendingUpload = (attachment) => isLocalUploadAttachment(attachment) && PENDING_UPLOAD_STATES.includes(attachment.localMetadata.uploadState);
5885
5916
  var isFinishedUpload = (attachment) => isLocalUploadAttachment(attachment) && attachment.localMetadata.uploadState === "finished";
5917
+ var isFailedUpload = (attachment) => isLocalUploadAttachment(attachment) && attachment.localMetadata.uploadState === "failed";
5886
5918
  var isFileAttachment = (attachment, supportedVideoFormat = []) => attachment.type === "file" || !!(attachment.custom?.mime_type && supportedVideoFormat.indexOf(
5887
5919
  attachment.custom?.mime_type
5888
5920
  ) === -1 && attachment.type !== "video");
@@ -5901,6 +5933,27 @@ var isUploadedAttachment = (attachment) => isAudioAttachment(attachment) || isFi
5901
5933
  var isSharedLocationResponseData = (location) => !!location.latitude && !!location.longitude && !!location.channel_cid;
5902
5934
  var isGiphyAttachment = (attachment) => attachment.type === "giphy";
5903
5935
 
5936
+ // src/messageComposer/attachmentUtils.ts
5937
+ var getAttachmentPreviewUrl = (attachment, ...urls) => {
5938
+ const resolved = urls.find(Boolean);
5939
+ if (resolved) return resolved;
5940
+ return isLocalUploadAttachment(attachment) ? attachment.localMetadata.previewUri : void 0;
5941
+ };
5942
+ var resolveAttachmentFileSize = (attachment) => attachment?.custom?.file_size ?? attachment?.file_size;
5943
+ var resolveAttachmentFullByteSize = (attachment) => {
5944
+ const fromFile = attachment?.localMetadata?.file?.size;
5945
+ if (typeof fromFile === "number" && Number.isFinite(fromFile) && fromFile >= 0) {
5946
+ return fromFile;
5947
+ }
5948
+ const raw = resolveAttachmentFileSize(attachment);
5949
+ if (typeof raw === "number" && Number.isFinite(raw) && raw >= 0) return raw;
5950
+ if (typeof raw === "string") {
5951
+ const parsed = parseFloat(raw);
5952
+ if (Number.isFinite(parsed) && parsed >= 0) return parsed;
5953
+ }
5954
+ return void 0;
5955
+ };
5956
+
5904
5957
  // src/messageComposer/fileUtils.ts
5905
5958
  var isFile = (fileLike) => !!fileLike.lastModified && !("uri" in fileLike);
5906
5959
  var isFileList = (obj) => {
@@ -6167,6 +6220,12 @@ var import_axios = __toESM(require("axios"));
6167
6220
  var import_state_store7 = require("@stream-io/state-store");
6168
6221
  var logger3 = chatLoggerSystem.getLogger("upload-manager");
6169
6222
  var isUploadCancellation = (error) => import_axios.default.isCancel(error) || error instanceof Error && error.name === "AbortError";
6223
+ var isUploadConfirmationPending = (localMetadata) => {
6224
+ if (!localMetadata || localMetadata.uploadState !== "uploading") return false;
6225
+ if (localMetadata.uploadConfirmationPending !== void 0)
6226
+ return localMetadata.uploadConfirmationPending;
6227
+ return localMetadata.uploadProgress !== void 0 && localMetadata.uploadProgress >= 100;
6228
+ };
6170
6229
  var initState = () => ({ uploads: {} });
6171
6230
  var upsertById = (uploads, record) => ({
6172
6231
  ...uploads,
@@ -6290,12 +6349,12 @@ var UploadManager = class {
6290
6349
  this.state = new import_state_store7.StateStore(initState());
6291
6350
  }
6292
6351
  resolveAttachmentManager(channelCid) {
6293
- const colon = channelCid.indexOf(":");
6294
- if (colon <= 0 || colon === channelCid.length - 1) {
6352
+ const colon = ":";
6353
+ const colonIndex = channelCid.indexOf(colon);
6354
+ if (colonIndex <= 0 || colonIndex === channelCid.length - 1) {
6295
6355
  throw new Error(`Invalid channelCid: ${channelCid}`);
6296
6356
  }
6297
- const channelType = channelCid.slice(0, colon);
6298
- const channelId = channelCid.slice(colon + 1);
6357
+ const [channelType, channelId] = channelCid.split(colon);
6299
6358
  return this.client.channel(channelType, channelId).messageComposer.attachmentManager;
6300
6359
  }
6301
6360
  get uploads() {
@@ -7029,13 +7088,15 @@ var composeWithPendingUploads = ({
7029
7088
  composer,
7030
7089
  state
7031
7090
  }) => {
7032
- const composerIsKeptAsDraft = !!composer.pollId;
7091
+ const composerIsKeptAsDraft = composer.retainsCompositionOnSubmit;
7033
7092
  const relevantAttachments = composer.attachmentManager.attachments.filter(
7034
- (attachment) => isFinishedUpload(attachment) || !composerIsKeptAsDraft && isPendingUpload(attachment)
7093
+ (attachment) => isFinishedUpload(attachment) || !composerIsKeptAsDraft && (isPendingUpload(attachment) || isFailedUpload(attachment))
7035
7094
  );
7036
7095
  const localAttachments = (state.localMessage.attachments ?? []).concat(
7096
+ // Only a finished upload has a URL of its own. Everything else keeps `localMetadata`, which
7097
+ // is what the message list joins to the live `uploadManager` record and what a retry needs.
7037
7098
  relevantAttachments.map(
7038
- (attachment) => isPendingUpload(attachment) ? attachment : localAttachmentToAttachment(attachment)
7099
+ (attachment) => isFinishedUpload(attachment) ? localAttachmentToAttachment(attachment) : attachment
7039
7100
  )
7040
7101
  );
7041
7102
  const messageAttachments = (state.message.attachments ?? []).concat(
@@ -7054,21 +7115,6 @@ var composeWithPendingUploads = ({
7054
7115
  } : state.message
7055
7116
  };
7056
7117
  };
7057
- var createSendWithPendingUploadsAttachmentsMiddleware = (composer) => ({
7058
- allowsPendingUploads: true,
7059
- id: "stream-io/message-composer-middleware/attachments",
7060
- handlers: {
7061
- compose: ({
7062
- state,
7063
- next,
7064
- forward
7065
- }) => {
7066
- const { attachmentManager } = composer;
7067
- if (!attachmentManager) return forward();
7068
- return next(composeWithPendingUploads({ composer, state }));
7069
- }
7070
- }
7071
- });
7072
7118
  var createAttachmentsCompositionMiddleware = (composer) => ({
7073
7119
  id: "stream-io/message-composer-middleware/attachments",
7074
7120
  handlers: {
@@ -7080,6 +7126,9 @@ var createAttachmentsCompositionMiddleware = (composer) => ({
7080
7126
  }) => {
7081
7127
  const { attachmentManager } = composer;
7082
7128
  if (!attachmentManager) return forward();
7129
+ if (attachmentManager.config.pendingUploadsEnabled) {
7130
+ return next(composeWithPendingUploads({ composer, state }));
7131
+ }
7083
7132
  if (attachmentManager.uploadsInProgressCount > 0) {
7084
7133
  composer.client.notifications.addWarning({
7085
7134
  message: "Wait until all attachments have uploaded",
@@ -8616,7 +8665,7 @@ var MessagePaginator = class extends MessageIntervalPaginator {
8616
8665
  const { lastMessage, seededLastMessageAt } = this.aggregateState.getLatestValue();
8617
8666
  const fromMessage = getMessageCreatedAtTimestamp(lastMessage);
8618
8667
  if (fromMessage !== null && seededLastMessageAt !== null) {
8619
- return Math.max(fromMessage, seededLastMessageAt);
8668
+ return asTimestampNS(Math.max(fromMessage, seededLastMessageAt));
8620
8669
  }
8621
8670
  return fromMessage ?? seededLastMessageAt;
8622
8671
  }
@@ -9805,10 +9854,11 @@ var createLinkPreviewsCompositionMiddleware = (composer) => ({
9805
9854
  const linkPreviews = linkPreviewsManager.loadingPreviews.length > 0 ? [] : linkPreviewsManager.loadedPreviews.map(
9806
9855
  (preview) => LinkPreviewsManager.getPreviewData(preview)
9807
9856
  );
9808
- const attachments = (state.message.attachments ?? []).concat(
9857
+ const messageAttachments = (state.message.attachments ?? []).concat(
9809
9858
  linkPreviews
9810
9859
  );
9811
- if (!attachments.length) return forward();
9860
+ const localAttachments = (state.localMessage.attachments ?? []).concat(linkPreviews);
9861
+ if (!messageAttachments.length && !localAttachments.length) return forward();
9812
9862
  const sendOptions = { ...state.sendOptions };
9813
9863
  const skip_enrich_url = !someLinkPreviewsLoading && linkPreviews.length > 0 || someLinkPreviewsDismissed;
9814
9864
  if (skip_enrich_url) {
@@ -9816,14 +9866,14 @@ var createLinkPreviewsCompositionMiddleware = (composer) => ({
9816
9866
  }
9817
9867
  return next({
9818
9868
  ...state,
9819
- message: {
9869
+ message: messageAttachments.length ? {
9820
9870
  ...state.message,
9821
- attachments
9822
- },
9823
- localMessage: {
9871
+ attachments: messageAttachments
9872
+ } : state.message,
9873
+ localMessage: localAttachments.length ? {
9824
9874
  ...state.localMessage,
9825
- attachments
9826
- },
9875
+ attachments: localAttachments
9876
+ } : state.localMessage,
9827
9877
  sendOptions
9828
9878
  });
9829
9879
  }
@@ -10180,13 +10230,6 @@ var MessageComposerMiddlewareExecutor = class extends MiddlewareExecutor {
10180
10230
  createCompositionDataCleanupMiddleware(composer)
10181
10231
  ]);
10182
10232
  }
10183
- /**
10184
- * Narrows the base accessor to the composition middleware type, so callers can read the
10185
- * declarations it carries (`allowsPendingUploads`) without casting.
10186
- */
10187
- get installedMiddleware() {
10188
- return super.installedMiddleware;
10189
- }
10190
10233
  };
10191
10234
  var MessageDraftComposerMiddlewareExecutor = class extends MiddlewareExecutor {
10192
10235
  constructor({ composer }) {
@@ -11335,6 +11378,7 @@ var DEFAULT_ATTACHMENT_MANAGER_CONFIG = {
11335
11378
  enabled: true,
11336
11379
  fileUploadFilter: () => true,
11337
11380
  maxNumberOfFilesPerMessage: API_MAX_FILES_ALLOWED_PER_MESSAGE,
11381
+ pendingUploadsEnabled: false,
11338
11382
  trackUploadProgress: true
11339
11383
  };
11340
11384
  var DEFAULT_TEXT_COMPOSER_CONFIG = {
@@ -11356,6 +11400,7 @@ var DEFAULT_COMPOSER_CONFIG = deepFreezeConfig({
11356
11400
  linkPreviews: DEFAULT_LINK_PREVIEW_MANAGER_CONFIG,
11357
11401
  location: DEFAULT_LOCATION_COMPOSER_CONFIG,
11358
11402
  polls: DEFAULT_POLL_COMPOSER_CONFIG,
11403
+ retainCompositionOnSubmit: (composer) => !!composer.pollId,
11359
11404
  text: DEFAULT_TEXT_COMPOSER_CONFIG
11360
11405
  });
11361
11406
 
@@ -11918,6 +11963,123 @@ var mergeServerRestrictions = (requested, restrictions, upperBounds) => {
11918
11963
  // src/thread.ts
11919
11964
  var import_state_store18 = require("@stream-io/state-store");
11920
11965
 
11966
+ // src/messageOperations/applyReactionLocally.ts
11967
+ var applyReactionLocally = (client, {
11968
+ enforceUnique = false,
11969
+ messageId,
11970
+ reaction,
11971
+ removed = false
11972
+ }) => {
11973
+ const store = client.messageStore;
11974
+ const user = client.user;
11975
+ const existing = store.get(messageId);
11976
+ if (!user || !existing) return;
11977
+ const now = nowNs();
11978
+ const reactionResponse = {
11979
+ ...reaction,
11980
+ created_at: reaction.created_at ?? now,
11981
+ custom: reaction.custom ?? {},
11982
+ message_id: messageId,
11983
+ score: reaction.score ?? 1,
11984
+ type: reaction.type,
11985
+ updated_at: reaction.updated_at ?? now,
11986
+ user,
11987
+ user_id: user.id
11988
+ };
11989
+ const removedReactions = removed ? existing.own_reactions?.filter((r) => r.type === reactionResponse.type) ?? [] : enforceUnique ? existing.own_reactions ?? [] : [];
11990
+ const withCounts = removed ? messageWithReactionRemoved(existing, reactionResponse) : messageWithReactionAdded(existing, reactionResponse, enforceUnique);
11991
+ const own_reactions = computeOwnReactions({
11992
+ current: existing.own_reactions ?? [],
11993
+ enforceUnique,
11994
+ reaction: reactionResponse,
11995
+ removed,
11996
+ userId: user.id
11997
+ });
11998
+ store.upsert({ ...withCounts, own_reactions });
11999
+ store.flushSubscribers(messageId);
12000
+ const persisted = store.get(messageId);
12001
+ if (persisted) {
12002
+ client.offlineDb?.executeQuerySafely(
12003
+ (db) => removed ? db.deleteReaction({ message: persisted, reaction: reactionResponse }) : enforceUnique ? db.updateReaction({ message: persisted, reaction: reactionResponse }) : db.insertReaction({ message: persisted, reaction: reactionResponse }),
12004
+ { method: "applyReactionLocally" }
12005
+ );
12006
+ }
12007
+ return () => {
12008
+ if (!removed) {
12009
+ applyReactionLocally(client, {
12010
+ messageId,
12011
+ reaction: reactionResponse,
12012
+ removed: true
12013
+ });
12014
+ }
12015
+ for (const removedReaction of removedReactions) {
12016
+ applyReactionLocally(client, {
12017
+ messageId,
12018
+ reaction: removedReaction,
12019
+ removed: false
12020
+ });
12021
+ }
12022
+ };
12023
+ };
12024
+
12025
+ // src/messageOperations/optimistic.ts
12026
+ var isQueuedForReplay = async (client, messageId, types) => {
12027
+ const { offlineDb } = client;
12028
+ if (!offlineDb?.state.getLatestValue().initialized) return false;
12029
+ const pending = await offlineDb.getPendingTasks({ messageId });
12030
+ return !!pending?.some((task) => types.includes(task.type));
12031
+ };
12032
+ var reconcileHeldMessage = (channel, message) => {
12033
+ if (!message) return;
12034
+ const { messageStore } = channel.getClient();
12035
+ if (!messageStore.has(message.id)) return;
12036
+ messageStore.upsert(formatMessage(message));
12037
+ };
12038
+ var addReactionOptimistically = async ({
12039
+ channel,
12040
+ messageId,
12041
+ options,
12042
+ reaction
12043
+ }) => {
12044
+ const client = channel.getClient();
12045
+ const { created_at, updated_at, ...restOfReaction } = reaction;
12046
+ const undo = applyReactionLocally(client, {
12047
+ enforceUnique: options?.enforce_unique ?? false,
12048
+ messageId,
12049
+ reaction: {
12050
+ ...restOfReaction,
12051
+ ...created_at ? { created_at: dateToNs(created_at) } : {},
12052
+ ...updated_at ? { updated_at: dateToNs(updated_at) } : {}
12053
+ }
12054
+ });
12055
+ try {
12056
+ const response = await channel.sendReaction({ id: messageId, reaction, ...options });
12057
+ reconcileHeldMessage(channel, response?.message);
12058
+ } catch (error) {
12059
+ if (!await isQueuedForReplay(client, messageId, ["send-reaction"])) undo?.();
12060
+ throw error;
12061
+ }
12062
+ };
12063
+ var deleteReactionOptimistically = async ({
12064
+ channel,
12065
+ messageId,
12066
+ type
12067
+ }) => {
12068
+ const client = channel.getClient();
12069
+ const undo = applyReactionLocally(client, {
12070
+ messageId,
12071
+ reaction: { type },
12072
+ removed: true
12073
+ });
12074
+ try {
12075
+ const response = await channel.deleteReaction({ id: messageId, type });
12076
+ reconcileHeldMessage(channel, response?.message);
12077
+ } catch (error) {
12078
+ if (!await isQueuedForReplay(client, messageId, ["delete-reaction"])) undo?.();
12079
+ throw error;
12080
+ }
12081
+ };
12082
+
11921
12083
  // src/messageOperations/MessageOperationStatePolicy.ts
11922
12084
  var parseError = (error) => {
11923
12085
  const stringError = JSON.stringify(error);
@@ -12172,15 +12334,57 @@ var MessageOperations = class {
12172
12334
  });
12173
12335
  }
12174
12336
  /**
12175
- * The shared lifecycle: apply the optimistic state, fire the request, then reconcile or record the
12176
- * failure. `kind` is threaded through because the three operations want materially different state
12177
- * transitions — see {@link MessageOperationStatePolicy}.
12337
+ * Resolves any in-flight upload and folds the URLs back into both payloads. A no-op without
12338
+ * {@link MessageOperationsContext.settlePendingUploads} or a pending attachment.
12339
+ */
12340
+ async settlePendingUploads(params) {
12341
+ const settle = this.ctx.settlePendingUploads;
12342
+ const attachments = params.localMessage.attachments;
12343
+ if (!settle || !attachments?.length) return { params };
12344
+ const { attachments: settledAttachments, failureReason } = await settle(attachments);
12345
+ if (settledAttachments === attachments) return { failureReason, params };
12346
+ const wirePayload = "message" in params && params.message ? { message: { ...params.message, attachments: settledAttachments } } : {};
12347
+ return {
12348
+ failureReason,
12349
+ params: {
12350
+ ...params,
12351
+ ...wirePayload,
12352
+ localMessage: { ...params.localMessage, attachments: settledAttachments }
12353
+ }
12354
+ };
12355
+ }
12356
+ /**
12357
+ * Applies the owner's request-ordering policy for this kind, if it supplied one.
12358
+ */
12359
+ withRequestSequencing(kind, request) {
12360
+ return this.ctx.sequenceRequests ? this.ctx.sequenceRequests(kind, request) : request();
12361
+ }
12362
+ /**
12363
+ * The shared lifecycle: apply the optimistic state, settle any attachment upload still in flight,
12364
+ * fire the request, then reconcile or record the failure. `kind` is threaded through because the
12365
+ * three operations want materially different state transitions — see
12366
+ * {@link MessageOperationStatePolicy}.
12178
12367
  */
12179
12368
  async run(kind, params, doRequest) {
12180
12369
  const messageId = params.localMessage.id;
12181
12370
  const optimistic = this.policy.optimistic(kind, params);
12371
+ let effective = params;
12182
12372
  try {
12183
- const response = await doRequest(params);
12373
+ const response = await this.withRequestSequencing(kind, async () => {
12374
+ if (kind !== "delete") {
12375
+ const settled2 = await this.settlePendingUploads(params);
12376
+ effective = settled2.params;
12377
+ if (settled2.failureReason) throw settled2.failureReason;
12378
+ const settledAttachments = effective.localMessage.attachments;
12379
+ const held = settledAttachments === params.localMessage.attachments ? void 0 : this.ctx.get(messageId);
12380
+ if (held) {
12381
+ const withUrls = { ...held, attachments: settledAttachments };
12382
+ this.ctx.ingest(withUrls);
12383
+ this.ctx.persist(withUrls);
12384
+ }
12385
+ }
12386
+ return await doRequest(effective);
12387
+ });
12184
12388
  this.policy.success({
12185
12389
  kind,
12186
12390
  messageFromResponse: response?.message,
@@ -12192,7 +12396,7 @@ var MessageOperations = class {
12192
12396
  await this.policy.failure({
12193
12397
  error: e,
12194
12398
  kind,
12195
- localMessage: params.localMessage,
12399
+ localMessage: effective.localMessage,
12196
12400
  messageId,
12197
12401
  optimistic,
12198
12402
  options: params.options
@@ -12200,6 +12404,13 @@ var MessageOperations = class {
12200
12404
  throw e;
12201
12405
  }
12202
12406
  }
12407
+ /*
12408
+ * Each operation resolves its request as `requestFn ?? handlers.<kind> ?? defaults.<kind>`.
12409
+ *
12410
+ * `handlers` is the integrator seam (`ChannelConfig.requestHandlers`). `requestFn` is a direct
12411
+ * injection point for tests; it is not exposed on the public methods because it wins over
12412
+ * `handlers`, so a caller reaching for it would bypass whatever the host SDK registered.
12413
+ */
12203
12414
  async send(params, requestFn) {
12204
12415
  const handlers = this.ctx.handlers();
12205
12416
  const messageToSend = this.normalizeMessage(
@@ -12272,143 +12483,252 @@ var MessageOperations = class {
12272
12483
  const doRequest = requestFn ?? handlers.delete ?? (async (p) => await this.ctx.defaults.delete(p.localMessage.id, p.options));
12273
12484
  return await this.run("delete", params, doRequest);
12274
12485
  }
12486
+ async retrySendWithLocalUpdate({
12487
+ localMessage,
12488
+ options
12489
+ }) {
12490
+ await this.retry({ localMessage: { ...localMessage, type: "regular" }, options });
12491
+ }
12492
+ /**
12493
+ * Adds a reaction with an optimistic local state update — see {@link addReactionOptimistically}.
12494
+ *
12495
+ * The request routes through the channel because reactions are channel-level, while the local write
12496
+ * is addressed by message id and so reaches a pure thread reply that no channel collection holds.
12497
+ */
12498
+ async addReactionWithLocalUpdate(params) {
12499
+ await addReactionOptimistically({ channel: this.ctx.channel, ...params });
12500
+ }
12501
+ /**
12502
+ * Removes the current user's reaction with an optimistic local state update, mirroring
12503
+ * {@link MessageOperations.addReactionWithLocalUpdate}.
12504
+ */
12505
+ async deleteReactionWithLocalUpdate(params) {
12506
+ await deleteReactionOptimistically({ channel: this.ctx.channel, ...params });
12507
+ }
12275
12508
  };
12276
12509
 
12277
- // src/entityStore/applyReactionLocally.ts
12278
- var applyReactionLocally = (client, {
12279
- enforceUnique = false,
12280
- messageId,
12281
- reaction,
12282
- removed = false
12283
- }) => {
12284
- const store = client.messageStore;
12285
- const user = client.user;
12286
- const existing = store.get(messageId);
12287
- if (!user || !existing) return;
12288
- const now = nowNs();
12289
- const reactionResponse = {
12290
- ...reaction,
12291
- created_at: reaction.created_at ?? now,
12292
- custom: reaction.custom ?? {},
12293
- message_id: messageId,
12294
- score: reaction.score ?? 1,
12295
- type: reaction.type,
12296
- updated_at: reaction.updated_at ?? now,
12297
- user,
12298
- user_id: user.id
12299
- };
12300
- const removedReactions = removed ? existing.own_reactions?.filter((r) => r.type === reactionResponse.type) ?? [] : enforceUnique ? existing.own_reactions ?? [] : [];
12301
- const withCounts = removed ? messageWithReactionRemoved(existing, reactionResponse) : messageWithReactionAdded(existing, reactionResponse, enforceUnique);
12302
- const own_reactions = computeOwnReactions({
12303
- current: existing.own_reactions ?? [],
12304
- enforceUnique,
12305
- reaction: reactionResponse,
12306
- removed,
12307
- userId: user.id
12308
- });
12309
- store.upsert({ ...withCounts, own_reactions });
12310
- store.flushSubscribers(messageId);
12311
- const persisted = store.get(messageId);
12312
- if (persisted) {
12313
- client.offlineDb?.executeQuerySafely(
12314
- (db) => removed ? db.deleteReaction({ message: persisted, reaction: reactionResponse }) : enforceUnique ? db.updateReaction({ message: persisted, reaction: reactionResponse }) : db.insertReaction({ message: persisted, reaction: reactionResponse }),
12315
- { method: "applyReactionLocally" }
12510
+ // src/messageOperations/persistence.ts
12511
+ var createMessageOperationsPersistence = ({
12512
+ channel
12513
+ }) => ({
12514
+ persist: (message) => {
12515
+ channel.getClient().offlineDb?.executeQuerySafely(
12516
+ (db) => db.upsertMessageWithChannelGuard({ message: { ...message, cid: channel.cid } }),
12517
+ { method: "messageOperations:persist" }
12316
12518
  );
12519
+ },
12520
+ purge: (id) => {
12521
+ channel.getClient().offlineDb?.executeQuerySafely((db) => db.hardDeleteMessage({ id }), {
12522
+ method: "messageOperations:purge"
12523
+ });
12524
+ },
12525
+ isQueued: (messageId, types) => isQueuedForReplay(channel.getClient(), messageId, types)
12526
+ });
12527
+
12528
+ // src/messageOperations/sendOrdering.ts
12529
+ var keepSendOrderWhilePendingUploadsAllowed = ({
12530
+ channelCid,
12531
+ composer,
12532
+ kind,
12533
+ request
12534
+ }) => (kind === "send" || kind === "retry") && composer.allowsPendingUploads ? withoutConcurrency(`stream-chat/send-message/${channelCid}`, request) : request();
12535
+
12536
+ // src/messageOperations/settlePendingAttachmentUploads.ts
12537
+ var applyUploadResult = (attachment, response) => {
12538
+ const enriched = { ...attachment };
12539
+ if (isLocalImageAttachment(enriched)) {
12540
+ enriched.image_url = response.file;
12541
+ } else {
12542
+ enriched.asset_url = response.file;
12317
12543
  }
12318
- return () => {
12319
- if (!removed) {
12320
- applyReactionLocally(client, {
12321
- messageId,
12322
- reaction: reactionResponse,
12323
- removed: true
12324
- });
12544
+ if (response.thumb_url) {
12545
+ enriched.thumb_url = response.thumb_url;
12546
+ }
12547
+ const { previewUri } = attachment.localMetadata;
12548
+ if (previewUri?.startsWith("blob:")) URL.revokeObjectURL?.(previewUri);
12549
+ const { localMetadata: _localMetadata, ...rest } = enriched;
12550
+ return rest;
12551
+ };
12552
+ var settlePendingAttachmentUploads = async ({
12553
+ attachments,
12554
+ channelCid,
12555
+ client
12556
+ }) => {
12557
+ const unsettledIndexes = attachments.reduce((indexes, attachment, index) => {
12558
+ if (isPendingUpload(attachment) || isFailedUpload(attachment)) indexes.push(index);
12559
+ return indexes;
12560
+ }, []);
12561
+ if (!unsettledIndexes.length) return { attachments };
12562
+ const settled2 = await Promise.allSettled(
12563
+ unsettledIndexes.map((index) => {
12564
+ const { file, id } = attachments[index].localMetadata;
12565
+ return client.uploadManager.upload({ channelCid, file, id });
12566
+ })
12567
+ );
12568
+ const nextAttachments = [...attachments];
12569
+ let failureReason;
12570
+ settled2.forEach((result, position) => {
12571
+ const index = unsettledIndexes[position];
12572
+ const attachment = nextAttachments[index];
12573
+ if (result.status === "fulfilled" && result.value) {
12574
+ nextAttachments[index] = applyUploadResult(attachment, result.value);
12575
+ return;
12325
12576
  }
12326
- for (const removedReaction of removedReactions) {
12327
- applyReactionLocally(client, {
12328
- messageId,
12329
- reaction: removedReaction,
12330
- removed: false
12331
- });
12577
+ if (!failureReason) {
12578
+ failureReason = result.status === "rejected" ? result.reason : new Error("Attachment upload returned no result");
12332
12579
  }
12333
- };
12580
+ });
12581
+ return { attachments: nextAttachments, failureReason };
12334
12582
  };
12335
12583
 
12336
- // src/messageOperations/optimistic.ts
12337
- var isQueuedForReplay = async (client, messageId, types) => {
12338
- const { offlineDb } = client;
12339
- if (!offlineDb?.state.getLatestValue().initialized) return false;
12340
- const pending = await offlineDb.getPendingTasks({ messageId });
12341
- return !!pending?.some((task) => types.includes(task.type));
12342
- };
12343
- var reconcileHeldMessage = (channel, message) => {
12344
- if (!message) return;
12345
- const { messageStore } = channel.getClient();
12346
- if (!messageStore.has(message.id)) return;
12347
- messageStore.upsert(formatMessage(message));
12584
+ // src/messageOperations/createMessageOperations.ts
12585
+ var createMessageOperations = (collection) => {
12586
+ const paginator = collection.messagePaginator;
12587
+ const { channel, parentMessageId } = paginator;
12588
+ return new MessageOperations({
12589
+ sequenceRequests: (kind, request) => keepSendOrderWhilePendingUploadsAllowed({
12590
+ channelCid: channel.cid,
12591
+ composer: collection.messageComposer,
12592
+ kind,
12593
+ request
12594
+ }),
12595
+ settlePendingUploads: (attachments) => settlePendingAttachmentUploads({
12596
+ attachments,
12597
+ // A reply uploads against the channel the thread belongs to - there is no thread-scoped
12598
+ // upload target.
12599
+ channelCid: channel.cid,
12600
+ client: channel.getClient()
12601
+ }),
12602
+ ...createMessageOperationsPersistence({ channel }),
12603
+ channel,
12604
+ ingest: (m) => {
12605
+ const store = channel.getClient().messageStore;
12606
+ if (paginator.matchesFilter(m)) {
12607
+ paginator.ingestItem(m);
12608
+ } else if (store.has(m.id)) {
12609
+ store.upsert(m);
12610
+ }
12611
+ store.flushSubscribers(m.id);
12612
+ },
12613
+ // Mirrors `ingest`'s routing: a message this paginator does not hold can still be held by the
12614
+ // client-global store (a thread parent, a message displayed by another collection), and the
12615
+ // policy uses this both for its freshness comparison and to decide whether there is anything to
12616
+ // update optimistically at all. Reading only the paginator would make those two disagree.
12617
+ get: (id) => paginator.getItem(id) ?? channel.getClient().messageStore.get(id),
12618
+ remove: (id) => {
12619
+ const client = channel.getClient();
12620
+ const parentId = paginator.getItem(id)?.parent_id ?? client.messageStore.get(id)?.parent_id;
12621
+ channel.messagePaginator.removeItem({ id });
12622
+ channel.pinnedMessagesPaginator.removeItem({ id });
12623
+ if (parentId) {
12624
+ client.threads.threadsById[parentId]?.messagePaginator.removeItem({ id });
12625
+ }
12626
+ },
12627
+ ...parentMessageId ? {
12628
+ normalizeOutgoingMessage: (m) => ({ ...m, parent_id: parentMessageId })
12629
+ } : {},
12630
+ handlers: () => {
12631
+ const { requestHandlers } = channel.configState.getLatestValue();
12632
+ const deleteMessageRequest = requestHandlers?.deleteMessageRequest;
12633
+ const sendMessageRequest = requestHandlers?.sendMessageRequest;
12634
+ const retrySendMessageRequest = requestHandlers?.retrySendMessageRequest;
12635
+ const updateMessageRequest = requestHandlers?.updateMessageRequest;
12636
+ return {
12637
+ delete: deleteMessageRequest ? (p) => deleteMessageRequest({
12638
+ localMessage: p.localMessage,
12639
+ options: p.options
12640
+ }) : void 0,
12641
+ send: sendMessageRequest ? (p) => sendMessageRequest({
12642
+ localMessage: p.localMessage,
12643
+ message: p.message,
12644
+ options: p.options
12645
+ }) : void 0,
12646
+ retry: retrySendMessageRequest ? (p) => retrySendMessageRequest({
12647
+ localMessage: p.localMessage,
12648
+ message: p.message,
12649
+ options: p.options
12650
+ }) : void 0,
12651
+ update: updateMessageRequest ? (p) => updateMessageRequest({
12652
+ localMessage: p.localMessage,
12653
+ options: p.options
12654
+ }) : void 0
12655
+ };
12656
+ },
12657
+ defaults: {
12658
+ delete: async (id, o) => {
12659
+ const result = await channel.getClient().deleteMessage({ id, ...o });
12660
+ return { message: result.message };
12661
+ },
12662
+ send: async (m, o) => {
12663
+ const result = await channel.sendMessage({ message: m, ...o });
12664
+ return { message: result.message };
12665
+ },
12666
+ update: async (m, o) => {
12667
+ const result = await channel.getClient().updateMessage({
12668
+ id: m.id,
12669
+ message: localMessageToNewMessagePayload(m),
12670
+ ...o
12671
+ });
12672
+ return { message: result.message };
12673
+ }
12674
+ }
12675
+ });
12348
12676
  };
12349
- var addReactionOptimistically = async ({
12350
- channel,
12351
- messageId,
12352
- options,
12353
- reaction
12354
- }) => {
12355
- const client = channel.getClient();
12356
- const { created_at, updated_at, ...restOfReaction } = reaction;
12357
- const undo = applyReactionLocally(client, {
12358
- enforceUnique: options?.enforce_unique ?? false,
12359
- messageId,
12360
- reaction: {
12361
- ...restOfReaction,
12362
- ...created_at ? { created_at: dateToNs(created_at) } : {},
12363
- ...updated_at ? { updated_at: dateToNs(updated_at) } : {}
12677
+
12678
+ // src/messageOperations/WithMessageOperations.ts
12679
+ var WithMessageOperations = (Base) => {
12680
+ class WithMessageOperationsMixin extends Base {
12681
+ /** Sends a message with optimistic local state update. */
12682
+ async sendMessageWithLocalUpdate(params) {
12683
+ await this.messageOperations.send(params);
12684
+ }
12685
+ /** Retry sending a failed message. */
12686
+ async retrySendMessageWithLocalUpdate(params) {
12687
+ await this.messageOperations.retrySendWithLocalUpdate(params);
12688
+ }
12689
+ /** Updates a message with optimistic local state update. */
12690
+ async updateMessageWithLocalUpdate(params) {
12691
+ await this.messageOperations.update(params);
12692
+ }
12693
+ /** Deletes a message with local state update. */
12694
+ async deleteMessageWithLocalUpdate(params) {
12695
+ await this.messageOperations.delete(params);
12696
+ }
12697
+ /** Adds a reaction with an optimistic local state update. */
12698
+ async addReactionWithLocalUpdate(params) {
12699
+ await this.messageOperations.addReactionWithLocalUpdate(params);
12700
+ }
12701
+ /** Removes the current user's reaction with an optimistic local state update. */
12702
+ async deleteReactionWithLocalUpdate(params) {
12703
+ await this.messageOperations.deleteReactionWithLocalUpdate(params);
12364
12704
  }
12365
- });
12366
- try {
12367
- const response = await channel.sendReaction({ id: messageId, reaction, ...options });
12368
- reconcileHeldMessage(channel, response?.message);
12369
- } catch (error) {
12370
- if (!await isQueuedForReplay(client, messageId, ["send-reaction"])) undo?.();
12371
- throw error;
12372
12705
  }
12706
+ return WithMessageOperationsMixin;
12373
12707
  };
12374
- var deleteReactionOptimistically = async ({
12375
- channel,
12376
- messageId,
12377
- type
12708
+
12709
+ // src/messageOperations/reflectReactionEvent.ts
12710
+ var reflectReactionEvent = (client, {
12711
+ enforceUnique = false,
12712
+ message,
12713
+ reaction,
12714
+ removed = false
12378
12715
  }) => {
12379
- const client = channel.getClient();
12380
- const undo = applyReactionLocally(client, {
12381
- messageId,
12382
- reaction: { type },
12383
- removed: true
12716
+ const store = client.messageStore;
12717
+ if (!store.has(message.id)) return;
12718
+ const formatted = formatMessage(message);
12719
+ const existing = store.get(message.id);
12720
+ store.upsert({
12721
+ ...formatted,
12722
+ own_reactions: computeOwnReactions({
12723
+ current: existing?.own_reactions ?? formatted.own_reactions ?? [],
12724
+ enforceUnique,
12725
+ reaction,
12726
+ removed,
12727
+ userId: client.userId
12728
+ })
12384
12729
  });
12385
- try {
12386
- const response = await channel.deleteReaction({ id: messageId, type });
12387
- reconcileHeldMessage(channel, response?.message);
12388
- } catch (error) {
12389
- if (!await isQueuedForReplay(client, messageId, ["delete-reaction"])) undo?.();
12390
- throw error;
12391
- }
12392
12730
  };
12393
12731
 
12394
- // src/messageOperations/persistence.ts
12395
- var createMessageOperationsPersistence = ({
12396
- channel
12397
- }) => ({
12398
- persist: (message) => {
12399
- channel.getClient().offlineDb?.executeQuerySafely(
12400
- (db) => db.upsertMessageWithChannelGuard({ message: { ...message, cid: channel.cid } }),
12401
- { method: "messageOperations:persist" }
12402
- );
12403
- },
12404
- purge: (id) => {
12405
- channel.getClient().offlineDb?.executeQuerySafely((db) => db.hardDeleteMessage({ id }), {
12406
- method: "messageOperations:purge"
12407
- });
12408
- },
12409
- isQueued: (messageId, types) => isQueuedForReplay(channel.getClient(), messageId, types)
12410
- });
12411
-
12412
12732
  // src/configuration/utils/declarativeSlices.ts
12413
12733
  var mergeDeclarativeSlice = (general, specific) => {
12414
12734
  if (!general) return specific;
@@ -12434,7 +12754,7 @@ var DEFAULT_PAGE_LIMIT = 50;
12434
12754
  var DEFAULT_SORT2 = [{ field: "created_at", direction: -1 }];
12435
12755
  var DEFAULT_ITEM_ORDER = [{ field: "created_at", direction: 1 }];
12436
12756
  var DEFAULT_THREAD_CONFIG = deepFreezeConfig({});
12437
- var Thread = class extends WithSubscriptions {
12757
+ var Thread = class extends WithMessageOperations(WithSubscriptions) {
12438
12758
  constructor({
12439
12759
  client,
12440
12760
  threadData,
@@ -12786,24 +13106,7 @@ var Thread = class extends WithSubscriptions {
12786
13106
  const unsubscribeReactions = reactionTypes.map(
12787
13107
  (eventType) => this.client.on(eventType, (event) => {
12788
13108
  if (!event.message || !event.reaction) return;
12789
- const { message, reaction } = event;
12790
- if (message.parent_id === this.id) {
12791
- this.messagePaginator.reflectReaction({
12792
- enforceUnique: eventType === "reaction.updated",
12793
- message,
12794
- reaction,
12795
- removed: eventType === "reaction.deleted"
12796
- });
12797
- } else if (!message.parent_id && message.id === this.id) {
12798
- const own_reactions = computeOwnReactions({
12799
- current: this.state.getLatestValue().parentMessage?.own_reactions ?? message.own_reactions ?? [],
12800
- enforceUnique: eventType === "reaction.updated",
12801
- reaction,
12802
- removed: eventType === "reaction.deleted",
12803
- userId: this.client.userId
12804
- });
12805
- this.updateParentMessageLocally({ message: { ...message, own_reactions } });
12806
- }
13109
+ const { message } = event;
12807
13110
  this.messagePaginator.reflectQuotedMessageUpdate(formatMessage(message));
12808
13111
  }).unsubscribe
12809
13112
  );
@@ -12869,6 +13172,7 @@ var Thread = class extends WithSubscriptions {
12869
13172
  } else if (this.failedRepliesMap.has(message.id)) {
12870
13173
  this.failedRepliesMap.delete(message.id);
12871
13174
  }
13175
+ if (this.messagePaginator.getItem(formattedMessage.id) === formattedMessage) return;
12872
13176
  this.messagePaginator.ingestItem(formattedMessage);
12873
13177
  };
12874
13178
  // todo: can be removed with the next breaking change and use MessagePaginator only
@@ -13008,77 +13312,7 @@ var Thread = class extends WithSubscriptions {
13008
13312
  composition: threadData?.draft ?? draft,
13009
13313
  compositionContext: this
13010
13314
  });
13011
- this.messageOperations = new MessageOperations({
13012
- ...createMessageOperationsPersistence({ channel: this.channel }),
13013
- ingest: (m) => {
13014
- const store = this.channel.getClient().messageStore;
13015
- if (this.messagePaginator.matchesFilter(m)) {
13016
- this.messagePaginator.ingestItem(m);
13017
- } else if (store.has(m.id)) {
13018
- store.upsert(m);
13019
- }
13020
- store.flushSubscribers(m.id);
13021
- },
13022
- // Mirrors `ingest`'s routing: a message this paginator does not hold can still be held by the
13023
- // client-global store (a thread parent, a message displayed by another collection), and the
13024
- // policy uses this both for its freshness comparison and to decide whether there is anything to
13025
- // update optimistically at all. Reading only the paginator would make those two disagree.
13026
- get: (id) => this.messagePaginator.getItem(id) ?? this.channel.getClient().messageStore.get(id),
13027
- remove: (id) => {
13028
- this.messagePaginator.removeItem({ id });
13029
- this.channel.messagePaginator.removeItem({ id });
13030
- this.channel.pinnedMessagesPaginator.removeItem({ id });
13031
- },
13032
- normalizeOutgoingMessage: (m) => ({
13033
- ...m,
13034
- parent_id: this.id
13035
- }),
13036
- handlers: () => {
13037
- const { requestHandlers } = this.channel.configState.getLatestValue();
13038
- const deleteMessageRequest = requestHandlers?.deleteMessageRequest;
13039
- const sendMessageRequest = requestHandlers?.sendMessageRequest;
13040
- const retrySendMessageRequest = requestHandlers?.retrySendMessageRequest;
13041
- const updateMessageRequest = requestHandlers?.updateMessageRequest;
13042
- return {
13043
- delete: deleteMessageRequest ? (p) => deleteMessageRequest({
13044
- localMessage: p.localMessage,
13045
- options: p.options
13046
- }) : void 0,
13047
- send: sendMessageRequest ? (p) => sendMessageRequest({
13048
- localMessage: p.localMessage,
13049
- message: p.message,
13050
- options: p.options
13051
- }) : void 0,
13052
- retry: retrySendMessageRequest ? (p) => retrySendMessageRequest({
13053
- localMessage: p.localMessage,
13054
- message: p.message,
13055
- options: p.options
13056
- }) : void 0,
13057
- update: updateMessageRequest ? (p) => updateMessageRequest({
13058
- localMessage: p.localMessage,
13059
- options: p.options
13060
- }) : void 0
13061
- };
13062
- },
13063
- defaults: {
13064
- delete: async (id, o) => {
13065
- const result = await this.channel.getClient().deleteMessage({ id, ...o });
13066
- return { message: result.message };
13067
- },
13068
- send: async (m, o) => {
13069
- const result = await this.channel.sendMessage({ message: m, ...o });
13070
- return { message: result.message };
13071
- },
13072
- update: async (m, o) => {
13073
- const result = await this.channel.getClient().updateMessage({
13074
- id: m.id,
13075
- message: localMessageToNewMessagePayload(m),
13076
- ...o
13077
- });
13078
- return { message: result.message };
13079
- }
13080
- }
13081
- });
13315
+ this.messageOperations = createMessageOperations(this);
13082
13316
  this.initializeConfig(declarativeConfig);
13083
13317
  }
13084
13318
  /**
@@ -13133,90 +13367,6 @@ var Thread = class extends WithSubscriptions {
13133
13367
  get ownUnreadCount() {
13134
13368
  return ownUnreadCountSelector(this.client.userId)(this.state.getLatestValue());
13135
13369
  }
13136
- /**
13137
- * Sends a message with optimistic local state update.
13138
- */
13139
- async sendMessageWithLocalUpdate({
13140
- localMessage,
13141
- message,
13142
- options,
13143
- sendMessageRequestFn
13144
- }) {
13145
- await this.messageOperations.send(
13146
- {
13147
- localMessage,
13148
- message,
13149
- options
13150
- },
13151
- sendMessageRequestFn
13152
- );
13153
- }
13154
- /**
13155
- * Retry sending a failed message.
13156
- */
13157
- async retrySendMessageWithLocalUpdate(params) {
13158
- await this.messageOperations.retry(
13159
- {
13160
- localMessage: { ...params.localMessage, type: "regular" },
13161
- options: params.options
13162
- },
13163
- params.sendMessageRequestFn
13164
- );
13165
- }
13166
- /**
13167
- * Updates a message with optimistic local state update.
13168
- *
13169
- * The update flows through `messagePaginator`, which is the sole reply source.
13170
- */
13171
- async updateMessageWithLocalUpdate(params) {
13172
- await this.messageOperations.update(
13173
- {
13174
- localMessage: params.localMessage,
13175
- options: params.options
13176
- },
13177
- params.updateMessageRequestFn
13178
- );
13179
- }
13180
- /**
13181
- * Deletes a message with local state update.
13182
- */
13183
- async deleteMessageWithLocalUpdate(params) {
13184
- await this.messageOperations.delete(
13185
- {
13186
- localMessage: params.localMessage,
13187
- options: params.options
13188
- },
13189
- params.deleteMessageRequestFn
13190
- );
13191
- }
13192
- /**
13193
- * Adds a reaction to a reply with an optimistic local state update — see
13194
- * {@link addReactionOptimistically}, which `Channel` shares. The request routes through the parent
13195
- * channel because reactions are channel-level, while the local write is addressed by message id and
13196
- * so reaches a pure reply no channel collection holds.
13197
- */
13198
- async addReactionWithLocalUpdate({
13199
- messageId,
13200
- reaction,
13201
- options
13202
- }) {
13203
- await addReactionOptimistically({
13204
- channel: this.channel,
13205
- messageId,
13206
- options,
13207
- reaction
13208
- });
13209
- }
13210
- /**
13211
- * Removes the current user's reaction from a reply with an optimistic local state update — see
13212
- * {@link deleteReactionOptimistically}, which `Channel` shares.
13213
- */
13214
- async deleteReactionWithLocalUpdate({
13215
- messageId,
13216
- type
13217
- }) {
13218
- await deleteReactionOptimistically({ channel: this.channel, messageId, type });
13219
- }
13220
13370
  };
13221
13371
  var normalizeThreadParticipants = (participants, channelCid) => {
13222
13372
  if (!participants) return void 0;
@@ -13808,6 +13958,99 @@ var _MessageComposer = class _MessageComposer extends WithSubscriptions {
13808
13958
  throw error;
13809
13959
  }
13810
13960
  };
13961
+ /**
13962
+ * Lets go of what the submitted message took with it.
13963
+ *
13964
+ * A poll message carries no text or attachments of its own, so the composer keeps whatever else
13965
+ * was drafted and releases only the poll. Everything else is a full clear.
13966
+ */
13967
+ this.releaseSubmittedComposition = () => {
13968
+ if (this.retainsCompositionOnSubmit) {
13969
+ this.state.partialNext({
13970
+ id: _MessageComposer.generateId(),
13971
+ // Unconditional, and safe whatever decided to retain: a poll that exists is always a poll
13972
+ // that just went out, because `createMessageComposerStateCompositionMiddleware` copies a
13973
+ // set `pollId` onto every composition - threads and edits included, where the poll-only
13974
+ // middleware forwards. There is no state where this detaches one the submission did not
13975
+ // carry.
13976
+ //
13977
+ // Detaching it is also not optional. While `pollId` is set, `hasSendableData` counts it as
13978
+ // content on its own and `createPollOnlyCompositionMiddleware` turns every composition
13979
+ // into a poll-only message, so a composer left bound to a spent poll nullifies whatever is
13980
+ // typed next and re-sends the same poll.
13981
+ pollId: null
13982
+ });
13983
+ return;
13984
+ }
13985
+ this.clear();
13986
+ };
13987
+ this.reportSubmitFailure = (type, message, error) => {
13988
+ this.client.notifications.addError({
13989
+ message,
13990
+ origin: {
13991
+ emitter: "MessageComposer",
13992
+ context: { composer: this }
13993
+ },
13994
+ options: {
13995
+ type,
13996
+ metadata: { reason: error.message },
13997
+ originalError: error instanceof Error ? error : void 0
13998
+ }
13999
+ });
14000
+ };
14001
+ /**
14002
+ * Composes the message and sends it, releasing the composer as it goes.
14003
+ *
14004
+ * Never rejects - see {@link MessageComposerSubmitResult} for what each outcome means and where
14005
+ * the content ends up. A failure is reported through `client.notifications`, and the message
14006
+ * stays in the list marked `failed`, which is where the user retries it. Callers rendering their
14007
+ * own post-send feedback have to check the result.
14008
+ */
14009
+ this.send = async () => {
14010
+ const composition = await this.compose();
14011
+ if (!composition?.message) return "nothing-to-send";
14012
+ const { localMessage, message, sendOptions } = composition;
14013
+ this.releaseSubmittedComposition();
14014
+ try {
14015
+ await this.defaultSubmitTarget.sendMessageWithLocalUpdate({
14016
+ localMessage,
14017
+ message,
14018
+ options: sendOptions
14019
+ });
14020
+ return "sent";
14021
+ } catch (error) {
14022
+ this.reportSubmitFailure(
14023
+ CORE_NOTIFICATION_TYPE.messageSendFailed,
14024
+ "Send message request failed",
14025
+ error
14026
+ );
14027
+ return "failed";
14028
+ }
14029
+ };
14030
+ /**
14031
+ * Composes the edit and saves it, on the same terms as {@link send} - including releasing the
14032
+ * composer before the request rather than after it.
14033
+ */
14034
+ this.update = async () => {
14035
+ const composition = await this.compose();
14036
+ if (!composition?.message) return "nothing-to-send";
14037
+ const { localMessage, sendOptions } = composition;
14038
+ this.releaseSubmittedComposition();
14039
+ try {
14040
+ await this.defaultSubmitTarget.updateMessageWithLocalUpdate({
14041
+ localMessage,
14042
+ options: sendOptions
14043
+ });
14044
+ return "sent";
14045
+ } catch (error) {
14046
+ this.reportSubmitFailure(
14047
+ CORE_NOTIFICATION_TYPE.messageUpdateFailed,
14048
+ "Edit message request failed",
14049
+ error
14050
+ );
14051
+ return "failed";
14052
+ }
14053
+ };
13811
14054
  this.sendLocation = async () => {
13812
14055
  const location = this.locationComposer.validLocation;
13813
14056
  if (this.threadId || !location) return;
@@ -13900,9 +14143,6 @@ var _MessageComposer = class _MessageComposer extends WithSubscriptions {
13900
14143
  if (compositionContext instanceof Thread) {
13901
14144
  return "thread";
13902
14145
  }
13903
- if (typeof compositionContext.legacyThreadId === "string") {
13904
- return "legacy_thread";
13905
- }
13906
14146
  return "message";
13907
14147
  }
13908
14148
  static constructTag(compositionContext) {
@@ -13938,9 +14178,6 @@ var _MessageComposer = class _MessageComposer extends WithSubscriptions {
13938
14178
  if (this.compositionContext instanceof Thread) {
13939
14179
  return this.compositionContext.id;
13940
14180
  }
13941
- if (typeof this.compositionContext.legacyThreadId === "string") {
13942
- return this.compositionContext.legacyThreadId;
13943
- }
13944
14181
  if (typeof this.compositionContext.parent_id === "string") {
13945
14182
  return this.compositionContext.parent_id;
13946
14183
  }
@@ -13972,27 +14209,41 @@ var _MessageComposer = class _MessageComposer extends WithSubscriptions {
13972
14209
  return !currentCommand || this.validateCommandSendability(currentCommand).ready;
13973
14210
  }
13974
14211
  /**
13975
- * Whether the installed composition middleware composes messages whose uploads are still in
13976
- * flight — see `MessageCompositionMiddleware.allowsPendingUploads`.
14212
+ * Whether a message may be sent while its attachments are still uploading — see
14213
+ * {@link AttachmentManagerConfig.pendingUploadsEnabled}.
14214
+ */
14215
+ get allowsPendingUploads() {
14216
+ return this.attachmentManager.config.pendingUploadsEnabled;
14217
+ }
14218
+ /**
14219
+ * Where a composed message goes: the thread when this composer belongs to one, the channel
14220
+ * otherwise.
13977
14221
  *
13978
- * Derived from the chain rather than from config, so there is exactly one switch: the UI SDK
13979
- * that implements the rest of the flow installs the middleware, and everything that has to
13980
- * follow from that (starting with {@link hasSendableData}) follows on its own.
14222
+ * Thread replies live in `thread.messagePaginator`, which is independent of the channel's, so
14223
+ * dispatching to the wrong one leaves the reply invisible where it was written. An edit composer
14224
+ * is built from the message rather than from its thread, so it can only find the thread when the
14225
+ * manager has it loaded. No composer reachable from a UI SDK is built that way today.
14226
+ */
14227
+ get defaultSubmitTarget() {
14228
+ if (this.compositionContext instanceof Thread) return this.compositionContext;
14229
+ const { threadId } = this;
14230
+ return threadId && this.client.threads.threadsById[threadId] || this.channel;
14231
+ }
14232
+ /**
14233
+ * Whether submitting keeps the composer's contents rather than clearing them - see
14234
+ * {@link MessageComposerConfig.retainCompositionOnSubmit}.
13981
14235
  *
13982
- * **Temporary API.** In v10 sending with pending uploads becomes part of the composer
13983
- * configuration (`MessageComposerConfig`, set through `updateConfig`), and this getter,
13984
- * together with the middleware declaration it reads, is replaced by that config field.
14236
+ * Read by the attachments composition step too: content the composer keeps must not also ride
14237
+ * along on the submitted message, or one `localMetadata.id` ends up owned in two places.
13985
14238
  */
13986
- get allowsPendingUploads() {
13987
- return this.compositionMiddlewareExecutor.installedMiddleware.some(
13988
- (middleware) => middleware.allowsPendingUploads
13989
- );
14239
+ get retainsCompositionOnSubmit() {
14240
+ return this.config.retainCompositionOnSubmit(this);
13990
14241
  }
13991
14242
  get hasSendableData() {
13992
14243
  if (!this.isCommandSendable) return false;
13993
14244
  if (this.allowsPendingUploads) {
13994
14245
  const hasSendableAttachment = this.attachmentManager.attachments.some(
13995
- (attachment) => isFinishedUpload(attachment) || isPendingUpload(attachment)
14246
+ (attachment) => isFinishedUpload(attachment) || isPendingUpload(attachment) || isFailedUpload(attachment)
13996
14247
  );
13997
14248
  return !!(!this.textComposer.textIsEmpty || hasSendableAttachment || this.pollId || this.locationComposer.validLocation);
13998
14249
  }
@@ -14420,7 +14671,10 @@ var MessageDeliveryReporter = class _MessageDeliveryReporter {
14420
14671
 
14421
14672
  // src/messageDelivery/MessageReceiptsTracker.ts
14422
14673
  var import_state_store20 = require("@stream-io/state-store");
14423
- var MIN_REF = { timestamp: Number.NEGATIVE_INFINITY, msgId: "" };
14674
+ var MIN_REF = {
14675
+ timestamp: asTimestampNS(Number.NEGATIVE_INFINITY),
14676
+ msgId: ""
14677
+ };
14424
14678
  var isValidReadState = (readState) => !!readState?.user && Number.isFinite(readState.last_read);
14425
14679
  var compareRefsAsc = (a, b) => a.timestamp !== b.timestamp ? a.timestamp - b.timestamp : 0;
14426
14680
  var findIndex = (arr, target, keyOf) => {
@@ -14462,7 +14716,9 @@ var MessageReceiptsTracker = class extends WithSubscriptions {
14462
14716
  this.snapshotStore = new import_state_store20.StateStore({
14463
14717
  revision: 0,
14464
14718
  readersByMessageId: {},
14465
- deliveredByMessageId: {}
14719
+ deliveredByMessageId: {},
14720
+ lastDeliveredRefByOthers: null,
14721
+ lastReadRefByOthers: null
14466
14722
  });
14467
14723
  this.registerSubscriptions = () => {
14468
14724
  this.incrementRefCount();
@@ -14658,6 +14914,21 @@ var MessageReceiptsTracker = class extends WithSubscriptions {
14658
14914
  );
14659
14915
  return this.deliveredSorted.slice(pos).map((x) => x.user);
14660
14916
  }
14917
+ /**
14918
+ * Whether at least one other member has this message delivered.
14919
+ *
14920
+ * Reads the single furthest cursor rather than asking who is past this message, so the answer is
14921
+ * monotonic by construction: an older message can never report less than a newer one.
14922
+ */
14923
+ isDeliveredToOthers(msgRef) {
14924
+ const ref = this.snapshotStore.getLatestValue().lastDeliveredRefByOthers;
14925
+ return !!ref && compareRefsAsc(msgRef, ref) <= 0;
14926
+ }
14927
+ /** As {@link isDeliveredToOthers}, for reads. */
14928
+ isReadByOthers(msgRef) {
14929
+ const ref = this.snapshotStore.getLatestValue().lastReadRefByOthers;
14930
+ return !!ref && compareRefsAsc(msgRef, ref) <= 0;
14931
+ }
14661
14932
  /** Users who delivered but have NOT read. */
14662
14933
  deliveredNotReadForMessage(msgRef) {
14663
14934
  const pos = findIndex(
@@ -14833,6 +15104,19 @@ var MessageReceiptsTracker = class extends WithSubscriptions {
14833
15104
  []
14834
15105
  );
14835
15106
  }
15107
+ /**
15108
+ * The furthest ref in an ascending-by-cursor array, ignoring this user's own progress - your own
15109
+ * cursor says nothing about whether anyone received the message.
15110
+ */
15111
+ furthestRefByOthers(sorted, refOf) {
15112
+ const ownUserId = this.channel.getClient().userID;
15113
+ for (let i = sorted.length - 1; i >= 0; i -= 1) {
15114
+ if (sorted[i].user.id === ownUserId) continue;
15115
+ const ref = refOf(sorted[i]);
15116
+ return Number.isFinite(ref.timestamp) ? ref : null;
15117
+ }
15118
+ return null;
15119
+ }
14836
15120
  emitSnapshot() {
14837
15121
  const readersByMessageId = this.groupUsersByLastReadMessage();
14838
15122
  const deliveredByMessageId = this.groupUsersByLastDeliveredMessage();
@@ -14840,7 +15124,15 @@ var MessageReceiptsTracker = class extends WithSubscriptions {
14840
15124
  this.snapshotStore.next({
14841
15125
  revision: currentSnapshot.revision + 1,
14842
15126
  readersByMessageId,
14843
- deliveredByMessageId
15127
+ deliveredByMessageId,
15128
+ lastDeliveredRefByOthers: this.furthestRefByOthers(
15129
+ this.deliveredSorted,
15130
+ ({ lastDeliveredRef }) => lastDeliveredRef
15131
+ ),
15132
+ lastReadRefByOthers: this.furthestRefByOthers(
15133
+ this.readSorted,
15134
+ ({ lastReadRef }) => lastReadRef
15135
+ )
14844
15136
  });
14845
15137
  }
14846
15138
  };
@@ -15164,7 +15456,7 @@ var DEFAULT_CHANNEL_CONFIG = deepFreezeConfig({
15164
15456
  typingEvents: { enabled: true },
15165
15457
  userMessageReminders: { enabled: true }
15166
15458
  });
15167
- var Channel = class extends ChannelApi {
15459
+ var Channel = class extends WithMessageOperations(ChannelApi) {
15168
15460
  /**
15169
15461
  * Creates a `Channel` instance bound to the given chat client.
15170
15462
  *
@@ -15277,78 +15569,7 @@ var Channel = class extends ChannelApi {
15277
15569
  this.messageReceiptsTracker.registerSubscriptions();
15278
15570
  this.cooldownTimer = new CooldownTimer({ channel: this });
15279
15571
  this.cooldownTimer.registerSubscriptions();
15280
- this.messageOperations = new MessageOperations({
15281
- ...createMessageOperationsPersistence({ channel: this }),
15282
- ingest: (m) => {
15283
- const store = this.getClient().messageStore;
15284
- if (this.messagePaginator.matchesFilter(m)) {
15285
- this.messagePaginator.ingestItem(m);
15286
- } else if (store.has(m.id)) {
15287
- store.upsert(m);
15288
- }
15289
- store.flushSubscribers(m.id);
15290
- },
15291
- // Mirrors `ingest`'s routing: a message this paginator does not hold can still be held by the
15292
- // client-global store (a thread parent, a message displayed by another collection), and the
15293
- // policy uses this both for its freshness comparison and to decide whether there is anything to
15294
- // update optimistically at all. Reading only the paginator would make those two disagree.
15295
- get: (id2) => this.messagePaginator.getItem(id2) ?? this.getClient().messageStore.get(id2),
15296
- remove: (id2) => {
15297
- const parentId = this.messagePaginator.getItem(id2)?.parent_id ?? this.getClient().messageStore.get(id2)?.parent_id;
15298
- this.messagePaginator.removeItem({ id: id2 });
15299
- this.pinnedMessagesPaginator.removeItem({ id: id2 });
15300
- if (parentId) {
15301
- this.getClient().threads.threadsById[parentId]?.messagePaginator.removeItem({
15302
- id: id2
15303
- });
15304
- }
15305
- },
15306
- handlers: () => {
15307
- const { requestHandlers } = this.configState.getLatestValue();
15308
- const deleteMessageRequest = requestHandlers?.deleteMessageRequest;
15309
- const sendMessageRequest = requestHandlers?.sendMessageRequest;
15310
- const retrySendMessageRequest = requestHandlers?.retrySendMessageRequest;
15311
- const updateMessageRequest = requestHandlers?.updateMessageRequest;
15312
- return {
15313
- delete: deleteMessageRequest ? (p) => deleteMessageRequest({
15314
- localMessage: p.localMessage,
15315
- options: p.options
15316
- }) : void 0,
15317
- send: sendMessageRequest ? (p) => sendMessageRequest({
15318
- localMessage: p.localMessage,
15319
- message: p.message,
15320
- options: p.options
15321
- }) : void 0,
15322
- retry: retrySendMessageRequest ? (p) => retrySendMessageRequest({
15323
- localMessage: p.localMessage,
15324
- message: p.message,
15325
- options: p.options
15326
- }) : void 0,
15327
- update: updateMessageRequest ? (p) => updateMessageRequest({
15328
- localMessage: p.localMessage,
15329
- options: p.options
15330
- }) : void 0
15331
- };
15332
- },
15333
- defaults: {
15334
- delete: async (id2, o) => {
15335
- const result = await this.getClient().deleteMessage({ id: id2, ...o });
15336
- return { message: result.message };
15337
- },
15338
- send: async (m, o) => {
15339
- const result = await this.sendMessage({ message: m, ...o });
15340
- return { message: result.message };
15341
- },
15342
- update: async (m, o) => {
15343
- const result = await this.getClient().updateMessage({
15344
- id: m.id,
15345
- message: localMessageToNewMessagePayload(m),
15346
- ...o
15347
- });
15348
- return { message: result.message };
15349
- }
15350
- }
15351
- });
15572
+ this.messageOperations = createMessageOperations(this);
15352
15573
  this._syncMuteStatus();
15353
15574
  this.configController = new ConfigController({
15354
15575
  defaults: DEFAULT_CHANNEL_CONFIG,
@@ -15529,74 +15750,16 @@ var Channel = class extends ChannelApi {
15529
15750
  });
15530
15751
  }
15531
15752
  /**
15532
- * Sends a message with optimistic local state update.
15753
+ * Sends a message with optimistic local state update, stopping the typing indicator first.
15754
+ *
15755
+ * The only collection-specific part of the optimistic API: typing is a channel concern and a
15756
+ * `Thread` has no counterpart, so it is an override rather than part of the mixin.
15533
15757
  */
15534
15758
  async sendMessageWithLocalUpdate(params) {
15535
- await this.messageOperations.send(
15536
- {
15537
- localMessage: params.localMessage,
15538
- message: params.message,
15539
- options: params.options
15540
- },
15541
- params.sendMessageRequestFn
15542
- );
15543
- if (this.messageComposer.config.text.publishTypingEvents) await this.stopTyping();
15544
- }
15545
- /**
15546
- * Retry sending a failed message.
15547
- */
15548
- async retrySendMessageWithLocalUpdate(params) {
15549
- await this.messageOperations.retry(
15550
- {
15551
- localMessage: { ...params.localMessage, type: "regular" },
15552
- options: params.options
15553
- },
15554
- params.sendMessageRequestFn
15555
- );
15556
- }
15557
- /**
15558
- * Updates a message with optimistic local state update.
15559
- */
15560
- async updateMessageWithLocalUpdate(params) {
15561
- await this.messageOperations.update(
15562
- {
15563
- localMessage: params.localMessage,
15564
- options: params.options
15565
- },
15566
- params.updateMessageRequestFn
15567
- );
15568
- }
15569
- /**
15570
- * Deletes a message with local state update.
15571
- */
15572
- async deleteMessageWithLocalUpdate(params) {
15573
- await this.messageOperations.delete(
15574
- {
15575
- localMessage: params.localMessage,
15576
- options: params.options
15577
- },
15578
- params.deleteMessageRequestFn
15579
- );
15580
- }
15581
- /**
15582
- * Adds a reaction with an optimistic local state update - see {@link addReactionOptimistically}.
15583
- */
15584
- async addReactionWithLocalUpdate({
15585
- messageId,
15586
- reaction,
15587
- options
15588
- }) {
15589
- await addReactionOptimistically({ channel: this, messageId, options, reaction });
15590
- }
15591
- /**
15592
- * Removes the current user's reaction with an optimistic local state update - see
15593
- * {@link deleteReactionOptimistically}.
15594
- */
15595
- async deleteReactionWithLocalUpdate({
15596
- messageId,
15597
- type
15598
- }) {
15599
- await deleteReactionOptimistically({ channel: this, messageId, type });
15759
+ if (this.messageComposer.config.text.publishTypingEvents) {
15760
+ this.stopTyping().catch(() => void 0);
15761
+ }
15762
+ await super.sendMessageWithLocalUpdate(params);
15600
15763
  }
15601
15764
  /**
15602
15765
  * Upload a file to this channel's file endpoint (multipart).
@@ -16042,7 +16205,7 @@ var Channel = class extends ChannelApi {
16042
16205
  /**
16043
16206
  * Returns the mute status for the current channel.
16044
16207
  *
16045
- * @returns An object of the form `{ muted: true | false, createdAt: number | null, expiresAt: number | null }`,
16208
+ * @returns An object of the form `{ muted: true | false, createdAt: TimestampNS | null, expiresAt: TimestampNS | null }`,
16046
16209
  * where the timestamps are unix nanoseconds as the API sends them.
16047
16210
  */
16048
16211
  muteStatus() {
@@ -16927,8 +17090,8 @@ var Channel = class extends ChannelApi {
16927
17090
  this.messagePaginator.removeItem({ id: event.message.id });
16928
17091
  this.pinnedMessagesPaginator.removeItem({ id: event.message.id });
16929
17092
  } else {
16930
- this.messagePaginator.ingestItem(formattedMessage);
16931
- this.pinnedMessagesPaginator.ingestItem(formattedMessage);
17093
+ const store = this.getClient().messageStore;
17094
+ if (store.has(event.message.id)) store.upsert(formattedMessage);
16932
17095
  }
16933
17096
  }
16934
17097
  this.messagePaginator.reflectQuotedMessageUpdate(formattedMessage);
@@ -16957,8 +17120,9 @@ var Channel = class extends ChannelApi {
16957
17120
  const ownMessage = event.user?.id === client.user?.id;
16958
17121
  const isThreadMessage = event.message.parent_id && !event.message.show_in_channel;
16959
17122
  if (!isThreadMessage) {
16960
- this.messagePaginator.ingestItem(formatMessage(event.message));
16961
- this.pinnedMessagesPaginator.ingestItem(formatMessage(event.message));
17123
+ const formattedMessage = formatMessage(event.message);
17124
+ this.messagePaginator.ingestItem(formattedMessage);
17125
+ this.pinnedMessagesPaginator.ingestItem(formattedMessage);
16962
17126
  }
16963
17127
  const preventUnreadCountUpdate = ownMessage || isThreadMessage;
16964
17128
  if (preventUnreadCountUpdate) break;
@@ -16994,7 +17158,7 @@ var Channel = class extends ChannelApi {
16994
17158
  }
16995
17159
  if (ownUserId && countsAsOwnUnread && !currentReadState[ownUserId]) {
16996
17160
  nextReadState[ownUserId] = {
16997
- last_read: 0,
17161
+ last_read: asTimestampNS(0),
16998
17162
  unread_messages: 1,
16999
17163
  user: client.user ?? { id: ownUserId }
17000
17164
  };
@@ -17038,8 +17202,9 @@ var Channel = class extends ChannelApi {
17038
17202
  this.pinnedMessagesPaginator.clearStateAndCache();
17039
17203
  }
17040
17204
  if (event.message) {
17041
- this.messagePaginator.ingestItem(formatMessage(event.message));
17042
- this.pinnedMessagesPaginator.ingestItem(formatMessage(event.message));
17205
+ const formattedMessage = formatMessage(event.message);
17206
+ this.messagePaginator.ingestItem(formattedMessage);
17207
+ this.pinnedMessagesPaginator.ingestItem(formattedMessage);
17043
17208
  }
17044
17209
  break;
17045
17210
  }
@@ -17120,49 +17285,16 @@ var Channel = class extends ChannelApi {
17120
17285
  }
17121
17286
  break;
17122
17287
  case "reaction.new":
17123
- if (event.message && event.reaction) {
17124
- const { reaction } = event;
17125
- if (!event.message?.parent_id || event.message.show_in_channel) {
17126
- this.messagePaginator.reflectReaction({ message: event.message, reaction });
17127
- this.pinnedMessagesPaginator.reflectReaction({
17128
- message: event.message,
17129
- reaction
17130
- });
17131
- }
17132
- }
17133
- break;
17134
- case "reaction.deleted":
17135
- if (event.message && event.reaction) {
17136
- const { reaction } = event;
17137
- if (event.message && (!event.message.parent_id || event.message.show_in_channel)) {
17138
- this.messagePaginator.reflectReaction({
17139
- message: event.message,
17140
- reaction,
17141
- removed: true
17142
- });
17143
- this.pinnedMessagesPaginator.reflectReaction({
17144
- message: event.message,
17145
- reaction,
17146
- removed: true
17147
- });
17148
- }
17149
- }
17150
- break;
17151
17288
  case "reaction.updated":
17289
+ case "reaction.deleted":
17152
17290
  if (event.message && event.reaction) {
17153
- const { reaction } = event;
17154
- if (!event.message?.parent_id || event.message.show_in_channel) {
17155
- this.messagePaginator.reflectReaction({
17156
- enforceUnique: true,
17157
- message: event.message,
17158
- reaction
17159
- });
17160
- this.pinnedMessagesPaginator.reflectReaction({
17161
- enforceUnique: true,
17162
- message: event.message,
17163
- reaction
17164
- });
17165
- }
17291
+ reflectReactionEvent(this.getClient(), {
17292
+ // reaction.updated is only sent when enforce_unique is set
17293
+ enforceUnique: event.type === "reaction.updated",
17294
+ message: event.message,
17295
+ reaction: event.reaction,
17296
+ removed: event.type === "reaction.deleted"
17297
+ });
17166
17298
  }
17167
17299
  break;
17168
17300
  case "channel.hidden": {
@@ -17642,8 +17774,9 @@ var ApiClient = class {
17642
17774
  if (limit) rateLimit.rate_limit = parseInt(limit, 10);
17643
17775
  const remaining = headers["x-ratelimit-remaining"];
17644
17776
  if (remaining) rateLimit.rate_limit_remaining = parseInt(remaining, 10);
17645
- const reset = headers["x-ratelimit-reset"];
17646
- if (reset) rateLimit.rate_limit_reset = new Date(reset);
17777
+ const reset = Number(headers["x-ratelimit-reset"]);
17778
+ if (Number.isFinite(reset) && reset > 0)
17779
+ rateLimit.rate_limit_reset = new Date(reset * 1e3);
17647
17780
  return {
17648
17781
  response_headers: headers,
17649
17782
  rate_limit: rateLimit,
@@ -17844,6 +17977,7 @@ var ModerationApi = class {
17844
17977
  appeal_reason: request?.appeal_reason,
17845
17978
  entity_id: request?.entity_id,
17846
17979
  entity_type: request?.entity_type,
17980
+ channel_cid: request?.channel_cid,
17847
17981
  review_queue_item_id: request?.review_queue_item_id,
17848
17982
  attachments: request?.attachments
17849
17983
  };
@@ -23190,6 +23324,7 @@ var ChatApi = class {
23190
23324
  id: request?.id,
23191
23325
  is_closed: request?.is_closed,
23192
23326
  max_votes_allowed: request?.max_votes_allowed,
23327
+ team: request?.team,
23193
23328
  voting_visibility: request?.voting_visibility,
23194
23329
  options: request?.options,
23195
23330
  custom: request?.custom
@@ -24277,7 +24412,7 @@ var StreamChat = class _StreamChat extends ChatApi {
24277
24412
  return this.userAgent;
24278
24413
  }
24279
24414
  if (!this.cachedUserAgent) {
24280
- const version = "10.0.0-rc.12";
24415
+ const version = "10.0.0-rc.14";
24281
24416
  const { name: sdkName, version: sdkVersion } = this.sdkIdentifier ?? {};
24282
24417
  const { name: appName, version: appVersion } = this.appIdentifier ?? {};
24283
24418
  const { os, model: deviceModel } = this.deviceIdentifier ?? {};
@@ -25095,6 +25230,10 @@ var StreamChat = class _StreamChat extends ChatApi {
25095
25230
  /**
25096
25231
  * Pins the message.
25097
25232
  *
25233
+ * A `number` is a relative offset in SECONDS, so a server-sent timestamp (`message.pinned_at`,
25234
+ * `message.pin_expires`) is rejected at compile time rather than read as ~1.79e18 seconds from now.
25235
+ * Hand one back as a date with `nsToDate(timestamp)`.
25236
+ *
25098
25237
  * @param messageOrMessageId - MessageRequest object or message ID.
25099
25238
  * @param timeoutOrExpirationDate - Expiration date or timeout. Use `number` to set the timeout
25100
25239
  * in seconds, `string` or `Date` to set the exact expiration date (optional).
@@ -25473,6 +25612,11 @@ var ATTACHMENTS_FIELDS = {
25473
25612
  kind: "value",
25474
25613
  type: "number"
25475
25614
  },
25615
+ pendingUploadsEnabled: {
25616
+ description: "Lets a message be sent while its attachments are still uploading. The message appears immediately and the request waits for the uploads; one failed upload fails the message.",
25617
+ kind: "value",
25618
+ type: "boolean"
25619
+ },
25476
25620
  trackUploadProgress: {
25477
25621
  description: "Reports upload progress on each attachment. Turning it off skips the progress bookkeeping.",
25478
25622
  kind: "value",
@@ -25593,6 +25737,11 @@ var MESSAGE_COMPOSER_FIELDS = {
25593
25737
  fields: POLLS_FIELDS,
25594
25738
  kind: "group"
25595
25739
  },
25740
+ retainCompositionOnSubmit: {
25741
+ description: "Decides whether submitting keeps the composer's contents instead of clearing them. Retains for a poll message by default.",
25742
+ kind: "value",
25743
+ type: "function"
25744
+ },
25596
25745
  text: { description: "The text input itself.", fields: TEXT_FIELDS, kind: "group" }
25597
25746
  };
25598
25747
  var MESSAGE_DELIVERY_FIELDS = {
@@ -26538,13 +26687,13 @@ var AbstractOfflineDB = class {
26538
26687
  execute = true
26539
26688
  }) => {
26540
26689
  const {
26541
- received_at: last_read = nowNs(),
26542
26690
  last_read_message_id,
26543
26691
  // @ts-expect-error property missing
26544
26692
  unread_messages = 0,
26545
26693
  user,
26546
26694
  cid
26547
26695
  } = event;
26696
+ const last_read = (event.type === "notification.mark_unread" ? event.last_read_at : void 0) ?? event.created_at ?? event.received_at ?? nowNs();
26548
26697
  const overriddenUnreadMessages = unreadMessages ?? unread_messages;
26549
26698
  if (user?.id && cid) {
26550
26699
  return await this.queriesWithChannelGuard(