feat: Bump version to 1.0.7, enhance message delivery handling, and add connection logs screen
This commit is contained in:
@@ -425,7 +425,9 @@ fun ChatDetailScreen(
|
||||
val inputText by viewModel.inputText.collectAsState()
|
||||
val isTyping by viewModel.opponentTyping.collectAsState()
|
||||
val isLoadingMore by viewModel.isLoadingMore.collectAsState()
|
||||
val isOnline by viewModel.opponentOnline.collectAsState()
|
||||
val rawIsOnline by viewModel.opponentOnline.collectAsState()
|
||||
// If typing, the user is obviously online — never show "offline" while typing
|
||||
val isOnline = rawIsOnline || isTyping
|
||||
val isLoading by viewModel.isLoading.collectAsState() // 🔥 Для скелетона
|
||||
|
||||
// <20>🔥 Reply/Forward state
|
||||
@@ -905,7 +907,7 @@ fun ChatDetailScreen(
|
||||
)
|
||||
.background(
|
||||
Color(
|
||||
0xFFFF3B30
|
||||
0xFF3B82F6
|
||||
)
|
||||
),
|
||||
contentAlignment =
|
||||
|
||||
@@ -584,6 +584,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
subscribedToOnlineStatus = false // 🔥 Сбрасываем флаг подписки при смене диалога
|
||||
isDialogActive = true // 🔥 Диалог активен!
|
||||
|
||||
// Desktop parity: refresh opponent name/username from server on dialog open,
|
||||
// so renamed contacts get their new name displayed immediately.
|
||||
messageRepository?.forceRequestUserInfo(publicKey)
|
||||
|
||||
// 📝 Восстанавливаем черновик для этого диалога (draft, как в Telegram)
|
||||
val draft = com.rosetta.messenger.data.DraftManager.getDraft(publicKey)
|
||||
_inputText.value = draft ?: ""
|
||||
|
||||
@@ -189,6 +189,10 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
|
||||
dialog.opponentTitle ==
|
||||
dialog.opponentKey.take(
|
||||
7
|
||||
) ||
|
||||
dialog.opponentTitle ==
|
||||
dialog.opponentKey.take(
|
||||
8
|
||||
))
|
||||
) {
|
||||
loadUserInfoForDialog(dialog.opponentKey)
|
||||
@@ -371,6 +375,20 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
|
||||
.collect { blockedSet -> _blockedUsers.value = blockedSet }
|
||||
}
|
||||
|
||||
// Desktop parity: when sync finishes (syncInProgress transitions true → false),
|
||||
// clear the one-shot requestedUserInfoKeys guard so the dialog-list .map{} block
|
||||
// can re-trigger loadUserInfoForDialog() on the next Room emission for any
|
||||
// dialogs that still have empty titles.
|
||||
launch {
|
||||
var wasSyncing = false
|
||||
ProtocolManager.syncInProgress.collect { syncing ->
|
||||
if (wasSyncing && !syncing) {
|
||||
requestedUserInfoKeys.clear()
|
||||
}
|
||||
wasSyncing = syncing
|
||||
}
|
||||
}
|
||||
|
||||
} // end accountSubscriptionsJob
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
package com.rosetta.messenger.ui.chats
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.rosetta.messenger.network.ProtocolManager
|
||||
import com.rosetta.messenger.network.ProtocolState
|
||||
import compose.icons.TablerIcons
|
||||
import compose.icons.tablericons.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Full-screen connection logs viewer.
|
||||
* Shows all protocol/WebSocket logs from ProtocolManager.debugLogs.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ConnectionLogsScreen(
|
||||
isDarkTheme: Boolean,
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
val logs by ProtocolManager.debugLogs.collectAsState()
|
||||
val protocolState by ProtocolManager.getProtocol().state.collectAsState()
|
||||
val syncInProgress by ProtocolManager.syncInProgress.collectAsState()
|
||||
|
||||
val bgColor = if (isDarkTheme) Color(0xFF0E0E0E) else Color(0xFFF5F5F5)
|
||||
val cardColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color.White
|
||||
val textColor = if (isDarkTheme) Color(0xFFE0E0E0) else Color(0xFF1A1A1A)
|
||||
val headerColor = if (isDarkTheme) Color(0xFF1E1E1E) else Color(0xFF228BE6)
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// Auto-scroll to bottom when new logs arrive
|
||||
LaunchedEffect(logs.size) {
|
||||
if (logs.isNotEmpty()) {
|
||||
listState.animateScrollToItem(logs.size - 1)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(bgColor)
|
||||
.statusBarsPadding()
|
||||
) {
|
||||
// Header
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(headerColor)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
imageVector = TablerIcons.ArrowLeft,
|
||||
contentDescription = "Back",
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Connection Logs",
|
||||
color = Color.White,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
// Clear button
|
||||
IconButton(onClick = { ProtocolManager.clearLogs() }) {
|
||||
Icon(
|
||||
imageVector = TablerIcons.Trash,
|
||||
contentDescription = "Clear logs",
|
||||
tint = Color.White.copy(alpha = 0.8f),
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// Scroll to bottom
|
||||
IconButton(onClick = {
|
||||
scope.launch {
|
||||
if (logs.isNotEmpty()) listState.animateScrollToItem(logs.size - 1)
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = TablerIcons.ArrowDown,
|
||||
contentDescription = "Scroll to bottom",
|
||||
tint = Color.White.copy(alpha = 0.8f),
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Status bar
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(if (isDarkTheme) Color(0xFF252525) else Color(0xFFE8E8E8))
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
val stateColor = when (protocolState) {
|
||||
ProtocolState.AUTHENTICATED -> Color(0xFF4CAF50)
|
||||
ProtocolState.CONNECTING, ProtocolState.HANDSHAKING -> Color(0xFFFFA726)
|
||||
ProtocolState.DISCONNECTED -> Color(0xFFEF5350)
|
||||
else -> Color(0xFF9E9E9E)
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(8.dp)
|
||||
.background(stateColor, RoundedCornerShape(4.dp))
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = protocolState.name,
|
||||
color = textColor,
|
||||
fontSize = 13.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
}
|
||||
|
||||
if (syncInProgress) {
|
||||
Text(
|
||||
text = "SYNCING…",
|
||||
color = Color(0xFFFFA726),
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "${logs.size} logs",
|
||||
color = textColor.copy(alpha = 0.5f),
|
||||
fontSize = 12.sp,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
}
|
||||
|
||||
// Logs list
|
||||
if (logs.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "No logs yet.\nConnect to see protocol activity.",
|
||||
color = textColor.copy(alpha = 0.4f),
|
||||
fontSize = 14.sp,
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
items(logs, key = { it.hashCode().toString() + logs.indexOf(it) }) { log ->
|
||||
val logColor = when {
|
||||
"❌" in log || "FAILED" in log || "Error" in log || "error" in log -> Color(0xFFEF5350)
|
||||
"✅" in log || "COMPLETE" in log || "SUCCESS" in log -> Color(0xFF4CAF50)
|
||||
"⚠️" in log || "WARNING" in log -> Color(0xFFFFA726)
|
||||
"🔄" in log || "RECONNECT" in log || "SYNC" in log -> Color(0xFF42A5F5)
|
||||
"💓" in log || "Heartbeat" in log -> Color(0xFF9E9E9E)
|
||||
"📤" in log || "Sending" in log -> Color(0xFF7E57C2)
|
||||
"📥" in log || "onMessage" in log -> Color(0xFF26A69A)
|
||||
"🤝" in log || "HANDSHAKE" in log -> Color(0xFFFFCA28)
|
||||
else -> textColor.copy(alpha = 0.85f)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = log,
|
||||
color = logColor,
|
||||
fontSize = 11.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
lineHeight = 15.sp,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
if ("❌" in log) Color.Red.copy(alpha = 0.08f)
|
||||
else Color.Transparent,
|
||||
RoundedCornerShape(4.dp)
|
||||
)
|
||||
.padding(horizontal = 6.dp, vertical = 2.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user