mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 19:45:25 +00:00
plus minus location notes
This commit is contained in:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user