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()