Skip to content

Commit 8394fa8

Browse files
VelikovPetarclaude
andauthored
Fix ChatTheme crashing in Compose previews and VRT tests when ChatClient is not initialized (#6366)
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1ef382a commit 8394fa8

File tree

4 files changed

+37
-22
lines changed

4 files changed

+37
-22
lines changed

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatTheme.kt

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

460462
val context = LocalContext.current
461-
val cdn = remember { ChatClient.instance().cdn }
462-
val imageLoader = remember(imageLoaderFactory, asyncImageHeadersProvider, cdn) {
463+
val imageLoader = remember(imageLoaderFactory, asyncImageHeadersProvider) {
463464
val interceptors = buildList {
464465
asyncImageHeadersProvider?.let { add(ImageHeadersInterceptor(it)) }
465-
cdn?.let { add(CDNImageInterceptor(it)) }
466-
}
467-
if (interceptors.isEmpty()) {
468-
imageLoaderFactory.imageLoader(context.applicationContext)
469-
} else {
470-
imageLoaderFactory.imageLoader(context.applicationContext, interceptors)
466+
add(CDNImageInterceptor())
471467
}
468+
imageLoaderFactory.imageLoader(context.applicationContext, interceptors)
472469
}
473470

474471
@Suppress("DEPRECATION")

stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/images/internal/CDNImageInterceptor.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package io.getstream.chat.android.ui.common.images.internal
1919
import coil3.intercept.Interceptor
2020
import coil3.network.httpHeaders
2121
import coil3.request.ImageResult
22+
import io.getstream.chat.android.client.ChatClient
2223
import io.getstream.chat.android.client.cdn.CDN
2324
import io.getstream.chat.android.core.internal.InternalStreamChatApi
2425
import io.getstream.log.taggedLogger
@@ -34,13 +35,21 @@ import kotlinx.coroutines.withContext
3435
* them for the same key.
3536
*
3637
* Only HTTP/HTTPS URLs are intercepted; local resources, content URIs, etc. pass through unchanged.
38+
*
39+
* The [CDN] instance is resolved lazily via [cdn] on each request, so the interceptor
40+
* is safe to install even before [ChatClient] is initialized (e.g. Compose previews, VRT tests).
41+
* When [cdn] returns `null` the request passes through unchanged.
3742
*/
3843
@InternalStreamChatApi
39-
public class CDNImageInterceptor(private val cdn: CDN) : Interceptor {
44+
public class CDNImageInterceptor(
45+
private val cdn: () -> CDN? = { if (ChatClient.isInitialized) ChatClient.instance().cdn else null },
46+
) : Interceptor {
4047

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

4350
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
51+
val cdn = cdn() ?: return chain.proceed()
52+
4453
val request = chain.request
4554
val url = request.data.toString()
4655

stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/images/internal/StreamCoil.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ package io.getstream.chat.android.ui.common.images.internal
1919
import android.content.Context
2020
import coil3.ImageLoader
2121
import coil3.SingletonImageLoader
22-
import io.getstream.chat.android.client.ChatClient
2322
import io.getstream.chat.android.core.internal.InternalStreamChatApi
2423
import io.getstream.chat.android.ui.common.images.StreamImageLoaderFactory
2524

@@ -48,10 +47,7 @@ public object StreamCoil {
4847
}
4948

5049
private fun newImageLoaderFactory(): SingletonImageLoader.Factory {
51-
val cdn = ChatClient.instance().cdn
52-
val interceptors = buildList {
53-
cdn?.let { add(CDNImageInterceptor(it)) }
54-
}
50+
val interceptors = listOf(CDNImageInterceptor())
5551
return StreamImageLoaderFactory(interceptors).apply {
5652
imageLoaderFactory = this
5753
}

stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/images/internal/CDNImageInterceptorTest.kt

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ internal class CDNImageInterceptorTest {
4848
override suspend fun imageRequest(url: String) =
4949
CDNRequest("https://cdn.example.com/image.jpg")
5050
}
51-
val interceptor = CDNImageInterceptor(cdn)
51+
val interceptor = CDNImageInterceptor { cdn }
5252
val request = ImageRequest.Builder(context)
5353
.data("https://original.com/image.jpg")
5454
.build()
@@ -66,7 +66,7 @@ internal class CDNImageInterceptorTest {
6666
override suspend fun imageRequest(url: String) =
6767
CDNRequest(url, mapOf("Authorization" to "Bearer token", "X-Custom" to "value"))
6868
}
69-
val interceptor = CDNImageInterceptor(cdn)
69+
val interceptor = CDNImageInterceptor { cdn }
7070
val request = ImageRequest.Builder(context)
7171
.data("https://original.com/image.jpg")
7272
.build()
@@ -85,7 +85,7 @@ internal class CDNImageInterceptorTest {
8585
override suspend fun imageRequest(url: String) =
8686
CDNRequest(url, mapOf("Authorization" to "CDN-token"))
8787
}
88-
val interceptor = CDNImageInterceptor(cdn)
88+
val interceptor = CDNImageInterceptor { cdn }
8989
val existingHeaders = NetworkHeaders.Builder()
9090
.add("Authorization", "Original-token")
9191
.add("X-Existing", "keep-me")
@@ -112,7 +112,7 @@ internal class CDNImageInterceptorTest {
112112
return CDNRequest("https://should-not-be-used.com")
113113
}
114114
}
115-
val interceptor = CDNImageInterceptor(cdn)
115+
val interceptor = CDNImageInterceptor { cdn }
116116
val request = ImageRequest.Builder(context)
117117
.data("content://media/image.jpg")
118118
.build()
@@ -132,7 +132,7 @@ internal class CDNImageInterceptorTest {
132132
throw RuntimeException("CDN unavailable")
133133
}
134134
}
135-
val interceptor = CDNImageInterceptor(cdn)
135+
val interceptor = CDNImageInterceptor { cdn }
136136
val request = ImageRequest.Builder(context)
137137
.data("https://original.com/image.jpg")
138138
.build()
@@ -143,6 +143,19 @@ internal class CDNImageInterceptorTest {
143143
assertTrue("Should fall back to direct proceed on CDN error", chain.directProceed)
144144
}
145145

146+
@Test
147+
fun `intercept passes through when cdn returns null`() = runTest {
148+
val interceptor = CDNImageInterceptor { null }
149+
val request = ImageRequest.Builder(context)
150+
.data("https://original.com/image.jpg")
151+
.build()
152+
val chain = FakeCoilChain(request)
153+
154+
interceptor.intercept(chain)
155+
156+
assertTrue("Should pass through when CDN is null", chain.directProceed)
157+
}
158+
146159
@Suppress("EmptyFunctionBlock")
147160
private class FakeCoilChain(
148161
override val request: ImageRequest,

0 commit comments

Comments
 (0)