package com.bitchat.android.ui import com.bitchat.android.model.BitchatMessage import com.bitchat.android.model.DeliveryStatus import java.util.* import java.util.Collections /** * Handles all message-related operations including deduplication and organization */ class MessageManager(private val state: ChatState) { // Message deduplication - FIXED: Prevent duplicate messages from dual connection paths private val processedUIMessages = Collections.synchronizedSet(mutableSetOf()) private val recentSystemEvents = Collections.synchronizedMap(mutableMapOf()) private val MESSAGE_DEDUP_TIMEOUT = 30000L // 30 seconds private val SYSTEM_EVENT_DEDUP_TIMEOUT = 5000L // 5 seconds // MARK: - Public Message Management fun addMessage(message: BitchatMessage) { val currentMessages = state.getMessagesValue().toMutableList() currentMessages.add(message) state.setMessages(currentMessages) } // Log a system message into the main chat (visible to user) fun addSystemMessage(text: String) { val sys = BitchatMessage( sender = "system", content = text, timestamp = Date(), isRelay = false ) addMessage(sys) } fun clearMessages() { state.setMessages(emptyList()) } // MARK: - Channel Message Management fun addChannelMessage(channel: String, message: BitchatMessage) { val currentChannelMessages = state.getChannelMessagesValue().toMutableMap() if (!currentChannelMessages.containsKey(channel)) { currentChannelMessages[channel] = mutableListOf() } val channelMessageList = currentChannelMessages[channel]?.toMutableList() ?: mutableListOf() channelMessageList.add(message) currentChannelMessages[channel] = channelMessageList state.setChannelMessages(currentChannelMessages) // Update unread count if not currently viewing this channel // Consider both classic channels (state.currentChannel) and geohash location channel selection val viewingClassicChannel = state.getCurrentChannelValue() == channel val viewingGeohashChannel = try { if (channel.startsWith("geo:")) { val geo = channel.removePrefix("geo:") val selected = state.selectedLocationChannel.value selected is com.bitchat.android.geohash.ChannelID.Location && selected.channel.geohash.equals(geo, ignoreCase = true) } else false } catch (_: Exception) { false } if (!viewingClassicChannel && !viewingGeohashChannel) { val currentUnread = state.getUnreadChannelMessagesValue().toMutableMap() currentUnread[channel] = (currentUnread[channel] ?: 0) + 1 state.setUnreadChannelMessages(currentUnread) } } fun clearChannelMessages(channel: String) { val updatedChannelMessages = state.getChannelMessagesValue().toMutableMap() updatedChannelMessages[channel] = emptyList() state.setChannelMessages(updatedChannelMessages) } fun removeChannelMessages(channel: String) { val updatedChannelMessages = state.getChannelMessagesValue().toMutableMap() updatedChannelMessages.remove(channel) state.setChannelMessages(updatedChannelMessages) val updatedUnread = state.getUnreadChannelMessagesValue().toMutableMap() updatedUnread.remove(channel) state.setUnreadChannelMessages(updatedUnread) } fun clearChannelUnreadCount(channel: String) { val currentUnread = state.getUnreadChannelMessagesValue().toMutableMap() currentUnread.remove(channel) state.setUnreadChannelMessages(currentUnread) } // MARK: - Private Message Management fun addPrivateMessage(peerID: String, message: BitchatMessage) { val currentPrivateChats = state.getPrivateChatsValue().toMutableMap() if (!currentPrivateChats.containsKey(peerID)) { currentPrivateChats[peerID] = mutableListOf() } val chatMessages = currentPrivateChats[peerID]?.toMutableList() ?: mutableListOf() chatMessages.add(message) currentPrivateChats[peerID] = chatMessages state.setPrivateChats(currentPrivateChats) // Mark as unread if not currently viewing this chat if (state.getSelectedPrivateChatPeerValue() != peerID && message.sender != state.getNicknameValue()) { val currentUnread = state.getUnreadPrivateMessagesValue().toMutableSet() currentUnread.add(peerID) state.setUnreadPrivateMessages(currentUnread) } } // Variant that does not mark unread (used when we know the message has been read already, e.g., persisted Nostr read store) fun addPrivateMessageNoUnread(peerID: String, message: BitchatMessage) { val currentPrivateChats = state.getPrivateChatsValue().toMutableMap() if (!currentPrivateChats.containsKey(peerID)) { currentPrivateChats[peerID] = mutableListOf() } val chatMessages = currentPrivateChats[peerID]?.toMutableList() ?: mutableListOf() chatMessages.add(message) currentPrivateChats[peerID] = chatMessages state.setPrivateChats(currentPrivateChats) } fun clearPrivateMessages(peerID: String) { val updatedChats = state.getPrivateChatsValue().toMutableMap() updatedChats[peerID] = emptyList() state.setPrivateChats(updatedChats) } fun initializePrivateChat(peerID: String) { if (state.getPrivateChatsValue().containsKey(peerID)) return val updatedChats = state.getPrivateChatsValue().toMutableMap() updatedChats[peerID] = emptyList() state.setPrivateChats(updatedChats) } fun clearPrivateUnreadMessages(peerID: String) { val updatedUnread = state.getUnreadPrivateMessagesValue().toMutableSet() updatedUnread.remove(peerID) state.setUnreadPrivateMessages(updatedUnread) } // MARK: - Message Deduplication /** * Generate a unique key for message deduplication */ fun generateMessageKey(message: BitchatMessage): String { val senderKey = message.senderPeerID ?: message.sender val contentHash = message.content.hashCode() return "$senderKey-${message.timestamp.time}-$contentHash" } /** * Check if a message has already been processed */ fun isMessageProcessed(messageKey: String): Boolean { return processedUIMessages.contains(messageKey) } /** * Mark a message as processed */ fun markMessageProcessed(messageKey: String) { processedUIMessages.add(messageKey) } /** * Check if a system event is a duplicate within the timeout window */ fun isDuplicateSystemEvent(eventType: String, peerID: String): Boolean { val now = System.currentTimeMillis() val eventKey = "$eventType-$peerID" val lastEvent = recentSystemEvents[eventKey] if (lastEvent != null && (now - lastEvent) < SYSTEM_EVENT_DEDUP_TIMEOUT) { return true // Duplicate event } recentSystemEvents[eventKey] = now return false } /** * Clean up old entries from deduplication caches */ fun cleanupDeduplicationCaches() { val now = System.currentTimeMillis() // Clean up processed UI messages (remove entries older than 30 seconds) if (processedUIMessages.size > 1000) { processedUIMessages.clear() } // Clean up recent system events (remove entries older than timeout) recentSystemEvents.entries.removeAll { (_, timestamp) -> (now - timestamp) > SYSTEM_EVENT_DEDUP_TIMEOUT * 2 } } // MARK: - Delivery Status Updates fun updateMessageDeliveryStatus(messageID: String, status: DeliveryStatus) { // Update in private chats val updatedPrivateChats = state.getPrivateChatsValue().toMutableMap() var updated = false updatedPrivateChats.forEach { (peerID, messages) -> val updatedMessages = messages.toMutableList() val messageIndex = updatedMessages.indexOfFirst { it.id == messageID } if (messageIndex >= 0) { updatedMessages[messageIndex] = updatedMessages[messageIndex].copy(deliveryStatus = status) updatedPrivateChats[peerID] = updatedMessages updated = true } } if (updated) { state.setPrivateChats(updatedPrivateChats) } // Update in main messages val updatedMessages = state.getMessagesValue().toMutableList() val messageIndex = updatedMessages.indexOfFirst { it.id == messageID } if (messageIndex >= 0) { updatedMessages[messageIndex] = updatedMessages[messageIndex].copy(deliveryStatus = status) state.setMessages(updatedMessages) } // Update in channel messages val updatedChannelMessages = state.getChannelMessagesValue().toMutableMap() updatedChannelMessages.forEach { (channel, messages) -> val channelMessagesList = messages.toMutableList() val channelMessageIndex = channelMessagesList.indexOfFirst { it.id == messageID } if (channelMessageIndex >= 0) { channelMessagesList[channelMessageIndex] = channelMessagesList[channelMessageIndex].copy(deliveryStatus = status) updatedChannelMessages[channel] = channelMessagesList } } state.setChannelMessages(updatedChannelMessages) } // MARK: - Utility Functions fun parseMentions(content: String, peerNicknames: Set, currentNickname: String?): List { val mentionRegex = "@([a-zA-Z0-9_]+)".toRegex() val allNicknames = peerNicknames + (currentNickname ?: "") return mentionRegex.findAll(content) .map { it.groupValues[1] } .filter { allNicknames.contains(it) } .distinct() .toList() } fun parseChannels(content: String): List { val channelRegex = "#([a-zA-Z0-9_]+)".toRegex() return channelRegex.findAll(content) .map { it.groupValues[0] } // Include the # .distinct() .toList() } // MARK: - Emergency Clear fun clearAllMessages() { state.setMessages(emptyList()) state.setPrivateChats(emptyMap()) state.setChannelMessages(emptyMap()) state.setUnreadPrivateMessages(emptySet()) state.setUnreadChannelMessages(emptyMap()) processedUIMessages.clear() recentSystemEvents.clear() } }