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.
This commit is contained in:
Taksh 2026-08-17 08:57:13 +05:30
parent b028270cc4
commit 293a751596

View File

@ -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
*/