Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -308,9 +308,11 @@ private val LocalMediaGalleryConfig = compositionLocalOf<MediaGalleryConfig> {
* @param durationFormatter [DurationFormatter] Used to format durations in the app.
* @param channelNameFormatter [ChannelNameFormatter] Used throughout the app for channel names.
* @param messagePreviewFormatter [MessagePreviewFormatter] Used to generate a string preview for the given message.
* @param imageLoaderFactory A factory that creates new Coil [ImageLoader] instances. If used in combination with
* [asyncImageHeadersProvider] you must override the [StreamCoilImageLoaderFactory.imageLoader] method accepting the
* interceptors parameter.
* @param imageLoaderFactory A factory that creates new Coil [ImageLoader] instances. If you provide a custom factory
* **and** use a custom CDN (via [io.getstream.chat.android.client.ChatClient.Builder]) or
* [asyncImageHeadersProvider], you must override the [StreamCoilImageLoaderFactory.imageLoader] overload that accepts
* interceptors; otherwise those features are silently ignored. If you don't use either, overriding the single-arg
* method is sufficient.
* @param imageAssetTransformer [ImageAssetTransformer] Used to transform image assets.
* @param imageHeadersProvider [ImageHeadersProvider] Deprecated. Use [asyncImageHeadersProvider] instead. Headers
* provided here are injected synchronously on the main thread, which blocks the UI for any non-trivial work.
Expand Down Expand Up @@ -458,17 +460,12 @@ public fun ChatTheme(
}

val context = LocalContext.current
val cdn = remember { ChatClient.instance().cdn }
Comment thread
aleksandar-apostolov marked this conversation as resolved.
val imageLoader = remember(imageLoaderFactory, asyncImageHeadersProvider, cdn) {
val imageLoader = remember(imageLoaderFactory, asyncImageHeadersProvider) {
val interceptors = buildList {
asyncImageHeadersProvider?.let { add(ImageHeadersInterceptor(it)) }
cdn?.let { add(CDNImageInterceptor(it)) }
}
if (interceptors.isEmpty()) {
imageLoaderFactory.imageLoader(context.applicationContext)
} else {
imageLoaderFactory.imageLoader(context.applicationContext, interceptors)
add(CDNImageInterceptor())
}
imageLoaderFactory.imageLoader(context.applicationContext, interceptors)
}

@Suppress("DEPRECATION")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package io.getstream.chat.android.ui.common.images.internal
import coil3.intercept.Interceptor
import coil3.network.httpHeaders
import coil3.request.ImageResult
import io.getstream.chat.android.client.ChatClient
import io.getstream.chat.android.client.cdn.CDN
import io.getstream.chat.android.core.internal.InternalStreamChatApi
import io.getstream.log.taggedLogger
Expand All @@ -34,13 +35,21 @@ import kotlinx.coroutines.withContext
* them for the same key.
*
* Only HTTP/HTTPS URLs are intercepted; local resources, content URIs, etc. pass through unchanged.
*
* The [CDN] instance is resolved lazily via [cdn] on each request, so the interceptor
* is safe to install even before [ChatClient] is initialized (e.g. Compose previews, VRT tests).
* When [cdn] returns `null` the request passes through unchanged.
*/
@InternalStreamChatApi
public class CDNImageInterceptor(private val cdn: CDN) : Interceptor {
public class CDNImageInterceptor(
private val cdn: () -> CDN? = { if (ChatClient.isInitialized) ChatClient.instance().cdn else null },
) : Interceptor {

private val logger by taggedLogger("Chat:CDNImageInterceptor")

override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
val cdn = cdn() ?: return chain.proceed()

val request = chain.request
val url = request.data.toString()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package io.getstream.chat.android.ui.common.images.internal
import android.content.Context
import coil3.ImageLoader
import coil3.SingletonImageLoader
import io.getstream.chat.android.client.ChatClient
import io.getstream.chat.android.core.internal.InternalStreamChatApi
import io.getstream.chat.android.ui.common.images.StreamImageLoaderFactory

Expand Down Expand Up @@ -48,10 +47,7 @@ public object StreamCoil {
}

private fun newImageLoaderFactory(): SingletonImageLoader.Factory {
val cdn = ChatClient.instance().cdn
val interceptors = buildList {
cdn?.let { add(CDNImageInterceptor(it)) }
}
val interceptors = listOf(CDNImageInterceptor())
return StreamImageLoaderFactory(interceptors).apply {
imageLoaderFactory = this
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ internal class CDNImageInterceptorTest {
override suspend fun imageRequest(url: String) =
CDNRequest("https://cdn.example.com/image.jpg")
}
val interceptor = CDNImageInterceptor(cdn)
val interceptor = CDNImageInterceptor { cdn }
val request = ImageRequest.Builder(context)
.data("https://original.com/image.jpg")
.build()
Expand All @@ -66,7 +66,7 @@ internal class CDNImageInterceptorTest {
override suspend fun imageRequest(url: String) =
CDNRequest(url, mapOf("Authorization" to "Bearer token", "X-Custom" to "value"))
}
val interceptor = CDNImageInterceptor(cdn)
val interceptor = CDNImageInterceptor { cdn }
val request = ImageRequest.Builder(context)
.data("https://original.com/image.jpg")
.build()
Expand All @@ -85,7 +85,7 @@ internal class CDNImageInterceptorTest {
override suspend fun imageRequest(url: String) =
CDNRequest(url, mapOf("Authorization" to "CDN-token"))
}
val interceptor = CDNImageInterceptor(cdn)
val interceptor = CDNImageInterceptor { cdn }
val existingHeaders = NetworkHeaders.Builder()
.add("Authorization", "Original-token")
.add("X-Existing", "keep-me")
Expand All @@ -112,7 +112,7 @@ internal class CDNImageInterceptorTest {
return CDNRequest("https://should-not-be-used.com")
}
}
val interceptor = CDNImageInterceptor(cdn)
val interceptor = CDNImageInterceptor { cdn }
val request = ImageRequest.Builder(context)
.data("content://media/image.jpg")
.build()
Expand All @@ -132,7 +132,7 @@ internal class CDNImageInterceptorTest {
throw RuntimeException("CDN unavailable")
}
}
val interceptor = CDNImageInterceptor(cdn)
val interceptor = CDNImageInterceptor { cdn }
val request = ImageRequest.Builder(context)
.data("https://original.com/image.jpg")
.build()
Expand All @@ -143,6 +143,19 @@ internal class CDNImageInterceptorTest {
assertTrue("Should fall back to direct proceed on CDN error", chain.directProceed)
}

@Test
fun `intercept passes through when cdn returns null`() = runTest {
val interceptor = CDNImageInterceptor { null }
val request = ImageRequest.Builder(context)
.data("https://original.com/image.jpg")
.build()
val chain = FakeCoilChain(request)

interceptor.intercept(chain)

assertTrue("Should pass through when CDN is null", chain.directProceed)
}

@Suppress("EmptyFunctionBlock")
private class FakeCoilChain(
override val request: ImageRequest,
Expand Down
Loading