@@ -143,7 +143,11 @@ means changed. Details in the linked section.
143143| ` useChannelContext().markRead() ` | ` useMarkRead(channel)() ` — or ` channel.markRead() ` | §5 |
144144| ` <Channel doMarkReadRequest> ` | ` client.config.set({ channel: { requestHandlers: { markReadRequest } } }) ` | §13.1 |
145145| ` <Channel doUpdateMessageRequest> ` | ` …{ requestHandlers: { updateMessageRequest } } ` | §13.1 |
146+ | ` <Channel doSendMessageRequest> ` | ` …{ requestHandlers: { sendMessageRequest } } ` (retry: ` retrySendMessageRequest ` ) | §13.1 |
146147| ` <Channel doFileUploadRequest> ` | ` client.config.set({ messageComposer: { attachments: { doUploadRequest } } }) ` | §13.1 |
148+ | ` attachment.custom?.localId ` | ` isLocalUploadAttachment(a) ? a.localMetadata.id : undefined ` | §17.6 |
149+ | ` attachment.custom?.originalFile?.uri ` | ` getAttachmentPreviewUrl(a, a.asset_url, a.image_url) ` (` stream-chat ` ); for video playback ` getPlayableVideoUrl(a) ` (SDK) | §17.6 |
150+ | ` createAttachmentsCompositionMiddleware ` (RN) | the same-named export from ` stream-chat ` | §17.6 |
147151| ` <Channel stateUpdateThrottleInterval> ` | ` …{ channel: { messagePaginator: { stateThrottleMs } } } ` | §13.1 |
148152| ` channel.getConfig() ` | ` channel.serverConfig ` (getter) — or ` channel.config ` for resolved gates | §13.1 |
149153| ` client.configs[cid] ` | ` client.channelServerConfigs[cid] ` | §13.1 |
@@ -756,10 +760,10 @@ Also removed, and covered in §13.1:
756760The two throttle props were declared but never read in v10 — they are now deleted
757761outright rather than left inert. ` stateThrottleMs ` is the real, reactive control.
758762
759- ` doSendMessageRequest ` is the one ` do * Request ` prop that **remains**. The SDK itself
760- occupies that handler slot to run the attachment-upload step inside the send pipeline,
761- so it wraps your handler rather than being replaced by it. Its ` message ` argument is
762- now typed ` MessageRequest ` (rename only; no shape change) .
763+ ` doSendMessageRequest ` is removed like every other ` do * Request ` prop — register a
764+ ` sendMessageRequest ` through ` client . config ` instead (§13.1). It used to stay because the SDK
765+ itself occupied that handler slot to run the attachment-upload step; ` stream - chat ` now awaits
766+ uploads before any handler runs, so the slot is yours alone .
763767
764768## 13.1 Instance configuration → ` client .config `
765769
@@ -826,7 +830,28 @@ Three things change with it:
826830 ` localMessage .cid ` you receive.
827831- **Thread-scoped handlers** go under the ` thread ` key with the same shape.
828832
829- ` doSendMessageRequest ` stays a prop — see §13.
833+ The send prop moves the same way. ` sendMessageRequest ` also receives the wire-ready ` message `
834+ ( ` MessageRequest ` , attachment uploads already resolved), and ` retrySendMessageRequest ` is a
835+ separate slot — register the same function in both to keep v9's behaviour, where one prop
836+ covered send and retry:
837+
838+ ` ` ` tsx
839+ // v9
840+ < Channel channel = {channel } doSendMessageRequest = {(channelId , message , options) => mySend(channelId , message , options )}>
841+
842+ // v10
843+ const sendMessageRequest = async ({ localMessage , message , options }) => {
844+ const response = await mySend (localMessage .cid , message , options );
845+ return { message: response .message };
846+ };
847+ client .config .set ({
848+ channel: { requestHandlers: { retrySendMessageRequest: sendMessageRequest , sendMessageRequest } },
849+ });
850+ ` ` `
851+
852+ If your handler resolves without a ` message ` , nothing is sent a second time — v9 fell back to
853+ ` channel .sendMessage ` , which sent the message twice. Return the server's response to update the
854+ message straight away; otherwise the ` message .new ` event reconciles it.
830855
831856### Behaviour, not values → setup functions
832857
@@ -1016,11 +1041,13 @@ root. Relevant only if you authored a custom component/override that reads them
10161041SDK's own reads are already migrated.
10171042
10181043- ` channel .data .name ` / ` channel .data .image ` → ` channel .data .custom ?.name ` / ` .custom ?.image `
1019- - Attachment metadata: ` attachment .mime_type ` / ` file_size ` / ` duration ` / ` originalFile ` →
1044+ - Attachment metadata: ` attachment .mime_type ` / ` file_size ` / ` duration ` →
10201045 ` attachment .custom ?.<same >` ( ` duration ` is now ` voiceRecording ` -only)
1046+ - ` attachment .originalFile ` → **gone entirely** — an attachment whose upload has not resolved
1047+ carries ` localMetadata ` instead, see §17.6
10211048
1022- The RN SDK augments ` CustomChannelData ` ( ` name ` , ` image ` ) and ` CustomAttachmentData `
1023- ( ` originalFile ` , ` localId ` ) . Add your own custom keys the same way (` declare module ' stream-chat' ` ).
1049+ The RN SDK augments ` CustomChannelData ` ( ` name ` , ` image ` ); ` mime_type ` and ` file_size ` come from
1050+ ` stream - chat ` itself . Add your own custom keys the same way (` declare module ' stream-chat' ` ).
10241051
10251052## 17.2 ` deleteMessage ` options are snake_case
10261053
@@ -1044,8 +1071,7 @@ params object rather than the prop's positional arguments:
10441071If you need the old ` { id , message }` request shape inside your handler, derive it with
10451072` localMessageToNewMessagePayload (localMessage )` — that is what the SDK's adapter used to do.
10461073
1047- ` doSendMessageRequest ` remains a prop; its ` message ` argument is now typed ` MessageRequest `
1048- (rename only; no shape change).
1074+ ` doSendMessageRequest ` is removed too — see §13.1 for its ` sendMessageRequest ` replacement.
10491075
10501076## 17.4 ` message .moderation_details ` → ` message .moderation `
10511077
@@ -1112,6 +1138,74 @@ Highlights that hit integrator code:
11121138 ` createAbortControllerForNextRequest ` moved to ` client .api ` .
11131139- **Sort is ` SortParamRequest []` ** — ` { last_message_at: - 1 }` → ` [{ field: ' last_message_at' , direction: - 1 }]` .
11141140
1141+ ## 17.6 Attachments mid-upload carry ` localMetadata ` , not ` custom .* `
1142+
1143+ Only relevant if you override a component that renders message attachments, or read an attachment
1144+ off a message that is still sending ( ` messageComposer .attachments .pendingUploadsEnabled ` , on by
1145+ default whenever ` enableOfflineSupport ` is set — see §O.3).
1146+
1147+ v9 flattened an unresolved attachment into a plain ` Attachment ` : the local file URI was written into
1148+ ` image_url ` / ` asset_url ` , and the upload id and file handle were smuggled through
1149+ ` custom .localId ` / ` custom .originalFile ` . v10 leaves this to ` stream - chat ` :
1150+ the switch is the composer's own ` attachments .pendingUploadsEnabled ` (§O.3), and with it on, ` stream - chat ` 's ` createAttachmentsCompositionMiddleware ` keeps the real
1151+ ` LocalUploadAttachment ` on the local message while the API payload carries only what already
1152+ resolved to a URL. ` MessageOperations ` then awaits the remaining uploads and fills in their URLs
1153+ before the request goes out:
1154+
1155+ ` ` ` ts
1156+ // v9
1157+ const localId = attachment .custom ?.localId ;
1158+ const uri = attachment .image_url ?? attachment .custom ?.originalFile ?.uri ;
1159+
1160+ // v10
1161+ import { getAttachmentPreviewUrl , isLocalUploadAttachment } from ' stream-chat' ;
1162+ import { getPlayableVideoUrl } from ' stream-chat-react-native' ; // or 'stream-chat-expo'
1163+
1164+ const localId = isLocalUploadAttachment (attachment ) ? attachment .localMetadata .id : undefined ;
1165+ const uri = getAttachmentPreviewUrl (attachment , attachment .asset_url , attachment .image_url ); // image, file, audio
1166+ const videoSource = getPlayableVideoUrl (attachment ); // video playback only
1167+ ` ` `
1168+
1169+ There is **no URL at all** on a pending attachment until its upload resolves — ` localMetadata `
1170+ holds ` id ` (the ` client .uploadManager ` key), ` file ` (the handle a retry needs), ` previewUri ` (what
1171+ to render meanwhile) and ` uploadState ` . Two helpers encode the distinction, and overrides should
1172+ use them rather than reading the fields directly:
1173+
1174+ - ` getAttachmentPreviewUrl (attachment , ... urls )` from ` stream - chat ` — the default for every type.
1175+ Returns the first of ` urls ` that is set (e.g. ` a .asset_url , a .image_url ` ), else
1176+ ` localMetadata .previewUri ` . For images, files and audio ` previewUri ` is the picked file's own
1177+ URI, so the same call renders an image, opens a file and plays audio.
1178+ - ` getPlayableVideoUrl (attachment )` from ` stream - chat - react - native ` / ` stream - chat - expo ` — **video
1179+ playback only**. It returns ` asset_url ` / ` image_url ` , else the local file URI, skipping
1180+ ` previewUri ` , which for a video is the thumbnail image
1181+ ( ` setupVideoAttachmentPreviewMiddleware ` puts it there).
1182+
1183+ ` getUrlOfImageAttachment ` already applies both — the preview fallback for images, the playable
1184+ source for videos — so the gallery, the image gallery and the channel-details media list need no
1185+ change.
1186+
1187+ Removed exports (they existed only to produce the v9 shape):
1188+
1189+ - ` createAttachmentsCompositionMiddleware ` — use ` stream - chat ` 's export of the same name, which the
1190+ composer installs by default. The SDK no longer replaces it; ` <Chat >` only supplies a
1191+ default for ` attachments .pendingUploadsEnabled ` (§O.3)
1192+ - ` createDraftAttachmentsCompositionMiddleware ` — the client's default applies now, which keeps
1193+ **successful uploads only**. A draft is sent to the server, so an unresolved attachment no longer
1194+ persists a local file URI no other device can read
1195+ - ` localAttachmentToAttachment `
1196+ - ` DefaultAttachmentData .originalFile ` / ` .localId `
1197+
1198+ ` setupVideoAttachmentPreviewMiddleware ` stays — a video file is not renderable, so its preview is
1199+ still the thumbnail the picker extracted.
1200+
1201+ Two exported URL utilities changed behaviour, because a pending attachment is rendered from a
1202+ local URI:
1203+
1204+ - ` isLocalUrl (url ? )` checks the scheme instead of searching for ` http ` anywhere in the string. A
1205+ ` content : // ` or `ph://` URI containing that substring now counts as local, where v9 called it
1206+ remote . It also accepts ` undefined ` and returns ` false ` for it ; v9 threw .
1207+ - ` makeImageCompatibleUrl(url?) ` accepts ` undefined ` and returns it unchanged ; v9 threw .
1208+
11151209-- -
11161210
11171211# Part J — ` ChannelList ` & ` ChannelManager ` (orchestrator )
@@ -1999,6 +2093,86 @@ What a prune does now, which is worth knowing if you build on the paginator:
19992093A value below the list's ` pageSize ` is raised to it: a cap smaller than a page would prune away the page a
20002094"load older" query had just fetched, and the list would immediately ask for it again.
20012095
2096+ ## O.3 ` < Channel allowSendBeforeAttachmentsUpload > ` removed; the switch is composer configuration
2097+
2098+ ` ` ` diff
2099+ - < Channel channel = {channel } allowSendBeforeAttachmentsUpload = {false }>
2100+ + < Channel channel = {channel }>
2101+ ` ` `
2102+
2103+ ` ` ` diff
2104+ + // before handing the client to <Chat>
2105+ + client .config .set ({ messageComposer: { attachments: { pendingUploadsEnabled: false } } });
2106+ ` ` `
2107+
2108+ Whether a message can be sent while its attachments are still uploading is now
2109+ ` stream - chat ` 's own composer setting, ` messageComposer .attachments .pendingUploadsEnabled ` . The prop
2110+ only ever forwarded to it, and it did so with an imperative ` updateConfig ` on every composer, which
2111+ outranks ` client .config ` . So a value you registered on the client could never win.
2112+
2113+ Removed with it:
2114+
2115+ - ` ChannelProps .allowSendBeforeAttachmentsUpload `
2116+ - ` MessageInputContextValue .allowSendBeforeAttachmentsUpload `
2117+ - ` ChannelProps .enableOfflineSupport ` . It existed only to seed the prop's default, and it is read
2118+ from ` <Chat >` .
2119+
2120+ Read the value with ` usePendingUploadsEnabled ()` in a component inside ` <Channel >` (it answers for
2121+ the current composer, so a thread composer answers for itself), or read
2122+ ` messageComposer .config .attachments .pendingUploadsEnabled ` directly. ` MessageList ` /
2123+ ` MessageFlashList ` take it as a ` pendingUploadsEnabled ` prop, which defaults to the composer's
2124+ value.
2125+
2126+ ### The default
2127+
2128+ ` <Chat >` writes ` pendingUploadsEnabled : enableOfflineSupport ` into ` client .config ` , **only if
2129+ nothing is registered there yet**. That's the same default the prop had. If ` enableOfflineSupport `
2130+ changes later, the SDK-written value follows it. A value anyone else registered is never
2131+ overwritten.
2132+
2133+ ### Turning it on or off for every channel
2134+
2135+ Register it on the client, ideally before the client reaches ` <Chat >` , so the SDK never writes at
2136+ all:
2137+
2138+ ` ` ` ts
2139+ client .config .set ({ messageComposer: { attachments: { pendingUploadsEnabled: false } } });
2140+ // or at construction
2141+ new StreamChat (apiKey , {
2142+ config: { messageComposer: { attachments: { pendingUploadsEnabled: false } } },
2143+ });
2144+ ` ` `
2145+
2146+ A ` client .config .set ` made after ` <Chat >` has mounted also takes effect. It deep-merges over the
2147+ SDK's default, and live composers re-derive.
2148+
2149+ ### Per channel (or per channel type): a ` messageComposer ` setup function
2150+
2151+ ` ` ` ts
2152+ client .config .setSetupFunction (' messageComposer' , ({ composer }) => {
2153+ if (composer .channel .type !== ' livestream' ) return ; // everything else keeps the default
2154+ composer .updateConfig ({ attachments: { pendingUploadsEnabled: false } });
2155+ });
2156+ ` ` `
2157+
2158+ It runs against every composer the client has, existing and future: a channel's own composer, each
2159+ of its threads' composers, and message-scoped edit composers. ` composer .channel ` is the parent
2160+ channel for all of them, so one check covers a channel and its threads. Branch on
2161+ ` composer .channel .cid ` for a single channel, or on ` composer .threadId ` for threads only.
2162+ ` updateConfig ` is a retained imperative patch, so it wins over both ` <Chat >` 's default and
2163+ ` client .config ` , whichever runs first.
2164+
2165+ - **One setup function per key.** Setting another replaces it. If you already use one (for
2166+ example, to insert composition middleware), put this branch into that same function.
2167+ - ` client .config .reset ()` clears the setup function and every patch it made.
2168+ - ` channel .messageComposer .updateConfig ({ attachments: { pendingUploadsEnabled: false } })` from a
2169+ channel screen also works, but only for that channel's own composer. It misses the channel's
2170+ thread composers, and the patch stays on the cached channel after the screen unmounts.
2171+
2172+ **Affects:** anyone passing ` allowSendBeforeAttachmentsUpload ` or ` enableOfflineSupport ` to
2173+ ` <Channel >` , or reading ` allowSendBeforeAttachmentsUpload ` from ` useMessageInputContext ()` . The
2174+ props are **removed, not deprecated**, so TypeScript flags them.
2175+
20022176---
20032177
20042178# Part I — i18n
0 commit comments