diff --git a/stream-chat-android-compose/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api index 826c04026d2..44e7788d5fb 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -3245,7 +3245,7 @@ public final class io/getstream/chat/android/compose/ui/theme/ChatTheme { } public final class io/getstream/chat/android/compose/ui/theme/ChatThemeKt { - public static final fun ChatTheme (ZZZZZLio/getstream/chat/android/ui/common/permissions/SystemAttachmentsPickerConfig;Lio/getstream/chat/android/compose/ui/theme/StreamColors;Lio/getstream/chat/android/compose/ui/theme/StreamDimens;Lio/getstream/chat/android/compose/ui/theme/StreamTypography;Lio/getstream/chat/android/compose/ui/theme/StreamShapes;Lio/getstream/chat/android/compose/ui/theme/StreamRippleConfiguration;Lio/getstream/chat/android/ui/common/model/UserPresence;Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Ljava/util/List;Lio/getstream/chat/android/compose/ui/components/messages/factory/MessageContentFactory;Ljava/util/List;Ljava/util/List;Lio/getstream/chat/android/compose/ui/util/ReactionIconFactory;Lio/getstream/chat/android/ui/common/helper/ReactionPushEmojiFactory;Lio/getstream/chat/android/compose/ui/theme/ReactionOptionsTheme;Lio/getstream/chat/android/compose/ui/util/MessagePreviewIconFactory;Lio/getstream/chat/android/compose/ui/util/PollSwitchItemFactory;ZLio/getstream/chat/android/ui/common/helper/DateFormatter;Lio/getstream/chat/android/ui/common/helper/TimeProvider;Lio/getstream/chat/android/ui/common/helper/DurationFormatter;Lio/getstream/chat/android/ui/common/utils/ChannelNameFormatter;Lio/getstream/chat/android/compose/ui/util/MessagePreviewFormatter;Lio/getstream/chat/android/compose/ui/util/SearchResultNameFormatter;Lio/getstream/chat/android/compose/ui/util/StreamCoilImageLoaderFactory;Lio/getstream/chat/android/ui/common/helper/ImageHeadersProvider;Lio/getstream/chat/android/ui/common/helper/DownloadAttachmentUriGenerator;Lio/getstream/chat/android/ui/common/helper/DownloadRequestInterceptor;Lio/getstream/chat/android/ui/common/helper/ImageAssetTransformer;Lio/getstream/chat/android/compose/ui/util/MessageAlignmentProvider;Lio/getstream/chat/android/compose/ui/theme/MessageOptionsTheme;Lio/getstream/chat/android/compose/ui/theme/ChannelOptionsTheme;Lio/getstream/chat/android/ui/common/state/messages/list/MessageOptionsUserReactionAlignment;Ljava/util/List;ZLio/getstream/chat/android/ui/common/images/resizing/StreamCdnImageResizing;ZLio/getstream/chat/android/compose/ui/theme/MessageTheme;Lio/getstream/chat/android/compose/ui/theme/MessageTheme;Lio/getstream/chat/android/compose/ui/theme/MessageDateSeparatorTheme;Lio/getstream/chat/android/compose/ui/theme/MessageUnreadSeparatorTheme;Lio/getstream/chat/android/compose/ui/theme/MessageComposerTheme;Lio/getstream/chat/android/compose/ui/theme/AttachmentPickerTheme;Lio/getstream/chat/android/compose/ui/util/MessageTextFormatter;Lio/getstream/chat/android/compose/ui/util/QuotedMessageTextFormatter;Lio/getstream/sdk/chat/audio/recording/StreamMediaRecorder;Lio/getstream/chat/android/compose/ui/theme/StreamKeyboardBehaviour;Lio/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryConfig;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;IIIIIIII)V + public static final fun ChatTheme (ZZZZZLio/getstream/chat/android/ui/common/permissions/SystemAttachmentsPickerConfig;Lio/getstream/chat/android/compose/ui/theme/StreamColors;Lio/getstream/chat/android/compose/ui/theme/StreamDimens;Lio/getstream/chat/android/compose/ui/theme/StreamTypography;Lio/getstream/chat/android/compose/ui/theme/StreamShapes;Lio/getstream/chat/android/compose/ui/theme/StreamRippleConfiguration;Lio/getstream/chat/android/ui/common/model/UserPresence;Lio/getstream/chat/android/compose/ui/theme/ChatComponentFactory;Ljava/util/List;Lio/getstream/chat/android/compose/ui/components/messages/factory/MessageContentFactory;Ljava/util/List;Ljava/util/List;Lio/getstream/chat/android/compose/ui/util/ReactionIconFactory;Lio/getstream/chat/android/ui/common/helper/ReactionPushEmojiFactory;Lio/getstream/chat/android/compose/ui/theme/ReactionOptionsTheme;Lio/getstream/chat/android/compose/ui/util/MessagePreviewIconFactory;Lio/getstream/chat/android/compose/ui/util/PollSwitchItemFactory;ZLio/getstream/chat/android/ui/common/helper/DateFormatter;Lio/getstream/chat/android/ui/common/helper/TimeProvider;Lio/getstream/chat/android/ui/common/helper/DurationFormatter;Lio/getstream/chat/android/ui/common/utils/ChannelNameFormatter;Lio/getstream/chat/android/compose/ui/util/MessagePreviewFormatter;Lio/getstream/chat/android/compose/ui/util/SearchResultNameFormatter;Lio/getstream/chat/android/compose/ui/util/StreamCoilImageLoaderFactory;Lio/getstream/chat/android/ui/common/helper/ImageHeadersProvider;Lio/getstream/chat/android/ui/common/helper/AsyncImageHeadersProvider;Lio/getstream/chat/android/ui/common/helper/DownloadAttachmentUriGenerator;Lio/getstream/chat/android/ui/common/helper/DownloadRequestInterceptor;Lio/getstream/chat/android/ui/common/helper/ImageAssetTransformer;Lio/getstream/chat/android/compose/ui/util/MessageAlignmentProvider;Lio/getstream/chat/android/compose/ui/theme/MessageOptionsTheme;Lio/getstream/chat/android/compose/ui/theme/ChannelOptionsTheme;Lio/getstream/chat/android/ui/common/state/messages/list/MessageOptionsUserReactionAlignment;Ljava/util/List;ZLio/getstream/chat/android/ui/common/images/resizing/StreamCdnImageResizing;ZLio/getstream/chat/android/compose/ui/theme/MessageTheme;Lio/getstream/chat/android/compose/ui/theme/MessageTheme;Lio/getstream/chat/android/compose/ui/theme/MessageDateSeparatorTheme;Lio/getstream/chat/android/compose/ui/theme/MessageUnreadSeparatorTheme;Lio/getstream/chat/android/compose/ui/theme/MessageComposerTheme;Lio/getstream/chat/android/compose/ui/theme/AttachmentPickerTheme;Lio/getstream/chat/android/compose/ui/util/MessageTextFormatter;Lio/getstream/chat/android/compose/ui/util/QuotedMessageTextFormatter;Lio/getstream/sdk/chat/audio/recording/StreamMediaRecorder;Lio/getstream/chat/android/compose/ui/theme/StreamKeyboardBehaviour;Lio/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryConfig;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;IIIIIIII)V public static final fun getLocalComponentFactory ()Landroidx/compose/runtime/ProvidableCompositionLocal; } @@ -4734,12 +4734,17 @@ public final class io/getstream/chat/android/compose/ui/util/StorageHelperWrappe public abstract interface class io/getstream/chat/android/compose/ui/util/StreamCoilImageLoaderFactory { public static final field Companion Lio/getstream/chat/android/compose/ui/util/StreamCoilImageLoaderFactory$Companion; public abstract fun imageLoader (Landroid/content/Context;)Lcoil3/ImageLoader; + public abstract fun imageLoader (Landroid/content/Context;Ljava/util/List;)Lcoil3/ImageLoader; } public final class io/getstream/chat/android/compose/ui/util/StreamCoilImageLoaderFactory$Companion { public final fun defaultFactory ()Lio/getstream/chat/android/compose/ui/util/StreamCoilImageLoaderFactory; } +public final class io/getstream/chat/android/compose/ui/util/StreamCoilImageLoaderFactory$DefaultImpls { + public static fun imageLoader (Lio/getstream/chat/android/compose/ui/util/StreamCoilImageLoaderFactory;Landroid/content/Context;Ljava/util/List;)Lcoil3/ImageLoader; +} + public final class io/getstream/chat/android/compose/ui/util/StreamImageLoaderProvidableCompositionLocal { public static final synthetic fun box-impl (Landroidx/compose/runtime/ProvidableCompositionLocal;)Lio/getstream/chat/android/compose/ui/util/StreamImageLoaderProvidableCompositionLocal; public static synthetic fun constructor-impl$default (Landroidx/compose/runtime/ProvidableCompositionLocal;ILkotlin/jvm/internal/DefaultConstructorMarker;)Landroidx/compose/runtime/ProvidableCompositionLocal; diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatTheme.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatTheme.kt index b01cb248a39..b73eef72968 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatTheme.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatTheme.kt @@ -30,6 +30,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.remember import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter @@ -49,6 +50,7 @@ import io.getstream.chat.android.compose.ui.messages.attachments.factory.Attachm import io.getstream.chat.android.compose.ui.messages.attachments.factory.AttachmentsPickerTabFactory import io.getstream.chat.android.compose.ui.theme.messages.attachments.FileAttachmentTheme import io.getstream.chat.android.compose.ui.util.DefaultPollSwitchItemFactory +import io.getstream.chat.android.compose.ui.util.ImageHeadersInterceptor import io.getstream.chat.android.compose.ui.util.LocalStreamImageLoader import io.getstream.chat.android.compose.ui.util.MessageAlignmentProvider import io.getstream.chat.android.compose.ui.util.MessagePreviewFormatter @@ -59,6 +61,7 @@ import io.getstream.chat.android.compose.ui.util.QuotedMessageTextFormatter import io.getstream.chat.android.compose.ui.util.ReactionIconFactory import io.getstream.chat.android.compose.ui.util.SearchResultNameFormatter import io.getstream.chat.android.compose.ui.util.StreamCoilImageLoaderFactory +import io.getstream.chat.android.ui.common.helper.AsyncImageHeadersProvider import io.getstream.chat.android.ui.common.helper.DateFormatter import io.getstream.chat.android.ui.common.helper.DefaultDownloadAttachmentUriGenerator import io.getstream.chat.android.ui.common.helper.DefaultImageAssetTransformer @@ -160,6 +163,12 @@ private val LocalQuotedMessageTextFormatter = compositionLocalOf { error("No SearchResultNameFormatter provided! Make sure to wrap all usages of Stream components in a ChatTheme.") } + +@Deprecated( + message = "ImageHeadersProvider is deprecated. Use asyncImageHeadersProvider in ChatTheme instead. " + + "Headers are now injected via Coil's interceptor pipeline, which is thread-safe and supports " + + "blocking/suspending operations.", +) private val LocalStreamImageHeadersProvider = compositionLocalOf { error("No ImageHeadersProvider provided! Make sure to wrap all usages of Stream components in a ChatTheme.") } @@ -291,9 +300,17 @@ private val LocalMediaGalleryConfig = compositionLocalOf { * @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. + * @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 imageAssetTransformer [ImageAssetTransformer] Used to transform image assets. - * @param imageHeadersProvider [ImageHeadersProvider] Used to provide headers for image requests. + * @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. + * @param asyncImageHeadersProvider [AsyncImageHeadersProvider] Used to provide headers for image + * requests. Invoked on IO Dispatcher inside Coil's interceptor pipeline, making it safe for blocking or suspending + * operations such as reading an auth token. Prefer this over [imageHeadersProvider]. If you are using this in + * combination with a custom [StreamCoilImageLoaderFactory] you must override the + * [StreamCoilImageLoaderFactory.imageLoader] method accepting the interceptors parameter. * @param downloadAttachmentUriGenerator [DownloadAttachmentUriGenerator] Used to generate download URIs for * attachments. * @param downloadRequestInterceptor [DownloadRequestInterceptor] Used to intercept download requests. @@ -361,6 +378,7 @@ public fun ChatTheme( searchResultNameFormatter: SearchResultNameFormatter = SearchResultNameFormatter.defaultFormatter(), imageLoaderFactory: StreamCoilImageLoaderFactory = StreamCoilImageLoaderFactory.defaultFactory(), imageHeadersProvider: ImageHeadersProvider = DefaultImageHeadersProvider, + asyncImageHeadersProvider: AsyncImageHeadersProvider? = null, downloadAttachmentUriGenerator: DownloadAttachmentUriGenerator = DefaultDownloadAttachmentUriGenerator, downloadRequestInterceptor: DownloadRequestInterceptor = DownloadRequestInterceptor { }, imageAssetTransformer: ImageAssetTransformer = DefaultImageAssetTransformer, @@ -430,6 +448,19 @@ public fun ChatTheme( ChatClient.VERSION_PREFIX_HEADER = VersionPrefixHeader.Compose } + val context = LocalContext.current + val imageLoader = remember(imageLoaderFactory, asyncImageHeadersProvider) { + if (asyncImageHeadersProvider == null) { + imageLoaderFactory.imageLoader(context.applicationContext) + } else { + imageLoaderFactory.imageLoader( + context.applicationContext, + listOf(ImageHeadersInterceptor(asyncImageHeadersProvider)), + ) + } + } + + @Suppress("DEPRECATION") CompositionLocalProvider( LocalColors provides colors, LocalDimens provides dimens, @@ -463,7 +494,7 @@ public fun ChatTheme( LocalMessageUnreadSeparatorTheme provides messageUnreadSeparatorTheme, LocalMessageComposerTheme provides messageComposerTheme, LocalAttachmentPickerTheme provides attachmentPickerTheme, - LocalStreamImageLoader provides imageLoaderFactory.imageLoader(LocalContext.current.applicationContext), + LocalStreamImageLoader provides imageLoader, LocalStreamImageHeadersProvider provides imageHeadersProvider, LocalStreamDownloadAttachmentUriGenerator provides downloadAttachmentUriGenerator, LocalStreamDownloadRequestInterceptor provides downloadRequestInterceptor, @@ -820,7 +851,15 @@ public object ChatTheme { /** * Retrieves the current [ImageHeadersProvider] at the call site's position in the hierarchy. + * + * @deprecated Use [asyncImageHeadersProvider] in [ChatTheme] for thread-safe header injection. */ + @Deprecated( + message = "ImageHeadersProvider is deprecated. Pass asyncImageHeadersProvider to ChatTheme instead. " + + "Headers are now injected via Coil's interceptor pipeline, which is thread-safe and supports " + + "blocking/suspending operations.", + ) + @Suppress("DEPRECATION") public val streamImageHeadersProvider: ImageHeadersProvider @Composable @ReadOnlyComposable diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/ImageHeadersInterceptor.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/ImageHeadersInterceptor.kt new file mode 100644 index 00000000000..9acaf6db0cc --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/ImageHeadersInterceptor.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.ui.util + +import coil3.intercept.Interceptor +import coil3.network.httpHeaders +import coil3.request.ImageResult +import io.getstream.chat.android.ui.common.helper.AsyncImageHeadersProvider +import io.getstream.chat.android.ui.common.images.internal.toNetworkHeaders +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** + * A Coil [Interceptor] that injects HTTP headers provided by [AsyncImageHeadersProvider] into + * each image request. The provider is invoked as part of Coil's background pipeline, so + * blocking or suspending operations (e.g. fetching an auth token) are safe to perform inside + * [AsyncImageHeadersProvider.getImageRequestHeaders]. + */ +internal class ImageHeadersInterceptor(private val headersProvider: AsyncImageHeadersProvider) : Interceptor { + + override suspend fun intercept(chain: Interceptor.Chain): ImageResult { + val url = chain.request.data.toString() + val headers = withContext(Dispatchers.IO) { + headersProvider.getImageRequestHeaders(url) + } + val newRequest = chain.request.newBuilder() + .httpHeaders(headers.toNetworkHeaders()) + .build() + return chain.withRequest(newRequest).proceed() + } +} diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/StreamCoilImageLoaderFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/StreamCoilImageLoaderFactory.kt index c6f5cf78367..30fb779a480 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/StreamCoilImageLoaderFactory.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/StreamCoilImageLoaderFactory.kt @@ -19,6 +19,7 @@ package io.getstream.chat.android.compose.ui.util import android.content.Context import coil3.ImageLoader import coil3.SingletonImageLoader +import coil3.intercept.Interceptor import io.getstream.chat.android.ui.common.images.StreamImageLoaderFactory /** @@ -31,15 +32,40 @@ public fun interface StreamCoilImageLoaderFactory { */ public fun imageLoader(context: Context): ImageLoader + /** + * Returns a new Coil [ImageLoader] with the given [interceptors] prepended to the component + * registry, ahead of all decoders and Coil's built-in EngineInterceptor. + * + * The default implementation **ignores [interceptors]** and delegates to [imageLoader]. + * This means that when a custom [StreamCoilImageLoaderFactory] is used alongside + * [ChatTheme]'s `asyncImageHeadersProvider`, the async headers will **not** be injected — + * the custom factory's loader is returned as-is. + * + * Custom class implementations that want to support interceptor injection should override this + * method, for example by forwarding [interceptors] to [StreamImageLoaderFactory]: + * ```kotlin + * override fun imageLoader(context: Context, interceptors: List): ImageLoader = + * StreamImageLoaderFactory(interceptors = interceptors, builder = myCustomBuilder) + * .newImageLoader(context) + * ``` + * + * Integrators using a custom [StreamCoilImageLoaderFactory] who also need auth headers on + * image requests should either override this method or inject the headers directly inside + * their factory's [imageLoader] implementation (e.g. via a custom OkHttp client). + * + * @param context The [Context] to build the [ImageLoader] with. + * @param interceptors Coil [Interceptor]s to prepend to the component registry. + */ + public fun imageLoader(context: Context, interceptors: List): ImageLoader = + imageLoader(context) + public companion object { /** * Returns the default singleton instance of [StreamCoilImageLoaderFactory]. * * @return The default implementation of [StreamCoilImageLoaderFactory]. */ - public fun defaultFactory(): StreamCoilImageLoaderFactory { - return DefaultStreamCoilImageLoaderFactory - } + public fun defaultFactory(): StreamCoilImageLoaderFactory = DefaultStreamCoilImageLoaderFactory } } @@ -68,6 +94,13 @@ internal object DefaultStreamCoilImageLoaderFactory : StreamCoilImageLoaderFacto */ override fun imageLoader(context: Context): ImageLoader = imageLoader ?: newImageLoader(context) + override fun imageLoader(context: Context, interceptors: List): ImageLoader = + if (interceptors.isEmpty()) { + imageLoader(context) + } else { + StreamImageLoaderFactory(interceptors = interceptors).newImageLoader(context) + } + /** * Builds a new [ImageLoader] using the given Android [Context]. If the loader already exists, we return it. * diff --git a/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api b/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api index b7b0be2abbe..52ef740895d 100644 --- a/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api +++ b/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api @@ -1065,6 +1065,10 @@ public final class io/getstream/chat/android/ui/common/feature/messages/translat public final class io/getstream/chat/android/ui/common/feature/threads/ThreadListController$Companion { } +public abstract interface class io/getstream/chat/android/ui/common/helper/AsyncImageHeadersProvider { + public abstract fun getImageRequestHeaders (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + public abstract interface class io/getstream/chat/android/ui/common/helper/ClipboardHandler { public abstract fun copyMessage (Lio/getstream/chat/android/models/Message;)V } @@ -1194,6 +1198,8 @@ public final class io/getstream/chat/android/ui/common/helper/internal/StorageHe public final class io/getstream/chat/android/ui/common/images/StreamImageLoaderFactory : coil3/SingletonImageLoader$Factory { public static final field $stable I public fun ()V + public fun (Ljava/util/List;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Ljava/util/List;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun (Lkotlin/jvm/functions/Function1;)V public synthetic fun (Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun newImageLoader (Landroid/content/Context;)Lcoil3/ImageLoader; diff --git a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/helper/AsyncImageHeadersProvider.kt b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/helper/AsyncImageHeadersProvider.kt new file mode 100644 index 00000000000..8b1c07e3fa5 --- /dev/null +++ b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/helper/AsyncImageHeadersProvider.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.ui.common.helper + +/** + * Provides HTTP headers for image loading requests in a suspending, thread-safe manner. + * + * Unlike [ImageHeadersProvider], this interface is designed for async operations such as + * reading an auth token from encrypted storage or fetching one from a remote endpoint. + * Implementations are always invoked on [kotlinx.coroutines.Dispatchers.IO], so blocking + * calls are safe. + * + * Prefer this over [ImageHeadersProvider] when integrating with [ChatTheme]. + * + * @see ImageHeadersProvider + */ +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 +} diff --git a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/images/StreamImageLoaderFactory.kt b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/images/StreamImageLoaderFactory.kt index d3510042ce7..77012a3472c 100644 --- a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/images/StreamImageLoaderFactory.kt +++ b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/images/StreamImageLoaderFactory.kt @@ -23,15 +23,13 @@ import coil3.SingletonImageLoader import coil3.disk.DiskCache import coil3.gif.AnimatedImageDecoder import coil3.gif.GifDecoder +import coil3.intercept.Interceptor import coil3.memory.MemoryCache -import coil3.network.okhttp.OkHttpNetworkFetcherFactory import coil3.request.allowHardware import coil3.request.crossfade import coil3.video.VideoFrameDecoder import io.getstream.chat.android.client.internal.file.StreamFileManager -import okhttp3.Dispatcher -import okhttp3.Interceptor -import okhttp3.OkHttpClient +import kotlinx.coroutines.Dispatchers import okio.Path.Companion.toOkioPath private const val DEFAULT_MEMORY_PERCENTAGE = 0.25 @@ -52,6 +50,24 @@ public class StreamImageLoaderFactory( private val builder: ImageLoader.Builder.() -> Unit = {}, ) : SingletonImageLoader.Factory { + /** + * Creates a [StreamImageLoaderFactory] with additional [Interceptor]s prepended to the + * component registry, before any decoders and before Coil's built-in [EngineInterceptor]. + * + * This constructor preserves the existing primary constructor signature and is purely additive. + * + * @param interceptors Coil [Interceptor]s to register ahead of all other components. + * @param builder Optional lambda to further customize the [ImageLoader] configuration. + */ + public constructor( + interceptors: List, + builder: ImageLoader.Builder.() -> Unit = {}, + ) : this(builder) { + this.interceptors = interceptors + } + + private var interceptors: List = emptyList() + private val fileManager = StreamFileManager() override fun newImageLoader(context: PlatformContext): ImageLoader { @@ -59,34 +75,15 @@ public class StreamImageLoaderFactory( .memoryCache { MemoryCache.Builder().maxSizePercent(context, DEFAULT_MEMORY_PERCENTAGE).build() } .allowHardware(false) .crossfade(true) - .components { - add( - OkHttpNetworkFetcherFactory( - callFactory = { - val cacheControlInterceptor = Interceptor { chain -> - chain.proceed(chain.request()) - .newBuilder() - .header("Cache-Control", "max-age=3600,public") - .build() - } - // Don't limit concurrent network requests by host. - val dispatcher = Dispatcher().apply { maxRequestsPerHost = maxRequests } - - OkHttpClient.Builder() - .dispatcher(dispatcher) - .addNetworkInterceptor(cacheControlInterceptor) - .build() - }, - ), - ) - } .diskCache { DiskCache.Builder() .directory(fileManager.getImageCache(context).toOkioPath()) .maxSizePercent(DEFAULT_DISK_CACHE_PERCENTAGE) .build() } + .interceptorCoroutineContext(Dispatchers.IO) .components { + interceptors.forEach { add(it) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { add(AnimatedImageDecoder.Factory(enforceMinimumFrameDelay = true)) } else {