package com.bitchat.android.ui import android.app.Application import android.util.Log import androidx.core.app.NotificationManagerCompat import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import com.bitchat.android.favorites.FavoritesPersistenceService import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import com.bitchat.android.mesh.BluetoothMeshDelegate import com.bitchat.android.mesh.BluetoothMeshService import com.bitchat.android.model.BitchatMessage import com.bitchat.android.model.BitchatMessageType import com.bitchat.android.nostr.NostrIdentityBridge import com.bitchat.android.protocol.BitchatPacket import kotlinx.coroutines.launch import com.bitchat.android.util.NotificationIntervalManager import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.util.Date import kotlin.random.Random import com.bitchat.android.services.VerificationService import com.bitchat.android.identity.SecureIdentityStateManager import com.bitchat.android.noise.NoiseSession import com.bitchat.android.nostr.GeohashAliasRegistry import com.bitchat.android.util.dataFromHexString import com.bitchat.android.util.hexEncodedString import java.security.MessageDigest /** * Refactored ChatViewModel - Main coordinator for bitchat functionality * Delegates specific responsibilities to specialized managers while maintaining 100% iOS compatibility */ class ChatViewModel( application: Application, val meshService: BluetoothMeshService ) : AndroidViewModel(application), BluetoothMeshDelegate { private val debugManager by lazy { try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance() } catch (e: Exception) { null } } companion object { private const val TAG = "ChatViewModel" } fun sendVoiceNote(toPeerIDOrNull: String?, channelOrNull: String?, filePath: String) { mediaSendingManager.sendVoiceNote(toPeerIDOrNull, channelOrNull, filePath) } fun sendFileNote(toPeerIDOrNull: String?, channelOrNull: String?, filePath: String) { mediaSendingManager.sendFileNote(toPeerIDOrNull, channelOrNull, filePath) } fun sendImageNote(toPeerIDOrNull: String?, channelOrNull: String?, filePath: String) { mediaSendingManager.sendImageNote(toPeerIDOrNull, channelOrNull, filePath) } fun getCurrentNpub(): String? { return try { NostrIdentityBridge .getCurrentNostrIdentity(getApplication()) ?.npub } catch (_: Exception) { null } } fun buildMyQRString(nickname: String, npub: String?): String { return VerificationService.buildMyQRString(nickname, npub) ?: "" } // MARK: - State management private val state = ChatState( scope = viewModelScope, ) // Transfer progress tracking private val transferMessageMap = mutableMapOf() private val messageTransferMap = mutableMapOf() // Specialized managers private val dataManager = DataManager(application.applicationContext) private val identityManager by lazy { SecureIdentityStateManager(getApplication()) } private val messageManager = MessageManager(state) private val channelManager = ChannelManager(state, messageManager, dataManager, viewModelScope) // Create Noise session delegate for clean dependency injection private val noiseSessionDelegate = object : NoiseSessionDelegate { override fun hasEstablishedSession(peerID: String): Boolean = meshService.hasEstablishedSession(peerID) override fun initiateHandshake(peerID: String) = meshService.initiateNoiseHandshake(peerID) override fun getMyPeerID(): String = meshService.myPeerID } val privateChatManager = PrivateChatManager(state, messageManager, dataManager, noiseSessionDelegate) private val commandProcessor = CommandProcessor(state, messageManager, channelManager, privateChatManager) private val notificationManager = NotificationManager( application.applicationContext, NotificationManagerCompat.from(application.applicationContext), NotificationIntervalManager() ) private val verificationHandler = VerificationHandler( context = application.applicationContext, scope = viewModelScope, meshService = meshService, identityManager = identityManager, state = state, notificationManager = notificationManager, messageManager = messageManager ) val verifiedFingerprints = verificationHandler.verifiedFingerprints // Media file sending manager private val mediaSendingManager = MediaSendingManager(state, messageManager, channelManager, meshService) // Delegate handler for mesh callbacks private val meshDelegateHandler = MeshDelegateHandler( state = state, messageManager = messageManager, channelManager = channelManager, privateChatManager = privateChatManager, notificationManager = notificationManager, coroutineScope = viewModelScope, onHapticFeedback = { ChatViewModelUtils.triggerHapticFeedback(application.applicationContext) }, getMyPeerID = { meshService.myPeerID }, getMeshService = { meshService } ) // New Geohash architecture ViewModel (replaces God object service usage in UI path) val geohashViewModel = GeohashViewModel( application = application, state = state, messageManager = messageManager, privateChatManager = privateChatManager, meshDelegateHandler = meshDelegateHandler, dataManager = dataManager, notificationManager = notificationManager ) val messages: StateFlow> = state.messages val connectedPeers: StateFlow> = state.connectedPeers val nickname: StateFlow = state.nickname val isConnected: StateFlow = state.isConnected val privateChats: StateFlow>> = state.privateChats val selectedPrivateChatPeer: StateFlow = state.selectedPrivateChatPeer val unreadPrivateMessages: StateFlow> = state.unreadPrivateMessages val joinedChannels: StateFlow> = state.joinedChannels val currentChannel: StateFlow = state.currentChannel val channelMessages: StateFlow>> = state.channelMessages val unreadChannelMessages: StateFlow> = state.unreadChannelMessages val passwordProtectedChannels: StateFlow> = state.passwordProtectedChannels val showPasswordPrompt: StateFlow = state.showPasswordPrompt val passwordPromptChannel: StateFlow = state.passwordPromptChannel val showSidebar: StateFlow = state.showSidebar val hasUnreadChannels = state.hasUnreadChannels val hasUnreadPrivateMessages = state.hasUnreadPrivateMessages val showCommandSuggestions: StateFlow = state.showCommandSuggestions val commandSuggestions: StateFlow> = state.commandSuggestions val showMentionSuggestions: StateFlow = state.showMentionSuggestions val mentionSuggestions: StateFlow> = state.mentionSuggestions val favoritePeers: StateFlow> = state.favoritePeers val peerSessionStates: StateFlow> = state.peerSessionStates val peerFingerprints: StateFlow> = state.peerFingerprints val peerNicknames: StateFlow> = state.peerNicknames val peerRSSI: StateFlow> = state.peerRSSI val peerDirect: StateFlow> = state.peerDirect val showAppInfo: StateFlow = state.showAppInfo val showVerificationSheet: StateFlow = state.showVerificationSheet val showSecurityVerificationSheet: StateFlow = state.showSecurityVerificationSheet val selectedLocationChannel: StateFlow = state.selectedLocationChannel val isTeleported: StateFlow = state.isTeleported val geohashPeople: StateFlow> = state.geohashPeople val teleportedGeo: StateFlow> = state.teleportedGeo val geohashParticipantCounts: StateFlow> = state.geohashParticipantCounts init { // Note: Mesh service delegate is now set by MainActivity loadAndInitialize() // Hydrate UI state from process-wide AppStateStore to survive Activity recreation viewModelScope.launch { try { com.bitchat.android.services.AppStateStore.peers.collect { peers -> state.setConnectedPeers(peers) state.setIsConnected(peers.isNotEmpty()) } } catch (_: Exception) { } } viewModelScope.launch { try { com.bitchat.android.services.AppStateStore.publicMessages.collect { msgs -> // Source of truth is AppStateStore; replace to avoid duplicate keys in LazyColumn state.setMessages(msgs) } } catch (_: Exception) { } } viewModelScope.launch { try { com.bitchat.android.services.AppStateStore.privateMessages.collect { byPeer -> // Replace with store snapshot state.setPrivateChats(byPeer) // Recompute unread set using SeenMessageStore for robustness across Activity recreation try { val seen = com.bitchat.android.services.SeenMessageStore.getInstance(getApplication()) val myNick = state.getNicknameValue() ?: meshService.myPeerID val unread = mutableSetOf() byPeer.forEach { (peer, list) -> if (list.any { msg -> msg.sender != myNick && !seen.hasRead(msg.id) }) unread.add(peer) } state.setUnreadPrivateMessages(unread) } catch (_: Exception) { } } } catch (_: Exception) { } } viewModelScope.launch { try { com.bitchat.android.services.AppStateStore.channelMessages.collect { byChannel -> // Replace with store snapshot state.setChannelMessages(byChannel) } } catch (_: Exception) { } } // Subscribe to BLE transfer progress and reflect in message deliveryStatus viewModelScope.launch { com.bitchat.android.mesh.TransferProgressManager.events.collect { evt -> mediaSendingManager.handleTransferProgressEvent(evt) } } // Removed background location notes subscription. Notes now load only when sheet opens. } fun cancelMediaSend(messageId: String) { // Delegate to MediaSendingManager which tracks transfer IDs and cleans up UI state mediaSendingManager.cancelMediaSend(messageId) } private fun loadAndInitialize() { // Load nickname val nickname = dataManager.loadNickname() state.setNickname(nickname) // Load data val (joinedChannels, protectedChannels) = channelManager.loadChannelData() state.setJoinedChannels(joinedChannels) state.setPasswordProtectedChannels(protectedChannels) // Initialize channel messages joinedChannels.forEach { channel -> if (!state.getChannelMessagesValue().containsKey(channel)) { val updatedChannelMessages = state.getChannelMessagesValue().toMutableMap() updatedChannelMessages[channel] = emptyList() state.setChannelMessages(updatedChannelMessages) } } // Load other data dataManager.loadFavorites() state.setFavoritePeers(dataManager.favoritePeers.toSet()) dataManager.loadBlockedUsers() dataManager.loadGeohashBlockedUsers() // Log all favorites at startup dataManager.logAllFavorites() logCurrentFavoriteState() // Initialize session state monitoring initializeSessionStateMonitoring() // Bridge DebugSettingsManager -> Chat messages when verbose logging is on viewModelScope.launch { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().debugMessages.collect { msgs -> if (com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().verboseLoggingEnabled.value) { // Only show debug logs in the Mesh chat timeline to avoid leaking into geohash chats val selectedLocation = state.selectedLocationChannel.value if (selectedLocation is com.bitchat.android.geohash.ChannelID.Mesh) { // Append only latest debug message as system message to avoid flooding msgs.lastOrNull()?.let { dm -> messageManager.addSystemMessage(dm.content) } } } } } // Initialize new geohash architecture geohashViewModel.initialize() // Initialize favorites persistence service com.bitchat.android.favorites.FavoritesPersistenceService.initialize(getApplication()) // Load verified fingerprints from secure storage verificationHandler.loadVerifiedFingerprints() // Ensure NostrTransport knows our mesh peer ID for embedded packets try { val nostrTransport = com.bitchat.android.nostr.NostrTransport.getInstance(getApplication()) nostrTransport.senderPeerID = meshService.myPeerID } catch (_: Exception) { } // Note: Mesh service is now started by MainActivity // BLE receives are inserted by MessageHandler path; no VoiceNoteBus for Tor in this branch. } override fun onCleared() { super.onCleared() // Note: Mesh service lifecycle is now managed by MainActivity } // MARK: - Nickname Management fun setNickname(newNickname: String) { state.setNickname(newNickname) dataManager.saveNickname(newNickname) meshService.sendBroadcastAnnounce() } /** * Ensure Nostr DM subscription for a geohash conversation key if known * Minimal-change approach: reflectively access GeohashViewModel internals to reuse pipeline */ private fun ensureGeohashDMSubscriptionIfNeeded(convKey: String) { try { val repoField = GeohashViewModel::class.java.getDeclaredField("repo") repoField.isAccessible = true val repo = repoField.get(geohashViewModel) as com.bitchat.android.nostr.GeohashRepository val gh = repo.getConversationGeohash(convKey) if (!gh.isNullOrEmpty()) { val subMgrField = GeohashViewModel::class.java.getDeclaredField("subscriptionManager") subMgrField.isAccessible = true val subMgr = subMgrField.get(geohashViewModel) as com.bitchat.android.nostr.NostrSubscriptionManager val identity = com.bitchat.android.nostr.NostrIdentityBridge.deriveIdentity(gh, getApplication()) val subId = "geo-dm-$gh" val currentDmSubField = GeohashViewModel::class.java.getDeclaredField("currentDmSubId") currentDmSubField.isAccessible = true val currentId = currentDmSubField.get(geohashViewModel) as String? if (currentId != subId) { (currentId)?.let { subMgr.unsubscribe(it) } currentDmSubField.set(geohashViewModel, subId) subMgr.subscribeGiftWraps( pubkey = identity.publicKeyHex, sinceMs = System.currentTimeMillis() - 172800000L, id = subId, handler = { event -> val dmHandlerField = GeohashViewModel::class.java.getDeclaredField("dmHandler") dmHandlerField.isAccessible = true val dmHandler = dmHandlerField.get(geohashViewModel) as com.bitchat.android.nostr.NostrDirectMessageHandler dmHandler.onGiftWrap(event, gh, identity) } ) } } } catch (e: Exception) { Log.w(TAG, "ensureGeohashDMSubscriptionIfNeeded failed: ${e.message}") } } // MARK: - Channel Management (delegated) fun joinChannel(channel: String, password: String? = null): Boolean { return channelManager.joinChannel(channel, password, meshService.myPeerID) } fun switchToChannel(channel: String?) { channelManager.switchToChannel(channel) } fun leaveChannel(channel: String) { channelManager.leaveChannel(channel) meshService.sendMessage("left $channel") } // MARK: - Private Chat Management (delegated) fun startPrivateChat(peerID: String) { // For geohash conversation keys, ensure DM subscription is active if (peerID.startsWith("nostr_")) { ensureGeohashDMSubscriptionIfNeeded(peerID) } val success = privateChatManager.startPrivateChat(peerID, meshService) if (success) { // Notify notification manager about current private chat setCurrentPrivateChatPeer(peerID) // Clear notifications for this sender since user is now viewing the chat clearNotificationsForSender(peerID) // Persistently mark all messages in this conversation as read so Nostr fetches // after app restarts won't re-mark them as unread. try { val seen = com.bitchat.android.services.SeenMessageStore.getInstance(getApplication()) val chats = state.getPrivateChatsValue() val messages = chats[peerID] ?: emptyList() messages.forEach { msg -> try { seen.markRead(msg.id) } catch (_: Exception) { } } } catch (_: Exception) { } } } fun endPrivateChat() { privateChatManager.endPrivateChat() // Notify notification manager that no private chat is active setCurrentPrivateChatPeer(null) // Clear mesh mention notifications since user is now back in mesh chat clearMeshMentionNotifications() } // MARK: - Open Latest Unread Private Chat fun openLatestUnreadPrivateChat() { try { val unreadKeys = state.getUnreadPrivateMessagesValue() if (unreadKeys.isEmpty()) return val me = state.getNicknameValue() ?: meshService.myPeerID val chats = state.getPrivateChatsValue() // Pick the latest incoming message among unread conversations var bestKey: String? = null var bestTime: Long = Long.MIN_VALUE unreadKeys.forEach { key -> val list = chats[key] if (!list.isNullOrEmpty()) { // Prefer the latest incoming message (sender != me), fallback to last message val latestIncoming = list.lastOrNull { it.sender != me } val candidateTime = (latestIncoming ?: list.last()).timestamp.time if (candidateTime > bestTime) { bestTime = candidateTime bestKey = key } } } val targetKey = bestKey ?: unreadKeys.firstOrNull() ?: return val openPeer: String = if (targetKey.startsWith("nostr_")) { // Use the exact conversation key for geohash DMs and ensure DM subscription ensureGeohashDMSubscriptionIfNeeded(targetKey) targetKey } else { // Resolve to a canonical mesh peer if needed val canonical = com.bitchat.android.services.ConversationAliasResolver.resolveCanonicalPeerID( selectedPeerID = targetKey, connectedPeers = state.getConnectedPeersValue(), meshNoiseKeyForPeer = { pid -> meshService.getPeerInfo(pid)?.noisePublicKey }, meshHasPeer = { pid -> meshService.getPeerInfo(pid)?.isConnected == true }, nostrPubHexForAlias = { alias -> com.bitchat.android.nostr.GeohashAliasRegistry.get(alias) }, findNoiseKeyForNostr = { key -> com.bitchat.android.favorites.FavoritesPersistenceService.shared.findNoiseKey(key) } ) canonical ?: targetKey } startPrivateChat(openPeer) // If sidebar visible, hide it to focus on the private chat if (state.getShowSidebarValue()) { state.setShowSidebar(false) } } catch (e: Exception) { Log.w(TAG, "openLatestUnreadPrivateChat failed: ${e.message}") } } // END - Open Latest Unread Private Chat // MARK: - Message Sending fun sendMessage(content: String) { if (content.isEmpty()) return // Check for commands if (content.startsWith("/")) { val selectedLocationForCommand = state.selectedLocationChannel.value commandProcessor.processCommand(content, meshService, meshService.myPeerID, { messageContent, mentions, channel -> if (selectedLocationForCommand is com.bitchat.android.geohash.ChannelID.Location) { // Route command-generated public messages via Nostr in geohash channels geohashViewModel.sendGeohashMessage( messageContent, selectedLocationForCommand.channel, meshService.myPeerID, state.getNicknameValue() ) } else { // Default: route via mesh meshService.sendMessage(messageContent, mentions, channel) } }) return } val mentions = messageManager.parseMentions(content, meshService.getPeerNicknames().values.toSet(), state.getNicknameValue()) // REMOVED: Auto-join mentioned channels feature that was incorrectly parsing hashtags from @mentions // This was causing messages like "test @jack#1234 test" to auto-join channel "#1234" var selectedPeer = state.getSelectedPrivateChatPeerValue() val currentChannelValue = state.getCurrentChannelValue() if (selectedPeer != null) { // If the selected peer is a temporary Nostr alias or a noise-hex identity, resolve to a canonical target selectedPeer = com.bitchat.android.services.ConversationAliasResolver.resolveCanonicalPeerID( selectedPeerID = selectedPeer, connectedPeers = state.getConnectedPeersValue(), meshNoiseKeyForPeer = { pid -> meshService.getPeerInfo(pid)?.noisePublicKey }, meshHasPeer = { pid -> meshService.getPeerInfo(pid)?.isConnected == true }, nostrPubHexForAlias = { alias -> com.bitchat.android.nostr.GeohashAliasRegistry.get(alias) }, findNoiseKeyForNostr = { key -> com.bitchat.android.favorites.FavoritesPersistenceService.shared.findNoiseKey(key) } ).also { canonical -> if (canonical != state.getSelectedPrivateChatPeerValue()) { privateChatManager.startPrivateChat(canonical, meshService) } } // Send private message val recipientNickname = meshService.getPeerNicknames()[selectedPeer] privateChatManager.sendPrivateMessage( content, selectedPeer, recipientNickname, state.getNicknameValue(), meshService.myPeerID ) { messageContent, peerID, recipientNicknameParam, messageId -> // Route via MessageRouter (mesh when connected+established, else Nostr) val router = com.bitchat.android.services.MessageRouter.getInstance(getApplication(), meshService) router.sendPrivate(messageContent, peerID, recipientNicknameParam, messageId) } } else { // Check if we're in a location channel val selectedLocationChannel = state.selectedLocationChannel.value if (selectedLocationChannel is com.bitchat.android.geohash.ChannelID.Location) { // Send to geohash channel via Nostr ephemeral event geohashViewModel.sendGeohashMessage(content, selectedLocationChannel.channel, meshService.myPeerID, state.getNicknameValue()) } else { // Send public/channel message via mesh val message = BitchatMessage( sender = state.getNicknameValue() ?: meshService.myPeerID, content = content, timestamp = Date(), isRelay = false, senderPeerID = meshService.myPeerID, mentions = if (mentions.isNotEmpty()) mentions else null, channel = currentChannelValue ) if (currentChannelValue != null) { channelManager.addChannelMessage(currentChannelValue, message, meshService.myPeerID) // Check if encrypted channel if (channelManager.hasChannelKey(currentChannelValue)) { channelManager.sendEncryptedChannelMessage( content, mentions, currentChannelValue, state.getNicknameValue(), meshService.myPeerID, onEncryptedPayload = { encryptedData -> // This would need proper mesh service integration meshService.sendMessage(content, mentions, currentChannelValue) }, onFallback = { meshService.sendMessage(content, mentions, currentChannelValue) } ) } else { meshService.sendMessage(content, mentions, currentChannelValue) } } else { messageManager.addMessage(message) meshService.sendMessage(content, mentions, null) } } } } // MARK: - Utility Functions fun getPeerIDForNickname(nickname: String): String? { return meshService.getPeerNicknames().entries.find { it.value == nickname }?.key } fun toggleFavorite(peerID: String) { Log.d("ChatViewModel", "toggleFavorite called for peerID: $peerID") privateChatManager.toggleFavorite(peerID) // Persist relationship in FavoritesPersistenceService try { var noiseKey: ByteArray? = null var nickname: String = meshService.getPeerNicknames()[peerID] ?: peerID // Case 1: Live mesh peer with known info val peerInfo = meshService.getPeerInfo(peerID) if (peerInfo?.noisePublicKey != null) { noiseKey = peerInfo.noisePublicKey nickname = peerInfo.nickname } else { // Case 2: Offline favorite entry using 64-hex noise public key as peerID if (peerID.length == 64 && peerID.matches(Regex("^[0-9a-fA-F]+$"))) { try { noiseKey = peerID.chunked(2).map { it.toInt(16).toByte() }.toByteArray() // Prefer nickname from favorites store if available val rel = com.bitchat.android.favorites.FavoritesPersistenceService.shared.getFavoriteStatus(noiseKey!!) if (rel != null) nickname = rel.peerNickname } catch (_: Exception) { } } } if (noiseKey != null) { // Determine current favorite state from DataManager using fingerprint val identityManager = com.bitchat.android.identity.SecureIdentityStateManager(getApplication()) val fingerprint = identityManager.generateFingerprint(noiseKey!!) val isNowFavorite = dataManager.favoritePeers.contains(fingerprint) com.bitchat.android.favorites.FavoritesPersistenceService.shared.updateFavoriteStatus( noisePublicKey = noiseKey!!, nickname = nickname, isFavorite = isNowFavorite ) // Send favorite notification via mesh or Nostr with our npub if available try { val myNostr = com.bitchat.android.nostr.NostrIdentityBridge.getCurrentNostrIdentity(getApplication()) val announcementContent = if (isNowFavorite) "[FAVORITED]:${myNostr?.npub ?: ""}" else "[UNFAVORITED]:${myNostr?.npub ?: ""}" // Prefer mesh if session established, else try Nostr if (meshService.hasEstablishedSession(peerID)) { // Reuse existing private message path for notifications meshService.sendPrivateMessage( announcementContent, peerID, nickname, java.util.UUID.randomUUID().toString() ) } else { val nostrTransport = com.bitchat.android.nostr.NostrTransport.getInstance(getApplication()) nostrTransport.senderPeerID = meshService.myPeerID nostrTransport.sendFavoriteNotification(peerID, isNowFavorite) } } catch (_: Exception) { } } } catch (_: Exception) { } // Log current state after toggle logCurrentFavoriteState() } private fun logCurrentFavoriteState() { Log.i("ChatViewModel", "=== CURRENT FAVORITE STATE ===") Log.i("ChatViewModel", "StateFlow favorite peers: ${favoritePeers.value}") Log.i("ChatViewModel", "DataManager favorite peers: ${dataManager.favoritePeers}") Log.i("ChatViewModel", "Peer fingerprints: ${privateChatManager.getAllPeerFingerprints()}") Log.i("ChatViewModel", "==============================") } /** * Initialize session state monitoring for reactive UI updates */ private fun initializeSessionStateMonitoring() { viewModelScope.launch { while (true) { delay(1000) // Check session states every second updateReactiveStates() } } } // Location notes subscription management moved to LocationNotesViewModelExtensions.kt /** * Update reactive states for all connected peers (session states, fingerprints, nicknames, RSSI) */ private fun updateReactiveStates() { val currentPeers = state.getConnectedPeersValue() // Update session states val prevStates = state.getPeerSessionStatesValue() val sessionStates = currentPeers.associateWith { peerID -> meshService.getSessionState(peerID).toString() } state.setPeerSessionStates(sessionStates) // Detect new established sessions and flush router outbox for them and their noiseHex aliases sessionStates.forEach { (peerID, newState) -> val old = prevStates[peerID] if (old != "established" && newState == "established") { com.bitchat.android.services.MessageRouter .getInstance(getApplication(), meshService) .onSessionEstablished(peerID) } } // Update fingerprint mappings from centralized manager val fingerprints = privateChatManager.getAllPeerFingerprints() state.setPeerFingerprints(fingerprints) fingerprints.forEach { (peerID, fingerprint) -> identityManager.cachePeerFingerprint(peerID, fingerprint) val info = try { meshService.getPeerInfo(peerID) } catch (_: Exception) { null } val noiseKeyHex = info?.noisePublicKey?.hexEncodedString() if (noiseKeyHex != null) { identityManager.cachePeerNoiseKey(peerID, noiseKeyHex) identityManager.cacheNoiseFingerprint(noiseKeyHex, fingerprint) } info?.nickname?.takeIf { it.isNotBlank() }?.let { nickname -> identityManager.cacheFingerprintNickname(fingerprint, nickname) } } val nicknames = meshService.getPeerNicknames() state.setPeerNicknames(nicknames) val rssiValues = meshService.getPeerRSSI() state.setPeerRSSI(rssiValues) // Update directness per peer (driven by PeerManager state) try { val directMap = state.getConnectedPeersValue().associateWith { pid -> meshService.getPeerInfo(pid)?.isDirectConnection == true } state.setPeerDirect(directMap) } catch (_: Exception) { } // Flush any pending QR verification once a Noise session is established currentPeers.forEach { peerID -> if (meshService.getSessionState(peerID) is NoiseSession.NoiseSessionState.Established) { verificationHandler.sendPendingVerificationIfNeeded(peerID) } } } // MARK: - QR Verification fun isPeerVerified(peerID: String, verifiedFingerprints: Set): Boolean { if (peerID.startsWith("nostr_") || peerID.startsWith("nostr:")) return false val fingerprint = verificationHandler.getPeerFingerprintForDisplay(peerID) return fingerprint != null && verifiedFingerprints.contains(fingerprint) } fun isNoisePublicKeyVerified(noisePublicKey: ByteArray, verifiedFingerprints: Set): Boolean { val fingerprint = verificationHandler.fingerprintFromNoiseBytes(noisePublicKey) return verifiedFingerprints.contains(fingerprint) } fun unverifyFingerprint(peerID: String) { verificationHandler.unverifyFingerprint(peerID) } fun beginQRVerification(qr: VerificationService.VerificationQR): Boolean { return verificationHandler.beginQRVerification(qr) } // MARK: - Debug and Troubleshooting fun getDebugStatus(): String { return meshService.getDebugStatus() } fun setCurrentPrivateChatPeer(peerID: String?) { notificationManager.setCurrentPrivateChatPeer(peerID) } fun setCurrentGeohash(geohash: String?) { notificationManager.setCurrentGeohash(geohash) } fun clearNotificationsForSender(peerID: String) { notificationManager.clearNotificationsForSender(peerID) } fun clearNotificationsForGeohash(geohash: String) { notificationManager.clearNotificationsForGeohash(geohash) } fun clearMeshMentionNotifications() { notificationManager.clearMeshMentionNotifications() } private var reopenSidebarAfterVerification = false fun showVerificationSheet(fromSidebar: Boolean = false) { if (fromSidebar) { reopenSidebarAfterVerification = true } state.setShowVerificationSheet(true) } fun hideVerificationSheet() { state.setShowVerificationSheet(false) if (reopenSidebarAfterVerification) { reopenSidebarAfterVerification = false state.setShowSidebar(true) } } fun showSecurityVerificationSheet() { state.setShowSecurityVerificationSheet(true) } fun hideSecurityVerificationSheet() { state.setShowSecurityVerificationSheet(false) } fun getPeerFingerprintForDisplay(peerID: String): String? { return verificationHandler.getPeerFingerprintForDisplay(peerID) } fun getMyFingerprint(): String { return verificationHandler.getMyFingerprint() } fun resolvePeerDisplayNameForFingerprint(peerID: String): String { return verificationHandler.resolvePeerDisplayNameForFingerprint(peerID) } fun verifyFingerprintValue(fingerprint: String) { verificationHandler.verifyFingerprintValue(fingerprint) } fun unverifyFingerprintValue(fingerprint: String) { verificationHandler.unverifyFingerprintValue(fingerprint) } // MARK: - Command Autocomplete (delegated) fun updateCommandSuggestions(input: String) { commandProcessor.updateCommandSuggestions(input) } fun selectCommandSuggestion(suggestion: CommandSuggestion): String { return commandProcessor.selectCommandSuggestion(suggestion) } // MARK: - Mention Autocomplete fun updateMentionSuggestions(input: String) { commandProcessor.updateMentionSuggestions(input, meshService, this) } fun selectMentionSuggestion(nickname: String, currentText: String): String { return commandProcessor.selectMentionSuggestion(nickname, currentText) } // MARK: - BluetoothMeshDelegate Implementation (delegated) override fun didReceiveMessage(message: BitchatMessage) { meshDelegateHandler.didReceiveMessage(message) } override fun didUpdatePeerList(peers: List) { meshDelegateHandler.didUpdatePeerList(peers) } override fun didReceiveChannelLeave(channel: String, fromPeer: String) { meshDelegateHandler.didReceiveChannelLeave(channel, fromPeer) } override fun didReceiveDeliveryAck(messageID: String, recipientPeerID: String) { meshDelegateHandler.didReceiveDeliveryAck(messageID, recipientPeerID) } override fun didReceiveReadReceipt(messageID: String, recipientPeerID: String) { meshDelegateHandler.didReceiveReadReceipt(messageID, recipientPeerID) } override fun didReceiveVerifyChallenge(peerID: String, payload: ByteArray, timestampMs: Long) { verificationHandler.didReceiveVerifyChallenge(peerID, payload) } override fun didReceiveVerifyResponse(peerID: String, payload: ByteArray, timestampMs: Long) { verificationHandler.didReceiveVerifyResponse(peerID, payload) } override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? { return meshDelegateHandler.decryptChannelMessage(encryptedContent, channel) } override fun getNickname(): String? { return meshDelegateHandler.getNickname() } override fun isFavorite(peerID: String): Boolean { return meshDelegateHandler.isFavorite(peerID) } // registerPeerPublicKey REMOVED - fingerprints now handled centrally in PeerManager // MARK: - Emergency Clear fun panicClearAllData() { Log.w(TAG, "🚨 PANIC MODE ACTIVATED - Clearing all sensitive data") // Clear all UI managers messageManager.clearAllMessages() channelManager.clearAllChannels() privateChatManager.clearAllPrivateChats() dataManager.clearAllData() // Clear all mesh service data clearAllMeshServiceData() // Clear all cryptographic data clearAllCryptographicData() // Clear all notifications notificationManager.clearAllNotifications() // Clear Nostr/geohash state, keys, connections, bookmarks, and reinitialize from scratch try { // Clear geohash bookmarks too (panic should remove everything) try { val store = com.bitchat.android.geohash.GeohashBookmarksStore.getInstance(getApplication()) store.clearAll() } catch (_: Exception) { } geohashViewModel.panicReset() } catch (e: Exception) { Log.e(TAG, "Failed to reset Nostr/geohash: ${e.message}") } // Reset nickname val newNickname = "anon${Random.nextInt(1000, 9999)}" state.setNickname(newNickname) dataManager.saveNickname(newNickname) Log.w(TAG, "🚨 PANIC MODE COMPLETED - All sensitive data cleared") // Note: Mesh service restart is now handled by MainActivity // This method now only clears data, not mesh service lifecycle } /** * Clear all mesh service related data */ private fun clearAllMeshServiceData() { try { // Request mesh service to clear all its internal data meshService.clearAllInternalData() Log.d(TAG, "✅ Cleared all mesh service data") } catch (e: Exception) { Log.e(TAG, "❌ Error clearing mesh service data: ${e.message}") } } /** * Clear all cryptographic data including persistent identity */ private fun clearAllCryptographicData() { try { // Clear encryption service persistent identity (Ed25519 signing keys) meshService.clearAllEncryptionData() // Clear secure identity state (if used) try { val identityManager = SecureIdentityStateManager(getApplication()) identityManager.clearIdentityData() // Also clear secure values used by FavoritesPersistenceService (favorites + peerID index) try { identityManager.clearSecureValues("favorite_relationships", "favorite_peerid_index") } catch (_: Exception) { } Log.d(TAG, "✅ Cleared secure identity state and secure favorites store") } catch (e: Exception) { Log.d(TAG, "SecureIdentityStateManager not available or already cleared: ${e.message}") } // Clear FavoritesPersistenceService persistent relationships try { FavoritesPersistenceService.shared.clearAllFavorites() Log.d(TAG, "✅ Cleared FavoritesPersistenceService relationships") } catch (_: Exception) { } Log.d(TAG, "✅ Cleared all cryptographic data") } catch (e: Exception) { Log.e(TAG, "❌ Error clearing cryptographic data: ${e.message}") } } /** * Get participant count for a specific geohash (5-minute activity window) */ fun geohashParticipantCount(geohash: String): Int { return geohashViewModel.geohashParticipantCount(geohash) } /** * Begin sampling multiple geohashes for participant activity */ fun beginGeohashSampling(geohashes: List) { geohashViewModel.beginGeohashSampling(geohashes) } /** * End geohash sampling */ fun endGeohashSampling() { // No-op in refactored architecture; sampling subscriptions are short-lived } /** * Check if a geohash person is teleported (iOS-compatible) */ fun isPersonTeleported(pubkeyHex: String): Boolean { return geohashViewModel.isPersonTeleported(pubkeyHex) } /** * Start geohash DM with pubkey hex (iOS-compatible) */ fun startGeohashDM(pubkeyHex: String) { geohashViewModel.startGeohashDM(pubkeyHex) { convKey -> startPrivateChat(convKey) } } fun selectLocationChannel(channel: com.bitchat.android.geohash.ChannelID) { geohashViewModel.selectLocationChannel(channel) } /** * Block a user in geohash channels by their nickname */ fun blockUserInGeohash(targetNickname: String) { geohashViewModel.blockUserInGeohash(targetNickname) } // MARK: - Navigation Management fun showAppInfo() { state.setShowAppInfo(true) } fun hideAppInfo() { state.setShowAppInfo(false) } fun showSidebar() { state.setShowSidebar(true) } fun hideSidebar() { state.setShowSidebar(false) } /** * Handle Android back navigation * Returns true if the back press was handled, false if it should be passed to the system */ fun handleBackPressed(): Boolean { return when { // Close app info dialog state.getShowAppInfoValue() -> { hideAppInfo() true } // Close sidebar state.getShowSidebarValue() -> { hideSidebar() true } // Close password dialog state.getShowPasswordPromptValue() -> { state.setShowPasswordPrompt(false) state.setPasswordPromptChannel(null) true } // Exit private chat state.getSelectedPrivateChatPeerValue() != null -> { endPrivateChat() true } // Exit channel view state.getCurrentChannelValue() != null -> { switchToChannel(null) true } // No special navigation state - let system handle (usually exits app) else -> false } } // MARK: - iOS-Compatible Color System /** * Get consistent color for a mesh peer by ID (iOS-compatible) */ fun colorForMeshPeer(peerID: String, isDark: Boolean): androidx.compose.ui.graphics.Color { // Try to get stable Noise key, fallback to peer ID val seed = "noise:${peerID.lowercase()}" return colorForPeerSeed(seed, isDark).copy() } /** * Get consistent color for a Nostr pubkey (iOS-compatible) */ fun colorForNostrPubkey(pubkeyHex: String, isDark: Boolean): androidx.compose.ui.graphics.Color { return geohashViewModel.colorForNostrPubkey(pubkeyHex, isDark) } }