Files
bitchat-android/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt
T
callebtcandGitHub f47819a31e Debug button (#377)
* feat(debug): add DebugSettingsManager + DebugSettingsSheet scaffold and AboutSheet entry; groundwork for verbose logging, GATT controls, relay stats, device/scan views

* fix(debug): wire debug sheet launch via onShowDebug in AboutSheet from ChatScreen; add public start/stop server/client methods}

* fix(about): remove misplaced debug item block inside PoW section; keep debug launcher managed by ChatScreen only

* feat(debug): wire DebugSettingsSheet role switches to explicit startServer/stopServer and startClient/stopClient; expose role controls + disconnectAll on BluetoothConnectionManager; fix syntax error; build passes

* feat(debug): add connect/disconnect helpers; wire DebugSettingsSheet connect/disconnect actions to BluetoothConnectionManager

* feat(debug): add debug settings button at bottom of AboutSheet; wire onShowDebug callback to open DebugSettingsSheet from ChatScreen

* feat(debug): wire verbose logging into chat view via DebugSettingsManager; add rolling relay stats; push connected devices and scan results; ensure GATT role stop closes connections; log incoming packets and relay events; add UI polling for devices; build passes

* chore(debug-branch): remove unrelated files that were mistakenly added in first commit; keep only intended debug settings changes

* fix(chat): prevent mesh→geohash leak

Root cause:
- DebugSettingsManager→chat bridge appended system logs to the global timeline regardless of active channel
- MeshDelegateHandler added public mesh messages to UI unconditionally

Fixes:
- Only inject debug system messages when selected location channel is Mesh
- Only add public mesh messages to UI when Mesh is selected (still send notifications)

Build: ./gradlew assembleDebug
}

* fix(timeline): restore message persistence across channel switches

Root cause:
- NostrGeohashService.switchLocationChannel() called messageManager.clearMessages()
  which wiped ALL messages (mesh + debug + geohash) from main timeline
- Geohash events were adding to main timeline, causing cross-contamination

Fixes:
- Remove messageManager.clearMessages() from channel switching
- ChatScreen displayMessages now routes to separate storage:
  - Mesh: messages (main timeline, includes debug logs when Mesh selected)
  - Geohash: viewModel.getGeohashMessages(geohash) from separate history
  - Private: privateChats[peerID]
  - Channels: channelMessages[channel]
- Geohash events no longer add to main timeline, only to geohash history
- Mesh messages always stored to preserve history when switching away

Result: Each chat type has persistent separate storage, no message loss

* Debug/relay logs: use MessageType names; include device route in verbose packet logs.

* Verbose device-peer assignment and connection/disconnection logs with peerId, deviceId, nickname; packet relay log uses MessageType names.}

* Debug logs API: include nickname and deviceId for incoming/relay; update callers.}

* Fix compile: import MessageType; correct debug manager API usage and remove stray insertion; build.}

* Respect relay toggle; helper to log relay with deviceId.}

* PacketRelayManager: add relay toggle and unified logging helper; clean file header comments; build fixes.}

* changes

* Geohash local echo: do not add to mesh timeline; rely on geohash history and ChatScreen display for location channels.

* persisting

* persist debug settings

* gitignore

* gatt server / client controls

* debugger

* max connection settings and graph

* more graph

* better logging

* refactor logging
2025-09-05 15:40:39 +02:00

768 lines
31 KiB
Kotlin

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.LiveData
import androidx.lifecycle.viewModelScope
import com.bitchat.android.mesh.BluetoothMeshDelegate
import com.bitchat.android.mesh.BluetoothMeshService
import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.protocol.BitchatPacket
import com.bitchat.android.nostr.NostrGeohashService
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
/**
* 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"
}
// State management
private val state = ChatState()
// Specialized managers
private val dataManager = DataManager(application.applicationContext)
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()
)
// 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 }
)
// Nostr and Geohash service - initialize singleton
private val nostrGeohashService = NostrGeohashService.initialize(
application = application,
state = state,
messageManager = messageManager,
privateChatManager = privateChatManager,
meshDelegateHandler = meshDelegateHandler,
coroutineScope = viewModelScope,
dataManager = dataManager,
notificationManager = notificationManager
)
// Expose state through LiveData (maintaining the same interface)
val messages: LiveData<List<BitchatMessage>> = state.messages
val connectedPeers: LiveData<List<String>> = state.connectedPeers
val nickname: LiveData<String> = state.nickname
val isConnected: LiveData<Boolean> = state.isConnected
val privateChats: LiveData<Map<String, List<BitchatMessage>>> = state.privateChats
val selectedPrivateChatPeer: LiveData<String?> = state.selectedPrivateChatPeer
val unreadPrivateMessages: LiveData<Set<String>> = state.unreadPrivateMessages
val joinedChannels: LiveData<Set<String>> = state.joinedChannels
val currentChannel: LiveData<String?> = state.currentChannel
val channelMessages: LiveData<Map<String, List<BitchatMessage>>> = state.channelMessages
val unreadChannelMessages: LiveData<Map<String, Int>> = state.unreadChannelMessages
val passwordProtectedChannels: LiveData<Set<String>> = state.passwordProtectedChannels
val showPasswordPrompt: LiveData<Boolean> = state.showPasswordPrompt
val passwordPromptChannel: LiveData<String?> = state.passwordPromptChannel
val showSidebar: LiveData<Boolean> = state.showSidebar
val hasUnreadChannels = state.hasUnreadChannels
val hasUnreadPrivateMessages = state.hasUnreadPrivateMessages
val showCommandSuggestions: LiveData<Boolean> = state.showCommandSuggestions
val commandSuggestions: LiveData<List<CommandSuggestion>> = state.commandSuggestions
val showMentionSuggestions: LiveData<Boolean> = state.showMentionSuggestions
val mentionSuggestions: LiveData<List<String>> = state.mentionSuggestions
val favoritePeers: LiveData<Set<String>> = state.favoritePeers
val peerSessionStates: LiveData<Map<String, String>> = state.peerSessionStates
val peerFingerprints: LiveData<Map<String, String>> = state.peerFingerprints
val peerNicknames: LiveData<Map<String, String>> = state.peerNicknames
val peerRSSI: LiveData<Map<String, Int>> = state.peerRSSI
val peerDirect: LiveData<Map<String, Boolean>> = state.peerDirect
val showAppInfo: LiveData<Boolean> = state.showAppInfo
val selectedLocationChannel: LiveData<com.bitchat.android.geohash.ChannelID?> = state.selectedLocationChannel
val isTeleported: LiveData<Boolean> = state.isTeleported
val geohashPeople: LiveData<List<GeoPerson>> = state.geohashPeople
val teleportedGeo: LiveData<Set<String>> = state.teleportedGeo
val geohashParticipantCounts: LiveData<Map<String, Int>> = state.geohashParticipantCounts
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.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 location channel state
nostrGeohashService.initializeLocationChannelState()
// Initialize favorites persistence service
com.bitchat.android.favorites.FavoritesPersistenceService.initialize(getApplication())
// Initialize Nostr integration
nostrGeohashService.initializeNostrIntegration()
// 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
// Show welcome message if no peers after delay
viewModelScope.launch {
delay(10000)
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)
// 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: - Message Sending
fun sendMessage(content: String) {
if (content.isEmpty()) return
// Check for commands
if (content.startsWith("/")) {
commandProcessor.processCommand(content, meshService, meshService.myPeerID, { messageContent, mentions, channel ->
meshService.sendMessage(messageContent, mentions, channel)
}, this)
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 -> nostrGeohashService.getNostrKeyMapping()[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
nostrGeohashService.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 when we have Noise key
try {
val peerInfo = meshService.getPeerInfo(peerID)
val noiseKey = peerInfo?.noisePublicKey
val nickname = peerInfo?.nickname ?: (meshService.getPeerNicknames()[peerID] ?: peerID)
if (noiseKey != null) {
val isNowFavorite = dataManager.favoritePeers.contains(
com.bitchat.android.mesh.PeerFingerprintManager.getInstance().getFingerprintForPeer(peerID) ?: ""
)
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", "LiveData 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()
}
}
}
/**
* 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)
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) { }
}
// 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 setCurrentGeohash(geohash: String?) {
// Update notification manager with current geohash for notification logic
notificationManager.setCurrentGeohash(geohash)
}
fun clearNotificationsForSender(peerID: String) {
// Clear notifications when user opens a chat
notificationManager.clearNotificationsForSender(peerID)
}
fun clearNotificationsForGeohash(geohash: String) {
// Clear notifications when user opens a geohash chat
notificationManager.clearNotificationsForGeohash(geohash)
}
/**
* Clear mesh mention notifications when user opens mesh chat
*/
fun clearMeshMentionNotifications() {
notificationManager.clearMeshMentionNotifications()
}
// 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<String>) {
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 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, and reinitialize from scratch
try {
nostrGeohashService.panicResetNostrAndGeohash()
} 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 = com.bitchat.android.identity.SecureIdentityStateManager(getApplication())
identityManager.clearIdentityData()
Log.d(TAG, "✅ Cleared secure identity state")
} catch (e: Exception) {
Log.d(TAG, "SecureIdentityStateManager not available or already cleared: ${e.message}")
}
Log.d(TAG, "✅ Cleared all cryptographic data")
} catch (e: Exception) {
Log.e(TAG, "❌ Error clearing cryptographic data: ${e.message}")
}
}
/**
* Get messages for a specific geohash timeline
*/
fun getGeohashMessages(geohash: String): List<BitchatMessage> {
return nostrGeohashService.getGeohashMessages(geohash)
}
/**
* Get participant count for a specific geohash (5-minute activity window)
*/
fun geohashParticipantCount(geohash: String): Int {
return nostrGeohashService.geohashParticipantCount(geohash)
}
/**
* Begin sampling multiple geohashes for participant activity
*/
fun beginGeohashSampling(geohashes: List<String>) {
nostrGeohashService.beginGeohashSampling(geohashes)
}
/**
* End geohash sampling
*/
fun endGeohashSampling() {
nostrGeohashService.endGeohashSampling()
}
/**
* Check if a geohash person is teleported (iOS-compatible)
*/
fun isPersonTeleported(pubkeyHex: String): Boolean {
return nostrGeohashService.isPersonTeleported(pubkeyHex)
}
/**
* Start geohash DM with pubkey hex (iOS-compatible)
*/
fun startGeohashDM(pubkeyHex: String) {
nostrGeohashService.startGeohashDM(pubkeyHex) { convKey ->
startPrivateChat(convKey)
}
}
fun selectLocationChannel(channel: com.bitchat.android.geohash.ChannelID) {
nostrGeohashService.selectLocationChannel(channel)
}
/**
* Block a user in geohash channels by their nickname
*/
fun blockUserInGeohash(targetNickname: String) {
nostrGeohashService.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 nostrGeohashService.colorForNostrPubkey(pubkeyHex, isDark)
}
}