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

View File

@@ -197,8 +197,19 @@ class AppleEmojiEditTextView @JvmOverloads constructor(
}
// Восстанавливаем курсор, убедившись что он в допустимых пределах
if (cursorPosition >= 0 && cursorPosition <= editable.length) {
post { setSelection(cursorPosition.coerceIn(0, editable.length)) }
// 🔥 Захватываем длину ДО post, т.к. text может измениться
val safePosition = cursorPosition.coerceIn(0, editable.length)
if (safePosition >= 0) {
post {
try {
val currentLength = text?.length ?: 0
if (safePosition <= currentLength) {
setSelection(safePosition.coerceIn(0, currentLength))
}
} catch (e: Exception) {
// Игнорируем - текст мог измениться
}
}
}
} finally {
isUpdating = false

View File

@@ -27,6 +27,7 @@ import com.rosetta.messenger.repository.AvatarRepository
import com.rosetta.messenger.ui.chats.AvatarColors
import com.rosetta.messenger.ui.chats.getAvatarColor
import com.rosetta.messenger.ui.chats.getAvatarText
import com.rosetta.messenger.ui.chats.getInitials
import com.rosetta.messenger.utils.AvatarFileManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -57,7 +58,8 @@ fun AvatarImage(
onClick: (() -> Unit)? = null,
showOnlineIndicator: Boolean = false,
isOnline: Boolean = false,
shape: Shape = CircleShape
shape: Shape = CircleShape,
displayName: String? = null // 🔥 Имя для инициалов (title/username)
) {
// Получаем аватары из репозитория
val avatars by avatarRepository?.getAvatars(publicKey, allDecode = false)?.collectAsState()
@@ -113,7 +115,8 @@ fun AvatarImage(
publicKey = publicKey,
size = size,
isDarkTheme = isDarkTheme,
shape = shape
shape = shape,
displayName = displayName
)
}
@@ -138,10 +141,16 @@ fun AvatarPlaceholder(
size: Dp = 40.dp,
isDarkTheme: Boolean,
fontSize: TextUnit? = null,
shape: Shape = CircleShape
shape: Shape = CircleShape,
displayName: String? = null // 🔥 Имя для инициалов
) {
val avatarColors = getAvatarColor(publicKey, isDarkTheme)
val avatarText = getAvatarText(publicKey)
// 🔥 Используем displayName для инициалов, если оно есть
val avatarText = if (!displayName.isNullOrEmpty() && displayName != publicKey && !displayName.startsWith(publicKey.take(7))) {
getInitials(displayName)
} else {
getAvatarText(publicKey)
}
Box(
modifier = Modifier

View File

@@ -1,7 +1,9 @@
package com.rosetta.messenger.ui.settings
import android.app.Activity
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.core.view.WindowCompat
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
@@ -36,6 +38,7 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
@@ -82,6 +85,26 @@ fun OtherProfileScreen(
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
val avatarColors = getAvatarColor(user.publicKey, isDarkTheme)
val context = LocalContext.current
val view = LocalView.current
val window = remember { (view.context as? Activity)?.window }
// 🎨 Статус бар - прозрачный с белыми иконками (поверх аватара)
DisposableEffect(Unit) {
val insetsController = window?.let { WindowCompat.getInsetsController(it, view) }
// Сохраняем оригинальные значения
val originalStatusBarColor = window?.statusBarColor ?: 0
val originalLightStatusBars = insetsController?.isAppearanceLightStatusBars ?: false
// Прозрачный статус бар с белыми иконками
window?.statusBarColor = android.graphics.Color.TRANSPARENT
insetsController?.isAppearanceLightStatusBars = false
onDispose {
// Восстанавливаем при выходе
window?.statusBarColor = originalStatusBarColor
insetsController?.isAppearanceLightStatusBars = originalLightStatusBars
}
}
// 🔥 Получаем тот же ChatViewModel что и в ChatDetailScreen для очистки истории
val viewModel: ChatViewModel = viewModel(key = "chat_${user.publicKey}")