Skip to content

Unified CDN & Media Loading Architecture (v5)#4056

Open
nuno-vieira wants to merge 46 commits intodevelopfrom
add/improve-custom-cdn
Open

Unified CDN & Media Loading Architecture (v5)#4056
nuno-vieira wants to merge 46 commits intodevelopfrom
add/improve-custom-cdn

Conversation

@nuno-vieira
Copy link
Copy Markdown
Member

@nuno-vieira nuno-vieira commented Apr 8, 2026

🔗 Issue Links

🎯 Goal

Consolidate and simplify all CDN and media loading logic into unified protocols shared between UIKit and SwiftUI SDKs, with async support for pre-signed URLs.

📝 Summary

  • Add CDNRequester protocol for read-only URL transformation (signing, headers, resize) with async completion handler support
  • Add CDNStorage protocol replacing both CDNClient and AttachmentUploader for upload/delete operations
  • Add CDNRequest value type carrying URL, headers, and caching key
  • Add CDNImageResize model for server-side image resize parameters
  • Add StreamCDNRequester default implementation with Stream CDN resize/cache logic
  • Add StreamCDNStorage default implementation using Stream's REST API for multipart uploads
  • Move ImageResize from StreamChatUI to StreamChatCommonUI
  • Add unified MediaLoader protocol in StreamChatCommonUI replacing separate ImageLoader and VideoLoader
  • Add StreamMediaLoader default implementation handling both image loading and video preview generation
  • Add ImageDownloading protocol so CommonUI never depends on Nuke directly
  • Add StreamImageDownloader as the UIKit ImageDownloading backend
  • Add Options pattern types (ImageLoadOptions, ImageBatchLoadOptions, VideoLoadOptions) for extensible method signatures
  • Add cdnRequester and cdnStorage as public mutable properties on ChatClient
  • Update ChatMessageController.downloadAttachment() to use ChatClient.cdnRequester internally for URL transformation
  • Remove old ImageCDN, StreamImageCDN, ImageLoading, NukeImageLoader, VideoLoading, CDNClient, AttachmentUploader
  • Update Components to use mediaLoader: MediaLoader (replacing separate imageLoader + videoLoader)
  • Update ImageLoaderOptions and ImageDownloadOptions to include cdnRequester in the options struct
  • Update ChatClientConfig with cdnRequester and cdnStorage replacing customCDNClient and customAttachmentUploader

🛠 Implementation

The architecture splits CDN concerns into read and write paths with unified protocols replacing scattered types across both repos:

Protocol Module Responsibility
CDNRequester StreamChat Read-side URL transformation (signing, headers, resize) before loading images/files
CDNStorage StreamChat Write-side attachment upload and delete operations
MediaLoader StreamChatCommonUI Unified image downloading, caching, batch loading, video preview generation
ImageDownloading StreamChatCommonUI Pluggable HTTP download + cache backend (e.g. Nuke)

CDNRequester vs CDNStorage:

  • CDNRequester handles the read path — transforming URLs before GET requests (adding resize query params, signing, injecting headers, computing cache keys). The default StreamCDNRequester adds width/height/resize query params for stream-io-cdn.com URLs and builds stable caching keys.
  • CDNStorage handles the write path — uploading attachments (multipart, with progress) and deleting them by remote URL. The default StreamCDNStorage uses Stream's REST API. Configured via ChatClientConfig.cdnStorage (optional; nil uses the default).
  • Both cdnRequester and cdnStorage are public mutable properties on ChatClient, initialized from ChatClientConfig but changeable at runtime.

Unified MediaLoader:

MediaLoader merges the previous ImageLoader and VideoLoader into a single protocol. This eliminates a stale-reference problem: previously, StreamVideoLoader held a reference to ImageLoader at init time, so replacing components.imageLoader wouldn't affect video thumbnail loading. Now there's one mediaLoader property in Components (UIKit) and Utils (SwiftUI).

The protocol uses an Options patternImageLoadOptions, ImageBatchLoadOptions, and VideoLoadOptions struct parameters — so the CDNRequester is passed on every call rather than captured at init. This means changing chatClient.cdnRequester at runtime takes effect immediately. The Options pattern also makes the protocol extensible: adding future parameters requires only adding a field with a default value, not changing the method signature.

StreamMediaLoader handles both image loading (delegates to ImageDownloading backend) and video preview generation (AVFoundation). Video thumbnails are cached in an NSCache.

Attachment downloads:

ChatMessageController.downloadAttachment() and Chat.downloadAttachment() no longer expose a remoteURL parameter. Instead, they internally use ChatClient.cdnRequester.fileRequest() to transform the attachment URL before passing it to the download worker.

🧪 Manual Testing Notes

  1. Open the Demo App
  2. Verify images load correctly in channel list (avatars) and message list (image/video attachments)
  3. Verify video previews load correctly
  4. Test file attachment downloads
  5. Test file attachment previews

☑️ Contributor Checklist

  • I have signed the Stream CLA (required)
  • This change should be manually QAed
  • Changelog is updated with client-facing changes
  • Changelog is updated with new localization keys
  • New code is covered by unit tests
  • Documentation has been updated in the docs-content repo

Introduce 4 unified protocols for CDN operations:
- CDN: read-only URL transformation (signing, headers, resize)
- CDNUploader: upload and delete attachments
- ImageResize moved from StreamChatUI to StreamChat

Replace the old CDNClient and AttachmentUploader protocols with the
new CDNUploader. Refactor StreamCDNClient into StreamCDNUploader.
Simplify ChatClient upload API to use local URL directly.
…mChatCommonUI

Add shared image/video loading protocols and implementations:
- ImageLoader protocol with StreamImageLoader
- VideoLoader protocol with StreamVideoLoader (AVFoundation-based)
- ImageDownloading protocol: thin abstraction over an image pipeline

StreamImageLoader delegates actual downloading to an ImageDownloading
backend provided by each UI SDK, keeping Nuke out of CommonUI entirely.
StreamVideoLoader uses CDN for URL signing and ImageLoader for
thumbnail fallback.
Remove old protocols and their implementations:
- ImageCDN, StreamImageCDN, ImageLoading, NukeImageLoader
- VideoLoading, StreamVideoLoader

Replace with unified types from StreamChatCommonUI:
- Components.imageLoader uses StreamImageLoader with NukeImageDownloader
- Components.videoLoader uses StreamVideoLoader
- Add NukeImageDownloader as the ImageDownloading backend
- Add UIKit convenience extensions (ImageLoader+UIKit, VideoLoader+UIKit)
- Update all view call sites to new APIs
- Rename CDNClient_Spy to CDNUploader_Spy
- Remove AttachmentUploader_Spy (merged into CDNUploader_Spy)
- Rename CustomCDNClient to CustomCDNUploader
- Update APIClient_Tests and APIClient_Spy for new constructor
- Update StreamCDNClient_Tests for uploadAttachment(localUrl:)
- Replace ImageLoader_Mock and VideoLoader_Mock with new protocol conformances
- Add StreamCDN_Tests, StreamImageLoader_Tests, StreamVideoLoader_Tests
- Remove obsolete NukeImageLoader_Tests, ImageLoading_Tests, StreamImageCDN_Tests
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 8, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR refactors attachment and media loading infrastructure by replacing the AttachmentUploader + CDNClient protocol pair with a unified CDNStorage protocol, introducing new CDNRequester and CDNImageResize protocols for URL transformation, and restructuring image/video loading across StreamChat, StreamChatCommonUI, and StreamChatUI modules with new ImageDownloading, ImageLoader, and VideoLoader protocols.

Changes

Cohort / File(s) Summary
CDN Storage Refactor
Sources/StreamChat/APIClient/APIClient.swift, Sources/StreamChat/APIClient/CDNClient/CDNClient.swift, Sources/StreamChat/APIClient/CDNClient/CDNStorage.swift, Sources/StreamChat/APIClient/AttachmentUploader/AttachmentUploader.swift
Removed AttachmentUploader and CDNClient protocols; introduced unified CDNStorage protocol with uploadAttachment (message and URL variants) and deleteAttachment methods. Updated APIClient to accept cdnStorage instead of separate attachmentUploader and cdnClient dependencies.
CDN URL Transformation
Sources/StreamChat/APIClient/CDNClient/CDNRequester.swift, Sources/StreamChat/APIClient/CDNClient/CDNImageResize.swift, Sources/StreamChat/APIClient/CDNClient/StreamCDNRequester.swift
Added new CDNRequester protocol for transforming CDN URLs with optional resizing; introduced CDNImageResize for image resize parameters; implemented StreamCDNRequester for Stream CDN URL signing and caching key generation.
Image Loading Infrastructure
Sources/StreamChatCommonUI/ImageLoading/ImageDownloading.swift, Sources/StreamChatCommonUI/ImageLoading/ImageLoader.swift, Sources/StreamChatCommonUI/ImageLoading/StreamImageLoader.swift, Sources/StreamChatCommonUI/ImageLoading/ImageRequestOptions+ImageResize.swift
Added ImageDownloading protocol for raw image downloading; introduced ImageLoader protocol coordinating CDN URL transformation and downloading; implemented StreamImageLoader integrating CDNRequester and ImageDownloading.
Image Loading UI Components
Sources/StreamChatUI/Utils/ImageLoading/ImageLoader+UIKit.swift, Sources/StreamChatUI/Utils/ImageLoading/StreamImageDownloader.swift
Added ImageLoader+UIKit extension with UIImageView integration, batching, and task cancellation; implemented StreamImageDownloader wrapping Nuke image pipeline with CDN-aware caching.
Video Loading Infrastructure
Sources/StreamChatCommonUI/ImageLoading/VideoLoader.swift, Sources/StreamChatCommonUI/ImageLoading/StreamVideoLoader.swift
Introduced VideoLoader protocol for preview generation; implemented StreamVideoLoader with AVFoundation-based frame extraction, thumbnail caching, and memory warning handling.
Image/Video Loader Removals
Sources/StreamChatUI/Utils/ImageLoading/ImageLoading.swift, Sources/StreamChatUI/Utils/ImageLoading/NukeImageLoader.swift, Sources/StreamChatUI/Utils/VideoLoading/VideoLoading.swift
Removed old ImageLoading protocol and NukeImageLoader implementation; removed old VideoLoading protocol and legacy StreamVideoLoader; replaced with new protocol-based architecture.
CDN/Image Protocol Removals
Sources/StreamChatUI/Utils/ImageLoading/ImageCDN/ImageCDN.swift, Sources/StreamChatUI/Utils/ImageLoading/ImageCDN/StreamCDN.swift
Removed deprecated ImageCDN protocol and StreamImageCDN implementation; functionality replaced by CDNRequester and StreamCDNRequester.
Configuration Updates
Sources/StreamChat/Config/ChatClientConfig.swift, Sources/StreamChat/ChatClient+Environment.swift, Sources/StreamChatUI/Components.swift
Updated ChatClientConfig to replace customCDNClient and customAttachmentUploader with cdnRequester and cdnStorage; refactored Components to wire together StreamCDNRequester, StreamImageLoader, and StreamVideoLoader; adjusted apiClientBuilder closure signature.
Client/Factory Wiring
Sources/StreamChat/ChatClient.swift, Sources/StreamChat/ChatClientFactory.swift
Updated ChatClient to delegate upload/delete to cdnStorage instead of separate uploader/CDN; removed generic standalone upload API; updated ChatClientFactory to construct cdnStorage instead of cdnClient and attachmentUploader.
Attachment Download Enhancement
Sources/StreamChat/Controllers/MessageController/MessageController.swift, Sources/StreamChat/StateLayer/Chat.swift, Sources/StreamChat/Workers/MessageUpdater.swift
Extended downloadAttachment APIs across MessageController, Chat, and MessageUpdater to accept optional remoteURL parameter for CDN-signed URL downloads.
UI Component Updates
Sources/StreamChatUI/CommonViews/Attachments/AttachmentViews/*, Sources/StreamChatUI/CommonViews/AvatarView/*, Sources/StreamChatUI/Gallery/*
Updated video loader method calls from loadPreviewForVideo(at:) to loadPreview(at:); added StreamChatCommonUI imports to avatar and attachment preview components.
Processor Rename
Sources/StreamChatUI/Utils/ImageProcessor/StreamImageProcessor.swift
Renamed NukeImageProcessor to StreamImageProcessor to align with new naming convention.
Import/Module Updates
Sources/StreamChatUI/Utils/ImageLoading/ImageDownloadOptions.swift, Sources/StreamChatUI/Utils/ImageLoading/ImageLoaderOptions.swift, Sources/StreamChatCommonUI/ImageLoading/ImageResize.swift
Added StreamChatCommonUI imports to ImageDownloadOptions and ImageLoaderOptions; changed ImageResize.swift import from UIKit to CoreGraphics.
Test Spy/Mock Updates
TestTools/StreamChatTestTools/SpyPattern/Spy/APIClient_Spy.swift, TestTools/StreamChatTestTools/SpyPattern/Spy/CDNClient_Spy.swift, TestTools/StreamChatTestTools/SpyPattern/Spy/AttachmentUploader_Spy.swift, TestTools/StreamChatTestTools/TestData/CustomCDNClient.swift, TestTools/StreamChatTestTools/TestData/TestBuilder.swift
Renamed CDNClient_Spy to CDNStorage_Spy and updated method signatures; removed AttachmentUploader_Spy; updated APIClient_Spy to accept cdnStorage instead of separate dependencies; renamed CustomCDNClient to CustomCDNStorage.
Test Snapshot/Unit Updates
Tests/StreamChatUITests/Mocks/ImageLoader_Mock.swift, Tests/StreamChatUITests/Mocks/Utils/VideoLoader_Mock.swift, Tests/StreamChatUITests/SnapshotTests/..., Tests/StreamChatTests/APIClient/APIClient_Tests.swift, Tests/StreamChatTests/APIClient/StreamCDNClient_Tests.swift, Tests/StreamChatCommonUITests/ImageLoading/StreamImageLoader_Tests.swift, Tests/StreamChatCommonUITests/ImageLoading/StreamVideoLoader_Tests.swift, Tests/StreamChatTests/APIClient/CDNClient/StreamCDN_Tests.swift, Tests/StreamChatTests/Workers/MessageUpdater_Tests.swift
Updated mocks to conform to new loader protocols; removed legacy loader tests; added new StreamImageLoader and StreamVideoLoader tests; added StreamCDNRequester_Tests; updated snapshot tests to use mock components; added download attachment tests with remoteURL parameter.
Project Configuration
StreamChat.xcodeproj/project.pbxproj, CHANGELOG.md
Updated Xcode project file references to add new image/video loader files and remove legacy files; documented new downloadAttachment remoteURL parameter in changelog.

Sequence Diagram

sequenceDiagram
    participant App as Application
    participant ImageLoader as StreamImageLoader
    participant CDNReq as CDNRequester
    participant ImgDL as ImageDownloading
    participant Pipeline as Nuke Pipeline
    participant Cache as Image Cache

    App->>ImageLoader: loadImage(url, resize)
    ImageLoader->>CDNReq: imageRequest(for:options:)
    CDNReq-->>ImageLoader: CDNRequest(url, headers, cachingKey)
    ImageLoader->>ImgDL: downloadImage(url, headers, cachingKey)
    ImgDL->>Pipeline: loadImage(with: ImageRequest)
    Pipeline->>Cache: check cachingKey
    alt Cache hit
        Cache-->>Pipeline: UIImage
    else Cache miss
        Pipeline->>Pipeline: download & process
        Pipeline->>Cache: store UIImage
    end
    Pipeline-->>ImgDL: Result<UIImage, Error>
    ImgDL-->>ImageLoader: completion(Result)
    ImageLoader-->>App: completion(Result)
Loading
sequenceDiagram
    participant App as Application
    participant VideoLoader as StreamVideoLoader
    participant CDNReq as CDNRequester
    participant ImgLoader as ImageLoader
    participant Cache as Video Preview Cache
    participant AVAsset as AVAssetImageGenerator

    App->>VideoLoader: loadPreview(with: attachment)
    alt Has thumbnailURL
        VideoLoader->>ImgLoader: loadImage(thumbnailURL)
        ImgLoader-->>VideoLoader: Result<UIImage, Error>
        alt Success
            VideoLoader->>Cache: store image
            VideoLoader-->>App: completion(success)
        else Failure
            Note over VideoLoader: Fall through to generation
        end
    end
    VideoLoader->>CDNReq: fileRequest(for: videoURL)
    CDNReq-->>VideoLoader: CDNRequest(signedURL)
    VideoLoader->>AVAsset: extract frame at 0.1s
    AVAsset-->>VideoLoader: CGImage
    VideoLoader->>Cache: store preview
    VideoLoader-->>App: completion(success)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Added a new upload endpoint #3788: Adds a standalone attachment upload API to AttachmentUploader/CDNClient prior to their removal; this PR replaces those protocols with unified CDNStorage and changes upload/delete method signatures, so the two PRs are directly related in their refactoring of the upload architecture.

Suggested reviewers

  • laevandus

Poem

🐰 A rabbit hops through streams of code,
Where CDN paths transform and flow—
Old uploaders rest, new storage grows,
Image loaders fetch with headers and glow,
Signatures dance on URLs below! 📸✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Unified CDN & Media Loading Architecture (v5)' directly reflects the main architectural changes: consolidating CDN and media loading into unified protocols shared across SDKs with async support.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch add/improve-custom-cdn

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 8, 2026

1 Warning
⚠️ Big PR

Generated by 🚫 Danger

Comment thread Sources/StreamChat/APIClient/CDNClient/CDNClient.swift Outdated
Comment thread Sources/StreamChatCommonUI/ImageLoading/StreamVideoLoader.swift Outdated
Comment thread Sources/StreamChatUI/Utils/ImageLoading/NukeImageDownloader.swift Outdated
Copy link
Copy Markdown
Contributor

@martinmitrevski martinmitrevski left a comment

Choose a reason for hiding this comment

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

Looks good! We should make sure we test this extensively, since it's an important part of the SDK.

Comment thread Sources/StreamChat/APIClient/CDNClient/CDNRequest.swift Outdated
Comment thread Sources/StreamChatUI/Utils/ImageLoading/ImageLoader+UIKit.swift Outdated
Comment thread Sources/StreamChatUI/Utils/ImageLoading/NukeImageDownloader.swift Outdated
Comment thread Sources/StreamChatUI/Utils/ImageLoading/NukeImageDownloader.swift Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
Sources/StreamChatCommonUI/ImageLoading/StreamVideoLoader.swift (2)

115-123: ⚠️ Potential issue | 🟠 Major

The nil-image / nil-error branch still exits before completion.

Line 121 builds a failure result, but Line 122 returns before the common completion block runs. On that edge case, callers wait forever.

🛠️ Minimal fix
                 } else {
                     result = .failure(ClientError.Unknown("Both error and image are nil"))
-                    return
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/StreamChatCommonUI/ImageLoading/StreamVideoLoader.swift` around lines
115 - 123, The code builds a failure Result on the nil-image/nil-error path but
returns early before calling the shared completion; remove the early return and
ensure the common completion handler (completion) is invoked with the
constructed result variable (result) so that callers are always called even when
both image and error are nil (keep using ClientError.Unknown(...) for the
failure case and UIImage(cgImage:) for the success path).

96-107: ⚠️ Potential issue | 🟠 Major

Still dropping CDNRequest.headers when building the preview asset.

Line 98 resolves the CDN request, but Line 107 only uses cdnRequest.url. Any requester that authenticates video access via headers will still fail here even after fileRequest succeeds; the asset needs to be created from the full CDNRequest, not just the rewritten URL.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/StreamChatCommonUI/ImageLoading/StreamVideoLoader.swift` around lines
96 - 107, The code drops CDNRequest.headers by only using cdnRequest.url when
creating the AVURLAsset; update the success branch that currently assigns
adjustedUrl and constructs AVURLAsset(url: adjustedUrl) in StreamVideoLoader to
create the asset from the full CDNRequest (e.g. use AVURLAsset(url:
cdnRequest.url, options: ["AVURLAssetHTTPHeaderFieldsKey": cdnRequest.headers]))
so any header-based auth is preserved, and continue to call
completion(.failure(error)) on the main queue in the failure branch as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/StreamChatCommonUI/ImageLoading/StreamVideoLoader.swift`:
- Around line 39-40: The synchronous videoAsset(at:) currently constructs
AVURLAsset(url:) directly and cannot await or surface failures from
cdnRequester.fileRequest(...); change the API to be asynchronous (e.g.,
videoAsset(at: URL, completion: (Result<AVURLAsset, Error>) -> Void) or async
throws returning AVURLAsset) so the loader can call
cdnRequester.fileRequest(...), wait for signed URL/headers, handle errors, and
only then create and return the AVURLAsset; update all callers of
videoAsset(at:) to use the new async/completion-based contract (or provide a new
async method and deprecate the sync one) and ensure errors from
cdnRequester.fileRequest(...) are propagated to the caller.

---

Duplicate comments:
In `@Sources/StreamChatCommonUI/ImageLoading/StreamVideoLoader.swift`:
- Around line 115-123: The code builds a failure Result on the
nil-image/nil-error path but returns early before calling the shared completion;
remove the early return and ensure the common completion handler (completion) is
invoked with the constructed result variable (result) so that callers are always
called even when both image and error are nil (keep using
ClientError.Unknown(...) for the failure case and UIImage(cgImage:) for the
success path).
- Around line 96-107: The code drops CDNRequest.headers by only using
cdnRequest.url when creating the AVURLAsset; update the success branch that
currently assigns adjustedUrl and constructs AVURLAsset(url: adjustedUrl) in
StreamVideoLoader to create the asset from the full CDNRequest (e.g. use
AVURLAsset(url: cdnRequest.url, options: ["AVURLAssetHTTPHeaderFieldsKey":
cdnRequest.headers])) so any header-based auth is preserved, and continue to
call completion(.failure(error)) on the main queue in the failure branch as
before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 09f3a8b2-1cba-499d-bf3a-0b74a381efb1

📥 Commits

Reviewing files that changed from the base of the PR and between 0ad546d and 0187194.

📒 Files selected for processing (1)
  • Sources/StreamChatCommonUI/ImageLoading/StreamVideoLoader.swift

Comment thread Sources/StreamChatCommonUI/ImageLoading/StreamVideoLoader.swift Outdated
Allow callers to provide a CDN-signed or transformed URL
for attachment downloads. This enables UI layers (SwiftUI/UIKit)
that configure a CDNRequester to resolve the URL before
passing it to the download flow.
@Stream-SDK-Bot
Copy link
Copy Markdown
Collaborator

SDK Size

title develop branch diff status
StreamChat 6.74 MB 6.76 MB +19 KB 🟢
StreamChatUI 4.29 MB 4.26 MB -22 KB 🚀
StreamChatCommonUI 0.75 MB 0.78 MB +27 KB 🟢

@Stream-SDK-Bot
Copy link
Copy Markdown
Collaborator

StreamChat XCSize

Object Diff (bytes)
StreamCDNRequester.o +60040
ChannelListPayload.o -26482
AnyAttachmentPayload.o -16165
MessageTranslationsPayload.o -10920
AttachmentTypes.o +10543
Show 40 more objects
Object Diff (bytes)
CDNRequester.o +6542
ChatMessageAudioAttachment.o -5909
CDNStorage.o +5444
CDNClient.o -3639
AttachmentUploader.o -3107
RequestEncoder.o -1528
APIClient.o +1238
CDNImageResize.o +1112
ChatClient.o -1062
AppSettings.o -396
Array+Sampling.o -224
MessageDTO.o -198
MessageUpdater.o +190
ChannelMemberUpdater.o -156
UserPayloads.o -140
MessagePayloads.o -128
ConnectionRepository.o +104
AttachmentDownloader.o +104
ChannelListUpdater.o -100
MessageController.o -96
ChannelController+Combine.o -88
IdentifiablePayload.o -88
ChatClientConfig.o -86
UploadedAttachment.o +84
ReadStateHandler.o -80
MessageDeliveryCriteriaValidator.o +72
Token.o -72
ChannelUpdater.o -68
AudioSessionConfiguring.o +64
URLRequest+cURL.o +60
Foundation.tbd +60
PushPreferencePayloads.o -58
TextLinkDetector.o -52
UnknownChannelEvent.o +52
AppSettingsPayload.o -52
ChatClient+Environment.o -52
UserSearchController.o -48
ChatState.o +48
Chat.o +44
PollVoteListQuery.o -44

@Stream-SDK-Bot
Copy link
Copy Markdown
Collaborator

StreamChatUI XCSize

Object Diff (bytes)
NukeImageLoader.o -11152
ImageViewExtensions.o -8092
ImageLoader+UIKit.o +7750
StreamCDN.o -5200
VideoLoading.o -5200
Show 25 more objects
Object Diff (bytes)
ImageResize.o -3918
StreamImageDownloader.o +3230
StreamImageProcessor.o +1842
NukeImageProcessor.o -1838
ImageLoading.o -1773
ChatChannelHeaderView.o -757
ChatChannelListItemView.o +316
FileAttachmentViewInjector.o -264
UIImageView+SwiftyGif.o +248
Foundation.tbd -244
ImageLoadingOptions.o -232
ChatMessageListVC.o +172
ChatChannelVC.o -154
VideoAttachmentGalleryCell.o +140
QuotedChatMessageView.o +128
StreamChatCommonUI.tbd +128
Components.o +116
VideoAttachmentComposerPreview.o +116
ImageCDN.o -104
VideoLoader+UIKit.o +92
ComposerVC.o -82
VideoAttachmentGalleryPreview.o +72
InputTextView.o +72
ChatMessageContentView.o +72
ChatChannelAvatarView.o +56

@Stream-SDK-Bot
Copy link
Copy Markdown
Collaborator

StreamChatCommonUI XCSize

Object Diff (bytes)
StreamVideoLoader.o +8311
StreamImageLoader.o +7419
ImageResize.o +4872
ImageLoader.o +4052
Appearance+Images.o +656
Show 8 more objects
Object Diff (bytes)
VideoLoader.o +279
ImageRequestOptions+ImageResize.o +236
AudioPlaybackRateFormatter.o +189
ChatChannelNamer.o -151
StreamChat.tbd +148
ImageDownloading.o +120
Foundation.tbd +68
libdispatch.dylib +60

Moves CDN configuration to be passed per-call on ImageLoader and
VideoLoader protocols, eliminating staleness when the CDNRequester
is changed at runtime. ChatClient now owns cdnRequester and
cdnStorage properties, and MessageController.downloadAttachment()
uses the client's CDNRequester internally for URL transformation.
Group additional parameters into options structs (ImageLoadOptions,
ImageBatchLoadOptions, VideoLoadOptions) so future parameters can be
added without breaking the protocol signature. UIKit's
ImageLoaderOptions and ImageDownloadOptions now include cdnRequester.
Eliminates the stale-reference problem where replacing imageLoader
didn't affect videoLoader (which held its own reference to the old one).
Customers now set a single mediaLoader on Components/Utils.
Renames loadPreview to loadVideoPreview for clarity.
- Introduce MediaLoaderImage, MediaLoaderVideoAsset, MediaLoaderVideoPreview
  wrapper types for all MediaLoader return values
- Make videoAsset() async with a completion handler to support CDN URL
  transformation before creating the AVURLAsset
- Update StreamMediaLoader, UIKit call sites, and test mocks
CDN configuration is now only available through ChatClientConfig.
Access via config.cdnRequester / config.cdnStorage instead.
Copy link
Copy Markdown
Contributor

@martinmitrevski martinmitrevski left a comment

Choose a reason for hiding this comment

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

LGTM! ✅ Left few small things, only bigger one I would consider is using final classes instead of structs in the options / requests. Size has grown a bit, which I think we can reduce.

}

/// Options for an image request through the CDN.
public struct ImageRequestOptions: Sendable {
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.

maybe we should use final class instead? Here and multiple other places.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah I will try to make them class to see if there is any improvement. But I don't think so because the properties are pretty much raw types, but lets see

Comment thread Sources/StreamChatCommonUI/ImageLoading/StreamMediaLoader.swift Outdated
Comment thread Sources/StreamChatUI/Components.swift Outdated
- Add ImageDownloadingOptions and DownloadedImage types to ImageDownloading
- Reorder CDNRequester, CDNStorage, and MediaLoader files: protocol at top, types at bottom
- Update StreamMediaLoader and StreamImageDownloader implementations
- Update test mocks
buildCachingKey was called with the original URL, so different resize
variants of the same image shared the same cache key. Now it uses the
final URL produced by buildImageURL, which includes the resize query
parameters.

Also fix test target membership for StreamCDN_Tests and add tests
covering the caching key correctness for resized images.
…oAsset

Pass CDN headers to AVURLAsset in StreamMediaLoader.loadVideoAsset()
so authenticated video playback works for custom CDN customers.
Rename videoAsset() to loadVideoAsset() for consistency with other
MediaLoader methods.
Capture `downloader` strongly before entering the CDN request callback
so `completion` is always called even if the loader is deallocated.
@github-actions
Copy link
Copy Markdown

Public Interface

+ public struct MediaLoaderVideoPreview: Sendable  
+ 
+   public var image: UIImage
+   
+ 
+   public init(image: UIImage)

+ extension MediaLoader  
+ 
+   @discardableResult @MainActor public func loadImage(into imageView: UIImageView,from url: URL?,with options: ImageLoaderOptions,completion: (@MainActor (Result<UIImage, Error>) -> Void)? = nil)-> ImageLoadingTask
+   public func downloadImage(with request: ImageDownloadRequest,completion: @escaping @MainActor (Result<UIImage, Error>) -> Void)
+   public func downloadMultipleImages(with requests: [ImageDownloadRequest],completion: @escaping @MainActor ([Result<UIImage, Error>]) -> Void)
+   @discardableResult @MainActor public func loadImage(into imageView: UIImageView,from attachmentPayload: ImageAttachmentPayload?,maxResolutionInPixels: Double,cdnRequester: CDNRequester,completion: (@MainActor (Result<UIImage, Error>) -> Void)? = nil)-> ImageLoadingTask

+ public struct DownloadedImage: Sendable  
+ 
+   public var image: UIImage
+   
+ 
+   public init(image: UIImage)

+ public protocol MediaLoader: AnyObject, Sendable

+ public protocol CDNRequester: Sendable

+ public struct MediaLoaderImage: Sendable  
+ 
+   public var image: UIImage
+   
+ 
+   public init(image: UIImage)

+ public struct ImageDownloadingOptions: Sendable  
+ 
+   public var headers: [String: String]?
+   public var cachingKey: String?
+   public var resize: CGSize?
+   
+ 
+   public init(headers: [String: String]? = nil,cachingKey: String? = nil,resize: CGSize? = nil)

+ extension CDNStorage  
+ 
+   public func uploadAttachment(_ attachment: AnyChatMessageAttachment,options: AttachmentUploadOptions = .init())async throws -> UploadedFile
+   public func uploadAttachment(localUrl: URL,options: AttachmentUploadOptions = .init())async throws -> UploadedFile
+   public func deleteAttachment(remoteUrl: URL,options: AttachmentDeleteOptions = .init())async throws

+ public struct AttachmentDeleteOptions: Sendable  
+ 
+   public init()

+ public struct AttachmentUploadOptions: Sendable  
+ 
+   public var progress: (@Sendable (Double) -> Void)?
+   
+ 
+   public init(progress: (@Sendable (Double) -> Void)? = nil)

+ public class ImageLoadingTask: Cancellable, @unchecked Sendable  
+ 
+   public private var isCancelled
+   
+ 
+   public func cancel()

+ public struct CDNRequest: Sendable  
+ 
+   public var url: URL
+   public var headers: [String: String]?
+   public var cachingKey: String?
+   
+ 
+   public init(url: URL,headers: [String: String]? = nil,cachingKey: String? = nil)

+ open class StreamImageProcessor: ImageProcessor, @unchecked Sendable  
+ 
+   open func crop(image: UIImage,to size: CGSize)-> UIImage?
+   open func scale(image: UIImage,to size: CGSize)-> UIImage

+ public protocol ImageDownloading: Sendable

+ public struct ImageLoadOptions: Sendable  
+ 
+   public var resize: ImageResize?
+   public var cdnRequester: CDNRequester
+   
+ 
+   public init(resize: ImageResize? = nil,cdnRequester: CDNRequester)

+ public struct CDNImageResize: Sendable  
+ 
+   public var width: CGFloat
+   public var height: CGFloat
+   public var resizeMode: String
+   public var crop: String?
+   
+ 
+   public init(width: CGFloat,height: CGFloat,resizeMode: String,crop: String? = nil)

+ extension CDNRequester  
+ 
+   public func imageRequest(for url: URL,options: ImageRequestOptions = .init())async throws -> CDNRequest
+   public func fileRequest(for url: URL,options: FileRequestOptions = .init())async throws -> CDNRequest

+ open class StreamMediaLoader: MediaLoader, @unchecked Sendable  
+ 
+   public let downloader: ImageDownloading
+   
+ 
+   public init(downloader: ImageDownloading,videoPreviewCacheCountLimit: Int = 50)
+   
+ 
+   open func loadImage(url: URL?,options: ImageLoadOptions,completion: @escaping @MainActor (Result<MediaLoaderImage, Error>) -> Void)
+   open func loadImages(from urls: [URL],options: ImageBatchLoadOptions,completion: @escaping @MainActor ([MediaLoaderImage]) -> Void)
+   open func loadVideoAsset(at url: URL,options: VideoLoadOptions,completion: @escaping @MainActor (Result<MediaLoaderVideoAsset, Error>) -> Void)
+   open func loadVideoPreview(at url: URL,options: VideoLoadOptions,completion: @escaping @MainActor (Result<MediaLoaderVideoPreview, Error>) -> Void)
+   open func loadVideoPreview(with attachment: ChatMessageVideoAttachment,options: VideoLoadOptions,completion: @escaping @MainActor (Result<MediaLoaderVideoPreview, Error>) -> Void)

+ public struct ImageRequestOptions: Sendable  
+ 
+   public var resize: CDNImageResize?
+   
+ 
+   public init(resize: CDNImageResize? = nil)

+ public struct VideoLoadOptions: Sendable  
+ 
+   public var cdnRequester: CDNRequester
+   
+ 
+   public init(cdnRequester: CDNRequester)

+ public protocol CDNStorage: Sendable

+ public struct ImageBatchLoadOptions: Sendable  
+ 
+   public var placeholders: [UIImage]
+   public var loadThumbnails: Bool
+   public var thumbnailSize: CGSize
+   public var cdnRequester: CDNRequester
+   
+ 
+   public init(placeholders: [UIImage] = [],loadThumbnails: Bool = true,thumbnailSize: CGSize = CGSize(width: 40, height: 40),cdnRequester: CDNRequester)

+ public final class StreamCDNRequester: CDNRequester, Sendable  
+ 
+   public let cdnHost: String
+   
+ 
+   public init(cdnHost: String = "stream-io-cdn.com")
+   
+ 
+   public func imageRequest(for url: URL,options: ImageRequestOptions,completion: @escaping (Result<CDNRequest, Error>) -> Void)
+   public func fileRequest(for url: URL,options: FileRequestOptions,completion: @escaping (Result<CDNRequest, Error>) -> Void)

+ public struct FileRequestOptions: Sendable  
+ 
+   public init()

+ public final class StreamImageDownloader: ImageDownloading, Sendable  
+ 
+   public init()
+   
+ 
+   public func downloadImage(url: URL,options: ImageDownloadingOptions,completion: @escaping @MainActor (Result<DownloadedImage, Error>) -> Void)

+ public struct MediaLoaderVideoAsset: Sendable  
+ 
+   public var asset: AVURLAsset
+   
+ 
+   public init(asset: AVURLAsset)

- public protocol VideoLoading: AnyObject

- public protocol ImageCDN: Sendable

- @MainActor public protocol ImageLoading: AnyObject

- public extension ImageLoading

- public protocol AttachmentUploader: Sendable

- public extension VideoLoading

- open class NukeImageProcessor: ImageProcessor, @unchecked Sendable  
- 
-   open func crop(image: UIImage,to size: CGSize)-> UIImage?
-   open func scale(image: UIImage,to size: CGSize)-> UIImage

- public protocol CDNClient: Sendable

- open class NukeImageLoader: ImageLoading  
- 
-   open var avatarThumbnailSize: CGSize
-   open var imageCDN: ImageCDN
-   
- 
-   public init()
-   
- 
-   @discardableResult @MainActor open func loadImage(into imageView: UIImageView,from url: URL?,with options: ImageLoaderOptions,completion: (@MainActor (Result<UIImage, Error>) -> Void)?)-> Cancellable?
-   @discardableResult open func downloadImage(with request: ImageDownloadRequest,completion: @escaping @MainActor (Result<UIImage, Error>) -> Void)-> Cancellable?
-   open func downloadMultipleImages(with requests: [ImageDownloadRequest],completion: @escaping @MainActor ([Result<UIImage, Error>]) -> Void)

- open class StreamImageCDN: ImageCDN, @unchecked Sendable  
- 
-   public nonisolated static var streamCDNURL
-   
- 
-   public init()
-   
- 
-   open func urlRequest(forImageUrl url: URL,resize: ImageResize?)-> URLRequest
-   open func cachingKey(forImageUrl url: URL)-> String

- public class StreamAttachmentUploader: AttachmentUploader, @unchecked Sendable  
- 
-   public func upload(_ attachment: AnyChatMessageAttachment,progress: (@Sendable (Double) -> Void)?,completion: @escaping @Sendable (Result<UploadedAttachment, Error>) -> Void)
-   public func uploadStandaloneAttachment(_ attachment: StreamAttachment<Payload>,progress: (@Sendable (Double) -> Void)?,completion: @escaping @Sendable (Result<UploadedFile, Error>) -> Void)

- open class StreamVideoLoader: VideoLoading, @unchecked Sendable  
- 
-   public init(cachedVideoPreviewsCountLimit: Int = 50)
-   
- 
-   open func loadPreviewForVideo(at url: URL,completion: @escaping @MainActor (Result<UIImage, Error>) -> Void)
-   open func videoAsset(at url: URL)-> AVURLAsset
-   @objc open func handleMemoryWarning(_ notification: NSNotification)

- public extension CDNClient

 public class ChatClient: @unchecked Sendable  
-   public func upload(_ attachment: StreamAttachment<Payload>,progress: (@Sendable (Double) -> Void)?,completion: @escaping @Sendable (Result<UploadedFile, Error>) -> Void)
+   public func uploadAttachment(localUrl: URL,progress: (@Sendable (Double) -> Void)?,completion: @escaping @Sendable (Result<UploadedFile, Error>) -> Void)
-   public func uploadAttachment(localUrl: URL,progress: (@Sendable (Double) -> Void)?,completion: @escaping @Sendable (Result<UploadedFile, Error>) -> Void)
+   public func deleteAttachment(remoteUrl: URL,completion: @escaping @Sendable (Error?) -> Void)
-   public func deleteAttachment(remoteUrl: URL,completion: @escaping @Sendable (Error?) -> Void)

 public struct ChatClientConfig: Sendable  
-   public var customCDNClient: CDNClient?
+   public var cdnRequester: CDNRequester
-   public var customAttachmentUploader: AttachmentUploader?
+   public var cdnStorage: CDNStorage?

 @MainActor public struct Components  
-   public var imageCDN: ImageCDN
+   public var cdnRequester: CDNRequester
-   public var imageLoader: ImageLoading
+   public var mediaLoader: MediaLoader
-   public var videoLoader: VideoLoading
+   public var maxAttachmentSize: Int64

 public struct ImageLoaderOptions: Sendable  
-   
+   public var cdnRequester: CDNRequester
- 
+   
-   public init(resize: ImageResize? = nil,placeholder: UIImage? = nil)
+ 
+   public init(resize: ImageResize? = nil,placeholder: UIImage? = nil,cdnRequester: CDNRequester)

- public struct UploadedFile: Decodable  
+ public struct UploadedFile: Sendable, Decodable  

- public struct ImageDownloadOptions  
+ public struct ImageDownloadOptions: Sendable  
-   
+   public var cdnRequester: CDNRequester
- 
+   
-   public init(resize: ImageResize? = nil)
+ 
+   public init(resize: ImageResize? = nil,cdnRequester: CDNRequester)

@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
65.7% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

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.

4 participants