feat: Bump version to 1.0.7, enhance message delivery handling, and add connection logs screen

This commit is contained in:
2026-02-25 11:16:31 +05:00
parent 75810a0696
commit aed685ee73
11 changed files with 618 additions and 59 deletions

View File

@@ -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 =

View File

@@ -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 ?: ""

View File

@@ -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
}

View File

@@ -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)
)
}
}
}
}
}