Skip to content
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

[AND-313] Fix the Avatar loading #5625

Merged
merged 11 commits into from
Feb 17, 2025
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.
Loading