Skip to content

Commit

Permalink
fix: blinking placeholder avatar on profile screen [WPB-3917] (#2201)
Browse files Browse the repository at this point in the history
Co-authored-by: Michał Saleniuk <30429749+saleniuk@users.noreply.github.com>
Co-authored-by: Michał Saleniuk <saleniuk@gmail.com>
  • Loading branch information
3 people committed Sep 8, 2023
1 parent 4cfebfd commit be44e32
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 47 deletions.
5 changes: 4 additions & 1 deletion app/src/main/kotlin/com/wire/android/model/ImageAsset.kt
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ sealed class ImageAsset(private val imageLoader: WireSessionImageLoader) {
}

@Composable
fun paint(fallbackData: Any? = null) = imageLoader.paint(asset = this, fallbackData)
fun paint(
fallbackData: Any? = null,
withCrossfadeAnimation: Boolean = false
) = imageLoader.paint(asset = this, fallbackData = fallbackData, withCrossfadeAnimation = withCrossfadeAnimation)
}

fun String.parseIntoPrivateImageAsset(
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/kotlin/com/wire/android/model/UserAvatarData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@

package com.wire.android.model

import androidx.compose.runtime.Stable
import com.wire.android.ui.home.conversationslist.model.Membership
import com.wire.kalium.logic.data.user.ConnectionState
import com.wire.kalium.logic.data.user.UserAvailabilityStatus

@Stable
data class UserAvatarData(
val asset: ImageAsset.UserAvatarAsset? = null,
val availabilityStatus: UserAvailabilityStatus = UserAvailabilityStatus.NONE,
Expand Down
44 changes: 33 additions & 11 deletions app/src/main/kotlin/com/wire/android/ui/common/UserProfileAvatar.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalInspectionMode
Expand All @@ -54,10 +56,10 @@ fun UserProfileAvatar(
avatarData: UserAvatarData = UserAvatarData(),
size: Dp = MaterialTheme.wireDimensions.userAvatarDefaultSize,
modifier: Modifier = Modifier,
clickable: Clickable? = null
clickable: Clickable? = null,
showPlaceholderIfNoAsset: Boolean = true,
withCrossfadeAnimation: Boolean = false,
) {
val painter = painter(avatarData)

Box(
contentAlignment = Alignment.Center,
modifier = modifier
Expand All @@ -66,6 +68,7 @@ fun UserProfileAvatar(
.clickable(clickable)
.padding(MaterialTheme.wireDimensions.userAvatarClickablePadding)
) {
val painter = painter(avatarData, showPlaceholderIfNoAsset, withCrossfadeAnimation)
Image(
painter = painter,
contentDescription = stringResource(R.string.content_description_user_avatar),
Expand All @@ -89,20 +92,39 @@ fun UserProfileAvatar(
* @see [painter] https://developer.android.com/jetpack/compose/tooling
*/
@Composable
private fun painter(data: UserAvatarData): Painter = if (data.connectionState == ConnectionState.BLOCKED) {
painterResource(id = R.drawable.ic_blocked_user_avatar)
} else if (LocalInspectionMode.current || data.asset == null) {
getDefaultAvatar(membership = data.membership)
} else {
data.asset.paint(R.drawable.ic_default_user_avatar)
private fun painter(
data: UserAvatarData,
showPlaceholderIfNoAsset: Boolean = true,
withCrossfadeAnimation: Boolean = false,
): Painter = when {
LocalInspectionMode.current -> {
getDefaultAvatar(membership = data.membership)
}

data.connectionState == ConnectionState.BLOCKED -> {
painterResource(id = R.drawable.ic_blocked_user_avatar)
}

data.asset == null -> {
if (showPlaceholderIfNoAsset) getDefaultAvatar(membership = data.membership)
else ColorPainter(Color.Transparent)
}

else -> {
data.asset.paint(getDefaultAvatarResourceId(membership = data.membership), withCrossfadeAnimation)
}
}

@Composable
private fun getDefaultAvatar(membership: Membership): Painter =
painterResource(id = getDefaultAvatarResourceId(membership))

@Composable
private fun getDefaultAvatarResourceId(membership: Membership): Int =
if (membership == Membership.Service) {
painterResource(id = R.drawable.ic_default_service_avatar)
R.drawable.ic_default_service_avatar
} else {
painterResource(id = R.drawable.ic_default_user_avatar)
R.drawable.ic_default_user_avatar
}

@Preview
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,16 @@

package com.wire.android.ui.userprofile.common

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.CircleShape
Expand All @@ -35,6 +39,10 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
Expand All @@ -55,13 +63,15 @@ import com.wire.android.ui.common.dimensions
import com.wire.android.ui.common.progress.WireCircularProgressIndicator
import com.wire.android.ui.home.conversationslist.model.Membership
import com.wire.android.ui.theme.wireColorScheme
import com.wire.android.ui.theme.wireDimensions
import com.wire.android.ui.theme.wireTypography
import com.wire.android.util.debug.LocalFeatureVisibilityFlags
import com.wire.android.util.ifNotEmpty
import com.wire.android.util.ui.UIText
import com.wire.kalium.logic.data.user.ConnectionState
import com.wire.kalium.logic.data.user.UserId
import kotlinx.coroutines.delay
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds

@Composable
fun UserProfileInfo(
Expand All @@ -75,7 +85,8 @@ fun UserProfileInfo(
onUserProfileClick: (() -> Unit)? = null,
editableState: EditableState,
modifier: Modifier = Modifier,
connection: ConnectionState = ConnectionState.ACCEPTED
connection: ConnectionState = ConnectionState.ACCEPTED,
delayToShowPlaceholderIfNoAsset: Duration = 200.milliseconds,
) {
Column(
horizontalAlignment = CenterHorizontally,
Expand All @@ -86,27 +97,48 @@ fun UserProfileInfo(
.padding(top = dimensions().spacing16x)
) {
Box(contentAlignment = Alignment.Center) {
UserProfileAvatar(
size = dimensions().userAvatarDefaultBigSize,
avatarData = UserAvatarData(
asset = avatarAsset,
connectionState = connection,
membership = membership
),
clickable = Clickable(
enabled = true,
clickBlockParams = ClickBlockParams(blockWhenSyncing = true, blockWhenConnecting = true),
) { onUserProfileClick?.invoke() }
val userAvatarData = UserAvatarData(
asset = avatarAsset,
connectionState = connection,
membership = membership
)
if (isLoading) {
val showPlaceholderIfNoAsset = remember { mutableStateOf(!delayToShowPlaceholderIfNoAsset.isPositive()) }
val currentAssetIsNull = rememberUpdatedState(avatarAsset == null)
if (delayToShowPlaceholderIfNoAsset.isPositive()) {
LaunchedEffect(Unit) {
delay(delayToShowPlaceholderIfNoAsset)
showPlaceholderIfNoAsset.value = currentAssetIsNull.value // show placeholder if there is still no proper avatar data
}
}
Crossfade(
targetState = userAvatarData to showPlaceholderIfNoAsset.value,
label = "UserProfileInfoAvatar"
) { (userAvatarData, showPlaceholderIfNoAsset) ->
UserProfileAvatar(
size = dimensions().userAvatarDefaultBigSize,
avatarData = userAvatarData,
clickable = remember(editableState) {
Clickable(
enabled = editableState is EditableState.IsEditable,
clickBlockParams = ClickBlockParams(blockWhenSyncing = true, blockWhenConnecting = true),
) { onUserProfileClick?.invoke() }
},
showPlaceholderIfNoAsset = showPlaceholderIfNoAsset,
withCrossfadeAnimation = true,
)
}
this@Column.AnimatedVisibility(visible = isLoading) {
Box(
Modifier
.padding(MaterialTheme.wireDimensions.userAvatarClickablePadding)
.padding(dimensions().userAvatarClickablePadding)
.size(dimensions().userAvatarDefaultBigSize)
.clip(CircleShape)
.background(MaterialTheme.wireColorScheme.onBackground.copy(alpha = 0.7f))
.background(MaterialTheme.wireColorScheme.background.copy(alpha = 0.6f))
) {
WireCircularProgressIndicator(
progressColor = MaterialTheme.wireColorScheme.surface,
size = dimensions().spacing32x,
strokeWidth = dimensions().spacing4x,
progressColor = MaterialTheme.wireColorScheme.onBackground,
modifier = Modifier.align(Alignment.Center)
)
}
Expand All @@ -116,6 +148,7 @@ fun UserProfileInfo(
Modifier
.fillMaxWidth()
.wrapContentHeight()
.animateContentSize()
) {
val (userDescription, editButton, teamDescription) = createRefs()

Expand All @@ -132,7 +165,10 @@ fun UserProfileInfo(
}
) {
Text(
text = fullName.ifBlank { UIText.StringResource(R.string.username_unavailable_label).asString() },
text = fullName.ifBlank {
if (isLoading) ""
else UIText.StringResource(R.string.username_unavailable_label).asString()
},
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = MaterialTheme.wireTypography.title02,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.wire.android.R
import com.wire.android.ui.common.dimensions
import com.wire.android.ui.common.spacers.VerticalSpace
import com.wire.android.ui.home.conversationslist.model.Membership
import com.wire.android.ui.theme.wireColorScheme
import com.wire.android.ui.theme.wireTypography
Expand Down Expand Up @@ -73,6 +74,7 @@ fun OtherUserConnectionStatusInfo(connectionStatus: ConnectionState, membership:
color = MaterialTheme.wireColorScheme.labelText,
style = MaterialTheme.wireTypography.body01
)
VerticalSpace.x24()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ import com.wire.android.ui.common.dialogs.UnblockUserDialogContent
import com.wire.android.ui.common.dialogs.UnblockUserDialogState
import com.wire.android.ui.common.dimensions
import com.wire.android.ui.common.snackbar.SwipeDismissSnackbarHost
import com.wire.android.ui.common.spacers.VerticalSpace.x24
import com.wire.android.ui.common.topBarElevation
import com.wire.android.ui.common.topappbar.NavigationIconType
import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar
Expand Down Expand Up @@ -348,21 +347,20 @@ private fun TopBarHeader(
)
}

@SuppressLint("UnusedCrossfadeTargetStateParameter")
@Composable
private fun TopBarCollapsing(state: OtherUserProfileState) {
Crossfade(targetState = state.isDataLoading, label = "OtherUserProfileScreenTopBarCollapsing") {
Crossfade(targetState = state, label = "OtherUserProfileScreenTopBarCollapsing") { targetState ->
UserProfileInfo(
userId = state.userId,
isLoading = state.isAvatarLoading,
avatarAsset = state.userAvatarAsset,
fullName = state.fullName,
userName = state.userName,
teamName = state.teamName,
membership = state.membership,
userId = targetState.userId,
isLoading = targetState.isAvatarLoading,
avatarAsset = targetState.userAvatarAsset,
fullName = targetState.fullName,
userName = targetState.userName,
teamName = targetState.teamName,
membership = targetState.membership,
editableState = EditableState.NotEditable,
modifier = Modifier.padding(bottom = dimensions().spacing16x),
connection = state.connectionState
connection = targetState.connectionState
)
}
}
Expand Down Expand Up @@ -414,8 +412,9 @@ private fun Content(

Crossfade(targetState = tabItems to state, label = "OtherUserProfile") { (tabItems, state) ->
Column {
OtherUserConnectionStatusInfo(state.connectionState, state.membership)
x24()
if (!state.isDataLoading) {
OtherUserConnectionStatusInfo(state.connectionState, state.membership)
}
when {
state.isDataLoading || state.botService != null -> Box {} // no content visible while loading
state.connectionState == ConnectionState.ACCEPTED -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ class SelfUserProfileViewModel @Inject constructor(
private val notificationManager: WireNotificationManager
) : ViewModel() {

var userProfileState by mutableStateOf(SelfUserProfileState(selfUserId))
var userProfileState by mutableStateOf(SelfUserProfileState(userId = selfUserId, isAvatarLoading = true))
private set

private lateinit var establishedCallsList: StateFlow<List<Call>>
Expand Down Expand Up @@ -154,7 +154,8 @@ class SelfUserProfileViewModel @Inject constructor(
userName = handle.orEmpty(),
teamName = selfTeam?.name,
otherAccounts = otherAccounts,
avatarAsset = userProfileState.avatarAsset
avatarAsset = userProfileState.avatarAsset,
isAvatarLoading = false,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ class WireSessionImageLoader(
@Composable
fun paint(
asset: ImageAsset?,
fallbackData: Any? = null
fallbackData: Any? = null,
withCrossfadeAnimation: Boolean = false,
): Painter {
var retryHash by remember { mutableStateOf(0) }
val exponentialDurationHelper = remember { ExponentialDurationHelperImpl(MIN_RETRY_DELAY, MAX_RETRY_DELAY) }
Expand All @@ -90,9 +91,9 @@ class WireSessionImageLoader(
value = retryHash,
memoryCacheKey = null
)
.crossfade(withCrossfadeAnimation)
.build(),
error = (fallbackData as? Int)?.let { painterResource(id = it) },
placeholder = (fallbackData as? Int)?.let { painterResource(id = it) },
imageLoader = coilImageLoader
)

Expand Down

0 comments on commit be44e32

Please sign in to comment.