eee8f3be34b5

feat: Complete feedback parity (v2.3.28)

- Cap and sanitize feedback messages before submit - Match browser feedback send-state labels and version metadata copy - Mark Pro and feedback parity complete in the browser parity map Verified: ./gradlew compileDebugKotlin testDebugUnitTest --tests com.peachpasswords.ui.settings.SettingsFeedbackTest Verified: ./gradlew testDebugUnitTest :app:assembleDebug lintDebug

time
2026-06-26 09:01 UTC · 4 months ago
machine
koh2
parent
31256f603665
4 files app/build.gradle+2 −2app/src/main/java/com/peachpasswords/ui/settings/SettingsCardBodies.kt+12 −3app/src/main/java/com/peachpasswords/ui/settings/SettingsViewModel.kt+31 −1app/src/test/java/com/peachpasswords/ui/settings/SettingsFeedbackTest.kt+38 −0
app/build.gradle+2 −2
@@ -16,8 +16,8 @@1616         applicationId "com.peachpasswords"1717         minSdk 261818         targetSdk 3519-        versionCode 23270020-        versionName "2.3.27"19+        versionCode 23280020+        versionName "2.3.28"2121 2222         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"2323         vectorDrawables {

view file

app/src/main/java/com/peachpasswords/ui/settings/SettingsCardBodies.kt+12 −3
@@ -52,6 +52,7 @@5252 import androidx.compose.ui.unit.em5353 import androidx.compose.ui.unit.sp5454 import androidx.compose.material3.Text55+import com.peachpasswords.BuildConfig5556 import com.peachpasswords.ui.PeachNav5657 import com.peachpasswords.ui.sync.SyncViewModel5758 import com.peachpasswords.ui.theme.ACCENT_COLORS@@ -969,16 +970,24 @@969970     ) {970971         if (message.isEmpty()) Text("Describe the issue or idea...", color = fg.copy(alpha = 0.42f), fontFamily = PeachType.Body, fontSize = 12.sp)971972         BasicTextField(972-            value = message, onValueChange = { message = it },973+            value = message, onValueChange = { message = it.take(MAX_FEEDBACK_MESSAGE_LENGTH) },973974             enabled = !sending,974975             textStyle = TextStyle(color = fg, fontFamily = PeachType.Body, fontSize = 12.sp),975976             cursorBrush = SolidColor(fg), modifier = Modifier.fillMaxWidth(),976977         )977978     }979+    Text(980+        "Version ${BuildConfig.VERSION_NAME} · will be included automatically",981+        modifier = Modifier.padding(top = 6.dp),982+        color = fg.copy(alpha = 0.52f),983+        fontFamily = PeachType.Mono,984+        fontSize = 10.sp,985+        lineHeight = 14.sp,986+    )978987     Spacer(Modifier.height(8.dp))979988 980989     Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {981-        val canSend = !sending && message.isNotBlank()990+        val canSend = !sending && sanitizeFeedbackMessage(message).isNotBlank()982991         Box(983992             Modifier.clip(RoundedCornerShape(8.dp))984993                 .border(1.dp, fg.copy(alpha = if (canSend) 0.28f else 0.14f), RoundedCornerShape(8.dp))@@ -987,7 +996,7 @@987996                 .padding(horizontal = 12.dp, vertical = 6.dp),988997         ) {989998             Text(990-                if (state is FeedbackState.Error) "Retry" else "Send",999+                feedbackSubmitLabel(state),9911000                 color = fg.copy(alpha = if (canSend) 1f else 0.4f),9921001                 fontFamily = PeachType.Mono, fontSize = 11.sp,9931002             )

view file

app/src/main/java/com/peachpasswords/ui/settings/SettingsViewModel.kt+31 −1
@@ -27,6 +27,34 @@2727 import java.security.MessageDigest2828 import javax.inject.Inject2929 30+internal const val MAX_FEEDBACK_MESSAGE_LENGTH = 400031+32+internal fun sanitizeFeedbackMessage(value: String): String {33+    val trimmed = value.trim().take(MAX_FEEDBACK_MESSAGE_LENGTH)34+    val withoutControlChars = buildString(trimmed.length) {35+        trimmed.forEach { char ->36+            val code = char.code37+            val strip = code in 0x0000..0x0008 ||38+                code == 0x000B ||39+                code == 0x000C ||40+                code in 0x000E..0x001F ||41+                code == 0x007F42+            if (!strip) append(char)43+        }44+    }45+    return withoutControlChars46+        .replace("&", "&amp;")47+        .replace("<", "&lt;")48+        .replace(">", "&gt;")49+        .replace("\"", "&quot;")50+}51+52+internal fun feedbackSubmitLabel(state: FeedbackState): String = when (state) {53+    FeedbackState.Sending -> "Sending..."54+    is FeedbackState.Error -> "Failed - try again"55+    else -> "Send"56+}57+3058 @HiltViewModel3159 class SettingsViewModel @Inject constructor(3260     private val vault: VaultRepository,@@ -88,10 +116,12 @@88116 89117     fun sendFeedback(type: String, message: String) {90118         if (_feedbackState.value is FeedbackState.Sending) return119+        val sanitized = sanitizeFeedbackMessage(message)120+        if (sanitized.isBlank()) return91121         _feedbackState.value = FeedbackState.Sending92122         viewModelScope.launch {93123             _feedbackState.value = try {94-                feedback.submit(type, message)124+                feedback.submit(type, sanitized)95125                 FeedbackState.Sent96126             } catch (e: com.peachpasswords.feedback.FeedbackUnavailableException) {97127                 FeedbackState.Error("Feedback unavailable — endpoint not configured")

view file

app/src/test/java/com/peachpasswords/ui/settings/SettingsFeedbackTest.kt+38 −0
@@ -0,0 +1,38 @@1+package com.peachpasswords.ui.settings2+3+import com.peachpasswords.feedback.FeedbackState4+import org.junit.Assert.assertEquals5+import org.junit.Assert.assertFalse6+import org.junit.Assert.assertTrue7+import org.junit.Test8+9+class SettingsFeedbackTest {10+11+    @Test12+    fun sanitizeFeedbackMessage_matchesBrowserLimitsAndEscaping() {13+        val raw = "  hello\u0000 <peach> & \"friends\"\nthere\u007F  "14+15+        val sanitized = sanitizeFeedbackMessage(raw)16+17+        assertEquals("hello &lt;peach&gt; &amp; &quot;friends&quot;\nthere", sanitized)18+        assertFalse(sanitized.contains('\u0000'))19+        assertFalse(sanitized.contains('\u007F'))20+        assertTrue(sanitized.contains('\n'))21+    }22+23+    @Test24+    fun sanitizeFeedbackMessage_capsAtBrowserLimitBeforeEscaping() {25+        val raw = "x".repeat(MAX_FEEDBACK_MESSAGE_LENGTH + 50)26+27+        val sanitized = sanitizeFeedbackMessage(raw)28+29+        assertEquals(MAX_FEEDBACK_MESSAGE_LENGTH, sanitized.length)30+    }31+32+    @Test33+    fun feedbackSubmitLabel_usesBrowserCopy() {34+        assertEquals("Send", feedbackSubmitLabel(FeedbackState.Idle))35+        assertEquals("Sending...", feedbackSubmitLabel(FeedbackState.Sending))36+        assertEquals("Failed - try again", feedbackSubmitLabel(FeedbackState.Error("nope")))37+    }38+}

view file