public interface FileUploader {
@WorkerThread
public fun sendFile(
channelType: String,
channelId: String,
userId: String,
file: File,
callback: ProgressCallback,
): Result<UploadedFile>
@WorkerThread
public fun sendImage(
channelType: String,
channelId: String,
userId: String,
file: File,
callback: ProgressCallback,
): Result<UploadedFile>
@WorkerThread
public fun deleteFile(
channelType: String,
channelId: String,
userId: String,
url: String,
): Result<Unit>
@WorkerThread
public fun deleteImage(
channelType: String,
channelId: String,
userId: String,
url: String,
): Result<Unit>
}Custom CDN
By default, files and images are uploaded to Stream's CDN and loaded without additional HTTP headers. However, you can customize both behaviors:
- File Uploads: Provide a custom
FileUploaderto upload files to your own CDN or storage service. - Image Loading: Provide an
ImageHeadersProviderto add custom HTTP headers (e.g., authentication tokens) when loading images.
Custom File Uploads
To use a custom CDN or storage service (such as AWS S3, Firebase Storage, or your own backend), implement the FileUploader interface and configure it on the ChatClient.Builder.
FileUploader Interface
The FileUploader interface defines the contract for uploading and deleting files and images:
The interface also includes overloaded versions of these methods without the ProgressCallback parameter, as well as methods for uploading/deleting files not associated with a specific channel (uploadFile, uploadImage, deleteFile, deleteImage). You will need to implement those as well (omitted in this snippet for brevity).
UploadedFile Model
When a file is successfully uploaded, return an UploadedFile containing the file URL and optional metadata:
public data class UploadedFile(
val file: String, // The URL of the uploaded file
val thumbUrl: String? = null, // Optional thumbnail URL (for videos)
val extraData: Map<String, Any> = emptyMap(), // Additional custom metadata
)Creating a Custom FileUploader
Here's an example of a custom FileUploader implementation:
class CustomFileUploader(
private val uploadApi: YourUploadApi,
) : FileUploader {
override fun sendFile(
channelType: String,
channelId: String,
userId: String,
file: File,
callback: ProgressCallback,
): Result<UploadedFile> {
return try {
val response = uploadApi.uploadFile(
file = file,
path = "channels/$channelType/$channelId/files",
onProgress = { bytesUploaded, totalBytes ->
callback.onProgress(bytesUploaded, totalBytes)
}
)
Result.Success(
UploadedFile(
file = response.url,
thumbUrl = response.thumbnailUrl,
)
)
} catch (e: Exception) {
callback.onError(e)
Result.Failure(Error.ThrowableError(e.message ?: "Upload failed", e))
}
}
override fun sendImage(
channelType: String,
channelId: String,
userId: String,
file: File,
callback: ProgressCallback,
): Result<UploadedFile> {
return try {
val response = uploadApi.uploadImage(
file = file,
path = "channels/$channelType/$channelId/images",
onProgress = { bytesUploaded, totalBytes ->
callback.onProgress(bytesUploaded, totalBytes)
}
)
Result.Success(
UploadedFile(
file = response.url,
thumbUrl = response.thumbnailUrl,
)
)
} catch (e: Exception) {
callback.onError(e)
Result.Failure(Error.ThrowableError(e.message ?: "Upload failed", e))
}
}
override fun deleteFile(
channelType: String,
channelId: String,
userId: String,
url: String,
): Result<Unit> {
return try {
uploadApi.deleteFile(url)
Result.Success(Unit)
} catch (e: Exception) {
Result.Failure(Error.ThrowableError(e.message ?: "Delete failed", e))
}
}
override fun deleteImage(
channelType: String,
channelId: String,
userId: String,
url: String,
): Result<Unit> {
return try {
uploadApi.deleteFile(url)
Result.Success(Unit)
} catch (e: Exception) {
Result.Failure(Error.ThrowableError(e.message ?: "Delete failed", e))
}
}
// Implement the remaining methods (without ProgressCallback, and channel-agnostic variants) similarly
}Configuring the Custom FileUploader
Pass your custom FileUploader to the ChatClient.Builder:
val client = ChatClient.Builder(apiKey, context)
.fileUploader(CustomFileUploader(YourUploadApi()))
.build()Custom Image Loading Headers
If your CDN requires authentication or custom headers when loading images, implement the ImageHeadersProvider interface to inject headers into all image loading requests.
ImageHeadersProvider Interface
public interface ImageHeadersProvider {
/**
* Returns a map of headers to be used for the image loading request.
*
* @param url The URL of the image to load.
* @return A map of headers to be used for the image loading request.
*/
public fun getImageRequestHeaders(url: String): Map<String, String>
}Creating a Custom ImageHeadersProvider
class AuthenticatedImageHeadersProvider(
private val tokenProvider: () -> String,
) : ImageHeadersProvider {
override fun getImageRequestHeaders(url: String): Map<String, String> {
// Only add headers for your CDN URLs
return if (url.contains("your-cdn.com")) {
mapOf("Authorization" to "Bearer ${tokenProvider()}")
} else {
emptyMap()
}
}
}Configuring ImageHeadersProvider
XML UI Components
Set the provider on ChatUI:
ChatUI.imageHeadersProvider = AuthenticatedImageHeadersProvider {
// Return your auth token
authRepository.getToken()
}Compose UI Components
Pass the provider to ChatTheme:
ChatTheme(
imageHeadersProvider = AuthenticatedImageHeadersProvider {
authRepository.getToken()
},
) {
// Your chat UI content
}Async Custom Image Loading Headers
For scenarios where obtaining headers requires asynchronous or blocking operations — such as reading an auth token from encrypted storage or fetching one from a remote endpoint — use AsyncImageHeadersProvider instead of ImageHeadersProvider.
AsyncImageHeadersProvider is currently only available for the Compose UI Components. For XML UI Components, continue using ImageHeadersProvider with ChatUI.
AsyncImageHeadersProvider Interface
Unlike ImageHeadersProvider, this interface exposes a suspend function and is always invoked on Dispatchers.IO, making blocking calls safe:
public interface AsyncImageHeadersProvider {
/**
* Returns a map of headers to be used for the image loading request.
*
* Always called on [kotlinx.coroutines.Dispatchers.IO], so blocking operations are safe.
*
* @param url The URL of the image to load.
* @return A map of headers to be used for the image loading request.
*/
public suspend fun getImageRequestHeaders(url: String): Map<String, String>
}Creating a Custom AsyncImageHeadersProvider
class AsyncAuthenticatedImageHeadersProvider(
private val tokenProvider: suspend () -> String,
) : AsyncImageHeadersProvider {
override suspend fun getImageRequestHeaders(url: String): Map<String, String> {
// Safe to call blocking or suspending operations here — runs on Dispatchers.IO
return if (url.contains("your-cdn.com")) {
mapOf("Authorization" to "Bearer ${tokenProvider()}")
} else {
emptyMap()
}
}
}Configuring AsyncImageHeadersProvider
Compose UI Components
Pass the provider to ChatTheme via the asyncImageHeadersProvider parameter:
ChatTheme(
asyncImageHeadersProvider = AsyncAuthenticatedImageHeadersProvider {
// Safe to call blocking or suspending operations here
authRepository.getToken()
},
) {
// Your chat UI content
}Internally, ChatTheme wraps the provider in a Coil interceptor that injects the headers into each image request as part of Coil's background pipeline.
If you supply a custom StreamCoilImageLoaderFactory to ChatTheme, you must override the two-argument imageLoader(context, interceptors) overload in addition to the single-argument one. ChatTheme delivers the AsyncImageHeadersProvider interceptor through this overload, so a factory that only implements imageLoader(context) will silently ignore the provider.
File Transformation Before Upload
The SDK supports transforming files before upload using the FileTransformer interface. This is useful for any preprocessing steps, such as image compression or format conversion.
public fun interface FileTransformer {
@WorkerThread
public fun transform(file: File): File
}Example implementation:
class ImageCompressorTransformer : FileTransformer {
override fun transform(file: File): File {
val mimeType = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(file.extension)
// Only compress image files
if (mimeType?.startsWith("image/") != true) {
return file
}
return compressImage(file)
}
private fun compressImage(file: File): File {
// Your compression logic here
}
}File transformation happens before the file is passed to the FileUploader.
Supported Image MIME Types
The SDK recognizes the following MIME types as images:
image/bmpimage/gifimage/jpegimage/pngimage/webpimage/heicimage/heic-sequenceimage/heifimage/heif-sequenceimage/svg+xml
Files with these MIME types are uploaded using sendImage(), while all other files use sendFile().