diff --git a/bitchat/Nostr/NostrRelayManager.swift b/bitchat/Nostr/NostrRelayManager.swift index 48788cdd..722d8a77 100644 --- a/bitchat/Nostr/NostrRelayManager.swift +++ b/bitchat/Nostr/NostrRelayManager.swift @@ -906,6 +906,16 @@ struct NostrFilter: Encodable { filter.limit = limit return filter } + + // For location notes with neighbors: subscribe to multiple geohashes (center + neighbors) + static func geohashNotes(_ geohashes: [String], since: Date? = nil, limit: Int = 200) -> NostrFilter { + var filter = NostrFilter() + filter.kinds = [1] + filter.since = since?.timeIntervalSince1970.toInt() + filter.tagFilters = ["g": geohashes] + filter.limit = limit + return filter + } } // Dynamic coding key for tag filters diff --git a/bitchat/Protocols/Geohash.swift b/bitchat/Protocols/Geohash.swift index e5576775..f436ea27 100644 --- a/bitchat/Protocols/Geohash.swift +++ b/bitchat/Protocols/Geohash.swift @@ -119,4 +119,57 @@ enum Geohash { } return (latInterval.0, latInterval.1, lonInterval.0, lonInterval.1) } + + /// Returns all 8 neighboring geohash cells at the same precision. + /// - Parameter geohash: Base32 geohash string. + /// - Returns: Array of 8 neighboring geohashes (N, NE, E, SE, S, SW, W, NW order). + static func neighbors(of geohash: String) -> [String] { + guard !geohash.isEmpty else { return [] } + + let precision = geohash.count + let bounds = decodeBounds(geohash) + let center = decodeCenter(geohash) + + // Calculate cell dimensions + let latHeight = bounds.latMax - bounds.latMin + let lonWidth = bounds.lonMax - bounds.lonMin + + // Helper to wrap longitude around ±180 + func wrapLongitude(_ lon: Double) -> Double { + var wrapped = lon + while wrapped > 180.0 { wrapped -= 360.0 } + while wrapped < -180.0 { wrapped += 360.0 } + return wrapped + } + + // Helper to clamp latitude to ±90 + func clampLatitude(_ lat: Double) -> Double { + return max(-90.0, min(90.0, lat)) + } + + // Calculate 8 neighbor centers + let neighbors: [(lat: Double, lon: Double)] = [ + (center.lat + latHeight, center.lon), // N + (center.lat + latHeight, center.lon + lonWidth), // NE + (center.lat, center.lon + lonWidth), // E + (center.lat - latHeight, center.lon + lonWidth), // SE + (center.lat - latHeight, center.lon), // S + (center.lat - latHeight, center.lon - lonWidth), // SW + (center.lat, center.lon - lonWidth), // W + (center.lat + latHeight, center.lon - lonWidth) // NW + ] + + // Encode each neighbor, handling boundary conditions + return neighbors.compactMap { neighbor in + let lat = clampLatitude(neighbor.lat) + let lon = wrapLongitude(neighbor.lon) + + // Skip if we've crossed a pole (latitude clamped to boundary) + if (neighbor.lat > 90.0 || neighbor.lat < -90.0) { + return nil + } + + return encode(latitude: lat, longitude: lon, precision: precision) + } + } } diff --git a/bitchat/Services/LocationNotesManager.swift b/bitchat/Services/LocationNotesManager.swift index 5c828a01..e9153163 100644 --- a/bitchat/Services/LocationNotesManager.swift +++ b/bitchat/Services/LocationNotesManager.swift @@ -163,14 +163,22 @@ final class LocationNotesManager: ObservableObject { subscriptionID = subID initialLoadComplete = false - // For persistent notes, allow relays to return recent history without an aggressive time cutoff - let filter = NostrFilter.geohashNotes(geohash, since: nil, limit: 200) + + // Subscribe to center + 8 neighbors (± 1 grid) + let neighbors = Geohash.neighbors(of: geohash) + let allGeohashes = [geohash] + neighbors + let filter = NostrFilter.geohashNotes(allGeohashes, since: nil, limit: 200) + + // Build a set of valid geohashes for tag matching (includes all 9 cells) + let validGeohashes = Set(allGeohashes.map { $0.lowercased() }) dependencies.subscribe(filter, subID, relays, { [weak self] event in guard let self = self else { return } guard event.kind == NostrProtocol.EventKind.textNote.rawValue else { return } - // Ensure matching tag - guard event.tags.contains(where: { $0.count >= 2 && $0[0].lowercased() == "g" && $0[1].lowercased() == self.geohash }) else { return } + // Ensure matching tag - accept any of our 9 geohashes + guard event.tags.contains(where: { tag in + tag.count >= 2 && tag[0].lowercased() == "g" && validGeohashes.contains(tag[1].lowercased()) + }) else { return } guard !self.noteIDs.contains(event.id) else { return } self.noteIDs.insert(event.id) let nick = event.tags.first(where: { $0.first?.lowercased() == "n" && $0.count >= 2 })?.dropFirst().first diff --git a/bitchat/Views/LocationNotesView.swift b/bitchat/Views/LocationNotesView.swift index 8dbe901d..a705719e 100644 --- a/bitchat/Views/LocationNotesView.swift +++ b/bitchat/Views/LocationNotesView.swift @@ -141,7 +141,7 @@ struct LocationNotesView: View { String( format: String(localized: "location_notes.header", comment: "Header displaying the geohash and localized note count"), locale: .current, - geohash, count + "\(geohash) ± 1", count ) }