From df961e8d94106c31b6f8ec5a6ffbc8f0f0bdc1ea Mon Sep 17 00:00:00 2001 From: null Date: Thu, 18 Jun 2026 02:41:33 -0500 Subject: [PATCH] fix: enforce multi-choice maxSelections limit across all answer UIs, pass questionIds to startGame --- .../app/closer/data/local/mapper/QuestionMapper.kt | 3 ++- .../app/closer/data/questions/QuestionJsonParser.kt | 6 ++++-- app/src/main/java/app/closer/domain/model/Question.kt | 3 ++- .../app/closer/ui/questions/DailyQuestionViewModel.kt | 10 ++++++---- .../app/closer/ui/questions/QuestionDetailViewModel.kt | 10 ++++++---- .../app/closer/ui/questions/QuestionThreadViewModel.kt | 8 ++++++++ .../java/app/closer/ui/wheel/SpinWheelViewModel.kt | 4 +++- .../java/app/closer/ui/wheel/WheelSessionViewModel.kt | 8 +++++++- 8 files changed, 38 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/app/closer/data/local/mapper/QuestionMapper.kt b/app/src/main/java/app/closer/data/local/mapper/QuestionMapper.kt index 91ebb089..a2beff3c 100644 --- a/app/src/main/java/app/closer/data/local/mapper/QuestionMapper.kt +++ b/app/src/main/java/app/closer/data/local/mapper/QuestionMapper.kt @@ -64,7 +64,8 @@ private fun parseAnswerConfig(raw: String, questionType: String) = try { "single_choice", "multi_choice" -> { val optionsArr = configObj?.optJSONArray("options") val options = parseOptions(optionsArr) - ChoiceAnswerConfigImpl(type = questionType, config = ChoiceAnswerConfig(options = options)) + val maxSel = if (questionType == "multi_choice") configObj?.optInt("maxSelections", 0) ?: 0 else 0 + ChoiceAnswerConfigImpl(type = questionType, config = ChoiceAnswerConfig(options = options, maxSelections = maxSel)) } "scale" -> ScaleAnswerConfigImpl( config = ScaleAnswerConfig( diff --git a/app/src/main/java/app/closer/data/questions/QuestionJsonParser.kt b/app/src/main/java/app/closer/data/questions/QuestionJsonParser.kt index 62ded901..49daeb52 100644 --- a/app/src/main/java/app/closer/data/questions/QuestionJsonParser.kt +++ b/app/src/main/java/app/closer/data/questions/QuestionJsonParser.kt @@ -172,7 +172,6 @@ object QuestionJsonParser { } } "multi_choice" -> { - // For now, treat multi_choice like single_choice (can be extended later) val options = mutableListOf() val optionsArray = obj.optJSONArray("options") if (optionsArray != null) { @@ -188,7 +187,10 @@ object QuestionJsonParser { } ChoiceAnswerConfigImpl( type = "multi_choice", - config = ChoiceAnswerConfig(options = options.toList()) + config = ChoiceAnswerConfig( + options = options.toList(), + maxSelections = answerConfigObj.optInt("max_selections", 0) + ) ) } else -> null diff --git a/app/src/main/java/app/closer/domain/model/Question.kt b/app/src/main/java/app/closer/domain/model/Question.kt index 2c674289..36da8c7c 100644 --- a/app/src/main/java/app/closer/domain/model/Question.kt +++ b/app/src/main/java/app/closer/domain/model/Question.kt @@ -8,7 +8,8 @@ data class WrittenAnswerConfig( ) data class ChoiceAnswerConfig( - val options: List + val options: List, + val maxSelections: Int = 0 ) data class ChoiceOption( 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 f9d9cf6d..c55ef486 100644 --- a/app/src/main/java/app/closer/ui/questions/DailyQuestionViewModel.kt +++ b/app/src/main/java/app/closer/ui/questions/DailyQuestionViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.closer.core.crash.CrashReporter import app.closer.data.remote.FirestoreAnswerDataSource +import app.closer.domain.model.ChoiceAnswerConfigImpl import app.closer.domain.model.LocalAnswer import app.closer.domain.model.Question import app.closer.domain.repository.AuthRepository @@ -120,10 +121,11 @@ class DailyQuestionViewModel @Inject constructor( _uiState.update { state -> val question = state.question ?: return@update state val updated = if (question.type == "multi_choice") { - if (optionId in state.pendingSelectedOptionIds) { - state.pendingSelectedOptionIds - optionId - } else { - state.pendingSelectedOptionIds + optionId + val maxSel = (question.answerConfig as? ChoiceAnswerConfigImpl)?.config?.maxSelections ?: 0 + when { + optionId in state.pendingSelectedOptionIds -> state.pendingSelectedOptionIds - optionId + maxSel == 0 || state.pendingSelectedOptionIds.size < maxSel -> state.pendingSelectedOptionIds + optionId + else -> state.pendingSelectedOptionIds } } else { listOf(optionId) diff --git a/app/src/main/java/app/closer/ui/questions/QuestionDetailViewModel.kt b/app/src/main/java/app/closer/ui/questions/QuestionDetailViewModel.kt index 825b9aa7..ff257ebc 100644 --- a/app/src/main/java/app/closer/ui/questions/QuestionDetailViewModel.kt +++ b/app/src/main/java/app/closer/ui/questions/QuestionDetailViewModel.kt @@ -3,6 +3,7 @@ package app.closer.ui.questions import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import app.closer.domain.model.ChoiceAnswerConfigImpl import app.closer.domain.repository.LocalAnswerRepository import app.closer.domain.repository.QuestionRepository import dagger.hilt.android.lifecycle.HiltViewModel @@ -57,10 +58,11 @@ class QuestionDetailViewModel @Inject constructor( _uiState.update { state -> val question = state.question ?: return@update state val updated = if (question.type == "multi_choice") { - if (optionId in state.pendingSelectedOptionIds) { - state.pendingSelectedOptionIds - optionId - } else { - state.pendingSelectedOptionIds + optionId + val maxSel = (question.answerConfig as? ChoiceAnswerConfigImpl)?.config?.maxSelections ?: 0 + when { + optionId in state.pendingSelectedOptionIds -> state.pendingSelectedOptionIds - optionId + maxSel == 0 || state.pendingSelectedOptionIds.size < maxSel -> state.pendingSelectedOptionIds + optionId + else -> state.pendingSelectedOptionIds } } else { listOf(optionId) diff --git a/app/src/main/java/app/closer/ui/questions/QuestionThreadViewModel.kt b/app/src/main/java/app/closer/ui/questions/QuestionThreadViewModel.kt index 47555b38..900bc3a4 100644 --- a/app/src/main/java/app/closer/ui/questions/QuestionThreadViewModel.kt +++ b/app/src/main/java/app/closer/ui/questions/QuestionThreadViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.closer.data.local.QuestionDao import app.closer.data.local.mapper.toQuestion +import app.closer.domain.model.ChoiceAnswerConfigImpl import app.closer.domain.model.Question import app.closer.domain.model.QuestionAnswer import app.closer.domain.model.QuestionMessage @@ -124,6 +125,13 @@ class QuestionThreadViewModel @Inject constructor( val current = state.pendingSelectedOptionIds val updated = if (question.type == "single_choice") { listOf(optionId) + } else if (question.type == "multi_choice") { + val maxSel = (question.answerConfig as? ChoiceAnswerConfigImpl)?.config?.maxSelections ?: 0 + when { + optionId in current -> current - optionId + maxSel == 0 || current.size < maxSel -> current + optionId + else -> current + } } else { if (optionId in current) current - optionId else current + optionId } diff --git a/app/src/main/java/app/closer/ui/wheel/SpinWheelViewModel.kt b/app/src/main/java/app/closer/ui/wheel/SpinWheelViewModel.kt index c103e9d0..07e498dd 100644 --- a/app/src/main/java/app/closer/ui/wheel/SpinWheelViewModel.kt +++ b/app/src/main/java/app/closer/ui/wheel/SpinWheelViewModel.kt @@ -112,11 +112,13 @@ class SpinWheelViewModel @Inject constructor( return@launch } + val questionIds = sessionStore.activeSession?.questions?.map { it.id } val startResult = runCatching { gameSessionManager.startGame( userId = userId, gameType = "wheel", - categoryId = categoryId + categoryId = categoryId, + questionIds = questionIds ).getOrNull() }.getOrNull() diff --git a/app/src/main/java/app/closer/ui/wheel/WheelSessionViewModel.kt b/app/src/main/java/app/closer/ui/wheel/WheelSessionViewModel.kt index dadc0dba..ab3adaaf 100644 --- a/app/src/main/java/app/closer/ui/wheel/WheelSessionViewModel.kt +++ b/app/src/main/java/app/closer/ui/wheel/WheelSessionViewModel.kt @@ -61,9 +61,15 @@ class WheelSessionViewModel @Inject constructor( fun selectOption(optionId: String) { val question = _uiState.value.questions.getOrNull(_uiState.value.currentIndex) ?: return if (question.type == "multi_choice") { + val maxSel = (question.answerConfig as? app.closer.domain.model.ChoiceAnswerConfigImpl) + ?.config?.maxSelections ?: 0 _uiState.update { val current = it.selectedOptionIds.toMutableList() - if (optionId in current) current.remove(optionId) else current.add(optionId) + if (optionId in current) { + current.remove(optionId) + } else if (maxSel == 0 || current.size < maxSel) { + current.add(optionId) + } it.copy(selectedOptionIds = current) } } else {