feat: Increase delay for clearing reply messages to improve user experience

This commit is contained in:
k1ngsterr1
2026-01-15 17:48:07 +05:00
parent a939054c54
commit 842bd4eedb
3 changed files with 401 additions and 2 deletions

View File

@@ -866,7 +866,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
// Очищаем reply ПОСЛЕ добавления сообщения в список с небольшой задержкой
viewModelScope.launch {
kotlinx.coroutines.delay(100)
kotlinx.coroutines.delay(300)
clearReplyMessages()
}

View File

@@ -18,11 +18,14 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.draw.scale
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
@@ -203,6 +206,11 @@ fun ChatsListScreen(
// 🔥 Используем rememberSaveable чтобы сохранить состояние при навигации
// Header сразу visible = true, без анимации при возврате из чата
var visible by rememberSaveable { mutableStateOf(true) }
// Confirmation dialogs state
var dialogToDelete by remember { mutableStateOf<DialogUiModel?>(null) }
var dialogToBlock by remember { mutableStateOf<DialogUiModel?>(null) }
var dialogToUnblock by remember { mutableStateOf<DialogUiModel?>(null) }
// Dev console dialog - commented out for now
/*
@@ -509,13 +517,31 @@ fun ChatsListScreen(
// Show dialogs list
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(dialogsList, key = { it.opponentKey }) { dialog ->
DialogItem(
val isSavedMessages = dialog.opponentKey == accountPublicKey
// Check if user is blocked
var isBlocked by remember { mutableStateOf(false) }
LaunchedEffect(dialog.opponentKey) {
isBlocked = chatsViewModel.isUserBlocked(dialog.opponentKey)
}
SwipeableDialogItem(
dialog = dialog,
isDarkTheme = isDarkTheme,
isTyping = typingUsers.contains(dialog.opponentKey),
isBlocked = isBlocked,
isSavedMessages = isSavedMessages,
onClick = {
val user = chatsViewModel.dialogToSearchUser(dialog)
onUserSelect(user)
},
onDelete = {
dialogToDelete = dialog
},
onBlock = {
dialogToBlock = dialog
},
onUnblock = {
dialogToUnblock = dialog
}
)
}
@@ -526,6 +552,170 @@ fun ChatsListScreen(
}
}
} // Close ModalNavigationDrawer
// 🔥 Confirmation Dialogs
// Delete Dialog Confirmation
dialogToDelete?.let { dialog ->
AlertDialog(
onDismissRequest = { dialogToDelete = null },
icon = {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = null,
tint = PrimaryBlue,
modifier = Modifier.size(32.dp)
)
},
title = {
Text(
text = "Delete conversation?",
fontWeight = FontWeight.SemiBold,
fontSize = 18.sp
)
},
text = {
val displayName = dialog.opponentTitle.ifEmpty { dialog.opponentKey.take(8) }
Text(
text = "All messages with $displayName will be permanently deleted. This action cannot be undone.",
fontSize = 15.sp,
color = if (isDarkTheme) Color(0xFFAAAAAA) else Color(0xFF666666)
)
},
confirmButton = {
TextButton(
onClick = {
scope.launch {
chatsViewModel.deleteDialog(dialog.opponentKey)
dialogToDelete = null
}
},
colors = ButtonDefaults.textButtonColors(
contentColor = Color(0xFFFF3B30)
)
) {
Text("Delete", fontWeight = FontWeight.SemiBold)
}
},
dismissButton = {
TextButton(
onClick = { dialogToDelete = null }
) {
Text("Cancel", fontWeight = FontWeight.Medium)
}
},
containerColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color.White,
shape = RoundedCornerShape(16.dp)
)
}
// Block Dialog Confirmation
dialogToBlock?.let { dialog ->
AlertDialog(
onDismissRequest = { dialogToBlock = null },
icon = {
Icon(
imageVector = Icons.Default.Block,
contentDescription = null,
tint = Color(0xFFFF6B6B),
modifier = Modifier.size(32.dp)
)
},
title = {
Text(
text = "Block user?",
fontWeight = FontWeight.SemiBold,
fontSize = 18.sp
)
},
text = {
val displayName = dialog.opponentTitle.ifEmpty { dialog.opponentKey.take(8) }
Text(
text = "$displayName will no longer be able to send you messages. You can unblock them later.",
fontSize = 15.sp,
color = if (isDarkTheme) Color(0xFFAAAAAA) else Color(0xFF666666)
)
},
confirmButton = {
TextButton(
onClick = {
scope.launch {
chatsViewModel.blockUser(dialog.opponentKey)
dialogToBlock = null
}
},
colors = ButtonDefaults.textButtonColors(
contentColor = Color(0xFFFF3B30)
)
) {
Text("Block", fontWeight = FontWeight.SemiBold)
}
},
dismissButton = {
TextButton(
onClick = { dialogToBlock = null }
) {
Text("Cancel", fontWeight = FontWeight.Medium)
}
},
containerColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color.White,
shape = RoundedCornerShape(16.dp)
)
}
// Unblock Dialog Confirmation
dialogToUnblock?.let { dialog ->
AlertDialog(
onDismissRequest = { dialogToUnblock = null },
icon = {
Icon(
imageVector = Icons.Default.LockOpen,
contentDescription = null,
tint = Color(0xFF4CAF50),
modifier = Modifier.size(32.dp)
)
},
title = {
Text(
text = "Unblock user?",
fontWeight = FontWeight.SemiBold,
fontSize = 18.sp
)
},
text = {
val displayName = dialog.opponentTitle.ifEmpty { dialog.opponentKey.take(8) }
Text(
text = "$displayName will be able to send you messages again.",
fontSize = 15.sp,
color = if (isDarkTheme) Color(0xFFAAAAAA) else Color(0xFF666666)
)
},
confirmButton = {
TextButton(
onClick = {
scope.launch {
chatsViewModel.unblockUser(dialog.opponentKey)
dialogToUnblock = null
}
},
colors = ButtonDefaults.textButtonColors(
contentColor = Color(0xFF4CAF50)
)
) {
Text("Unblock", fontWeight = FontWeight.SemiBold)
}
},
dismissButton = {
TextButton(
onClick = { dialogToUnblock = null }
) {
Text("Cancel", fontWeight = FontWeight.Medium)
}
},
containerColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color.White,
shape = RoundedCornerShape(16.dp)
)
}
} // Close Box
}
@@ -791,6 +981,155 @@ fun DrawerMenuItem(
}
}
/**
* 🔥 Swipeable wrapper для DialogItem с кнопками Block и Delete
* Свайп влево показывает действия (как в React Native версии)
*/
@Composable
fun SwipeableDialogItem(
dialog: DialogUiModel,
isDarkTheme: Boolean,
isTyping: Boolean = false,
isBlocked: Boolean = false,
isSavedMessages: Boolean = false,
onClick: () -> Unit,
onDelete: () -> Unit = {},
onBlock: () -> Unit = {},
onUnblock: () -> Unit = {}
) {
val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF)
var offsetX by remember { mutableStateOf(0f) }
val swipeWidthDp = if (isSavedMessages) 80.dp else 160.dp
val density = androidx.compose.ui.platform.LocalDensity.current
val swipeWidthPx = with(density) { swipeWidthDp.toPx() }
// Фиксированная высота элемента (как в DialogItem)
val itemHeight = 80.dp
// Анимация возврата
val animatedOffsetX by animateFloatAsState(
targetValue = offsetX,
animationSpec = spring(dampingRatio = 0.7f, stiffness = 400f),
label = "swipeOffset"
)
Box(
modifier = Modifier
.fillMaxWidth()
.height(itemHeight)
.clipToBounds()
) {
// 1. КНОПКИ - позиционированы справа, всегда видны при свайпе
Row(
modifier = Modifier
.align(Alignment.CenterEnd)
.height(itemHeight)
.width(swipeWidthDp)
) {
// Кнопка Block/Unblock (только если не Saved Messages)
if (!isSavedMessages) {
Box(
modifier = Modifier
.width(80.dp)
.fillMaxHeight()
.background(if (isBlocked) Color(0xFF4CAF50) else Color(0xFFFF6B6B))
.clickable {
if (isBlocked) onUnblock() else onBlock()
offsetX = 0f
},
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = if (isBlocked) Icons.Default.LockOpen else Icons.Default.Block,
contentDescription = if (isBlocked) "Unblock" else "Block",
tint = Color.White,
modifier = Modifier.size(22.dp)
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = if (isBlocked) "Unblock" else "Block",
color = Color.White,
fontSize = 12.sp,
fontWeight = FontWeight.SemiBold
)
}
}
}
// Кнопка Delete
Box(
modifier = Modifier
.width(80.dp)
.fillMaxHeight()
.background(PrimaryBlue)
.clickable {
onDelete()
offsetX = 0f
},
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Delete",
tint = Color.White,
modifier = Modifier.size(22.dp)
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Delete",
color = Color.White,
fontSize = 12.sp,
fontWeight = FontWeight.SemiBold
)
}
}
}
// 2. КОНТЕНТ - поверх кнопок, сдвигается при свайпе
Box(
modifier = Modifier
.fillMaxSize()
.offset { IntOffset(animatedOffsetX.toInt(), 0) }
.background(backgroundColor)
.pointerInput(Unit) {
detectHorizontalDragGestures(
onDragEnd = {
// Если свайпнули больше чем на половину - фиксируем
if (kotlin.math.abs(offsetX) > swipeWidthPx / 2) {
offsetX = -swipeWidthPx
} else {
offsetX = 0f
}
},
onDragCancel = {
offsetX = 0f
},
onHorizontalDrag = { _, dragAmount ->
// Только свайп влево (отрицательное значение)
val newOffset = offsetX + dragAmount
offsetX = newOffset.coerceIn(-swipeWidthPx, 0f)
}
)
}
) {
DialogItem(
dialog = dialog,
isDarkTheme = isDarkTheme,
isTyping = isTyping,
onClick = onClick
)
}
}
}
/** Элемент диалога из базы данных - ОПТИМИЗИРОВАННЫЙ */
@Composable
fun DialogItem(dialog: DialogUiModel, isDarkTheme: Boolean, isTyping: Boolean = false, onClick: () -> Unit) {

View File

@@ -168,4 +168,64 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
online = dialog.isOnline
)
}
/**
* Удалить диалог и все сообщения с собеседником
*/
suspend fun deleteDialog(opponentKey: String) {
if (currentAccount.isEmpty()) return
try {
// Удаляем все сообщения
database.messageDao().deleteDialog(currentAccount, opponentKey)
// Удаляем диалог
database.dialogDao().deleteDialog(currentAccount, opponentKey)
} catch (e: Exception) {
android.util.Log.e("ChatsListViewModel", "Error deleting dialog", e)
}
}
/**
* Заблокировать пользователя
*/
suspend fun blockUser(publicKey: String) {
if (currentAccount.isEmpty()) return
try {
database.blacklistDao().blockUser(
com.rosetta.messenger.database.BlacklistEntity(
publicKey = publicKey,
account = currentAccount
)
)
} catch (e: Exception) {
android.util.Log.e("ChatsListViewModel", "Error blocking user", e)
}
}
/**
* Разблокировать пользователя
*/
suspend fun unblockUser(publicKey: String) {
if (currentAccount.isEmpty()) return
try {
database.blacklistDao().unblockUser(publicKey, currentAccount)
} catch (e: Exception) {
android.util.Log.e("ChatsListViewModel", "Error unblocking user", e)
}
}
/**
* Проверить заблокирован ли пользователь
*/
suspend fun isUserBlocked(publicKey: String): Boolean {
if (currentAccount.isEmpty()) return false
return try {
database.blacklistDao().isUserBlocked(publicKey, currentAccount)
} catch (e: Exception) {
false
}
}
}