feat: add 'Waiting for you' unfinished business dashboard (batch v1.0.3)
- PendingActionCard data class with priority-ordered cards - 6 card types: reveal ready, partner answered, game, challenge, date, capsule - Max 3 cards, highest priority first, deep link to correct screen - Placeholder guards for game/challenge/date/capsule (wired in later batches) - Compact cards with purple/pink palette, no private text leaked
This commit is contained in:
parent
c38e83b8ee
commit
9828e73171
|
|
@ -0,0 +1,58 @@
|
|||
package app.closer.notifications
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
|
||||
/**
|
||||
* Creates the Android notification channels used by partner-triggered notifications.
|
||||
*
|
||||
* Channels are idempotent; calling [createChannels] multiple times is safe.
|
||||
*/
|
||||
class NotificationChannelSetup(private val context: Context) {
|
||||
|
||||
companion object {
|
||||
const val CHANNEL_REMINDERS = "reminders"
|
||||
const val CHANNEL_PARTNER_ACTIONS = "partner_activity"
|
||||
const val CHANNEL_GAMES = "game_activity"
|
||||
|
||||
@JvmStatic
|
||||
fun createChannels(context: Context) {
|
||||
NotificationChannelSetup(context).createChannels()
|
||||
}
|
||||
}
|
||||
|
||||
fun createChannels() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
|
||||
val notificationManager =
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
notificationManager.createNotificationChannels(
|
||||
listOf(
|
||||
NotificationChannel(
|
||||
CHANNEL_PARTNER_ACTIONS,
|
||||
"Partner activity",
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
).apply {
|
||||
description = "When your partner answers, starts a game, or unlocks something for you"
|
||||
},
|
||||
NotificationChannel(
|
||||
CHANNEL_REMINDERS,
|
||||
"Daily reminders",
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
).apply {
|
||||
description = "Gentle nudges for today's question and shared moments"
|
||||
},
|
||||
NotificationChannel(
|
||||
CHANNEL_GAMES,
|
||||
"Games",
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
).apply {
|
||||
description = "When your partner starts or completes a game round"
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -100,6 +100,17 @@ fun HomeScreen(
|
|||
onInvite = { onNavigate(AppRoute.CREATE_INVITE) },
|
||||
onReminder = viewModel::sendGentleReminder,
|
||||
onReveal = { state.dailyQuestion?.id?.let { onNavigate(AppRoute.answerReveal(it)) } },
|
||||
onPendingAction = { card ->
|
||||
when (card.priority) {
|
||||
1 -> state.dailyQuestion?.id?.let { onNavigate(AppRoute.answerReveal(it)) }
|
||||
2 -> onNavigate(AppRoute.DAILY_QUESTION)
|
||||
3 -> onNavigate(AppRoute.PLAY)
|
||||
4 -> onNavigate(AppRoute.CONNECTION_CHALLENGES)
|
||||
5 -> onNavigate(AppRoute.DATE_MATCHES)
|
||||
6 -> onNavigate(AppRoute.MEMORY_LANE)
|
||||
else -> {}
|
||||
}
|
||||
},
|
||||
onFollowUp = { state.dailyQuestion?.let { onNavigate(AppRoute.questionThread(state.coupleId ?: "", it.id)) } },
|
||||
onRefresh = viewModel::loadHome
|
||||
)
|
||||
|
|
@ -110,6 +121,7 @@ data class HomeCallbacks(
|
|||
val onReminder: () -> Unit,
|
||||
val onReveal: () -> Unit,
|
||||
val onFollowUp: () -> Unit,
|
||||
val onPendingAction: (PendingActionCard) -> Unit,
|
||||
val onPacks: () -> Unit,
|
||||
val onCategory: (String) -> Unit,
|
||||
val onHistory: () -> Unit,
|
||||
|
|
@ -125,9 +137,11 @@ private fun HomeCallbacks.toActionHandler(): (HomeAction) -> Unit = { action ->
|
|||
HomeActionTarget.AnswerHistory -> onHistory()
|
||||
HomeActionTarget.QuestionPacks -> action.categoryId?.let(onCategory) ?: onPacks()
|
||||
HomeActionTarget.Settings -> onSettings()
|
||||
else -> {
|
||||
// Batch 4 streak changes do not add new action targets.
|
||||
}
|
||||
HomeActionTarget.AnswerReveal -> onReveal()
|
||||
HomeActionTarget.Game -> onPacks()
|
||||
HomeActionTarget.Challenge -> onPacks()
|
||||
HomeActionTarget.DatePlan -> onPacks()
|
||||
HomeActionTarget.MemoryCapsule -> onPacks()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -144,6 +158,7 @@ private fun HomeContent(
|
|||
onReminder: () -> Unit,
|
||||
onReveal: () -> Unit,
|
||||
onFollowUp: () -> Unit,
|
||||
onPendingAction: (PendingActionCard) -> Unit,
|
||||
onRefresh: () -> Unit
|
||||
) {
|
||||
val callbacks = remember(
|
||||
|
|
@ -734,6 +749,88 @@ private fun CategoryMiniCard(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WaitingForYouSection(
|
||||
actions: List<PendingActionCard>,
|
||||
onAction: (PendingActionCard) -> Unit
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Text(
|
||||
text = "Waiting for you",
|
||||
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
actions.take(3).forEach { card ->
|
||||
PendingActionCardView(card = card, onClick = { onAction(card) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PendingActionCardView(
|
||||
card: PendingActionCard,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val colors = HomeActionTone.Pending.actionColors()
|
||||
|
||||
CloserClickableCard(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(CloserRadii.Card),
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
elevation = CloserElevations.Card
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(13.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.background(colors.soft, RoundedCornerShape(CloserRadii.Button)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Favorite,
|
||||
contentDescription = null,
|
||||
tint = colors.deep,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
Text(
|
||||
text = card.title,
|
||||
style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.SemiBold),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
card.subtitle?.let { subtitle ->
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowForward,
|
||||
contentDescription = "Open",
|
||||
tint = colors.deep,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingHomeCard() {
|
||||
LoadingState(message = "Opening your dashboard")
|
||||
|
|
@ -809,6 +906,7 @@ fun HomeScreenPreview() {
|
|||
onHistory = {},
|
||||
onSettings = {},
|
||||
onInvite = {},
|
||||
onPendingAction = {},
|
||||
onRefresh = {}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue