feat: Refactor message bubble layout to display timestamp and status inline for outgoing messages
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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 ->
|
scope.launch {
|
||||||
addLog(" User ${item.publicKey.take(16)}... is ${item.state}")
|
onlinePacket.publicKeysState.forEach { item ->
|
||||||
|
val isOnline = item.state == OnlineState.ONLINE
|
||||||
scope.launch {
|
addLog(" ${item.publicKey.take(16)}... -> ${if (isOnline) "ONLINE" else "OFFLINE"}")
|
||||||
messageRepository?.updateOnlineStatus(
|
messageRepository?.updateOnlineStatus(item.publicKey, isOnline)
|
||||||
publicKey = item.publicKey,
|
|
||||||
isOnline = item.state == OnlineState.ONLINE
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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) {
|
Row(
|
||||||
// Входящие сообщения - время справа inline с текстом
|
verticalAlignment = Alignment.Bottom, // Выравнивание по нижней границе (baseline)
|
||||||
Box {
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
AppleEmojiText(
|
) {
|
||||||
text = message.text + " ", // Пробелы для места под время
|
// Текст (не растягивается на всю ширину)
|
||||||
color = textColor,
|
AppleEmojiText(
|
||||||
fontSize = 16.sp
|
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
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomEnd)
|
|
||||||
.padding(top = 2.dp)
|
|
||||||
)
|
)
|
||||||
}
|
if (message.isOutgoing) {
|
||||||
} else {
|
|
||||||
// Исходящие сообщения - время + статус справа внизу
|
|
||||||
Box {
|
|
||||||
AppleEmojiText(
|
|
||||||
text = message.text + " ", // Пробелы для места под время и статус
|
|
||||||
color = textColor,
|
|
||||||
fontSize = 16.sp
|
|
||||||
)
|
|
||||||
// Время и статус в правом нижнем углу
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomEnd)
|
|
||||||
.padding(top = 2.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = timeFormat.format(message.timestamp),
|
|
||||||
color = timeColor,
|
|
||||||
fontSize = 11.sp
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(3.dp))
|
|
||||||
AnimatedMessageStatus(
|
AnimatedMessageStatus(
|
||||||
status = message.status,
|
status = message.status,
|
||||||
timeColor = timeColor,
|
timeColor = timeColor,
|
||||||
|
|||||||
@@ -395,12 +395,12 @@ 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) {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
@@ -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 зелёный цвет
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user