feat(notif): foreground game-start banner + bold Home waiting hero — join specific game from both
This commit is contained in:
parent
6e79cd9704
commit
38fdc6d2cc
|
|
@ -137,6 +137,11 @@ class MainActivity : AppCompatActivity() {
|
|||
*/
|
||||
private fun deepLinkRouteFromIntent(intent: Intent?): String? {
|
||||
intent ?: return null
|
||||
// Foreground-posted notifications (AppMessagingService → PartnerNotificationManager) carry the
|
||||
// already-resolved app route as an extra. Prefer it so routing never depends on per-route
|
||||
// navDeepLink registration — game/challenge/date/capsule routes aren't registered as Uri
|
||||
// deep links, so those taps used to fall through to Home when the app was foregrounded.
|
||||
intent.getStringExtra("app_route")?.takeIf { it.isNotBlank() }?.let { return it }
|
||||
if (intent.data != null) return null
|
||||
val type = intent.getStringExtra("type") ?: return null
|
||||
val coupleId = intent.getStringExtra("couple_id") ?: ""
|
||||
|
|
|
|||
|
|
@ -557,6 +557,22 @@ fun AppNavigation(
|
|||
app.closer.ui.components.MessageBubbleOverlay(
|
||||
onOpen = { c, conv -> navigateRoute(AppRoute.conversation(c, conv)) }
|
||||
)
|
||||
|
||||
// Prominent in-app banner when a partner starts a game while this app is foregrounded
|
||||
// (background delivery uses the OS notification). Tap "Join" → the game route, which
|
||||
// auto-joins the couple's active session on open.
|
||||
app.closer.ui.components.GamePromptBanner(
|
||||
onJoin = { gameType, sessionId ->
|
||||
val route = app.closer.notifications.PartnerNotificationType.PARTNER_STARTED_GAME
|
||||
.routeFor(
|
||||
app.closer.notifications.PartnerNotificationPayload(
|
||||
gameType = gameType,
|
||||
gameSessionId = sessionId
|
||||
)
|
||||
)
|
||||
navigateRoute(route)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ class AppMessagingService : FirebaseMessagingService() {
|
|||
@Inject lateinit var partnerNotificationManager: PartnerNotificationManager
|
||||
@Inject lateinit var activeThreadMonitor: app.closer.notifications.ActiveThreadMonitor
|
||||
@Inject lateinit var messageBubbleController: app.closer.notifications.MessageBubbleController
|
||||
@Inject lateinit var gamePromptController: app.closer.notifications.GamePromptController
|
||||
@Inject lateinit var activeGameSessionMonitor: app.closer.notifications.ActiveGameSessionMonitor
|
||||
|
||||
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
|
|
@ -69,6 +71,23 @@ class AppMessagingService : FirebaseMessagingService() {
|
|||
return
|
||||
}
|
||||
|
||||
// Partner started a game while we're foregrounded → surface the prominent in-app banner
|
||||
// instead of the OS notification (mirrors chat). Skip if we're already viewing that game live.
|
||||
if (type == "partner_started_game") {
|
||||
val sessionId = message.data["game_session_id"]
|
||||
val alreadyViewing = sessionId != null && sessionId == activeGameSessionMonitor.activeSessionId
|
||||
if (!alreadyViewing) {
|
||||
gamePromptController.show(
|
||||
coupleId = coupleId,
|
||||
gameType = message.data["game_type"] ?: "",
|
||||
gameSessionId = sessionId,
|
||||
starterName = message.data["starter_name"],
|
||||
avatarUrl = message.data["sender_avatar_url"]
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
serviceScope.launch {
|
||||
runCatching {
|
||||
partnerNotificationManager.handleRemote(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
package app.closer.notifications
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* State for the in-app "partner started a game" banner. Shown over the app's own UI when a game
|
||||
* start arrives while the app is FOREGROUNDED (background delivery uses the OS notification). Mirrors
|
||||
* [MessageBubbleController] — the foreground FCM path posts here, the root UI observes [prompt].
|
||||
*/
|
||||
data class IncomingGamePrompt(
|
||||
val coupleId: String,
|
||||
val gameType: String,
|
||||
val gameSessionId: String? = null,
|
||||
val starterName: String? = null,
|
||||
val avatarUrl: String? = null
|
||||
)
|
||||
|
||||
@Singleton
|
||||
class GamePromptController @Inject constructor() {
|
||||
private val _prompt = MutableStateFlow<IncomingGamePrompt?>(null)
|
||||
val prompt: StateFlow<IncomingGamePrompt?> = _prompt.asStateFlow()
|
||||
|
||||
fun show(
|
||||
coupleId: String,
|
||||
gameType: String,
|
||||
gameSessionId: String?,
|
||||
starterName: String?,
|
||||
avatarUrl: String?
|
||||
) {
|
||||
if (coupleId.isBlank() || gameType.isBlank()) return
|
||||
_prompt.value = IncomingGamePrompt(coupleId, gameType, gameSessionId, starterName, avatarUrl)
|
||||
}
|
||||
|
||||
fun dismiss() {
|
||||
_prompt.value = null
|
||||
}
|
||||
}
|
||||
|
|
@ -130,6 +130,10 @@ class PartnerNotificationManager @Inject constructor(
|
|||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
data = deepLinkUri
|
||||
// Also carry the resolved route so MainActivity can navigate without relying on a
|
||||
// per-route navDeepLink registration (game/challenge/date/capsule routes aren't
|
||||
// registered as Uri deep links). See MainActivity.deepLinkRouteFromIntent.
|
||||
putExtra("app_route", route)
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
}
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,202 @@
|
|||
package app.closer.ui.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.ViewModel
|
||||
import app.closer.domain.model.GameType
|
||||
import app.closer.notifications.GamePromptController
|
||||
import app.closer.notifications.IncomingGamePrompt
|
||||
import app.closer.ui.theme.CloserPalette
|
||||
import coil.compose.AsyncImage
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class GamePromptViewModel @Inject constructor(
|
||||
private val controller: GamePromptController
|
||||
) : ViewModel() {
|
||||
val prompt = controller.prompt
|
||||
fun dismiss() = controller.dismiss()
|
||||
}
|
||||
|
||||
private fun gameDisplayName(gameType: String): String = when (gameType) {
|
||||
GameType.WHEEL -> "Spin the Wheel"
|
||||
GameType.THIS_OR_THAT -> "This or That"
|
||||
GameType.HOW_WELL -> "How Well Do You Know Me"
|
||||
GameType.DESIRE_SYNC -> "Desire Sync"
|
||||
else -> "a game"
|
||||
}
|
||||
|
||||
/**
|
||||
* Prominent in-app banner shown over the app's own UI when a partner starts a game while the app is
|
||||
* FOREGROUNDED (background delivery uses the OS notification). Slides in from the top, tap "Join" to
|
||||
* jump straight into the game (each game screen auto-joins the couple's active session on open).
|
||||
* Mirrors the chat [MessageBubbleOverlay] foreground pattern. Auto-dismisses after a few seconds.
|
||||
*/
|
||||
@Composable
|
||||
fun GamePromptBanner(
|
||||
onJoin: (gameType: String, gameSessionId: String?) -> Unit,
|
||||
viewModel: GamePromptViewModel = hiltViewModel()
|
||||
) {
|
||||
val prompt by viewModel.prompt.collectAsState()
|
||||
|
||||
// Retain the last prompt so the slide-out animation still has data to render.
|
||||
var last by remember { mutableStateOf<IncomingGamePrompt?>(null) }
|
||||
LaunchedEffect(prompt) { if (prompt != null) last = prompt }
|
||||
|
||||
// Auto-dismiss a few seconds after it appears (it's a transient nudge, not a persistent card).
|
||||
LaunchedEffect(prompt) {
|
||||
if (prompt != null) {
|
||||
delay(9000)
|
||||
viewModel.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
AnimatedVisibility(
|
||||
visible = prompt != null,
|
||||
enter = slideInVertically { -it } + fadeIn(),
|
||||
exit = slideOutVertically { -it } + fadeOut(),
|
||||
modifier = Modifier.align(Alignment.TopCenter)
|
||||
) {
|
||||
val p = last ?: return@AnimatedVisibility
|
||||
GamePromptCard(
|
||||
prompt = p,
|
||||
onJoin = { onJoin(p.gameType, p.gameSessionId); viewModel.dismiss() },
|
||||
onDismiss = { viewModel.dismiss() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GamePromptCard(
|
||||
prompt: IncomingGamePrompt,
|
||||
onJoin: () -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val name = prompt.starterName?.takeIf { it.isNotBlank() } ?: "Your partner"
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.statusBarsPadding()
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||
.fillMaxWidth()
|
||||
.shadow(10.dp, RoundedCornerShape(20.dp))
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
listOf(CloserPalette.PurpleDeep, CloserPalette.PurpleRich)
|
||||
)
|
||||
)
|
||||
.border(1.dp, Color.White.copy(alpha = 0.18f), RoundedCornerShape(20.dp))
|
||||
.padding(start = 14.dp, end = 8.dp, top = 12.dp, bottom = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Partner avatar (falls back to a play glyph).
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color.White.copy(alpha = 0.18f))
|
||||
.border(1.5.dp, Color.White.copy(alpha = 0.5f), CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val avatar = prompt.avatarUrl
|
||||
if (!avatar.isNullOrBlank()) {
|
||||
AsyncImage(
|
||||
model = avatar,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.size(40.dp).clip(CircleShape)
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.PlayArrow,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.weight(1f).padding(horizontal = 12.dp)) {
|
||||
Text(
|
||||
text = "$name started a game",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = Color.White.copy(alpha = 0.85f)
|
||||
)
|
||||
Text(
|
||||
text = gameDisplayName(prompt.gameType),
|
||||
style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.Bold),
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = onJoin,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.White,
|
||||
contentColor = CloserPalette.PurpleDeep
|
||||
),
|
||||
shape = RoundedCornerShape(14.dp)
|
||||
) {
|
||||
Icon(Icons.Filled.PlayArrow, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Text(
|
||||
text = "Join",
|
||||
style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.Bold),
|
||||
modifier = Modifier.padding(start = 4.dp)
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = onDismiss) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Close,
|
||||
contentDescription = "Dismiss",
|
||||
tint = Color.White.copy(alpha = 0.8f),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -27,6 +27,8 @@ import app.closer.ui.theme.closerCardColor
|
|||
import app.closer.ui.theme.isCloserDarkTheme
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -47,6 +49,8 @@ import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
|||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.filled.LocalFireDepartment
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import app.closer.domain.model.OutcomeDay
|
||||
import app.closer.ui.components.OutcomeCheckInDialog
|
||||
import androidx.compose.material3.Icon
|
||||
|
|
@ -318,7 +322,12 @@ private fun HomeContent(
|
|||
state.pendingActions.takeIf { it.isNotEmpty() }?.let { pending ->
|
||||
WaitingForYouSection(
|
||||
actions = pending,
|
||||
onAction = onPendingActionSelected
|
||||
partnerName = state.partnerName,
|
||||
waitingGameType = state.waitingGameType,
|
||||
onAction = onPendingActionSelected,
|
||||
// The game-waiting hero joins the specific waiting game directly (not the
|
||||
// generic Play hub the pending-card fallback would use).
|
||||
onJoinGame = { onNavigate(state.waitingGameRoute ?: AppRoute.PLAY) }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1241,20 +1250,115 @@ private fun CategoryMiniCard(
|
|||
@Composable
|
||||
private fun WaitingForYouSection(
|
||||
actions: List<PendingActionCard>,
|
||||
onAction: (PendingActionCard) -> Unit
|
||||
partnerName: String?,
|
||||
waitingGameType: String?,
|
||||
onAction: (PendingActionCard) -> Unit,
|
||||
onJoinGame: () -> Unit
|
||||
) {
|
||||
val gameCard = actions.firstOrNull { it.target == HomeActionTarget.Game }
|
||||
val others = actions.filter { it.target != HomeActionTarget.Game }
|
||||
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 ->
|
||||
// The game-waiting prompt gets a bold hero treatment (promoted to the top) and joins the
|
||||
// specific waiting game directly.
|
||||
if (gameCard != null) {
|
||||
GameWaitingHeroCard(
|
||||
partnerName = partnerName,
|
||||
gameName = gameDisplayName(waitingGameType),
|
||||
onJoin = onJoinGame
|
||||
)
|
||||
}
|
||||
others.take(3).forEach { card ->
|
||||
PendingActionCardView(card = card, onClick = { onAction(card) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun gameDisplayName(gameType: String?): String = when (gameType) {
|
||||
"wheel" -> "Spin the Wheel"
|
||||
"this_or_that" -> "This or That"
|
||||
"how_well" -> "How Well Do You Know Me"
|
||||
"desire_sync" -> "Desire Sync"
|
||||
else -> "a game together"
|
||||
}
|
||||
|
||||
/** Bold, eye-catching "your partner is waiting to play <Game>" hero with a direct Join CTA. */
|
||||
@Composable
|
||||
private fun GameWaitingHeroCard(
|
||||
partnerName: String?,
|
||||
gameName: String,
|
||||
onJoin: () -> Unit
|
||||
) {
|
||||
val name = partnerName?.takeIf { it.isNotBlank() } ?: "Your partner"
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(CloserRadii.Card))
|
||||
.background(
|
||||
Brush.linearGradient(listOf(CloserPalette.PurpleDeep, CloserPalette.PurpleRich))
|
||||
)
|
||||
.padding(18.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(14.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(44.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color.White.copy(alpha = 0.18f))
|
||||
.border(1.5.dp, Color.White.copy(alpha = 0.5f), CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.PlayArrow,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "$name is waiting to play",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = Color.White.copy(alpha = 0.85f)
|
||||
)
|
||||
Text(
|
||||
text = gameName,
|
||||
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
Button(
|
||||
onClick = onJoin,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.White,
|
||||
contentColor = CloserPalette.PurpleDeep
|
||||
),
|
||||
shape = RoundedCornerShape(CloserRadii.Button)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.PlayArrow,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Text(
|
||||
text = "Join the game",
|
||||
style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.Bold),
|
||||
modifier = Modifier.padding(start = 6.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PendingActionCardView(
|
||||
card: PendingActionCard,
|
||||
|
|
|
|||
|
|
@ -149,6 +149,8 @@ data class HomeUiState(
|
|||
// The route of the active game waiting for this user, so the Home "Play now" CTA
|
||||
// resumes that specific game instead of dumping on the generic Play hub (B-002).
|
||||
val waitingGameRoute: String? = null,
|
||||
// The waiting game's type (e.g. "wheel"), so the Home card can name the game ("… in Spin the Wheel").
|
||||
val waitingGameType: String? = null,
|
||||
val hasActiveChallenge: Boolean = false,
|
||||
val hasUpcomingDatePlan: Boolean = false,
|
||||
val hasUnlockedCapsule: Boolean = false,
|
||||
|
|
@ -265,6 +267,7 @@ class HomeViewModel @Inject constructor(
|
|||
// Retention signal fetches — run in parallel, failures silently default to false.
|
||||
var hasWaitingGame = false
|
||||
var waitingGameRoute: String? = null
|
||||
var waitingGameType: String? = null
|
||||
var hasActiveChallenge = false
|
||||
var hasUpcomingDatePlan = false
|
||||
var hasUnlockedCapsule = false
|
||||
|
|
@ -302,6 +305,7 @@ class HomeViewModel @Inject constructor(
|
|||
val (waitingSession, waitingRoute) = gameJob.await()
|
||||
hasWaitingGame = waitingSession != null
|
||||
waitingGameRoute = waitingRoute
|
||||
waitingGameType = waitingSession?.gameType
|
||||
hasActiveChallenge = challengeJob.await()
|
||||
hasUpcomingDatePlan = dateJob.await()
|
||||
hasUnlockedCapsule = capsuleJob.await()
|
||||
|
|
@ -321,6 +325,7 @@ class HomeViewModel @Inject constructor(
|
|||
needsRecovery = needsRecovery,
|
||||
hasWaitingGame = hasWaitingGame,
|
||||
waitingGameRoute = waitingGameRoute,
|
||||
waitingGameType = waitingGameType,
|
||||
hasActiveChallenge = hasActiveChallenge,
|
||||
hasUpcomingDatePlan = hasUpcomingDatePlan,
|
||||
hasUnlockedCapsule = hasUnlockedCapsule,
|
||||
|
|
|
|||
|
|
@ -42,7 +42,8 @@ class WheelSessionViewModel @Inject constructor(
|
|||
private val sessionStore: LocalWheelSessionStore,
|
||||
private val repository: QuestionRepository,
|
||||
private val gameSessionManager: GameSessionManager,
|
||||
private val answerDataSource: FirestoreWheelAnswerDataSource
|
||||
private val answerDataSource: FirestoreWheelAnswerDataSource,
|
||||
private val activeGameSessionMonitor: app.closer.notifications.ActiveGameSessionMonitor
|
||||
) : ViewModel() {
|
||||
|
||||
private val sessionId: String = savedStateHandle["sessionId"] ?: ""
|
||||
|
|
@ -56,9 +57,17 @@ class WheelSessionViewModel @Inject constructor(
|
|||
private var submitting = false
|
||||
|
||||
init {
|
||||
// Mark this game active so a foreground "partner started a game" banner is suppressed while
|
||||
// the user is already playing this wheel session (mirrors the other game screens).
|
||||
activeGameSessionMonitor.enter(sessionId)
|
||||
load()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
activeGameSessionMonitor.leave(sessionId)
|
||||
}
|
||||
|
||||
private fun load() {
|
||||
viewModelScope.launch {
|
||||
val uid = gameSessionManager.currentUserId
|
||||
|
|
|
|||
Loading…
Reference in New Issue