diff --git a/app/src/main/java/app/closer/ui/components/GamePromptBanner.kt b/app/src/main/java/app/closer/ui/components/GamePromptBanner.kt index 368a94b1..19a94cef 100644 --- a/app/src/main/java/app/closer/ui/components/GamePromptBanner.kt +++ b/app/src/main/java/app/closer/ui/components/GamePromptBanner.kt @@ -149,10 +149,24 @@ private fun styleFor(prompt: IncomingGamePrompt): PromptStyle { return when (prompt.kind) { GamePromptKind.STARTED -> PromptStyle("$name started a game", "Play $game together", "Join") GamePromptKind.JOINED -> PromptStyle("$name's here", "Jump into $game together", "View") - GamePromptKind.YOUR_TURN -> PromptStyle("$name played their part", "Your turn — reveal how you line up", "Play") + GamePromptKind.YOUR_TURN -> PromptStyle( + "$name played their part", + when (prompt.gameType) { + app.closer.domain.model.GameType.HOW_WELL -> "Your turn — guess their answers" + app.closer.domain.model.GameType.DESIRE_SYNC -> "Your turn — only mutual yeses ever show" + app.closer.domain.model.GameType.THIS_OR_THAT -> "Your turn — see where you line up" + else -> "Your turn — reveal how you line up" + }, + "Play" + ) GamePromptKind.RESULTS -> PromptStyle( "You both finished", - if (resolved != null) "See how you and $resolved compare" else "See how you both compare", + when (prompt.gameType) { + app.closer.domain.model.GameType.HOW_WELL -> + if (resolved != null) "See how well you know $resolved" else "See how well you know each other" + app.closer.domain.model.GameType.DESIRE_SYNC -> "See what you both said yes to" + else -> if (resolved != null) "See how you and $resolved compare" else "See how you both compare" + }, "View" ) } diff --git a/app/src/main/java/app/closer/ui/games/WaitingForPartnerScreen.kt b/app/src/main/java/app/closer/ui/games/WaitingForPartnerScreen.kt index efacd0a5..47570e94 100644 --- a/app/src/main/java/app/closer/ui/games/WaitingForPartnerScreen.kt +++ b/app/src/main/java/app/closer/ui/games/WaitingForPartnerScreen.kt @@ -1,5 +1,11 @@ package app.closer.ui.games +import androidx.compose.ui.platform.LocalContext +import android.widget.Toast +import android.util.Log +import kotlinx.coroutines.tasks.await +import com.google.firebase.functions.FirebaseFunctionsException +import com.google.firebase.functions.FirebaseFunctions import app.closer.ui.theme.closerBackgroundBrush import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -16,6 +22,7 @@ import androidx.compose.material3.Button import app.closer.ui.components.CloserHeartLoader import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -61,12 +68,15 @@ data class WaitingForPartnerUiState( */ val joinRoute: String? = null, /** True when the partner already submitted their part — this user isn't waiting, it's their turn. */ - val partnerFinished: Boolean = false + val partnerFinished: Boolean = false, + val isSendingNudge: Boolean = false, + val nudgeResult: String? = null ) @HiltViewModel class WaitingForPartnerViewModel @Inject constructor( - private val gameSessionManager: GameSessionManager + private val gameSessionManager: GameSessionManager, + private val functions: FirebaseFunctions ) : ViewModel() { private val _uiState = MutableStateFlow(WaitingForPartnerUiState()) val uiState: StateFlow = _uiState.asStateFlow() @@ -108,6 +118,32 @@ class WaitingForPartnerViewModel @Inject constructor( } } + /** + * Nudge the partner while waiting on them — reuses the generic thinking-of-you callable + * (10/day, quiet-hours-aware server-side). Fails gracefully to a one-shot message. + */ + fun sendNudge() { + if (_uiState.value.isSendingNudge) return + _uiState.update { it.copy(isSendingNudge = true) } + viewModelScope.launch { + val message = runCatching { + functions.getHttpsCallable("sendThinkingOfYouCallable").call().await() + }.fold( + onSuccess = { "Sent 💜" }, + onFailure = { e -> + Log.w("WaitingForPartner", "nudge failed", e) + if ((e as? FirebaseFunctionsException)?.code == + FirebaseFunctionsException.Code.RESOURCE_EXHAUSTED + ) "You've nudged a few times — give it a moment 💜" + else "Couldn't send right now. Try again." + } + ) + _uiState.update { it.copy(isSendingNudge = false, nudgeResult = message) } + } + } + + fun consumeNudgeResult() = _uiState.update { it.copy(nudgeResult = null) } + /** Force-end the partner's active session so this user can start a new game. */ fun abandonPartnerGame() { val cId = coupleId ?: return @@ -138,6 +174,14 @@ fun WaitingForPartnerScreen( } } + val context = LocalContext.current + LaunchedEffect(state.nudgeResult) { + state.nudgeResult?.let { + Toast.makeText(context, it, Toast.LENGTH_SHORT).show() + viewModel.consumeNudgeResult() + } + } + Box( modifier = Modifier .fillMaxSize() @@ -233,6 +277,15 @@ fun WaitingForPartnerScreen( Text("Back to Games") } } + if (!state.partnerFinished) { + OutlinedButton( + onClick = viewModel::sendNudge, + enabled = !state.isSendingNudge, + modifier = Modifier.fillMaxWidth() + ) { + Text(if (state.isSendingNudge) "Sending…" else "Send a little nudge 💜") + } + } TextButton(onClick = viewModel::abandonPartnerGame) { Text( "End their game", diff --git a/app/src/main/java/app/closer/ui/howwell/HowWellScreen.kt b/app/src/main/java/app/closer/ui/howwell/HowWellScreen.kt index db06c3ef..60206afa 100644 --- a/app/src/main/java/app/closer/ui/howwell/HowWellScreen.kt +++ b/app/src/main/java/app/closer/ui/howwell/HowWellScreen.kt @@ -24,6 +24,8 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.draw.clip import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -1018,7 +1020,9 @@ private fun HowWellScoreRing( ) { val progress = if (total == 0) 0f else score.toFloat() / total Box( - modifier = modifier, + modifier = modifier.semantics(mergeDescendants = true) { + contentDescription = "You guessed $score of $total correctly" + }, contentAlignment = Alignment.Center ) { Canvas(modifier = Modifier.fillMaxSize()) { diff --git a/app/src/main/java/app/closer/ui/thisorthat/ThisOrThatScreen.kt b/app/src/main/java/app/closer/ui/thisorthat/ThisOrThatScreen.kt index 185efd86..aeb9e66b 100644 --- a/app/src/main/java/app/closer/ui/thisorthat/ThisOrThatScreen.kt +++ b/app/src/main/java/app/closer/ui/thisorthat/ThisOrThatScreen.kt @@ -51,6 +51,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import app.closer.ui.components.CelebrationOverlay +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -1139,7 +1141,11 @@ private fun ThisOrThatReveal( private fun MatchScoreBadge(matched: Int, total: Int) { val cs = MaterialTheme.colorScheme Surface( - modifier = Modifier.size(116.dp), + modifier = Modifier + .size(116.dp) + .semantics(mergeDescendants = true) { + contentDescription = "You matched on $matched of $total" + }, shape = CircleShape, color = cs.primaryContainer, shadowElevation = 8.dp diff --git a/functions/src/games/onGameSessionUpdate.ts b/functions/src/games/onGameSessionUpdate.ts index 8426581b..72bb7b41 100644 --- a/functions/src/games/onGameSessionUpdate.ts +++ b/functions/src/games/onGameSessionUpdate.ts @@ -228,11 +228,24 @@ export const onGamePartFinished = functions.firestore await notifyPartner( db, messaging, recipient, finisherName, gameType, - 'partner_completed_part', `${finisherName} finished their part — your turn to play!`, coupleId, + 'partner_completed_part', yourTurnBody(gameType), coupleId, finisherAvatar, sessionId ) }) +/** + * Per-game body for the "your turn" (partner_completed_part) push — mirrors the client banner's + * YOUR_TURN copy in ui/components/GamePromptBanner.kt so foreground and background read the same. + */ +function yourTurnBody(gameType: string): string { + switch (gameType) { + case 'how_well': return 'Your turn — guess their answers' + case 'desire_sync': return 'Your turn — only mutual yeses ever show' + case 'this_or_that': return 'Your turn — see where you line up' + default: return 'Your turn — reveal how you line up' + } +} + /** * Send notification to partner via FCM and write to notification_queue. */