Unified CDN & Media Loading Architecture (v5)#4056
Unified CDN & Media Loading Architecture (v5)#4056nuno-vieira wants to merge 46 commits intodevelopfrom
Conversation
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
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR refactors attachment and media loading infrastructure by replacing the Changes
Sequence DiagramsequenceDiagram
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)
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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Generated by 🚫 Danger |
martinmitrevski
left a comment
There was a problem hiding this comment.
Looks good! We should make sure we test this extensively, since it's an important part of the SDK.
Rename to StreamImageDownloader and StreamImageProcessor to decouple public API naming from the Nuke dependency.
# Conflicts: # Sources/StreamChatUI/Components.swift # Sources/StreamChatUI/Utils/ImageLoading/ImageLoading.swift # Tests/StreamChatUITests/Utils/NukeImageLoader_Tests.swift # Tests/StreamChatUITests/Utils/StreamImageCDN_Tests.swift
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
Sources/StreamChatCommonUI/ImageLoading/StreamVideoLoader.swift (2)
115-123:⚠️ Potential issue | 🟠 MajorThe 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 | 🟠 MajorStill dropping
CDNRequest.headerswhen 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 afterfileRequestsucceeds; the asset needs to be created from the fullCDNRequest, 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
📒 Files selected for processing (1)
Sources/StreamChatCommonUI/ImageLoading/StreamVideoLoader.swift
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.
SDK Size
|
StreamChat XCSize
Show 40 more objects
|
StreamChatUI XCSize
Show 25 more objects
|
StreamChatCommonUI XCSize
Show 8 more objects
|
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.
martinmitrevski
left a comment
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
maybe we should use final class instead? Here and multiple other places.
There was a problem hiding this comment.
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
- 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.
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) |
|


🔗 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
CDNRequesterprotocol for read-only URL transformation (signing, headers, resize) with async completion handler supportCDNStorageprotocol replacing bothCDNClientandAttachmentUploaderfor upload/delete operationsCDNRequestvalue type carrying URL, headers, and caching keyCDNImageResizemodel for server-side image resize parametersStreamCDNRequesterdefault implementation with Stream CDN resize/cache logicStreamCDNStoragedefault implementation using Stream's REST API for multipart uploadsImageResizefrom StreamChatUI to StreamChatCommonUIMediaLoaderprotocol in StreamChatCommonUI replacing separateImageLoaderandVideoLoaderStreamMediaLoaderdefault implementation handling both image loading and video preview generationImageDownloadingprotocol so CommonUI never depends on Nuke directlyStreamImageDownloaderas the UIKitImageDownloadingbackendImageLoadOptions,ImageBatchLoadOptions,VideoLoadOptions) for extensible method signaturescdnRequesterandcdnStorageas public mutable properties onChatClientChatMessageController.downloadAttachment()to useChatClient.cdnRequesterinternally for URL transformationImageCDN,StreamImageCDN,ImageLoading,NukeImageLoader,VideoLoading,CDNClient,AttachmentUploaderComponentsto usemediaLoader: MediaLoader(replacing separateimageLoader+videoLoader)ImageLoaderOptionsandImageDownloadOptionsto includecdnRequesterin the options structChatClientConfigwithcdnRequesterandcdnStoragereplacingcustomCDNClientandcustomAttachmentUploader🛠 Implementation
The architecture splits CDN concerns into read and write paths with unified protocols replacing scattered types across both repos:
CDNRequesterCDNStorageMediaLoaderImageDownloadingCDNRequester vs CDNStorage:
CDNRequesterhandles the read path — transforming URLs before GET requests (adding resize query params, signing, injecting headers, computing cache keys). The defaultStreamCDNRequesteradds width/height/resize query params forstream-io-cdn.comURLs and builds stable caching keys.CDNStoragehandles the write path — uploading attachments (multipart, with progress) and deleting them by remote URL. The defaultStreamCDNStorageuses Stream's REST API. Configured viaChatClientConfig.cdnStorage(optional; nil uses the default).cdnRequesterandcdnStorageare public mutable properties onChatClient, initialized fromChatClientConfigbut changeable at runtime.Unified MediaLoader:
MediaLoadermerges the previousImageLoaderandVideoLoaderinto a single protocol. This eliminates a stale-reference problem: previously,StreamVideoLoaderheld a reference toImageLoaderat init time, so replacingcomponents.imageLoaderwouldn't affect video thumbnail loading. Now there's onemediaLoaderproperty inComponents(UIKit) andUtils(SwiftUI).The protocol uses an Options pattern —
ImageLoadOptions,ImageBatchLoadOptions, andVideoLoadOptionsstruct parameters — so theCDNRequesteris passed on every call rather than captured at init. This means changingchatClient.cdnRequesterat 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.StreamMediaLoaderhandles both image loading (delegates toImageDownloadingbackend) and video preview generation (AVFoundation). Video thumbnails are cached in anNSCache.Attachment downloads:
ChatMessageController.downloadAttachment()andChat.downloadAttachment()no longer expose aremoteURLparameter. Instead, they internally useChatClient.cdnRequester.fileRequest()to transform the attachment URL before passing it to the download worker.🧪 Manual Testing Notes
☑️ Contributor Checklist
docs-contentrepo