Skip to content

Commit

Permalink
[AND-313] Fix the Avatar loading (#5625)
Browse files Browse the repository at this point in the history
* Stop using StreamImage in Avatars. Hence:
- Fix the fading issue.
- Stop using preview avatar so that we can have snapshot tests doing the same as in production.

* Fix the InitialsAvatar font size when using on poll components

* Update snapshots as now we are using initials as in production, instead of preview avatars

* CHANGELOG

* Add missed testTag

* Add missed testTag

* Introduce StreamAsyncImage

* Missed testTag

* Move StreamAsyncImage to ImageUtils.kt
  • Loading branch information
andremion authored Feb 17, 2025
1 parent e16af00 commit 78ffd80
Show file tree
Hide file tree
Showing 26 changed files with 131 additions and 98 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@

## stream-chat-android-compose
### 🐞 Fixed
- Fading issue with the `Avatar`. [#5625](https://github.com/GetStream/stream-chat-android/pull/5625)
- Fix `InitialsAvatar` font size in Poll components. [#5625](https://github.com/GetStream/stream-chat-android/pull/5625)

### ⬆️ Improved
- Autofocus the input fields in the poll creation screen. [#5629](https://github.com/GetStream/stream-chat-android/pull/5629)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,22 @@

package io.getstream.chat.android.compose.ui.components.avatar

import androidx.compose.foundation.clickable
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import com.skydoves.landscapist.ImageOptions
import com.skydoves.landscapist.animation.crossfade.CrossfadePlugin
import com.skydoves.landscapist.components.rememberImageComponent
import com.skydoves.landscapist.placeholder.placeholder.PlaceholderPlugin
import io.getstream.chat.android.compose.R
import coil.compose.AsyncImagePainter
import io.getstream.chat.android.compose.ui.theme.ChatTheme
import io.getstream.chat.android.compose.ui.util.StreamImage
import io.getstream.chat.android.compose.ui.util.StreamAsyncImage
import io.getstream.chat.android.ui.common.images.resizing.applyStreamCdnImageResizingIfEnabled

/**
Expand Down Expand Up @@ -64,59 +60,39 @@ public fun Avatar(
initialsAvatarOffset: DpOffset = DpOffset(0.dp, 0.dp),
onClick: (() -> Unit)? = null,
) {
if (imageUrl.isBlank()) {
InitialsAvatar(
modifier = modifier,
initials = initials,
shape = shape,
textStyle = textStyle,
onClick = onClick,
avatarOffset = initialsAvatarOffset,
)
return
}

val cdnImageResizing = ChatTheme.streamCdnImageResizing
val clickableModifier = if (onClick != null) {
Modifier.clickable { onClick() }
} else {
Modifier
}
StreamImage(
modifier = modifier
.testTag("Stream_QuotedMessageAuthorAvatar")
.clip(shape)
.then(clickableModifier),
data = { imageUrl.applyStreamCdnImageResizingIfEnabled(cdnImageResizing) },
loading = {
if (placeholderPainter != null) {
ImageAvatar(
modifier = modifier,
shape = shape,
painter = placeholderPainter,
contentDescription = contentDescription,
onClick = onClick,
)
val streamCdnImageResizing = ChatTheme.streamCdnImageResizing
val data = remember { imageUrl.applyStreamCdnImageResizingIfEnabled(streamCdnImageResizing) }
StreamAsyncImage(
data = data,
modifier = modifier.testTag("Stream_QuotedMessageAuthorAvatar"),
content = { state ->
val targetPainter = when (state) {
is AsyncImagePainter.State.Empty -> placeholderPainter
is AsyncImagePainter.State.Loading -> placeholderPainter
is AsyncImagePainter.State.Success -> state.painter
is AsyncImagePainter.State.Error -> null
}
},
failure = {
InitialsAvatar(
modifier = modifier,
initials = initials,
shape = shape,
textStyle = textStyle,
onClick = onClick,
avatarOffset = initialsAvatarOffset,
)
},
component = rememberImageComponent {
if (placeholderPainter == null) {
+PlaceholderPlugin.Loading(painterResource(id = R.drawable.stream_compose_preview_avatar))
Crossfade(targetState = targetPainter) { painter ->
if (painter == null) {
InitialsAvatar(
modifier = Modifier.fillMaxSize(),
initials = initials,
shape = shape,
textStyle = textStyle,
onClick = onClick,
avatarOffset = initialsAvatarOffset,
)
} else {
ImageAvatar(
modifier = Modifier.fillMaxSize(),
shape = shape,
painter = painter,
contentDescription = contentDescription,
onClick = onClick,
)
}
}
+CrossfadePlugin()
},
previewPlaceholder = painterResource(id = R.drawable.stream_compose_preview_avatar),
imageOptions = ImageOptions(contentDescription = contentDescription),
)
}

Expand All @@ -130,7 +106,6 @@ public fun Avatar(
private fun AvatarWithImageUrlPreview() {
AvatarPreview(
imageUrl = "https://sample.com/image.png",
initials = "JC",
)
}

Expand All @@ -144,26 +119,23 @@ private fun AvatarWithImageUrlPreview() {
private fun AvatarWithoutImageUrlPreview() {
AvatarPreview(
imageUrl = "",
initials = "JC",
)
}

/**
* Shows [Avatar] preview for the provided parameters.
*
* @param imageUrl The image URL to load.
* @param initials The fallback initials.
*/
@Composable
private fun AvatarPreview(
imageUrl: String,
initials: String,
) {
ChatTheme {
Avatar(
modifier = Modifier.size(36.dp),
imageUrl = imageUrl,
initials = initials,
initials = "JC",
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public fun UserAvatarRow(
size: Dp = 20.dp,
offset: Int = 7,
shape: Shape = ChatTheme.shapes.avatar,
textStyle: TextStyle = ChatTheme.typography.title3Bold,
textStyle: TextStyle = ChatTheme.typography.captionBold,
contentDescription: String? = null,
initialsAvatarOffset: DpOffset = DpOffset(0.dp, 0.dp),
onClick: (() -> Unit)? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import io.getstream.chat.android.compose.ui.theme.ChatTheme
Expand Down Expand Up @@ -114,6 +115,7 @@ internal fun DefaultQuotedMessageLeadingContent(
if (!message.isMine(currentUser)) {
ChatTheme.componentFactory.Avatar(
modifier = Modifier
.testTag("Stream_QuotedMessageAuthorAvatar")
.padding(start = 2.dp)
.size(24.dp),
imageUrl = message.user.image,
Expand Down Expand Up @@ -147,6 +149,7 @@ internal fun DefaultQuotedMessageTrailingContent(

ChatTheme.componentFactory.Avatar(
modifier = Modifier
.testTag("Stream_QuotedMessageAuthorAvatar")
.padding(start = 2.dp)
.size(24.dp),
imageUrl = message.user.image,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ internal fun PollAnswersItem(
imageUrl = user.image,
initials = user.initials,
shape = ChatTheme.shapes.avatar,
textStyle = ChatTheme.typography.title3Bold,
textStyle = ChatTheme.typography.captionBold,
placeholderPainter = null,
contentDescription = user.name,
initialsAvatarOffset = DpOffset.Zero,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ private fun PollVoteItem(vote: Vote) {
imageUrl = user.image,
initials = user.initials,
shape = ChatTheme.shapes.avatar,
textStyle = ChatTheme.typography.title3Bold,
textStyle = ChatTheme.typography.captionBold,
placeholderPainter = null,
contentDescription = user.name,
initialsAvatarOffset = DpOffset.Zero,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,14 @@ package io.getstream.chat.android.compose.ui.util

import android.content.Context
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Brush
Expand All @@ -29,11 +34,13 @@ import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.LayoutDirection
import coil.compose.AsyncImagePainter
import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest
import coil.size.Size
import com.skydoves.landscapist.ImageOptions
import com.skydoves.landscapist.coil.CoilImage
import com.skydoves.landscapist.coil.CoilImageState
Expand Down Expand Up @@ -84,6 +91,77 @@ public fun Modifier.mirrorRtl(layoutDirection: LayoutDirection): Modifier {
)
}

/**
* Displays an image asynchronously using `Coil` and the [LocalStreamImageLoader].
* It transforms the image URL and provides headers before loading the image.
*
* @param data The data to load the image from. Can be a URL, URI, resource ID, etc.
* @param modifier Modifier for styling.
* @param contentScale The scale to be used for the content.
* @param content A composable function that defines the content to be displayed based on the image loading state.
*
* @see ImageAssetTransformer
* @see ImageHeadersProvider
*/
@Composable
internal fun StreamAsyncImage(
data: Any?,
modifier: Modifier = Modifier,
contentScale: ContentScale = ContentScale.Fit,
content: @Composable BoxScope.(state: AsyncImagePainter.State) -> Unit,
) {
StreamAsyncImage(
imageRequest = ImageRequest.Builder(LocalContext.current)
.data(data)
.build(),
modifier = modifier,
contentScale = contentScale,
content = content,
)
}

/**
* Displays an image asynchronously using `Coil` and the [LocalStreamImageLoader].
* It transforms the image URL and provides headers before loading the image.
*
* @see ImageAssetTransformer
* @see ImageHeadersProvider
*
* @param imageRequest The request to load the image.
* @param modifier Modifier for styling.
* @param contentScale The scale to be used for the content.
* @param content A composable function that defines the content to be displayed based on the image loading state.
*/
@Composable
internal fun StreamAsyncImage(
imageRequest: ImageRequest,
modifier: Modifier = Modifier,
contentScale: ContentScale = ContentScale.Fit,
content: @Composable BoxScope.(state: AsyncImagePainter.State) -> Unit,
) {
var imageSize by remember { mutableStateOf(Size.ORIGINAL) }
Box(
modifier = modifier.onSizeChanged { size -> imageSize = Size(size.width, size.height) },
) {
if (imageSize == Size.ORIGINAL) {
content(AsyncImagePainter.State.Empty)
} else {
val context = LocalContext.current
val imageAssetTransformer = ChatTheme.streamImageAssetTransformer
val imageHeaderProvider = ChatTheme.streamImageHeadersProvider
val asyncImagePainter = rememberAsyncImagePainter(
model = imageRequest
.convertUrl(context, imageAssetTransformer)
.provideHeaders(context, imageHeaderProvider)
.size(context, imageSize),
imageLoader = LocalStreamImageLoader.current,
contentScale = contentScale,
)
content(asyncImagePainter.state)
}
}
}

/**
* Wrapper around the [CoilImage] that plugs in our [LocalStreamImageLoader] singleton
* that can be used to customize all image loading requests, like adding headers, interceptors and similar.
Expand Down Expand Up @@ -351,6 +429,15 @@ private fun ImageRequest.provideHeaders(
}
.build()

/**
* Set the [Size] as a new build of the [ImageRequest].
*/
private fun ImageRequest.size(context: Context, size: Size): ImageRequest = run {
newBuilder(context)
.size(size)
.build()
}

/**
* Used to change a parameter set on Coil requests in order
* to force Coil into retrying a request.
Expand Down

This file was deleted.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 78ffd80

Please sign in to comment.