Skip to content

feat!: move send with pending attachments logic to LLC - #3821

Merged
szuperaz merged 9 commits into
V10from
adapt-message-composer-async-upload
Sep 24, 2026
Merged

szuperaz merged 9 commits into
V10from
adapt-message-composer-async-upload

Conversation

@szuperaz

@szuperaz szuperaz commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

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
  1. 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.

@Stream-SDK-Bot

Stream-SDK-Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

SDK Size

title develop branch diff status
js_bundle_size 2031 KB 2034 KB +3578 B 🔴

@szuperaz
szuperaz marked this pull request as ready for review September 23, 2026 14:59
@szuperaz
szuperaz requested a review from isekovanic September 23, 2026 14:59

@isekovanic isekovanic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great ! Thanks for taking this up 🤝

Have a few nitpicky comments but other than that LGTM

Comment thread package/src/components/Channel/hooks/useChannelRequestHandlers.ts
const localId = attachment.custom?.localId;
const localId = isLocalUploadAttachment(attachment) ? attachment.localMetadata.id : undefined;

const defaultOnPress = () => openUrlSafely(attachment.asset_url);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still reads attachment.asset_url, but after this PR a file attachment whose upload hasn't
settled has no asset_url if I understand correctly. Since the URL lives in localMetadata and openUrlSafely doesn't guard
undefined, this ends up at 'http://' + undefined, and thencanOpenURL('http://undefined') returns
true and tapping the attachment opens the browser at http://undefined.

I guess should be reproducible by just clicking on a file attachment whose upload hasn't finished yet (with async uploads enabled).

I think guarding openUrlSafely would be great (in general too) and would resolve this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this, fixed in 4245ca5

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
totalBytes={resolveAttachmentFullByteSize(attachment)}

I'm guessing this also needs to match how FileAttachment handles it ? Otherwise it would just be a spinner (with no max size)

@szuperaz szuperaz Sep 23, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works because custom.file_size is already set here: https://github.com/GetStream/stream-chat-js/blob/release-v10/src/messageComposer/attachmentManager.ts#L502 - so for RN both works, but I wanted to use the method defined by stream-chat to futureproof us, so changed it everywhere for resolveAttachmentFullByteSize in 173ca72

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
totalBytes={resolveAttachmentFullByteSize(attachment)}

same here I think

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
totalBytes={resolveAttachmentFullByteSize(attachment)}

same here

* The value this hook wrote is remembered, so a later `enableOfflineSupport` change moves the
* default along with it, while a value registered by anyone else is never overwritten.
*/
export const usePendingUploadsDefault = (client: StreamChat, enableOfflineSupport: boolean) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get what the hook is trying to do, but I think it can be simplified as:

Suggested change
export const usePendingUploadsDefault = (client: StreamChat, enableOfflineSupport: boolean) => {
export const usePendingUploadsDefault = (client: StreamChat, enableOfflineSupport: boolean) => {
useEffect(() => {
if (!client || !enableOfflineSupport) {
return;
}
const registered =
client.config.getConfig('messageComposer')?.attachments?.pendingUploadsEnabled;
if (registered !== undefined) {
return;
}
client.config.setConfig('messageComposer', {
attachments: { pendingUploadsEnabled: true },
});
// Deliberately no teardown, for the same reason as the network reporter in `useIsOnline`: the
// configuration lives as long as the client, which outlives `<Chat>`.
}, [client, enableOfflineSupport]);
};

the reason being, if the following chain of events occurs:

  1. we enable offline support
  2. we mount Chat (and the hook runs)
  3. after this, we manually set the config pendingUploads config to true (but the hook is unaware of this yet)
  4. further down the line, we disable offline support
  5. the hook now runs again and sets the config to false (even though we've already set it to true ourselves)

with the above hook, the only downside is that if we leave it to the defaults, setting offline to true and then to false after that will keep pending uploads to true. But this is kinda expected behaviour perhaps.

I know it's a weird edge case but probably safer.

What're your thoughts here ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with only setting if the integrators haven't set anything, fixed here: b8e11b8

Comment thread package/src/components/Channel/Channel.tsx

@isekovanic isekovanic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool ! Please update the AI migration guide when you get the chance as well so it reflects the changes

@szuperaz
szuperaz merged commit 88490a5 into V10 Sep 24, 2026
3 of 4 checks passed
@szuperaz
szuperaz deleted the adapt-message-composer-async-upload branch September 24, 2026 12:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants