Skip to content

[AND-464] Provide custom ImageLoader to MediaGalleryPreviewActivity. #5736

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from

Conversation

VelikovPetar
Copy link
Contributor

@VelikovPetar VelikovPetar commented Apr 18, 2025

🎯 Goal

Currently it is not possible to use custom ImageLoaders inside the MediaGalleryPreviewActivity because it is an Activity that is launched from the SDK, and it is not possible to pass custom imageLoaderFactory(or other customization) via the ChatTheme.
With this PR, we introduce an internal way of passing the ImageLoader from the MediaAttachmentContent to the MediaGalleryPreviewActivity. This ensures that if a custom StreamCoilImageLoaderFactory is passed to the ChatTheme wrapping the MediaAttachmentContent, the same one is used in the MediaGalleryPreviewActivity as well.

🛠 Implementation details

Introduce a MediaGalleryInjector - a global holder for the ImageLoader. It is populated from the MediaAttachmentContent(it can be done from anywhere, but I placed it there as it is directly linked to the MediaGalleryPreviewActivity). It is then read from the MediaGalleryPreviewActivity, and is bound to the local LocalStreamImageLoader.

🧪 Testing

  1. Apply the provided patch. The patch adds some logging on the DefaultStreamCoilImageLoaderFactory, and defines a CustomCoilImageLoaderFactory - which has a different logging mechanism. It then provides the new factory in the MessagesActivitys ChatTheme.
  2. Open the compose sample app
  3. Open Channels -> Open channel -> Open Media Gallery Activity
  4. Observe the logs in LogCat with tag: StreamImageLoader: The logs on the channel list should originate from the default imageLoader, while the logs from the messages/mediaGallery should originate from the custom imageLoader.
Patch
Subject: [PATCH] [AND-464] Provide custom ImageLoader to MediaGalleryPreviewActivity.
---
Index: stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt
--- a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt	(revision 5da4a1e14e5ab7102742ff6dce6adbd0a55a9c62)
+++ b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt	(date 1744970340177)
@@ -84,6 +84,7 @@
 import io.getstream.chat.android.compose.ui.theme.StreamColors
 import io.getstream.chat.android.compose.ui.theme.StreamShapes
 import io.getstream.chat.android.compose.ui.theme.StreamTypography
+import io.getstream.chat.android.compose.ui.util.CustomCoilImageLoaderFactory
 import io.getstream.chat.android.compose.ui.util.rememberMessageListState
 import io.getstream.chat.android.compose.viewmodel.messages.AttachmentsPickerViewModel
 import io.getstream.chat.android.compose.viewmodel.messages.MessageComposerViewModel
@@ -174,6 +175,7 @@
                 optionVisibility = MessageOptionItemVisibility(),
             ),
             ownMessageTheme = ownMessageTheme,
+            imageLoaderFactory = CustomCoilImageLoaderFactory,
         ) {
             SetupContent()
         }
Index: stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/StreamCoilImageLoaderFactory.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
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
--- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/StreamCoilImageLoaderFactory.kt	(revision 5da4a1e14e5ab7102742ff6dce6adbd0a55a9c62)
+++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/StreamCoilImageLoaderFactory.kt	(date 1744976021940)
@@ -17,8 +17,11 @@
 package io.getstream.chat.android.compose.ui.util
 
 import android.content.Context
+import android.util.Log
+import coil3.EventListener
 import coil3.ImageLoader
 import coil3.SingletonImageLoader
+import coil3.request.ImageRequest
 import io.getstream.chat.android.ui.common.images.StreamImageLoaderFactory
 
 /**
@@ -43,6 +46,62 @@
     }
 }
 
+public object CustomCoilImageLoaderFactory : StreamCoilImageLoaderFactory {
+    /**
+     * Loads images in the app, with our default settings.
+     */
+    private var imageLoader: ImageLoader? = null
+
+    /**
+     * Provides an [ImageLoader] using our custom [ImageLoaderFactory].
+     */
+    private var imageLoaderFactory: SingletonImageLoader.Factory? = null
+
+    /**
+     * Returns either the currently available [ImageLoader] or builds a new one.
+     *
+     * @param context - The [Context] to build the [ImageLoader] with.
+     * @return [ImageLoader] that loads images in the app.
+     */
+    override fun imageLoader(context: Context): ImageLoader = imageLoader ?: newImageLoader(context)
+
+    /**
+     * Builds a new [ImageLoader] using the given Android [Context]. If the loader already exists, we return it.
+     *
+     * @param context - The [Context] to build the [ImageLoader] with.
+     * @return [ImageLoader] that loads images in the app.
+     */
+    @Synchronized
+    private fun newImageLoader(context: Context): ImageLoader {
+        imageLoader?.let { return it }
+
+        val imageLoaderFactory = imageLoaderFactory ?: newImageLoaderFactory()
+        return imageLoaderFactory.newImageLoader(context).apply {
+            imageLoader = this
+        }
+    }
+
+    /**
+     * Uses Android [Context] to build a new [StreamImageLoaderFactory].
+     *
+     * @return [ImageLoaderFactory] that loads all the images in the app.
+     */
+    private fun newImageLoaderFactory(): SingletonImageLoader.Factory {
+        return StreamImageLoaderFactory(
+            builder = {
+                this.eventListener(object : EventListener() {
+                    override fun onStart(request: ImageRequest) {
+                        Log.d("StreamImageLoader", "onStart [custom loader]")
+                        super.onStart(request)
+                    }
+                })
+            },
+        ).apply {
+            imageLoaderFactory = this
+        }
+    }
+}
+
 /**
  * Provides a custom image loader that uses the [StreamImageLoaderFactory] to build the default loading settings.
  *
@@ -90,7 +149,16 @@
      * @return [ImageLoaderFactory] that loads all the images in the app.
      */
     private fun newImageLoaderFactory(): SingletonImageLoader.Factory {
-        return StreamImageLoaderFactory().apply {
+        return StreamImageLoaderFactory(
+            builder = {
+                this.eventListener(object : EventListener() {
+                    override fun onStart(request: ImageRequest) {
+                        Log.d("StreamImageLoader", "onStart [default loader]")
+                        super.onStart(request)
+                    }
+                })
+            },
+        ).apply {
             imageLoaderFactory = this
         }
     }

Copy link
Contributor

github-actions bot commented Apr 18, 2025

SDK Size Comparison 📏

SDK Before After Difference Status
stream-chat-android-client 3.16 MB 3.16 MB 0.00 MB 🟢
stream-chat-android-offline 3.38 MB 3.38 MB 0.00 MB 🟢
stream-chat-android-ui-components 7.89 MB 7.89 MB 0.00 MB 🟢
stream-chat-android-compose 9.94 MB 9.94 MB 0.00 MB 🟢

@VelikovPetar VelikovPetar marked this pull request as ready for review April 18, 2025 12:08
@VelikovPetar VelikovPetar requested a review from a team as a code owner April 18, 2025 12:08
PetarVelikov added 3 commits April 21, 2025 12:43
# Conflicts:
#	stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt
# Conflicts:
#	stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt
#	stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
38.1% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

Comment on lines +426 to +430
val imageLoader = LocalStreamImageLoader.current
LaunchedEffect(imageLoader) {
MediaGalleryInjector.install(imageLoader)
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just thinking out loud... this installation happens on every content item composition. Shouldn't it be moved higher in the chain instead? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My idea was keep it close to where the MediaGalleryPreviewContract is instantiated, so that the whole MediaGallery setup is done in the same place. We can of course move it higher, but I was worried that we might lose context on what is done where. Where do you think it would be more appropriate to do this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering this composable might be called many times when it's a multiple attachment, maybe installing it in MediaAttachmentContent
WDYT?
Given you have touched more in this area than me, feel free to keep it this way or change it if you think it makes more sense.

* limitations under the License.
*/

package io.getstream.chat.android.compose.ui.attachments.preview.internal
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm my opinion, no need to have it inside a internal package.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a pattern that is used in some places - placing the internal-only classes/methods in a separate internal package. I can of course move it, but maybe we should agree on a global level how to proceed with the structure of such internal methods/classes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. Let's bring this topic to Slack 👍🏻

*
* @param imageLoader The [ImageLoader] instance to set.
*/
internal fun install(imageLoader: ImageLoader) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The object is already internal. I believe internal is not needed here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will update it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants