Skip to content

Commit 4c4148b

Browse files
authored
android: fix issue where default avatar wasn't shown (#558)
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 18ca09d commit 4c4148b

File tree

1 file changed

+47
-37
lines changed
  • android/src/main/java/com/tailscale/ipn/ui/view

1 file changed

+47
-37
lines changed

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

Lines changed: 47 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -43,43 +43,53 @@ fun Avatar(
4343
var isFocused = remember { mutableStateOf(false) }
4444
val focusManager = LocalFocusManager.current
4545

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

0 commit comments

Comments
 (0)