diff --git a/apps/flipcash/core/src/main/res/drawable/ic_bubble_outline.xml b/apps/flipcash/core/src/main/res/drawable/ic_bubble_outline.xml new file mode 100644 index 000000000..6cd8e0f86 --- /dev/null +++ b/apps/flipcash/core/src/main/res/drawable/ic_bubble_outline.xml @@ -0,0 +1,13 @@ + + + + diff --git a/apps/flipcash/core/src/main/res/values/strings.xml b/apps/flipcash/core/src/main/res/values/strings.xml index 56d2b88bf..e53cd366a 100644 --- a/apps/flipcash/core/src/main/res/values/strings.xml +++ b/apps/flipcash/core/src/main/res/values/strings.xml @@ -913,6 +913,8 @@ Tips Tips Chats + No Chats Yet + Send a tip or share your Tip Card to start chatting Receive Tips From Everyone Add your name to receive tips Start Receiving Tips diff --git a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/TipsScreen.kt b/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/TipsScreen.kt index 06f88e60a..4b01564a1 100644 --- a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/TipsScreen.kt +++ b/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/TipsScreen.kt @@ -1,25 +1,38 @@ package com.flipcash.app.tipping +import androidx.compose.foundation.Image 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.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewWrapper import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.flipcash.app.core.AppRoute import com.flipcash.app.core.chat.ChatIdentifier +import com.flipcash.app.core.data.isLoaded +import com.flipcash.app.core.navigation.LocalTabBarPadding import com.flipcash.app.core.tipping.TipStep import com.flipcash.app.featureflags.FeatureFlag import com.flipcash.app.featureflags.LocalFeatureFlags +import com.flipcash.app.theme.FlipcashThemeWrapper import com.flipcash.app.tipping.internal.TipFlowViewModel import com.flipcash.app.tipping.internal.components.TipChatRow import com.flipcash.features.tipping.R @@ -70,10 +83,14 @@ fun TipsScreen() { } } ) { padding -> + val chats = state.tipChats LazyColumn( modifier = Modifier .fillMaxSize() .padding(padding), + // Clears the hoisted v2 tab bar (empty for v1, which has no tab bar): keeps the last row + // reachable and centers the empty state in the space the bar leaves visible. + contentPadding = LocalTabBarPadding.current, ) { // v1 surfaces the tip card via a button here; v2 has a dedicated tip-card tab instead. if (!isNewUi) { @@ -98,13 +115,61 @@ fun TipsScreen() { } } - tipChatItems(state.tipChats) { chat -> - navigator.push(AppRoute.Messaging.Chat(ChatIdentifier.ByChatId(chat.chatId))) + // v2 only: once the feed has loaded and there's nothing to show, the list is replaced by + // a centered prompt. v1's sheet keeps its bare list under the Tip Card button. + if (isNewUi && chats.isLoaded() && chats.data.isEmpty()) { + item { NoChatsYet(Modifier.fillParentMaxSize()) } + } else { + tipChatItems(chats.dataOrNull.orEmpty()) { chat -> + navigator.push(AppRoute.Messaging.Chat(ChatIdentifier.ByChatId(chat.chatId))) + } } } } } +/** + * The "Chats" tab empty state (node 9340:2746) — bubble mark, title and prompt, centered in the + * space the caller gives it (the list viewport). + */ +@Composable +private fun NoChatsYet(modifier: Modifier = Modifier) { + Box( + modifier = modifier, + contentAlignment = Alignment.Center, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = CodeTheme.dimens.inset), + verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x3), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + modifier = Modifier.size(CodeTheme.dimens.grid.x16), + painter = painterResource(R.drawable.ic_bubble_outline), + contentDescription = null, + colorFilter = ColorFilter.tint(CodeTheme.colors.textMain), + ) + + Text( + text = stringResource(R.string.title_noChatsYet), + style = CodeTheme.typography.textLarge, + color = CodeTheme.colors.textMain, + textAlign = TextAlign.Center, + ) + + Text( + modifier = Modifier.fillMaxWidth(0.6f), + text = stringResource(R.string.description_noChatsYet), + style = CodeTheme.typography.textSmall, + color = CodeTheme.colors.textSecondary, + textAlign = TextAlign.Center, + ) + } + } +} + private fun LazyListScope.tipChatItems( chats: List, onClick: (ConversationReference) -> Unit, @@ -118,3 +183,10 @@ private fun LazyListScope.tipChatItems( } } } + +@Composable +@Preview +@PreviewWrapper(FlipcashThemeWrapper::class) +private fun PreviewNoChatsYet() { + NoChatsYet(Modifier.fillMaxSize()) +} diff --git a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/TipFlowViewModel.kt b/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/TipFlowViewModel.kt index ebc8a01c5..ab91d460d 100644 --- a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/TipFlowViewModel.kt +++ b/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/TipFlowViewModel.kt @@ -3,6 +3,7 @@ package com.flipcash.app.tipping.internal import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import com.flipcash.app.core.bill.Scannable +import com.flipcash.app.core.data.Loadable import com.flipcash.app.core.extensions.onResult import com.flipcash.app.core.tipping.TipStep import com.flipcash.app.shareable.ShareSheetController @@ -46,14 +47,16 @@ internal class TipFlowViewModel @Inject constructor( // true when re-entering right after the user-profile setup handoff (Tips(resumed = true)). val resumed: Boolean = false, val currentStep: TipStep? = null, - val tipChats: List = emptyList(), + // Loading until the chat feed emits — distinguishes "still loading" from "loaded, none", so + // the empty state doesn't flash on top of a list that's about to arrive. + val tipChats: Loadable> = Loadable.Loading(), val tipCard: Scannable.TipCard? = null, ) sealed interface Event { data class StepsUpdated(val steps: List) : Event data class OnStepChanged(val step: TipStep) : Event - data class ChatsUpdated(val tips: List) : Event + data class ChatsUpdated(val tips: Loadable>) : Event /** How the flow was entered — [resumed] is true for the post-setup handoff re-entry. */ data class OnResumed(val resumed: Boolean) : Event @@ -101,7 +104,7 @@ internal class TipFlowViewModel @Inject constructor( val selfId = userManager.accountId val tokensByMint = tokens.associateBy { it.address } summaries.map { it.toConversationReference(selfId, tokensByMint, resources) } - }.onEach { dispatchEvent(Event.ChatsUpdated(it)) }.launchIn(viewModelScope) + }.onEach { dispatchEvent(Event.ChatsUpdated(Loadable.Loaded(it))) }.launchIn(viewModelScope) eventFlow .filterIsInstance()