Nostr refactor simplify (#390)

* fix bug

* geoDM receive works, send doesnt, and incoming message doesnt make sender appear in peer list

* fix nostr dm

* geohash dms work

* Geohash DM UI: stop mixing Nostr DM temp chats into mesh offline list; ensure geohash DM senders are added to geohash people list only. Removed nostr_* sidebar append in PeopleSection; kept 64-hex mesh offline favorites. Verified build.

* refactor

* nice

* works

* merging nostr -> mesh works

* tripple click to delete all

* fix sidebar icon

* remove hash

* dms have correct recipient

* works

* wip unread badge

* geohash dms wip

* dms work
This commit is contained in:
callebtc
2025-09-08 13:46:15 +02:00
committed by GitHub
parent ba518269b4
commit 998ee606b1
21 changed files with 1509 additions and 2043 deletions
@@ -0,0 +1,176 @@
package com.bitchat.android.nostr
import android.app.Application
import android.util.Log
import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.model.DeliveryStatus
import com.bitchat.android.protocol.BitchatPacket
import com.bitchat.android.services.SeenMessageStore
import com.bitchat.android.ui.ChatState
import com.bitchat.android.ui.MeshDelegateHandler
import com.bitchat.android.ui.PrivateChatManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.Date
class NostrDirectMessageHandler(
private val application: Application,
private val state: ChatState,
private val privateChatManager: PrivateChatManager,
private val meshDelegateHandler: MeshDelegateHandler,
private val scope: CoroutineScope,
private val repo: GeohashRepository
) {
companion object { private const val TAG = "NostrDirectMessageHandler" }
private val seenStore by lazy { SeenMessageStore.getInstance(application) }
// Simple event deduplication
private val processedIds = ArrayDeque<String>()
private val seen = HashSet<String>()
private val max = 2000
private fun dedupe(id: String): Boolean {
if (seen.contains(id)) return true
seen.add(id)
processedIds.addLast(id)
if (processedIds.size > max) {
val old = processedIds.removeFirst()
seen.remove(old)
}
return false
}
fun onGiftWrap(giftWrap: NostrEvent, geohash: String, identity: NostrIdentity) {
scope.launch(Dispatchers.Default) {
try {
if (dedupe(giftWrap.id)) return@launch
val messageAge = System.currentTimeMillis() / 1000 - giftWrap.createdAt
if (messageAge > 173700) return@launch // 48 hours + 15 mins
val decryptResult = NostrProtocol.decryptPrivateMessage(giftWrap, identity)
if (decryptResult == null) {
Log.w(TAG, "Failed to decrypt Nostr message")
return@launch
}
val (content, senderPubkey, rumorTimestamp) = decryptResult
if (!content.startsWith("bitchat1:")) return@launch
val base64Content = content.removePrefix("bitchat1:")
val packetData = base64URLDecode(base64Content) ?: return@launch
val packet = BitchatPacket.fromBinaryData(packetData) ?: return@launch
if (packet.type != com.bitchat.android.protocol.MessageType.NOISE_ENCRYPTED.value) return@launch
val noisePayload = com.bitchat.android.model.NoisePayload.decode(packet.payload) ?: return@launch
val messageTimestamp = Date(rumorTimestamp * 1000L)
val convKey = "nostr_${senderPubkey.take(16)}"
repo.putNostrKeyMapping(convKey, senderPubkey)
com.bitchat.android.nostr.GeohashAliasRegistry.put(convKey, senderPubkey)
if (geohash.isNotEmpty()) {
// Remember which geohash this conversation belongs to so we can subscribe on-demand
repo.setConversationGeohash(convKey, geohash)
GeohashConversationRegistry.set(convKey, geohash)
}
// Ensure sender appears in geohash people list even if they haven't posted publicly yet
if (geohash.isNotEmpty()) {
// Cache a best-effort nickname and mark as participant
val cached = repo.getCachedNickname(senderPubkey)
if (cached == null) {
val base = repo.displayNameForNostrPubkeyUI(senderPubkey).substringBefore("#")
repo.cacheNickname(senderPubkey, base)
}
repo.updateParticipant(geohash, senderPubkey, messageTimestamp)
}
val senderNickname = repo.displayNameForNostrPubkeyUI(senderPubkey)
processNoisePayload(noisePayload, convKey, senderNickname, messageTimestamp, senderPubkey, identity)
} catch (e: Exception) {
Log.e(TAG, "onGiftWrap error: ${e.message}")
}
}
}
private suspend fun processNoisePayload(
payload: com.bitchat.android.model.NoisePayload,
convKey: String,
senderNickname: String,
timestamp: Date,
senderPubkey: String,
recipientIdentity: NostrIdentity
) {
when (payload.type) {
com.bitchat.android.model.NoisePayloadType.PRIVATE_MESSAGE -> {
val pm = com.bitchat.android.model.PrivateMessagePacket.decode(payload.data) ?: return
val existingMessages = state.getPrivateChatsValue()[convKey] ?: emptyList()
if (existingMessages.any { it.id == pm.messageID }) return
val message = BitchatMessage(
id = pm.messageID,
sender = senderNickname,
content = pm.content,
timestamp = timestamp,
isRelay = false,
isPrivate = true,
recipientNickname = state.getNicknameValue(),
senderPeerID = convKey,
deliveryStatus = DeliveryStatus.Delivered(to = state.getNicknameValue() ?: "Unknown", at = Date())
)
val isViewing = state.getSelectedPrivateChatPeerValue() == convKey
val suppressUnread = seenStore.hasRead(pm.messageID)
withContext(Dispatchers.Main) {
privateChatManager.handleIncomingPrivateMessage(message, suppressUnread)
}
if (!seenStore.hasDelivered(pm.messageID)) {
val nostrTransport = NostrTransport.getInstance(application)
nostrTransport.sendDeliveryAckGeohash(pm.messageID, senderPubkey, recipientIdentity)
seenStore.markDelivered(pm.messageID)
}
if (isViewing && !suppressUnread) {
val nostrTransport = NostrTransport.getInstance(application)
nostrTransport.sendReadReceiptGeohash(pm.messageID, senderPubkey, recipientIdentity)
seenStore.markRead(pm.messageID)
}
}
com.bitchat.android.model.NoisePayloadType.DELIVERED -> {
val messageId = String(payload.data, Charsets.UTF_8)
withContext(Dispatchers.Main) {
meshDelegateHandler.didReceiveDeliveryAck(messageId, convKey)
}
}
com.bitchat.android.model.NoisePayloadType.READ_RECEIPT -> {
val messageId = String(payload.data, Charsets.UTF_8)
withContext(Dispatchers.Main) {
meshDelegateHandler.didReceiveReadReceipt(messageId, convKey)
}
}
}
}
private fun base64URLDecode(input: String): ByteArray? {
return try {
val padded = input.replace("-", "+")
.replace("_", "/")
.let { str ->
val padding = (4 - str.length % 4) % 4
str + "=".repeat(padding)
}
android.util.Base64.decode(padded, android.util.Base64.DEFAULT)
} catch (e: Exception) {
Log.e(TAG, "Failed to decode base64url: ${e.message}")
null
}
}
}