stream-chat 9.41.0 → 9.42.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.
@@ -748,6 +748,7 @@ export declare class Channel {
748
748
  _checkInitialized(): void;
749
749
  _initializeState(state: ChannelAPIResponse, messageSetToAddToIfDoesNotExist?: MessageSetType): {
750
750
  messageSet: import("./types").MessageSet;
751
+ filteredMessageIds: string[];
751
752
  };
752
753
  _extendEventWithOwnReactions(event: Event): void;
753
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
@@ -3,6 +3,7 @@ import type WebSocket from 'isomorphic-ws';
3
3
  import { Channel } from './channel';
4
4
  import { ClientState } from './client_state';
5
5
  import { StableWSConnection } from './connection';
6
+ import { UploadManager } from './uploadManager';
6
7
  import { TokenManager } from './token_manager';
7
8
  import { WSConnectionFallback } from './connection_fallback';
8
9
  import { Campaign } from './campaign';
@@ -44,6 +45,10 @@ export type MessageComposerSetupState = {
44
45
  export declare class StreamChat {
45
46
  private static _instance?;
46
47
  messageDeliveryReporter: MessageDeliveryReporter;
48
+ /**
49
+ * @internal
50
+ */
51
+ uploadManager: UploadManager;
47
52
  _user?: OwnUserResponse | UserResponse;
48
53
  appSettingsPromise?: Promise<AppSettingsAPIResponse>;
49
54
  activeChannels: {
@@ -28,6 +28,7 @@ export type { ThreadState, ThreadReadState, ThreadRepliesPagination, ThreadUserR
28
28
  export * from './thread_manager';
29
29
  export * from './token_manager';
30
30
  export * from './types';
31
+ export * from './uploadManager';
31
32
  export * from './channel_manager';
32
33
  export * from './offline-support';
33
34
  export * from './LiveLocationManager';
@@ -21,7 +21,7 @@ export declare class LocationComposer {
21
21
  readonly composer: MessageComposer;
22
22
  private _deviceId;
23
23
  constructor({ composer, message }: LocationComposerOptions);
24
- get config(): import("..").LocationComposerConfig;
24
+ get config(): import("./configuration").LocationComposerConfig;
25
25
  get deviceId(): string;
26
26
  get location(): StaticLocationPayload | LiveLocationPreview | null;
27
27
  get validLocation(): StaticLocationPayload | LiveLocationPayload | null;
@@ -73,4 +73,5 @@ export declare class AttachmentManager {
73
73
  uploadAttachment: (attachment: LocalUploadAttachment) => Promise<LocalUploadAttachment | undefined>;
74
74
  uploadFile: (file: FileReference | FileLike) => Promise<LocalUploadAttachment>;
75
75
  uploadFiles: (files: FileReference[] | FileList | FileLike[]) => Promise<LocalUploadAttachment[] | undefined>;
76
+ private upload;
76
77
  }
@@ -5,9 +5,14 @@ export type MinimumUploadRequestResult = {
5
5
  file: string;
6
6
  thumb_url?: string;
7
7
  } & Partial<Record<string, unknown>>;
8
- /** Optional second argument to `UploadRequestFn`; integrators may call `onProgress` to report 0–100 or `undefined` when indeterminate. */
8
+ /**
9
+ * Optional second argument to `UploadRequestFn`.
10
+ * - Call `onProgress` to report 0–100 or `undefined` when indeterminate.
11
+ * - Forward `abortSignal` to your upload implementation to cancel the upload if the user deletes the attachment while it's uploading.
12
+ */
9
13
  export type UploadRequestOptions = {
10
14
  onProgress?: (percent: number | undefined) => void;
15
+ abortSignal?: AbortSignal;
11
16
  };
12
17
  export type UploadRequestFn = (fileLike: FileReference | FileLike, options?: UploadRequestOptions) => Promise<MinimumUploadRequestResult>;
13
18
  export type DraftsConfiguration = {
@@ -13,5 +13,5 @@ export declare const getExtensionFromMimeType: (mimeType: string) => string | un
13
13
  export declare const readFileAsArrayBuffer: (file: File) => Promise<ArrayBuffer>;
14
14
  export declare const generateFileName: (mimeType: string) => string;
15
15
  export declare const isImageFile: (fileLike: FileReference | FileLike) => boolean;
16
- export declare const getAttachmentTypeFromMimeType: (mimeType: string) => "file" | "audio" | "video" | "image";
16
+ export declare const getAttachmentTypeFromMimeType: (mimeType: string) => "file" | "image" | "audio" | "video";
17
17
  export declare const ensureIsLocalAttachment: (attachment: Attachment | LocalAttachment) => LocalAttachment | null;
@@ -61,15 +61,15 @@ export declare class MessageComposer extends WithSubscriptions {
61
61
  locationComposer: LocationComposer;
62
62
  customDataManager: CustomDataManager;
63
63
  constructor({ composition, config, compositionContext, client, }: MessageComposerOptions);
64
- static evaluateContextType(compositionContext: CompositionContext): "channel" | "message" | "thread" | "legacy_thread";
64
+ static evaluateContextType(compositionContext: CompositionContext): "message" | "channel" | "thread" | "legacy_thread";
65
65
  static constructTag(compositionContext: CompositionContext): `${ReturnType<typeof MessageComposer.evaluateContextType>}_${string}`;
66
66
  static generateId: typeof generateUUIDv4;
67
67
  get config(): MessageComposerConfig;
68
68
  get editedMessage(): LocalMessage | undefined;
69
69
  set editedMessage(editedMessage: LocalMessage | undefined);
70
70
  setEditedMessage: (editedMessage: LocalMessage | null | undefined) => void;
71
- get contextType(): "channel" | "message" | "thread" | "legacy_thread";
72
- get tag(): `channel_${string}` | `message_${string}` | `thread_${string}` | `legacy_thread_${string}`;
71
+ get contextType(): "message" | "channel" | "thread" | "legacy_thread";
72
+ get tag(): `message_${string}` | `channel_${string}` | `thread_${string}` | `legacy_thread_${string}`;
73
73
  get threadId(): string | null;
74
74
  get client(): StreamChat;
75
75
  get id(): string;
@@ -111,7 +111,7 @@ export declare class MessageComposer extends WithSubscriptions {
111
111
  clear: () => void;
112
112
  restore: () => void;
113
113
  compose: () => Promise<MessageComposerMiddlewareValue["state"] | undefined>;
114
- composeDraft: () => Promise<import("..").MessageDraftComposerMiddlewareValueState | undefined>;
114
+ composeDraft: () => Promise<import("./middleware").MessageDraftComposerMiddlewareValueState | undefined>;
115
115
  createDraft: () => Promise<void>;
116
116
  deleteDraft: () => Promise<void>;
117
117
  getDraft: () => Promise<void>;
@@ -21,12 +21,12 @@ export declare class CommandSearchSource extends BaseSearchSourceSync<CommandSug
21
21
  };
22
22
  query(searchQuery: string): {
23
23
  items: {
24
- id: "all" | "giphy" | "mute" | "unmute" | "ban" | "fun_set" | "moderation_set" | "unban";
24
+ id: "all" | "giphy" | "ban" | "fun_set" | "moderation_set" | "mute" | "unban" | "unmute";
25
25
  created_at?: string | undefined;
26
26
  updated_at?: string | undefined;
27
27
  args?: string;
28
28
  description?: string;
29
- name: "all" | "giphy" | "mute" | "unmute" | "ban" | "fun_set" | "moderation_set" | "unban";
29
+ name: "all" | "giphy" | "ban" | "fun_set" | "moderation_set" | "mute" | "unban" | "unmute";
30
30
  set?: import("../../..").CommandVariants;
31
31
  }[];
32
32
  next: null;
@@ -20,7 +20,7 @@ export declare class PollComposer {
20
20
  get id(): string;
21
21
  get max_votes_allowed(): string;
22
22
  get name(): string;
23
- get options(): import("..").PollComposerOption[];
23
+ get options(): import("./middleware").PollComposerOption[];
24
24
  get user_id(): string | undefined;
25
25
  get voting_visibility(): VotingVisibility | undefined;
26
26
  get canCreatePoll(): boolean;
@@ -32,5 +32,5 @@ export declare class PollComposer {
32
32
  */
33
33
  updateFields: (data: UpdateFieldsData, injectedFieldErrors?: PollComposerFieldErrors) => Promise<void>;
34
34
  handleFieldBlur: (field: keyof PollComposerState["data"]) => Promise<void>;
35
- compose: () => Promise<import("..").PollComposerCompositionMiddlewareValueState | undefined>;
35
+ compose: () => Promise<import("./middleware").PollComposerCompositionMiddlewareValueState | undefined>;
36
36
  }
@@ -17,7 +17,7 @@ export declare class TextComposer {
17
17
  middlewareExecutor: TextComposerMiddlewareExecutor;
18
18
  constructor({ composer, message }: TextComposerOptions);
19
19
  get channel(): import("..").Channel;
20
- get config(): import("..").TextComposerConfig;
20
+ get config(): import("./configuration").TextComposerConfig;
21
21
  get enabled(): boolean;
22
22
  set enabled(enabled: boolean);
23
23
  get defaultValue(): string | undefined;
@@ -31,7 +31,7 @@ export declare class TextComposer {
31
31
  get command(): CommandResponse | null | undefined;
32
32
  get mentionedUsers(): UserResponse[];
33
33
  get selection(): TextSelection;
34
- get suggestions(): Suggestions<import("..").Suggestion> | undefined;
34
+ get suggestions(): Suggestions<import("./middleware").Suggestion> | undefined;
35
35
  get text(): string;
36
36
  get textIsEmpty(): boolean;
37
37
  initState: ({ message }?: {
@@ -0,0 +1,45 @@
1
+ import type { StreamChat } from './client';
2
+ import { StateStore } from './store';
3
+ import type { AttachmentManager } from '.';
4
+ export type UploadRecord = {
5
+ id: string;
6
+ uploadProgress?: number;
7
+ };
8
+ export type UploadManagerState = {
9
+ uploads: Record<string, UploadRecord>;
10
+ };
11
+ /**
12
+ * @internal
13
+ */
14
+ export declare class UploadManager {
15
+ private readonly client;
16
+ readonly state: StateStore<UploadManagerState>;
17
+ private inFlightUploads;
18
+ constructor(client: StreamChat);
19
+ private resolveAttachmentManager;
20
+ get uploads(): Record<string, UploadRecord>;
21
+ getUpload: (id: string) => UploadRecord;
22
+ /**
23
+ * Clears all upload records.
24
+ * Invoked when the user disconnects so a later session does not inherit stale upload state.
25
+ * Aborts every in-flight upload request via its `UploadRequestOptions.abortSignal`.
26
+ */
27
+ reset: () => void;
28
+ /**
29
+ * Removes the upload record for `id` if present.
30
+ * If an upload is still in progress, aborts its `UploadRequestOptions.abortSignal`.
31
+ */
32
+ deleteUploadRecord: (id: string) => void;
33
+ /**
34
+ * Starts an upload for `id`, or returns the existing in-flight promise if one is already running.
35
+ * Uses {@link StreamChat.channel}(`channelCid`) → `messageComposer.attachmentManager.doUploadRequest`.
36
+ * Resolves with that result; rejects if the upload rejects (the record is removed from state either way).
37
+ */
38
+ upload: ({ id, channelCid, file, }: {
39
+ id: string;
40
+ channelCid: string;
41
+ file: Parameters<typeof AttachmentManager.prototype.doUploadRequest>[0];
42
+ }) => ReturnType<typeof AttachmentManager.prototype.doUploadRequest>;
43
+ private upsertUpload;
44
+ private updateUpload;
45
+ }
@@ -89,8 +89,6 @@ export declare const toDeletedMessage: ({ message, deletedAt, hardDelete, }: {
89
89
  deleted_at: Date | null;
90
90
  text?: string | undefined;
91
91
  user_id?: string | undefined;
92
- user?: (UserResponse | null) | undefined;
93
- channel?: import("./types").ChannelResponse | undefined;
94
92
  command?: string | undefined;
95
93
  mentioned_users?: UserResponse[] | undefined;
96
94
  latest_reactions?: import("./types").ReactionResponse[] | undefined;
@@ -103,9 +101,10 @@ export declare const toDeletedMessage: ({ message, deletedAt, hardDelete, }: {
103
101
  language: import("./types").TranslationLanguages;
104
102
  }) | undefined;
105
103
  html?: string | undefined;
106
- parent_id?: string | undefined;
104
+ user?: (UserResponse | null) | undefined;
107
105
  id: string;
108
106
  mml?: string | undefined;
107
+ parent_id?: string | undefined;
109
108
  pin_expires?: string | null | undefined;
110
109
  pinned?: boolean | undefined;
111
110
  poll_id?: string | undefined;
@@ -115,6 +114,7 @@ export declare const toDeletedMessage: ({ message, deletedAt, hardDelete, }: {
115
114
  silent?: boolean | undefined;
116
115
  args?: string | undefined;
117
116
  before_message_send_failed?: boolean | undefined;
117
+ channel?: import("./types").ChannelResponse | undefined;
118
118
  cid?: string | undefined;
119
119
  command_info?: {
120
120
  name?: string;
@@ -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.41.0",
3
+ "version": "9.42.0",
4
4
  "description": "JS SDK for the Stream Chat API",
5
5
  "homepage": "https://getstream.io/chat/",
6
6
  "author": {
@@ -51,7 +51,7 @@
51
51
  "dependencies": {
52
52
  "@types/jsonwebtoken": "^9.0.8",
53
53
  "@types/ws": "^8.5.14",
54
- "axios": "^1.12.2",
54
+ "axios": "^1.15.1",
55
55
  "base64-js": "^1.5.1",
56
56
  "form-data": "^4.0.4",
57
57
  "isomorphic-ws": "^5.0.0",
package/src/channel.ts CHANGED
@@ -1581,7 +1581,10 @@ export class Channel {
1581
1581
  }
1582
1582
 
1583
1583
  // add any messages to our channel state
1584
- const { messageSet } = this._initializeState(state, messageSetToAddToIfDoesNotExist);
1584
+ const { messageSet, filteredMessageIds } = this._initializeState(
1585
+ state,
1586
+ messageSetToAddToIfDoesNotExist,
1587
+ );
1585
1588
  messageSet.pagination = {
1586
1589
  ...messageSet.pagination,
1587
1590
  ...messageSetPagination({
@@ -1590,6 +1593,9 @@ export class Channel {
1590
1593
  requestedPageSize:
1591
1594
  options?.messages?.limit ?? DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE,
1592
1595
  returnedPage: state.messages,
1596
+ filteredReturnedPage: state.messages.filter(
1597
+ (m) => !filteredMessageIds.includes(m.id),
1598
+ ),
1593
1599
  logger: this.getClient().logger,
1594
1600
  }),
1595
1601
  };
@@ -2368,7 +2374,7 @@ export class Channel {
2368
2374
  if (!this.state.messages) {
2369
2375
  this.state.initMessages();
2370
2376
  }
2371
- const { messageSet } = this.state.addMessagesSorted(
2377
+ const { messageSet, filteredMessageIds } = this.state.addMessagesSorted(
2372
2378
  messages,
2373
2379
  false,
2374
2380
  true,
@@ -2434,6 +2440,7 @@ export class Channel {
2434
2440
 
2435
2441
  return {
2436
2442
  messageSet,
2443
+ filteredMessageIds,
2437
2444
  };
2438
2445
  }
2439
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
@@ -9,6 +9,7 @@ import type WebSocket from 'isomorphic-ws';
9
9
  import { Channel } from './channel';
10
10
  import { ClientState } from './client_state';
11
11
  import { StableWSConnection } from './connection';
12
+ import { UploadManager } from './uploadManager';
12
13
  import { CheckSignature, DevToken, JWTUserToken } from './signing';
13
14
  import { TokenManager } from './token_manager';
14
15
  import { WSConnectionFallback } from './connection_fallback';
@@ -296,6 +297,10 @@ export type MessageComposerSetupState = {
296
297
  export class StreamChat {
297
298
  private static _instance?: unknown | StreamChat; // type is undefined|StreamChat, unknown is due to TS limitations with statics
298
299
  messageDeliveryReporter: MessageDeliveryReporter;
300
+ /**
301
+ * @internal
302
+ */
303
+ uploadManager: UploadManager;
299
304
  _user?: OwnUserResponse | UserResponse;
300
305
  appSettingsPromise?: Promise<AppSettingsAPIResponse>;
301
306
  activeChannels: {
@@ -401,6 +406,7 @@ export class StreamChat {
401
406
  this.moderation = new Moderation(this);
402
407
 
403
408
  this.notifications = options?.notifications ?? new NotificationManager();
409
+ this.uploadManager = new UploadManager(this);
404
410
 
405
411
  // set the secret
406
412
  if (secretOrOptions && isString(secretOrOptions)) {
@@ -1020,6 +1026,7 @@ export class StreamChat {
1020
1026
  this.state = new ClientState({ client: this });
1021
1027
  // reset thread manager
1022
1028
  this.threads.resetState();
1029
+ this.uploadManager.reset();
1023
1030
 
1024
1031
  // Since we wipe all user data already, we should reset token manager as well
1025
1032
  closePromise
@@ -2040,12 +2047,17 @@ export class StreamChat {
2040
2047
  c.push_preferences = channelState.push_preferences;
2041
2048
 
2042
2049
  let updatedMessagesSet;
2050
+ let filteredMessageIds: string[] = [];
2043
2051
  if (skipInitialization === undefined) {
2044
- const { messageSet } = c._initializeState(channelState, 'latest');
2052
+ const { messageSet, filteredMessageIds: _filteredMessageIds } =
2053
+ c._initializeState(channelState, 'latest');
2054
+ filteredMessageIds = _filteredMessageIds;
2045
2055
  updatedMessagesSet = messageSet;
2046
2056
  } else if (!skipInitialization.includes(channelState.channel.id)) {
2047
2057
  c.state.clearMessages();
2048
- const { messageSet } = c._initializeState(channelState, 'latest');
2058
+ const { messageSet, filteredMessageIds: _filteredMessageIds } =
2059
+ c._initializeState(channelState, 'latest');
2060
+ filteredMessageIds = _filteredMessageIds;
2049
2061
  updatedMessagesSet = messageSet;
2050
2062
  }
2051
2063
 
@@ -2058,6 +2070,9 @@ export class StreamChat {
2058
2070
  queryChannelsOptions?.message_limit ||
2059
2071
  DEFAULT_QUERY_CHANNELS_MESSAGE_LIST_PAGE_SIZE,
2060
2072
  returnedPage: channelState.messages,
2073
+ filteredReturnedPage: channelState.messages.filter(
2074
+ (m) => !filteredMessageIds.includes(m.id),
2075
+ ),
2061
2076
  logger: this.logger,
2062
2077
  }),
2063
2078
  };
package/src/index.ts CHANGED
@@ -33,6 +33,7 @@ export type {
33
33
  export * from './thread_manager';
34
34
  export * from './token_manager';
35
35
  export * from './types';
36
+ export * from './uploadManager';
36
37
  export * from './channel_manager';
37
38
  export * from './offline-support';
38
39
  export * from './LiveLocationManager';
@@ -237,12 +237,12 @@ export class AttachmentManager {
237
237
  return attachments;
238
238
  }
239
239
  }
240
- return null;
240
+ return stateAttachments;
241
241
  };
242
242
 
243
243
  updateAttachment = (attachmentToUpdate: LocalAttachment) => {
244
244
  const updatedAttachments = this.prepareAttachmentUpdate(attachmentToUpdate);
245
- if (updatedAttachments) {
245
+ if (updatedAttachments && updatedAttachments !== this.attachments) {
246
246
  this.state.partialNext({ attachments: updatedAttachments });
247
247
  }
248
248
  };
@@ -253,16 +253,17 @@ export class AttachmentManager {
253
253
  let hasUpdates = false;
254
254
  attachmentsToUpsert.forEach((attachment) => {
255
255
  const updatedAttachments = this.prepareAttachmentUpdate(attachment);
256
- if (updatedAttachments) {
257
- attachments = updatedAttachments;
258
- hasUpdates = true;
259
- } else {
256
+ if (updatedAttachments === null) {
260
257
  const localAttachment = ensureIsLocalAttachment(attachment);
261
258
  if (localAttachment) {
262
259
  attachments.push(localAttachment);
263
260
  hasUpdates = true;
264
261
  }
262
+ } else if (updatedAttachments !== this.attachments) {
263
+ attachments = updatedAttachments;
264
+ hasUpdates = true;
265
265
  }
266
+ // else: id exists and merge was a no-op (`prepareAttachmentUpdate` returns current state)
266
267
  });
267
268
  if (hasUpdates) {
268
269
  this.state.partialNext({ attachments });
@@ -270,11 +271,17 @@ export class AttachmentManager {
270
271
  };
271
272
 
272
273
  removeAttachments = (localAttachmentIds: string[]) => {
274
+ if (!localAttachmentIds.length) return;
275
+
273
276
  this.state.partialNext({
274
277
  attachments: this.attachments.filter(
275
278
  (attachment) => !localAttachmentIds.includes(attachment.localMetadata?.id),
276
279
  ),
277
280
  });
281
+
282
+ for (const id of localAttachmentIds) {
283
+ this.client.uploadManager.deleteUploadRecord(id);
284
+ }
278
285
  };
279
286
 
280
287
  getUploadConfigCheck = async (
@@ -464,13 +471,21 @@ export class AttachmentManager {
464
471
  }
465
472
  : undefined;
466
473
 
474
+ const axiosUploadConfig =
475
+ progressHandler || options?.abortSignal
476
+ ? {
477
+ ...(progressHandler ? { onUploadProgress: progressHandler } : {}),
478
+ ...(options?.abortSignal ? { signal: options.abortSignal } : {}),
479
+ }
480
+ : undefined;
481
+
467
482
  if (isFileReference(fileLike)) {
468
483
  return this.channel[isImageFile(fileLike) ? 'sendImage' : 'sendFile'](
469
484
  fileLike.uri,
470
485
  fileLike.name,
471
486
  fileLike.type,
472
487
  undefined,
473
- progressHandler ? { onUploadProgress: progressHandler } : undefined,
488
+ axiosUploadConfig,
474
489
  );
475
490
  }
476
491
 
@@ -485,13 +500,7 @@ export class AttachmentManager {
485
500
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
486
501
  const { duration, ...result } = await this.channel[
487
502
  isImageFile(fileLike) ? 'sendImage' : 'sendFile'
488
- ](
489
- file,
490
- undefined,
491
- undefined,
492
- undefined,
493
- progressHandler ? { onUploadProgress: progressHandler } : undefined,
494
- );
503
+ ](file, undefined, undefined, undefined, axiosUploadConfig);
495
504
  return result;
496
505
  };
497
506
 
@@ -537,37 +546,9 @@ export class AttachmentManager {
537
546
  return localAttachment;
538
547
  }
539
548
 
540
- const shouldTrackProgress = this.config.trackUploadProgress;
541
- const uploadingAttachment: LocalUploadAttachment = {
542
- ...attachment,
543
- localMetadata: {
544
- ...attachment.localMetadata,
545
- uploadState: 'uploading',
546
- ...(shouldTrackProgress && { uploadProgress: 0 }),
547
- },
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;
564
-
565
549
  let response: MinimumUploadRequestResult;
566
550
  try {
567
- response = await this.doUploadRequest(
568
- localAttachment.localMetadata.file,
569
- uploadOptions,
570
- );
551
+ response = await this.upload(attachment);
571
552
  } catch (error) {
572
553
  const reason = error instanceof Error ? error.message : 'unknown error';
573
554
  const failedAttachment: LocalUploadAttachment = {
@@ -655,35 +636,10 @@ export class AttachmentManager {
655
636
  return preUpload.state.attachment;
656
637
  }
657
638
 
658
- const shouldTrackProgress = this.config.trackUploadProgress;
659
- attachment = {
660
- ...attachment,
661
- localMetadata: {
662
- ...attachment.localMetadata,
663
- uploadState: 'uploading',
664
- ...(shouldTrackProgress && { uploadProgress: 0 }),
665
- },
666
- };
667
- this.upsertAttachments([attachment]);
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
-
683
639
  let response: MinimumUploadRequestResult | undefined;
684
640
  let error: Error | undefined;
685
641
  try {
686
- response = await this.doUploadRequest(file, uploadOptions);
642
+ response = await this.upload(attachment);
687
643
  } catch (err) {
688
644
  error = err instanceof Error ? err : undefined;
689
645
  }
@@ -725,4 +681,44 @@ export class AttachmentManager {
725
681
  iterableFiles.slice(0, this.availableUploadSlots).map(this.uploadFile),
726
682
  );
727
683
  };
684
+
685
+ private upload(attachment: LocalUploadAttachment) {
686
+ const localId = attachment.localMetadata.id;
687
+
688
+ this.upsertAttachments([
689
+ {
690
+ ...attachment,
691
+ localMetadata: {
692
+ ...attachment.localMetadata,
693
+ uploadState: 'uploading',
694
+ uploadProgress: this.config.trackUploadProgress ? 0 : undefined,
695
+ },
696
+ },
697
+ ]);
698
+
699
+ const unsubscribe = this.client.uploadManager.state.subscribeWithSelector(
700
+ (s) => ({ upload: s.uploads[localId] }),
701
+ ({ upload: nextUpload }) => {
702
+ if (!nextUpload) return;
703
+ this.updateAttachment({
704
+ ...attachment,
705
+ localMetadata: {
706
+ ...attachment.localMetadata,
707
+ uploadState: 'uploading',
708
+ uploadProgress: nextUpload.uploadProgress,
709
+ },
710
+ });
711
+ },
712
+ );
713
+
714
+ return this.client.uploadManager
715
+ .upload({
716
+ id: localId,
717
+ channelCid: this.channel.cid,
718
+ file: attachment.localMetadata.file,
719
+ })
720
+ .finally(() => {
721
+ unsubscribe();
722
+ });
723
+ }
728
724
  }
@@ -6,9 +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. */
9
+ /**
10
+ * Optional second argument to `UploadRequestFn`.
11
+ * - Call `onProgress` to report 0–100 or `undefined` when indeterminate.
12
+ * - Forward `abortSignal` to your upload implementation to cancel the upload if the user deletes the attachment while it's uploading.
13
+ */
10
14
  export type UploadRequestOptions = {
11
15
  onProgress?: (percent: number | undefined) => void;
16
+ abortSignal?: AbortSignal;
12
17
  };
13
18
 
14
19
  export type UploadRequestFn = (