Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
aeb64e3
Add CDN, CDNUploader protocols and replace CDNClient/AttachmentUploader
nuno-vieira Apr 8, 2026
6834ee5
Add ImageLoader, VideoLoader, and ImageDownloading protocols in Strea…
nuno-vieira Apr 8, 2026
5657a91
Replace old ImageCDN/ImageLoading/VideoLoading in StreamChatUI
nuno-vieira Apr 8, 2026
e6586f4
Update test infrastructure for new CDN/loader protocols
nuno-vieira Apr 8, 2026
c306d8b
Rename NukeImageDownloader and NukeImageProcessor to Stream prefix
nuno-vieira Apr 13, 2026
926a655
Rename CDNUploader to CDNStorage
nuno-vieira Apr 14, 2026
6ee5c32
Remove maxAttachmentSize from CDNStorage protocol
nuno-vieira Apr 14, 2026
ea6adf9
Rename CDN to CDNRequester
nuno-vieira Apr 14, 2026
65212f7
Introduce options parameters to the CDNStorage and CDNRequester
nuno-vieira Apr 14, 2026
d4bcbbd
Move CDNRequest object to CDNRequester
nuno-vieira Apr 14, 2026
5ef94cf
PR Feedback
nuno-vieira Apr 14, 2026
893ddca
Merge branch 'develop' into add/improve-custom-cdn
nuno-vieira Apr 14, 2026
e587f1d
Fix import issues
nuno-vieira Apr 14, 2026
76d1f77
Fix tests not compiling
nuno-vieira Apr 14, 2026
8670250
Fix Xcode project
nuno-vieira Apr 14, 2026
7d70b7a
Fix targets of loaders in common module
nuno-vieira Apr 14, 2026
7024b1c
Missing StreamChat import
nuno-vieira Apr 14, 2026
62ceeac
Move ImageResize to common module
nuno-vieira Apr 14, 2026
f6efc35
Fix compiling mock loaders
nuno-vieira Apr 14, 2026
8bfa60c
Merge branch 'develop' into add/improve-custom-cdn
nuno-vieira Apr 14, 2026
b0a30c2
Fix snapshot tests
nuno-vieira Apr 14, 2026
499d81d
Re-record snapshots
nuno-vieira Apr 14, 2026
70e066d
Fix StreamChatUI tests
nuno-vieira Apr 14, 2026
0ad546d
Fix swiftformat
nuno-vieira Apr 14, 2026
0187194
Fix sendable warning in StreamVideoLoader
nuno-vieira Apr 14, 2026
fbb7826
Add remoteURL parameter to downloadAttachment
nuno-vieira Apr 14, 2026
113ad3b
Add tests for downloadAttachment remoteURL parameter
nuno-vieira Apr 15, 2026
8b83aa8
Update changelog
nuno-vieira Apr 15, 2026
c60a3e9
Pass CDNRequester on every loader call instead of storing it
nuno-vieira Apr 15, 2026
f1ec6cb
Introduce Options pattern for ImageLoader and VideoLoader protocols
nuno-vieira Apr 15, 2026
549ec4e
Merge ImageLoader and VideoLoader into unified MediaLoader protocol
nuno-vieira Apr 15, 2026
49ce1cd
Add wrapper result types and make videoAsset async
nuno-vieira Apr 15, 2026
7e412fc
Remove cdnRequester and cdnStorage from ChatClient
nuno-vieira Apr 15, 2026
d0aa17a
Refine ImageDownloading with Options pattern and DownloadedImage output
nuno-vieira Apr 15, 2026
e288332
Fix caching key using original URL instead of final resized URL
nuno-vieira Apr 15, 2026
5830e77
Unify video loading via MediaLoader and rename videoAsset to loadVide…
nuno-vieira Apr 15, 2026
8e78882
Merge branch 'develop' into add/improve-custom-cdn
nuno-vieira Apr 15, 2026
c84b40e
Make UploadedFile sendable
nuno-vieira Apr 15, 2026
197db4f
Make StreamCDNRequester Sendable
nuno-vieira Apr 15, 2026
1594527
Fix silent no-op when StreamMediaLoader is deallocated during image load
nuno-vieira Apr 15, 2026
8308a3d
Make StreamImageDownloader final Sendable
nuno-vieira Apr 15, 2026
c0b0d64
Fix loadImages results order
nuno-vieira Apr 15, 2026
bfadff8
Move COmponents initializer
nuno-vieira Apr 15, 2026
8b65898
Improve test coverage
nuno-vieira Apr 15, 2026
71d5083
Remove loadImages from MediaLoader, since it is not used
nuno-vieira Apr 15, 2026
82db0b3
Fix compilation
nuno-vieira Apr 15, 2026
383b3d3
Refactor avatar views to use the correct components
nuno-vieira Apr 15, 2026
a2d6ad9
Fix avatar tests
nuno-vieira Apr 15, 2026
108de47
Update CDNStorage test coverage
nuno-vieira Apr 15, 2026
d7c1019
Fix completion handler not always called in StreamMediaLoader
nuno-vieira Apr 15, 2026
0df573c
Fix completion handler not always called in StreamCDNStorage
nuno-vieira Apr 15, 2026
6d43eb6
Rename StreamCDN_Tests.swift to StreamCDNRequester_Tests.swift
nuno-vieira Apr 15, 2026
b2c26fd
Rename CustomCDNClient.swift and StreamCDNClient_Tests.swift
nuno-vieira Apr 15, 2026
f7f3874
Fix stale CDNClient references and misleading log strings
nuno-vieira Apr 15, 2026
2fca66f
Improve StreamCDNStorage test coverage
nuno-vieira Apr 15, 2026
fb7f51c
Remove URL-based loadVideoPreview, use attachment variant only
nuno-vieira Apr 15, 2026
9bd596c
Explain video preview local caching
nuno-vieira Apr 15, 2026
b53149a
Remove default implementation of loadVideoAsset from MediaLoader
nuno-vieira Apr 15, 2026
fdcbfa0
Update Sources/StreamChatCommonUI/ImageLoading/MediaLoader.swift
nuno-vieira Apr 15, 2026
0ae2e26
Move cdnRequester out of ChatClientConfig into UI layer
nuno-vieira Apr 15, 2026
df96c6b
Fix tests related to CDNRequester moving
nuno-vieira Apr 15, 2026
c5a0aea
Fix StreamCDNStorage tests
nuno-vieira Apr 16, 2026
9a09605
Fix wrong log message
nuno-vieira Apr 16, 2026
b6735e7
Introduce back the local only video preview
nuno-vieira Apr 16, 2026
9c5dccf
Fix message content view tests
nuno-vieira Apr 16, 2026
2bd362a
Fix video composer preview tests
nuno-vieira Apr 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
29 changes: 15 additions & 14 deletions Sources/StreamChat/APIClient/APIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,9 @@

/// The attachment downloader.
let attachmentDownloader: AttachmentDownloader

/// The attachment uploader.
let attachmentUploader: AttachmentUploader

/// The CDN Client to store and delete attachments.
let cdnClient: CDNClient
/// The CDN storage for uploading and deleting attachments.
let cdnStorage: CDNStorage

/// Queue in charge of handling incoming requests
private let operationQueue: OperationQueue = {
Expand Down Expand Up @@ -66,15 +63,13 @@
requestEncoder: RequestEncoder,
requestDecoder: RequestDecoder,
attachmentDownloader: AttachmentDownloader,
attachmentUploader: AttachmentUploader,
cdnClient: CDNClient
cdnStorage: CDNStorage
) {
encoder = requestEncoder
decoder = requestDecoder
session = URLSession(configuration: sessionConfiguration)
self.attachmentDownloader = attachmentDownloader
self.attachmentUploader = attachmentUploader
self.cdnClient = cdnClient
self.cdnStorage = cdnStorage
}

/// Performs a network request and retries in case of network failures
Expand Down Expand Up @@ -323,18 +318,24 @@
completion: @escaping @Sendable (Result<UploadedAttachment, Error>) -> Void
) {
let uploadOperation = AsyncOperation(maxRetries: maximumRequestRetries) { [weak self] operation, done in
self?.attachmentUploader.upload(attachment, progress: progress) { [weak self] result in
switch result {
self?.cdnStorage.uploadAttachment(attachment, options: .init(progress: progress)) { [weak self] result in
let mappedResult = result.map { file in
UploadedAttachment(
attachment: attachment,
remoteURL: file.fileURL,
thumbnailURL: file.thumbnailURL
)
}

Check warning on line 328 in Sources/StreamChat/APIClient/APIClient.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not nest more than 2 closure expressions.

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-chat-swift&issues=AZ2MCIr0dLTmnmPetufD&open=AZ2MCIr0dLTmnmPetufD&pullRequest=4056
switch mappedResult {
case let .failure(error) where self?.isConnectionError(error) == true:
// Do not retry unless its a connection problem and we still have retries left
if operation.canRetry {
done(.retry)
} else {
completion(result)
completion(mappedResult)
done(.continue)
}
case .success, .failure:
completion(result)
completion(mappedResult)
done(.continue)
}
}
Expand Down

This file was deleted.

127 changes: 28 additions & 99 deletions Sources/StreamChat/APIClient/CDNClient/CDNClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,78 +15,11 @@
}
}

/// The CDN client is responsible to upload files to a CDN.
public protocol CDNClient: Sendable {
static var maxAttachmentSize: Int64 { get }

/// Uploads attachment as a multipart/form-data and returns only the uploaded remote file.
/// - Parameters:
/// - attachment: An attachment to upload.
/// - progress: A closure that broadcasts upload progress.
/// - completion: Returns the uploaded file's information.
func uploadAttachment(
_ attachment: AnyChatMessageAttachment,
progress: (@Sendable (Double) -> Void)?,
completion: @escaping @Sendable (Result<URL, Error>) -> Void
)

/// Uploads attachment as a multipart/form-data and returns the uploaded remote file and its thumbnail.
/// - Parameters:
/// - attachment: An attachment to upload.
/// - progress: A closure that broadcasts upload progress.
/// - completion: Returns the uploaded file's information.
func uploadAttachment(
_ attachment: AnyChatMessageAttachment,
progress: (@Sendable (Double) -> Void)?,
completion: @escaping @Sendable (Result<UploadedFile, Error>) -> Void
)

/// Uploads standalone attachment as a multipart/form-data and returns the uploaded remote file and its thumbnail.
/// - Parameters:
/// - attachment: An attachment to upload.
/// - progress: A closure that broadcasts upload progress.
/// - completion: Returns the uploaded file's information.
func uploadStandaloneAttachment<Payload>(
_ attachment: StreamAttachment<Payload>,
progress: (@Sendable (Double) -> Void)?,
completion: @escaping @Sendable (Result<UploadedFile, Error>) -> Void
)

/// Deletes the attachment from the CDN, given the remote URL.
/// - Parameters:
/// - remoteUrl: The remote url of the attachment.
/// - completion: Returns an error in case the delete operation fails.
func deleteAttachment(
remoteUrl: URL,
completion: @escaping @Sendable (Error?) -> Void
)
}

public extension CDNClient {
func uploadAttachment(
_ attachment: AnyChatMessageAttachment,
progress: (@Sendable (Double) -> Void)?,
completion: @escaping @Sendable (Result<UploadedFile, Error>) -> Void
) {
uploadAttachment(attachment, progress: progress, completion: { (result: Result<URL, Error>) in
switch result {
case let .success(url):
completion(.success(UploadedFile(fileURL: url, thumbnailURL: nil)))
case let .failure(error):
completion(.failure(error))
}
})
}
}

/// Default implementation of CDNClient that uses Stream CDN
final class StreamCDNClient: CDNClient, @unchecked Sendable {
static var maxAttachmentSize: Int64 { 100 * 1024 * 1024 }

/// Default implementation of CDNStorage that uses Stream's API.
final class StreamCDNStorage: CDNStorage, @unchecked Sendable {
private let decoder: RequestDecoder
private let encoder: RequestEncoder
private let session: URLSession
/// Keeps track of uploading tasks progress
@Atomic private var taskProgressObservers: [Int: NSKeyValueObservation] = [:]

init(
Expand All @@ -101,64 +34,61 @@

func uploadAttachment(
_ attachment: AnyChatMessageAttachment,
progress: (@Sendable (Double) -> Void)? = nil,
completion: @escaping @Sendable (Result<URL, Error>) -> Void
) {
uploadAttachment(attachment, progress: progress, completion: { (result: Result<UploadedFile, Error>) in
switch result {
case let .success(file):
completion(.success(file.fileURL))
case let .failure(error):
completion(.failure(error))
}
})
}

func uploadAttachment(
_ attachment: AnyChatMessageAttachment,
progress: (@Sendable (Double) -> Void)? = nil,
options: AttachmentUploadOptions,
completion: @escaping @Sendable (Result<UploadedFile, Error>) -> Void
) {
guard
let uploadingState = attachment.uploadingState,
let fileData = try? Data(contentsOf: uploadingState.localFileURL) else {
let fileData = try? Data(contentsOf: uploadingState.localFileURL, options: .mappedIfSafe) else {
return completion(.failure(ClientError.AttachmentUploading(id: attachment.id)))
}
let endpoint = Endpoint<FileUploadPayload>.uploadAttachment(with: attachment.id.cid, type: attachment.type)

uploadAttachment(
endpoint: endpoint,
fileData: fileData,
uploadingState: uploadingState,
progress: progress,
progress: options.progress,
completion: completion
)
}
func uploadStandaloneAttachment<Payload>(
_ attachment: StreamAttachment<Payload>,
progress: (@Sendable (Double) -> Void)? = nil,

func uploadAttachment(
localUrl: URL,
options: AttachmentUploadOptions,
completion: @escaping @Sendable (Result<UploadedFile, Error>) -> Void
) {
guard
let uploadingState = attachment.uploadingState,
let fileData = try? Data(contentsOf: uploadingState.localFileURL) else {
let uploadingState: AttachmentUploadingState
do {
uploadingState = AttachmentUploadingState(
localFileURL: localUrl,
state: .pendingUpload,
file: try .init(url: localUrl)
)
} catch {
completion(.failure(error))
return
}

guard let fileData = try? Data(contentsOf: localUrl, options: .mappedIfSafe) else {
return completion(.failure(ClientError.Unknown()))
}

let endpoint = Endpoint<FileUploadPayload>.uploadAttachment(type: attachment.type)

let isImage = uploadingState.file.type.isImage
let endpoint = Endpoint<FileUploadPayload>.uploadAttachment(type: isImage ? .image : .file)

uploadAttachment(
endpoint: endpoint,
fileData: fileData,
uploadingState: uploadingState,
progress: progress,
progress: options.progress,
completion: completion
)
}

func deleteAttachment(
remoteUrl: URL,
options: AttachmentDeleteOptions,

Check warning on line 91 in Sources/StreamChat/APIClient/CDNClient/CDNClient.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused function parameter "options" or name it "_".

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-chat-swift&issues=AZ2MCIradLTmnmPetue_&open=AZ2MCIradLTmnmPetue_&pullRequest=4056
completion: @escaping @Sendable (Error?) -> Void
) {
let isImage = AttachmentFileType(ext: remoteUrl.pathExtension).isImage
Expand Down Expand Up @@ -196,7 +126,6 @@
progress: (@Sendable (Double) -> Void)? = nil,
completion: @escaping @Sendable (Result<UploadedFile, Error>) -> Void
) {
// Encode locally stored attachment into multipart form data
let multipartFormData = MultipartFormData(
fileData,
fileName: uploadingState.localFileURL.lastPathComponent,
Expand Down
24 changes: 24 additions & 0 deletions Sources/StreamChat/APIClient/CDNClient/CDNImageResize.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// Copyright Β© 2026 Stream.io Inc. All rights reserved.
//

import CoreGraphics

/// Parameters for CDN server-side image resize (Stream `w` / `h` / `resize` / `crop` query parameters).
public struct CDNImageResize: Sendable {
/// Width in points (scaled to pixels using screen scale when building the URL).
public var width: CGFloat
/// Height in points (scaled to pixels using screen scale when building the URL).
public var height: CGFloat
/// Value for the `resize` query parameter (for example `"clip"`, `"crop"`, `"fill"`, `"scale"`).
public var resizeMode: String
/// Value for the `crop` query parameter when using crop resize mode.
public var crop: String?

public init(width: CGFloat, height: CGFloat, resizeMode: String, crop: String? = nil) {
self.width = width
self.height = height
self.resizeMode = resizeMode
self.crop = crop
}
}
Loading
Loading