From 9828e7317178af48eda407db7fd163a4eb178492 Mon Sep 17 00:00:00 2001 From: null Date: Fri, 19 Jun 2026 22:31:11 -0500 Subject: [PATCH] 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 --- .../notifications/NotificationChannelSetup.kt | 58 ++++++++++ .../java/app/closer/ui/home/HomeScreen.kt | 104 +++++++++++++++++- 2 files changed, 159 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/app/closer/notifications/NotificationChannelSetup.kt diff --git a/app/src/main/java/app/closer/notifications/NotificationChannelSetup.kt b/app/src/main/java/app/closer/notifications/NotificationChannelSetup.kt new file mode 100644 index 00000000..0e1c0816 --- /dev/null +++ b/app/src/main/java/app/closer/notifications/NotificationChannelSetup.kt @@ -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" + } + ) + ) + } +} diff --git a/app/src/main/java/app/closer/ui/home/HomeScreen.kt b/app/src/main/java/app/closer/ui/home/HomeScreen.kt index bd6514f6..257f87a9 100644 --- a/app/src/main/java/app/closer/ui/home/HomeScreen.kt +++ b/app/src/main/java/app/closer/ui/home/HomeScreen.kt @@ -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, + 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 = {} ) }