Skip to content
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
1 change: 1 addition & 0 deletions apps/flipcash/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ dependencies {
implementation(project(":apps:flipcash:shared:region-selection:core"))
implementation(project(":apps:flipcash:shared:region-selection:ui"))
implementation(project(":apps:flipcash:shared:contacts"))
implementation(project(":apps:flipcash:shared:common-ui"))
implementation(project(":apps:flipcash:shared:notifications"))
implementation(project(":apps:flipcash:shared:onramp:coinbase"))
implementation(project(":apps:flipcash:shared:onramp:deeplinks"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,20 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.flipcash.app.core.AppRoute
import com.flipcash.app.core.LocalUserManager
import dev.chrisbanes.haze.HazeState
import com.flipcash.app.core.navigation.NavBarButton
import com.flipcash.app.core.navigation.asNavBarTab
import com.flipcash.app.core.navigation.destinationRoute
import com.flipcash.app.core.ui.NavigationBar
import com.flipcash.app.core.ui.rememberNavigationBarState
import com.flipcash.app.session.LocalSessionController
import com.flipcash.services.user.AuthState
import com.flipcash.shared.common.ui.ContactAvatar
import com.getcode.manager.BottomBarManager
import com.getcode.navigation.core.CodeNavigator
import com.getcode.theme.CodeTheme
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map

Expand Down Expand Up @@ -72,6 +76,8 @@ internal fun AppNavigationBar(
session?.state?.map { it.tipsUnreadCount } ?: flowOf(0)
}.collectAsStateWithLifecycle(initialValue = 0)

val avatar = rememberProfileAvatar()

Box(
modifier = Modifier
.then(modifier),
Expand All @@ -97,7 +103,30 @@ internal fun AppNavigationBar(
navigator.replaceAll(button.destinationRoute())
},
hazeState = hazeState,
avatar = avatar,
)
}
}
}

/**
* The account's own photo, ready to drop into the You tab, or null when there isn't one.
*
* Gated on Ready like the rest of the profile-driven chrome: a named account restores its cached
* profile before auth completes, so an ungated read would show the previous account's avatar on
* the way in.
*/
@Composable
private fun rememberProfileAvatar(): (@Composable (Modifier) -> Unit)? {
val userManager = LocalUserManager.current
val profile by remember(userManager) {
userManager?.state
?.filter { it.authState is AuthState.Ready }
?.map { it.userProfile }
?: flowOf(null)
}.collectAsStateWithLifecycle(initialValue = null)

val picture = profile?.profilePicture ?: return null
val displayName = profile?.displayName.orEmpty()
return { modifier -> ContactAvatar(picture, displayName, modifier) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
Expand Down Expand Up @@ -87,6 +88,11 @@ fun NavigationBar(
state: NavigationBarState,
onButtonClick: (NavBarButton) -> Unit = {},
hazeState: HazeState? = null,
// The You tab wears the account's own photo in place of its glyph once one is set (node
// 9641:17130). This module sits below the profile layer, so the caller supplies the avatar and
// passes null when there is no photo. The modifier handed back already sizes, clips and fades
// the slot; the avatar only has to fill it.
avatar: (@Composable (Modifier) -> Unit)? = null,
) {
val order = NavBarButton.tabs
if (order.isEmpty()) return
Expand Down Expand Up @@ -174,14 +180,36 @@ fun NavigationBar(
contentAlignment = Alignment.Center,
) {
Box {
Image(
modifier = Modifier
.size(iconSize)
.graphicsLayer { alpha = iconAlpha },
painter = painterResource(button.icon),
colorFilter = ColorFilter.tint(Color.White),
contentDescription = null,
)
if (button == NavBarButton.TipCard && avatar != null) {
Box(
modifier = Modifier
.size(iconSize)
.graphicsLayer { alpha = iconAlpha }
.padding(CodeTheme.dimens.thickBorder),
) {
avatar(Modifier.fillMaxSize().clip(CircleShape))
// Drawn over the photo rather than behind it, so the ring survives
// whatever background the avatar paints for itself.
Box(
modifier = Modifier
.fillMaxSize()
.border(
CodeTheme.dimens.thickBorder,
Color.White,
CircleShape,
),
)
}
} else {
Image(
modifier = Modifier
.size(iconSize)
.graphicsLayer { alpha = iconAlpha },
painter = painterResource(button.icon),
colorFilter = ColorFilter.tint(Color.White),
contentDescription = null,
)
}
// Overlaps the glyph's top-right corner (matching the iOS bar) rather than
// floating detached above it. Full opacity regardless of tab selection —
// the count must stay readable on an unselected tab.
Expand Down
Loading