fix: refine swipe-back thresholds for improved navigation responsiveness
This commit is contained in:
@@ -68,6 +68,73 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
private const val TAG = "ProfileScreen"
|
||||
private const val PROFILE_NAME_MAX_LENGTH = 40
|
||||
private const val PROFILE_USERNAME_MIN_LENGTH = 5
|
||||
private const val PROFILE_USERNAME_MAX_LENGTH = 32
|
||||
|
||||
private val PROFILE_NAME_ALLOWED_REGEX = Regex("^[\\p{L}\\p{N} ._'-]+\$")
|
||||
private val PROFILE_USERNAME_ALLOWED_REGEX = Regex("^[A-Za-z0-9_]+\$")
|
||||
private val PROFILE_ERROR_CODE_REGEX = Regex("error code:\\s*(\\d+)", RegexOption.IGNORE_CASE)
|
||||
|
||||
private data class ProfileSaveErrorUi(
|
||||
val toastMessage: String,
|
||||
val nameError: String? = null,
|
||||
val usernameError: String? = null,
|
||||
val generalMessage: String? = null
|
||||
)
|
||||
|
||||
private fun mapProfileSaveError(rawError: String): ProfileSaveErrorUi {
|
||||
val errorCode = PROFILE_ERROR_CODE_REGEX.find(rawError)?.groupValues?.getOrNull(1)?.toIntOrNull()
|
||||
|
||||
return when {
|
||||
errorCode == 3 ->
|
||||
ProfileSaveErrorUi(
|
||||
toastMessage = "Username is unavailable. Try another one.",
|
||||
usernameError = "Username is unavailable or invalid",
|
||||
generalMessage = "Could not save profile: username is unavailable."
|
||||
)
|
||||
rawError.contains("timeout", ignoreCase = true) ->
|
||||
ProfileSaveErrorUi(
|
||||
toastMessage = "Server timeout. Please try again.",
|
||||
generalMessage = "Server timeout. Check connection and try again."
|
||||
)
|
||||
rawError.contains("network", ignoreCase = true) ->
|
||||
ProfileSaveErrorUi(
|
||||
toastMessage = "Network error. Please try again.",
|
||||
generalMessage = "Network error while saving profile."
|
||||
)
|
||||
else ->
|
||||
ProfileSaveErrorUi(
|
||||
toastMessage = "Error: $rawError",
|
||||
generalMessage = "Could not save profile. Please try again."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateProfileName(name: String): String? {
|
||||
if (name.isBlank()) return "Name can't be empty"
|
||||
if (name.length > PROFILE_NAME_MAX_LENGTH) {
|
||||
return "Name is too long (max $PROFILE_NAME_MAX_LENGTH)"
|
||||
}
|
||||
if (!PROFILE_NAME_ALLOWED_REGEX.matches(name)) {
|
||||
return "Only letters, numbers, spaces, . _ - ' are allowed"
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun validateProfileUsername(username: String): String? {
|
||||
if (username.isBlank()) return null // optional field
|
||||
if (username.length < PROFILE_USERNAME_MIN_LENGTH) {
|
||||
return "Username must be at least $PROFILE_USERNAME_MIN_LENGTH characters"
|
||||
}
|
||||
if (username.length > PROFILE_USERNAME_MAX_LENGTH) {
|
||||
return "Username is too long (max $PROFILE_USERNAME_MAX_LENGTH)"
|
||||
}
|
||||
if (!PROFILE_USERNAME_ALLOWED_REGEX.matches(username)) {
|
||||
return "Use only letters, numbers, and underscore"
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 🎨 Avatar colors - используем те же цвета что и в ChatsListScreen
|
||||
private val avatarColorsLight =
|
||||
@@ -300,11 +367,35 @@ fun ProfileScreen(
|
||||
var editedName by remember(accountName) { mutableStateOf(accountName) }
|
||||
var editedUsername by remember(accountUsername) { mutableStateOf(accountUsername) }
|
||||
var hasChanges by remember { mutableStateOf(false) }
|
||||
var nameTouched by remember { mutableStateOf(false) }
|
||||
var usernameTouched by remember { mutableStateOf(false) }
|
||||
var showValidationErrors by remember { mutableStateOf(false) }
|
||||
var serverNameError by remember { mutableStateOf<String?>(null) }
|
||||
var serverUsernameError by remember { mutableStateOf<String?>(null) }
|
||||
var serverGeneralError by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
val trimmedName = editedName.trim()
|
||||
val trimmedUsername = editedUsername.trim()
|
||||
val nameValidationError =
|
||||
if (nameTouched || showValidationErrors) validateProfileName(trimmedName) else null
|
||||
val usernameValidationError =
|
||||
if (usernameTouched || showValidationErrors)
|
||||
validateProfileUsername(trimmedUsername)
|
||||
else null
|
||||
val nameError = nameValidationError ?: serverNameError
|
||||
val usernameError = usernameValidationError ?: serverUsernameError
|
||||
val isFormValid = nameError == null && usernameError == null
|
||||
|
||||
// Sync edited fields when account data changes from parent (after save)
|
||||
LaunchedEffect(accountName, accountUsername) {
|
||||
editedName = accountName
|
||||
editedUsername = accountUsername
|
||||
nameTouched = false
|
||||
usernameTouched = false
|
||||
showValidationErrors = false
|
||||
serverNameError = null
|
||||
serverUsernameError = null
|
||||
serverGeneralError = null
|
||||
}
|
||||
|
||||
// ViewModel state
|
||||
@@ -546,6 +637,9 @@ fun ProfileScreen(
|
||||
viewModel.updateLocalProfile(accountPublicKey, editedName, editedUsername)
|
||||
|
||||
hasChanges = false
|
||||
serverNameError = null
|
||||
serverUsernameError = null
|
||||
serverGeneralError = null
|
||||
viewModel.resetSaveState()
|
||||
|
||||
// Notify parent about profile update (updates UI in MainActivity)
|
||||
@@ -555,10 +649,28 @@ fun ProfileScreen(
|
||||
|
||||
// Show error toast
|
||||
LaunchedEffect(profileState.error) {
|
||||
profileState.error?.let { error ->
|
||||
profileState.error?.let { rawError ->
|
||||
val mappedError = mapProfileSaveError(rawError)
|
||||
serverNameError = mappedError.nameError
|
||||
serverUsernameError = mappedError.usernameError
|
||||
serverGeneralError =
|
||||
if (mappedError.nameError == null && mappedError.usernameError == null) {
|
||||
mappedError.generalMessage
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (mappedError.nameError != null) {
|
||||
nameTouched = true
|
||||
showValidationErrors = true
|
||||
}
|
||||
if (mappedError.usernameError != null) {
|
||||
usernameTouched = true
|
||||
showValidationErrors = true
|
||||
}
|
||||
|
||||
android.widget.Toast.makeText(
|
||||
context,
|
||||
"Error: $error",
|
||||
mappedError.toastMessage,
|
||||
android.widget.Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
@@ -590,15 +702,34 @@ fun ProfileScreen(
|
||||
// ═════════════════════════════════════════════════════════════
|
||||
TelegramSectionTitle(title = "Account", isDarkTheme = isDarkTheme)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = !serverGeneralError.isNullOrBlank(),
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
) {
|
||||
Text(
|
||||
text = serverGeneralError ?: "",
|
||||
fontSize = 13.sp,
|
||||
color = Color(0xFFFF3B30),
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// Name field
|
||||
TelegramTextField(
|
||||
value = editedName,
|
||||
label = "Your name",
|
||||
isDarkTheme = isDarkTheme,
|
||||
isEditable = true,
|
||||
onValueChange = { editedName = it },
|
||||
onValueChange = {
|
||||
editedName = it
|
||||
nameTouched = true
|
||||
serverNameError = null
|
||||
serverGeneralError = null
|
||||
},
|
||||
showDivider = true,
|
||||
placeholder = "Add your name"
|
||||
placeholder = "Add your name",
|
||||
errorText = nameError
|
||||
)
|
||||
|
||||
// Username field
|
||||
@@ -607,9 +738,15 @@ fun ProfileScreen(
|
||||
label = "Username",
|
||||
isDarkTheme = isDarkTheme,
|
||||
isEditable = true,
|
||||
onValueChange = { editedUsername = it },
|
||||
onValueChange = {
|
||||
editedUsername = it
|
||||
usernameTouched = true
|
||||
serverUsernameError = null
|
||||
serverGeneralError = null
|
||||
},
|
||||
showDivider = true,
|
||||
placeholder = "Add username"
|
||||
placeholder = "Add username",
|
||||
errorText = usernameError
|
||||
)
|
||||
|
||||
// Public Key field
|
||||
@@ -696,7 +833,27 @@ fun ProfileScreen(
|
||||
expansionProgress = expansionProgress,
|
||||
onBack = onBack,
|
||||
hasChanges = hasChanges,
|
||||
onSave = {
|
||||
isSaveEnabled = isFormValid && !profileState.isSaving,
|
||||
onSave = saveProfile@{
|
||||
showValidationErrors = true
|
||||
nameTouched = true
|
||||
usernameTouched = true
|
||||
|
||||
val nameToSave = editedName.trim()
|
||||
val usernameToSave = editedUsername.trim()
|
||||
val localNameError = validateProfileName(nameToSave)
|
||||
val localUsernameError = validateProfileUsername(usernameToSave)
|
||||
if (localNameError != null || localUsernameError != null) {
|
||||
return@saveProfile
|
||||
}
|
||||
|
||||
serverNameError = null
|
||||
serverUsernameError = null
|
||||
serverGeneralError = null
|
||||
|
||||
editedName = nameToSave
|
||||
editedUsername = usernameToSave
|
||||
|
||||
// Following desktop version logic:
|
||||
// 1. Send packet to server
|
||||
// 2. Wait for response in ProfileViewModel
|
||||
@@ -704,8 +861,8 @@ fun ProfileScreen(
|
||||
viewModel.saveProfile(
|
||||
publicKey = accountPublicKey,
|
||||
privateKeyHash = accountPrivateKeyHash,
|
||||
name = editedName,
|
||||
username = editedUsername
|
||||
name = nameToSave,
|
||||
username = usernameToSave
|
||||
)
|
||||
// Note: Local update happens in LaunchedEffect when saveSuccess is true
|
||||
},
|
||||
@@ -761,6 +918,7 @@ private fun CollapsingProfileHeader(
|
||||
expansionProgress: Float,
|
||||
onBack: () -> Unit,
|
||||
hasChanges: Boolean,
|
||||
isSaveEnabled: Boolean,
|
||||
onSave: () -> Unit,
|
||||
isDarkTheme: Boolean,
|
||||
showAvatarMenu: Boolean,
|
||||
@@ -892,10 +1050,17 @@ private fun CollapsingProfileHeader(
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
AnimatedVisibility(visible = hasChanges, enter = fadeIn(), exit = fadeOut()) {
|
||||
TextButton(onClick = onSave) {
|
||||
TextButton(onClick = onSave, enabled = isSaveEnabled) {
|
||||
Text(
|
||||
text = "Save",
|
||||
color = if (isDarkTheme) Color.White else Color.Black,
|
||||
color =
|
||||
if (isSaveEnabled) {
|
||||
if (isDarkTheme) Color.White else Color.Black
|
||||
} else {
|
||||
(if (isDarkTheme) Color.White else Color.Black).copy(
|
||||
alpha = 0.45f
|
||||
)
|
||||
},
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
@@ -1178,14 +1343,46 @@ private fun TelegramTextField(
|
||||
isEditable: Boolean = false,
|
||||
onValueChange: ((String) -> Unit)? = null,
|
||||
showDivider: Boolean = false,
|
||||
placeholder: String = ""
|
||||
placeholder: String = "",
|
||||
errorText: String? = null
|
||||
) {
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||
val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE0E0E0)
|
||||
val hasError = !errorText.isNullOrBlank()
|
||||
val errorColor = Color(0xFFFF3B30)
|
||||
val labelColor by
|
||||
animateColorAsState(
|
||||
targetValue = if (hasError) errorColor else secondaryTextColor,
|
||||
label = "profile_field_label_color"
|
||||
)
|
||||
val containerColor by
|
||||
animateColorAsState(
|
||||
targetValue =
|
||||
if (hasError) {
|
||||
if (isDarkTheme) errorColor.copy(alpha = 0.18f)
|
||||
else errorColor.copy(alpha = 0.08f)
|
||||
} else {
|
||||
Color.Transparent
|
||||
},
|
||||
label = "profile_field_container_color"
|
||||
)
|
||||
val borderColor by
|
||||
animateColorAsState(
|
||||
targetValue = if (hasError) errorColor else Color.Transparent,
|
||||
label = "profile_field_border_color"
|
||||
)
|
||||
|
||||
Column {
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp)) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(containerColor)
|
||||
.border(1.dp, borderColor, RoundedCornerShape(12.dp))
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||
) {
|
||||
if (isEditable && onValueChange != null) {
|
||||
BasicTextField(
|
||||
value = value,
|
||||
@@ -1217,7 +1414,17 @@ private fun TelegramTextField(
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Text(text = label, fontSize = 13.sp, color = secondaryTextColor)
|
||||
Text(text = label, fontSize = 13.sp, color = labelColor)
|
||||
|
||||
if (hasError) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = errorText ?: "",
|
||||
fontSize = 12.sp,
|
||||
color = errorColor,
|
||||
lineHeight = 14.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showDivider) {
|
||||
|
||||
Reference in New Issue
Block a user