plus minus location notes

This commit is contained in:
callebtc
2025-10-18 12:15:49 +02:00
parent 02868a95d4
commit d5f27aa4b3
3 changed files with 78 additions and 27 deletions
@@ -115,4 +115,36 @@ object Geohash {
lonMax = maxOf(lonInterval.first, lonInterval.second) lonMax = maxOf(lonInterval.first, lonInterval.second)
) )
} }
/**
* Returns the 8 neighboring geohash cells at the same precision as the input.
* Neighbors include N, NE, E, SE, S, SW, W, NW, even when crossing parent cell boundaries.
*/
fun neighborsSamePrecision(geohash: String): Set<String> {
if (geohash.isEmpty()) return emptySet()
val p = geohash.length
val b = decodeToBounds(geohash)
val dLat = b.latMax - b.latMin
val dLon = b.lonMax - b.lonMin
fun wrapLon(lon: Double): Double {
var x = lon
while (x > 180.0) x -= 360.0
while (x < -180.0) x += 360.0
return x
}
val neighbors = mutableSetOf<String>()
for (dy in -1..1) {
for (dx in -1..1) {
if (dx == 0 && dy == 0) continue // skip center
val centerLat = (b.latMin + b.latMax) / 2 + dy * dLat
val rawLonCenter = (b.lonMin + b.lonMax) / 2 + dx * dLon
val centerLon = wrapLon(rawLonCenter)
val enc = encode(centerLat.coerceIn(-90.0, 90.0), centerLon, p)
if (enc.isNotEmpty() && enc != geohash) neighbors.add(enc)
}
}
return neighbors
}
} }
@@ -80,8 +80,9 @@ class LocationNotesManager private constructor() {
val errorMessage: LiveData<String?> = _errorMessage val errorMessage: LiveData<String?> = _errorMessage
// Private state // Private state
private var subscriptionID: String? = null private var subscriptionIDs: MutableMap<String, String> = mutableMapOf()
private val noteIDs = mutableSetOf<String>() // For deduplication private val noteIDs = mutableSetOf<String>() // For deduplication
private var subscribedGeohashes: Set<String> = emptySet()
// Dependencies (injected via setters for flexibility) // Dependencies (injected via setters for flexibility)
private var relayLookup: (() -> NostrRelayManager)? = null private var relayLookup: (() -> NostrRelayManager)? = null
@@ -143,8 +144,14 @@ class LocationNotesManager private constructor() {
noteIDs.clear() noteIDs.clear()
_geohash.value = normalized _geohash.value = normalized
// Start new subscription // Compute target geohashes: center + neighbors (±1)
subscribe() val neighbors = try {
com.bitchat.android.geohash.Geohash.neighborsSamePrecision(normalized)
} catch (_: Exception) { emptySet() }
subscribedGeohashes = (neighbors + normalized).toSet()
// Start new subscriptions for all cells
subscribeAll()
} }
/** /**
@@ -168,12 +175,17 @@ class LocationNotesManager private constructor() {
Log.d(TAG, "Refreshing notes for geohash: $currentGeohash") Log.d(TAG, "Refreshing notes for geohash: $currentGeohash")
// Cancel and restart subscription // Cancel and restart subscriptions for current ±1 set
cancel() cancel()
_notes.value = emptyList() _notes.value = emptyList()
noteIDs.clear() noteIDs.clear()
_initialLoadComplete.value = false _initialLoadComplete.value = false
subscribe() // Rebuild subscribedGeohashes and resubscribe
val neighbors = try {
com.bitchat.android.geohash.Geohash.neighborsSamePrecision(currentGeohash)
} catch (_: Exception) { emptySet() }
subscribedGeohashes = (neighbors + currentGeohash).toSet()
subscribeAll()
} }
/** /**
@@ -275,7 +287,7 @@ class LocationNotesManager private constructor() {
/** /**
* Subscribe to location notes for current geohash * Subscribe to location notes for current geohash
*/ */
private fun subscribe() { private fun subscribeAll() {
val currentGeohash = _geohash.value val currentGeohash = _geohash.value
if (currentGeohash == null) { if (currentGeohash == null) {
Log.w(TAG, "Cannot subscribe - no geohash set") Log.w(TAG, "Cannot subscribe - no geohash set")
@@ -292,18 +304,21 @@ class LocationNotesManager private constructor() {
_state.value = State.LOADING _state.value = State.LOADING
val filter = NostrFilter.geohashNotes( // Subscribe for each geohash in the ±1 set
geohash = currentGeohash, subscribedGeohashes.forEach { gh ->
since = null, // Get all notes (relays will limit) val filter = NostrFilter.geohashNotes(
limit = 200 geohash = gh,
) since = null,
limit = 200
val subId = "location-notes-$currentGeohash" )
val subId = "location-notes-$gh"
Log.d(TAG, "📡 Subscribing to location notes: $subId") Log.d(TAG, "📡 Subscribing to location notes: $subId")
try {
subscriptionID = subscribe(filter, subId) { event -> val id = subscribe(filter, subId) { event -> handleEvent(event) }
handleEvent(event) subscriptionIDs[gh] = id
} catch (e: Exception) {
Log.e(TAG, "Failed to subscribe for $gh: ${e.message}")
}
} }
// Mark initial load complete after brief delay to allow relay responses // Mark initial load complete after brief delay to allow relay responses
@@ -336,8 +351,8 @@ class LocationNotesManager private constructor() {
// Check if matches current geohash // Check if matches current geohash
val eventGeohash = geohashTag[1] val eventGeohash = geohashTag[1]
if (eventGeohash != _geohash.value) { if (!subscribedGeohashes.contains(eventGeohash)) {
Log.v(TAG, "Ignoring event for different geohash: $eventGeohash") Log.v(TAG, "Ignoring event for non-subscribed geohash: $eventGeohash")
return return
} }
@@ -406,12 +421,16 @@ class LocationNotesManager private constructor() {
* Cancel subscription and clear state * Cancel subscription and clear state
*/ */
fun cancel() { fun cancel() {
subscriptionID?.let { subId -> if (subscriptionIDs.isNotEmpty()) {
Log.d(TAG, "🚫 Canceling subscription: $subId") subscriptionIDs.values.forEach { subId ->
unsubscribeFunc?.invoke(subId) try {
subscriptionID = null Log.d(TAG, "🚫 Canceling subscription: $subId")
unsubscribeFunc?.invoke(subId)
} catch (_: Exception) { }
}
subscriptionIDs.clear()
} }
subscribedGeohashes = emptySet()
_state.value = State.IDLE _state.value = State.IDLE
} }
@@ -215,9 +215,9 @@ private fun LocationNotesHeader(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// iOS format: "#geohash • X notes" // "#geohash ± 1 • X notes" to indicate neighboring cells are included
Text( Text(
text = "#$geohash$count ${if (count == 1) "note" else "notes"}", text = "#$geohash ± 1$count ${if (count == 1) "note" else "notes"}",
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
fontSize = 18.sp, fontSize = 18.sp,
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface