feat: add thumbnail disk cache for profile pictures

Profile pictures are displayed at small fixed sizes (18-100dp) but Coil's
disk cache stores full-size originals, causing expensive re-decode on every
memory cache miss. With 60 avatars on screen, this creates significant I/O.

Adds a ProfilePictureFetcher with a dedicated ThumbnailDiskCache that stores
pre-resized 256px JPEG thumbnails (~5-10KB each). On cache hit, reads a tiny
file instead of re-decoding the full original. The zoomable full-screen dialog
bypasses this cache and loads the original via the normal Coil pipeline.

https://claude.ai/code/session_01PBJS3HDrurLP3i5n4tMsCv
This commit is contained in:
Claude
2026-04-05 03:06:52 +00:00
parent 5349121443
commit 9b356d31ba
6 changed files with 331 additions and 3 deletions
@@ -41,6 +41,8 @@ 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"
/**
* Shared avatar component that displays a user's profile picture with Robohash fallback.
*
@@ -51,6 +53,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.isLight
* @param contentDescription Accessibility description
* @param loadProfilePicture Whether to load the profile picture (false = show robohash only)
* @param loadRobohash Whether to generate robohash (false = show generic icon)
* @param useThumbnailCache Whether to wrap the URL with profilepic:// scheme for thumbnail caching
*/
@Composable
fun UserAvatar(
@@ -61,6 +64,7 @@ fun UserAvatar(
contentDescription: String? = null,
loadProfilePicture: Boolean = true,
loadRobohash: Boolean = true,
useThumbnailCache: Boolean = false,
) {
val avatarModifier =
remember(size, modifier) {
@@ -69,7 +73,14 @@ fun UserAvatar(
.clip(shape = CircleShape)
}
if (pictureUrl != null && loadProfilePicture) {
val imageModel =
if (pictureUrl != null && useThumbnailCache) {
"$PROFILE_PIC_SCHEME://$pictureUrl"
} else {
pictureUrl
}
if (imageModel != null && loadProfilePicture) {
// Show profile picture with robohash/icon as fallback
val fallbackPainter =
if (loadRobohash) {
@@ -83,7 +94,7 @@ fun UserAvatar(
}
AsyncImage(
model = pictureUrl,
model = imageModel,
contentDescription = contentDescription,
modifier = avatarModifier,
placeholder = fallbackPainter,