fix: update status bar color management for improved visibility in ChatDetailScreen and OtherProfileScreen

This commit is contained in:
2026-02-03 02:39:13 +05:00
parent 7d90a9d744
commit da76243e3a
8 changed files with 197 additions and 50 deletions

View File

@@ -194,22 +194,24 @@ fun ChatDetailScreen(
var imageViewerInitialIndex by remember { mutableStateOf(0) }
var imageViewerSourceBounds by remember { mutableStateOf<ImageSourceBounds?>(null) }
// 🎨 Управление статус баром - чёрный при просмотре фото
// 🎨 Управление статус баром
DisposableEffect(isDarkTheme, showImageViewer) {
val insetsController = window?.let { WindowCompat.getInsetsController(it, view) }
if (showImageViewer) {
// 📸 При просмотре фото - чёрный статус бар
window?.statusBarColor = 0xFF000000.toInt()
window?.statusBarColor = android.graphics.Color.BLACK
insetsController?.isAppearanceLightStatusBars = false
} else {
// Обычный режим - цвет хедера
val headerColor = if (isDarkTheme) 0xFF212121.toInt() else 0xFFFFFFFF.toInt()
window?.statusBarColor = headerColor
// Обычный режим - прозрачный статус бар, иконки по теме
window?.statusBarColor = android.graphics.Color.TRANSPARENT
insetsController?.isAppearanceLightStatusBars = !isDarkTheme
}
onDispose { }
onDispose {
// Восстанавливаем прозрачный статус бар при выходе
window?.statusBarColor = android.graphics.Color.TRANSPARENT
}
}
// 📷 Camera: URI для сохранения фото
@@ -342,8 +344,14 @@ fun ChatDetailScreen(
Pair<ChatMessage, Boolean>>() // message, showDateHeader
var lastDateString = ""
// 🔥 КРИТИЧНО: Дедупликация по ID перед сортировкой!
val uniqueMessages = messages.distinctBy { it.id }
if (uniqueMessages.size != messages.size) {
android.util.Log.e("ChatDetailScreen", "🚨 DEDUPLICATED ${messages.size - uniqueMessages.size} messages in UI! Original: ${messages.map { it.id }}")
}
// Сортируем по времени (новые -> старые) для reversed layout
val sortedMessages = messages.sortedByDescending { it.timestamp.time }
val sortedMessages = uniqueMessages.sortedByDescending { it.timestamp.time }
for (i in sortedMessages.indices) {
val message = sortedMessages[i]
@@ -892,7 +900,8 @@ fun ChatDetailScreen(
publicKey = user.publicKey,
avatarRepository = avatarRepository,
size = 40.dp,
isDarkTheme = isDarkTheme
isDarkTheme = isDarkTheme,
displayName = user.title.ifEmpty { user.username } // 🔥 Для инициалов
)
}
}

View File

@@ -258,7 +258,18 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
// Добавляем все сразу
kotlinx.coroutines.withContext(Dispatchers.Main.immediate) {
val currentList = _messages.value
_messages.value = (currentList + newMessages).sortedBy { it.timestamp }
val newList = (currentList + newMessages).sortedBy { it.timestamp }
// 🔍 DEBUG: Проверка на дублирующиеся ID
val allIds = newList.map { it.id }
val duplicates = allIds.groupBy { it }.filter { it.value.size > 1 }.keys
if (duplicates.isNotEmpty()) {
android.util.Log.e("ChatViewModel", "🚨 DUPLICATE IDS FOUND in pollLatestMessages: $duplicates")
android.util.Log.e("ChatViewModel", " currentList ids: ${currentList.map { it.id }}")
android.util.Log.e("ChatViewModel", " newMessages ids: ${newMessages.map { it.id }}")
}
_messages.value = newList
}
// Обновляем кэш
@@ -359,6 +370,23 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
// ✅ handleIncomingMessage удалён - обработка входящих сообщений теперь ТОЛЬКО в MessageRepository
// Это предотвращает дублирование сообщений
/**
* 🔥 Безопасное добавление сообщения - предотвращает дубликаты
* Возвращает true если сообщение было добавлено
*/
private fun addMessageSafely(message: ChatMessage): Boolean {
val currentMessages = _messages.value
val currentIds = currentMessages.map { it.id }.toSet()
android.util.Log.d("ChatViewModel", "🔍 addMessageSafely: id=${message.id}, currentCount=${currentMessages.size}, ids=${currentIds.take(5)}...")
if (message.id in currentIds) {
android.util.Log.e("ChatViewModel", "🚨 BLOCKED DUPLICATE: id=${message.id} already exists in ${currentIds.size} messages!")
return false
}
_messages.value = currentMessages + message
android.util.Log.d("ChatViewModel", "✅ Added message: id=${message.id}, newCount=${_messages.value.size}")
return true
}
private fun updateMessageStatus(messageId: String, status: MessageStatus) {
_messages.value = _messages.value.map { msg ->
if (msg.id == messageId) msg.copy(status = status) else msg
@@ -565,10 +593,27 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
// 🔥 СРАЗУ обновляем UI - пользователь видит сообщения мгновенно
// НО сохраняем оптимистичные сообщения (SENDING), которые ещё не в БД
withContext(Dispatchers.Main.immediate) {
val optimisticMessages = _messages.value.filter { msg ->
msg.status == MessageStatus.SENDING && messages.none { it.id == msg.id }
val dbIds = messages.map { it.id }.toSet()
val currentMsgs = _messages.value
android.util.Log.d("ChatViewModel", "📥 loadMessages: dbCount=${messages.size}, currentCount=${currentMsgs.size}")
android.util.Log.d("ChatViewModel", " DB ids: ${dbIds.take(5)}...")
android.util.Log.d("ChatViewModel", " Current ids: ${currentMsgs.map { it.id }.take(5)}...")
val optimisticMessages = currentMsgs.filter { msg ->
msg.status == MessageStatus.SENDING && msg.id !in dbIds
}
_messages.value = messages + optimisticMessages
android.util.Log.d("ChatViewModel", " Optimistic (SENDING, not in DB): ${optimisticMessages.size} - ${optimisticMessages.map { it.id }}")
val newList = messages + optimisticMessages
// 🔍 Финальная дедупликация по ID (на всякий случай)
val deduplicatedList = newList.distinctBy { it.id }
if (deduplicatedList.size != newList.size) {
android.util.Log.e("ChatViewModel", "🚨 DEDUPLICATED ${newList.size - deduplicatedList.size} messages!")
}
_messages.value = deduplicatedList
_isLoading.value = false
}
@@ -1287,7 +1332,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
status = MessageStatus.SENDING,
replyData = replyData // Данные для reply bubble
)
_messages.value = _messages.value + optimisticMessage
// <20> Безопасное добавление с проверкой дубликатов
addMessageSafely(optimisticMessage)
_inputText.value = ""
// 🔥 Очищаем reply после отправки - данные сохраняются в displayReplyMessages для анимации
@@ -1475,7 +1522,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
)
)
)
_messages.value = _messages.value + optimisticMessage
// 🔥 Безопасное добавление с проверкой дубликатов
addMessageSafely(optimisticMessage)
_inputText.value = ""
@@ -1531,10 +1579,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
ProtocolManager.send(packet)
}
withContext(Dispatchers.Main) {
updateMessageStatus(messageId, MessageStatus.SENT)
}
// 💾 Сохраняем изображение в файл локально (как в desktop)
AttachmentFileManager.saveAttachment(
context = getApplication(),
@@ -1564,10 +1608,19 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
encryptedKey = encryptedKey,
timestamp = timestamp,
isFromMe = true,
delivered = if (isSavedMessages) 2 else 0,
delivered = if (isSavedMessages) 2 else 0, // SENDING для обычных
attachmentsJson = attachmentsJson
)
// 🔥 После успешной отправки обновляем статус на SENT (2) в БД и UI
if (!isSavedMessages) {
updateMessageStatusInDb(messageId, 2) // SENT
}
withContext(Dispatchers.Main) {
updateMessageStatus(messageId, MessageStatus.SENT)
}
saveDialog(if (text.isNotEmpty()) text else "photo", timestamp)
} catch (e: Exception) {
@@ -1634,7 +1687,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
status = MessageStatus.SENDING,
attachments = attachmentsList
)
_messages.value = _messages.value + optimisticMessage
// 🔥 Безопасное добавление с проверкой дубликатов
addMessageSafely(optimisticMessage)
_inputText.value = ""
@@ -1715,10 +1769,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
ProtocolManager.send(packet)
}
withContext(Dispatchers.Main) {
updateMessageStatus(messageId, MessageStatus.SENT)
}
// Сохраняем в БД
saveMessageToDatabase(
messageId = messageId,
@@ -1731,6 +1781,16 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
attachmentsJson = attachmentsJsonArray.toString()
)
// 🔥 Обновляем статус в БД после отправки
if (!isSavedMessages) {
updateMessageStatusInDb(messageId, 2) // SENT
}
// Обновляем UI
withContext(Dispatchers.Main) {
updateMessageStatus(messageId, MessageStatus.SENT)
}
saveDialog(if (text.isNotEmpty()) text else "📷 ${images.size} photos", timestamp)
} catch (e: Exception) {
@@ -1785,7 +1845,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
)
)
)
_messages.value = _messages.value + optimisticMessage
// 🔥 Безопасное добавление с проверкой дубликатов
addMessageSafely(optimisticMessage)
_inputText.value = ""
@@ -1838,10 +1899,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
ProtocolManager.send(packet)
}
withContext(Dispatchers.Main) {
updateMessageStatus(messageId, MessageStatus.SENT)
}
// ⚠️ НЕ сохраняем файл локально - они слишком большие
// Файлы загружаются с Transport Server при необходимости
val attachmentsJson = JSONArray().apply {
@@ -1853,6 +1910,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
})
}.toString()
// 🔥 Сохраняем сначала с SENDING, потом обновляем на SENT
saveMessageToDatabase(
messageId = messageId,
text = text,
@@ -1860,10 +1918,19 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
encryptedKey = encryptedKey,
timestamp = timestamp,
isFromMe = true,
delivered = if (isSavedMessages) 2 else 0,
delivered = if (isSavedMessages) 2 else 0, // SENDING для обычных, SENT для saved
attachmentsJson = attachmentsJson
)
// 🔥 После успешной отправки обновляем статус на SENT (2) в БД и UI
if (!isSavedMessages) {
updateMessageStatusInDb(messageId, 2) // SENT
}
withContext(Dispatchers.Main) {
updateMessageStatus(messageId, MessageStatus.SENT)
}
saveDialog(if (text.isNotEmpty()) text else "file", timestamp)
} catch (e: Exception) {
@@ -1973,7 +2040,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
)
)
withContext(Dispatchers.Main) {
_messages.value = _messages.value + optimisticMessage
addMessageSafely(optimisticMessage)
}
// 2. Шифрование текста (пустой текст для аватарки)
@@ -2029,10 +2096,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
ProtocolManager.send(packet)
}
withContext(Dispatchers.Main) {
updateMessageStatus(messageId, MessageStatus.SENT)
}
// 💾 Сохраняем аватар в файл локально (как IMAGE - с приватным ключом)
AttachmentFileManager.saveAttachment(
context = getApplication(),
@@ -2063,6 +2126,16 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
attachmentsJson = attachmentsJson
)
// 🔥 Обновляем статус в БД после отправки
if (!isSavedMessages) {
updateMessageStatusInDb(messageId, 2) // SENT
}
// Обновляем UI
withContext(Dispatchers.Main) {
updateMessageStatus(messageId, MessageStatus.SENT)
}
saveDialog("\$a=Avatar", timestamp)

View File

@@ -448,7 +448,8 @@ fun ChatsListScreen(
publicKey = accountPublicKey,
avatarRepository = avatarRepository,
size = 66.dp,
isDarkTheme = isDarkTheme
isDarkTheme = isDarkTheme,
displayName = accountName.ifEmpty { accountUsername } // 🔥 Для инициалов
)
}
@@ -1247,7 +1248,8 @@ fun ChatItem(
size = 56.dp,
isDarkTheme = isDarkTheme,
showOnlineIndicator = true,
isOnline = chat.isOnline
isOnline = chat.isOnline,
displayName = chat.name // 🔥 Для инициалов
)
Spacer(modifier = Modifier.width(12.dp))
@@ -1724,11 +1726,20 @@ fun DialogItemContent(
)
}
} else {
// 🔥 Формируем displayName для инициалов в placeholder
val avatarDisplayName = when {
dialog.opponentTitle.isNotEmpty() &&
dialog.opponentTitle != dialog.opponentKey &&
!dialog.opponentTitle.startsWith(dialog.opponentKey.take(7)) -> dialog.opponentTitle
dialog.opponentUsername.isNotEmpty() -> dialog.opponentUsername
else -> null
}
com.rosetta.messenger.ui.components.AvatarImage(
publicKey = dialog.opponentKey,
avatarRepository = avatarRepository,
size = 56.dp,
isDarkTheme = isDarkTheme
isDarkTheme = isDarkTheme,
displayName = avatarDisplayName
)
}

View File

@@ -193,12 +193,22 @@ private fun SearchResultItem(
modifier = Modifier.size(20.dp)
)
} else {
// Приоритет: title -> username -> publicKey
val initials = when {
user.title.isNotEmpty() &&
user.title != user.publicKey &&
!user.title.startsWith(user.publicKey.take(7)) -> {
getInitials(user.title)
}
user.username.isNotEmpty() -> {
user.username.take(2).uppercase()
}
else -> {
user.publicKey.take(2).uppercase()
}
}
Text(
text = if (user.title.isNotEmpty()) {
getInitials(user.title)
} else {
user.publicKey.take(2).uppercase()
},
text = initials,
fontSize = 15.sp,
fontWeight = FontWeight.SemiBold,
color = avatarColors.textColor

View File

@@ -369,7 +369,8 @@ private fun RecentUserItem(
size = 48.dp,
isDarkTheme = isDarkTheme,
showOnlineIndicator = false,
isOnline = false
isOnline = false,
displayName = user.title.ifEmpty { user.username } // 🔥 Для инициалов
)
Spacer(modifier = Modifier.width(12.dp))