stream-chat 9.50.3 → 9.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/cjs/index.browser.js +601 -275
  2. package/dist/cjs/index.browser.js.map +4 -4
  3. package/dist/cjs/index.node.js +610 -276
  4. package/dist/cjs/index.node.js.map +4 -4
  5. package/dist/esm/index.mjs +599 -273
  6. package/dist/esm/index.mjs.map +4 -4
  7. package/dist/types/channel.d.ts +4 -3
  8. package/dist/types/client.d.ts +23 -9
  9. package/dist/types/index.d.ts +7 -0
  10. package/dist/types/messageComposer/attachmentIdentity.d.ts +4 -0
  11. package/dist/types/messageComposer/attachmentManager.d.ts +8 -0
  12. package/dist/types/messageComposer/messageComposer.d.ts +13 -0
  13. package/dist/types/messageComposer/middleware/attachmentManager/postUpload/attachmentEnrichment.d.ts +20 -1
  14. package/dist/types/messageComposer/middleware/messageComposer/MessageComposerMiddlewareExecutor.d.ts +6 -1
  15. package/dist/types/messageComposer/middleware/messageComposer/attachments.d.ts +24 -0
  16. package/dist/types/messageComposer/middleware/messageComposer/types.d.ts +22 -1
  17. package/dist/types/messageComposer/middleware/textComposer/mentions.d.ts +15 -9
  18. package/dist/types/messageComposer/types.d.ts +5 -0
  19. package/dist/types/middleware.d.ts +8 -0
  20. package/dist/types/offline-support/offline_support_api.d.ts +7 -1
  21. package/dist/types/offline-support/offline_sync_manager.d.ts +12 -1
  22. package/dist/types/search/BaseSearchSource.d.ts +42 -24
  23. package/dist/types/search/ChannelMemberSearchSource.d.ts +9 -4
  24. package/dist/types/search/ChannelSearchSource.d.ts +9 -3
  25. package/dist/types/search/MessageSearchSource.d.ts +14 -3
  26. package/dist/types/search/UserSearchSource.d.ts +9 -3
  27. package/dist/types/search/types.d.ts +14 -1
  28. package/dist/types/types.d.ts +12 -1
  29. package/dist/types/uploadManager.d.ts +30 -0
  30. package/dist/types/utils.d.ts +21 -2
  31. package/package.json +7 -7
  32. package/src/channel.ts +20 -8
  33. package/src/client.ts +71 -13
  34. package/src/index.ts +12 -0
  35. package/src/messageComposer/attachmentIdentity.ts +27 -0
  36. package/src/messageComposer/attachmentManager.ts +61 -12
  37. package/src/messageComposer/messageComposer.ts +43 -8
  38. package/src/messageComposer/middleware/attachmentManager/postUpload/AttachmentPostUploadMiddlewareExecutor.ts +1 -1
  39. package/src/messageComposer/middleware/attachmentManager/postUpload/attachmentEnrichment.ts +58 -33
  40. package/src/messageComposer/middleware/attachmentManager/postUpload/uploadErrorHandler.ts +4 -0
  41. package/src/messageComposer/middleware/messageComposer/MessageComposerMiddlewareExecutor.ts +9 -0
  42. package/src/messageComposer/middleware/messageComposer/attachments.ts +109 -0
  43. package/src/messageComposer/middleware/messageComposer/types.ts +22 -1
  44. package/src/messageComposer/middleware/textComposer/mentions.ts +75 -23
  45. package/src/messageComposer/types.ts +5 -0
  46. package/src/middleware.ts +11 -0
  47. package/src/offline-support/offline_support_api.ts +21 -2
  48. package/src/offline-support/offline_sync_manager.ts +36 -8
  49. package/src/search/BaseSearchSource.ts +123 -39
  50. package/src/search/ChannelMemberSearchSource.ts +22 -11
  51. package/src/search/ChannelSearchSource.ts +16 -5
  52. package/src/search/MessageSearchSource.ts +36 -4
  53. package/src/search/UserSearchSource.ts +16 -5
  54. package/src/search/types.ts +14 -1
  55. package/src/types.ts +13 -1
  56. package/src/uploadManager.ts +45 -0
  57. package/src/utils.ts +78 -3
@@ -189,6 +189,7 @@ __export(index_exports, {
189
189
  createMessageComposerStateCompositionMiddleware: () => createMessageComposerStateCompositionMiddleware,
190
190
  createPollComposerStateMiddleware: () => createPollComposerStateMiddleware,
191
191
  createPostUploadAttachmentEnrichmentMiddleware: () => createPostUploadAttachmentEnrichmentMiddleware,
192
+ createSendWithPendingUploadsAttachmentsMiddleware: () => createSendWithPendingUploadsAttachmentsMiddleware,
192
193
  createSharedLocationCompositionMiddleware: () => createSharedLocationCompositionMiddleware,
193
194
  createTextComposerCompositionMiddleware: () => createTextComposerCompositionMiddleware,
194
195
  createTextComposerPreValidationMiddleware: () => createTextComposerPreValidationMiddleware,
@@ -217,6 +218,7 @@ __export(index_exports, {
217
218
  getTokenizedSuggestionDisplayName: () => getTokenizedSuggestionDisplayName,
218
219
  getTriggerCharWithToken: () => getTriggerCharWithToken,
219
220
  gunzipPayload: () => gunzipPayload,
221
+ hasPending: () => hasPending,
220
222
  insertItemWithTrigger: () => insertItemWithTrigger,
221
223
  isAudioAttachment: () => isAudioAttachment,
222
224
  isBlobButNotFile: () => isBlobButNotFile,
@@ -224,6 +226,7 @@ __export(index_exports, {
224
226
  isFileAttachment: () => isFileAttachment,
225
227
  isFileList: () => isFileList,
226
228
  isFileReference: () => isFileReference,
229
+ isFinishedUpload: () => isFinishedUpload,
227
230
  isGiphyAttachment: () => isGiphyAttachment,
228
231
  isImageAttachment: () => isImageAttachment,
229
232
  isImageFile: () => isImageFile,
@@ -236,9 +239,11 @@ __export(index_exports, {
236
239
  isLocalVoiceRecordingAttachment: () => isLocalVoiceRecordingAttachment,
237
240
  isOwnUser: () => isOwnUser,
238
241
  isPatch: () => isPatch,
242
+ isPendingUpload: () => isPendingUpload,
239
243
  isScrapedContent: () => isScrapedContent,
240
244
  isSharedLocationResponse: () => isSharedLocationResponse,
241
245
  isTargetedOptionTextUpdate: () => isTargetedOptionTextUpdate,
246
+ isUploadCancellation: () => isUploadCancellation,
242
247
  isUploadedAttachment: () => isUploadedAttachment,
243
248
  isVideoAttachment: () => isVideoAttachment,
244
249
  isVoiceRecordingAttachment: () => isVoiceRecordingAttachment,
@@ -258,12 +263,15 @@ __export(index_exports, {
258
263
  readFileAsArrayBuffer: () => readFileAsArrayBuffer,
259
264
  removeDiacritics: () => removeDiacritics,
260
265
  replaceWordWithEntity: () => replaceWordWithEntity,
266
+ settled: () => settled,
261
267
  stripCommandFromText: () => stripCommandFromText,
262
268
  stripMentionTokens: () => stripMentionTokens,
263
269
  textIsEmpty: () => textIsEmpty,
264
270
  timeLeftMs: () => timeLeftMs,
265
271
  verifyAndParseWebhook: () => verifyAndParseWebhook,
266
- verifySignature: () => verifySignature
272
+ verifySignature: () => verifySignature,
273
+ withCancellation: () => withCancellation,
274
+ withoutConcurrency: () => withoutConcurrency
267
275
  });
268
276
  module.exports = __toCommonJS(index_exports);
269
277
 
@@ -530,7 +538,7 @@ var ChannelBatchUpdater = class {
530
538
  };
531
539
 
532
540
  // src/client.ts
533
- var import_axios3 = __toESM(require("axios"));
541
+ var import_axios4 = __toESM(require("axios"));
534
542
  var import_https = __toESM(require_https());
535
543
 
536
544
  // src/utils.ts
@@ -790,6 +798,38 @@ function unformatMessage(message) {
790
798
  quoted_message: toMessageResponseBase(message.quoted_message)
791
799
  };
792
800
  }
801
+ var isRemoteUrl = (url) => !!url && /^https?:\/\//i.test(url.trim());
802
+ var sanitizeOutgoingAttachments = ({
803
+ client,
804
+ message
805
+ }) => {
806
+ const attachments = message.attachments;
807
+ if (!attachments?.length) return message;
808
+ const unresolved = [];
809
+ let changed = false;
810
+ const sanitized = attachments.reduce((acc, attachment) => {
811
+ const { localMetadata, ...rest } = attachment;
812
+ if (!localMetadata) {
813
+ acc.push(attachment);
814
+ return acc;
815
+ }
816
+ changed = true;
817
+ const hasRemoteSource = isRemoteUrl(rest.asset_url) || isRemoteUrl(rest.image_url) || isRemoteUrl(rest.og_scrape_url) || isRemoteUrl(rest.title_link);
818
+ if (hasRemoteSource) {
819
+ acc.push(rest);
820
+ } else {
821
+ unresolved.push(rest);
822
+ }
823
+ return acc;
824
+ }, []);
825
+ if (unresolved.length) {
826
+ const text = `Dropped ${unresolved.length} attachment(s) whose upload never completed. Composing with pending uploads requires awaiting them before sending.`;
827
+ const extraData = { attachments: unresolved, message, tags: ["attachments"] };
828
+ client.logger("warn", text, extraData);
829
+ console.warn(text, extraData);
830
+ }
831
+ return changed ? { ...message, attachments: sanitized } : message;
832
+ };
793
833
  var localMessageToNewMessagePayload = (localMessage) => {
794
834
  const {
795
835
  // Remove all timestamp fields and client-specific fields.
@@ -998,7 +1038,8 @@ var debounce = (fn, timeout = 0, { leading = false, trailing = true } = {}) => {
998
1038
  }
999
1039
  runningTimeout = null;
1000
1040
  };
1001
- runningTimeout = setTimeout(timeoutHandler, timeout);
1041
+ const delay = typeof timeout === "function" ? timeout(...args) : timeout;
1042
+ runningTimeout = setTimeout(timeoutHandler, delay);
1002
1043
  return lastResult;
1003
1044
  };
1004
1045
  debouncedFn.cancel = () => {
@@ -2561,6 +2602,12 @@ var CooldownTimer = class extends WithSubscriptions {
2561
2602
  var isScrapedContent = (attachment) => !!attachment?.og_scrape_url || !!attachment?.title_link;
2562
2603
  var isLocalAttachment = (attachment) => !!attachment?.localMetadata?.id;
2563
2604
  var isLocalUploadAttachment = (attachment) => !!attachment?.localMetadata?.uploadState;
2605
+ var PENDING_UPLOAD_STATES = [
2606
+ "pending",
2607
+ "uploading"
2608
+ ];
2609
+ var isPendingUpload = (attachment) => isLocalUploadAttachment(attachment) && PENDING_UPLOAD_STATES.includes(attachment.localMetadata.uploadState);
2610
+ var isFinishedUpload = (attachment) => isLocalUploadAttachment(attachment) && attachment.localMetadata.uploadState === "finished";
2564
2611
  var isFileAttachment = (attachment, supportedVideoFormat = []) => attachment.type === "file" || !!(attachment.mime_type && supportedVideoFormat.indexOf(attachment.mime_type) === -1 && attachment.type !== "video");
2565
2612
  var isLocalFileAttachment = (attachment) => isFileAttachment(attachment) && isLocalAttachment(attachment);
2566
2613
  var isImageAttachment = (attachment) => attachment.type === "image" && !isScrapedContent(attachment);
@@ -2636,7 +2683,7 @@ var ensureIsLocalAttachment = (attachment) => {
2636
2683
  };
2637
2684
 
2638
2685
  // src/messageComposer/middleware/attachmentManager/postUpload/attachmentEnrichment.ts
2639
- var createPostUploadAttachmentEnrichmentMiddleware = () => ({
2686
+ var createPostUploadAttachmentEnrichmentMiddleware = (composer) => ({
2640
2687
  id: "stream-io/attachment-manager-middleware/post-upload-enrichment",
2641
2688
  handlers: {
2642
2689
  postProcess: ({
@@ -2649,9 +2696,10 @@ var createPostUploadAttachmentEnrichmentMiddleware = () => ({
2649
2696
  if (error) return forward();
2650
2697
  if (!attachment || !response) return discard();
2651
2698
  const enrichedAttachment = { ...attachment };
2652
- const previewUri = attachment.localMetadata.previewUri;
2653
- if (previewUri) {
2654
- if (previewUri.startsWith("blob:")) URL.revokeObjectURL(previewUri);
2699
+ const { id, previewUri } = attachment.localMetadata;
2700
+ const attachmentIsStillInComposer = !composer || !!composer.attachmentManager.attachmentsById[id];
2701
+ if (previewUri && attachmentIsStillInComposer) {
2702
+ if (previewUri.startsWith("blob:")) URL.revokeObjectURL?.(previewUri);
2655
2703
  delete enrichedAttachment.localMetadata.previewUri;
2656
2704
  }
2657
2705
  if (isLocalImageAttachment(attachment)) {
@@ -2674,6 +2722,12 @@ var createPostUploadAttachmentEnrichmentMiddleware = () => ({
2674
2722
  var withoutConcurrency = createRunner(wrapWithContinuationTracking);
2675
2723
  var withCancellation = createRunner(wrapWithCancellation);
2676
2724
  var pendingPromises = /* @__PURE__ */ new Map();
2725
+ function hasPending(tag) {
2726
+ return pendingPromises.has(tag);
2727
+ }
2728
+ async function settled(tag) {
2729
+ await pendingPromises.get(tag)?.promise;
2730
+ }
2677
2731
  function createRunner(wrapper) {
2678
2732
  return function run(tag, cb) {
2679
2733
  const { cb: wrapped, onContinued } = wrapper(tag, cb);
@@ -2716,6 +2770,16 @@ var MiddlewareExecutor = class {
2716
2770
  this.middleware = [];
2717
2771
  this.id = generateUUIDv4();
2718
2772
  }
2773
+ /**
2774
+ * The middleware currently installed, in execution order.
2775
+ *
2776
+ * Read-only: use `use` / `insert` / `replace` / `setOrder` / `remove` to change the chain. It
2777
+ * exists so components can ask what the chain is capable of - a middleware may declare
2778
+ * behaviour its host has to account for, and the host has no other way to find out.
2779
+ */
2780
+ get installedMiddleware() {
2781
+ return this.middleware;
2782
+ }
2719
2783
  use(middleware) {
2720
2784
  this.middleware = this.middleware.concat(middleware);
2721
2785
  return this;
@@ -2819,6 +2883,142 @@ var MiddlewareExecutor = class {
2819
2883
  }
2820
2884
  };
2821
2885
 
2886
+ // src/uploadManager.ts
2887
+ var import_axios = __toESM(require("axios"));
2888
+ var isUploadCancellation = (error) => import_axios.default.isCancel(error) || error instanceof Error && error.name === "AbortError";
2889
+ var initState = () => ({ uploads: {} });
2890
+ var upsertById = (uploads, record) => ({
2891
+ ...uploads,
2892
+ [record.id]: { ...uploads[record.id], ...record }
2893
+ });
2894
+ var updateById = (uploads, record) => {
2895
+ if (!(record.id in uploads)) return null;
2896
+ const current = uploads[record.id];
2897
+ return { ...uploads, [record.id]: { ...current, ...record } };
2898
+ };
2899
+ var UploadManager = class {
2900
+ constructor(client) {
2901
+ this.client = client;
2902
+ this.inFlightUploads = /* @__PURE__ */ new Map();
2903
+ this.getUpload = (id) => this.uploads[id];
2904
+ /**
2905
+ * Clears all upload records.
2906
+ * Invoked when the user disconnects so a later session does not inherit stale upload state.
2907
+ * Aborts every in-flight upload request via its `UploadRequestOptions.abortSignal`.
2908
+ */
2909
+ this.reset = () => {
2910
+ for (const { abortController } of this.inFlightUploads.values()) {
2911
+ abortController.abort();
2912
+ }
2913
+ this.inFlightUploads.clear();
2914
+ this.state.next(initState());
2915
+ };
2916
+ /**
2917
+ * Removes the upload record for `id` if present.
2918
+ * If an upload is still in progress, aborts its `UploadRequestOptions.abortSignal`.
2919
+ */
2920
+ this.deleteUploadRecord = (id) => {
2921
+ const flight = this.inFlightUploads.get(id);
2922
+ if (flight) {
2923
+ this.inFlightUploads.delete(id);
2924
+ flight.abortController.abort();
2925
+ }
2926
+ this.state.next((current) => {
2927
+ if (!(id in current.uploads)) return current;
2928
+ const uploads = { ...current.uploads };
2929
+ delete uploads[id];
2930
+ return { ...current, uploads };
2931
+ });
2932
+ };
2933
+ /**
2934
+ * Starts an upload for `id`, or returns the existing in-flight promise if one is already running.
2935
+ * Uses {@link StreamChat.channel}(`channelCid`) → `messageComposer.attachmentManager.doUploadRequest`.
2936
+ * Resolves with that result; rejects if the upload rejects (the record is removed from state either way).
2937
+ */
2938
+ this.upload = ({
2939
+ id,
2940
+ channelCid,
2941
+ file
2942
+ }) => {
2943
+ const existing = this.inFlightUploads.get(id);
2944
+ if (existing) return existing.promise;
2945
+ let resolvePromise;
2946
+ let rejectPromise;
2947
+ const promise = new Promise((resolve, reject) => {
2948
+ resolvePromise = resolve;
2949
+ rejectPromise = reject;
2950
+ });
2951
+ const abortController = new AbortController();
2952
+ this.inFlightUploads.set(id, { promise, abortController });
2953
+ void (async () => {
2954
+ const attachmentManager = this.resolveAttachmentManager(channelCid);
2955
+ const trackProgress = attachmentManager.config.trackUploadProgress;
2956
+ try {
2957
+ this.upsertUpload({
2958
+ uploadConfirmationPending: false,
2959
+ id,
2960
+ uploadProgress: trackProgress ? 0 : void 0
2961
+ });
2962
+ const onProgress = trackProgress ? (progress) => {
2963
+ this.updateUpload({
2964
+ id,
2965
+ uploadProgress: progress,
2966
+ // Only a number says anything about the flush, so a report without one leaves
2967
+ // the flag alone: `undefined` means the transport cannot measure this upload,
2968
+ // not that bytes were un-sent. Lowering it there would drop a UI that had
2969
+ // already gone indeterminate back to a determinate one with nothing to show.
2970
+ //
2971
+ // The record is removed in the `finally` below, so a record that still exists
2972
+ // while reporting 100% can only mean "flushed, awaiting the response".
2973
+ ...typeof progress === "number" ? { uploadConfirmationPending: progress >= 100 } : {}
2974
+ });
2975
+ } : void 0;
2976
+ const uploadRequestOptions = {
2977
+ abortSignal: abortController.signal,
2978
+ ...onProgress ? { onProgress } : {}
2979
+ };
2980
+ const response = await attachmentManager.doUploadRequest(
2981
+ file,
2982
+ uploadRequestOptions
2983
+ );
2984
+ resolvePromise(response);
2985
+ } catch (error) {
2986
+ rejectPromise(error);
2987
+ } finally {
2988
+ this.inFlightUploads.delete(id);
2989
+ this.deleteUploadRecord(id);
2990
+ }
2991
+ })();
2992
+ return promise;
2993
+ };
2994
+ this.upsertUpload = (record) => {
2995
+ this.state.partialNext({
2996
+ uploads: upsertById(this.uploads, record)
2997
+ });
2998
+ };
2999
+ this.updateUpload = (record) => {
3000
+ this.state.next((current) => {
3001
+ const nextUploads = updateById(current.uploads, record);
3002
+ if (!nextUploads) return current;
3003
+ return { ...current, uploads: nextUploads };
3004
+ });
3005
+ };
3006
+ this.state = new StateStore(initState());
3007
+ }
3008
+ resolveAttachmentManager(channelCid) {
3009
+ const colon = channelCid.indexOf(":");
3010
+ if (colon <= 0 || colon === channelCid.length - 1) {
3011
+ throw new Error(`Invalid channelCid: ${channelCid}`);
3012
+ }
3013
+ const channelType = channelCid.slice(0, colon);
3014
+ const channelId = channelCid.slice(colon + 1);
3015
+ return this.client.channel(channelType, channelId).messageComposer.attachmentManager;
3016
+ }
3017
+ get uploads() {
3018
+ return this.state.getLatestValue().uploads;
3019
+ }
3020
+ };
3021
+
2822
3022
  // src/messageComposer/middleware/attachmentManager/postUpload/uploadErrorHandler.ts
2823
3023
  var createUploadErrorHandlerMiddleware = (composer) => ({
2824
3024
  id: "stream-io/attachment-manager-middleware/upload-error",
@@ -2831,6 +3031,7 @@ var createUploadErrorHandlerMiddleware = (composer) => ({
2831
3031
  const { attachment, error } = state;
2832
3032
  if (!error) return forward();
2833
3033
  if (!attachment) return discard();
3034
+ if (isUploadCancellation(error)) return forward();
2834
3035
  const reason = error instanceof Error ? error.message : "unknown error";
2835
3036
  composer.client.notifications.addError({
2836
3037
  message: "Error uploading attachment",
@@ -2855,7 +3056,7 @@ var AttachmentPostUploadMiddlewareExecutor = class extends MiddlewareExecutor {
2855
3056
  super();
2856
3057
  this.use([
2857
3058
  createUploadErrorHandlerMiddleware(composer),
2858
- createPostUploadAttachmentEnrichmentMiddleware()
3059
+ createPostUploadAttachmentEnrichmentMiddleware(composer)
2859
3060
  ]);
2860
3061
  }
2861
3062
  };
@@ -3303,7 +3504,7 @@ function mergeWithDiff(target, source, customizer) {
3303
3504
  }
3304
3505
 
3305
3506
  // src/messageComposer/attachmentManager.ts
3306
- var initState = ({
3507
+ var initState2 = ({
3307
3508
  message
3308
3509
  }) => ({
3309
3510
  attachments: (message?.attachments ?? [])?.filter(({ og_scrape_url }) => !og_scrape_url).map((att) => {
@@ -3331,13 +3532,14 @@ var _AttachmentManager = class _AttachmentManager {
3331
3532
  ...attachment,
3332
3533
  localMetadata: {
3333
3534
  ...attachment.localMetadata,
3535
+ uploadConfirmationPending: false,
3334
3536
  uploadProgress: void 0,
3335
3537
  uploadState: "failed"
3336
3538
  }
3337
3539
  };
3338
3540
  };
3339
3541
  this.initState = ({ message } = {}) => {
3340
- this.state.next(initState({ message }));
3542
+ this.state.next(initState2({ message }));
3341
3543
  };
3342
3544
  this.getSnapshot = () => {
3343
3545
  const state = this.state.getLatestValue();
@@ -3414,8 +3616,22 @@ var _AttachmentManager = class _AttachmentManager {
3414
3616
  this.state.partialNext({ attachments });
3415
3617
  }
3416
3618
  };
3619
+ /**
3620
+ * Releases a local preview URL the SDK minted, if it minted one.
3621
+ *
3622
+ * Only `blob:` URLs are ours to revoke: `toLocalUploadAttachment` puts a
3623
+ * `URL.createObjectURL` result in `previewUri` for a `File`, but for a React Native
3624
+ * `FileReference` it stores the caller's own native file URI, which must be left alone.
3625
+ */
3626
+ this.revokePreviewUri = (attachment) => {
3627
+ const previewUri = attachment.localMetadata?.previewUri;
3628
+ if (previewUri?.startsWith("blob:")) URL.revokeObjectURL?.(previewUri);
3629
+ };
3417
3630
  this.removeAttachments = (localAttachmentIds) => {
3418
3631
  if (!localAttachmentIds.length) return;
3632
+ const removedAttachments = this.attachments.filter(
3633
+ (attachment) => localAttachmentIds.includes(attachment.localMetadata?.id)
3634
+ );
3419
3635
  this.state.partialNext({
3420
3636
  attachments: this.attachments.filter(
3421
3637
  (attachment) => !localAttachmentIds.includes(attachment.localMetadata?.id)
@@ -3424,6 +3640,7 @@ var _AttachmentManager = class _AttachmentManager {
3424
3640
  for (const id of localAttachmentIds) {
3425
3641
  this.client.uploadManager.deleteUploadRecord(id);
3426
3642
  }
3643
+ removedAttachments.forEach(this.revokePreviewUri);
3427
3644
  };
3428
3645
  this.getUploadConfigCheck = async (fileLike) => {
3429
3646
  const client = this.channel.getClient();
@@ -3575,21 +3792,27 @@ var _AttachmentManager = class _AttachmentManager {
3575
3792
  localMetadata: {
3576
3793
  ...attachment.localMetadata,
3577
3794
  uploadState: "failed",
3578
- uploadProgress: void 0
3795
+ uploadProgress: void 0,
3796
+ // `false`, not `undefined`: these updates go through `updateAttachment`, which merges
3797
+ // via `mergeWith` — and that skips `undefined` source values, so `undefined` would
3798
+ // leave a stale `true` in place.
3799
+ uploadConfirmationPending: false
3579
3800
  }
3580
3801
  };
3581
- this.client.notifications.addError({
3582
- message: "Error uploading attachment",
3583
- origin: {
3584
- emitter: "AttachmentManager",
3585
- context: { attachment, failedAttachment }
3586
- },
3587
- options: {
3588
- type: "api:attachment:upload:failed",
3589
- metadata: { reason },
3590
- originalError: error instanceof Error ? error : void 0
3591
- }
3592
- });
3802
+ if (!isUploadCancellation(error)) {
3803
+ this.client.notifications.addError({
3804
+ message: "Error uploading attachment",
3805
+ origin: {
3806
+ emitter: "AttachmentManager",
3807
+ context: { attachment, failedAttachment }
3808
+ },
3809
+ options: {
3810
+ type: "api:attachment:upload:failed",
3811
+ metadata: { reason },
3812
+ originalError: error instanceof Error ? error : void 0
3813
+ }
3814
+ });
3815
+ }
3593
3816
  this.updateAttachment(failedAttachment);
3594
3817
  return failedAttachment;
3595
3818
  }
@@ -3602,7 +3825,9 @@ var _AttachmentManager = class _AttachmentManager {
3602
3825
  localMetadata: {
3603
3826
  ...attachment.localMetadata,
3604
3827
  uploadState: "finished",
3605
- uploadProgress: void 0
3828
+ uploadProgress: void 0,
3829
+ // See the note in the failure branch: `undefined` cannot clear through `mergeWith`.
3830
+ uploadConfirmationPending: false
3606
3831
  }
3607
3832
  };
3608
3833
  const previewUri = uploadedAttachment.localMetadata.previewUri;
@@ -3651,7 +3876,9 @@ var _AttachmentManager = class _AttachmentManager {
3651
3876
  localMetadata: {
3652
3877
  ...attachment.localMetadata,
3653
3878
  uploadState: error ? "failed" : "finished",
3654
- uploadProgress: void 0
3879
+ uploadProgress: void 0,
3880
+ // See the note in `uploadAttachment`: `undefined` cannot clear through `mergeWith`.
3881
+ uploadConfirmationPending: false
3655
3882
  }
3656
3883
  },
3657
3884
  error,
@@ -3676,7 +3903,7 @@ var _AttachmentManager = class _AttachmentManager {
3676
3903
  );
3677
3904
  };
3678
3905
  this.composer = composer;
3679
- this.state = new StateStore(initState({ message }));
3906
+ this.state = new StateStore(initState2({ message }));
3680
3907
  this.attachmentsByIdGetterCache = { attachmentsById: {}, attachments: [] };
3681
3908
  this.preUploadMiddlewareExecutor = new AttachmentPreUploadMiddlewareExecutor({
3682
3909
  composer
@@ -3781,7 +4008,8 @@ var _AttachmentManager = class _AttachmentManager {
3781
4008
  localMetadata: {
3782
4009
  ...attachment.localMetadata,
3783
4010
  uploadState: "uploading",
3784
- uploadProgress: this.config.trackUploadProgress ? 0 : void 0
4011
+ uploadProgress: this.config.trackUploadProgress ? 0 : void 0,
4012
+ uploadConfirmationPending: false
3785
4013
  }
3786
4014
  }
3787
4015
  ]);
@@ -3794,7 +4022,8 @@ var _AttachmentManager = class _AttachmentManager {
3794
4022
  localMetadata: {
3795
4023
  ...attachment.localMetadata,
3796
4024
  uploadState: "uploading",
3797
- uploadProgress: nextUpload.uploadProgress
4025
+ uploadProgress: nextUpload.uploadProgress,
4026
+ uploadConfirmationPending: nextUpload.uploadConfirmationPending
3798
4027
  }
3799
4028
  });
3800
4029
  }
@@ -3848,6 +4077,50 @@ var localAttachmentToAttachment = (localAttachment) => {
3848
4077
  const { localMetadata, ...attachment } = localAttachment;
3849
4078
  return attachment;
3850
4079
  };
4080
+ var composeWithPendingUploads = ({
4081
+ composer,
4082
+ state
4083
+ }) => {
4084
+ const composerIsKeptAsDraft = !!composer.pollId;
4085
+ const relevantAttachments = composer.attachmentManager.attachments.filter(
4086
+ (attachment) => isFinishedUpload(attachment) || !composerIsKeptAsDraft && isPendingUpload(attachment)
4087
+ );
4088
+ const localAttachments = (state.localMessage.attachments ?? []).concat(
4089
+ relevantAttachments.map(
4090
+ (attachment) => isPendingUpload(attachment) ? attachment : localAttachmentToAttachment(attachment)
4091
+ )
4092
+ );
4093
+ const messageAttachments = (state.message.attachments ?? []).concat(
4094
+ relevantAttachments.filter(isFinishedUpload).map(localAttachmentToAttachment)
4095
+ );
4096
+ if (!localAttachments.length && !messageAttachments.length) return state;
4097
+ return {
4098
+ ...state,
4099
+ localMessage: localAttachments.length ? {
4100
+ ...state.localMessage,
4101
+ attachments: localAttachments
4102
+ } : state.localMessage,
4103
+ message: messageAttachments.length ? {
4104
+ ...state.message,
4105
+ attachments: messageAttachments
4106
+ } : state.message
4107
+ };
4108
+ };
4109
+ var createSendWithPendingUploadsAttachmentsMiddleware = (composer) => ({
4110
+ allowsPendingUploads: true,
4111
+ id: "stream-io/message-composer-middleware/attachments",
4112
+ handlers: {
4113
+ compose: ({
4114
+ state,
4115
+ next,
4116
+ forward
4117
+ }) => {
4118
+ const { attachmentManager } = composer;
4119
+ if (!attachmentManager) return forward();
4120
+ return next(composeWithPendingUploads({ composer, state }));
4121
+ }
4122
+ }
4123
+ });
3851
4124
  var createAttachmentsCompositionMiddleware = (composer) => ({
3852
4125
  id: "stream-io/message-composer-middleware/attachments",
3853
4126
  handlers: {
@@ -4064,7 +4337,7 @@ var getInitialMentions = (message) => {
4064
4337
  };
4065
4338
  var isSameMentionEntity = (first, second) => first.id === second.id && first.mentionType === second.mentionType;
4066
4339
  var getMentionsFromState = (state) => state.mentions ?? state.mentionedUsers.map(userResponseToMentionEntity);
4067
- var initState2 = ({
4340
+ var initState3 = ({
4068
4341
  composer,
4069
4342
  message
4070
4343
  }) => {
@@ -4093,7 +4366,7 @@ var initState2 = ({
4093
4366
  var TextComposer = class {
4094
4367
  constructor({ composer, message }) {
4095
4368
  this.initState = ({ message } = {}) => {
4096
- this.state.next(initState2({ composer: this.composer, message }));
4369
+ this.state.next(initState3({ composer: this.composer, message }));
4097
4370
  };
4098
4371
  this.getSnapshot = (state = this.state.getLatestValue()) => state;
4099
4372
  this.restoreSnapshot = (snapshot) => {
@@ -4286,7 +4559,7 @@ var TextComposer = class {
4286
4559
  this.commitState(output.state);
4287
4560
  };
4288
4561
  this.composer = composer;
4289
- this.state = new StateStore(initState2({ composer, message }));
4562
+ this.state = new StateStore(initState3({ composer, message }));
4290
4563
  this.middlewareExecutor = new TextComposerMiddlewareExecutor({ composer });
4291
4564
  }
4292
4565
  get channel() {
@@ -4436,13 +4709,41 @@ function isErrorResponse(res) {
4436
4709
 
4437
4710
  // src/search/BaseSearchSource.ts
4438
4711
  var DEFAULT_SEARCH_SOURCE_OPTIONS = {
4439
- debounceMs: 300,
4440
4712
  pageSize: 10,
4441
4713
  allowEmptySearchString: false,
4442
4714
  resetOnNewSearchQuery: true
4443
4715
  };
4716
+ var resolveDebounceOptions = ({
4717
+ debounceMs,
4718
+ shortQueryDebounceMs,
4719
+ longQueryDebounceMs,
4720
+ shortQueryMaxLength
4721
+ }) => ({
4722
+ shortQueryDebounceMs: shortQueryDebounceMs ?? debounceMs ?? 500,
4723
+ longQueryDebounceMs: longQueryDebounceMs ?? debounceMs ?? 300,
4724
+ shortQueryMaxLength: shortQueryMaxLength ?? 2
4725
+ });
4444
4726
  var BaseSearchSourceBase = class {
4445
4727
  constructor(options) {
4728
+ this.setDebounceOptions = (options = {}) => {
4729
+ const resolved = resolveDebounceOptions(options);
4730
+ this.shortQueryDebounceMs = resolved.shortQueryDebounceMs;
4731
+ this.longQueryDebounceMs = resolved.longQueryDebounceMs;
4732
+ this.shortQueryMaxLength = resolved.shortQueryMaxLength;
4733
+ this.searchDebounced = debounce(
4734
+ this.executeQuery.bind(this),
4735
+ (searchString) => this.getDebounceMs(searchString)
4736
+ );
4737
+ };
4738
+ /**
4739
+ * Short queries match too much and are slow server-side, so they get a longer debounce.
4740
+ * Resolved per call, which is what lets the interval switch as the user keeps typing.
4741
+ */
4742
+ this.getDebounceMs = (searchString) => {
4743
+ const { length } = searchString ?? this.searchQuery;
4744
+ return length <= this.shortQueryMaxLength ? this.shortQueryDebounceMs : this.longQueryDebounceMs;
4745
+ };
4746
+ this.search = (searchQuery) => this.searchDebounced(searchQuery);
4446
4747
  this.activate = () => {
4447
4748
  if (this.isActive) return;
4448
4749
  this.state.partialNext({ isActive: true });
@@ -4454,7 +4755,7 @@ var BaseSearchSourceBase = class {
4454
4755
  this.canExecuteQuery = (newSearchString) => {
4455
4756
  const hasNewSearchQuery = typeof newSearchString !== "undefined";
4456
4757
  const searchString = newSearchString ?? this.searchQuery;
4457
- return !!(this.isActive && !this.isLoading && (this.hasNext || hasNewSearchQuery) && (this.allowEmptySearchString || searchString));
4758
+ return !!(this.isActive && this.canDispatchQuery(hasNewSearchQuery) && (this.allowEmptySearchString || searchString));
4458
4759
  };
4459
4760
  const { pageSize, allowEmptySearchString, resetOnNewSearchQuery } = {
4460
4761
  ...DEFAULT_SEARCH_SOURCE_OPTIONS,
@@ -4464,6 +4765,14 @@ var BaseSearchSourceBase = class {
4464
4765
  this.allowEmptySearchString = allowEmptySearchString;
4465
4766
  this.resetOnNewSearchQuery = resetOnNewSearchQuery;
4466
4767
  this.state = new StateStore(this.initialState);
4768
+ this.setDebounceOptions(options ?? {});
4769
+ }
4770
+ /**
4771
+ * A new search query preempts an in-flight one; pagination waits for it to finish
4772
+ * and needs a next page to fetch.
4773
+ */
4774
+ canDispatchQuery(hasNewSearchQuery) {
4775
+ return hasNewSearchQuery || !this.isLoading && this.hasNext;
4467
4776
  }
4468
4777
  get lastQueryError() {
4469
4778
  return this.state.getLatestValue().lastQueryError;
@@ -4558,21 +4867,30 @@ var BaseSearchSourceBase = class {
4558
4867
  }
4559
4868
  };
4560
4869
  var BaseSearchSource = class extends BaseSearchSourceBase {
4561
- constructor(options) {
4562
- const { debounceMs } = { ...DEFAULT_SEARCH_SOURCE_OPTIONS, ...options };
4563
- super(options);
4564
- this.setDebounceOptions = ({ debounceMs }) => {
4565
- this.searchDebounced = debounce(this.executeQuery.bind(this), debounceMs);
4566
- };
4567
- this.search = (searchQuery) => this.searchDebounced(searchQuery);
4568
- this.setDebounceOptions({ debounceMs });
4870
+ constructor() {
4871
+ super(...arguments);
4872
+ /** Aborts the in-flight request, if any, once a newer query is dispatched. */
4873
+ this.queryAbortController = null;
4874
+ }
4875
+ /** Aborts the in-flight request, if any. Returns true when one was aborted. */
4876
+ abortInFlightQuery() {
4877
+ if (!this.queryAbortController) return false;
4878
+ this.queryAbortController.abort();
4879
+ this.queryAbortController = null;
4880
+ return true;
4881
+ }
4882
+ resetState() {
4883
+ this.abortInFlightQuery();
4884
+ super.resetState();
4569
4885
  }
4570
4886
  async executeQuery(newSearchString) {
4571
4887
  if (!this.canExecuteQuery(newSearchString)) return;
4888
+ this.abortInFlightQuery();
4889
+ const { signal } = this.queryAbortController = new AbortController();
4572
4890
  const { hasNewSearchQuery, searchString } = this.prepareStateForQuery(newSearchString);
4573
4891
  let stateUpdate = {};
4574
4892
  try {
4575
- const results = await this.query(searchString);
4893
+ const results = await this.query(searchString, { signal });
4576
4894
  if (!results) return;
4577
4895
  const { items } = results;
4578
4896
  stateUpdate = this.updatePaginationStateFromQuery(results);
@@ -4583,23 +4901,19 @@ var BaseSearchSource = class extends BaseSearchSourceBase {
4583
4901
  stateUpdate.hasNext = false;
4584
4902
  }
4585
4903
  } finally {
4586
- this.state.next(this.getStateAfterQuery(stateUpdate, hasNewSearchQuery));
4904
+ if (!signal.aborted) {
4905
+ this.state.next(this.getStateAfterQuery(stateUpdate, hasNewSearchQuery));
4906
+ }
4587
4907
  }
4588
4908
  }
4589
4909
  cancelScheduledQuery() {
4590
4910
  this.searchDebounced.cancel();
4911
+ if (this.abortInFlightQuery() && this.isLoading) {
4912
+ this.state.partialNext({ isLoading: false });
4913
+ }
4591
4914
  }
4592
4915
  };
4593
4916
  var BaseSearchSourceSync = class extends BaseSearchSourceBase {
4594
- constructor(options) {
4595
- const { debounceMs } = { ...DEFAULT_SEARCH_SOURCE_OPTIONS, ...options };
4596
- super(options);
4597
- this.setDebounceOptions = ({ debounceMs }) => {
4598
- this.searchDebounced = debounce(this.executeQuery.bind(this), debounceMs);
4599
- };
4600
- this.search = (searchQuery) => this.searchDebounced(searchQuery);
4601
- this.setDebounceOptions({ debounceMs });
4602
- }
4603
4917
  executeQuery(newSearchString) {
4604
4918
  if (!this.canExecuteQuery(newSearchString)) return;
4605
4919
  const { hasNewSearchQuery, searchString } = this.prepareStateForQuery(newSearchString);
@@ -4895,13 +5209,13 @@ var UserGroupPaginator = class extends BasePaginator {
4895
5209
  // src/search/ChannelMemberSearchSource.ts
4896
5210
  var ChannelMemberSearchSource = class extends BaseSearchSource {
4897
5211
  constructor(channel, options, filterBuilderOptions = {}) {
4898
- super(options);
5212
+ const { filters, sort, searchOptions, ...restOptions } = options || {};
5213
+ super({ ...restOptions, allowEmptySearchString: true });
4899
5214
  this.type = "members";
4900
- this.canExecuteQuery = (newSearchString) => {
4901
- const hasNewSearchQuery = typeof newSearchString !== "undefined";
4902
- return this.isActive && !this.isLoading && (this.hasNext || hasNewSearchQuery);
4903
- };
4904
5215
  this.channel = channel;
5216
+ this.filters = filters;
5217
+ this.sort = sort;
5218
+ this.searchOptions = searchOptions;
4905
5219
  this.filterBuilder = new FilterBuilder({
4906
5220
  initialFilterConfig: {
4907
5221
  default: {
@@ -4917,7 +5231,7 @@ var ChannelMemberSearchSource = class extends BaseSearchSource {
4917
5231
  ...filterBuilderOptions
4918
5232
  });
4919
5233
  }
4920
- async query(searchQuery) {
5234
+ async query(searchQuery, queryOptions = {}) {
4921
5235
  const filters = this.filterBuilder.buildFilters({
4922
5236
  baseFilters: this.filters,
4923
5237
  context: {
@@ -4926,7 +5240,12 @@ var ChannelMemberSearchSource = class extends BaseSearchSource {
4926
5240
  });
4927
5241
  const sort = this.sort ?? [];
4928
5242
  const options = { ...this.searchOptions, limit: this.pageSize, offset: this.offset };
4929
- const { members } = await this.channel.queryMembers(filters ?? {}, sort, options);
5243
+ const { members } = await this.channel.queryMembers(
5244
+ filters ?? {},
5245
+ sort,
5246
+ options,
5247
+ queryOptions
5248
+ );
4930
5249
  return { items: members };
4931
5250
  }
4932
5251
  filterQueryResults(items) {
@@ -5041,9 +5360,13 @@ var SearchController = class {
5041
5360
  // src/search/UserSearchSource.ts
5042
5361
  var UserSearchSource = class extends BaseSearchSource {
5043
5362
  constructor(client, options, filterBuilderOptions = {}) {
5044
- super(options);
5363
+ const { filters, sort, searchOptions, ...restOptions } = options || {};
5364
+ super(restOptions);
5045
5365
  this.type = "users";
5046
5366
  this.client = client;
5367
+ this.filters = filters;
5368
+ this.sort = sort;
5369
+ this.searchOptions = searchOptions;
5047
5370
  this.filterBuilder = new FilterBuilder({
5048
5371
  initialFilterConfig: {
5049
5372
  $or: {
@@ -5059,7 +5382,7 @@ var UserSearchSource = class extends BaseSearchSource {
5059
5382
  ...filterBuilderOptions
5060
5383
  });
5061
5384
  }
5062
- async query(searchQuery) {
5385
+ async query(searchQuery, queryOptions = {}) {
5063
5386
  const filters = this.filterBuilder.buildFilters({
5064
5387
  baseFilters: this.filters,
5065
5388
  context: { searchQuery }
@@ -5072,7 +5395,7 @@ var UserSearchSource = class extends BaseSearchSource {
5072
5395
  sort = { id: 1, ...this.sort };
5073
5396
  }
5074
5397
  const options = { ...this.searchOptions, limit: this.pageSize, offset: this.offset };
5075
- const { users } = await this.client.queryUsers(filters, sort, options);
5398
+ const { users } = await this.client.queryUsers(filters, sort, options, queryOptions);
5076
5399
  return { items: users };
5077
5400
  }
5078
5401
  filterQueryResults(items) {
@@ -5083,9 +5406,13 @@ var UserSearchSource = class extends BaseSearchSource {
5083
5406
  // src/search/ChannelSearchSource.ts
5084
5407
  var ChannelSearchSource = class extends BaseSearchSource {
5085
5408
  constructor(client, options, filterBuilderOptions = {}) {
5086
- super(options);
5409
+ const { filters, sort, searchOptions, ...restOptions } = options || {};
5410
+ super(restOptions);
5087
5411
  this.type = "channels";
5088
5412
  this.client = client;
5413
+ this.filters = filters;
5414
+ this.sort = sort;
5415
+ this.searchOptions = searchOptions;
5089
5416
  this.filterBuilder = new FilterBuilder({
5090
5417
  ...filterBuilderOptions,
5091
5418
  initialFilterConfig: {
@@ -5097,7 +5424,7 @@ var ChannelSearchSource = class extends BaseSearchSource {
5097
5424
  }
5098
5425
  });
5099
5426
  }
5100
- async query(searchQuery) {
5427
+ async query(searchQuery, queryOptions = {}) {
5101
5428
  const filters = this.filterBuilder.buildFilters({
5102
5429
  baseFilters: {
5103
5430
  ...this.client.userID ? { members: { $in: [this.client.userID] } } : {},
@@ -5107,7 +5434,7 @@ var ChannelSearchSource = class extends BaseSearchSource {
5107
5434
  });
5108
5435
  const sort = this.sort ?? {};
5109
5436
  const options = { ...this.searchOptions, limit: this.pageSize, offset: this.offset };
5110
- const items = await this.client.queryChannels(filters, sort, options);
5437
+ const items = await this.client.queryChannels(filters, sort, options, queryOptions);
5111
5438
  return { items };
5112
5439
  }
5113
5440
  filterQueryResults(items) {
@@ -5118,9 +5445,24 @@ var ChannelSearchSource = class extends BaseSearchSource {
5118
5445
  // src/search/MessageSearchSource.ts
5119
5446
  var MessageSearchSource = class extends BaseSearchSource {
5120
5447
  constructor(client, options, filterBuilderOptions) {
5121
- super(options);
5448
+ const {
5449
+ messageSearchChannelFilters,
5450
+ messageSearchFilters,
5451
+ messageSearchSort,
5452
+ channelQueryFilters,
5453
+ channelQuerySort,
5454
+ channelQueryOptions,
5455
+ ...restOptions
5456
+ } = options || {};
5457
+ super(restOptions);
5122
5458
  this.type = "messages";
5123
5459
  this.client = client;
5460
+ this.messageSearchChannelFilters = messageSearchChannelFilters;
5461
+ this.messageSearchFilters = messageSearchFilters;
5462
+ this.messageSearchSort = messageSearchSort;
5463
+ this.channelQueryFilters = channelQueryFilters;
5464
+ this.channelQuerySort = channelQuerySort;
5465
+ this.channelQueryOptions = channelQueryOptions;
5124
5466
  this.messageSearchChannelFilterBuilder = new FilterBuilder(filterBuilderOptions?.messageSearchChannel);
5125
5467
  this.messageSearchFilterBuilder = new FilterBuilder({
5126
5468
  ...filterBuilderOptions?.messageSearch,
@@ -5143,7 +5485,7 @@ var MessageSearchSource = class extends BaseSearchSource {
5143
5485
  }
5144
5486
  });
5145
5487
  }
5146
- async query(searchQuery) {
5488
+ async query(searchQuery, queryOptions = {}) {
5147
5489
  if (!this.client.userID || this.next === null) return { items: [] };
5148
5490
  const channelFilters = this.messageSearchChannelFilterBuilder.buildFilters({
5149
5491
  baseFilters: {
@@ -5171,9 +5513,11 @@ var MessageSearchSource = class extends BaseSearchSource {
5171
5513
  const { next, results } = await this.client.search(
5172
5514
  channelFilters,
5173
5515
  messageFilters,
5174
- options
5516
+ options,
5517
+ queryOptions
5175
5518
  );
5176
5519
  const items = results.map(({ message }) => message);
5520
+ if (queryOptions.signal?.aborted) return { items, next };
5177
5521
  const cids = Array.from(
5178
5522
  items.reduce((acc, message) => {
5179
5523
  if (message.cid && !this.client.activeChannels[message.cid]) acc.add(message.cid);
@@ -5191,7 +5535,8 @@ var MessageSearchSource = class extends BaseSearchSource {
5191
5535
  last_message_at: -1,
5192
5536
  ...this.channelQuerySort
5193
5537
  },
5194
- this.channelQueryOptions
5538
+ this.channelQueryOptions,
5539
+ queryOptions
5195
5540
  );
5196
5541
  }
5197
5542
  return { items, next };
@@ -5916,6 +6261,13 @@ var MessageComposerMiddlewareExecutor = class extends MiddlewareExecutor {
5916
6261
  createCompositionDataCleanupMiddleware(composer)
5917
6262
  ]);
5918
6263
  }
6264
+ /**
6265
+ * Narrows the base accessor to the composition middleware type, so callers can read the
6266
+ * declarations it carries (`allowsPendingUploads`) without casting.
6267
+ */
6268
+ get installedMiddleware() {
6269
+ return super.installedMiddleware;
6270
+ }
5919
6271
  };
5920
6272
  var MessageDraftComposerMiddlewareExecutor = class extends MiddlewareExecutor {
5921
6273
  constructor({ composer }) {
@@ -6485,13 +6837,18 @@ var MentionsSearchSource = class extends BaseSearchSource {
6485
6837
  constructor(channel, options) {
6486
6838
  const {
6487
6839
  mentionAllAppUsers,
6840
+ memberFilters,
6841
+ memberSort,
6842
+ searchOptions,
6488
6843
  suggestionFactoryMappers,
6489
6844
  textComposerText,
6490
6845
  transliterate,
6491
6846
  trigger,
6847
+ userFilters,
6848
+ userSort,
6492
6849
  ...restOptions
6493
6850
  } = options || {};
6494
- super(restOptions);
6851
+ super({ ...restOptions, allowEmptySearchString: true });
6495
6852
  this.type = "mentions";
6496
6853
  this.normalizeSearchValue = (value) => this.transliterate(removeDiacritics(value)).toLowerCase();
6497
6854
  this.matchesSearchQuery = (value, searchQuery) => {
@@ -6529,10 +6886,6 @@ var MentionsSearchSource = class extends BaseSearchSource {
6529
6886
  this.toHereMentionSuggestion = (searchToken = this.searchQuery) => this.mapMentionSuggestion("here", "here", searchToken);
6530
6887
  this.toRoleMentionSuggestion = (role, searchToken = this.searchQuery) => this.mapMentionSuggestion("role", role, searchToken);
6531
6888
  this.toUserGroupMentionSuggestion = (userGroup, searchToken = this.searchQuery) => this.mapMentionSuggestion("user_group", userGroup, searchToken);
6532
- this.canExecuteQuery = (newSearchString) => {
6533
- const hasNewSearchQuery = typeof newSearchString !== "undefined";
6534
- return this.isActive && !this.isLoading && (hasNewSearchQuery || this.hasNext);
6535
- };
6536
6889
  this.transliterate = (text) => text;
6537
6890
  this.getMembersAndWatchers = () => {
6538
6891
  const memberUsers = Object.values(this.channel.state.members ?? {}).map(
@@ -6552,10 +6905,10 @@ var MentionsSearchSource = class extends BaseSearchSource {
6552
6905
  ...this.isMentionTypeAllowed("channel") ? [this.toChannelMentionSuggestion(searchQuery)] : [],
6553
6906
  ...this.isMentionTypeAllowed("here") ? [this.toHereMentionSuggestion(searchQuery)] : []
6554
6907
  ].filter(({ name }) => this.matchesPrefixSearchQuery(name, searchQuery));
6555
- this.getRoleMentionSuggestions = async (query) => {
6908
+ this.getRoleMentionSuggestions = async (query, queryOptions = {}) => {
6556
6909
  if (!this.isMentionTypeAllowed("role")) return [];
6557
6910
  if (!query) return [];
6558
- const { roles } = await this.client.searchRoles({ query });
6911
+ const { roles } = await this.client.searchRoles({ query }, queryOptions);
6559
6912
  return [...roles?.map((role) => role.name) ?? []].sort((left, right) => left.localeCompare(right)).map((role) => this.toRoleMentionSuggestion(role, query));
6560
6913
  };
6561
6914
  this.searchMembersLocally = (searchQuery) => {
@@ -6617,20 +6970,25 @@ var MentionsSearchSource = class extends BaseSearchSource {
6617
6970
  options: { ...this.searchOptions, limit: this.pageSize, offset }
6618
6971
  };
6619
6972
  };
6620
- this.queryUsers = async (searchQuery, offset = 0) => {
6973
+ this.queryUsers = async (searchQuery, offset = 0, queryOptions = {}) => {
6621
6974
  const { filters, sort, options } = this.prepareQueryUsersParams(searchQuery, offset);
6622
- const { users } = await this.client.queryUsers(filters, sort, options);
6975
+ const { users } = await this.client.queryUsers(filters, sort, options, queryOptions);
6623
6976
  return users;
6624
6977
  };
6625
- this.queryMembers = async (searchQuery, offset = 0) => {
6978
+ this.queryMembers = async (searchQuery, offset = 0, queryOptions = {}) => {
6626
6979
  const { filters, sort, options } = this.prepareQueryMembersParams(
6627
6980
  searchQuery,
6628
6981
  offset
6629
6982
  );
6630
- const response = await this.channel.queryMembers(filters, sort, options);
6983
+ const response = await this.channel.queryMembers(
6984
+ filters,
6985
+ sort,
6986
+ options,
6987
+ queryOptions
6988
+ );
6631
6989
  return response.members.map((member) => member.user);
6632
6990
  };
6633
- this.getUserSuggestionsPage = async (searchQuery, userOffset = 0) => {
6991
+ this.getUserSuggestionsPage = async (searchQuery, userOffset = 0, queryOptions = {}) => {
6634
6992
  if (!this.isMentionTypeAllowed("user")) {
6635
6993
  return {
6636
6994
  items: [],
@@ -6640,7 +6998,7 @@ var MentionsSearchSource = class extends BaseSearchSource {
6640
6998
  let users;
6641
6999
  const shouldSearchLocally = this.allMembersLoadedWithInitialChannelQuery || !searchQuery;
6642
7000
  if (this.config.mentionAllAppUsers) {
6643
- users = await this.queryUsers(searchQuery, userOffset);
7001
+ users = await this.queryUsers(searchQuery, userOffset, queryOptions);
6644
7002
  } else if (shouldSearchLocally) {
6645
7003
  const localUsers = this.searchMembersLocally(searchQuery);
6646
7004
  const items2 = localUsers.slice(userOffset, userOffset + this.pageSize).map((user) => this.toUserSuggestion(user, searchQuery));
@@ -6649,7 +7007,7 @@ var MentionsSearchSource = class extends BaseSearchSource {
6649
7007
  nextOffset: localUsers.length > userOffset + this.pageSize ? userOffset + items2.length : void 0
6650
7008
  };
6651
7009
  } else {
6652
- users = await this.queryMembers(searchQuery, userOffset);
7010
+ users = await this.queryMembers(searchQuery, userOffset, queryOptions);
6653
7011
  }
6654
7012
  const items = users.map((user) => this.toUserSuggestion(user, searchQuery));
6655
7013
  return {
@@ -6666,7 +7024,7 @@ var MentionsSearchSource = class extends BaseSearchSource {
6666
7024
  name_gt: lastItem.name
6667
7025
  });
6668
7026
  };
6669
- this.getUserGroupSuggestionsPage = async (searchQuery, cursor) => {
7027
+ this.getUserGroupSuggestionsPage = async (searchQuery, cursor, queryOptions = {}) => {
6670
7028
  if (!this.isMentionTypeAllowed("user_group")) {
6671
7029
  return {
6672
7030
  items: [],
@@ -6688,7 +7046,7 @@ var MentionsSearchSource = class extends BaseSearchSource {
6688
7046
  ...userGroupCursor?.id_gt ? { id_gt: userGroupCursor.id_gt } : {},
6689
7047
  ...userGroupCursor?.name_gt ? { name_gt: userGroupCursor.name_gt } : {}
6690
7048
  };
6691
- const { user_groups } = await this.client.searchUserGroups(options);
7049
+ const { user_groups } = await this.client.searchUserGroups(options, queryOptions);
6692
7050
  return {
6693
7051
  items: user_groups.map(
6694
7052
  (userGroup) => this.toUserGroupMentionSuggestion(userGroup, searchQuery)
@@ -6698,6 +7056,11 @@ var MentionsSearchSource = class extends BaseSearchSource {
6698
7056
  };
6699
7057
  this.client = channel.getClient();
6700
7058
  this.channel = channel;
7059
+ this.userFilters = userFilters;
7060
+ this.userSort = userSort;
7061
+ this.memberFilters = memberFilters;
7062
+ this.memberSort = memberSort;
7063
+ this.searchOptions = searchOptions;
6701
7064
  this.config = {
6702
7065
  mentionAllAppUsers,
6703
7066
  suggestionFactoryMappers,
@@ -6730,16 +7093,21 @@ var MentionsSearchSource = class extends BaseSearchSource {
6730
7093
  offset: (this.offset ?? 0) + userPaginationState.itemCount
6731
7094
  };
6732
7095
  }
6733
- async query(searchQuery) {
7096
+ async query(searchQuery, queryOptions = {}) {
6734
7097
  const userOffset = this.offset ?? 0;
6735
7098
  const isFirstPage = userOffset === 0 && typeof this.userGroupCursor === "undefined";
6736
7099
  const previousUserPaginationState = this.latestUserPaginationState;
6737
7100
  const previousUserGroupCursor = this.userGroupCursor;
6738
7101
  const [userResultsState, userGroupResultsState, roleSuggestionsState] = await Promise.allSettled([
6739
- this.getUserSuggestionsPage(searchQuery, userOffset),
6740
- this.getUserGroupSuggestionsPage(searchQuery, previousUserGroupCursor),
6741
- isFirstPage ? this.getRoleMentionSuggestions(searchQuery) : Promise.resolve([])
7102
+ this.getUserSuggestionsPage(searchQuery, userOffset, queryOptions),
7103
+ this.getUserGroupSuggestionsPage(
7104
+ searchQuery,
7105
+ previousUserGroupCursor,
7106
+ queryOptions
7107
+ ),
7108
+ isFirstPage ? this.getRoleMentionSuggestions(searchQuery, queryOptions) : Promise.resolve([])
6742
7109
  ]);
7110
+ if (queryOptions.signal?.aborted) return { items: [] };
6743
7111
  const userResults = userResultsState.status === "fulfilled" ? userResultsState.value : {
6744
7112
  items: [],
6745
7113
  nextOffset: isFirstPage ? void 0 : previousUserPaginationState?.nextOffset
@@ -7014,7 +7382,7 @@ var DEFAULT_COMPOSER_CONFIG = {
7014
7382
  };
7015
7383
 
7016
7384
  // src/messageComposer/CustomDataManager.ts
7017
- var initState3 = (options) => {
7385
+ var initState4 = (options) => {
7018
7386
  if (!options)
7019
7387
  return { message: {}, custom: {} };
7020
7388
  return { message: {}, custom: {} };
@@ -7023,14 +7391,14 @@ var CustomDataManager = class {
7023
7391
  constructor({ composer, message }) {
7024
7392
  this.isMessageDataEqual = (nextState, previousState) => JSON.stringify(nextState.message) === JSON.stringify(previousState?.message);
7025
7393
  this.initState = ({ message } = {}) => {
7026
- this.state.next(initState3({ composer: this.composer, message }));
7394
+ this.state.next(initState4({ composer: this.composer, message }));
7027
7395
  };
7028
7396
  this.getSnapshot = () => this.state.getLatestValue();
7029
7397
  this.restoreSnapshot = (snapshot) => {
7030
7398
  this.state.next(snapshot);
7031
7399
  };
7032
7400
  this.composer = composer;
7033
- this.state = new StateStore(initState3({ composer, message }));
7401
+ this.state = new StateStore(initState4({ composer, message }));
7034
7402
  }
7035
7403
  get customMessageData() {
7036
7404
  return this.state.getLatestValue().message;
@@ -7066,7 +7434,7 @@ var LinkPreviewStatus = /* @__PURE__ */ ((LinkPreviewStatus2) => {
7066
7434
  return LinkPreviewStatus2;
7067
7435
  })(LinkPreviewStatus || {});
7068
7436
  var linkPreviewArrayToMap = (linkPreviews) => new Map(linkPreviews.map((linkPreview) => [linkPreview.og_scrape_url, linkPreview]));
7069
- var initState4 = ({
7437
+ var initState5 = ({
7070
7438
  message
7071
7439
  }) => message ? {
7072
7440
  previews: message.attachments?.reduce((acc, attachment) => {
@@ -7085,7 +7453,7 @@ var _LinkPreviewsManager = class _LinkPreviewsManager {
7085
7453
  this.shouldDiscardEnrichQueries = false;
7086
7454
  this.initState = ({ message } = {}) => {
7087
7455
  this.cancelURLEnrichment();
7088
- this.state.next(initState4({ message: this.enabled ? message : void 0 }));
7456
+ this.state.next(initState5({ message: this.enabled ? message : void 0 }));
7089
7457
  };
7090
7458
  this.getSnapshot = () => this.state.getLatestValue();
7091
7459
  this.restoreSnapshot = (snapshot) => {
@@ -7188,7 +7556,7 @@ var _LinkPreviewsManager = class _LinkPreviewsManager {
7188
7556
  };
7189
7557
  this.composer = composer;
7190
7558
  this.state = new StateStore(
7191
- initState4({ message: this.enabled ? message : void 0 })
7559
+ initState5({ message: this.enabled ? message : void 0 })
7192
7560
  );
7193
7561
  this.findAndEnrichUrls = debounce(
7194
7562
  this._findAndEnrichUrls.bind(this),
@@ -7276,7 +7644,7 @@ var LinkPreviewsManager = _LinkPreviewsManager;
7276
7644
 
7277
7645
  // src/messageComposer/LocationComposer.ts
7278
7646
  var MIN_LIVE_LOCATION_SHARE_DURATION = 60 * 1e3;
7279
- var initState5 = ({
7647
+ var initState6 = ({
7280
7648
  message
7281
7649
  }) => ({
7282
7650
  location: message?.shared_location ?? null
@@ -7284,7 +7652,7 @@ var initState5 = ({
7284
7652
  var LocationComposer = class {
7285
7653
  constructor({ composer, message }) {
7286
7654
  this.initState = ({ message } = {}) => {
7287
- this.state.next(initState5({ message }));
7655
+ this.state.next(initState6({ message }));
7288
7656
  };
7289
7657
  this.getSnapshot = () => this.state.getLatestValue();
7290
7658
  this.restoreSnapshot = (snapshot) => {
@@ -7302,7 +7670,7 @@ var LocationComposer = class {
7302
7670
  });
7303
7671
  };
7304
7672
  this.composer = composer;
7305
- this.state = new StateStore(initState5({ message }));
7673
+ this.state = new StateStore(initState6({ message }));
7306
7674
  this._deviceId = this.config.getDeviceId();
7307
7675
  }
7308
7676
  get config() {
@@ -8023,7 +8391,7 @@ var initEditingAuditState = (composition) => {
8023
8391
  }
8024
8392
  };
8025
8393
  };
8026
- var initState6 = (composition) => {
8394
+ var initState7 = (composition) => {
8027
8395
  if (!composition) {
8028
8396
  return {
8029
8397
  draftId: null,
@@ -8110,7 +8478,7 @@ var _MessageComposer = class _MessageComposer extends WithSubscriptions {
8110
8478
  this.textComposer.initState({ message });
8111
8479
  this.pollComposer.initState();
8112
8480
  this.customDataManager.initState({ message });
8113
- this.state.next(initState6(composition));
8481
+ this.state.next(initState7(composition));
8114
8482
  };
8115
8483
  this.initStateFromChannelResponse = (channelApiResponse) => {
8116
8484
  if (this.channel.cid !== channelApiResponse.channel.cid) {
@@ -8576,7 +8944,7 @@ var _MessageComposer = class _MessageComposer extends WithSubscriptions {
8576
8944
  this.editingAuditState = new StateStore(
8577
8945
  this.initEditingAuditState(composition)
8578
8946
  );
8579
- this.state = new StateStore(initState6(composition));
8947
+ this.state = new StateStore(initState7(composition));
8580
8948
  this.compositionMiddlewareExecutor = new MessageComposerMiddlewareExecutor({
8581
8949
  composer: this
8582
8950
  });
@@ -8655,8 +9023,32 @@ var _MessageComposer = class _MessageComposer extends WithSubscriptions {
8655
9023
  const currentCommand = this.textComposer.command;
8656
9024
  return !currentCommand || this.validateCommandSendability(currentCommand).ready;
8657
9025
  }
9026
+ /**
9027
+ * Whether the installed composition middleware composes messages whose uploads are still in
9028
+ * flight — see `MessageCompositionMiddleware.allowsPendingUploads`.
9029
+ *
9030
+ * Derived from the chain rather than from config, so there is exactly one switch: the UI SDK
9031
+ * that implements the rest of the flow installs the middleware, and everything that has to
9032
+ * follow from that (starting with {@link hasSendableData}) follows on its own.
9033
+ *
9034
+ * **Temporary API.** In v10 sending with pending uploads becomes part of the composer
9035
+ * configuration (`MessageComposerConfig`, set through `updateConfig`), and this getter,
9036
+ * together with the middleware declaration it reads, is replaced by that config field.
9037
+ */
9038
+ get allowsPendingUploads() {
9039
+ return this.compositionMiddlewareExecutor.installedMiddleware.some(
9040
+ (middleware) => middleware.allowsPendingUploads
9041
+ );
9042
+ }
8658
9043
  get hasSendableData() {
8659
- return this.isCommandSendable && !!(!this.attachmentManager.uploadsInProgressCount && (!this.textComposer.textIsEmpty || this.attachmentManager.successfulUploadsCount > 0) || this.pollId || !!this.locationComposer.validLocation);
9044
+ if (!this.isCommandSendable) return false;
9045
+ if (this.allowsPendingUploads) {
9046
+ const hasSendableAttachment = this.attachmentManager.attachments.some(
9047
+ (attachment) => isFinishedUpload(attachment) || isPendingUpload(attachment)
9048
+ );
9049
+ return !!(!this.textComposer.textIsEmpty || hasSendableAttachment || this.pollId || this.locationComposer.validLocation);
9050
+ }
9051
+ return !!(!this.attachmentManager.uploadsInProgressCount && (!this.textComposer.textIsEmpty || this.attachmentManager.successfulUploadsCount > 0) || this.pollId || !!this.locationComposer.validLocation);
8660
9052
  }
8661
9053
  get compositionIsEmpty() {
8662
9054
  return !this.quotedMessage && this.contentIsEmpty;
@@ -9283,14 +9675,16 @@ var Channel = class {
9283
9675
  *
9284
9676
  * @return {Promise<SendMessageAPIResponse>} The Server Response
9285
9677
  */
9286
- async _sendMessage(message, options) {
9287
- return await this.getClient().post(
9288
- this._channelURL() + "/message",
9289
- {
9290
- message,
9291
- ...options
9292
- }
9293
- );
9678
+ async _sendMessage(rawMessage, options) {
9679
+ const client = this.getClient();
9680
+ const message = sanitizeOutgoingAttachments({
9681
+ client,
9682
+ message: rawMessage
9683
+ });
9684
+ return await client.post(this._channelURL() + "/message", {
9685
+ message,
9686
+ ...options
9687
+ });
9294
9688
  }
9295
9689
  async sendMessage(message, options) {
9296
9690
  try {
@@ -9415,10 +9809,11 @@ var Channel = class {
9415
9809
  * @param {MemberSort} [sort] Sort options, for instance [{created_at: -1}].
9416
9810
  * When using multiple fields, make sure you use array of objects to guarantee field order, for instance [{name: -1}, {created_at: 1}]
9417
9811
  * @param {{ limit?: number; offset?: number }} [options] Option object, {limit: 10, offset:10}
9812
+ * @param {RequestOptions} [requestOptions] Carries an abort signal. Not sent in the request.
9418
9813
  *
9419
9814
  * @return {Promise<ChannelMemberAPIResponse>} Query Members response
9420
9815
  */
9421
- async queryMembers(filterConditions, sort = [], options = {}) {
9816
+ async queryMembers(filterConditions, sort = [], options = {}, requestOptions = {}) {
9422
9817
  let id;
9423
9818
  const type = this.type;
9424
9819
  let members;
@@ -9438,7 +9833,8 @@ var Channel = class {
9438
9833
  filter_conditions: filterConditions,
9439
9834
  ...options
9440
9835
  }
9441
- }
9836
+ },
9837
+ requestOptions
9442
9838
  );
9443
9839
  }
9444
9840
  /**
@@ -10739,6 +11135,7 @@ var Channel = class {
10739
11135
  // delivery-report network sync below is skipped for it.
10740
11136
  case "message.read_locally":
10741
11137
  case "message.read":
11138
+ if (event.thread) break;
10742
11139
  if (event.user?.id && event.created_at) {
10743
11140
  const previousReadState = channelState.read[event.user.id];
10744
11141
  channelState.read[event.user.id] = {
@@ -11188,7 +11585,7 @@ var ClientState = class {
11188
11585
  var import_isomorphic_ws = __toESM(require("isomorphic-ws"));
11189
11586
 
11190
11587
  // src/insights.ts
11191
- var import_axios = __toESM(require("axios"));
11588
+ var import_axios2 = __toESM(require("axios"));
11192
11589
  var InsightMetrics = class {
11193
11590
  constructor() {
11194
11591
  this.connectionStartTimestamp = null;
@@ -11201,7 +11598,7 @@ var postInsights = async (insightType, insights) => {
11201
11598
  const maxAttempts = 3;
11202
11599
  for (let i = 0; i < maxAttempts; i++) {
11203
11600
  try {
11204
- await import_axios.default.post(
11601
+ await import_axios2.default.post(
11205
11602
  `https://chat-insights.getstream.io/insights/${insightType}`,
11206
11603
  insights
11207
11604
  );
@@ -11709,131 +12106,6 @@ var StableWSConnection = class {
11709
12106
  }
11710
12107
  };
11711
12108
 
11712
- // src/uploadManager.ts
11713
- var initState7 = () => ({ uploads: {} });
11714
- var upsertById = (uploads, record) => ({
11715
- ...uploads,
11716
- [record.id]: { ...uploads[record.id], ...record }
11717
- });
11718
- var updateById = (uploads, record) => {
11719
- if (!(record.id in uploads)) return null;
11720
- const current = uploads[record.id];
11721
- return { ...uploads, [record.id]: { ...current, ...record } };
11722
- };
11723
- var UploadManager = class {
11724
- constructor(client) {
11725
- this.client = client;
11726
- this.inFlightUploads = /* @__PURE__ */ new Map();
11727
- this.getUpload = (id) => this.uploads[id];
11728
- /**
11729
- * Clears all upload records.
11730
- * Invoked when the user disconnects so a later session does not inherit stale upload state.
11731
- * Aborts every in-flight upload request via its `UploadRequestOptions.abortSignal`.
11732
- */
11733
- this.reset = () => {
11734
- for (const { abortController } of this.inFlightUploads.values()) {
11735
- abortController.abort();
11736
- }
11737
- this.inFlightUploads.clear();
11738
- this.state.next(initState7());
11739
- };
11740
- /**
11741
- * Removes the upload record for `id` if present.
11742
- * If an upload is still in progress, aborts its `UploadRequestOptions.abortSignal`.
11743
- */
11744
- this.deleteUploadRecord = (id) => {
11745
- const flight = this.inFlightUploads.get(id);
11746
- if (flight) {
11747
- this.inFlightUploads.delete(id);
11748
- flight.abortController.abort();
11749
- }
11750
- this.state.next((current) => {
11751
- if (!(id in current.uploads)) return current;
11752
- const uploads = { ...current.uploads };
11753
- delete uploads[id];
11754
- return { ...current, uploads };
11755
- });
11756
- };
11757
- /**
11758
- * Starts an upload for `id`, or returns the existing in-flight promise if one is already running.
11759
- * Uses {@link StreamChat.channel}(`channelCid`) → `messageComposer.attachmentManager.doUploadRequest`.
11760
- * Resolves with that result; rejects if the upload rejects (the record is removed from state either way).
11761
- */
11762
- this.upload = ({
11763
- id,
11764
- channelCid,
11765
- file
11766
- }) => {
11767
- const existing = this.inFlightUploads.get(id);
11768
- if (existing) return existing.promise;
11769
- let resolvePromise;
11770
- let rejectPromise;
11771
- const promise = new Promise((resolve, reject) => {
11772
- resolvePromise = resolve;
11773
- rejectPromise = reject;
11774
- });
11775
- const abortController = new AbortController();
11776
- this.inFlightUploads.set(id, { promise, abortController });
11777
- void (async () => {
11778
- const attachmentManager = this.resolveAttachmentManager(channelCid);
11779
- const trackProgress = attachmentManager.config.trackUploadProgress;
11780
- try {
11781
- this.upsertUpload({
11782
- id,
11783
- uploadProgress: trackProgress ? 0 : void 0
11784
- });
11785
- const onProgress = trackProgress ? (progress) => {
11786
- this.updateUpload({
11787
- id,
11788
- uploadProgress: progress
11789
- });
11790
- } : void 0;
11791
- const uploadRequestOptions = {
11792
- abortSignal: abortController.signal,
11793
- ...onProgress ? { onProgress } : {}
11794
- };
11795
- const response = await attachmentManager.doUploadRequest(
11796
- file,
11797
- uploadRequestOptions
11798
- );
11799
- resolvePromise(response);
11800
- } catch (error) {
11801
- rejectPromise(error);
11802
- } finally {
11803
- this.inFlightUploads.delete(id);
11804
- this.deleteUploadRecord(id);
11805
- }
11806
- })();
11807
- return promise;
11808
- };
11809
- this.upsertUpload = (record) => {
11810
- this.state.partialNext({
11811
- uploads: upsertById(this.uploads, record)
11812
- });
11813
- };
11814
- this.updateUpload = (record) => {
11815
- this.state.next((current) => {
11816
- const nextUploads = updateById(current.uploads, record);
11817
- if (!nextUploads) return current;
11818
- return { ...current, uploads: nextUploads };
11819
- });
11820
- };
11821
- this.state = new StateStore(initState7());
11822
- }
11823
- resolveAttachmentManager(channelCid) {
11824
- const colon = channelCid.indexOf(":");
11825
- if (colon <= 0 || colon === channelCid.length - 1) {
11826
- throw new Error(`Invalid channelCid: ${channelCid}`);
11827
- }
11828
- const channelType = channelCid.slice(0, colon);
11829
- const channelId = channelCid.slice(colon + 1);
11830
- return this.client.channel(channelType, channelId).messageComposer.attachmentManager;
11831
- }
11832
- get uploads() {
11833
- return this.state.getLatestValue().uploads;
11834
- }
11835
- };
11836
-
11837
12109
  // src/signing.ts
11838
12110
  var import_jsonwebtoken = __toESM(require_jsonwebtoken());
11839
12111
  var import_crypto = __toESM(require_crypto());
@@ -12092,7 +12364,7 @@ var TokenManager = class {
12092
12364
  };
12093
12365
 
12094
12366
  // src/connection_fallback.ts
12095
- var import_axios2 = __toESM(require("axios"));
12367
+ var import_axios3 = __toESM(require("axios"));
12096
12368
  var WSConnectionFallback = class {
12097
12369
  constructor({ client }) {
12098
12370
  /** @private */
@@ -12111,7 +12383,7 @@ var WSConnectionFallback = class {
12111
12383
  /** @private */
12112
12384
  this._req = async (params, config, retry) => {
12113
12385
  if (!this.cancelToken && !params.close) {
12114
- this.cancelToken = import_axios2.default.CancelToken.source();
12386
+ this.cancelToken = import_axios3.default.CancelToken.source();
12115
12387
  }
12116
12388
  try {
12117
12389
  const res = await this.client.doAxiosRequest(
@@ -12147,7 +12419,7 @@ var WSConnectionFallback = class {
12147
12419
  }
12148
12420
  }
12149
12421
  } catch (error) {
12150
- if (import_axios2.default.isCancel(error)) {
12422
+ if (import_axios3.default.isCancel(error)) {
12151
12423
  this._log(`_poll() - axios canceled request`);
12152
12424
  return;
12153
12425
  }
@@ -14570,6 +14842,7 @@ var StreamChat = class _StreamChat {
14570
14842
  return this.handleResponse(response);
14571
14843
  } catch (e) {
14572
14844
  e.client_request_id = requestConfig.headers?.["x-client-request-id"];
14845
+ if (import_axios4.default.isCancel(e)) throw e;
14573
14846
  this._logApiError(type, url, e);
14574
14847
  this.consecutiveFailures += 1;
14575
14848
  if (e.response) {
@@ -14881,7 +15154,7 @@ var StreamChat = class _StreamChat {
14881
15154
  return this.userAgent;
14882
15155
  }
14883
15156
  if (!this.cachedUserAgent) {
14884
- const version = "9.50.3";
15157
+ const version = "9.52.0";
14885
15158
  const { name: sdkName, version: sdkVersion } = this.sdkIdentifier ?? {};
14886
15159
  const { name: appName, version: appVersion } = this.appIdentifier ?? {};
14887
15160
  const { os, model: deviceModel } = this.deviceIdentifier ?? {};
@@ -14949,7 +15222,7 @@ var StreamChat = class _StreamChat {
14949
15222
  keepAliveMsecs: 3e3
14950
15223
  });
14951
15224
  }
14952
- this.axiosInstance = import_axios3.default.create(this.options);
15225
+ this.axiosInstance = import_axios4.default.create(this.options);
14953
15226
  this.setBaseURL(this.options.baseURL || "https://chat.stream-io-api.com");
14954
15227
  if (typeof process !== "undefined" && "env" in process && process.env.STREAM_LOCAL_TEST_RUN) {
14955
15228
  this.setBaseURL("http://localhost:3030");
@@ -15257,14 +15530,14 @@ var StreamChat = class _StreamChat {
15257
15530
  error
15258
15531
  });
15259
15532
  }
15260
- get(url, params) {
15261
- return this.doAxiosRequest("get", url, null, { params });
15533
+ get(url, params, config) {
15534
+ return this.doAxiosRequest("get", url, null, { params, config });
15262
15535
  }
15263
15536
  put(url, data) {
15264
15537
  return this.doAxiosRequest("put", url, data);
15265
15538
  }
15266
- post(url, data) {
15267
- return this.doAxiosRequest("post", url, data);
15539
+ post(url, data, config) {
15540
+ return this.doAxiosRequest("post", url, data, { config });
15268
15541
  }
15269
15542
  patch(url, data) {
15270
15543
  return this.doAxiosRequest("patch", url, data);
@@ -15459,10 +15732,11 @@ var StreamChat = class _StreamChat {
15459
15732
  * @param {UserSort} sort Sort options, for instance [{last_active: -1}].
15460
15733
  * When using multiple fields, make sure you use array of objects to guarantee field order, for instance [{last_active: -1}, {created_at: 1}]
15461
15734
  * @param {UserOptions} options Option object, {presence: true}
15735
+ * @param {RequestOptions} [requestOptions] Carries an abort signal. Not sent in the request.
15462
15736
  *
15463
15737
  * @return {Promise<{ users: Array<UserResponse> }>} User Query Response
15464
15738
  */
15465
- async queryUsers(filterConditions, sort = [], options = {}) {
15739
+ async queryUsers(filterConditions, sort = [], options = {}, requestOptions = {}) {
15466
15740
  const defaultOptions = {
15467
15741
  presence: false
15468
15742
  };
@@ -15479,7 +15753,8 @@ var StreamChat = class _StreamChat {
15479
15753
  ...defaultOptions,
15480
15754
  ...options
15481
15755
  }
15482
- }
15756
+ },
15757
+ requestOptions
15483
15758
  );
15484
15759
  this.state.updateUsers(data.users);
15485
15760
  return data;
@@ -15525,13 +15800,14 @@ var StreamChat = class _StreamChat {
15525
15800
  * searchUserGroups - Search user groups by prefix for autocomplete
15526
15801
  *
15527
15802
  * @param {SearchUserGroupsOptions} options The search options
15528
- *
15803
+ * @param {RequestOptions} [requestOptions] Carries an abort signal. Not sent in the request.
15529
15804
  * @return {Promise<SearchUserGroupsResponse>} User Group Search Response
15530
15805
  */
15531
- async searchUserGroups(options) {
15806
+ async searchUserGroups(options, requestOptions = {}) {
15532
15807
  return await this.get(
15533
15808
  this.baseURL + "/usergroups/search",
15534
- options
15809
+ options,
15810
+ requestOptions
15535
15811
  );
15536
15812
  }
15537
15813
  /**
@@ -15651,10 +15927,11 @@ var StreamChat = class _StreamChat {
15651
15927
  * @param {ChannelSort} [sort] Sort options, for instance {created_at: -1}.
15652
15928
  * When using multiple fields, make sure you use array of objects to guarantee field order, for instance [{last_updated: -1}, {created_at: 1}]
15653
15929
  * @param {ChannelOptions} [options] Options object. Can include predefined_filter, filter_values, and sort_values for using predefined filters.
15930
+ * @param {RequestOptions} [requestOptions] Carries an abort signal. Not sent in the request.
15654
15931
  *
15655
15932
  * @return {Promise<QueryChannelsAPIResponse>} full search channels response
15656
15933
  */
15657
- async queryChannelsRequestWithResponse(filterConditions, sort = [], options = {}) {
15934
+ async queryChannelsRequestWithResponse(filterConditions, sort = [], options = {}, requestOptions = {}) {
15658
15935
  const defaultOptions = {
15659
15936
  state: true,
15660
15937
  watch: true,
@@ -15679,7 +15956,11 @@ var StreamChat = class _StreamChat {
15679
15956
  ...defaultOptions,
15680
15957
  ...restOptions
15681
15958
  };
15682
- return await this.post(this.baseURL + "/channels", payload);
15959
+ return await this.post(
15960
+ this.baseURL + "/channels",
15961
+ payload,
15962
+ requestOptions
15963
+ );
15683
15964
  }
15684
15965
  /**
15685
15966
  * queryChannelsRequest - Queries channels and returns the raw channel response list.
@@ -15708,7 +15989,9 @@ var StreamChat = class _StreamChat {
15708
15989
  const queryChannelsResponse = await this.queryChannelsRequestWithResponse(
15709
15990
  filterConditions,
15710
15991
  sort,
15711
- options
15992
+ options,
15993
+ // stateOptions is never serialized, so it carries the abort signal
15994
+ { signal: stateOptions.signal }
15712
15995
  );
15713
15996
  const channels = queryChannelsResponse.channels;
15714
15997
  this.dispatchEvent({
@@ -15825,10 +16108,11 @@ var StreamChat = class _StreamChat {
15825
16108
  * @param {ChannelFilters} filterConditions MongoDB style filter conditions
15826
16109
  * @param {MessageFilters | string} query search query or object MongoDB style filters
15827
16110
  * @param {SearchOptions} [options] Option object, {user_id: 'tommaso'}
16111
+ * @param {RequestOptions} [requestOptions] Carries an abort signal. Not sent in the request.
15828
16112
  *
15829
16113
  * @return {Promise<SearchAPIResponse>} search messages response
15830
16114
  */
15831
- async search(filterConditions, query, options = {}) {
16115
+ async search(filterConditions, query, options = {}, requestOptions = {}) {
15832
16116
  if (options.offset && options.next) {
15833
16117
  throw Error(`Cannot specify offset with next`);
15834
16118
  }
@@ -15845,7 +16129,11 @@ var StreamChat = class _StreamChat {
15845
16129
  throw Error(`Invalid type ${typeof query} for query parameter`);
15846
16130
  }
15847
16131
  await this.wsPromise;
15848
- return await this.get(this.baseURL + "/search", { payload });
16132
+ return await this.get(
16133
+ this.baseURL + "/search",
16134
+ { payload },
16135
+ requestOptions
16136
+ );
15849
16137
  }
15850
16138
  /**
15851
16139
  * setLocalDevice - Set the device info for the current client(device) that will be sent via WS connection automatically
@@ -16286,6 +16574,11 @@ var StreamChat = class _StreamChat {
16286
16574
  }
16287
16575
  /**
16288
16576
  * unflagMessage - unflag a message
16577
+ *
16578
+ * @deprecated The `/moderation/unflag` endpoint is deprecated and is a no-op on
16579
+ * the backend - calling this resolves successfully but does not remove the flag.
16580
+ * Use `client.moderation.submitAction()` against the review queue item instead.
16581
+ *
16289
16582
  * @param {string} targetMessageID
16290
16583
  * @param {string} [options.user_id] currentUserID, only used with serverside auth
16291
16584
  * @returns {Promise<APIResponse>}
@@ -16298,6 +16591,11 @@ var StreamChat = class _StreamChat {
16298
16591
  }
16299
16592
  /**
16300
16593
  * unflagUser - unflag a user
16594
+ *
16595
+ * @deprecated The `/moderation/unflag` endpoint is deprecated and is a no-op on
16596
+ * the backend - calling this resolves successfully but does not remove the flag.
16597
+ * Use `client.moderation.submitAction()` against the review queue item instead.
16598
+ *
16301
16599
  * @param {string} targetID
16302
16600
  * @param {string} [options.user_id] currentUserID, only used with serverside auth
16303
16601
  * @returns {Promise<APIResponse>}
@@ -16582,7 +16880,10 @@ var StreamChat = class _StreamChat {
16582
16880
  if (!message.id) {
16583
16881
  throw Error("Please specify the message.id when calling updateMessage");
16584
16882
  }
16585
- const payload = toUpdatedMessagePayload(message);
16883
+ const payload = sanitizeOutgoingAttachments({
16884
+ client: this,
16885
+ message: toUpdatedMessagePayload(message)
16886
+ });
16586
16887
  if (typeof partialUserOrUserId === "string") {
16587
16888
  payload.user_id = partialUserOrUserId;
16588
16889
  } else if (typeof partialUserOrUserId?.id === "string") {
@@ -16882,6 +17183,7 @@ var StreamChat = class _StreamChat {
16882
17183
  headers: axiosRequestConfigHeaders,
16883
17184
  ...axiosRequestConfigRest
16884
17185
  } = this.options.axiosRequestConfig || {};
17186
+ const resolvedSignal = options.config?.signal ?? axiosRequestConfigRest.signal ?? signal;
16885
17187
  return {
16886
17188
  params: {
16887
17189
  user_id: this.userID,
@@ -16897,9 +17199,9 @@ var StreamChat = class _StreamChat {
16897
17199
  ...options.headers,
16898
17200
  ...axiosRequestConfigHeaders || {}
16899
17201
  },
16900
- ...signal ? { signal } : {},
16901
17202
  ...options.config,
16902
- ...axiosRequestConfigRest || {}
17203
+ ...axiosRequestConfigRest,
17204
+ ...resolvedSignal ? { signal: resolvedSignal } : {}
16903
17205
  };
16904
17206
  }
16905
17207
  _getToken() {
@@ -17039,8 +17341,12 @@ var StreamChat = class _StreamChat {
17039
17341
  *
17040
17342
  * @returns {Promise<APIResponse>}
17041
17343
  */
17042
- searchRoles(options) {
17043
- return this.get(`${this.baseURL}/roles/search`, options);
17344
+ searchRoles(options, requestOptions = {}) {
17345
+ return this.get(
17346
+ `${this.baseURL}/roles/search`,
17347
+ options,
17348
+ requestOptions
17349
+ );
17044
17350
  }
17045
17351
  /** deleteRole - deletes a custom role
17046
17352
  *
@@ -18393,11 +18699,12 @@ var OfflineError = class extends Error {
18393
18699
  };
18394
18700
 
18395
18701
  // src/offline-support/offline_sync_manager.ts
18396
- var import_axios4 = require("axios");
18702
+ var import_axios5 = require("axios");
18397
18703
  var OfflineDBSyncManager = class {
18398
18704
  constructor({
18399
18705
  client,
18400
- offlineDb
18706
+ offlineDb,
18707
+ syncMaxEventCount
18401
18708
  }) {
18402
18709
  this.syncStatus = false;
18403
18710
  this.connectionChangedListener = null;
@@ -18514,13 +18821,23 @@ var OfflineDBSyncManager = class {
18514
18821
  await this.offlineDb.resetDB();
18515
18822
  } else {
18516
18823
  const result = await this.client.sync(cids, lastSyncedAtDate.toISOString());
18517
- const queryPromises = result.events.map(
18518
- (event) => this.offlineDb.handleEvent({ event, execute: false })
18519
- );
18520
- const queriesArray = await Promise.all(queryPromises);
18521
- const queries = queriesArray.flat();
18522
- if (queries.length) {
18523
- await this.offlineDb.executeSqlBatch(queries);
18824
+ const { syncMaxEventCount } = this;
18825
+ const exceedsLimit = typeof syncMaxEventCount === "number" && syncMaxEventCount > 0 && result.events.length > syncMaxEventCount;
18826
+ if (exceedsLimit) {
18827
+ this.client.logger(
18828
+ "warn",
18829
+ `Skipping sync event replay: received ${result.events.length} events, which exceeds the configured limit of ${syncMaxEventCount}. Visible channels are refreshed on reconnect instead.`,
18830
+ { tags: ["sync", "offline"] }
18831
+ );
18832
+ } else {
18833
+ const queryPromises = result.events.map(
18834
+ (event) => this.offlineDb.handleEvent({ event, execute: false })
18835
+ );
18836
+ const queriesArray = await Promise.all(queryPromises);
18837
+ const queries = queriesArray.flat();
18838
+ if (queries.length) {
18839
+ await this.offlineDb.executeSqlBatch(queries);
18840
+ }
18524
18841
  }
18525
18842
  }
18526
18843
  }
@@ -18530,7 +18847,7 @@ var OfflineDBSyncManager = class {
18530
18847
  });
18531
18848
  } catch (e) {
18532
18849
  console.log("An error has occurred while syncing the DB.", e);
18533
- if ((0, import_axios4.isAxiosError)(e) && e.code === "ECONNABORTED") {
18850
+ if ((0, import_axios5.isAxiosError)(e) && e.code === "ECONNABORTED") {
18534
18851
  return;
18535
18852
  }
18536
18853
  const error = e;
@@ -18564,12 +18881,16 @@ var OfflineDBSyncManager = class {
18564
18881
  };
18565
18882
  this.client = client;
18566
18883
  this.offlineDb = offlineDb;
18884
+ this.syncMaxEventCount = syncMaxEventCount;
18567
18885
  }
18568
18886
  };
18569
18887
 
18570
18888
  // src/offline-support/offline_support_api.ts
18571
18889
  var AbstractOfflineDB = class {
18572
- constructor({ client }) {
18890
+ constructor({
18891
+ client,
18892
+ syncMaxEventCount
18893
+ }) {
18573
18894
  /**
18574
18895
  * Initializes the DB as well as its syncManager for a given userId.
18575
18896
  * It will update the DBs reactive state with initialization values.
@@ -19035,6 +19356,7 @@ var AbstractOfflineDB = class {
19035
19356
  return this.handleMessageUpdatedEvent({ event, execute });
19036
19357
  }
19037
19358
  if (type === "message.read" || type === "notification.mark_read") {
19359
+ if (event.thread) return [];
19038
19360
  return this.handleRead({ event, unreadMessages: 0, execute });
19039
19361
  }
19040
19362
  if (type === "message.read_locally") {
@@ -19259,7 +19581,11 @@ var AbstractOfflineDB = class {
19259
19581
  }
19260
19582
  };
19261
19583
  this.client = client;
19262
- this.syncManager = new OfflineDBSyncManager({ client, offlineDb: this });
19584
+ this.syncManager = new OfflineDBSyncManager({
19585
+ client,
19586
+ offlineDb: this,
19587
+ syncMaxEventCount
19588
+ });
19263
19589
  this.state = new StateStore({
19264
19590
  initialized: false,
19265
19591
  userId: this.client.userID