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

fix: blinking placeholder avatar on profile screen [WPB-3917] #2184

Merged
merged 6 commits into from
Sep 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,7 +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 @@ -56,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 @@ -76,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 @@ -87,29 +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 = remember(editableState) {
Clickable(
enabled = editableState is EditableState.IsEditable,
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,
)
}
[email protected](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 @@ -119,6 +148,7 @@ fun UserProfileInfo(
Modifier
.fillMaxWidth()
.wrapContentHeight()
.animateContentSize()
) {
val (userDescription, editButton, teamDescription) = createRefs()

Expand All @@ -135,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 @@ -95,7 +95,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 @@ -155,7 +155,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
Loading