Skip to content

Commit 4e70a1f

Browse files
committed
android: fix issue where default avatar wasn't shown
Always render the default icon first so that if the profile picture is not loaded or has an issue, the default is shown. Fixes tailscale/corp#24217 Signed-off-by: kari-ts <[email protected]>
1 parent ba306bf commit 4e70a1f

File tree

1 file changed

+36
-44
lines changed
  • android/src/main/java/com/tailscale/ipn/ui/view

1 file changed

+36
-44
lines changed

android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt

Lines changed: 36 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -35,56 +35,48 @@ import com.tailscale.ipn.ui.model.IpnLocal
3535
@OptIn(ExperimentalCoilApi::class)
3636
@Composable
3737
fun Avatar(profile: IpnLocal.LoginProfile?, size: Int = 50, action: (() -> Unit)? = null) {
38-
var isFocused = remember { mutableStateOf(false) }
39-
val focusManager = LocalFocusManager.current
38+
var isFocused = remember { mutableStateOf(false) }
39+
val focusManager = LocalFocusManager.current
4040

41-
// Outer Box for the larger focusable and clickable area
42-
Box(
43-
contentAlignment = Alignment.Center,
44-
modifier = Modifier
45-
.padding(4.dp)
46-
.size((size * 1.5f).dp) // Focusable area is larger than the avatar
47-
.clip(CircleShape) // Ensure both the focus and click area are circular
48-
.background(
49-
if (isFocused.value) MaterialTheme.colorScheme.surface
50-
else Color.Transparent,
51-
)
52-
.onFocusChanged { focusState ->
53-
isFocused.value = focusState.isFocused
54-
}
55-
.focusable() // Make this outer Box focusable (after onFocusChanged)
56-
.clickable(
57-
interactionSource = remember { MutableInteractionSource() },
58-
indication = ripple(bounded = true), // Apply ripple effect inside circular bounds
59-
onClick = {
60-
action?.invoke()
41+
// Outer Box for the larger focusable and clickable area
42+
Box(
43+
contentAlignment = Alignment.Center,
44+
modifier =
45+
Modifier.padding(4.dp)
46+
.size((size * 1.5f).dp) // Focusable area is larger than the avatar
47+
.clip(CircleShape) // Ensure both the focus and click area are circular
48+
.background(
49+
if (isFocused.value) MaterialTheme.colorScheme.surface else Color.Transparent,
50+
)
51+
.onFocusChanged { focusState -> isFocused.value = focusState.isFocused }
52+
.focusable() // Make this outer Box focusable (after onFocusChanged)
53+
.clickable(
54+
interactionSource = remember { MutableInteractionSource() },
55+
indication = ripple(bounded = true), // Apply ripple effect inside circular bounds
56+
onClick = {
57+
action?.invoke()
6158
focusManager.clearFocus() // Clear focus after clicking the avatar
62-
}
63-
)
64-
) {
59+
})) {
6560
// Inner Box to hold the avatar content (Icon or AsyncImage)
6661
Box(
6762
contentAlignment = Alignment.Center,
68-
modifier = Modifier
69-
.size(size.dp)
70-
.clip(CircleShape)
71-
) {
72-
if (profile?.UserProfile?.ProfilePicURL != null) {
63+
modifier = Modifier.size(size.dp).clip(CircleShape)) {
64+
// Always display the default icon as a background layer
65+
Icon(
66+
imageVector = Icons.Default.Person,
67+
contentDescription = stringResource(R.string.settings_title),
68+
modifier =
69+
Modifier.size((size * 0.8f).dp)
70+
.clip(CircleShape) // Icon size slightly smaller than the Box
71+
)
72+
73+
// Overlay the profile picture if available
74+
profile?.UserProfile?.ProfilePicURL?.let { url ->
7375
AsyncImage(
74-
model = profile.UserProfile.ProfilePicURL,
76+
model = url,
7577
modifier = Modifier.size(size.dp).clip(CircleShape),
76-
contentDescription = null
77-
)
78-
} else {
79-
Icon(
80-
imageVector = Icons.Default.Person,
81-
contentDescription = stringResource(R.string.settings_title),
82-
modifier = Modifier
83-
.size((size * 0.8f).dp)
84-
.clip(CircleShape) // Icon size slightly smaller than the Box
85-
)
78+
contentDescription = null)
79+
}
8680
}
87-
}
88-
}
81+
}
8982
}
90-

0 commit comments

Comments
 (0)