diff --git a/app/src/main/java/app/closer/core/notifications/AppMessagingService.kt b/app/src/main/java/app/closer/core/notifications/AppMessagingService.kt index f3d30389..53d9bf8b 100644 --- a/app/src/main/java/app/closer/core/notifications/AppMessagingService.kt +++ b/app/src/main/java/app/closer/core/notifications/AppMessagingService.kt @@ -98,28 +98,28 @@ class AppMessagingService : FirebaseMessagingService() { } private fun resolveTitle(type: String): String? = when (type) { - "daily_question" -> "Your daily question is waiting!" - "partner_answered" -> "Your partner just answered!" - "partner_left" -> "Your partner has left" - "streak" -> "Keep your streak going — answer today's question!" - "partner_started_game" -> "Partner is playing!" - "partner_finished_game" -> "Partner finished!" - "partner_waiting" -> "Partner waiting" - "memory_capsule_unlocked" -> "Your memory capsule opened" - "challenge_day_ready" -> "Today's challenge is ready" + "daily_question" -> "Today's question is here." + "partner_answered" -> "Your partner answered." + "partner_left" -> "You've been unlinked." + "streak" -> "A question is waiting for you." + "partner_started_game" -> "Your partner started a game." + "partner_finished_game" -> "Your partner finished the round." + "partner_waiting" -> "Your partner is waiting." + "memory_capsule_unlocked" -> "Your capsule just opened." + "challenge_day_ready" -> "A new connection moment is ready." else -> null } private fun resolveBody(type: String): String? = when (type) { - "daily_question" -> "Tap to answer today's question together." - "partner_answered" -> "See what your partner shared." - "partner_left" -> "You are no longer paired. Tap to create a new invite." - "streak" -> "Don't break the chain. Open the app now." - "partner_started_game" -> "Your partner has started a game. Tap to join!" - "partner_finished_game" -> "Your partner has finished. Tap to see results!" - "partner_waiting" -> "Your partner is waiting for you to finish." - "memory_capsule_unlocked" -> "Open Memory Lane to read it together." - "challenge_day_ready" -> "Open your connection challenge for today's prompt." + "daily_question" -> "Take a moment to answer. Your partner's waiting too." + "partner_answered" -> "See what they shared — then reveal when you're ready." + "partner_left" -> "Your shared space has been closed. Create a new invite whenever you're ready." + "streak" -> "Answer today's question to keep your shared rhythm going." + "partner_started_game" -> "They're in — tap to join them." + "partner_finished_game" -> "Time to compare notes. See your results together." + "partner_waiting" -> "They finished their side. Whenever you're ready, complete yours." + "memory_capsule_unlocked" -> "Something you sealed together is ready to open." + "challenge_day_ready" -> "Your next connection challenge is here — open it together." else -> null } diff --git a/app/src/main/java/app/closer/core/notifications/NotificationHelper.kt b/app/src/main/java/app/closer/core/notifications/NotificationHelper.kt index be38d10d..313dfa6d 100644 --- a/app/src/main/java/app/closer/core/notifications/NotificationHelper.kt +++ b/app/src/main/java/app/closer/core/notifications/NotificationHelper.kt @@ -24,7 +24,7 @@ object NotificationHelper { "Daily reminders", NotificationManager.IMPORTANCE_DEFAULT ).apply { - description = "Daily question and streak reminders" + description = "Gentle nudges for today's question and shared moments" } ) nm.createNotificationChannel( @@ -33,16 +33,16 @@ object NotificationHelper { "Partner activity", NotificationManager.IMPORTANCE_HIGH ).apply { - description = "When your partner answers a question or plays a game" + description = "When your partner answers, reveals, or opens a conversation" } ) nm.createNotificationChannel( NotificationChannel( CHANNEL_GAME_ACTIVITY, - "Game activity", + "Games", NotificationManager.IMPORTANCE_HIGH ).apply { - description = "When your partner starts or finishes a game session" + description = "When your partner starts or completes a game round" } ) } diff --git a/app/src/main/java/app/closer/ui/answers/AnswerRevealScreen.kt b/app/src/main/java/app/closer/ui/answers/AnswerRevealScreen.kt index 5eedc3fb..b1ebd5ac 100644 --- a/app/src/main/java/app/closer/ui/answers/AnswerRevealScreen.kt +++ b/app/src/main/java/app/closer/ui/answers/AnswerRevealScreen.kt @@ -1,6 +1,11 @@ package app.closer.ui.answers import app.closer.ui.theme.closerBackgroundBrush +import android.provider.Settings +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.expandVertically import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -28,6 +33,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush @@ -78,6 +84,11 @@ private fun AnswerRevealContent( onHistory: () -> Unit, onHome: () -> Unit ) { + val context = LocalContext.current + val reducedMotion = Settings.Global.getFloat( + context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f + ) == 0f + Box( modifier = Modifier .fillMaxSize() @@ -119,13 +130,19 @@ private fun AnswerRevealContent( onAnswerQuestion = onAnswerQuestion, onHome = onHome ) - state.answer.isRevealed -> RevealedState( - answer = state.answer, - partnerAnswer = state.partnerAnswer, - question = state.question, - onHistory = onHistory, - onHome = onHome - ) + state.answer.isRevealed -> AnimatedVisibility( + visible = true, + enter = if (reducedMotion) fadeIn(tween(0)) + else fadeIn(tween(380)) + expandVertically(tween(380)) + ) { + RevealedState( + answer = state.answer, + partnerAnswer = state.partnerAnswer, + question = state.question, + onHistory = onHistory, + onHome = onHome + ) + } else -> ReadyToRevealState( answer = state.answer, partnerAnswer = state.partnerAnswer, diff --git a/app/src/main/java/app/closer/ui/components/ErrorState.kt b/app/src/main/java/app/closer/ui/components/ErrorState.kt index 47890001..18001c7e 100644 --- a/app/src/main/java/app/closer/ui/components/ErrorState.kt +++ b/app/src/main/java/app/closer/ui/components/ErrorState.kt @@ -13,7 +13,7 @@ import androidx.compose.ui.text.font.FontWeight @Composable fun ErrorState( - title: String = "Something went wrong", + title: String = "Didn't quite load", message: String, retryLabel: String = "Try again", onRetry: (() -> Unit)? = null, diff --git a/app/src/main/java/app/closer/ui/dates/DateMatchScreen.kt b/app/src/main/java/app/closer/ui/dates/DateMatchScreen.kt index 6c2703e7..d60631e0 100644 --- a/app/src/main/java/app/closer/ui/dates/DateMatchScreen.kt +++ b/app/src/main/java/app/closer/ui/dates/DateMatchScreen.kt @@ -128,8 +128,8 @@ private fun DateMatchContent( state.error != null -> { ErrorState( - title = "Could not load ideas", - message = state.error ?: "Something went wrong.", + title = "Couldn't load date ideas", + message = state.error ?: "Tap below to try again.", onRetry = onRetry, modifier = Modifier.padding(top = 120.dp) ) diff --git a/app/src/main/java/app/closer/ui/dates/DateMatchesScreen.kt b/app/src/main/java/app/closer/ui/dates/DateMatchesScreen.kt index a92ae9fb..faddc0a9 100644 --- a/app/src/main/java/app/closer/ui/dates/DateMatchesScreen.kt +++ b/app/src/main/java/app/closer/ui/dates/DateMatchesScreen.kt @@ -115,8 +115,8 @@ private fun DateMatchesContent( state.error != null -> item { ErrorState( - title = "Could not load matches", - message = state.error ?: "Something went wrong.", + title = "Couldn't load your matches", + message = state.error ?: "Pull down to try again.", onRetry = onRetry, modifier = Modifier.padding(top = 80.dp) ) diff --git a/app/src/main/java/app/closer/ui/desiresync/DesireSyncScreen.kt b/app/src/main/java/app/closer/ui/desiresync/DesireSyncScreen.kt index 644d68b3..1f4eb27d 100644 --- a/app/src/main/java/app/closer/ui/desiresync/DesireSyncScreen.kt +++ b/app/src/main/java/app/closer/ui/desiresync/DesireSyncScreen.kt @@ -207,7 +207,7 @@ class DesireSyncViewModel @Inject constructor( sessionId = existingSessionId val byId = loadNeutralQuestions().associateBy { it.id } val questions = questionIds.mapNotNull { byId[it] } - if (questions.isEmpty()) return fail("Could not load this game.") + if (questions.isEmpty()) return fail("Couldn't load this round's questions.") _uiState.update { it.copy(phase = DesireSyncPhase.INTRO, questions = questions) } observeReveal() } @@ -374,7 +374,7 @@ fun DesireSyncScreen( color = CloserPalette.Romantic ) DesireSyncPhase.ERROR -> DSErrorScreen( - message = state.error ?: "Something went wrong.", + message = state.error ?: "Something didn't load. Go back and try again.", onBack = viewModel::quit ) DesireSyncPhase.SETUP -> DSSetupScreen( diff --git a/app/src/main/java/app/closer/ui/home/HomeViewModel.kt b/app/src/main/java/app/closer/ui/home/HomeViewModel.kt index 94c778b6..cb81987f 100644 --- a/app/src/main/java/app/closer/ui/home/HomeViewModel.kt +++ b/app/src/main/java/app/closer/ui/home/HomeViewModel.kt @@ -138,7 +138,7 @@ class HomeViewModel @Inject constructor( _uiState.update { it.copy( isLoading = false, - error = e.message ?: "Could not load your dashboard." + error = e.message ?: "Couldn't load your space right now." ).withHomeActions() } } 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 e9b6e041..0540a65e 100644 --- a/app/src/main/java/app/closer/ui/howwell/HowWellScreen.kt +++ b/app/src/main/java/app/closer/ui/howwell/HowWellScreen.kt @@ -239,7 +239,7 @@ class HowWellViewModel @Inject constructor( _uiState.update { it.copy(navigateTo = AppRoute.WAITING_FOR_PARTNER) } else -> { Log.w(TAG, "Could not start session", startResult.exceptionOrNull()) - fail("Could not start the game.") + fail("Couldn't start this round. Go back and try again.") } } } @@ -254,7 +254,7 @@ class HowWellViewModel @Inject constructor( sessionId = existingSessionId startedByUserId = startedBy val questions = questionIds.mapNotNull { repository.getQuestionById(it) } - if (questions.isEmpty()) return fail("Could not load this game.") + if (questions.isEmpty()) return fail("Couldn't load this round's questions.") _uiState.update { it.copy(phase = HowWellPhase.INTRO, questions = questions, amSubject = startedBy == uid) } @@ -418,7 +418,7 @@ fun HowWellScreen( color = CloserPalette.PurpleDeep ) HowWellPhase.ERROR -> HowWellErrorScreen( - message = state.error ?: "Something went wrong.", + message = state.error ?: "Something didn't load. Go back and try again.", onBack = viewModel::quit ) HowWellPhase.SETUP -> HWSetupScreen( diff --git a/app/src/main/java/app/closer/ui/paywall/PaywallScreen.kt b/app/src/main/java/app/closer/ui/paywall/PaywallScreen.kt index 6934476b..a19b512f 100644 --- a/app/src/main/java/app/closer/ui/paywall/PaywallScreen.kt +++ b/app/src/main/java/app/closer/ui/paywall/PaywallScreen.kt @@ -115,8 +115,8 @@ fun PaywallScreen( modifier = Modifier.fillMaxWidth() ) uiState.error != null -> ErrorState( - title = "Could not load plans", - message = uiState.error ?: "Something went wrong.", + title = "Couldn't load plans", + message = uiState.error ?: "Check your connection and tap to try again.", retryLabel = "Try again", onRetry = { viewModel.retry() }, modifier = Modifier.fillMaxWidth() diff --git a/app/src/main/java/app/closer/ui/questions/DailyQuestionViewModel.kt b/app/src/main/java/app/closer/ui/questions/DailyQuestionViewModel.kt index 44d76107..50f182c2 100644 --- a/app/src/main/java/app/closer/ui/questions/DailyQuestionViewModel.kt +++ b/app/src/main/java/app/closer/ui/questions/DailyQuestionViewModel.kt @@ -63,7 +63,7 @@ class DailyQuestionViewModel @Inject constructor( crashReporter.recordException(e) _uiState.value = LocalQuestionUiState( isLoading = false, - error = e.message ?: "Could not load today's question." + error = e.message ?: "Couldn't open today's question." ) } } diff --git a/app/src/main/java/app/closer/ui/questions/LocalQuestionContent.kt b/app/src/main/java/app/closer/ui/questions/LocalQuestionContent.kt index 57967a67..36a60730 100644 --- a/app/src/main/java/app/closer/ui/questions/LocalQuestionContent.kt +++ b/app/src/main/java/app/closer/ui/questions/LocalQuestionContent.kt @@ -1,6 +1,11 @@ package app.closer.ui.questions import app.closer.ui.theme.closerBackgroundBrush +import android.provider.Settings +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.slideInVertically import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -33,6 +38,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset @@ -98,6 +104,10 @@ fun LocalQuestionContent( ) else -> { val question = state.question + val context = LocalContext.current + val reducedMotion = Settings.Global.getFloat( + context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f + ) == 0f var helpExpanded by remember(question.id) { mutableStateOf(false) } QuestionMetaRow(question = question) QuestionHeader( @@ -118,7 +128,11 @@ fun LocalQuestionContent( isSubmitting = false ) - if (state.submitted) { + AnimatedVisibility( + visible = state.submitted, + enter = if (reducedMotion) fadeIn(tween(0)) + else fadeIn(tween(260)) + slideInVertically(tween(260)) { it / 3 } + ) { SubmittedAnswerCard(question = question, state = state) } diff --git a/app/src/main/java/app/closer/ui/settings/NotificationSettingsScreen.kt b/app/src/main/java/app/closer/ui/settings/NotificationSettingsScreen.kt index 7f942fe5..e6d8f21a 100644 --- a/app/src/main/java/app/closer/ui/settings/NotificationSettingsScreen.kt +++ b/app/src/main/java/app/closer/ui/settings/NotificationSettingsScreen.kt @@ -130,21 +130,21 @@ fun NotificationSettingsScreen( Column { NotifToggleRow( label = "Daily question", - description = "Remind me to answer today's question", + description = "A gentle nudge when today's question is ready", checked = state.dailyReminderEnabled, onCheckedChange = viewModel::toggleDailyReminder ) Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp) NotifToggleRow( label = "Partner answered", - description = "Notify me when my partner answers", + description = "Let me know when they're ready to reveal", checked = state.partnerAnsweredEnabled, onCheckedChange = viewModel::togglePartnerAnswered ) Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp) NotifToggleRow( - label = "Streak reminder", - description = "Nudge me before the streak resets", + label = "Shared rhythm reminder", + description = "Remind me to keep our shared rhythm going", checked = state.streakReminderEnabled, onCheckedChange = viewModel::toggleStreakReminder ) @@ -167,8 +167,8 @@ fun NotificationSettingsScreen( ) { Column { NotifToggleRow( - label = "Enable quiet hours", - description = "Suppress notifications 10 PM – 8 AM", + label = "Quiet hours", + description = "Pause all notifications from 10 PM to 8 AM", checked = state.quietHoursEnabled, onCheckedChange = viewModel::toggleQuietHours ) @@ -178,7 +178,7 @@ fun NotificationSettingsScreen( Spacer(Modifier.height(8.dp)) Text( - text = "These preferences shape the reminders you see from the app.", + text = "Notifications from Closer are gentle invitations, not alerts. You're always in control of when they arrive.", style = MaterialTheme.typography.bodySmall, color = SettingsMuted, modifier = Modifier.padding(horizontal = 4.dp) 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 5d7e4615..04297e0b 100644 --- a/app/src/main/java/app/closer/ui/thisorthat/ThisOrThatScreen.kt +++ b/app/src/main/java/app/closer/ui/thisorthat/ThisOrThatScreen.kt @@ -238,7 +238,7 @@ class ThisOrThatViewModel @Inject constructor( _uiState.update { it.copy(navigateTo = AppRoute.WAITING_FOR_PARTNER) } else -> { Log.w(TAG, "Could not start session", startResult.exceptionOrNull()) - fail("Could not start the game.") + fail("Couldn't start this round. Go back and try again.") } } } @@ -250,7 +250,7 @@ class ThisOrThatViewModel @Inject constructor( .getOrElse { emptyList() } .associateBy { it.id } val questions = questionIds.mapNotNull { byId[it] } - if (questions.isEmpty()) return fail("Could not load this game.") + if (questions.isEmpty()) return fail("Couldn't load this round's questions.") _uiState.update { it.copy(phase = TotPhase.PLAYING, questions = questions) } observeReveal() } @@ -421,7 +421,7 @@ fun ThisOrThatScreen( color = CloserPalette.PurpleDeep ) TotPhase.ERROR -> ErrorState( - message = state.error ?: "Something went wrong.", + message = state.error ?: "Something didn't load. Go back and try again.", onBack = viewModel::quit ) TotPhase.WAITING -> WaitingForRevealScreen( diff --git a/app/src/main/java/app/closer/ui/wheel/WheelHistoryViewModel.kt b/app/src/main/java/app/closer/ui/wheel/WheelHistoryViewModel.kt index 872748a5..eaefcbb5 100644 --- a/app/src/main/java/app/closer/ui/wheel/WheelHistoryViewModel.kt +++ b/app/src/main/java/app/closer/ui/wheel/WheelHistoryViewModel.kt @@ -58,7 +58,7 @@ class WheelHistoryViewModel @Inject constructor( _uiState.update { it.copy(isLoading = false, sessions = sessions) } } .onFailure { e -> - _uiState.update { it.copy(isLoading = false, error = e.message ?: "Failed to load history") } + _uiState.update { it.copy(isLoading = false, error = e.message ?: "Couldn't load your past games.") } } } } diff --git a/docs/screenshots/01-onboarding.png b/docs/screenshots/01-onboarding.png index 9e64add9..9cf6ff47 100644 Binary files a/docs/screenshots/01-onboarding.png and b/docs/screenshots/01-onboarding.png differ diff --git a/docs/screenshots/02-login.png b/docs/screenshots/02-login.png index 8028b509..a2d8d8ef 100644 Binary files a/docs/screenshots/02-login.png and b/docs/screenshots/02-login.png differ diff --git a/docs/screenshots/03-home.png b/docs/screenshots/03-home.png index 1c171e5a..1c7690de 100644 Binary files a/docs/screenshots/03-home.png and b/docs/screenshots/03-home.png differ diff --git a/docs/screenshots/04-daily-question.png b/docs/screenshots/04-daily-question.png index 8609aa36..39548992 100644 Binary files a/docs/screenshots/04-daily-question.png and b/docs/screenshots/04-daily-question.png differ diff --git a/docs/screenshots/05-question-packs.png b/docs/screenshots/05-question-packs.png index 2f449f16..9309b62e 100644 Binary files a/docs/screenshots/05-question-packs.png and b/docs/screenshots/05-question-packs.png differ diff --git a/docs/screenshots/06-answer-history.png b/docs/screenshots/06-answer-history.png index 863998f6..f7c5966e 100644 Binary files a/docs/screenshots/06-answer-history.png and b/docs/screenshots/06-answer-history.png differ diff --git a/docs/screenshots/07-settings.png b/docs/screenshots/07-settings.png index ed6738bb..b61014e5 100644 Binary files a/docs/screenshots/07-settings.png and b/docs/screenshots/07-settings.png differ