chore(cleanup): remove verified dead files (Tier 1 dead-file sweep)
Removes files confirmed unreferenced by symbol-level git-grep and proven safe by a
clean :app:compileDebugKotlin + compileDebugUnitTestKotlin (no main/test references).
Dead code (every top-level type had 0 external references):
- core/notifications/NotificationHelper.kt — dead duplicate of the live
NotificationChannelSetup (which is what CloserApp/AppMessagingService actually call)
- core/notifications/NotificationPermissionHelper.kt — unused permission helper
- notifications/PartnerNotificationScheduler.kt — superseded by PartnerNotificationManager
- data/questions/QuestionJsonParser.kt — superseded by the Room/asset-DB path
- data/repository/FakeQuestionRepository.kt — orphaned fake, no test/DI consumer
- ui/questions/QuestionDetailViewModel.kt — superseded VM, no composable binds it
- domain/model/{Entitlement,InviteStatus,QuestionSessionStatus}.kt — unused models/enums
(entitlement state is read as Firestore booleans, not this model)
Stray artifacts:
- gitleaks-current.json / gitleaks-history.json — sanitized scan output (history = []),
referenced by no CI/config; regenerable
- 19 stale .gitkeep placeholders in directories that now hold real files
Deliberately KEPT (flagged but not dead): WheelHistoryScreen/ViewModel.kt (named
"WheelHistory" but declare the live, nav-wired GameHistoryScreen/GameHistoryViewModel).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
a8819650d2
commit
21392ec2f3
|
|
@ -1,70 +0,0 @@
|
|||
package app.closer.core.notifications
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import app.closer.MainActivity
|
||||
import app.closer.R
|
||||
|
||||
object NotificationHelper {
|
||||
|
||||
const val CHANNEL_REMINDERS = "reminders"
|
||||
const val CHANNEL_PARTNER = "partner_activity"
|
||||
const val CHANNEL_GAME_ACTIVITY = "game_activity"
|
||||
|
||||
fun createChannels(context: Context) {
|
||||
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
nm.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
CHANNEL_REMINDERS,
|
||||
"Daily reminders",
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
).apply {
|
||||
description = "Gentle nudges for today's question and shared moments"
|
||||
}
|
||||
)
|
||||
nm.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
CHANNEL_PARTNER,
|
||||
"Partner activity",
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
).apply {
|
||||
description = "When your partner answers, reveals, or opens a conversation"
|
||||
}
|
||||
)
|
||||
nm.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
CHANNEL_GAME_ACTIVITY,
|
||||
"Games",
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
).apply {
|
||||
description = "When your partner starts or completes a game round"
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun show(context: Context, id: Int, channelId: String, title: String, body: String) {
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
}
|
||||
val pending = PendingIntent.getActivity(
|
||||
context, 0, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
val notification = NotificationCompat.Builder(context, channelId)
|
||||
.setSmallIcon(R.drawable.ic_notification_closer)
|
||||
.setContentTitle(title)
|
||||
.setContentText(body)
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(pending)
|
||||
.build()
|
||||
|
||||
if (NotificationManagerCompat.from(context).areNotificationsEnabled()) {
|
||||
NotificationManagerCompat.from(context).notify(id, notification)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
package app.closer.core.notifications
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Handles Android 13+ (API 33) POST_NOTIFICATIONS permission requests.
|
||||
*
|
||||
* Callers should:
|
||||
* 1. Check [isGranted] before asking.
|
||||
* 2. If [shouldShowRationale] is true, show an in-app rationale first.
|
||||
* 3. Invoke [requestPermission] from an Activity context.
|
||||
*/
|
||||
@Singleton
|
||||
class NotificationPermissionHelper @Inject constructor() {
|
||||
|
||||
val permission: String = Manifest.permission.POST_NOTIFICATIONS
|
||||
|
||||
fun isGranted(context: Context): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
|
||||
} else {
|
||||
// Pre-Android 13 notifications are granted at install time.
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun shouldShowRationale(activity: Activity): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun requestPermission(activity: Activity, requestCode: Int = REQUEST_CODE) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
ActivityCompat.requestPermissions(
|
||||
activity,
|
||||
arrayOf(permission),
|
||||
requestCode
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onRequestResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
): Boolean {
|
||||
if (requestCode != REQUEST_CODE) return false
|
||||
|
||||
val index = permissions.indexOf(permission)
|
||||
if (index < 0 || index >= grantResults.size) return false
|
||||
|
||||
return grantResults[index] == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val REQUEST_CODE = 9001
|
||||
}
|
||||
}
|
||||
|
|
@ -1,199 +0,0 @@
|
|||
package app.closer.data.questions
|
||||
|
||||
import android.util.Log
|
||||
import app.closer.BuildConfig
|
||||
import app.closer.domain.model.*
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
/**
|
||||
* Parses a JSON file containing question data in the v2 schema format.
|
||||
*
|
||||
* Expected JSON structure:
|
||||
* {
|
||||
* "category": { ... },
|
||||
* "questions": [
|
||||
* {
|
||||
* "id": "...",
|
||||
* "category_id": "...",
|
||||
* "type": "written|single_choice|multi_choice|scale|this_or_that",
|
||||
* "text": "...",
|
||||
* "depth": 1-5,
|
||||
* "access": "free|premium",
|
||||
* "tags": [...],
|
||||
* "answer_config": { ... }
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
object QuestionJsonParser {
|
||||
|
||||
private const val TAG = "QuestionJsonParser"
|
||||
|
||||
data class ParsedQuestionBundle(
|
||||
val category: QuestionCategory,
|
||||
val questions: List<Question>
|
||||
)
|
||||
|
||||
fun parseFromFile(jsonFilePath: String): ParsedQuestionBundle? {
|
||||
val jsonContent = try {
|
||||
java.io.File(jsonFilePath).readText()
|
||||
} catch (e: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.e(TAG, "Failed to read JSON file: ${e.message}")
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
return parseFromJsonString(jsonContent)
|
||||
}
|
||||
|
||||
fun parseFromJsonString(jsonString: String): ParsedQuestionBundle? {
|
||||
return try {
|
||||
val root = JSONObject(jsonString)
|
||||
val categoryObj = root.getJSONObject("category")
|
||||
val questionsArray = root.getJSONArray("questions")
|
||||
|
||||
val category = parseCategory(categoryObj)
|
||||
val questions = mutableListOf<Question>()
|
||||
|
||||
for (i in 0 until questionsArray.length()) {
|
||||
val questionObj = questionsArray.getJSONObject(i)
|
||||
questionObj?.let { q ->
|
||||
questions.add(parseQuestion(q, category.id))
|
||||
}
|
||||
}
|
||||
|
||||
ParsedQuestionBundle(category, questions)
|
||||
} catch (e: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.e(TAG, "Failed to parse JSON: ${e.message}")
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseCategory(obj: JSONObject): QuestionCategory {
|
||||
return QuestionCategory(
|
||||
id = obj.optString("id", ""),
|
||||
displayName = obj.optString("display_name", ""),
|
||||
description = obj.optString("description", ""),
|
||||
access = obj.optString("access", "free"),
|
||||
iconName = obj.optString("icon_name", "message")
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseQuestion(obj: JSONObject, categoryId: String): Question {
|
||||
val type = obj.optString("type", "written")
|
||||
val tags = obj.optJSONArray("tags")?.let { arr ->
|
||||
mutableListOf<String>().apply {
|
||||
for (i in 0 until arr.length()) {
|
||||
add(arr.optString(i))
|
||||
}
|
||||
}.toList()
|
||||
} ?: emptyList()
|
||||
|
||||
val answerConfig = parseAnswerConfig(obj, type)
|
||||
|
||||
return Question(
|
||||
id = obj.optString("id", ""),
|
||||
text = obj.optString("text", ""),
|
||||
category = categoryId,
|
||||
depthLevel = obj.optInt("depth", 1),
|
||||
isPremium = obj.optString("access", "free") == "premium",
|
||||
type = type,
|
||||
tags = tags,
|
||||
answerConfig = answerConfig
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseAnswerConfig(obj: JSONObject, type: String): AnswerConfig? {
|
||||
val answerConfigObj = obj.optJSONObject("answer_config")
|
||||
if (answerConfigObj == null) return null
|
||||
|
||||
return when (type) {
|
||||
"written" -> {
|
||||
WrittenAnswerConfigImpl(
|
||||
config = WrittenAnswerConfig(
|
||||
minLength = answerConfigObj.optInt("min_length", 1),
|
||||
maxLength = answerConfigObj.optInt("max_length", 1000),
|
||||
placeholder = answerConfigObj.optString("placeholder", "Write your answer...")
|
||||
)
|
||||
)
|
||||
}
|
||||
"single_choice" -> {
|
||||
val options = mutableListOf<ChoiceOption>()
|
||||
val optionsArray = obj.optJSONArray("options")
|
||||
if (optionsArray != null) {
|
||||
for (i in 0 until optionsArray.length()) {
|
||||
val opt = optionsArray.optJSONObject(i)
|
||||
opt?.let { o ->
|
||||
options.add(ChoiceOption(
|
||||
id = o.optString("id", ""),
|
||||
text = o.optString("text", "")
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
ChoiceAnswerConfigImpl(
|
||||
config = ChoiceAnswerConfig(options = options.toList())
|
||||
)
|
||||
}
|
||||
"scale" -> {
|
||||
ScaleAnswerConfigImpl(
|
||||
config = ScaleAnswerConfig(
|
||||
minScale = answerConfigObj.optInt("min_scale", 1),
|
||||
maxScale = answerConfigObj.optInt("max_scale", 5),
|
||||
minLabel = answerConfigObj.optString("min_label", ""),
|
||||
maxLabel = answerConfigObj.optString("max_label", ""),
|
||||
scaleStep = answerConfigObj.optInt("scale_step", 1)
|
||||
)
|
||||
)
|
||||
}
|
||||
"this_or_that" -> {
|
||||
val optionsArray = obj.optJSONArray("options")
|
||||
if (optionsArray?.length() == 2) {
|
||||
val optA = optionsArray.optJSONObject(0)
|
||||
val optB = optionsArray.optJSONObject(1)
|
||||
ThisOrThatAnswerConfigImpl(
|
||||
config = ThisOrThatAnswerConfig(
|
||||
optionA = ChoiceOption(
|
||||
id = optA?.optString("id", "") ?: "",
|
||||
text = optA?.optString("text", "") ?: ""
|
||||
),
|
||||
optionB = ChoiceOption(
|
||||
id = optB?.optString("id", "") ?: "",
|
||||
text = optB?.optString("text", "") ?: ""
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
"multi_choice" -> {
|
||||
val options = mutableListOf<ChoiceOption>()
|
||||
val optionsArray = obj.optJSONArray("options")
|
||||
if (optionsArray != null) {
|
||||
for (i in 0 until optionsArray.length()) {
|
||||
val opt = optionsArray.optJSONObject(i)
|
||||
opt?.let { o ->
|
||||
options.add(ChoiceOption(
|
||||
id = o.optString("id", ""),
|
||||
text = o.optString("text", "")
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
ChoiceAnswerConfigImpl(
|
||||
type = "multi_choice",
|
||||
config = ChoiceAnswerConfig(
|
||||
options = options.toList(),
|
||||
maxSelections = answerConfigObj.optInt("max_selections", 0)
|
||||
)
|
||||
)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
package app.closer.data.repository
|
||||
|
||||
import app.closer.domain.model.Question
|
||||
import app.closer.domain.model.QuestionCategory
|
||||
import app.closer.domain.repository.QuestionRepository
|
||||
|
||||
class FakeQuestionRepository : QuestionRepository {
|
||||
override suspend fun getDailyQuestion(): Question? = null
|
||||
override suspend fun getDailyQuestionForMode(modeTag: String, isPremium: Boolean): Question? = null
|
||||
|
||||
override suspend fun getQuestionById(id: String): Question? = null
|
||||
|
||||
override suspend fun getQuestionsByCategory(categoryId: String): List<Question> = emptyList()
|
||||
|
||||
override suspend fun getCategories(): List<QuestionCategory> = emptyList()
|
||||
|
||||
override suspend fun getCategoryById(id: String): QuestionCategory? = null
|
||||
|
||||
override suspend fun getQuestionCountByCategory(categoryId: String): Int = 0
|
||||
|
||||
override suspend fun getQuestionsByType(type: String): List<Question> = emptyList()
|
||||
|
||||
override suspend fun getQuestionsForPrediction(): List<Question> = emptyList()
|
||||
|
||||
override suspend fun getDesireSyncQuestions(sex: String): List<Question> = emptyList()
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
package app.closer.domain.model
|
||||
|
||||
data class Entitlement(
|
||||
val id: String = "",
|
||||
val userId: String = "",
|
||||
val source: String = "",
|
||||
val productId: String = "",
|
||||
val isActive: Boolean = false,
|
||||
val expiresAt: Long? = null,
|
||||
val updatedAt: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
package app.closer.domain.model
|
||||
|
||||
enum class InviteStatus {
|
||||
PENDING,
|
||||
ACCEPTED,
|
||||
EXPIRED,
|
||||
CANCELLED
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
package app.closer.domain.model
|
||||
|
||||
enum class QuestionSessionStatus {
|
||||
ACTIVE,
|
||||
COMPLETED
|
||||
}
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
package app.closer.notifications
|
||||
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Schedules partner-trigger notifications in response to partner actions.
|
||||
*
|
||||
* This class is intentionally synchronous: it validates quiet hours, opt-outs,
|
||||
* and rate limits at the moment the event is received. Background deferral is
|
||||
* handled by the backend FCM delivery schedule and [PartnerNotificationManager].
|
||||
*/
|
||||
class PartnerNotificationScheduler @Inject constructor(
|
||||
private val notificationManager: PartnerNotificationManager
|
||||
) {
|
||||
|
||||
/**
|
||||
* The partner answered today's question; prompt the user to answer too.
|
||||
*/
|
||||
suspend fun onPartnerAnswered(coupleId: String, questionId: String? = null) {
|
||||
notificationManager.show(
|
||||
PartnerNotificationType.PARTNER_ANSWERED,
|
||||
coupleId,
|
||||
PartnerNotificationPayload(questionId = questionId)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Both partners have answered and the reveal is ready.
|
||||
*/
|
||||
suspend fun onRevealReady(coupleId: String, questionId: String) {
|
||||
notificationManager.show(
|
||||
PartnerNotificationType.REVEAL_READY,
|
||||
coupleId,
|
||||
PartnerNotificationPayload(questionId = questionId)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The partner started a game for the couple.
|
||||
*/
|
||||
suspend fun onPartnerStartedGame(coupleId: String, gameSessionId: String? = null) {
|
||||
notificationManager.show(
|
||||
PartnerNotificationType.PARTNER_STARTED_GAME,
|
||||
coupleId,
|
||||
PartnerNotificationPayload(gameSessionId = gameSessionId)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The partner completed their side of a two-player game.
|
||||
*/
|
||||
suspend fun onPartnerCompletedPart(coupleId: String, gameSessionId: String? = null) {
|
||||
notificationManager.show(
|
||||
PartnerNotificationType.PARTNER_COMPLETED_PART,
|
||||
coupleId,
|
||||
PartnerNotificationPayload(gameSessionId = gameSessionId)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A daily connection challenge is waiting for the couple.
|
||||
*/
|
||||
suspend fun onChallengeWaiting(coupleId: String, challengeId: String? = null) {
|
||||
notificationManager.show(
|
||||
PartnerNotificationType.CHALLENGE_WAITING,
|
||||
coupleId,
|
||||
PartnerNotificationPayload(challengeId = challengeId)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A previously sealed memory capsule reached its unlock date.
|
||||
*/
|
||||
suspend fun onCapsuleUnlocked(coupleId: String, capsuleId: String? = null) {
|
||||
notificationManager.show(
|
||||
PartnerNotificationType.CAPSULE_UNLOCKED,
|
||||
coupleId,
|
||||
PartnerNotificationPayload(capsuleId = capsuleId)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
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 app.closer.ui.components.TextInputLimits
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@HiltViewModel
|
||||
class QuestionDetailViewModel @Inject constructor(
|
||||
private val repository: QuestionRepository,
|
||||
private val localAnswerRepository: LocalAnswerRepository,
|
||||
savedStateHandle: SavedStateHandle
|
||||
) : ViewModel() {
|
||||
|
||||
private val questionId: String = savedStateHandle["questionId"] ?: ""
|
||||
|
||||
private val _uiState = MutableStateFlow(LocalQuestionUiState())
|
||||
val uiState: StateFlow<LocalQuestionUiState> = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
loadQuestion()
|
||||
}
|
||||
|
||||
fun loadQuestion() {
|
||||
viewModelScope.launch {
|
||||
_uiState.value = LocalQuestionUiState(isLoading = true)
|
||||
try {
|
||||
val question = repository.getQuestionById(questionId)
|
||||
val answer = question?.let { localAnswerRepository.getAnswer(it.id) }
|
||||
_uiState.value = LocalQuestionUiState(
|
||||
isLoading = false,
|
||||
question = question,
|
||||
pendingScaleValue = defaultScaleValue(question)
|
||||
).withLocalAnswer(answer)
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = LocalQuestionUiState(
|
||||
isLoading = false,
|
||||
error = e.message ?: "Could not load this question."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateWrittenText(text: String) {
|
||||
_uiState.update { it.copy(pendingWrittenText = text.take(MAX_ANSWER_LENGTH), submitted = false) }
|
||||
}
|
||||
|
||||
fun toggleOption(optionId: String) {
|
||||
_uiState.update { state ->
|
||||
val question = state.question ?: return@update state
|
||||
val updated = if (question.type == "multi_choice") {
|
||||
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)
|
||||
}
|
||||
state.copy(pendingSelectedOptionIds = updated, submitted = false)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateScale(value: Int) {
|
||||
_uiState.update { it.copy(pendingScaleValue = value, submitted = false) }
|
||||
}
|
||||
|
||||
fun submitAnswer() {
|
||||
val state = _uiState.value
|
||||
val question = state.question ?: return
|
||||
if (!canSubmit(state)) return
|
||||
viewModelScope.launch {
|
||||
localAnswerRepository.saveAnswer(state.toLocalAnswer(question))
|
||||
_uiState.update { it.copy(submitted = true) }
|
||||
}
|
||||
}
|
||||
|
||||
fun canSubmit(): Boolean = canSubmit(_uiState.value)
|
||||
|
||||
companion object {
|
||||
const val MAX_ANSWER_LENGTH = TextInputLimits.WRITTEN_ANSWER
|
||||
}
|
||||
|
||||
private fun canSubmit(state: LocalQuestionUiState): Boolean {
|
||||
val question = state.question ?: return false
|
||||
return when (question.type) {
|
||||
"written" -> state.pendingWrittenText.isNotBlank()
|
||||
"single_choice", "multi_choice", "this_or_that" -> state.pendingSelectedOptionIds.isNotEmpty()
|
||||
"scale" -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
[
|
||||
{
|
||||
"Description": "Generic API Key",
|
||||
"StartLine": 31,
|
||||
"EndLine": 31,
|
||||
"StartColumn": 21,
|
||||
"EndColumn": 67,
|
||||
"Match": "key\": \"REDACTED\"",
|
||||
"Secret": "REDACTED",
|
||||
"File": "app/google-services.json",
|
||||
"SymlinkFile": "",
|
||||
"Commit": "",
|
||||
"Entropy": 4.7851515,
|
||||
"Author": "",
|
||||
"Email": "",
|
||||
"Date": "",
|
||||
"Message": "",
|
||||
"Tags": [],
|
||||
"RuleID": "generic-api-key",
|
||||
"Fingerprint": "app/google-services.json:generic-api-key:31"
|
||||
},
|
||||
{
|
||||
"Description": "Generic API Key",
|
||||
"StartLine": 68,
|
||||
"EndLine": 68,
|
||||
"StartColumn": 21,
|
||||
"EndColumn": 67,
|
||||
"Match": "key\": \"REDACTED\"",
|
||||
"Secret": "REDACTED",
|
||||
"File": "app/google-services.json",
|
||||
"SymlinkFile": "",
|
||||
"Commit": "",
|
||||
"Entropy": 4.7851515,
|
||||
"Author": "",
|
||||
"Email": "",
|
||||
"Date": "",
|
||||
"Message": "",
|
||||
"Tags": [],
|
||||
"RuleID": "generic-api-key",
|
||||
"Fingerprint": "app/google-services.json:generic-api-key:68"
|
||||
}
|
||||
]
|
||||
|
|
@ -1 +0,0 @@
|
|||
[]
|
||||
Loading…
Reference in New Issue