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
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,37 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.Text
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import com.flipcash.app.core.navigation.NavBarButton
import com.flipcash.app.core.navigation.NavBarConfig
import com.flipcash.app.theme.FlipcashThemeWrapper
import com.flipcash.core.R
import com.getcode.theme.CodeTheme
import com.getcode.theme.xxl
Expand Down Expand Up @@ -98,7 +112,6 @@ fun NavigationBar(
modifier = buttonModifier,
label = stringResource(R.string.action_wallet),
painter = painterResource(R.drawable.ic_flipcash_balance),
badgeCount = state.notificationUnreadCount,
onClick = { onButtonClick(NavBarButton.Wallet) },
toast = {
AnimatedVisibility(
Expand Down Expand Up @@ -131,8 +144,8 @@ fun NavigationBar(
NavBarButton.Send -> BottomBarAction(
modifier = buttonModifier,
label = stringResource(R.string.action_send),
badgeCount = state.notificationUnreadCount,
painter = painterResource(R.drawable.ic_send_outlined),
badgeCount = 0,
onClick = { onButtonClick(NavBarButton.Send) }
)
}
Expand All @@ -154,7 +167,9 @@ private fun BottomBarAction(
onClick: (() -> Unit)?,
) {
Column(
modifier = modifier.width(IntrinsicSize.Max),
modifier = modifier
.then(if (badgeCount > 0) Modifier.zIndex(1f) else Modifier)
.width(IntrinsicSize.Max),
horizontalAlignment = Alignment.CenterHorizontally,
) {
toast()
Expand All @@ -165,7 +180,6 @@ private fun BottomBarAction(
imageSize = imageSize,
badge = {
Badge(
modifier = Modifier.padding(top = 6.dp, end = 1.dp),
count = badgeCount,
color = CodeTheme.colors.indicator,
enterTransition = scaleIn(
Expand Down Expand Up @@ -195,6 +209,9 @@ private fun BottomBarAction(
badge: @Composable () -> Unit = { },
onClick: (() -> Unit)?,
) {
val maskPadding = 4.dp
var badgeSize by remember { mutableStateOf(IntSize.Zero) }

Layout(
modifier = modifier,
content = {
Expand All @@ -209,6 +226,21 @@ private fun BottomBarAction(
) {
Image(
modifier = Modifier
.graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen }
.drawWithContent {
drawContent()
val bs = badgeSize
if (bs.width > 0 && bs.height > 0) {
val mp = maskPadding.toPx()
val cpTop = contentPadding.calculateTopPadding().toPx()
drawCircle(
color = Color.Black,
radius = bs.height / 2f + mp,
center = Offset(size.width, cpTop),
blendMode = BlendMode.DstOut,
)
}
}
.padding(contentPadding)
.size(imageSize),
painter = painter,
Expand All @@ -222,7 +254,11 @@ private fun BottomBarAction(
)
}

Box(modifier = Modifier.layoutId("badge")) {
Box(
modifier = Modifier
.layoutId("badge")
.onSizeChanged { badgeSize = it }
) {
badge()
}
}
Expand All @@ -233,17 +269,48 @@ private fun BottomBarAction(
val badgePlaceable =
measurables.find { it.layoutId == "badge" }?.measure(constraints)

val maxWidth = widthOrZero(actionPlaceable)
val maxHeight = heightOrZero(actionPlaceable)
val badgeWidth = widthOrZero(badgePlaceable)
val badgeHeight = heightOrZero(badgePlaceable)

val actionWidth = widthOrZero(actionPlaceable)
val actionHeight = heightOrZero(actionPlaceable)

// Position badge so its left circular end is centered on the icon's top-right corner
val imageSizePx = imageSize.roundToPx()
val iconTop = contentPadding.calculateTopPadding().roundToPx()
val iconRight = (actionWidth + imageSizePx) / 2
val badgeX = iconRight - badgeHeight / 2
val badgeY = iconTop - badgeHeight / 2

layout(
width = maxWidth,
height = maxHeight,
width = actionWidth,
height = actionHeight,
) {
actionPlaceable?.placeRelative(0, 0)
badgePlaceable?.placeRelative(
x = maxWidth - widthOrZero(badgePlaceable),
y = -(heightOrZero(badgePlaceable) / 3)
)
badgePlaceable?.placeRelativeWithLayer(x = badgeX, y = badgeY) {
clip = false
}
}
}
}


@Preview
@PreviewWrapper(FlipcashThemeWrapper::class)
@Composable
private fun NavigationBarPreview() {
NavigationBar(
state = NavigationBarState(notificationUnreadCount = 100),
)
}
@Preview
@PreviewWrapper(FlipcashThemeWrapper::class)
@Composable
private fun SendActionPreview() {
BottomBarAction(
painter = painterResource(R.drawable.ic_send_outlined),
label = "Send",
badgeCount = 100,
onClick = null,
)
}
18 changes: 7 additions & 11 deletions apps/flipcash/core/src/main/res/drawable/ic_send_outlined.xml
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="36dp"
android:viewportWidth="36"
android:viewportHeight="36">
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<group>
<clip-path
android:pathData="M0,0h36v36h-36z"/>
android:pathData="M0,0h40v40h-40z"/>
<path
android:pathData="M14.607,20.95L4,16L28.395,7.161L19.556,31.556L14.607,20.95ZM14.607,20.95L18.496,17.061"
android:strokeLineJoin="round"
android:strokeWidth="2.5"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
android:pathData="M20,35L18.395,35.45L19.452,39.218L21.436,35.845L20,35ZM36.667,6.667L38.103,7.512L39.581,5H36.667V6.667ZM4.167,6.667V5H0.289L2.957,7.814L4.167,6.667ZM15.363,18.473L16.968,18.023L16.857,17.626L16.573,17.326L15.363,18.473ZM35.808,9.048L37.266,8.24L35.65,5.325L34.192,6.133L35.808,9.048ZM21.436,35.845L38.103,7.512L35.23,5.822L18.563,34.155L21.436,35.845ZM36.667,5H4.167V8.333H36.667V5ZM2.957,7.814L14.154,19.62L16.573,17.326L5.376,5.52L2.957,7.814ZM13.759,18.924L18.395,35.45L21.605,34.55L16.968,18.023L13.759,18.924ZM16.841,19.56L35.808,9.048L34.192,6.133L15.225,16.644L16.841,19.56Z"
android:fillColor="#ffffff"/>
</group>
</vector>
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,10 @@ internal class SendFlowViewModel @Inject constructor(

combine(
contactCoordinator.state,
stateFlow.map { it.searchState }.distinctUntilChanged().flatMapLatest { snapshotFlow { it.text } },
stateFlow
.map { it.searchState }
.distinctUntilChanged()
.flatMapLatest { snapshotFlow { it.text } },
chatCoordinator.feed,
tokenCoordinator.tokens,
) { contactState, searchText, chatFeed, tokens ->
Expand Down Expand Up @@ -261,7 +264,7 @@ internal class SendFlowViewModel @Inject constructor(
val formattedPhone = phone?.let { phoneUtils.formatNumber(it) }
val displayName = otherMember.userProfile.displayName?.takeIf { it.isNotBlank() }
?: formattedPhone
?: return@mapNotNull null // filter out anonymous chats
?: return@mapNotNull null

val unknown = DeviceContact.unknownContact(
e164 = phone.orEmpty(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,23 @@ class ChatCoordinator @Inject constructor(

val feed: Flow<List<ChatSummary>>
get() = _state.map { state ->
state.feed.map { metadata ->
val selfId = userManager.accountId
state.feed.mapNotNull { metadata ->
// Filter out anonymous chats (DMs where the other member has no name or phone)
val otherMember = metadata.members.firstOrNull { it.userId != selfId }
if (otherMember != null) {
val profile = otherMember.userProfile
val hasIdentity = !profile.displayName.isNullOrBlank() ||
!profile.verifiedPhoneNumber.isNullOrBlank()
if (!hasIdentity) return@mapNotNull null
}

val readPointer = metadata.members
.firstOrNull { it.userId == userManager.accountId }
.firstOrNull { it.userId == selfId }
?.pointers
?.firstOrNull { it.type == PointerType.READ }
?.value ?: 0L

val selfId = userManager.accountId
val unreadCount = metadata.lastMessage?.let { lastMsg ->
if (lastMsg.messageId > readPointer && lastMsg.senderId != selfId) 1 else 0
} ?: 0
Expand Down Expand Up @@ -189,6 +198,10 @@ class ChatCoordinator @Inject constructor(
return runCatching { ChatId(raw.decodeBase58()) }
}

fun observeUnreadConversations(): Flow<Int> {
return feed.map { summaries -> summaries.count { it.unreadCount > 0 } }
}

fun observeMessages(chatId: ChatId): Flow<List<ChatMessage>> {
return messageDataSource.observeMessages(chatId)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class ChatMessageDataSource @Inject constructor(
&& entity.senderIdHex == selfHex
&& entity.pendingClientIdHex == null
) {
entity.copy(pendingClientIdHex = rescuedIds.removeFirst())
entity.copy(pendingClientIdHex = rescuedIds.removeAt(0))
} else entity
}
} else entities
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
Expand Down Expand Up @@ -183,13 +186,22 @@ class RealSessionController @Inject constructor(
.launchIn(scope)

userManager.state
.mapNotNull { it.authState }
.map { it.authState }
.filter { it.isAtLeastRegistered }
.distinctUntilChanged()
.filter { userManager.state.value.flags?.requiresIapForRegistration == true }
.onEach { billingClient.connect() }
.launchIn(scope)

userManager.state
.map { it.authState }
.filter { it.isAtLeastRegistered }
.distinctUntilChanged()
.flatMapLatest { chatCoordinator.observeUnreadConversations() }
.distinctUntilChanged()
.onEach { count -> _state.update { it.copy(notificationUnreadCount = count) } }
.launchIn(scope)

appSettingsCoordinator
.observeValue(AppSettingValue.CameraStartByDefault)
.onEach { autoStart -> _state.update { it.copy(autoStartCamera = autoStart) } }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ object Flipcash2ColorSpec {
val secondary = Color(115, 129, 121)
val secondaryText = Color.White.copy(alpha = 0.5f)
val cashBill = Color(0xFF06450F)
val notification = Color(0xFF009EE7)
val notification = Color(0xFF058AFF)
val trackColor = Color.White.copy(alpha = 0.07f)
val bannerThemed = Color(0xFF252526)
val success = Color(0xFF1AC86A)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ import com.getcode.theme.CodeTheme
fun Badge(
count: Int,
modifier: Modifier = Modifier,
showMoreUnread: Boolean = count > 99,
showMoreUnread: Boolean = count > 100,
color: Color = CodeTheme.colors.brand,
contentColor: Color = Color.White,
textStyle: TextStyle = CodeTheme.typography.textMedium.copy(fontWeight = FontWeight.W700),
textStyle: TextStyle = CodeTheme.typography.caption.copy(fontWeight = FontWeight.SemiBold),
enterTransition: EnterTransition = scaleIn(tween(durationMillis = 300)) + fadeIn(),
exitTransition: ExitTransition = fadeOut() + scaleOut(tween(durationMillis = 300))
) {
Expand All @@ -49,7 +49,7 @@ fun Badge(
contentColor = contentColor,
contentPadding = PaddingValues(
horizontal = CodeTheme.dimens.grid.x1,
vertical = 0.dp
vertical = 3.dp
)
) {
Text(
Expand Down
Loading