fix: update status bar color management for improved visibility in ChatDetailScreen and OtherProfileScreen
This commit is contained in:
@@ -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 } // 🔥 Для инициалов
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user