diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/ProfilePictureFetcher.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/ProfilePictureFetcher.kt index 340906c5d..cc7c4c3d3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/ProfilePictureFetcher.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/ProfilePictureFetcher.kt @@ -24,7 +24,6 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import androidx.compose.runtime.Stable import coil3.ImageLoader -import coil3.Uri import coil3.annotation.ExperimentalCoilApi import coil3.asImage import coil3.decode.DataSource @@ -39,7 +38,7 @@ import coil3.network.ConnectivityChecker import coil3.network.NetworkFetcher import coil3.network.okhttp.asNetworkClient import coil3.request.Options -import com.vitorpamplona.amethyst.commons.ui.components.PROFILE_PIC_SCHEME +import com.vitorpamplona.amethyst.commons.ui.components.ProfilePictureUrl import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import okhttp3.Call @@ -48,7 +47,8 @@ import kotlin.coroutines.cancellation.CancellationException /** * Coil Fetcher that serves pre-resized profile picture thumbnails from a dedicated disk cache. * - * URLs are wrapped as `profilepic://https://example.com/pic.jpg` by the avatar composables. + * Composables pass [ProfilePictureUrl] as the model to AsyncImage. Coil routes to this + * fetcher by type — no string concatenation or scheme parsing needed. * * Flow: * - Thumbnail cache hit: returns the tiny ~5KB JPEG directly. No network, no large decode. @@ -56,7 +56,7 @@ import kotlin.coroutines.cancellation.CancellationException * pipeline (zero overhead). In the background, reads the file from Coil's disk cache * after download and generates a thumbnail for next time. * - * The zoomable full-screen dialog uses the raw URL (without profilepic:// prefix), + * The zoomable full-screen dialog uses the raw URL string (not wrapped in ProfilePictureUrl), * which goes through the normal Coil pipeline for full-resolution display. */ @Stable @@ -164,39 +164,26 @@ class ProfilePictureFetcher( return inSampleSize } - companion object { - const val SCHEME = PROFILE_PIC_SCHEME - - fun extractOriginalUrl(data: Uri): String { - // profilepic://https://example.com/pic.jpg → https://example.com/pic.jpg - val full = data.toString() - return full.removePrefix("$SCHEME://") - } - } - @OptIn(ExperimentalCoilApi::class) class Factory( private val thumbnailCache: ThumbnailDiskCache, private val networkClient: (url: String) -> Call.Factory, private val backgroundScope: CoroutineScope, - ) : Fetcher.Factory { + ) : Fetcher.Factory { private val connectivityCheckerLazy = singleParameterLazy(::ConnectivityChecker) override fun create( - data: Uri, + data: ProfilePictureUrl, options: Options, imageLoader: ImageLoader, - ): Fetcher? { - if (data.scheme != SCHEME) return null - - val originalUrl = extractOriginalUrl(data) + ): Fetcher { val diskCacheLazy = lazy { imageLoader.diskCache } val netFetcher = NetworkFetcher( - url = originalUrl, + url = data.url, options = options, - networkClient = lazy { networkClient(originalUrl).asNetworkClient() }, + networkClient = lazy { networkClient(data.url).asNetworkClient() }, diskCache = diskCacheLazy, cacheStrategy = lazy { CacheStrategy.DEFAULT }, connectivityChecker = lazy { connectivityCheckerLazy.get(options.context) }, @@ -204,7 +191,7 @@ class ProfilePictureFetcher( ) return ProfilePictureFetcher( - originalUrl, + data.url, options, thumbnailCache, netFetcher, @@ -214,15 +201,10 @@ class ProfilePictureFetcher( } } - object BKeyer : Keyer { + object BKeyer : Keyer { override fun key( - data: Uri, + data: ProfilePictureUrl, options: Options, - ): String? = - if (data.scheme == SCHEME) { - "profilepic_thumb_${extractOriginalUrl(data)}" - } else { - null - } + ): String = "profilepic_thumb_${data.url}" } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt index 39aadfd1e..936429f36 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt @@ -35,7 +35,7 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.layout.ContentScale import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.commons.robohash.CachedRobohash -import com.vitorpamplona.amethyst.commons.ui.components.PROFILE_PIC_SCHEME +import com.vitorpamplona.amethyst.commons.ui.components.ProfilePictureUrl import com.vitorpamplona.amethyst.ui.theme.isLight import com.vitorpamplona.amethyst.ui.theme.onBackgroundColorFilter @@ -93,7 +93,7 @@ fun RobohashFallbackAsyncImage( } AsyncImage( - model = "$PROFILE_PIC_SCHEME://$model", + model = ProfilePictureUrl(model), contentDescription = contentDescription, modifier = modifier, placeholder = painter, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UserAvatar.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UserAvatar.kt index d1fbd9d1f..e48d70b3a 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UserAvatar.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UserAvatar.kt @@ -41,7 +41,15 @@ import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.commons.robohash.CachedRobohash import com.vitorpamplona.amethyst.commons.ui.theme.isLight -const val PROFILE_PIC_SCHEME = "profilepic" +/** + * Wrapper class for profile picture URLs that signals Coil to use the thumbnail + * disk cache fetcher instead of the normal network pipeline. Passing this as the + * model to AsyncImage avoids string concatenation/parsing and lets Coil route + * by type via Fetcher.Factory. + */ +data class ProfilePictureUrl( + val url: String, +) /** * Shared avatar component that displays a user's profile picture with Robohash fallback. @@ -73,9 +81,9 @@ fun UserAvatar( .clip(shape = CircleShape) } - val imageModel = + val imageModel: Any? = if (pictureUrl != null && useThumbnailCache) { - "$PROFILE_PIC_SCHEME://$pictureUrl" + ProfilePictureUrl(pictureUrl) } else { pictureUrl }