Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -100,25 +100,17 @@ class LostFoundEditViewModel @Inject constructor(
wechat: String,
phone: String
) {
val normalizedTitle = title.trim()
val normalizedDescription = description.trim()
val normalizedLocation = location.trim()
val normalizedQq = qq.trim()
val normalizedWechat = wechat.trim()
val normalizedPhone = phone.trim()
val form = LostFoundFormValidator.normalize(title, description, location, qq, wechat, phone)
val normalizedTitle = form.title
val normalizedDescription = form.description
val normalizedLocation = form.location
val normalizedQq = form.qq
val normalizedWechat = form.wechat
val normalizedPhone = form.phone
val errorRes = LostFoundFormValidator.errorRes(form)

when {
normalizedTitle.isBlank() -> emitMessage(context.getString(R.string.lost_found_publish_title_required))
normalizedTitle.length > 25 -> emitMessage(context.getString(R.string.lost_found_publish_title_too_long))
normalizedDescription.isBlank() -> emitMessage(context.getString(R.string.lost_found_publish_description_required))
normalizedDescription.length > 100 -> emitMessage(context.getString(R.string.lost_found_publish_description_too_long))
normalizedLocation.isBlank() -> emitMessage(context.getString(R.string.lost_found_publish_location_required))
normalizedLocation.length > 30 -> emitMessage(context.getString(R.string.lost_found_publish_location_too_long))
normalizedQq.isBlank() && normalizedWechat.isBlank() && normalizedPhone.isBlank() ->
emitMessage(context.getString(R.string.lost_found_publish_contact_required))
normalizedQq.length > 20 -> emitMessage(context.getString(R.string.lost_found_publish_qq_too_long))
normalizedWechat.length > 20 -> emitMessage(context.getString(R.string.lost_found_publish_wechat_too_long))
normalizedPhone.length > 11 -> emitMessage(context.getString(R.string.lost_found_publish_phone_too_long))
errorRes != null -> emitMessage(context.getString(errorRes))
else -> {
viewModelScope.launch {
_state.update { it.copy(isSubmitting = true, error = null) }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package cn.gdeiassistant.ui.lostfound

import cn.gdeiassistant.R

/**
* The publish and edit screens accept the same form. Keeping the rules here means a new
* field or limit is changed once instead of in two view models that can silently drift.
*/
data class LostFoundForm(
val title: String,
val description: String,
val location: String,
val qq: String,
val wechat: String,
val phone: String
)

object LostFoundFormValidator {

fun normalize(
title: String,
description: String,
location: String,
qq: String,
wechat: String,
phone: String
): LostFoundForm = LostFoundForm(
title = title.trim(),
description = description.trim(),
location = location.trim(),
qq = qq.trim(),
wechat = wechat.trim(),
phone = phone.trim()
)

/** String resource of the first failed rule, or null when the form is valid. */
fun errorRes(form: LostFoundForm): Int? = when {
form.title.isBlank() -> R.string.lost_found_publish_title_required
form.title.length > 25 -> R.string.lost_found_publish_title_too_long
form.description.isBlank() -> R.string.lost_found_publish_description_required
form.description.length > 100 -> R.string.lost_found_publish_description_too_long
form.location.isBlank() -> R.string.lost_found_publish_location_required
form.location.length > 30 -> R.string.lost_found_publish_location_too_long
form.qq.isBlank() && form.wechat.isBlank() && form.phone.isBlank() ->
R.string.lost_found_publish_contact_required
form.qq.length > 20 -> R.string.lost_found_publish_qq_too_long
form.wechat.length > 20 -> R.string.lost_found_publish_wechat_too_long
form.phone.length > 11 -> R.string.lost_found_publish_phone_too_long
else -> null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,25 +65,17 @@ class LostFoundPublishViewModel @Inject constructor(
phone: String,
images: List<Uri>
) {
val normalizedTitle = title.trim()
val normalizedDescription = description.trim()
val normalizedLocation = location.trim()
val normalizedQq = qq.trim()
val normalizedWechat = wechat.trim()
val normalizedPhone = phone.trim()
val form = LostFoundFormValidator.normalize(title, description, location, qq, wechat, phone)
val normalizedTitle = form.title
val normalizedDescription = form.description
val normalizedLocation = form.location
val normalizedQq = form.qq
val normalizedWechat = form.wechat
val normalizedPhone = form.phone
val errorRes = LostFoundFormValidator.errorRes(form)

when {
normalizedTitle.isBlank() -> emitMessage(context.getString(R.string.lost_found_publish_title_required))
normalizedTitle.length > 25 -> emitMessage(context.getString(R.string.lost_found_publish_title_too_long))
normalizedDescription.isBlank() -> emitMessage(context.getString(R.string.lost_found_publish_description_required))
normalizedDescription.length > 100 -> emitMessage(context.getString(R.string.lost_found_publish_description_too_long))
normalizedLocation.isBlank() -> emitMessage(context.getString(R.string.lost_found_publish_location_required))
normalizedLocation.length > 30 -> emitMessage(context.getString(R.string.lost_found_publish_location_too_long))
normalizedQq.isBlank() && normalizedWechat.isBlank() && normalizedPhone.isBlank() ->
emitMessage(context.getString(R.string.lost_found_publish_contact_required))
normalizedQq.length > 20 -> emitMessage(context.getString(R.string.lost_found_publish_qq_too_long))
normalizedWechat.length > 20 -> emitMessage(context.getString(R.string.lost_found_publish_wechat_too_long))
normalizedPhone.length > 11 -> emitMessage(context.getString(R.string.lost_found_publish_phone_too_long))
errorRes != null -> emitMessage(context.getString(errorRes))
images.isEmpty() -> emitMessage(context.getString(R.string.lost_found_publish_images_required))
else -> {
viewModelScope.launch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,32 +98,24 @@ class MarketplaceEditViewModel @Inject constructor(
qq: String,
phone: String
) {
val normalizedTitle = title.trim()
val normalizedDescription = description.trim()
val normalizedLocation = location.trim()
val normalizedQq = qq.trim()
val normalizedPhone = phone.trim()
val price = priceText.trim().toDoubleOrNull()
val form = MarketplaceFormValidator.normalize(title, priceText, description, location, qq, phone)
val normalizedTitle = form.title
val normalizedDescription = form.description
val normalizedLocation = form.location
val normalizedQq = form.qq
val normalizedPhone = form.phone
val errorRes = MarketplaceFormValidator.errorRes(form)

when {
normalizedTitle.isBlank() -> emitMessage(context.getString(R.string.marketplace_publish_title_required))
normalizedTitle.length > 25 -> emitMessage(context.getString(R.string.marketplace_publish_title_too_long))
price == null || price <= 0.0 || price > 9999.99 -> emitMessage(context.getString(R.string.marketplace_publish_price_invalid))
normalizedDescription.isBlank() -> emitMessage(context.getString(R.string.marketplace_publish_description_required))
normalizedDescription.length > 100 -> emitMessage(context.getString(R.string.marketplace_publish_description_too_long))
normalizedLocation.isBlank() -> emitMessage(context.getString(R.string.marketplace_publish_location_required))
normalizedLocation.length > 30 -> emitMessage(context.getString(R.string.marketplace_publish_location_too_long))
normalizedQq.isBlank() -> emitMessage(context.getString(R.string.marketplace_publish_qq_required))
normalizedQq.length > 20 -> emitMessage(context.getString(R.string.marketplace_publish_qq_too_long))
normalizedPhone.length > 11 -> emitMessage(context.getString(R.string.marketplace_publish_phone_too_long))
errorRes != null -> emitMessage(context.getString(errorRes))
else -> {
viewModelScope.launch {
_state.update { it.copy(isSubmitting = true, error = null) }
marketplaceRepository.updateItem(
id = itemId,
draft = MarketplaceUpdateDraft(
title = normalizedTitle,
price = price,
price = MarketplaceFormValidator.price(form),
description = normalizedDescription,
location = normalizedLocation,
typeId = typeId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package cn.gdeiassistant.ui.marketplace

import cn.gdeiassistant.R

/**
* Shared rules for the marketplace publish and edit forms. Both screens used to carry the
* same eleven-branch validation chain, so a limit change had to be applied twice.
*/
data class MarketplaceForm(
val title: String,
val price: Double?,
val description: String,
val location: String,
val qq: String,
val phone: String
)

object MarketplaceFormValidator {

fun normalize(
title: String,
priceText: String,
description: String,
location: String,
qq: String,
phone: String
): MarketplaceForm = MarketplaceForm(
title = title.trim(),
price = priceText.trim().toDoubleOrNull(),
description = description.trim(),
location = location.trim(),
qq = qq.trim(),
phone = phone.trim()
)

/** String resource of the first failed rule, or null when the form is valid. */
fun errorRes(form: MarketplaceForm): Int? = when {
form.title.isBlank() -> R.string.marketplace_publish_title_required
form.title.length > 25 -> R.string.marketplace_publish_title_too_long
form.price == null || form.price <= 0.0 || form.price > 9999.99 ->
R.string.marketplace_publish_price_invalid
form.description.isBlank() -> R.string.marketplace_publish_description_required
form.description.length > 100 -> R.string.marketplace_publish_description_too_long
form.location.isBlank() -> R.string.marketplace_publish_location_required
form.location.length > 30 -> R.string.marketplace_publish_location_too_long
form.qq.isBlank() -> R.string.marketplace_publish_qq_required
form.qq.length > 20 -> R.string.marketplace_publish_qq_too_long
form.phone.length > 11 -> R.string.marketplace_publish_phone_too_long
else -> null
}

/** Non-null price; call only after [errorRes] returned null. */
fun price(form: MarketplaceForm): Double = requireNotNull(form.price) {
"price() requires a validated form"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -133,32 +133,24 @@ class MarketplacePublishViewModel @Inject constructor(
phone: String,
images: List<Uri>
) {
val normalizedTitle = title.trim()
val normalizedDescription = description.trim()
val normalizedLocation = location.trim()
val normalizedQq = qq.trim()
val normalizedPhone = phone.trim()
val price = priceText.trim().toDoubleOrNull()
val form = MarketplaceFormValidator.normalize(title, priceText, description, location, qq, phone)
val normalizedTitle = form.title
val normalizedDescription = form.description
val normalizedLocation = form.location
val normalizedQq = form.qq
val normalizedPhone = form.phone
val errorRes = MarketplaceFormValidator.errorRes(form)

when {
normalizedTitle.isBlank() -> emitMessage(context.getString(R.string.marketplace_publish_title_required))
normalizedTitle.length > 25 -> emitMessage(context.getString(R.string.marketplace_publish_title_too_long))
price == null || price <= 0.0 || price > 9999.99 -> emitMessage(context.getString(R.string.marketplace_publish_price_invalid))
normalizedDescription.isBlank() -> emitMessage(context.getString(R.string.marketplace_publish_description_required))
normalizedDescription.length > 100 -> emitMessage(context.getString(R.string.marketplace_publish_description_too_long))
normalizedLocation.isBlank() -> emitMessage(context.getString(R.string.marketplace_publish_location_required))
normalizedLocation.length > 30 -> emitMessage(context.getString(R.string.marketplace_publish_location_too_long))
normalizedQq.isBlank() -> emitMessage(context.getString(R.string.marketplace_publish_qq_required))
normalizedQq.length > 20 -> emitMessage(context.getString(R.string.marketplace_publish_qq_too_long))
normalizedPhone.length > 11 -> emitMessage(context.getString(R.string.marketplace_publish_phone_too_long))
errorRes != null -> emitMessage(context.getString(errorRes))
images.isEmpty() -> emitMessage(context.getString(R.string.marketplace_publish_images_required))
else -> {
viewModelScope.launch {
_state.update { it.copy(isSubmitting = true, error = null) }
marketplaceRepository.publish(
draft = MarketplaceDraft(
title = normalizedTitle,
price = price,
price = MarketplaceFormValidator.price(form),
description = normalizedDescription,
location = normalizedLocation,
typeId = typeId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,17 +204,3 @@ private fun InteractionListRow(
}
}

@Composable
private fun interactionModuleLabel(module: String?): String {
return when (module) {
"secret" -> stringResource(R.string.messages_module_secret)
"secondhand" -> stringResource(R.string.messages_module_marketplace)
"lostandfound" -> stringResource(R.string.messages_module_lost_found)
"topic" -> stringResource(R.string.messages_module_topic)
"express" -> stringResource(R.string.messages_module_express)
"dating" -> stringResource(R.string.messages_module_dating)
"delivery" -> stringResource(R.string.messages_module_delivery)
"photograph" -> stringResource(R.string.messages_module_photograph)
else -> stringResource(R.string.messages_interaction_badge)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package cn.gdeiassistant.ui.messages

import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import cn.gdeiassistant.R

/** One mapping from an interaction module id to its display name, shared by both lists. */
@Composable
fun interactionModuleLabel(module: String?): String = when (module) {
"secret" -> stringResource(R.string.messages_module_secret)
"secondhand" -> stringResource(R.string.messages_module_marketplace)
"lostandfound" -> stringResource(R.string.messages_module_lost_found)
"topic" -> stringResource(R.string.messages_module_topic)
"express" -> stringResource(R.string.messages_module_express)
"dating" -> stringResource(R.string.messages_module_dating)
"delivery" -> stringResource(R.string.messages_module_delivery)
"photograph" -> stringResource(R.string.messages_module_photograph)
else -> stringResource(R.string.messages_interaction_badge)
}
14 changes: 0 additions & 14 deletions app/src/main/java/cn/gdeiassistant/ui/messages/MessagesScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -369,20 +369,6 @@ private fun FestivalCard(festival: Festival) {
}
}

@Composable
private fun interactionModuleLabel(module: String?): String {
return when (module) {
"secret" -> stringResource(R.string.messages_module_secret)
"secondhand" -> stringResource(R.string.messages_module_marketplace)
"lostandfound" -> stringResource(R.string.messages_module_lost_found)
"topic" -> stringResource(R.string.messages_module_topic)
"express" -> stringResource(R.string.messages_module_express)
"dating" -> stringResource(R.string.messages_module_dating)
"delivery" -> stringResource(R.string.messages_module_delivery)
"photograph" -> stringResource(R.string.messages_module_photograph)
else -> stringResource(R.string.messages_interaction_badge)
}
}

@Composable
private fun InfoSectionCard(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package cn.gdeiassistant.ui.lostfound

import cn.gdeiassistant.R
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test

class LostFoundFormValidatorTest {

private fun form(
title: String = "Found keys",
description: String = "Near the library",
location: String = "Library",
qq: String = "12345",
wechat: String = "",
phone: String = ""
) = LostFoundFormValidator.normalize(title, description, location, qq, wechat, phone)

@Test
fun trimsEveryField() {
val result = form(title = " keys ", qq = " 123 ", wechat = " wx ")
assertEquals("keys", result.title)
assertEquals("123", result.qq)
assertEquals("wx", result.wechat)
}

@Test
fun validFormHasNoError() {
assertNull(LostFoundFormValidator.errorRes(form()))
}

@Test
fun reportsRulesInTheSameOrderAsTheOriginalScreens() {
assertEquals(
R.string.lost_found_publish_title_required,
LostFoundFormValidator.errorRes(form(title = " "))
)
assertEquals(
R.string.lost_found_publish_title_too_long,
LostFoundFormValidator.errorRes(form(title = "x".repeat(26)))
)
assertEquals(
R.string.lost_found_publish_contact_required,
LostFoundFormValidator.errorRes(form(qq = "", wechat = "", phone = ""))
)
assertEquals(
R.string.lost_found_publish_phone_too_long,
LostFoundFormValidator.errorRes(form(phone = "1".repeat(12)))
)
}
}
Loading
Loading