stream-chat 9.51.0 → 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 (38) hide show
  1. package/dist/cjs/index.browser.js +382 -187
  2. package/dist/cjs/index.browser.js.map +4 -4
  3. package/dist/cjs/index.node.js +391 -188
  4. package/dist/cjs/index.node.js.map +4 -4
  5. package/dist/esm/index.mjs +380 -185
  6. package/dist/esm/index.mjs.map +4 -4
  7. package/dist/types/channel.d.ts +1 -1
  8. package/dist/types/client.d.ts +10 -0
  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/types.d.ts +5 -0
  18. package/dist/types/middleware.d.ts +8 -0
  19. package/dist/types/uploadManager.d.ts +30 -0
  20. package/dist/types/utils.d.ts +20 -1
  21. package/package.json +1 -1
  22. package/src/channel.ts +16 -8
  23. package/src/client.ts +17 -1
  24. package/src/index.ts +12 -0
  25. package/src/messageComposer/attachmentIdentity.ts +27 -0
  26. package/src/messageComposer/attachmentManager.ts +61 -12
  27. package/src/messageComposer/messageComposer.ts +43 -8
  28. package/src/messageComposer/middleware/attachmentManager/postUpload/AttachmentPostUploadMiddlewareExecutor.ts +1 -1
  29. package/src/messageComposer/middleware/attachmentManager/postUpload/attachmentEnrichment.ts +58 -33
  30. package/src/messageComposer/middleware/attachmentManager/postUpload/uploadErrorHandler.ts +4 -0
  31. package/src/messageComposer/middleware/messageComposer/MessageComposerMiddlewareExecutor.ts +9 -0
  32. package/src/messageComposer/middleware/messageComposer/attachments.ts +109 -0
  33. package/src/messageComposer/middleware/messageComposer/types.ts +22 -1
  34. package/src/messageComposer/types.ts +5 -0
  35. package/src/middleware.ts +11 -0
  36. package/src/offline-support/offline_support_api.ts +4 -0
  37. package/src/uploadManager.ts +45 -0
  38. package/src/utils.ts +72 -0
@@ -80,7 +80,7 @@ export declare class Channel {
80
80
  *
81
81
  * @return {Promise<SendMessageAPIResponse>} The Server Response
82
82
  */
83
- _sendMessage(message: Message, options?: SendMessageOptions): Promise<SendMessageAPIResponse>;
83
+ _sendMessage(rawMessage: Message, options?: SendMessageOptions): Promise<SendMessageAPIResponse>;
84
84
  sendMessage(message: Message, options?: SendMessageOptions): Promise<SendMessageAPIResponse>;
85
85
  /**
86
86
  * Upload a file to this channel’s file endpoint (multipart). Forwards to the client’s `sendFile` implementation.
@@ -990,6 +990,11 @@ export declare class StreamChat {
990
990
  }): Promise<FlagUserResponse>;
991
991
  /**
992
992
  * unflagMessage - unflag a message
993
+ *
994
+ * @deprecated The `/moderation/unflag` endpoint is deprecated and is a no-op on
995
+ * the backend - calling this resolves successfully but does not remove the flag.
996
+ * Use `client.moderation.submitAction()` against the review queue item instead.
997
+ *
993
998
  * @param {string} targetMessageID
994
999
  * @param {string} [options.user_id] currentUserID, only used with serverside auth
995
1000
  * @returns {Promise<APIResponse>}
@@ -999,6 +1004,11 @@ export declare class StreamChat {
999
1004
  }): Promise<FlagMessageResponse>;
1000
1005
  /**
1001
1006
  * unflagUser - unflag a user
1007
+ *
1008
+ * @deprecated The `/moderation/unflag` endpoint is deprecated and is a no-op on
1009
+ * the backend - calling this resolves successfully but does not remove the flag.
1010
+ * Use `client.moderation.submitAction()` against the review queue item instead.
1011
+ *
1002
1012
  * @param {string} targetID
1003
1013
  * @param {string} [options.user_id] currentUserID, only used with serverside auth
1004
1014
  * @returns {Promise<APIResponse>}
@@ -35,3 +35,10 @@ export * from './LiveLocationManager';
35
35
  export type { CustomAttachmentData, CustomChannelData, CustomCommandData, CustomEventData, CustomEventTypes, CustomMemberData, CustomMessageComposerData, CustomMessageData, CustomPollOptionData, CustomPollData, CustomReactionData, CustomUserData, CustomThreadData, } from './custom_types';
36
36
  export { isOwnUser, chatCodes, logChatPromiseExecution, localMessageToNewMessagePayload, formatMessage, promoteChannel, } from './utils';
37
37
  export { FixedSizeQueueCache } from './utils/FixedSizeQueueCache';
38
+ /**
39
+ * Tag-keyed async runners. Exported so the UI SDKs (and integrators) serialize their own
40
+ * actions with the same primitive the SDK uses internally instead of hand-rolling promise
41
+ * chains. Tags share one process-wide map, so namespace yours (`my-app/thing/${id}`) to avoid
42
+ * colliding with another caller's.
43
+ */
44
+ export { hasPending, settled, withCancellation, withoutConcurrency, } from './utils/concurrency';
@@ -3,6 +3,10 @@ import type { AudioAttachment, FileAttachment, GiphyAttachment, ImageAttachment,
3
3
  export declare const isScrapedContent: (attachment: Attachment) => boolean;
4
4
  export declare const isLocalAttachment: (attachment: unknown) => attachment is LocalAttachment;
5
5
  export declare const isLocalUploadAttachment: (attachment: unknown) => attachment is LocalUploadAttachment;
6
+ /** Whether an upload for this attachment is still expected to resolve. */
7
+ export declare const isPendingUpload: (attachment: unknown) => attachment is LocalUploadAttachment;
8
+ /** Whether this attachment's upload already resolved to a URL. */
9
+ export declare const isFinishedUpload: (attachment: unknown) => attachment is LocalUploadAttachment;
6
10
  export declare const isFileAttachment: (attachment: Attachment | LocalAttachment, supportedVideoFormat?: string[]) => attachment is FileAttachment;
7
11
  export declare const isLocalFileAttachment: (attachment: Attachment | LocalAttachment) => attachment is LocalFileAttachment;
8
12
  export declare const isImageAttachment: (attachment: Attachment) => attachment is ImageAttachment;
@@ -57,6 +57,14 @@ export declare class AttachmentManager {
57
57
  private prepareAttachmentUpdate;
58
58
  updateAttachment: (attachmentToUpdate: LocalAttachment) => void;
59
59
  upsertAttachments: (attachmentsToUpsert: LocalAttachment[]) => void;
60
+ /**
61
+ * Releases a local preview URL the SDK minted, if it minted one.
62
+ *
63
+ * Only `blob:` URLs are ours to revoke: `toLocalUploadAttachment` puts a
64
+ * `URL.createObjectURL` result in `previewUri` for a `File`, but for a React Native
65
+ * `FileReference` it stores the caller's own native file URI, which must be left alone.
66
+ */
67
+ private revokePreviewUri;
60
68
  removeAttachments: (localAttachmentIds: string[]) => void;
61
69
  getUploadConfigCheck: (fileLike: FileReference | FileLike) => Promise<UploadPermissionCheckResult>;
62
70
  static toLocalUploadAttachment: (fileLike: FileReference | FileLike) => LocalUploadAttachment;
@@ -107,6 +107,19 @@ export declare class MessageComposer extends WithSubscriptions {
107
107
  isCommandDisabled: (command: CommandResponse) => boolean;
108
108
  validateCommandSendability: (command: CommandResponse, text?: string) => CommandSendability;
109
109
  get isCommandSendable(): boolean;
110
+ /**
111
+ * Whether the installed composition middleware composes messages whose uploads are still in
112
+ * flight — see `MessageCompositionMiddleware.allowsPendingUploads`.
113
+ *
114
+ * Derived from the chain rather than from config, so there is exactly one switch: the UI SDK
115
+ * that implements the rest of the flow installs the middleware, and everything that has to
116
+ * follow from that (starting with {@link hasSendableData}) follows on its own.
117
+ *
118
+ * **Temporary API.** In v10 sending with pending uploads becomes part of the composer
119
+ * configuration (`MessageComposerConfig`, set through `updateConfig`), and this getter,
120
+ * together with the middleware declaration it reads, is replaced by that config field.
121
+ */
122
+ get allowsPendingUploads(): boolean;
110
123
  get hasSendableData(): boolean;
111
124
  get compositionIsEmpty(): boolean;
112
125
  get contentIsEmpty(): boolean;
@@ -1,2 +1,21 @@
1
1
  import type { AttachmentPostUploadMiddleware } from '../types';
2
- export declare const createPostUploadAttachmentEnrichmentMiddleware: () => AttachmentPostUploadMiddleware;
2
+ import type { MessageComposer } from '../../../messageComposer';
3
+ /**
4
+ * Writes the uploaded URL onto the attachment and releases its local preview.
5
+ *
6
+ * The preview is a blob URL the composer created for the file. Releasing it once the CDN URL is
7
+ * in is what keeps the browser from holding the file in memory.
8
+ *
9
+ * Pass `composer` and the preview is released only while the attachment is still one of the
10
+ * composer's own — still listed in `attachmentManager.attachments`, i.e. still shown above the
11
+ * input, waiting to be sent.
12
+ *
13
+ * This runs when an upload finishes, which can be after the composer was cleared: `uploadFile`
14
+ * executes this chain before the (by then no-op) `updateAttachment`. If the attachment is gone
15
+ * from the composer, a message is rendering from the preview instead, and releasing it would
16
+ * blank that message out. A message can be sent with uploads still running — see
17
+ * `createSendWithPendingUploadsAttachmentsMiddleware`.
18
+ *
19
+ * Omit `composer` and the preview is always released.
20
+ */
21
+ export declare const createPostUploadAttachmentEnrichmentMiddleware: (composer?: MessageComposer) => AttachmentPostUploadMiddleware;
@@ -1,7 +1,12 @@
1
1
  import { MiddlewareExecutor } from '../../../middleware';
2
- import type { MessageComposerMiddlewareExecutorOptions, MessageComposerMiddlewareState, MessageDraftComposerMiddlewareExecutorOptions, MessageDraftComposerMiddlewareValueState } from './types';
2
+ import type { MessageComposerMiddlewareExecutorOptions, MessageComposerMiddlewareState, MessageCompositionMiddleware, MessageDraftComposerMiddlewareExecutorOptions, MessageDraftComposerMiddlewareValueState } from './types';
3
3
  export declare class MessageComposerMiddlewareExecutor extends MiddlewareExecutor<MessageComposerMiddlewareState, 'compose'> {
4
4
  constructor({ composer }: MessageComposerMiddlewareExecutorOptions);
5
+ /**
6
+ * Narrows the base accessor to the composition middleware type, so callers can read the
7
+ * declarations it carries (`allowsPendingUploads`) without casting.
8
+ */
9
+ get installedMiddleware(): ReadonlyArray<MessageCompositionMiddleware>;
5
10
  }
6
11
  export declare class MessageDraftComposerMiddlewareExecutor extends MiddlewareExecutor<MessageDraftComposerMiddlewareValueState, 'compose'> {
7
12
  constructor({ composer }: MessageDraftComposerMiddlewareExecutorOptions);
@@ -1,4 +1,28 @@
1
1
  import type { MessageComposer } from '../../messageComposer';
2
2
  import type { MessageCompositionMiddleware, MessageDraftCompositionMiddleware } from './types';
3
+ /**
4
+ * Drop-in replacement for {@link createAttachmentsCompositionMiddleware} that lets a message be
5
+ * composed while its attachments are still uploading.
6
+ *
7
+ * Reuses the same middleware id, so installing it with
8
+ * `compositionMiddlewareExecutor.replace([...])` keeps its position in the chain. The default
9
+ * refuses instead: it warns "Wait until all attachments have uploaded" and discards the
10
+ * composition.
11
+ *
12
+ * **Installing this is only half of the flow.** The composition it produces is not ready for the
13
+ * wire — `message.attachments` omits everything that has no URL yet. Whoever performs the send
14
+ * has to await those uploads (`UploadManager.upload` is idempotent by `localMetadata.id`, so
15
+ * calling it again returns the in-flight promise), write the resolved URLs in, and send after
16
+ * that.
17
+ *
18
+ * Sendability follows on its own: the `allowsPendingUploads` declaration below is what
19
+ * {@link MessageComposer.hasSendableData} reads to stop treating an upload in flight as a
20
+ * blocker, so installing this middleware is the only switch there is.
21
+ *
22
+ * This is why there is no config option turning it on: the switch belongs to the UI SDK that
23
+ * implements the other half. stream-chat-react exposes it as a `Chat` prop;
24
+ * stream-chat-react-native as `allowSendBeforeAttachmentsUpload` on the message input.
25
+ */
26
+ export declare const createSendWithPendingUploadsAttachmentsMiddleware: (composer: MessageComposer) => MessageCompositionMiddleware;
3
27
  export declare const createAttachmentsCompositionMiddleware: (composer: MessageComposer) => MessageCompositionMiddleware;
4
28
  export declare const createDraftAttachmentsCompositionMiddleware: (composer: MessageComposer) => MessageDraftCompositionMiddleware;
@@ -16,5 +16,26 @@ export type MessageDraftComposerMiddlewareValueState = {
16
16
  export type MessageDraftComposerMiddlewareExecutorOptions = {
17
17
  composer: MessageComposer;
18
18
  };
19
- export type MessageCompositionMiddleware = Middleware<MessageComposerMiddlewareState, 'compose'>;
19
+ export type MessageCompositionMiddleware = Middleware<MessageComposerMiddlewareState, 'compose'> & {
20
+ /**
21
+ * Declares that this middleware composes messages whose attachment uploads are still in
22
+ * flight, instead of refusing to compose until they finish.
23
+ *
24
+ * {@link MessageComposer.hasSendableData} reads this: an upload in flight must not block the
25
+ * send while such a middleware is installed, or the send could never be triggered. Any
26
+ * middleware implementing that contract may set it - the composer keys off the declaration,
27
+ * not off a middleware id, so a custom implementation is treated the same as the one shipped
28
+ * here.
29
+ *
30
+ * Composing this way is only half of the flow: whoever performs the send has to await those
31
+ * uploads and write the resolved URLs into the payload. See
32
+ * {@link createSendWithPendingUploadsAttachmentsMiddleware}.
33
+ *
34
+ * **Temporary API.** In v10 sending with pending uploads becomes part of the composer
35
+ * configuration (`MessageComposerConfig`, set through `updateConfig`), and this declaration,
36
+ * together with {@link MessageComposer.allowsPendingUploads}, is replaced by that config
37
+ * field.
38
+ */
39
+ allowsPendingUploads?: boolean;
40
+ };
20
41
  export type MessageDraftCompositionMiddleware = Middleware<MessageDraftComposerMiddlewareValueState, 'compose'>;
@@ -68,6 +68,11 @@ export type LocalAttachmentUploadMetadata = {
68
68
  */
69
69
  previewUri?: string;
70
70
  uploadState: AttachmentLoadingState;
71
+ /**
72
+ * Mirrors {@link UploadRecord.uploadConfirmationPending}: every byte has been sent, but the server has
73
+ * not confirmed yet - not returned response to the upload request. Render an indeterminate indicator rather than a full bar.
74
+ */
75
+ uploadConfirmationPending?: boolean;
71
76
  uploadPermissionCheck?: UploadPermissionCheckResult;
72
77
  /** 0–100 while uploading when progress tracking is enabled; undefined otherwise or when indeterminate */
73
78
  uploadProgress?: number;
@@ -34,6 +34,14 @@ export declare class MiddlewareExecutor<TValue, THandlers extends string> {
34
34
  readonly id: string;
35
35
  private middleware;
36
36
  constructor();
37
+ /**
38
+ * The middleware currently installed, in execution order.
39
+ *
40
+ * Read-only: use `use` / `insert` / `replace` / `setOrder` / `remove` to change the chain. It
41
+ * exists so components can ask what the chain is capable of - a middleware may declare
42
+ * behaviour its host has to account for, and the host has no other way to find out.
43
+ */
44
+ get installedMiddleware(): ReadonlyArray<Middleware<TValue, THandlers>>;
37
45
  use(middleware: Middleware<TValue, THandlers> | Middleware<TValue, THandlers>[]): this;
38
46
  replace(middleware: Middleware<TValue, THandlers>[]): this;
39
47
  insert({ middleware, position, unique, }: {
@@ -1,8 +1,38 @@
1
1
  import type { StreamChat } from './client';
2
2
  import { StateStore } from './store';
3
3
  import type { AttachmentManager } from '.';
4
+ /**
5
+ * Whether an upload ended because it was aborted rather than because it failed.
6
+ *
7
+ * {@link UploadManager.deleteUploadRecord} and {@link UploadManager.cancelAllUploads} abort the
8
+ * request through its `AbortController`, which is what happens when an attachment is removed
9
+ * from the composer mid-upload or the client disconnects. Axios reports that as a
10
+ * `CanceledError` (`axios.isCancel`); a custom `doUploadRequest` on another transport
11
+ * conventionally throws a `DOMException` named `AbortError`.
12
+ *
13
+ * Callers use this to keep a deliberate cancellation from being reported as an error.
14
+ */
15
+ export declare const isUploadCancellation: (error: unknown) => boolean;
4
16
  export type UploadRecord = {
5
17
  id: string;
18
+ /**
19
+ * `true` once every byte has been handed to the transport but the server has not responded
20
+ * yet.
21
+ *
22
+ * Upload progress measures bytes *written to the connection*, not bytes the server
23
+ * acknowledged, so it reaches 100% the moment the request body is flushed — then the
24
+ * connection sits idle while the CDN ingests the file and the response travels back. On a
25
+ * large file over a slow link that window is long, and a UI that renders 100% as "done"
26
+ * claims the upload is confirmed while it is not.
27
+ *
28
+ * Nothing measurable happens during that window, so it is the point at which a determinate
29
+ * progress bar should hand over to an indeterminate one. Confirmation is the record being
30
+ * removed, not progress reaching 100.
31
+ *
32
+ * Requires `AttachmentManagerConfig.trackUploadProgress`; without progress reporting there is
33
+ * no way to tell when the flush happened, and this stays `false`.
34
+ */
35
+ uploadConfirmationPending?: boolean;
6
36
  uploadProgress?: number;
7
37
  };
8
38
  export type UploadManagerState = {
@@ -1,5 +1,5 @@
1
1
  import FormData from 'form-data';
2
- import type { AscDesc, ChannelFilters, ChannelQueryOptions, ChannelSort, ChannelSortBase, LocalMessage, LocalMessageBase, Logger, Message, MessagePaginationOptions, MessageResponse, MessageResponseBase, MessageSet, OwnUserResponse, PromoteChannelParams, ReactionGroupResponse, UpdatedMessage, UserResponse } from './types';
2
+ import type { AscDesc, Attachment, ChannelFilters, ChannelQueryOptions, ChannelSort, ChannelSortBase, LocalMessage, LocalMessageBase, Logger, Message, MessagePaginationOptions, MessageResponse, MessageResponseBase, MessageSet, OwnUserResponse, PromoteChannelParams, ReactionGroupResponse, UpdatedMessage, UserResponse } from './types';
3
3
  import type { StreamChat } from './client';
4
4
  import type { Channel } from './channel';
5
5
  import type { AxiosRequestConfig } from 'axios';
@@ -74,6 +74,25 @@ export declare function formatMessage(message: MessageResponse | MessageResponse
74
74
  * @param {MessageResponse} message `MessageResponse` object
75
75
  */
76
76
  export declare function unformatMessage(message: LocalMessage): MessageResponse;
77
+ /**
78
+ * Strips composer-internal state from a message's attachments, and drops any whose upload never
79
+ * resolved.
80
+ *
81
+ * `localMetadata` is how `AttachmentManager` tracks an upload (its id, the `File`, the local
82
+ * preview); it must never reach the API. An attachment that still carries it and has no remote
83
+ * URL was never settled — which is what happens when
84
+ * `createSendWithPendingUploadsAttachmentsMiddleware` is installed by a UI SDK that does not
85
+ * implement the rest of the flow (awaiting those uploads before sending). Sending it would store
86
+ * an attachment pointing at nothing, so it is dropped and logged instead.
87
+ *
88
+ * Returns the same message object when there was nothing to change.
89
+ */
90
+ export declare const sanitizeOutgoingAttachments: <T extends {
91
+ attachments?: Attachment[];
92
+ }>({ client, message, }: {
93
+ client: StreamChat;
94
+ message: T;
95
+ }) => T;
77
96
  export declare const localMessageToNewMessagePayload: (localMessage: LocalMessage) => Message;
78
97
  export declare const toUpdatedMessagePayload: (message: LocalMessage | Partial<MessageResponse>) => UpdatedMessage;
79
98
  export declare const toDeletedMessage: ({ message, deletedAt, hardDelete, }: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stream-chat",
3
- "version": "9.51.0",
3
+ "version": "9.52.0",
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
@@ -9,6 +9,7 @@ import {
9
9
  logChatPromiseExecution,
10
10
  messageSetPagination,
11
11
  normalizeQuerySort,
12
+ sanitizeOutgoingAttachments,
12
13
  } from './utils';
13
14
  import type { StreamChat } from './client';
14
15
  import { DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE } from './constants';
@@ -212,14 +213,17 @@ export class Channel {
212
213
  *
213
214
  * @return {Promise<SendMessageAPIResponse>} The Server Response
214
215
  */
215
- async _sendMessage(message: Message, options?: SendMessageOptions) {
216
- return await this.getClient().post<SendMessageAPIResponse>(
217
- this._channelURL() + '/message',
218
- {
219
- message,
220
- ...options,
221
- },
222
- );
216
+ async _sendMessage(rawMessage: Message, options?: SendMessageOptions) {
217
+ const client = this.getClient();
218
+ const message = sanitizeOutgoingAttachments({
219
+ client,
220
+ message: rawMessage,
221
+ });
222
+
223
+ return await client.post<SendMessageAPIResponse>(this._channelURL() + '/message', {
224
+ message,
225
+ ...options,
226
+ });
223
227
  }
224
228
 
225
229
  async sendMessage(message: Message, options?: SendMessageOptions) {
@@ -2007,6 +2011,10 @@ export class Channel {
2007
2011
  // delivery-report network sync below is skipped for it.
2008
2012
  case 'message.read_locally':
2009
2013
  case 'message.read':
2014
+ // Thread read events are handled indiscriminately within the reactive `thread` object, we can
2015
+ // skip them here in order to ensure the channel does not get read incidentally when it should
2016
+ // not.
2017
+ if (event.thread) break;
2010
2018
  if (event.user?.id && event.created_at) {
2011
2019
  const previousReadState = channelState.read[event.user.id];
2012
2020
  channelState.read[event.user.id] = {
package/src/client.ts CHANGED
@@ -36,6 +36,7 @@ import {
36
36
  normalizeQuerySort,
37
37
  randomId,
38
38
  retryInterval,
39
+ sanitizeOutgoingAttachments,
39
40
  sleep,
40
41
  toUpdatedMessagePayload,
41
42
  } from './utils';
@@ -3051,6 +3052,11 @@ export class StreamChat {
3051
3052
 
3052
3053
  /**
3053
3054
  * unflagMessage - unflag a message
3055
+ *
3056
+ * @deprecated The `/moderation/unflag` endpoint is deprecated and is a no-op on
3057
+ * the backend - calling this resolves successfully but does not remove the flag.
3058
+ * Use `client.moderation.submitAction()` against the review queue item instead.
3059
+ *
3054
3060
  * @param {string} targetMessageID
3055
3061
  * @param {string} [options.user_id] currentUserID, only used with serverside auth
3056
3062
  * @returns {Promise<APIResponse>}
@@ -3064,6 +3070,11 @@ export class StreamChat {
3064
3070
 
3065
3071
  /**
3066
3072
  * unflagUser - unflag a user
3073
+ *
3074
+ * @deprecated The `/moderation/unflag` endpoint is deprecated and is a no-op on
3075
+ * the backend - calling this resolves successfully but does not remove the flag.
3076
+ * Use `client.moderation.submitAction()` against the review queue item instead.
3077
+ *
3067
3078
  * @param {string} targetID
3068
3079
  * @param {string} [options.user_id] currentUserID, only used with serverside auth
3069
3080
  * @returns {Promise<APIResponse>}
@@ -3420,7 +3431,12 @@ export class StreamChat {
3420
3431
  }
3421
3432
 
3422
3433
  // should not include user object
3423
- const payload = toUpdatedMessagePayload(message);
3434
+ // Sanitized at the point of sending, which is the only place every path converges: the
3435
+ // offline replay of a queued `update-message` task calls this method directly.
3436
+ const payload = sanitizeOutgoingAttachments({
3437
+ client: this,
3438
+ message: toUpdatedMessagePayload(message),
3439
+ });
3424
3440
 
3425
3441
  // add user_id (if exists)
3426
3442
  if (typeof partialUserOrUserId === 'string') {
package/src/index.ts CHANGED
@@ -62,3 +62,15 @@ export {
62
62
  promoteChannel,
63
63
  } from './utils';
64
64
  export { FixedSizeQueueCache } from './utils/FixedSizeQueueCache';
65
+ /**
66
+ * Tag-keyed async runners. Exported so the UI SDKs (and integrators) serialize their own
67
+ * actions with the same primitive the SDK uses internally instead of hand-rolling promise
68
+ * chains. Tags share one process-wide map, so namespace yours (`my-app/thing/${id}`) to avoid
69
+ * colliding with another caller's.
70
+ */
71
+ export {
72
+ hasPending,
73
+ settled,
74
+ withCancellation,
75
+ withoutConcurrency,
76
+ } from './utils/concurrency';
@@ -1,5 +1,6 @@
1
1
  import type { Attachment, SharedLocationResponse } from '../types';
2
2
  import type {
3
+ AttachmentLoadingState,
3
4
  AudioAttachment,
4
5
  FileAttachment,
5
6
  GiphyAttachment,
@@ -27,6 +28,32 @@ export const isLocalUploadAttachment = (
27
28
  ): attachment is LocalUploadAttachment =>
28
29
  !!(attachment as LocalAttachment)?.localMetadata?.uploadState;
29
30
 
31
+ /**
32
+ * Upload states meaning "the bytes are not on the CDN yet, but they are on their way".
33
+ * `blocked` and `failed` are excluded - those never resolve on their own.
34
+ *
35
+ * Typed against {@link AttachmentLoadingState} rather than `string`, so renaming a state fails
36
+ * the build instead of silently making this list match nothing - it decides what gets sent.
37
+ */
38
+ const PENDING_UPLOAD_STATES: ReadonlyArray<AttachmentLoadingState> = [
39
+ 'pending',
40
+ 'uploading',
41
+ ];
42
+
43
+ /** Whether an upload for this attachment is still expected to resolve. */
44
+ export const isPendingUpload = (
45
+ attachment: unknown,
46
+ ): attachment is LocalUploadAttachment =>
47
+ isLocalUploadAttachment(attachment) &&
48
+ PENDING_UPLOAD_STATES.includes(attachment.localMetadata.uploadState);
49
+
50
+ /** Whether this attachment's upload already resolved to a URL. */
51
+ export const isFinishedUpload = (
52
+ attachment: unknown,
53
+ ): attachment is LocalUploadAttachment =>
54
+ isLocalUploadAttachment(attachment) &&
55
+ attachment.localMetadata.uploadState === 'finished';
56
+
30
57
  export const isFileAttachment = (
31
58
  attachment: Attachment | LocalAttachment,
32
59
  supportedVideoFormat: string[] = [],
@@ -27,6 +27,7 @@ import type {
27
27
  FileLike,
28
28
  FileReference,
29
29
  LocalAttachment,
30
+ LocalAttachmentUploadMetadata,
30
31
  LocalNotImageAttachment,
31
32
  LocalUploadAttachment,
32
33
  UploadPermissionCheckResult,
@@ -34,6 +35,7 @@ import type {
34
35
  import type { ChannelResponse, DraftMessage, LocalMessage } from '../types';
35
36
  import type { MessageComposer } from './messageComposer';
36
37
  import { mergeWithDiff } from '../utils/mergeWith';
38
+ import { isUploadCancellation } from '../uploadManager';
37
39
 
38
40
  export type FileUploadFilter = (file: Partial<LocalUploadAttachment>) => boolean;
39
41
 
@@ -233,6 +235,7 @@ export class AttachmentManager {
233
235
  ...attachment,
234
236
  localMetadata: {
235
237
  ...attachment.localMetadata,
238
+ uploadConfirmationPending: false,
236
239
  uploadProgress: undefined,
237
240
  uploadState: 'failed',
238
241
  },
@@ -332,9 +335,28 @@ export class AttachmentManager {
332
335
  }
333
336
  };
334
337
 
338
+ /**
339
+ * Releases a local preview URL the SDK minted, if it minted one.
340
+ *
341
+ * Only `blob:` URLs are ours to revoke: `toLocalUploadAttachment` puts a
342
+ * `URL.createObjectURL` result in `previewUri` for a `File`, but for a React Native
343
+ * `FileReference` it stores the caller's own native file URI, which must be left alone.
344
+ */
345
+ private revokePreviewUri = (attachment: LocalAttachment) => {
346
+ const previewUri = (
347
+ attachment.localMetadata as Partial<LocalAttachmentUploadMetadata>
348
+ )?.previewUri;
349
+
350
+ if (previewUri?.startsWith('blob:')) URL.revokeObjectURL?.(previewUri);
351
+ };
352
+
335
353
  removeAttachments = (localAttachmentIds: string[]) => {
336
354
  if (!localAttachmentIds.length) return;
337
355
 
356
+ const removedAttachments = this.attachments.filter((attachment) =>
357
+ localAttachmentIds.includes(attachment.localMetadata?.id),
358
+ );
359
+
338
360
  this.state.partialNext({
339
361
  attachments: this.attachments.filter(
340
362
  (attachment) => !localAttachmentIds.includes(attachment.localMetadata?.id),
@@ -344,6 +366,18 @@ export class AttachmentManager {
344
366
  for (const id of localAttachmentIds) {
345
367
  this.client.uploadManager.deleteUploadRecord(id);
346
368
  }
369
+
370
+ // Removal is the end of the line for these attachments, so the preview blob goes with
371
+ // them - otherwise every removed (or cancelled mid-upload) attachment leaks one blob URL
372
+ // until the page unloads. Revoked after the record is deleted, so nothing is still
373
+ // rendering from it.
374
+ //
375
+ // Deliberately not done in `initState`/`restoreSnapshot`: those drop the composer's *view*
376
+ // of the attachments without ending their life. A finished upload has already had its
377
+ // `previewUri` revoked and removed by the post-upload middleware, and an unfinished one may
378
+ // still be rendered by whoever took it over (an optimistic message sent with the upload
379
+ // still in flight, for instance).
380
+ removedAttachments.forEach(this.revokePreviewUri);
347
381
  };
348
382
 
349
383
  getUploadConfigCheck = async (
@@ -619,21 +653,30 @@ export class AttachmentManager {
619
653
  ...attachment.localMetadata,
620
654
  uploadState: 'failed',
621
655
  uploadProgress: undefined,
656
+ // `false`, not `undefined`: these updates go through `updateAttachment`, which merges
657
+ // via `mergeWith` — and that skips `undefined` source values, so `undefined` would
658
+ // leave a stale `true` in place.
659
+ uploadConfirmationPending: false,
622
660
  },
623
661
  };
624
662
 
625
- this.client.notifications.addError({
626
- message: 'Error uploading attachment',
627
- origin: {
628
- emitter: 'AttachmentManager',
629
- context: { attachment, failedAttachment },
630
- },
631
- options: {
632
- type: 'api:attachment:upload:failed',
633
- metadata: { reason },
634
- originalError: error instanceof Error ? error : undefined,
635
- },
636
- });
663
+ // A cancellation is the user getting what they asked for, so it gets no error
664
+ // notification. This path predates the post-upload middleware chain and notifies
665
+ // directly, so it needs the same guard `createUploadErrorHandlerMiddleware` applies.
666
+ if (!isUploadCancellation(error)) {
667
+ this.client.notifications.addError({
668
+ message: 'Error uploading attachment',
669
+ origin: {
670
+ emitter: 'AttachmentManager',
671
+ context: { attachment, failedAttachment },
672
+ },
673
+ options: {
674
+ type: 'api:attachment:upload:failed',
675
+ metadata: { reason },
676
+ originalError: error instanceof Error ? error : undefined,
677
+ },
678
+ });
679
+ }
637
680
 
638
681
  this.updateAttachment(failedAttachment);
639
682
  return failedAttachment;
@@ -654,6 +697,8 @@ export class AttachmentManager {
654
697
  ...attachment.localMetadata,
655
698
  uploadState: 'finished',
656
699
  uploadProgress: undefined,
700
+ // See the note in the failure branch: `undefined` cannot clear through `mergeWith`.
701
+ uploadConfirmationPending: false,
657
702
  },
658
703
  };
659
704
 
@@ -715,6 +760,8 @@ export class AttachmentManager {
715
760
  ...attachment.localMetadata,
716
761
  uploadState: error ? 'failed' : 'finished',
717
762
  uploadProgress: undefined,
763
+ // See the note in `uploadAttachment`: `undefined` cannot clear through `mergeWith`.
764
+ uploadConfirmationPending: false,
718
765
  },
719
766
  },
720
767
  error,
@@ -759,6 +806,7 @@ export class AttachmentManager {
759
806
  ...attachment.localMetadata,
760
807
  uploadState: 'uploading',
761
808
  uploadProgress: this.config.trackUploadProgress ? 0 : undefined,
809
+ uploadConfirmationPending: false,
762
810
  },
763
811
  },
764
812
  ]);
@@ -773,6 +821,7 @@ export class AttachmentManager {
773
821
  ...attachment.localMetadata,
774
822
  uploadState: 'uploading',
775
823
  uploadProgress: nextUpload.uploadProgress,
824
+ uploadConfirmationPending: nextUpload.uploadConfirmationPending,
776
825
  },
777
826
  });
778
827
  },