Skip to content

Commit 88490a5

Browse files
authored
feat!: move send with pending attachments logic to LLC (#3821)
Moves the logic for sending a message with pending (in progress or failed) attachments to the LLC. ## Breaking changes 1. Removed props from `Channel` - `doSendMessageRequest` is removed, use `client.config.set` to register a channel request handler - `allowSendBeforeAttachmentsUpload` is removed: - the SDK sets the `pendingUploadsEnabled` at the client level to `true` if offline support is enabled and integrators didn't already set a value for `pendingUploadsEnabled` - integrators can override this either on client-level or on channel-level 2. Attachment shape change The previous solution stored pending attachment metadata in `attachment.custom` and `attachment.image_url`/`attachment.asset_url` the new shape: ``` attachment.localMetadata // carries id and uri ``` The easiest way to display attachments is to use the two new helper methods: - `getAttachmentPreviewUrl` -> defined by `stream-chat-js` - `getPlayableVideoUrl` -> defined by RN SDK, used when we want to play a video (in this case we can't return the thumb URL - that the SDK creates) I also checked this comment: GetStream/stream-chat-js#1845 (comment) -> it doesn't cause any issues because the SDK always reads upload state from `uploadManager`, so a stale `uploading` state won't cause any issues. That being said typing wise it's not really developer friendly that we emit this data since `localMetadata` is never refreshed after the message is sent, so even in React integrators can think it's ok to read these fields, when it's not. But that issue is not RN specific.
1 parent 0f1c3c9 commit 88490a5

41 files changed

Lines changed: 841 additions & 755 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎ai-docs/ai-migration-v9-to-v10.md‎

Lines changed: 184 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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:
756760
The two throttle props were declared but never read in v10 — they are now deleted
757761
outright 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
10161041
SDK'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:
10441071
If 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:
19992093
A 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

Comments
 (0)