feat: update message status handling for error cases in ChatViewModel and UI components

This commit is contained in:
2026-03-04 01:00:08 +05:00
parent 2501296d70
commit d1aca8439a
4 changed files with 134 additions and 60 deletions

View File

@@ -1158,7 +1158,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
when (entity.delivered) {
0 -> MessageStatus.SENDING
1 -> if (entity.read == 1) MessageStatus.READ else MessageStatus.DELIVERED
2 -> MessageStatus.SENT
2 -> MessageStatus.ERROR
3 -> MessageStatus.READ
else -> MessageStatus.SENT
},
@@ -2509,8 +2509,11 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
saveDialog(text, timestamp)
} catch (e: Exception) {
withContext(Dispatchers.Main) {
updateMessageStatus(messageId, MessageStatus.SENT) // Changed from ERROR
updateMessageStatus(messageId, MessageStatus.ERROR)
}
// Update error status in DB + dialog
updateMessageStatusInDb(messageId, DeliveryStatus.ERROR.value)
saveDialog(text, timestamp)
} finally {
isSending = false
}
@@ -3712,7 +3715,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
saveDialog(if (text.isNotEmpty()) text else "file", timestamp)
} catch (e: Exception) {
withContext(Dispatchers.Main) { updateMessageStatus(messageId, MessageStatus.SENT) }
withContext(Dispatchers.Main) { updateMessageStatus(messageId, MessageStatus.ERROR) }
updateMessageStatusInDb(messageId, DeliveryStatus.ERROR.value)
saveDialog(if (text.isNotEmpty()) text else "file", timestamp)
} finally {
isSending = false
}
@@ -3942,7 +3947,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
saveDialog("\$a=Avatar", timestamp)
} catch (e: Exception) {
withContext(Dispatchers.Main) {
updateMessageStatus(messageId, MessageStatus.SENT)
updateMessageStatus(messageId, MessageStatus.ERROR)
android.widget.Toast.makeText(
getApplication(),
"Failed to send avatar: ${e.message}",
@@ -3950,6 +3955,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
)
.show()
}
updateMessageStatusInDb(messageId, DeliveryStatus.ERROR.value)
saveDialog("\$a=Avatar", timestamp)
} finally {
isSending = false
}

View File

@@ -3866,24 +3866,8 @@ fun DialogItemContent(
)
}
dialog.lastMessageDelivered == 2 -> {
// ERROR - показываем иконку ошибки
Icon(
imageVector =
TablerIcons
.AlertCircle,
contentDescription =
"Sending failed",
tint =
Color(
0xFFFF3B30
), // iOS красный
modifier =
Modifier.size(16.dp)
)
Spacer(
modifier =
Modifier.width(4.dp)
)
// ERROR - не показываем статус рядом с временем,
// error badge показывается внизу справа (как в Telegram)
}
dialog.lastMessageDelivered == 1 -> {
// DELIVERED - одна серая галочка
@@ -4120,6 +4104,27 @@ fun DialogItemContent(
}
}
// Error badge (Telegram-style) — красный кружок с "!" вместо unread badge
if (dialog.lastMessageFromMe == 1 && dialog.lastMessageDelivered == 2) {
Spacer(modifier = Modifier.width(8.dp))
Box(
modifier =
Modifier.size(22.dp)
.clip(CircleShape)
.background(Color(0xFFE53935)),
contentAlignment = Alignment.Center
) {
Text(
text = "!",
fontSize = 13.sp,
fontWeight = FontWeight.Bold,
color = Color.White,
lineHeight = 13.sp,
maxLines = 1
)
}
}
// Unread badge
if (dialog.unreadCount > 0) {
Spacer(modifier = Modifier.width(8.dp))

View File

@@ -1650,6 +1650,19 @@ fun AnimatedMessageStatus(
onRetry: () -> Unit = {},
onDelete: () -> Unit = {}
) {
// Force recomposition when SENDING message exceeds 80s timeout
var timeoutTick by remember { mutableLongStateOf(0L) }
if (status == MessageStatus.SENDING && timestamp > 0) {
val remainingMs = (timestamp + 80_000L) - System.currentTimeMillis()
if (remainingMs > 0) {
LaunchedEffect(timestamp) {
kotlinx.coroutines.delay(remainingMs + 100)
timeoutTick++
}
}
}
@Suppress("UNUSED_EXPRESSION") timeoutTick // read to subscribe
val isTimedOut =
status == MessageStatus.SENDING &&
timestamp > 0 &&
@@ -1706,18 +1719,30 @@ fun AnimatedMessageStatus(
label = "statusIcon"
) { currentStatus ->
if (currentStatus == MessageStatus.ERROR) {
Icon(
imageVector = TablerIcons.AlertCircle,
contentDescription = null,
tint = animatedColor,
// Telegram-style: red filled circle with white "!" inside
Box(
modifier =
Modifier.size(iconSize)
Modifier.padding(start = 4.dp)
.size(iconSize)
.align(Alignment.CenterStart)
.scale(scale)
.background(
color = Color(0xFFE53935),
shape = CircleShape
)
.clickable {
showErrorMenu = true
}
)
},
contentAlignment = Alignment.Center
) {
Text(
text = "!",
color = Color.White,
fontSize = 10.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold,
lineHeight = 10.sp
)
}
} else {
if (currentStatus == MessageStatus.READ) {
Box(
@@ -1764,39 +1789,76 @@ fun AnimatedMessageStatus(
}
}
DropdownMenu(
expanded = showErrorMenu,
onDismissRequest = { showErrorMenu = false }
val menuBgColor = if (isDarkTheme) Color(0xFF272829) else Color.White
val menuTextColor = if (isDarkTheme) Color.White else Color(0xFF222222)
val menuIconColor = if (isDarkTheme) Color.White.copy(alpha = 0.47f) else Color(0xFF676B70)
MaterialTheme(
colorScheme = MaterialTheme.colorScheme.copy(
surface = menuBgColor,
onSurface = menuTextColor
)
) {
DropdownMenuItem(
text = { Text("Retry") },
onClick = {
showErrorMenu = false
onRetry()
},
leadingIcon = {
Icon(
painter = TelegramIcons.Retry,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
DropdownMenu(
expanded = showErrorMenu,
onDismissRequest = { showErrorMenu = false },
modifier = Modifier
.defaultMinSize(minWidth = 196.dp)
.background(menuBgColor)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.defaultMinSize(minHeight = 48.dp)
.clickable {
showErrorMenu = false
onRetry()
}
.padding(horizontal = 18.dp),
contentAlignment = Alignment.CenterStart
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
painter = TelegramIcons.Retry,
contentDescription = null,
tint = menuIconColor,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(19.dp))
Text(
text = "Retry",
color = menuTextColor,
fontSize = 16.sp
)
}
}
)
DropdownMenuItem(
text = { Text("Delete", color = Color(0xFFE53935)) },
onClick = {
showErrorMenu = false
onDelete()
},
leadingIcon = {
Icon(
painter = TelegramIcons.Delete,
contentDescription = null,
tint = Color(0xFFE53935),
modifier = Modifier.size(18.dp)
)
Box(
modifier = Modifier
.fillMaxWidth()
.defaultMinSize(minHeight = 48.dp)
.clickable {
showErrorMenu = false
onDelete()
}
.padding(horizontal = 18.dp),
contentAlignment = Alignment.CenterStart
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
painter = TelegramIcons.Delete,
contentDescription = null,
tint = Color(0xFFFF3B30),
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(19.dp))
Text(
text = "Delete",
color = Color(0xFFFF3B30),
fontSize = 16.sp
)
}
}
)
}
}
}
}

View File

@@ -69,7 +69,7 @@ fun Message.toChatMessage() = ChatMessage(
status = when (deliveryStatus) {
DeliveryStatus.WAITING -> MessageStatus.SENDING
DeliveryStatus.DELIVERED -> if (isRead) MessageStatus.READ else MessageStatus.DELIVERED
DeliveryStatus.ERROR -> MessageStatus.SENT
DeliveryStatus.ERROR -> MessageStatus.ERROR
DeliveryStatus.READ -> MessageStatus.READ
}
)