mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 13:05:19 +00:00
Nostr geohash (#276)
* first nostr build * add test file * internet access * fix relay manager * fix serialization * demo service - remove later * fix nostr * event dedupe * dedupe * ui wip * can send messages * subscription works * works * favs * works * delete chat on change * fix mentions * remove autojoin channels * styling * adjust colors * ui changes * live updates working * use local timestamp * message history in background * robust * fixes * nicknames refresh optimization * nostr service * refactor nostr * style * geohash works * centralize colors * refactoring * disable DMs for now: click on peer nickname doesnt open chat list in geohash mode * use local time * less logging * robustness * scroll nickname * adjust some text
This commit is contained in:
@@ -0,0 +1,320 @@
|
||||
package com.bitchat.android.favorites
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.bitchat.android.identity.SecureIdentityStateManager
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Bridging Noise and Nostr favorites
|
||||
* Direct port from iOS FavoritesPersistenceService.swift
|
||||
*/
|
||||
data class FavoriteRelationship(
|
||||
val peerNoisePublicKey: ByteArray, // Noise static public key (32 bytes)
|
||||
val peerNostrPublicKey: String?, // npub bech32 string
|
||||
val peerNickname: String,
|
||||
val isFavorite: Boolean, // We favorited them
|
||||
val theyFavoritedUs: Boolean, // They favorited us
|
||||
val favoritedAt: Date,
|
||||
val lastUpdated: Date
|
||||
) {
|
||||
val isMutual: Boolean get() = isFavorite && theyFavoritedUs
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as FavoriteRelationship
|
||||
|
||||
if (!peerNoisePublicKey.contentEquals(other.peerNoisePublicKey)) return false
|
||||
if (peerNostrPublicKey != other.peerNostrPublicKey) return false
|
||||
if (peerNickname != other.peerNickname) return false
|
||||
if (isFavorite != other.isFavorite) return false
|
||||
if (theyFavoritedUs != other.theyFavoritedUs) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = peerNoisePublicKey.contentHashCode()
|
||||
result = 31 * result + (peerNostrPublicKey?.hashCode() ?: 0)
|
||||
result = 31 * result + peerNickname.hashCode()
|
||||
result = 31 * result + isFavorite.hashCode()
|
||||
result = 31 * result + theyFavoritedUs.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages favorites with Noise↔Nostr mapping
|
||||
* Singleton pattern matching iOS implementation
|
||||
*/
|
||||
class FavoritesPersistenceService private constructor(private val context: Context) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "FavoritesPersistenceService"
|
||||
private const val FAVORITES_KEY = "favorite_relationships"
|
||||
|
||||
@Volatile
|
||||
private var INSTANCE: FavoritesPersistenceService? = null
|
||||
|
||||
val shared: FavoritesPersistenceService
|
||||
get() = INSTANCE ?: throw IllegalStateException("FavoritesPersistenceService not initialized")
|
||||
|
||||
fun initialize(context: Context) {
|
||||
if (INSTANCE == null) {
|
||||
synchronized(this) {
|
||||
if (INSTANCE == null) {
|
||||
INSTANCE = FavoritesPersistenceService(context.applicationContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val stateManager = SecureIdentityStateManager(context)
|
||||
private val gson = Gson()
|
||||
private val favorites = mutableMapOf<String, FavoriteRelationship>() // noiseKeyHex -> relationship
|
||||
|
||||
init {
|
||||
loadFavorites()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get favorite status for Noise public key
|
||||
*/
|
||||
fun getFavoriteStatus(noisePublicKey: ByteArray): FavoriteRelationship? {
|
||||
val keyHex = noisePublicKey.joinToString("") { "%02x".format(it) }
|
||||
return favorites[keyHex]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get favorite status for 16-hex peerID
|
||||
*/
|
||||
fun getFavoriteStatus(peerID: String): FavoriteRelationship? {
|
||||
// For 16-hex peerIDs, we need to find the corresponding full Noise key
|
||||
// This is a simplified lookup - in practice you'd use fingerprint matching
|
||||
for ((_, relationship) in favorites) {
|
||||
val noiseKeyHex = relationship.peerNoisePublicKey.joinToString("") { "%02x".format(it) }
|
||||
if (noiseKeyHex.startsWith(peerID)) {
|
||||
return relationship
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Nostr public key for a peer
|
||||
*/
|
||||
fun updateNostrPublicKey(noisePublicKey: ByteArray, nostrPubkey: String) {
|
||||
val keyHex = noisePublicKey.joinToString("") { "%02x".format(it) }
|
||||
val existing = favorites[keyHex]
|
||||
|
||||
if (existing != null) {
|
||||
val updated = existing.copy(
|
||||
peerNostrPublicKey = nostrPubkey,
|
||||
lastUpdated = Date()
|
||||
)
|
||||
favorites[keyHex] = updated
|
||||
} else {
|
||||
// Create new relationship
|
||||
val relationship = FavoriteRelationship(
|
||||
peerNoisePublicKey = noisePublicKey,
|
||||
peerNostrPublicKey = nostrPubkey,
|
||||
peerNickname = "Unknown",
|
||||
isFavorite = false,
|
||||
theyFavoritedUs = false,
|
||||
favoritedAt = Date(),
|
||||
lastUpdated = Date()
|
||||
)
|
||||
favorites[keyHex] = relationship
|
||||
}
|
||||
|
||||
saveFavorites()
|
||||
Log.d(TAG, "Updated Nostr pubkey association for ${keyHex.take(16)}...")
|
||||
}
|
||||
|
||||
/**
|
||||
* Update favorite status
|
||||
*/
|
||||
fun updateFavoriteStatus(noisePublicKey: ByteArray, nickname: String, isFavorite: Boolean) {
|
||||
val keyHex = noisePublicKey.joinToString("") { "%02x".format(it) }
|
||||
val existing = favorites[keyHex]
|
||||
|
||||
val updated = if (existing != null) {
|
||||
existing.copy(
|
||||
peerNickname = nickname,
|
||||
isFavorite = isFavorite,
|
||||
lastUpdated = Date(),
|
||||
favoritedAt = if (isFavorite && !existing.isFavorite) Date() else existing.favoritedAt
|
||||
)
|
||||
} else {
|
||||
FavoriteRelationship(
|
||||
peerNoisePublicKey = noisePublicKey,
|
||||
peerNostrPublicKey = null,
|
||||
peerNickname = nickname,
|
||||
isFavorite = isFavorite,
|
||||
theyFavoritedUs = false,
|
||||
favoritedAt = Date(),
|
||||
lastUpdated = Date()
|
||||
)
|
||||
}
|
||||
|
||||
favorites[keyHex] = updated
|
||||
saveFavorites()
|
||||
|
||||
Log.d(TAG, "Updated favorite status for $nickname: $isFavorite")
|
||||
}
|
||||
|
||||
/**
|
||||
* Update peer favorited us status
|
||||
*/
|
||||
fun updatePeerFavoritedUs(noisePublicKey: ByteArray, theyFavoritedUs: Boolean) {
|
||||
val keyHex = noisePublicKey.joinToString("") { "%02x".format(it) }
|
||||
val existing = favorites[keyHex]
|
||||
|
||||
if (existing != null) {
|
||||
val updated = existing.copy(
|
||||
theyFavoritedUs = theyFavoritedUs,
|
||||
lastUpdated = Date()
|
||||
)
|
||||
favorites[keyHex] = updated
|
||||
saveFavorites()
|
||||
|
||||
Log.d(TAG, "Updated peer favorited us for ${keyHex.take(16)}...: $theyFavoritedUs")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all mutual favorites
|
||||
*/
|
||||
fun getMutualFavorites(): List<FavoriteRelationship> {
|
||||
return favorites.values.filter { it.isMutual }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all favorites we have
|
||||
*/
|
||||
fun getOurFavorites(): List<FavoriteRelationship> {
|
||||
return favorites.values.filter { it.isFavorite }
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all favorites
|
||||
*/
|
||||
fun clearAllFavorites() {
|
||||
favorites.clear()
|
||||
saveFavorites()
|
||||
Log.i(TAG, "Cleared all favorites")
|
||||
}
|
||||
|
||||
/**
|
||||
* Find Noise key by Nostr pubkey
|
||||
*/
|
||||
fun findNoiseKey(forNostrPubkey: String): ByteArray? {
|
||||
return favorites.values.firstOrNull { it.peerNostrPublicKey == forNostrPubkey }?.peerNoisePublicKey
|
||||
}
|
||||
|
||||
/**
|
||||
* Find Nostr pubkey by Noise key
|
||||
*/
|
||||
fun findNostrPubkey(forNoiseKey: ByteArray): String? {
|
||||
val keyHex = forNoiseKey.joinToString("") { "%02x".format(it) }
|
||||
return favorites[keyHex]?.peerNostrPublicKey
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
|
||||
private fun loadFavorites() {
|
||||
try {
|
||||
// Use reflection to access encrypted preferences
|
||||
val clazz = stateManager.javaClass
|
||||
val prefsField = clazz.getDeclaredField("prefs")
|
||||
prefsField.isAccessible = true
|
||||
val prefs = prefsField.get(stateManager) as android.content.SharedPreferences
|
||||
|
||||
val favoritesJson = prefs.getString(FAVORITES_KEY, null)
|
||||
if (favoritesJson != null) {
|
||||
val type = object : TypeToken<Map<String, FavoriteRelationshipData>>() {}.type
|
||||
val data: Map<String, FavoriteRelationshipData> = gson.fromJson(favoritesJson, type)
|
||||
|
||||
favorites.clear()
|
||||
data.forEach { (key, relationshipData) ->
|
||||
val relationship = relationshipData.toFavoriteRelationship()
|
||||
favorites[key] = relationship
|
||||
}
|
||||
|
||||
Log.d(TAG, "Loaded ${favorites.size} favorite relationships")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to load favorites: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveFavorites() {
|
||||
try {
|
||||
// Convert to serializable format
|
||||
val data = favorites.mapValues { (_, relationship) ->
|
||||
FavoriteRelationshipData.fromFavoriteRelationship(relationship)
|
||||
}
|
||||
|
||||
val favoritesJson = gson.toJson(data)
|
||||
|
||||
// Use reflection to access encrypted preferences
|
||||
val clazz = stateManager.javaClass
|
||||
val prefsField = clazz.getDeclaredField("prefs")
|
||||
prefsField.isAccessible = true
|
||||
val prefs = prefsField.get(stateManager) as android.content.SharedPreferences
|
||||
|
||||
prefs.edit()
|
||||
.putString(FAVORITES_KEY, favoritesJson)
|
||||
.apply()
|
||||
|
||||
Log.d(TAG, "Saved ${favorites.size} favorite relationships")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to save favorites: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializable data class for JSON storage
|
||||
*/
|
||||
private data class FavoriteRelationshipData(
|
||||
val peerNoisePublicKeyHex: String,
|
||||
val peerNostrPublicKey: String?,
|
||||
val peerNickname: String,
|
||||
val isFavorite: Boolean,
|
||||
val theyFavoritedUs: Boolean,
|
||||
val favoritedAt: Long,
|
||||
val lastUpdated: Long
|
||||
) {
|
||||
companion object {
|
||||
fun fromFavoriteRelationship(relationship: FavoriteRelationship): FavoriteRelationshipData {
|
||||
return FavoriteRelationshipData(
|
||||
peerNoisePublicKeyHex = relationship.peerNoisePublicKey.joinToString("") { "%02x".format(it) },
|
||||
peerNostrPublicKey = relationship.peerNostrPublicKey,
|
||||
peerNickname = relationship.peerNickname,
|
||||
isFavorite = relationship.isFavorite,
|
||||
theyFavoritedUs = relationship.theyFavoritedUs,
|
||||
favoritedAt = relationship.favoritedAt.time,
|
||||
lastUpdated = relationship.lastUpdated.time
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun toFavoriteRelationship(): FavoriteRelationship {
|
||||
val noiseKeyBytes = peerNoisePublicKeyHex.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||
return FavoriteRelationship(
|
||||
peerNoisePublicKey = noiseKeyBytes,
|
||||
peerNostrPublicKey = peerNostrPublicKey,
|
||||
peerNickname = peerNickname,
|
||||
isFavorite = isFavorite,
|
||||
theyFavoritedUs = theyFavoritedUs,
|
||||
favoritedAt = Date(favoritedAt),
|
||||
lastUpdated = Date(lastUpdated)
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user