diff --git a/app/src/main/java/com/bitchat/android/BitchatApplication.kt b/app/src/main/java/com/bitchat/android/BitchatApplication.kt index b120b216..59cb7ec9 100644 --- a/app/src/main/java/com/bitchat/android/BitchatApplication.kt +++ b/app/src/main/java/com/bitchat/android/BitchatApplication.kt @@ -41,6 +41,12 @@ class BitchatApplication : Application() { // Initialize debug preference manager (persists debug toggles) try { com.bitchat.android.ui.debug.DebugPreferenceManager.init(this) } catch (_: Exception) { } + // Initialize Geohash Registries for persistence + try { + com.bitchat.android.nostr.GeohashAliasRegistry.initialize(this) + com.bitchat.android.nostr.GeohashConversationRegistry.initialize(this) + } catch (_: Exception) { } + // Initialize mesh service preferences try { com.bitchat.android.service.MeshServicePreferences.init(this) } catch (_: Exception) { } diff --git a/app/src/main/java/com/bitchat/android/nostr/GeohashAliasRegistry.kt b/app/src/main/java/com/bitchat/android/nostr/GeohashAliasRegistry.kt index ea2ab0ba..9ead2485 100644 --- a/app/src/main/java/com/bitchat/android/nostr/GeohashAliasRegistry.kt +++ b/app/src/main/java/com/bitchat/android/nostr/GeohashAliasRegistry.kt @@ -1,17 +1,40 @@ package com.bitchat.android.nostr +import android.content.Context +import android.content.SharedPreferences import java.util.concurrent.ConcurrentHashMap /** * GeohashAliasRegistry * - Global, thread-safe registry for alias->Nostr pubkey mappings (e.g., nostr_ -> pubkeyHex) - * - Allows non-UI components (e.g., MessageRouter) to resolve geohash DM aliases without depending on UI ViewModels + * - Persisted to SharedPreferences to survive app restarts. */ object GeohashAliasRegistry { private val map: MutableMap = ConcurrentHashMap() + private const val PREFS_NAME = "geohash_alias_registry" + private var prefs: SharedPreferences? = null + + fun initialize(context: Context) { + if (prefs == null) { + prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + loadFromPrefs() + } + } + + private fun loadFromPrefs() { + prefs?.let { p -> + val allEntries = p.all + for ((key, value) in allEntries) { + if (key is String && value is String) { + map[key] = value + } + } + } + } fun put(alias: String, pubkeyHex: String) { map[alias] = pubkeyHex + prefs?.edit()?.putString(alias, pubkeyHex)?.apply() } fun get(alias: String): String? = map[alias] @@ -20,5 +43,8 @@ object GeohashAliasRegistry { fun snapshot(): Map = HashMap(map) - fun clear() { map.clear() } + fun clear() { + map.clear() + prefs?.edit()?.clear()?.apply() + } } diff --git a/app/src/main/java/com/bitchat/android/nostr/GeohashConversationRegistry.kt b/app/src/main/java/com/bitchat/android/nostr/GeohashConversationRegistry.kt index 0ae2a6db..68c1b29e 100644 --- a/app/src/main/java/com/bitchat/android/nostr/GeohashConversationRegistry.kt +++ b/app/src/main/java/com/bitchat/android/nostr/GeohashConversationRegistry.kt @@ -1,22 +1,51 @@ package com.bitchat.android.nostr +import android.content.Context +import android.content.SharedPreferences import java.util.concurrent.ConcurrentHashMap /** * GeohashConversationRegistry * - Global, thread-safe registry of conversationKey (e.g., "nostr_") -> source geohash * - Enables routing geohash DMs from anywhere by providing the correct geohash identity + * - Persisted to SharedPreferences to survive app restarts. */ object GeohashConversationRegistry { private val map = ConcurrentHashMap() + private const val PREFS_NAME = "geohash_conversation_registry" + private var prefs: SharedPreferences? = null + + fun initialize(context: Context) { + if (prefs == null) { + prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + loadFromPrefs() + } + } + + private fun loadFromPrefs() { + prefs?.let { p -> + val allEntries = p.all + for ((key, value) in allEntries) { + if (key is String && value is String) { + map[key] = value + } + } + } + } fun set(convKey: String, geohash: String) { - if (geohash.isNotEmpty()) map[convKey] = geohash + if (geohash.isNotEmpty()) { + map[convKey] = geohash + prefs?.edit()?.putString(convKey, geohash)?.apply() + } } fun get(convKey: String): String? = map[convKey] fun snapshot(): Map = map.toMap() - fun clear() = map.clear() + fun clear() { + map.clear() + prefs?.edit()?.clear()?.apply() + } } diff --git a/app/src/main/java/com/bitchat/android/ui/GeohashViewModel.kt b/app/src/main/java/com/bitchat/android/ui/GeohashViewModel.kt index 52e00edf..4d66e306 100644 --- a/app/src/main/java/com/bitchat/android/ui/GeohashViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/GeohashViewModel.kt @@ -12,6 +12,8 @@ import com.bitchat.android.nostr.NostrProtocol import com.bitchat.android.nostr.NostrRelayManager import com.bitchat.android.nostr.NostrSubscriptionManager import com.bitchat.android.nostr.PoWPreferenceManager +import com.bitchat.android.nostr.GeohashAliasRegistry +import com.bitchat.android.nostr.GeohashConversationRegistry import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.StateFlow @@ -93,6 +95,8 @@ class GeohashViewModel( fun panicReset() { repo.clearAll() + GeohashAliasRegistry.clear() + GeohashConversationRegistry.clear() subscriptionManager.disconnect() currentGeohashSubId = null currentDmSubId = null @@ -168,7 +172,7 @@ class GeohashViewModel( val gh = (current as? com.bitchat.android.geohash.ChannelID.Location)?.channel?.geohash if (!gh.isNullOrEmpty()) { repo.setConversationGeohash(convKey, gh) - com.bitchat.android.nostr.GeohashConversationRegistry.set(convKey, gh) + GeohashConversationRegistry.set(convKey, gh) } onStartPrivateChat(convKey) Log.d(TAG, "🗨️ Started geohash DM with ${pubkeyHex} -> ${convKey} (geohash=${gh})") @@ -260,7 +264,7 @@ class GeohashViewModel( handler = { event -> dmHandler.onGiftWrap(event, geohash, dmIdentity) } ) // Also register alias in global registry for routing convenience - com.bitchat.android.nostr.GeohashAliasRegistry.put("nostr_${dmIdentity.publicKeyHex.take(16)}", dmIdentity.publicKeyHex) + GeohashAliasRegistry.put("nostr_${dmIdentity.publicKeyHex.take(16)}", dmIdentity.publicKeyHex) } } null -> { diff --git a/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt b/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt index 098a83d7..53cc22bd 100644 --- a/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt +++ b/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt @@ -298,6 +298,16 @@ class PrivateChatManager( if (!isPeerBlocked(senderPeerID)) { // Ensure chat exists messageManager.initializePrivateChat(senderPeerID) + + // Exception: Nostr messages (nostr_ prefix) originate in Kotlin layer and MUST be added here. + if (senderPeerID.startsWith("nostr_")) { + if (suppressUnread) { + messageManager.addPrivateMessageNoUnread(senderPeerID, message) + } else { + messageManager.addPrivateMessage(senderPeerID, message) + } + } + // Track as unread for read receipt purposes if not focused if (!suppressUnread && state.getSelectedPrivateChatPeerValue() != senderPeerID) { val unreadList = unreadReceivedMessages.getOrPut(senderPeerID) { mutableListOf() }