stream-chat 9.40.0 → 9.41.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
+ import type { AxiosRequestConfig } from 'axios';
1
2
  import { ChannelState } from './channel_state';
2
3
  import { CooldownTimer } from './CooldownTimer';
3
4
  import { MessageComposer } from './messageComposer';
@@ -81,8 +82,28 @@ export declare class Channel {
81
82
  */
82
83
  _sendMessage(message: Message, options?: SendMessageOptions): Promise<SendMessageAPIResponse>;
83
84
  sendMessage(message: Message, options?: SendMessageOptions): Promise<SendMessageAPIResponse>;
84
- sendFile(uri: string | NodeJS.ReadableStream | Buffer | File, name?: string, contentType?: string, user?: UserResponse): Promise<import("./types").SendFileAPIResponse>;
85
- sendImage(uri: string | NodeJS.ReadableStream | File, name?: string, contentType?: string, user?: UserResponse): Promise<import("./types").SendFileAPIResponse>;
85
+ /**
86
+ * Upload a file to this channel’s file endpoint (multipart). Forwards to the client’s `sendFile` implementation.
87
+ *
88
+ * @param uri File source: URL string, `File`, `Buffer`, or readable stream (Node).
89
+ * @param name File name sent in the multipart body.
90
+ * @param contentType MIME type; defaults are applied when omitted.
91
+ * @param user Optional user payload appended to the form as JSON.
92
+ * @param axiosRequestConfig Optional Axios per-request config, merged after upload defaults (e.g. `onUploadProgress`, `signal` from `AbortController`).
93
+ * @return Promise resolving to `{ file: string, ... }` with the CDN URL.
94
+ */
95
+ sendFile(uri: string | NodeJS.ReadableStream | Buffer | File, name?: string, contentType?: string, user?: UserResponse, axiosRequestConfig?: AxiosRequestConfig): Promise<import("./types").SendFileAPIResponse>;
96
+ /**
97
+ * Upload an image to this channel’s image endpoint (multipart). Uses the same transport as `sendFile`.
98
+ *
99
+ * @param uri Image source: URL string, `File`, or readable stream (Node). For `Buffer` uploads, use `sendFile` toward the channel file endpoint instead.
100
+ * @param name File name sent in the multipart body.
101
+ * @param contentType MIME type.
102
+ * @param user Optional user payload appended to the form as JSON.
103
+ * @param axiosRequestConfig Optional Axios per-request config, merged after upload defaults (e.g. `onUploadProgress`, `signal`).
104
+ * @return Promise resolving to `{ file: string, ... }` with the CDN URL.
105
+ */
106
+ sendImage(uri: string | NodeJS.ReadableStream | File, name?: string, contentType?: string, user?: UserResponse, axiosRequestConfig?: AxiosRequestConfig): Promise<import("./types").SendFileAPIResponse>;
86
107
  deleteFile(url: string): Promise<APIResponse>;
87
108
  deleteImage(url: string): Promise<APIResponse>;
88
109
  /**
@@ -727,6 +748,7 @@ export declare class Channel {
727
748
  _checkInitialized(): void;
728
749
  _initializeState(state: ChannelAPIResponse, messageSetToAddToIfDoesNotExist?: MessageSetType): {
729
750
  messageSet: import("./types").MessageSet;
751
+ filteredMessageIds: string[];
730
752
  };
731
753
  _extendEventWithOwnReactions(event: Event): void;
732
754
  _hydrateMembers({ members, overrideCurrentState, }: {
@@ -67,6 +67,7 @@ export declare class ChannelState {
67
67
  */
68
68
  addMessageSorted(newMessage: MessageResponse | LocalMessage, timestampChanged?: boolean, addIfDoesNotExist?: boolean, messageSetToAddToIfDoesNotExist?: MessageSetType): {
69
69
  messageSet: MessageSet;
70
+ filteredMessageIds: string[];
70
71
  };
71
72
  /**
72
73
  * Takes the message object, parses the dates, sets `__html`
@@ -87,6 +88,7 @@ export declare class ChannelState {
87
88
  */
88
89
  addMessagesSorted(newMessages: (MessageResponse | LocalMessage)[], timestampChanged?: boolean, initializing?: boolean, addIfDoesNotExist?: boolean, messageSetToAddToIfDoesNotExist?: MessageSetType): {
89
90
  messageSet: MessageSet;
91
+ filteredMessageIds: string[];
90
92
  };
91
93
  /**
92
94
  * addPinnedMessages - adds messages in pinnedMessages property
@@ -400,7 +400,7 @@ export declare class StreamChat {
400
400
  post<T>(url: string, data?: unknown): Promise<T>;
401
401
  patch<T>(url: string, data?: unknown): Promise<T>;
402
402
  delete<T>(url: string, params?: AxiosRequestConfig['params']): Promise<T>;
403
- sendFile(url: string, uri: string | NodeJS.ReadableStream | Buffer | File, name?: string, contentType?: string, user?: UserResponse): Promise<SendFileAPIResponse>;
403
+ sendFile(url: string, uri: string | NodeJS.ReadableStream | Buffer | File, name?: string, contentType?: string, user?: UserResponse, axiosRequestConfig?: AxiosRequestConfig): Promise<SendFileAPIResponse>;
404
404
  errorFromResponse(response: AxiosResponse<APIErrorResponse>): ErrorFromResponse<APIErrorResponse>;
405
405
  handleResponse<T>(response: AxiosResponse<T>): T;
406
406
  dispatchEvent: (event: Event) => void;
@@ -1975,10 +1975,11 @@ export declare class StreamChat {
1975
1975
  * @param {string} [name] The name of the file
1976
1976
  * @param {string} [contentType] The content type of the file
1977
1977
  * @param {UserResponse} [user] Optional user information
1978
+ * @param {AxiosRequestConfig} [axiosRequestConfig] Optional axios config (e.g. onUploadProgress for progress tracking)
1978
1979
  *
1979
1980
  * @return {Promise<SendFileAPIResponse>} Response containing the file URL
1980
1981
  */
1981
- uploadFile(uri: string | NodeJS.ReadableStream | Buffer | File, name?: string, contentType?: string, user?: UserResponse): Promise<SendFileAPIResponse>;
1982
+ uploadFile(uri: string | NodeJS.ReadableStream | Buffer | File, name?: string, contentType?: string, user?: UserResponse, axiosRequestConfig?: AxiosRequestConfig): Promise<SendFileAPIResponse>;
1982
1983
  /**
1983
1984
  * uploadImage - Uploads an image to the configured storage (defaults to Stream CDN)
1984
1985
  *
@@ -1986,10 +1987,11 @@ export declare class StreamChat {
1986
1987
  * @param {string} [name] The name of the image
1987
1988
  * @param {string} [contentType] The content type of the image
1988
1989
  * @param {UserResponse} [user] Optional user information
1990
+ * @param {AxiosRequestConfig} [axiosRequestConfig] Optional axios config (e.g. onUploadProgress for progress tracking)
1989
1991
  *
1990
1992
  * @return {Promise<SendFileAPIResponse>} Response containing the image URL
1991
1993
  */
1992
- uploadImage(uri: string | NodeJS.ReadableStream | File, name?: string, contentType?: string, user?: UserResponse): Promise<SendFileAPIResponse>;
1994
+ uploadImage(uri: string | NodeJS.ReadableStream | File, name?: string, contentType?: string, user?: UserResponse, axiosRequestConfig?: AxiosRequestConfig): Promise<SendFileAPIResponse>;
1993
1995
  /**
1994
1996
  * deleteFile - Deletes a file from the configured storage
1995
1997
  *
@@ -1,4 +1,4 @@
1
- import type { AttachmentManagerConfig, MinimumUploadRequestResult, UploadRequestFn } from './configuration';
1
+ import type { AttachmentManagerConfig, MinimumUploadRequestResult, UploadRequestFn, UploadRequestOptions } from './configuration';
2
2
  import { AttachmentPostUploadMiddlewareExecutor, AttachmentPreUploadMiddlewareExecutor } from './middleware/attachmentManager';
3
3
  import { StateStore } from '../store';
4
4
  import type { AttachmentLoadingState, FileLike, FileReference, LocalAttachment, LocalUploadAttachment, UploadPermissionCheckResult } from './types';
@@ -57,7 +57,7 @@ export declare class AttachmentManager {
57
57
  * Method to perform the default upload behavior without checking for custom upload functions
58
58
  * to prevent recursive calls
59
59
  */
60
- doDefaultUploadRequest: (fileLike: FileReference | FileLike) => Promise<{
60
+ doDefaultUploadRequest: (fileLike: FileReference | FileLike, options?: UploadRequestOptions) => Promise<{
61
61
  blocklist?: import("..").BlockListResponse;
62
62
  file: string;
63
63
  thumb_url?: string;
@@ -65,7 +65,7 @@ export declare class AttachmentManager {
65
65
  /**
66
66
  * todo: docs how to customize the image and file upload by overriding do
67
67
  */
68
- doUploadRequest: (fileLike: FileReference | FileLike) => Promise<MinimumUploadRequestResult | {
68
+ doUploadRequest: (fileLike: FileReference | FileLike, options?: UploadRequestOptions) => Promise<MinimumUploadRequestResult | {
69
69
  blocklist?: import("..").BlockListResponse;
70
70
  file: string;
71
71
  thumb_url?: string;
@@ -5,7 +5,11 @@ export type MinimumUploadRequestResult = {
5
5
  file: string;
6
6
  thumb_url?: string;
7
7
  } & Partial<Record<string, unknown>>;
8
- export type UploadRequestFn = (fileLike: FileReference | FileLike) => Promise<MinimumUploadRequestResult>;
8
+ /** Optional second argument to `UploadRequestFn`; integrators may call `onProgress` to report 0–100 or `undefined` when indeterminate. */
9
+ export type UploadRequestOptions = {
10
+ onProgress?: (percent: number | undefined) => void;
11
+ };
12
+ export type UploadRequestFn = (fileLike: FileReference | FileLike, options?: UploadRequestOptions) => Promise<MinimumUploadRequestResult>;
9
13
  export type DraftsConfiguration = {
10
14
  enabled: boolean;
11
15
  };
@@ -36,6 +40,12 @@ export type AttachmentManagerConfig = {
36
40
  acceptedFiles: string[];
37
41
  /** Function that allows to customize the upload request. */
38
42
  doUploadRequest?: UploadRequestFn;
43
+ /**
44
+ * When true, the attachment manager sets `localMetadata.uploadProgress` and passes `options.onProgress`
45
+ * to `doUploadRequest` (built-in and custom). Set to false to disable progress tracking.
46
+ * @default true
47
+ */
48
+ trackUploadProgress: boolean;
39
49
  };
40
50
  export type LinkPreviewsManagerConfig = {
41
51
  /** Number of milliseconds to debounce firing the URL enrichment queries when typing. The default value is 1500(ms). */
@@ -69,6 +69,8 @@ export type LocalAttachmentUploadMetadata = {
69
69
  previewUri?: string;
70
70
  uploadState: AttachmentLoadingState;
71
71
  uploadPermissionCheck?: UploadPermissionCheckResult;
72
+ /** 0–100 while uploading when progress tracking is enabled; undefined otherwise or when indeterminate */
73
+ uploadProgress?: number;
72
74
  };
73
75
  export type LocalImageAttachmentUploadMetadata = LocalAttachmentUploadMetadata & {
74
76
  /**
@@ -223,6 +223,7 @@ type MessagePaginationUpdatedParams = {
223
223
  parentSet: MessageSet;
224
224
  requestedPageSize: number;
225
225
  returnedPage: MessageResponse[];
226
+ filteredReturnedPage: MessageResponse[];
226
227
  logger?: Logger;
227
228
  messagePaginationOptions?: MessagePaginationOptions;
228
229
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stream-chat",
3
- "version": "9.40.0",
3
+ "version": "9.41.1",
4
4
  "description": "JS SDK for the Stream Chat API",
5
5
  "homepage": "https://getstream.io/chat/",
6
6
  "author": {
package/src/channel.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { AxiosRequestConfig } from 'axios';
1
2
  import { ChannelState } from './channel_state';
2
3
  import { CooldownTimer } from './CooldownTimer';
3
4
  import { MessageComposer } from './messageComposer';
@@ -245,11 +246,22 @@ export class Channel {
245
246
  return await this._sendMessage(message, options);
246
247
  }
247
248
 
249
+ /**
250
+ * Upload a file to this channel’s file endpoint (multipart). Forwards to the client’s `sendFile` implementation.
251
+ *
252
+ * @param uri File source: URL string, `File`, `Buffer`, or readable stream (Node).
253
+ * @param name File name sent in the multipart body.
254
+ * @param contentType MIME type; defaults are applied when omitted.
255
+ * @param user Optional user payload appended to the form as JSON.
256
+ * @param axiosRequestConfig Optional Axios per-request config, merged after upload defaults (e.g. `onUploadProgress`, `signal` from `AbortController`).
257
+ * @return Promise resolving to `{ file: string, ... }` with the CDN URL.
258
+ */
248
259
  sendFile(
249
260
  uri: string | NodeJS.ReadableStream | Buffer | File,
250
261
  name?: string,
251
262
  contentType?: string,
252
263
  user?: UserResponse,
264
+ axiosRequestConfig?: AxiosRequestConfig,
253
265
  ) {
254
266
  return this.getClient().sendFile(
255
267
  `${this._channelURL()}/file`,
@@ -257,14 +269,26 @@ export class Channel {
257
269
  name,
258
270
  contentType,
259
271
  user,
272
+ axiosRequestConfig,
260
273
  );
261
274
  }
262
275
 
276
+ /**
277
+ * Upload an image to this channel’s image endpoint (multipart). Uses the same transport as `sendFile`.
278
+ *
279
+ * @param uri Image source: URL string, `File`, or readable stream (Node). For `Buffer` uploads, use `sendFile` toward the channel file endpoint instead.
280
+ * @param name File name sent in the multipart body.
281
+ * @param contentType MIME type.
282
+ * @param user Optional user payload appended to the form as JSON.
283
+ * @param axiosRequestConfig Optional Axios per-request config, merged after upload defaults (e.g. `onUploadProgress`, `signal`).
284
+ * @return Promise resolving to `{ file: string, ... }` with the CDN URL.
285
+ */
263
286
  sendImage(
264
287
  uri: string | NodeJS.ReadableStream | File,
265
288
  name?: string,
266
289
  contentType?: string,
267
290
  user?: UserResponse,
291
+ axiosRequestConfig?: AxiosRequestConfig,
268
292
  ) {
269
293
  return this.getClient().sendFile(
270
294
  `${this._channelURL()}/image`,
@@ -272,6 +296,7 @@ export class Channel {
272
296
  name,
273
297
  contentType,
274
298
  user,
299
+ axiosRequestConfig,
275
300
  );
276
301
  }
277
302
 
@@ -1556,7 +1581,10 @@ export class Channel {
1556
1581
  }
1557
1582
 
1558
1583
  // add any messages to our channel state
1559
- const { messageSet } = this._initializeState(state, messageSetToAddToIfDoesNotExist);
1584
+ const { messageSet, filteredMessageIds } = this._initializeState(
1585
+ state,
1586
+ messageSetToAddToIfDoesNotExist,
1587
+ );
1560
1588
  messageSet.pagination = {
1561
1589
  ...messageSet.pagination,
1562
1590
  ...messageSetPagination({
@@ -1565,6 +1593,9 @@ export class Channel {
1565
1593
  requestedPageSize:
1566
1594
  options?.messages?.limit ?? DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE,
1567
1595
  returnedPage: state.messages,
1596
+ filteredReturnedPage: state.messages.filter(
1597
+ (m) => !filteredMessageIds.includes(m.id),
1598
+ ),
1568
1599
  logger: this.getClient().logger,
1569
1600
  }),
1570
1601
  };
@@ -2343,7 +2374,7 @@ export class Channel {
2343
2374
  if (!this.state.messages) {
2344
2375
  this.state.initMessages();
2345
2376
  }
2346
- const { messageSet } = this.state.addMessagesSorted(
2377
+ const { messageSet, filteredMessageIds } = this.state.addMessagesSorted(
2347
2378
  messages,
2348
2379
  false,
2349
2380
  true,
@@ -2409,6 +2440,7 @@ export class Channel {
2409
2440
 
2410
2441
  return {
2411
2442
  messageSet,
2443
+ filteredMessageIds,
2412
2444
  };
2413
2445
  }
2414
2446
 
@@ -217,9 +217,12 @@ export class ChannelState {
217
217
  messageSetToAddToIfDoesNotExist,
218
218
  );
219
219
 
220
+ const filteredMessageIds: string[] = [];
221
+
220
222
  for (let i = 0; i < messagesToAdd.length; i += 1) {
221
223
  const isFromShadowBannedUser = messagesToAdd[i].shadowed;
222
- if (isFromShadowBannedUser) {
224
+ if (isFromShadowBannedUser && addIfDoesNotExist) {
225
+ filteredMessageIds.push(messagesToAdd[i].id);
223
226
  continue;
224
227
  }
225
228
  // If message is already formatted we can skip the tasks below
@@ -306,6 +309,7 @@ export class ChannelState {
306
309
 
307
310
  return {
308
311
  messageSet: this.messageSets[targetMessageSetIndex],
312
+ filteredMessageIds,
309
313
  };
310
314
  }
311
315
 
package/src/client.ts CHANGED
@@ -1311,6 +1311,7 @@ export class StreamChat {
1311
1311
  name?: string,
1312
1312
  contentType?: string,
1313
1313
  user?: UserResponse,
1314
+ axiosRequestConfig?: AxiosRequestConfig,
1314
1315
  ) {
1315
1316
  const data = addFileToFormData(uri, name, contentType || 'multipart/form-data');
1316
1317
  if (user != null) data.append('user', JSON.stringify(user));
@@ -1321,6 +1322,7 @@ export class StreamChat {
1321
1322
  timeout: 0,
1322
1323
  maxContentLength: Infinity,
1323
1324
  maxBodyLength: Infinity,
1325
+ ...axiosRequestConfig,
1324
1326
  },
1325
1327
  });
1326
1328
  }
@@ -2038,12 +2040,17 @@ export class StreamChat {
2038
2040
  c.push_preferences = channelState.push_preferences;
2039
2041
 
2040
2042
  let updatedMessagesSet;
2043
+ let filteredMessageIds: string[] = [];
2041
2044
  if (skipInitialization === undefined) {
2042
- const { messageSet } = c._initializeState(channelState, 'latest');
2045
+ const { messageSet, filteredMessageIds: _filteredMessageIds } =
2046
+ c._initializeState(channelState, 'latest');
2047
+ filteredMessageIds = _filteredMessageIds;
2043
2048
  updatedMessagesSet = messageSet;
2044
2049
  } else if (!skipInitialization.includes(channelState.channel.id)) {
2045
2050
  c.state.clearMessages();
2046
- const { messageSet } = c._initializeState(channelState, 'latest');
2051
+ const { messageSet, filteredMessageIds: _filteredMessageIds } =
2052
+ c._initializeState(channelState, 'latest');
2053
+ filteredMessageIds = _filteredMessageIds;
2047
2054
  updatedMessagesSet = messageSet;
2048
2055
  }
2049
2056
 
@@ -2056,6 +2063,9 @@ export class StreamChat {
2056
2063
  queryChannelsOptions?.message_limit ||
2057
2064
  DEFAULT_QUERY_CHANNELS_MESSAGE_LIST_PAGE_SIZE,
2058
2065
  returnedPage: channelState.messages,
2066
+ filteredReturnedPage: channelState.messages.filter(
2067
+ (m) => !filteredMessageIds.includes(m.id),
2068
+ ),
2059
2069
  logger: this.logger,
2060
2070
  }),
2061
2071
  };
@@ -4866,6 +4876,7 @@ export class StreamChat {
4866
4876
  * @param {string} [name] The name of the file
4867
4877
  * @param {string} [contentType] The content type of the file
4868
4878
  * @param {UserResponse} [user] Optional user information
4879
+ * @param {AxiosRequestConfig} [axiosRequestConfig] Optional axios config (e.g. onUploadProgress for progress tracking)
4869
4880
  *
4870
4881
  * @return {Promise<SendFileAPIResponse>} Response containing the file URL
4871
4882
  */
@@ -4874,8 +4885,16 @@ export class StreamChat {
4874
4885
  name?: string,
4875
4886
  contentType?: string,
4876
4887
  user?: UserResponse,
4888
+ axiosRequestConfig?: AxiosRequestConfig,
4877
4889
  ) {
4878
- return this.sendFile(`${this.baseURL}/uploads/file`, uri, name, contentType, user);
4890
+ return this.sendFile(
4891
+ `${this.baseURL}/uploads/file`,
4892
+ uri,
4893
+ name,
4894
+ contentType,
4895
+ user,
4896
+ axiosRequestConfig,
4897
+ );
4879
4898
  }
4880
4899
 
4881
4900
  /**
@@ -4885,6 +4904,7 @@ export class StreamChat {
4885
4904
  * @param {string} [name] The name of the image
4886
4905
  * @param {string} [contentType] The content type of the image
4887
4906
  * @param {UserResponse} [user] Optional user information
4907
+ * @param {AxiosRequestConfig} [axiosRequestConfig] Optional axios config (e.g. onUploadProgress for progress tracking)
4888
4908
  *
4889
4909
  * @return {Promise<SendFileAPIResponse>} Response containing the image URL
4890
4910
  */
@@ -4893,8 +4913,16 @@ export class StreamChat {
4893
4913
  name?: string,
4894
4914
  contentType?: string,
4895
4915
  user?: UserResponse,
4916
+ axiosRequestConfig?: AxiosRequestConfig,
4896
4917
  ) {
4897
- return this.sendFile(`${this.baseURL}/uploads/image`, uri, name, contentType, user);
4918
+ return this.sendFile(
4919
+ `${this.baseURL}/uploads/image`,
4920
+ uri,
4921
+ name,
4922
+ contentType,
4923
+ user,
4924
+ axiosRequestConfig,
4925
+ );
4898
4926
  }
4899
4927
 
4900
4928
  /**
@@ -2,6 +2,7 @@ import type {
2
2
  AttachmentManagerConfig,
3
3
  MinimumUploadRequestResult,
4
4
  UploadRequestFn,
5
+ UploadRequestOptions,
5
6
  } from './configuration';
6
7
  import { isLocalImageAttachment, isUploadedAttachment } from './attachmentIdentity';
7
8
  import {
@@ -445,12 +446,31 @@ export class AttachmentManager {
445
446
  * Method to perform the default upload behavior without checking for custom upload functions
446
447
  * to prevent recursive calls
447
448
  */
448
- doDefaultUploadRequest = async (fileLike: FileReference | FileLike) => {
449
+ doDefaultUploadRequest = async (
450
+ fileLike: FileReference | FileLike,
451
+ options?: UploadRequestOptions,
452
+ ) => {
453
+ const progressHandler = options?.onProgress
454
+ ? (progressEvent: {
455
+ loaded: number;
456
+ total?: number;
457
+ lengthComputable?: boolean;
458
+ }) => {
459
+ const percent =
460
+ progressEvent.lengthComputable && progressEvent.total
461
+ ? Math.round((progressEvent.loaded * 100) / progressEvent.total)
462
+ : undefined;
463
+ options.onProgress?.(percent);
464
+ }
465
+ : undefined;
466
+
449
467
  if (isFileReference(fileLike)) {
450
468
  return this.channel[isImageFile(fileLike) ? 'sendImage' : 'sendFile'](
451
469
  fileLike.uri,
452
470
  fileLike.name,
453
471
  fileLike.type,
472
+ undefined,
473
+ progressHandler ? { onUploadProgress: progressHandler } : undefined,
454
474
  );
455
475
  }
456
476
 
@@ -463,8 +483,15 @@ export class AttachmentManager {
463
483
  });
464
484
 
465
485
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
466
- const { duration, ...result } =
467
- await this.channel[isImageFile(fileLike) ? 'sendImage' : 'sendFile'](file);
486
+ const { duration, ...result } = await this.channel[
487
+ isImageFile(fileLike) ? 'sendImage' : 'sendFile'
488
+ ](
489
+ file,
490
+ undefined,
491
+ undefined,
492
+ undefined,
493
+ progressHandler ? { onUploadProgress: progressHandler } : undefined,
494
+ );
468
495
  return result;
469
496
  };
470
497
 
@@ -472,13 +499,16 @@ export class AttachmentManager {
472
499
  * todo: docs how to customize the image and file upload by overriding do
473
500
  */
474
501
 
475
- doUploadRequest = async (fileLike: FileReference | FileLike) => {
502
+ doUploadRequest = async (
503
+ fileLike: FileReference | FileLike,
504
+ options?: UploadRequestOptions,
505
+ ) => {
476
506
  const customUploadFn = this.config.doUploadRequest;
477
507
  if (customUploadFn) {
478
- return await customUploadFn(fileLike);
508
+ return await customUploadFn(fileLike, options);
479
509
  }
480
510
 
481
- return this.doDefaultUploadRequest(fileLike);
511
+ return this.doDefaultUploadRequest(fileLike, options);
482
512
  };
483
513
 
484
514
  // @deprecated use attachmentManager.uploadFile(file)
@@ -507,19 +537,37 @@ export class AttachmentManager {
507
537
  return localAttachment;
508
538
  }
509
539
 
510
- this.upsertAttachments([
511
- {
512
- ...attachment,
513
- localMetadata: {
514
- ...attachment.localMetadata,
515
- uploadState: 'uploading',
516
- },
540
+ const shouldTrackProgress = this.config.trackUploadProgress;
541
+ const uploadingAttachment: LocalUploadAttachment = {
542
+ ...attachment,
543
+ localMetadata: {
544
+ ...attachment.localMetadata,
545
+ uploadState: 'uploading',
546
+ ...(shouldTrackProgress && { uploadProgress: 0 }),
517
547
  },
518
- ]);
548
+ };
549
+ this.upsertAttachments([uploadingAttachment]);
550
+
551
+ const uploadOptions = shouldTrackProgress
552
+ ? {
553
+ onProgress: (percent: number | undefined) => {
554
+ this.updateAttachment({
555
+ ...uploadingAttachment,
556
+ localMetadata: {
557
+ ...uploadingAttachment.localMetadata,
558
+ uploadProgress: percent,
559
+ },
560
+ });
561
+ },
562
+ }
563
+ : undefined;
519
564
 
520
565
  let response: MinimumUploadRequestResult;
521
566
  try {
522
- response = await this.doUploadRequest(localAttachment.localMetadata.file);
567
+ response = await this.doUploadRequest(
568
+ localAttachment.localMetadata.file,
569
+ uploadOptions,
570
+ );
523
571
  } catch (error) {
524
572
  const reason = error instanceof Error ? error.message : 'unknown error';
525
573
  const failedAttachment: LocalUploadAttachment = {
@@ -527,6 +575,7 @@ export class AttachmentManager {
527
575
  localMetadata: {
528
576
  ...attachment.localMetadata,
529
577
  uploadState: 'failed',
578
+ uploadProgress: undefined,
530
579
  },
531
580
  };
532
581
 
@@ -561,6 +610,7 @@ export class AttachmentManager {
561
610
  localMetadata: {
562
611
  ...attachment.localMetadata,
563
612
  uploadState: 'finished',
613
+ uploadProgress: undefined,
564
614
  },
565
615
  };
566
616
 
@@ -605,19 +655,35 @@ export class AttachmentManager {
605
655
  return preUpload.state.attachment;
606
656
  }
607
657
 
658
+ const shouldTrackProgress = this.config.trackUploadProgress;
608
659
  attachment = {
609
660
  ...attachment,
610
661
  localMetadata: {
611
662
  ...attachment.localMetadata,
612
663
  uploadState: 'uploading',
664
+ ...(shouldTrackProgress && { uploadProgress: 0 }),
613
665
  },
614
666
  };
615
667
  this.upsertAttachments([attachment]);
616
668
 
669
+ const uploadOptions = shouldTrackProgress
670
+ ? {
671
+ onProgress: (percent: number | undefined) => {
672
+ this.updateAttachment({
673
+ ...attachment,
674
+ localMetadata: {
675
+ ...attachment.localMetadata,
676
+ uploadProgress: percent,
677
+ },
678
+ });
679
+ },
680
+ }
681
+ : undefined;
682
+
617
683
  let response: MinimumUploadRequestResult | undefined;
618
684
  let error: Error | undefined;
619
685
  try {
620
- response = await this.doUploadRequest(file);
686
+ response = await this.doUploadRequest(file, uploadOptions);
621
687
  } catch (err) {
622
688
  error = err instanceof Error ? err : undefined;
623
689
  }
@@ -630,6 +696,7 @@ export class AttachmentManager {
630
696
  localMetadata: {
631
697
  ...attachment.localMetadata,
632
698
  uploadState: error ? 'failed' : 'finished',
699
+ uploadProgress: undefined,
633
700
  },
634
701
  },
635
702
  error,
@@ -31,6 +31,7 @@ export const DEFAULT_ATTACHMENT_MANAGER_CONFIG: AttachmentManagerConfig = {
31
31
  acceptedFiles: [], // an empty array means all files are accepted
32
32
  fileUploadFilter: () => true,
33
33
  maxNumberOfFilesPerMessage: API_MAX_FILES_ALLOWED_PER_MESSAGE,
34
+ trackUploadProgress: true,
34
35
  };
35
36
 
36
37
  export const DEFAULT_TEXT_COMPOSER_CONFIG: TextComposerConfig = {
@@ -6,8 +6,14 @@ export type MinimumUploadRequestResult = { file: string; thumb_url?: string } &
6
6
  Record<string, unknown>
7
7
  >;
8
8
 
9
+ /** Optional second argument to `UploadRequestFn`; integrators may call `onProgress` to report 0–100 or `undefined` when indeterminate. */
10
+ export type UploadRequestOptions = {
11
+ onProgress?: (percent: number | undefined) => void;
12
+ };
13
+
9
14
  export type UploadRequestFn = (
10
15
  fileLike: FileReference | FileLike,
16
+ options?: UploadRequestOptions,
11
17
  ) => Promise<MinimumUploadRequestResult>;
12
18
 
13
19
  export type DraftsConfiguration = {
@@ -43,6 +49,12 @@ export type AttachmentManagerConfig = {
43
49
  acceptedFiles: string[];
44
50
  /** Function that allows to customize the upload request. */
45
51
  doUploadRequest?: UploadRequestFn;
52
+ /**
53
+ * When true, the attachment manager sets `localMetadata.uploadProgress` and passes `options.onProgress`
54
+ * to `doUploadRequest` (built-in and custom). Set to false to disable progress tracking.
55
+ * @default true
56
+ */
57
+ trackUploadProgress: boolean;
46
58
  };
47
59
 
48
60
  export type LinkPreviewsManagerConfig = {
@@ -120,6 +120,8 @@ export type LocalAttachmentUploadMetadata = {
120
120
  previewUri?: string;
121
121
  uploadState: AttachmentLoadingState;
122
122
  uploadPermissionCheck?: UploadPermissionCheckResult; // added new
123
+ /** 0–100 while uploading when progress tracking is enabled; undefined otherwise or when indeterminate */
124
+ uploadProgress?: number;
123
125
  };
124
126
 
125
127
  export type LocalImageAttachmentUploadMetadata = LocalAttachmentUploadMetadata & {