From 293a7515960f8431677b34dcf0c624892acb2663 Mon Sep 17 00:00:00 2001 From: Taksh Date: Mon, 17 Aug 2026 08:57:13 +0530 Subject: [PATCH] fix(notes): drop expired notes and match geohash tags case-insensitively Two ways the Android note handler disagrees with the iOS one, on the same event: - NIP-40. Relays are not required to drop expired events, so iOS checks the `expiration` tag client-side "so 24h dead drops actually vanish". Android had no such check, so an expired note kept showing until the list was cleared - the opposite of what a dead drop promises. - Case. Tag names and geohashes are case-insensitive, and iOS lowercases both before matching. Android compared them exactly, so a note tagged ["G", "U4PRUYD"] was visible on iOS and invisible on Android. Both now follow the iOS handler. --- .../android/nostr/LocationNotesManager.kt | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/nostr/LocationNotesManager.kt b/app/src/main/java/com/bitchat/android/nostr/LocationNotesManager.kt index a6927d71..d2526f26 100644 --- a/app/src/main/java/com/bitchat/android/nostr/LocationNotesManager.kt +++ b/app/src/main/java/com/bitchat/android/nostr/LocationNotesManager.kt @@ -417,23 +417,30 @@ class LocationNotesManager private constructor() { return } - // Check for geohash tag - val geohashTag = event.tags.firstOrNull { it.size >= 2 && it[0] == "g" } + // Check for geohash tag. Tag names and geohashes are case-insensitive, + // and iOS matches them lowercased, so a note tagged ["G", "U4PRUYD"] + // has to be the same note on both platforms. + val validGeohashes = subscribedGeohashes.map { it.lowercase() }.toSet() + val geohashTag = event.tags.firstOrNull { + it.size >= 2 && it[0].lowercase() == "g" && validGeohashes.contains(it[1].lowercase()) + } if (geohashTag == null) { - Log.v(TAG, "Ignoring event without geohash tag: ${event.id.take(16)}...") + Log.v(TAG, "Ignoring event without a matching geohash tag: ${event.id.take(16)}...") return } - - // Check if matches current geohash - val eventGeohash = geohashTag[1] - if (!subscribedGeohashes.contains(eventGeohash)) { - return - } - + // Deduplicate if (noteIDs.contains(event.id)) { return } + + // NIP-40: relays are not required to drop expired events, so enforce it + // here - otherwise a 24h dead drop stays visible past its expiry. + val expiresAt = expirationSeconds(event) + if (expiresAt != null && expiresAt * 1000L <= System.currentTimeMillis()) { + Log.v(TAG, "Ignoring expired note: ${event.id.take(16)}...") + return + } // Extract nickname from tags val nicknameTag = event.tags.firstOrNull { it.size >= 2 && it[0] == "n" } @@ -465,6 +472,15 @@ class LocationNotesManager private constructor() { _state.value = State.READY } + /** + * The NIP-40 `expiration` tag as unix seconds, when the event carries one. + */ + private fun expirationSeconds(event: NostrEvent): Long? { + val tag = event.tags.firstOrNull { it.size >= 2 && it[0].lowercase() == "expiration" } + ?: return null + return tag[1].toLongOrNull() + } + /** * Trim oldest notes to stay within memory limit */