feat: Refactor message bubble layout to display timestamp and status inline for outgoing messages

This commit is contained in:
k1ngsterr1
2026-01-13 20:14:39 +05:00
parent a7576865ef
commit 7c911835ea
4 changed files with 105 additions and 94 deletions

View File

@@ -55,6 +55,9 @@ class MainActivity : ComponentActivity() {
accountManager = AccountManager(this) accountManager = AccountManager(this)
RecentSearchesManager.init(this) RecentSearchesManager.init(this)
// 🔥 Инициализируем ProtocolManager для обработки онлайн статусов
ProtocolManager.initialize(this)
setContent { setContent {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val isDarkTheme by preferencesManager.isDarkTheme.collectAsState(initial = true) val isDarkTheme by preferencesManager.isDarkTheme.collectAsState(initial = true)

View File

@@ -102,14 +102,11 @@ object ProtocolManager {
val onlinePacket = packet as PacketOnlineState val onlinePacket = packet as PacketOnlineState
addLog("🟢 Online status received: ${onlinePacket.publicKeysState.size} entries") addLog("🟢 Online status received: ${onlinePacket.publicKeysState.size} entries")
onlinePacket.publicKeysState.forEach { item ->
addLog(" User ${item.publicKey.take(16)}... is ${item.state}")
scope.launch { scope.launch {
messageRepository?.updateOnlineStatus( onlinePacket.publicKeysState.forEach { item ->
publicKey = item.publicKey, val isOnline = item.state == OnlineState.ONLINE
isOnline = item.state == OnlineState.ONLINE addLog(" ${item.publicKey.take(16)}... -> ${if (isOnline) "ONLINE" else "OFFLINE"}")
) messageRepository?.updateOnlineStatus(item.publicKey, isOnline)
} }
} }
} }
@@ -126,20 +123,6 @@ object ProtocolManager {
_typingUsers.value = _typingUsers.value - typingPacket.fromPublicKey _typingUsers.value = _typingUsers.value - typingPacket.fromPublicKey
} }
} }
// 🟢 Обработчик онлайн статуса (0x05)
waitPacket(0x05) { packet ->
val onlinePacket = packet as PacketOnlineState
addLog("🟢 Online status received: ${onlinePacket.publicKeysState.size} entries")
scope.launch {
onlinePacket.publicKeysState.forEach { item ->
val isOnline = item.state == OnlineState.ONLINE
addLog(" ${item.publicKey.take(16)}... -> ${if (isOnline) "ONLINE" else "OFFLINE"}")
messageRepository?.updateOnlineStatus(item.publicKey, isOnline)
}
}
}
} }
/** /**

View File

@@ -648,56 +648,80 @@ fun ChatDetailScreen(
) )
} }
// Выпадающее меню // Выпадающее меню - чистый дизайн без артефактов
DropdownMenu( DropdownMenu(
expanded = showMenu, expanded = showMenu,
onDismissRequest = { showMenu = false }, onDismissRequest = { showMenu = false },
modifier = Modifier modifier = Modifier
.width(220.dp)
.clip(RoundedCornerShape(16.dp))
.background( .background(
color = if (isDarkTheme) Color(0xFF2C2C2E) else Color.White, color = if (isDarkTheme) Color(0xFF1C1C1E) else Color.White
shape = RoundedCornerShape(12.dp) )
.shadow(
elevation = 8.dp,
shape = RoundedCornerShape(16.dp)
) )
) { ) {
// Delete Chat // Delete Chat - красный
DropdownMenuItem( DropdownMenuItem(
text = { text = {
Row(verticalAlignment = Alignment.CenterVertically) { Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 4.dp)
) {
Icon( Icon(
Icons.Default.Delete, Icons.Default.Delete,
contentDescription = null, contentDescription = null,
tint = PrimaryBlue, tint = Color(0xFFE53935),
modifier = Modifier.size(20.dp) modifier = Modifier.size(22.dp)
) )
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(14.dp))
Text( Text(
"Delete Chat", "Delete Chat",
color = textColor, color = Color(0xFFE53935),
fontSize = 16.sp fontSize = 16.sp,
fontWeight = FontWeight.Medium
) )
} }
}, },
onClick = { onClick = {
showMenu = false showMenu = false
showDeleteConfirm = true showDeleteConfirm = true
} },
modifier = Modifier.padding(horizontal = 8.dp)
) )
// Block/Unblock User (не показываем для Saved Messages) // Разделитель
if (!isSavedMessages) {
Divider(
modifier = Modifier.padding(horizontal = 16.dp),
thickness = 0.5.dp,
color = if (isDarkTheme) Color.White.copy(alpha = 0.1f)
else Color.Black.copy(alpha = 0.08f)
)
}
// Block/Unblock User - синий (не показываем для Saved Messages)
if (!isSavedMessages) { if (!isSavedMessages) {
DropdownMenuItem( DropdownMenuItem(
text = { text = {
Row(verticalAlignment = Alignment.CenterVertically) { Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 4.dp)
) {
Icon( Icon(
if (isBlocked) Icons.Default.Check else Icons.Default.Block, if (isBlocked) Icons.Default.Check else Icons.Default.Block,
contentDescription = null, contentDescription = null,
tint = if (isBlocked) PrimaryBlue else Color(0xFFFF3B30), tint = PrimaryBlue,
modifier = Modifier.size(20.dp) modifier = Modifier.size(22.dp)
) )
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(14.dp))
Text( Text(
if (isBlocked) "Unblock User" else "Block User", if (isBlocked) "Unblock User" else "Block User",
color = if (isBlocked) PrimaryBlue else Color(0xFFFF3B30), color = PrimaryBlue,
fontSize = 16.sp fontSize = 16.sp,
fontWeight = FontWeight.Medium
) )
} }
}, },
@@ -708,32 +732,46 @@ fun ChatDetailScreen(
} else { } else {
showBlockConfirm = true showBlockConfirm = true
} }
} },
modifier = Modifier.padding(horizontal = 8.dp)
)
// Разделитель
Divider(
modifier = Modifier.padding(horizontal = 16.dp),
thickness = 0.5.dp,
color = if (isDarkTheme) Color.White.copy(alpha = 0.1f)
else Color.Black.copy(alpha = 0.08f)
) )
} }
// Debug Logs // Debug Logs
DropdownMenuItem( DropdownMenuItem(
text = { text = {
Row(verticalAlignment = Alignment.CenterVertically) { Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 4.dp)
) {
Icon( Icon(
Icons.Default.BugReport, Icons.Default.BugReport,
contentDescription = null, contentDescription = null,
tint = secondaryTextColor, tint = secondaryTextColor,
modifier = Modifier.size(20.dp) modifier = Modifier.size(22.dp)
) )
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(14.dp))
Text( Text(
"Debug Logs", "Debug Logs",
color = textColor, color = textColor,
fontSize = 16.sp fontSize = 16.sp,
fontWeight = FontWeight.Medium
) )
} }
}, },
onClick = { onClick = {
showMenu = false showMenu = false
showLogs = true showLogs = true
} },
modifier = Modifier.padding(horizontal = 8.dp)
) )
} }
} }
@@ -1580,8 +1618,9 @@ private fun MessageBubble(
.background(bubbleColor) .background(bubbleColor)
.padding(horizontal = 12.dp, vertical = 7.dp) .padding(horizontal = 12.dp, vertical = 7.dp)
) { ) {
// 🔥 Telegram-style: текст и время на одной строке, выровнены по нижней границе
Column { Column {
// 🔥 Reply bubble (цитата) - как в React Native // Reply bubble (цитата)
message.replyData?.let { reply -> message.replyData?.let { reply ->
ReplyBubble( ReplyBubble(
replyData = reply, replyData = reply,
@@ -1591,46 +1630,31 @@ private fun MessageBubble(
Spacer(modifier = Modifier.height(6.dp)) Spacer(modifier = Modifier.height(6.dp))
} }
// 🔥 Telegram-style: текст + время inline // Текст и время в одной строке (Row)
if (!message.isOutgoing) {
// Входящие сообщения - время справа inline с текстом
Box {
AppleEmojiText(
text = message.text + " ", // Пробелы для места под время
color = textColor,
fontSize = 16.sp
)
// Время в правом нижнем углу
Text(
text = timeFormat.format(message.timestamp),
color = timeColor,
fontSize = 11.sp,
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(top = 2.dp)
)
}
} else {
// Исходящие сообщения - время + статус справа внизу
Box {
AppleEmojiText(
text = message.text + " ", // Пробелы для места под время и статус
color = textColor,
fontSize = 16.sp
)
// Время и статус в правом нижнем углу
Row( Row(
modifier = Modifier verticalAlignment = Alignment.Bottom, // Выравнивание по нижней границе (baseline)
.align(Alignment.BottomEnd) horizontalArrangement = Arrangement.spacedBy(6.dp)
.padding(top = 2.dp), ) {
verticalAlignment = Alignment.CenterVertically // Текст (не растягивается на всю ширину)
AppleEmojiText(
text = message.text,
color = textColor,
fontSize = 16.sp,
modifier = Modifier.weight(1f, fill = false)
)
// Время и статус справа
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(3.dp),
modifier = Modifier.padding(bottom = 1.dp) // Небольшая коррекция для выравнивания
) { ) {
Text( Text(
text = timeFormat.format(message.timestamp), text = timeFormat.format(message.timestamp),
color = timeColor, color = timeColor,
fontSize = 11.sp fontSize = 11.sp
) )
Spacer(modifier = Modifier.width(3.dp)) if (message.isOutgoing) {
AnimatedMessageStatus( AnimatedMessageStatus(
status = message.status, status = message.status,
timeColor = timeColor, timeColor = timeColor,

View File

@@ -395,11 +395,11 @@ fun ChatsListScreen(
topBar = { topBar = {
AnimatedVisibility( AnimatedVisibility(
visible = visible, visible = visible,
enter = enter = fadeIn(tween(300)) + expandVertically(
fadeIn(tween(400)) + animationSpec = tween(300, easing = FastOutSlowInEasing)
slideInVertically( ),
initialOffsetY = { -it }, exit = fadeOut(tween(200)) + shrinkVertically(
animationSpec = tween(400) animationSpec = tween(200)
) )
) { ) {
key(isDarkTheme) { key(isDarkTheme) {
@@ -833,20 +833,21 @@ fun DialogItem(dialog: DialogUiModel, isDarkTheme: Boolean, onClick: () -> Unit)
) )
} }
// Online indicator // Online indicator - зелёный кружок с белой обводкой
if (dialog.isOnline == 1) { if (dialog.isOnline == 1) {
Box( Box(
modifier = modifier =
Modifier.size(16.dp) Modifier.size(18.dp)
.align(Alignment.BottomEnd) .align(Alignment.BottomEnd)
.offset(x = (-2).dp, y = (-2).dp)
.clip(CircleShape) .clip(CircleShape)
.background( .background(
if (isDarkTheme) Color(0xFF1A1A1A) if (isDarkTheme) Color(0xFF1C1C1E)
else Color.White else Color.White
) )
.padding(2.dp) .padding(3.dp)
.clip(CircleShape) .clip(CircleShape)
.background(Color(0xFF4CAF50)) .background(Color(0xFF34C759)) // iOS зелёный цвет
) )
} }
} }