Files
bitchat/bitchat/Protocols/LocationChannel.swift
T
2870acdcc7 Location Notes (kind 1) at Building Precision + Live Updates (#598)
* Location notes: Matrix loader with 1–3s minimum display; allow multiple notes; wire notes UI from mesh toolbar

* Location notes: prevent duplicate loading/subscriptions (StateObject manager, geohash captured at open, guard subscribe)

* Location notes: fix duplicate  state redeclaration in ContentView

* Location notes: render note bodies with monospaced font in notes list

* Location notes: add close (x) button in sheet header (upper-right) using @Environment(\.dismiss)

* Location notes: acquire geohash on open (force refresh) and show Matrix loader until block-level geohash resolves; add LocationNotesSheet wrapper

* Location notes: inline sheet wrapper to avoid missing file in target; force location refresh and show Matrix loader until block geohash resolves

* Location notes: remove Matrix animation; use simple spinner when acquiring location; simplify notes view layout

* Location notes: remove per-note relative timestamp from sheet (no seconds display)

* Location notes: fix intermittent first load by ensuring resubscribe after geohash change, removing over-eager subscribe guard, and widening note fetch window (no since filter)

* Location notes: add background counter service and show count next to notes icon on mesh; auto-subscribe to current block geohash

* Location notes: move notes icon to the right of #mesh badge in toolbar

* Location notes: restore timestamps in notes list; remove loading state from manager and sheet (no spinner/animation)

* Location notes: drop 'teleport' tag from kind-1 events; simplify note builder API and usage

* Location notes: show timestamp as relative within 7 days, else absolute date (MMM d or MMM d, y); keep monospaced styling

* Location notes: append 'ago' to relative timestamps (within 7 days)

* Switch location notes and UI helpers to building-level geohash (precision 8): add GeohashChannelLevel.building, map length 8, use building for notes selection and counter, add building name mapping

* Location notes: change notes toolbar icon to SF Symbol 'long.text.page.and.pencil'

* Revert notes geohash selection back to block-level (precision 7); keep building level available but unused; update counter and sheet accordingly

* Location notes: show block name (from reverse geocode) in header instead of 'street-level notes'

* Nostr: add EOSE handling with callback support for subscriptions; wire to LocationNotesManager/Counter (initialLoadComplete). Remove 'building' level from channel enum and geocoder mapping; geohash list shows block/neighborhood/city/province/region only

* Fix Swift 6 isolation: hop to @MainActor inside Timer callback for EOSE tracker mutations

* Notes counter: show 0 by default; subscribe at building-level (precision 8) for notes and counter; keep building hidden in channel list

* Notes header: show building name when available (fallback to block name)

* Notes: subscribe counter to building + parent block (merge results); hide notes icon unless location is authorized; replace 10s timer with live location updates while sheet open

* Notes counter: subscribe only to building geohash (precision 8) so count reflects current 8-cell; auto-begin live location refresh on mesh (and permission) to update as you walk

* Notes header: show count in parentheses; icon shows blue if any notes, grey if none; only start live location updates while sheet is open; reduce nickname width; set live distance filter to 10m

* Notes header: show count as '(N note/notes)' with non-bold, secondary styling next to title

* Notes header: move count before title as '<N> note(s) @ #gh'; remove parentheses; keep count non-bold

* Notes header: bold count text; ensure sheet updates when building geohash changes by calling manager.setGeohash on geohash change

* Notes sheet: add light haptic feedback when building geohash changes (iOS only)

* Notes icon: force a one-shot location refresh before (re)subscribing the counter so icon turns blue immediately on current 8-cell

* Notes subscribe: fallback to default relays when GeoRelayDirectory has no entries (pass nil relayUrls) so counter/icon turn blue and sheet loads even before georelay fetch

* Notes icon: turn blue immediately when sheet shows notes by passing count up from sheet (closure) and OR-ing with counter; reset on sheet close

---------

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
2025-09-13 14:55:04 +02:00

114 lines
3.3 KiB
Swift

import Foundation
/// Levels of location channels mapped to geohash precisions.
enum GeohashChannelLevel: CaseIterable, Codable, Equatable {
case building
case block
case neighborhood
case city
case province // previously .region
case region // previously .country
/// Geohash length used for this level.
var precision: Int {
switch self {
case .building: return 8
case .block: return 7
case .neighborhood: return 6
case .city: return 5
case .province: return 4
case .region: return 2
}
}
var displayName: String {
switch self {
case .building: return "Building"
case .block: return "Block"
case .neighborhood: return "Neighborhood"
case .city: return "City"
case .province: return "Province"
case .region: return "Region"
}
}
}
// Backward-compatible Codable for renamed cases
extension GeohashChannelLevel {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let raw = try? container.decode(String.self) {
switch raw {
case "building": self = .building
case "block": self = .block
case "neighborhood": self = .neighborhood
case "city": self = .city
case "region": self = .province // old "region" maps to new .province
case "country": self = .region // old "country" maps to new .region
case "province": self = .province
default:
self = .block
}
} else if let precision = try? container.decode(Int.self) {
switch precision {
case 8: self = .building
case 7: self = .block
case 6: self = .neighborhood
case 5: self = .city
case 4: self = .province
case 0...3: self = .region
default: self = .block
}
} else {
self = .block
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .building: try container.encode("building")
case .block: try container.encode("block")
case .neighborhood: try container.encode("neighborhood")
case .city: try container.encode("city")
case .province: try container.encode("province")
case .region: try container.encode("region")
}
}
}
/// A computed geohash channel option.
struct GeohashChannel: Codable, Equatable, Hashable, Identifiable {
let level: GeohashChannelLevel
let geohash: String
var id: String { "\(level)-\(geohash)" }
var displayName: String {
"\(level.displayName)\(geohash)"
}
}
/// Identifier for current public chat channel (mesh or a location geohash).
enum ChannelID: Equatable, Codable {
case mesh
case location(GeohashChannel)
/// Human readable name for UI.
var displayName: String {
switch self {
case .mesh:
return "Mesh"
case .location(let ch):
return ch.displayName
}
}
/// Nostr tag value for scoping (geohash), if applicable.
var nostrGeohashTag: String? {
switch self {
case .mesh: return nil
case .location(let ch): return ch.geohash
}
}
}