feat: Add message count query to MessageDao, enhance ChatDetailScreen with auto-focus on reply input, and improve read receipt handling in ChatViewModel
This commit is contained in:
@@ -145,6 +145,15 @@ interface MessageDao {
|
|||||||
""")
|
""")
|
||||||
fun getMessagesFlow(account: String, dialogKey: String): Flow<List<MessageEntity>>
|
fun getMessagesFlow(account: String, dialogKey: String): Flow<List<MessageEntity>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить количество сообщений в диалоге
|
||||||
|
*/
|
||||||
|
@Query("""
|
||||||
|
SELECT COUNT(*) FROM messages
|
||||||
|
WHERE account = :account AND dialog_key = :dialogKey
|
||||||
|
""")
|
||||||
|
suspend fun getMessageCount(account: String, dialogKey: String): Int
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Получить последние N сообщений диалога
|
* Получить последние N сообщений диалога
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -240,6 +240,17 @@ fun ChatDetailScreen(
|
|||||||
// 🔥 FocusRequester для автофокуса на инпут при reply
|
// 🔥 FocusRequester для автофокуса на инпут при reply
|
||||||
val inputFocusRequester = remember { FocusRequester() }
|
val inputFocusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
|
// 🔥 Автофокус на инпут при появлении reply панели
|
||||||
|
LaunchedEffect(hasReply) {
|
||||||
|
if (hasReply) {
|
||||||
|
try {
|
||||||
|
inputFocusRequester.requestFocus()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Игнорируем если фокус не удался
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 🔥 Дополнительная высота для reply панели (~50dp)
|
// 🔥 Дополнительная высота для reply панели (~50dp)
|
||||||
val replyPanelHeight = if (hasReply) 50.dp else 0.dp
|
val replyPanelHeight = if (hasReply) 50.dp else 0.dp
|
||||||
|
|
||||||
@@ -2041,7 +2052,8 @@ private fun MessageInputBar(
|
|||||||
textSize = 16f,
|
textSize = 16f,
|
||||||
hint = "Type message...",
|
hint = "Type message...",
|
||||||
hintColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93),
|
hintColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93),
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
requestFocus = hasReply
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,8 +105,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
private var lastTypingSentTime = 0L
|
private var lastTypingSentTime = 0L
|
||||||
private val TYPING_THROTTLE_MS = 2000L // Отправляем не чаще чем раз в 2 сек
|
private val TYPING_THROTTLE_MS = 2000L // Отправляем не чаще чем раз в 2 сек
|
||||||
|
|
||||||
// Отслеживание прочитанных сообщений
|
// Отслеживание прочитанных сообщений - храним timestamp последнего прочитанного
|
||||||
private val sentReadReceipts = mutableSetOf<String>()
|
private var lastReadMessageTimestamp = 0L
|
||||||
|
// Флаг что read receipt уже отправлен для текущего диалога
|
||||||
|
private var readReceiptSentForCurrentDialog = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setupPacketListeners()
|
setupPacketListeners()
|
||||||
@@ -116,10 +118,15 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
// Входящие сообщения
|
// Входящие сообщения
|
||||||
ProtocolManager.waitPacket(0x06) { packet ->
|
ProtocolManager.waitPacket(0x06) { packet ->
|
||||||
val msgPacket = packet as PacketMessage
|
val msgPacket = packet as PacketMessage
|
||||||
|
ProtocolManager.addLog("📨 ChatVM got packet 0x06: from=${msgPacket.fromPublicKey.take(16)}, to=${msgPacket.toPublicKey.take(16)}")
|
||||||
|
ProtocolManager.addLog("📨 opponentKey=${opponentKey?.take(16) ?: "NULL"}")
|
||||||
if (msgPacket.fromPublicKey == opponentKey || msgPacket.toPublicKey == opponentKey) {
|
if (msgPacket.fromPublicKey == opponentKey || msgPacket.toPublicKey == opponentKey) {
|
||||||
|
ProtocolManager.addLog("📨 ✅ Match! Processing message...")
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
handleIncomingMessage(msgPacket)
|
handleIncomingMessage(msgPacket)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
ProtocolManager.addLog("📨 ❌ No match, ignoring")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,6 +206,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val privateKey = myPrivateKey ?: return@launch
|
val privateKey = myPrivateKey ?: return@launch
|
||||||
|
val account = myPublicKey ?: return@launch
|
||||||
|
|
||||||
ProtocolManager.addLog("📩 Incoming message: ${packet.messageId.take(8)}...")
|
ProtocolManager.addLog("📩 Incoming message: ${packet.messageId.take(8)}...")
|
||||||
ProtocolManager.addLog("📎 Attachments count: ${packet.attachments.size}")
|
ProtocolManager.addLog("📎 Attachments count: ${packet.attachments.size}")
|
||||||
@@ -269,7 +277,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
jsonArray.toString()
|
jsonArray.toString()
|
||||||
} else "[]"
|
} else "[]"
|
||||||
|
|
||||||
// Обновляем UI в Main потоке
|
// Обновляем UI в Main потоке (как в архиве - просто добавляем без лишних проверок)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
val message = ChatMessage(
|
val message = ChatMessage(
|
||||||
id = packet.messageId,
|
id = packet.messageId,
|
||||||
@@ -277,12 +285,14 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
isOutgoing = packet.fromPublicKey == myPublicKey,
|
isOutgoing = packet.fromPublicKey == myPublicKey,
|
||||||
timestamp = Date(packet.timestamp),
|
timestamp = Date(packet.timestamp),
|
||||||
status = MessageStatus.DELIVERED,
|
status = MessageStatus.DELIVERED,
|
||||||
replyData = replyData // 🔥 Добавляем reply данные
|
replyData = replyData
|
||||||
)
|
)
|
||||||
|
// Просто добавляем как в архиве: setMessages((prev) => ([...prev, newMessage]))
|
||||||
_messages.value = _messages.value + message
|
_messages.value = _messages.value + message
|
||||||
|
ProtocolManager.addLog("✅ Added to UI: ${packet.messageId.take(8)}... text: ${decryptedText.take(20)}")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сохраняем в БД (уже в IO потоке)
|
// Сохраняем в БД (INSERT OR IGNORE - не будет дублей)
|
||||||
saveMessageToDatabase(
|
saveMessageToDatabase(
|
||||||
messageId = packet.messageId,
|
messageId = packet.messageId,
|
||||||
text = decryptedText,
|
text = decryptedText,
|
||||||
@@ -293,17 +303,22 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
delivered = 1,
|
delivered = 1,
|
||||||
attachmentsJson = attachmentsJson // 🔥 Сохраняем attachments
|
attachmentsJson = attachmentsJson // 🔥 Сохраняем attachments
|
||||||
)
|
)
|
||||||
|
|
||||||
// Обновляем диалог
|
// Обновляем диалог
|
||||||
saveDialog(decryptedText, packet.timestamp)
|
saveDialog(decryptedText, packet.timestamp)
|
||||||
|
|
||||||
// ⚠️ Delivery отправляется в ProtocolManager.setupPacketHandlers()
|
// ⚠️ Delivery отправляется в ProtocolManager.setupPacketHandlers()
|
||||||
// Не отправляем повторно чтобы избежать дублирования!
|
// Не отправляем повторно чтобы избежать дублирования!
|
||||||
|
|
||||||
// 👁️ Сразу отправляем read receipt (как в Telegram - сообщения прочитаны если чат открыт)
|
// 👁️ Отмечаем сообщение как прочитанное в БД
|
||||||
|
messageDao.markAsRead(account, packet.messageId)
|
||||||
|
|
||||||
|
// 👁️ Отправляем read receipt собеседнику (как в архиве - сразу при получении)
|
||||||
delay(100) // Небольшая задержка для естественности
|
delay(100) // Небольшая задержка для естественности
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
sendReadReceipt(packet.messageId, packet.fromPublicKey)
|
// Обновляем timestamp и отправляем read receipt
|
||||||
|
lastReadMessageTimestamp = packet.timestamp
|
||||||
|
sendReadReceiptToOpponent()
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -364,7 +379,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
currentOffset = 0
|
currentOffset = 0
|
||||||
hasMoreMessages = true
|
hasMoreMessages = true
|
||||||
isLoadingMessages = false
|
isLoadingMessages = false
|
||||||
sentReadReceipts.clear()
|
lastReadMessageTimestamp = 0L
|
||||||
|
readReceiptSentForCurrentDialog = false
|
||||||
|
|
||||||
ProtocolManager.addLog("💬 Dialog opened: ${title.ifEmpty { publicKey.take(16) }}...")
|
ProtocolManager.addLog("💬 Dialog opened: ${title.ifEmpty { publicKey.take(16) }}...")
|
||||||
|
|
||||||
@@ -394,10 +410,14 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
val dialogKey = getDialogKey(account, opponent)
|
val dialogKey = getDialogKey(account, opponent)
|
||||||
ProtocolManager.addLog("📂 Loading messages from DB for dialog: $dialogKey")
|
ProtocolManager.addLog("📂 Loading messages from DB for dialog: $dialogKey")
|
||||||
|
|
||||||
|
// 🔍 Проверяем общее количество сообщений в диалоге
|
||||||
|
val totalCount = messageDao.getMessageCount(account, dialogKey)
|
||||||
|
ProtocolManager.addLog("📂 Total messages in DB: $totalCount")
|
||||||
|
|
||||||
// Получаем первую страницу сообщений
|
// Получаем первую страницу сообщений
|
||||||
val entities = messageDao.getMessages(account, dialogKey, limit = PAGE_SIZE, offset = 0)
|
val entities = messageDao.getMessages(account, dialogKey, limit = PAGE_SIZE, offset = 0)
|
||||||
|
|
||||||
ProtocolManager.addLog("📂 Loaded ${entities.size} messages from DB")
|
ProtocolManager.addLog("📂 Loaded ${entities.size} messages from DB (offset: 0, limit: $PAGE_SIZE)")
|
||||||
|
|
||||||
hasMoreMessages = entities.size >= PAGE_SIZE
|
hasMoreMessages = entities.size >= PAGE_SIZE
|
||||||
currentOffset = entities.size
|
currentOffset = entities.size
|
||||||
@@ -407,10 +427,24 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
entityToChatMessage(entity)
|
entityToChatMessage(entity)
|
||||||
}.reversed()
|
}.reversed()
|
||||||
|
|
||||||
|
// 🔥 Отмечаем все входящие сообщения как прочитанные в БД (как в архиве)
|
||||||
|
messageDao.markDialogAsRead(account, dialogKey)
|
||||||
|
// 🔥 Очищаем счетчик непрочитанных в диалоге
|
||||||
|
dialogDao.clearUnreadCount(account, opponent)
|
||||||
|
ProtocolManager.addLog("👁️ Marked all incoming messages as read in DB, cleared unread count")
|
||||||
|
|
||||||
// Обновляем UI в Main потоке
|
// Обновляем UI в Main потоке
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
_messages.value = messages
|
_messages.value = messages
|
||||||
_isLoading.value = false
|
_isLoading.value = false
|
||||||
|
|
||||||
|
// 🔥 Отправляем read receipt собеседнику (как в архиве)
|
||||||
|
if (messages.isNotEmpty()) {
|
||||||
|
val lastIncoming = messages.lastOrNull { !it.isOutgoing }
|
||||||
|
if (lastIncoming != null && lastIncoming.timestamp.time > lastReadMessageTimestamp) {
|
||||||
|
sendReadReceiptToOpponent()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingMessages = false
|
isLoadingMessages = false
|
||||||
@@ -907,6 +941,11 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
try {
|
try {
|
||||||
val dialogKey = getDialogKey(account, opponent)
|
val dialogKey = getDialogKey(account, opponent)
|
||||||
|
|
||||||
|
ProtocolManager.addLog("💾 Saving message to DB:")
|
||||||
|
ProtocolManager.addLog(" messageId: ${messageId.take(8)}...")
|
||||||
|
ProtocolManager.addLog(" dialogKey: $dialogKey")
|
||||||
|
ProtocolManager.addLog(" text: ${text.take(20)}...")
|
||||||
|
|
||||||
val entity = MessageEntity(
|
val entity = MessageEntity(
|
||||||
account = account,
|
account = account,
|
||||||
fromPublicKey = if (isFromMe) account else opponent,
|
fromPublicKey = if (isFromMe) account else opponent,
|
||||||
@@ -924,8 +963,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
dialogKey = dialogKey
|
dialogKey = dialogKey
|
||||||
)
|
)
|
||||||
|
|
||||||
messageDao.insertMessage(entity)
|
val insertedId = messageDao.insertMessage(entity)
|
||||||
ProtocolManager.addLog("💾 Message saved to DB: ${messageId.take(8)}...")
|
ProtocolManager.addLog("✅ Message saved with DB id: $insertedId")
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
ProtocolManager.addLog("❌ Message save error: ${e.message}")
|
ProtocolManager.addLog("❌ Message save error: ${e.message}")
|
||||||
@@ -989,18 +1028,20 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 👁️ Отправить подтверждение о прочтении сообщения
|
* 👁️ Отправить read receipt собеседнику
|
||||||
* В Desktop PacketRead не содержит messageId - он просто сообщает что мы прочитали сообщения
|
* Как в архиве - просто отправляем PacketRead без messageId
|
||||||
|
* Означает что мы прочитали все сообщения от этого собеседника
|
||||||
*/
|
*/
|
||||||
fun sendReadReceipt(messageId: String, senderPublicKey: String) {
|
private fun sendReadReceiptToOpponent() {
|
||||||
// Не отправляем повторно для этого собеседника
|
val opponent = opponentKey ?: return
|
||||||
val receiptKey = senderPublicKey
|
|
||||||
if (sentReadReceipts.contains(receiptKey)) return
|
|
||||||
|
|
||||||
val sender = myPublicKey ?: return
|
val sender = myPublicKey ?: return
|
||||||
val privateKey = myPrivateKey ?: return
|
val privateKey = myPrivateKey ?: return
|
||||||
|
|
||||||
sentReadReceipts.add(receiptKey)
|
// Обновляем timestamp последнего прочитанного
|
||||||
|
val lastIncoming = _messages.value.lastOrNull { !it.isOutgoing }
|
||||||
|
if (lastIncoming != null) {
|
||||||
|
lastReadMessageTimestamp = lastIncoming.timestamp.time
|
||||||
|
}
|
||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
@@ -1010,14 +1051,12 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
val packet = PacketRead().apply {
|
val packet = PacketRead().apply {
|
||||||
this.privateKey = privateKeyHash
|
this.privateKey = privateKeyHash
|
||||||
fromPublicKey = sender // Мы (кто прочитал)
|
fromPublicKey = sender // Мы (кто прочитал)
|
||||||
toPublicKey = senderPublicKey // Кому отправляем уведомление
|
toPublicKey = opponent // Кому отправляем уведомление (собеседник)
|
||||||
}
|
}
|
||||||
|
|
||||||
ProtocolManager.send(packet)
|
ProtocolManager.send(packet)
|
||||||
ProtocolManager.addLog("👁️ Read receipt sent to: ${senderPublicKey.take(8)}...")
|
ProtocolManager.addLog("👁️ Read receipt sent to: ${opponent.take(8)}...")
|
||||||
|
readReceiptSentForCurrentDialog = true
|
||||||
// Обновляем в БД что сообщение прочитано
|
|
||||||
updateMessageReadInDb(messageId)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Read receipt send error", e)
|
Log.e(TAG, "Read receipt send error", e)
|
||||||
}
|
}
|
||||||
@@ -1025,18 +1064,36 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 👁️ Отметить все непрочитанные входящие сообщения как прочитанные
|
* 👁️ Публичный метод для отправки read receipt (вызывается из ChatDetailScreen)
|
||||||
|
* Теперь работает как в архиве - при изменении списка сообщений
|
||||||
*/
|
*/
|
||||||
fun markVisibleMessagesAsRead() {
|
fun markVisibleMessagesAsRead() {
|
||||||
val opponent = opponentKey ?: return
|
val opponent = opponentKey ?: return
|
||||||
|
val account = myPublicKey ?: return
|
||||||
|
|
||||||
viewModelScope.launch {
|
// Находим последнее входящее сообщение
|
||||||
_messages.value
|
val lastIncoming = _messages.value.lastOrNull { !it.isOutgoing }
|
||||||
.filter { !it.isOutgoing && it.status != MessageStatus.READ }
|
if (lastIncoming == null) return
|
||||||
.forEach { message ->
|
|
||||||
sendReadReceipt(message.id, opponent)
|
// Если timestamp не изменился - не отправляем повторно
|
||||||
}
|
if (lastIncoming.timestamp.time <= lastReadMessageTimestamp) return
|
||||||
|
|
||||||
|
ProtocolManager.addLog("👁️ markVisibleMessagesAsRead: new message detected")
|
||||||
|
|
||||||
|
// Отмечаем в БД и очищаем счетчик непрочитанных
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val dialogKey = getDialogKey(account, opponent)
|
||||||
|
messageDao.markDialogAsRead(account, dialogKey)
|
||||||
|
dialogDao.clearUnreadCount(account, opponent)
|
||||||
|
ProtocolManager.addLog("👁️ Marked dialog as read in DB, cleared unread count")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Mark as read error", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Отправляем read receipt
|
||||||
|
sendReadReceiptToOpponent()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1064,23 +1121,12 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Обновить статус прочтения в БД
|
|
||||||
*/
|
|
||||||
private suspend fun updateMessageReadInDb(messageId: String) {
|
|
||||||
try {
|
|
||||||
val account = myPublicKey ?: return
|
|
||||||
messageDao.markAsRead(account, messageId)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Update read status error", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun canSend(): Boolean = _inputText.value.isNotBlank() && !isSending
|
fun canSend(): Boolean = _inputText.value.isNotBlank() && !isSending
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
sentReadReceipts.clear()
|
lastReadMessageTimestamp = 0L
|
||||||
|
readReceiptSentForCurrentDialog = false
|
||||||
opponentKey = null
|
opponentKey = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -877,30 +877,38 @@ fun DialogItem(dialog: DialogEntity, isDarkTheme: Boolean, onClick: () -> Unit)
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// 🔥 Используем AppleEmojiText для отображения эмодзи
|
// 🔥 Используем AppleEmojiText для отображения эмодзи
|
||||||
|
// Если есть непрочитанные - текст темнее
|
||||||
AppleEmojiText(
|
AppleEmojiText(
|
||||||
text = dialog.lastMessage.ifEmpty { "No messages" },
|
text = dialog.lastMessage.ifEmpty { "No messages" },
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
color = secondaryTextColor,
|
color = if (dialog.unreadCount > 0) textColor.copy(alpha = 0.85f) else secondaryTextColor,
|
||||||
|
fontWeight = if (dialog.unreadCount > 0) FontWeight.Medium else FontWeight.Normal,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Unread badge
|
// Unread badge
|
||||||
if (dialog.unreadCount > 0) {
|
if (dialog.unreadCount > 0) {
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
val unreadText = when {
|
||||||
|
dialog.unreadCount > 999 -> "999+"
|
||||||
|
dialog.unreadCount > 99 -> "99+"
|
||||||
|
else -> dialog.unreadCount.toString()
|
||||||
|
}
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.size(22.dp)
|
Modifier.height(22.dp)
|
||||||
.clip(CircleShape)
|
.widthIn(min = 22.dp)
|
||||||
.background(PrimaryBlue),
|
.clip(RoundedCornerShape(11.dp))
|
||||||
|
.background(PrimaryBlue)
|
||||||
|
.padding(horizontal = 6.dp),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text =
|
text = unreadText,
|
||||||
if (dialog.unreadCount > 99) "99+"
|
|
||||||
else dialog.unreadCount.toString(),
|
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = Color.White
|
color = Color.White,
|
||||||
|
maxLines = 1
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -207,8 +207,22 @@ fun AppleEmojiTextField(
|
|||||||
textSize: Float = 16f,
|
textSize: Float = 16f,
|
||||||
hint: String = "Message",
|
hint: String = "Message",
|
||||||
hintColor: androidx.compose.ui.graphics.Color = androidx.compose.ui.graphics.Color.Gray,
|
hintColor: androidx.compose.ui.graphics.Color = androidx.compose.ui.graphics.Color.Gray,
|
||||||
onViewCreated: ((AppleEmojiEditTextView) -> Unit)? = null
|
onViewCreated: ((AppleEmojiEditTextView) -> Unit)? = null,
|
||||||
|
requestFocus: Boolean = false
|
||||||
) {
|
) {
|
||||||
|
// Храним ссылку на view для управления фокусом
|
||||||
|
var editTextView by remember { mutableStateOf<AppleEmojiEditTextView?>(null) }
|
||||||
|
|
||||||
|
// 🔥 Автоматический запрос фокуса когда requestFocus = true
|
||||||
|
LaunchedEffect(requestFocus) {
|
||||||
|
if (requestFocus && editTextView != null) {
|
||||||
|
editTextView?.requestFocus()
|
||||||
|
// Показываем клавиатуру
|
||||||
|
val imm = editTextView?.context?.getSystemService(android.content.Context.INPUT_METHOD_SERVICE) as? android.view.inputmethod.InputMethodManager
|
||||||
|
imm?.showSoftInput(editTextView, android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
AndroidView(
|
AndroidView(
|
||||||
factory = { ctx ->
|
factory = { ctx ->
|
||||||
AppleEmojiEditTextView(ctx).apply {
|
AppleEmojiEditTextView(ctx).apply {
|
||||||
@@ -220,6 +234,8 @@ fun AppleEmojiTextField(
|
|||||||
// Убираем все возможные фоны у EditText
|
// Убираем все возможные фоны у EditText
|
||||||
background = null
|
background = null
|
||||||
setBackgroundColor(android.graphics.Color.TRANSPARENT)
|
setBackgroundColor(android.graphics.Color.TRANSPARENT)
|
||||||
|
// Сохраняем ссылку на view
|
||||||
|
editTextView = this
|
||||||
// Уведомляем о создании view
|
// Уведомляем о создании view
|
||||||
onViewCreated?.invoke(this)
|
onViewCreated?.invoke(this)
|
||||||
}
|
}
|
||||||
@@ -249,7 +265,8 @@ fun AppleEmojiText(
|
|||||||
text: String,
|
text: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
color: androidx.compose.ui.graphics.Color = androidx.compose.ui.graphics.Color.White,
|
color: androidx.compose.ui.graphics.Color = androidx.compose.ui.graphics.Color.White,
|
||||||
fontSize: androidx.compose.ui.unit.TextUnit = androidx.compose.ui.unit.TextUnit.Unspecified
|
fontSize: androidx.compose.ui.unit.TextUnit = androidx.compose.ui.unit.TextUnit.Unspecified,
|
||||||
|
fontWeight: androidx.compose.ui.text.font.FontWeight? = null
|
||||||
) {
|
) {
|
||||||
val fontSizeValue = if (fontSize == androidx.compose.ui.unit.TextUnit.Unspecified) 15f
|
val fontSizeValue = if (fontSize == androidx.compose.ui.unit.TextUnit.Unspecified) 15f
|
||||||
else fontSize.value
|
else fontSize.value
|
||||||
@@ -257,17 +274,29 @@ fun AppleEmojiText(
|
|||||||
// Минимальная высота для корректного отображения emoji
|
// Минимальная высота для корректного отображения emoji
|
||||||
val minHeight = (fontSizeValue * 1.5).toInt()
|
val minHeight = (fontSizeValue * 1.5).toInt()
|
||||||
|
|
||||||
|
// Преобразуем FontWeight в Android typeface style
|
||||||
|
val typefaceStyle = when (fontWeight) {
|
||||||
|
androidx.compose.ui.text.font.FontWeight.Bold,
|
||||||
|
androidx.compose.ui.text.font.FontWeight.ExtraBold,
|
||||||
|
androidx.compose.ui.text.font.FontWeight.Black,
|
||||||
|
androidx.compose.ui.text.font.FontWeight.SemiBold -> android.graphics.Typeface.BOLD
|
||||||
|
androidx.compose.ui.text.font.FontWeight.Medium -> android.graphics.Typeface.NORMAL // Medium не поддерживается напрямую
|
||||||
|
else -> android.graphics.Typeface.NORMAL
|
||||||
|
}
|
||||||
|
|
||||||
AndroidView(
|
AndroidView(
|
||||||
factory = { ctx ->
|
factory = { ctx ->
|
||||||
AppleEmojiTextView(ctx).apply {
|
AppleEmojiTextView(ctx).apply {
|
||||||
setTextColor(color.toArgb())
|
setTextColor(color.toArgb())
|
||||||
setTextSize(fontSizeValue)
|
setTextSize(fontSizeValue)
|
||||||
minimumHeight = (minHeight * ctx.resources.displayMetrics.density).toInt()
|
minimumHeight = (minHeight * ctx.resources.displayMetrics.density).toInt()
|
||||||
|
setTypeface(typeface, typefaceStyle)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
update = { view ->
|
update = { view ->
|
||||||
view.setTextWithEmojis(text)
|
view.setTextWithEmojis(text)
|
||||||
view.setTextColor(color.toArgb())
|
view.setTextColor(color.toArgb())
|
||||||
|
view.setTypeface(view.typeface, typefaceStyle)
|
||||||
},
|
},
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user