package com.bitchat.android.ui import android.app.Application import android.content.Context import android.util.Log import androidx.compose.ui.focus.FocusManager import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.viewModelScope import com.bitchat.android.mesh.BluetoothMeshService import com.bitchat.android.mesh.BluetoothMeshDelegate import com.bitchat.android.model.BitchatMessage import com.bitchat.android.model.DeliveryAck import com.bitchat.android.model.ReadReceipt import com.bitchat.android.model.RoutedPacket import com.bitchat.android.ui.payment.PaymentManager import com.bitchat.android.ui.payment.PaymentStatus import com.bitchat.android.wallet.viewmodel.WalletViewModel import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.delay import java.util.* import kotlin.random.Random /** * ViewModel for managing the chat functionality * Refactored to use composition over inheritance for better code organization */ class ChatViewModel( application: Application, val meshService: BluetoothMeshService ) : AndroidViewModel(application), BluetoothMeshDelegate { private val context: Context = application.applicationContext // Focus manager for keyboard control private var focusManager: FocusManager? = null // State management private val state = ChatState() // Specialized managers private val dataManager = DataManager(context) private val messageManager = MessageManager(state) private val channelManager = ChannelManager(state, messageManager, dataManager, viewModelScope) val privateChatManager = PrivateChatManager(state, messageManager, dataManager) private val commandProcessor = CommandProcessor(state, messageManager, channelManager, privateChatManager) private val notificationManager = NotificationManager(application.applicationContext) // Payment manager (lazily initialized with wallet ViewModel) private var paymentManager: PaymentManager? = null // Initialize payment manager with wallet ViewModel fun initializePaymentManager(walletViewModel: WalletViewModel) { paymentManager = PaymentManager(viewModelScope, walletViewModel) } // Initialize focus manager for keyboard control fun setFocusManager(focusManager: FocusManager) { this.focusManager = focusManager } // Hide keyboard fun hideKeyboard() { focusManager?.clearFocus() } // Delegate handler for mesh callbacks private val meshDelegateHandler = MeshDelegateHandler( state = state, messageManager = messageManager, channelManager = channelManager, privateChatManager = privateChatManager, notificationManager = notificationManager, coroutineScope = viewModelScope, onHapticFeedback = { ChatViewModelUtils.triggerHapticFeedback(context) }, getMyPeerID = { meshService.myPeerID } ) // Expose state through LiveData (maintaining the same interface) val messages: LiveData> = state.messages val connectedPeers: LiveData> = state.connectedPeers val nickname: LiveData = state.nickname val isConnected: LiveData = state.isConnected val privateChats: LiveData>> = state.privateChats val selectedPrivateChatPeer: LiveData = state.selectedPrivateChatPeer val unreadPrivateMessages: LiveData> = state.unreadPrivateMessages val joinedChannels: LiveData> = state.joinedChannels val currentChannel: LiveData = state.currentChannel val channelMessages: LiveData>> = state.channelMessages val unreadChannelMessages: LiveData> = state.unreadChannelMessages val passwordProtectedChannels: LiveData> = state.passwordProtectedChannels val showPasswordPrompt: LiveData = state.showPasswordPrompt val passwordPromptChannel: LiveData = state.passwordPromptChannel val showSidebar: LiveData = state.showSidebar val hasUnreadChannels = state.hasUnreadChannels val hasUnreadPrivateMessages = state.hasUnreadPrivateMessages val showCommandSuggestions: LiveData = state.showCommandSuggestions val commandSuggestions: LiveData> = state.commandSuggestions val favoritePeers: LiveData> = state.favoritePeers val showAppInfo: LiveData = state.showAppInfo init { // Note: Mesh service delegate is now set by MainActivity loadAndInitialize() } 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) dataManager.loadBlockedUsers() // Log all favorites at startup dataManager.logAllFavorites() logCurrentFavoriteState() // Note: Mesh service is now started by MainActivity // Show welcome message if no peers after delay viewModelScope.launch { delay(3000) if (state.getConnectedPeersValue().isEmpty() && state.getMessagesValue().isEmpty()) { val welcomeMessage = BitchatMessage( sender = "system", content = "get people around you to download bitchat…and chat with them here!", timestamp = Date(), isRelay = false ) messageManager.addMessage(welcomeMessage) } } } 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() } // 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) { 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) } } fun endPrivateChat() { privateChatManager.endPrivateChat() // Notify notification manager that no private chat is active setCurrentPrivateChatPeer(null) } // MARK: - Message Sending fun sendMessage(content: String) { if (content.isEmpty()) return // Check for commands if (content.startsWith("/")) { commandProcessor.processCommand( command = content, meshService = meshService, myPeerID = meshService.myPeerID, onSendMessage = { messageContent, mentions, channel -> meshService.sendMessage(messageContent, mentions, channel) }, onPaymentRequest = { amount -> handlePaymentRequest(amount) } ) return } val mentions = messageManager.parseMentions(content, meshService.getPeerNicknames().values.toSet(), state.getNicknameValue()) val channels = messageManager.parseChannels(content) // Auto-join mentioned channels channels.forEach { channel -> if (!state.getJoinedChannelsValue().contains(channel)) { joinChannel(channel) } } val selectedPeer = state.getSelectedPrivateChatPeerValue() val currentChannelValue = state.getCurrentChannelValue() if (selectedPeer != null) { // Send private message val recipientNickname = meshService.getPeerNicknames()[selectedPeer] privateChatManager.sendPrivateMessage( content, selectedPeer, recipientNickname, state.getNicknameValue(), meshService.myPeerID ) { messageContent, peerID, recipientNicknameParam, messageId -> meshService.sendPrivateMessage(messageContent, peerID, recipientNicknameParam, messageId) } } else { // Send public/channel message 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: - Payment Management /** * Handle payment request from /pay command */ private fun handlePaymentRequest(amount: Long) { paymentManager?.createPayment(amount) { token -> // Send the created token to the current chat sendCashuTokenToChat(token) } } /** * Send a Cashu token to the current chat */ private fun sendCashuTokenToChat(token: String) { val selectedPeer = state.getSelectedPrivateChatPeerValue() val currentChannelValue = state.getCurrentChannelValue() if (selectedPeer != null) { // Send token as private message val recipientNickname = meshService.getPeerNicknames()[selectedPeer] privateChatManager.sendPrivateMessage( token, selectedPeer, recipientNickname, state.getNicknameValue(), meshService.myPeerID ) { messageContent, peerID, recipientNicknameParam, messageId -> meshService.sendPrivateMessage(messageContent, peerID, recipientNicknameParam, messageId) } } else { // Send token as public/channel message val message = BitchatMessage( sender = state.getNicknameValue() ?: meshService.myPeerID, content = token, timestamp = Date(), isRelay = false, senderPeerID = meshService.myPeerID, channel = currentChannelValue ) if (currentChannelValue != null) { channelManager.addChannelMessage(currentChannelValue, message, meshService.myPeerID) meshService.sendMessage(token, emptyList(), currentChannelValue) } else { messageManager.addMessage(message) meshService.sendMessage(token, emptyList(), null) } } } /** * Get payment status for UI */ fun getPaymentStatus(): StateFlow { return paymentManager?.paymentStatus ?: kotlinx.coroutines.flow.MutableStateFlow(null).asStateFlow() } /** * Clear payment status */ fun clearPaymentStatus() { paymentManager?.clearStatus() } // 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) // Log current state after toggle logCurrentFavoriteState() } private fun logCurrentFavoriteState() { Log.i("ChatViewModel", "=== CURRENT FAVORITE STATE ===") Log.i("ChatViewModel", "LiveData favorite peers: ${favoritePeers.value}") Log.i("ChatViewModel", "DataManager favorite peers: ${dataManager.favoritePeers}") Log.i("ChatViewModel", "Peer fingerprints: ${privateChatManager.getAllPeerFingerprints()}") Log.i("ChatViewModel", "==============================") } // MARK: - Debug and Troubleshooting fun getDebugStatus(): String { return meshService.getDebugStatus() } // Note: Mesh service restart is now handled by MainActivity // This function is no longer needed fun setAppBackgroundState(inBackground: Boolean) { // Forward to notification manager for notification logic notificationManager.setAppBackgroundState(inBackground) } fun setCurrentPrivateChatPeer(peerID: String?) { // Update notification manager with current private chat peer notificationManager.setCurrentPrivateChatPeer(peerID) } fun clearNotificationsForSender(peerID: String) { // Clear notifications when user opens a chat notificationManager.clearNotificationsForSender(peerID) } // MARK: - Command Autocomplete (delegated) fun updateCommandSuggestions(input: String) { commandProcessor.updateCommandSuggestions(input) } fun selectCommandSuggestion(suggestion: CommandSuggestion): String { return commandProcessor.selectCommandSuggestion(suggestion) } // MARK: - BluetoothMeshDelegate Implementation (delegated) override fun didReceiveMessage(message: BitchatMessage) { meshDelegateHandler.didReceiveMessage(message) } override fun didConnectToPeer(peerID: String) { meshDelegateHandler.didConnectToPeer(peerID) } override fun didDisconnectFromPeer(peerID: String) { meshDelegateHandler.didDisconnectFromPeer(peerID) } override fun didUpdatePeerList(peers: List) { meshDelegateHandler.didUpdatePeerList(peers) } override fun didReceiveChannelLeave(channel: String, fromPeer: String) { meshDelegateHandler.didReceiveChannelLeave(channel, fromPeer) } override fun didReceiveDeliveryAck(ack: DeliveryAck) { meshDelegateHandler.didReceiveDeliveryAck(ack) } override fun didReceiveReadReceipt(receipt: ReadReceipt) { meshDelegateHandler.didReceiveReadReceipt(receipt) } 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) } override fun registerPeerPublicKey(peerID: String, publicKeyData: ByteArray) { privateChatManager.registerPeerPublicKey(peerID, publicKeyData) } // MARK: - Emergency Clear fun panicClearAllData() { // Clear all managers messageManager.clearAllMessages() channelManager.clearAllChannels() privateChatManager.clearAllPrivateChats() dataManager.clearAllData() // Reset nickname val newNickname = "anon${Random.nextInt(1000, 9999)}" state.setNickname(newNickname) dataManager.saveNickname(newNickname) // Note: Mesh service restart is now handled by MainActivity // This method now only clears data, not mesh service lifecycle } // 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 } } }