eee8f3be34b5
feat: Complete feedback parity (v2.3.28)
- time
- 2026-06-26 09:01 UTC · 4 months ago
- machine
- koh2
- parent
- 31256f603665
↓ peach-android-eee8f3be.tar.gzatlas at this save
files changed4 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 −0app/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 {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 )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("&", "&")47+ .replace("<", "<")48+ .replace(">", ">")49+ .replace("\"", """)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")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 <peach> & "friends"\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+}