mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 05:05:19 +00:00
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>
This commit is contained in:
@@ -25,6 +25,7 @@ struct ContentView: View {
|
||||
@EnvironmentObject var viewModel: ChatViewModel
|
||||
@ObservedObject private var locationManager = LocationChannelManager.shared
|
||||
@ObservedObject private var bookmarks = GeohashBookmarksStore.shared
|
||||
@ObservedObject private var notesCounter = LocationNotesCounter.shared
|
||||
@State private var messageText = ""
|
||||
@State private var textFieldSelection: NSRange? = nil
|
||||
@FocusState private var isTextFieldFocused: Bool
|
||||
@@ -49,6 +50,10 @@ struct ContentView: View {
|
||||
@State private var showLocationChannelsSheet = false
|
||||
@State private var showVerifySheet = false
|
||||
@State private var expandedMessageIDs: Set<String> = []
|
||||
@State private var showLocationNotes = false
|
||||
@State private var notesGeohash: String? = nil
|
||||
@State private var sheetNotesCount: Int = 0
|
||||
// Timer-based refresh removed; use LocationChannelManager live updates instead
|
||||
// Window sizes for rendering (infinite scroll up)
|
||||
@State private var windowCountPublic: Int = 300
|
||||
@State private var windowCountPrivate: [String: Int] = [:]
|
||||
@@ -466,14 +471,14 @@ struct ContentView: View {
|
||||
guard (2...12).contains(gh.count), gh.allSatisfy({ allowed.contains($0) }) else { return }
|
||||
func levelForLength(_ len: Int) -> GeohashChannelLevel {
|
||||
switch len {
|
||||
case 0...2: return .region
|
||||
case 3...4: return .province
|
||||
case 5: return .city
|
||||
case 6: return .neighborhood
|
||||
case 0...2: return .region
|
||||
case 3...4: return .province
|
||||
case 5: return .city
|
||||
case 6: return .neighborhood
|
||||
case 7: return .block
|
||||
default: return .block
|
||||
}
|
||||
}
|
||||
}
|
||||
let level = levelForLength(gh.count)
|
||||
let ch = GeohashChannel(level: level, geohash: gh)
|
||||
// Do not mark teleported when opening a geohash that is in our regional set.
|
||||
@@ -1091,7 +1096,7 @@ struct ContentView: View {
|
||||
TextField("nickname", text: $viewModel.nickname)
|
||||
.textFieldStyle(.plain)
|
||||
.font(.system(size: 14, design: .monospaced))
|
||||
.frame(maxWidth: 100)
|
||||
.frame(maxWidth: 80)
|
||||
.foregroundColor(textColor)
|
||||
.focused($isNicknameFieldFocused)
|
||||
.autocorrectionDisabled(true)
|
||||
@@ -1170,6 +1175,29 @@ struct ContentView: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
// Notes icon (mesh only and when location is authorized), to the right of #mesh
|
||||
if case .mesh = locationManager.selectedChannel, locationManager.permissionState == .authorized {
|
||||
Button(action: {
|
||||
// Kick a one-shot refresh and show the sheet immediately.
|
||||
LocationChannelManager.shared.enableLocationChannels()
|
||||
LocationChannelManager.shared.refreshChannels()
|
||||
// If we already have a block geohash, pass it; otherwise wait in the sheet.
|
||||
notesGeohash = LocationChannelManager.shared.availableChannels.first(where: { $0.level == .building })?.geohash
|
||||
showLocationNotes = true
|
||||
}) {
|
||||
HStack(alignment: .center, spacing: 4) {
|
||||
let hasNotes = ((notesCounter.count ?? 0) > 0) || (sheetNotesCount > 0)
|
||||
Image(systemName: "long.text.page.and.pencil")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(hasNotes ? Color(hue: 0.60, saturation: 0.85, brightness: 0.82) : Color.gray)
|
||||
.padding(.top, 1)
|
||||
}
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("Location notes for this place")
|
||||
}
|
||||
|
||||
HStack(spacing: 4) {
|
||||
// People icon with count
|
||||
Image(systemName: "person.2.fill")
|
||||
@@ -1180,9 +1208,12 @@ struct ContentView: View {
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.foregroundColor(headerCountColor)
|
||||
.lineLimit(1)
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
|
||||
// QR moved to the PEOPLE header in the sidebar when on mesh channel
|
||||
}
|
||||
.layoutPriority(3)
|
||||
.onTapGesture {
|
||||
withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) {
|
||||
showSidebar.toggle()
|
||||
@@ -1201,6 +1232,91 @@ struct ContentView: View {
|
||||
.onAppear { viewModel.isLocationChannelsSheetPresented = true }
|
||||
.onDisappear { viewModel.isLocationChannelsSheetPresented = false }
|
||||
}
|
||||
.sheet(isPresented: $showLocationNotes) {
|
||||
Group {
|
||||
if let gh = notesGeohash ?? LocationChannelManager.shared.availableChannels.first(where: { $0.level == .building })?.geohash {
|
||||
LocationNotesView(geohash: gh, onNotesCountChanged: { cnt in sheetNotesCount = cnt })
|
||||
.environmentObject(viewModel)
|
||||
} else {
|
||||
VStack(spacing: 12) {
|
||||
HStack {
|
||||
Text("notes")
|
||||
.font(.system(size: 16, weight: .bold, design: .monospaced))
|
||||
Spacer()
|
||||
Button(action: { showLocationNotes = false }) {
|
||||
Image(systemName: "xmark")
|
||||
.font(.system(size: 13, weight: .semibold, design: .monospaced))
|
||||
.foregroundColor(textColor)
|
||||
.frame(width: 32, height: 32)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("Close")
|
||||
}
|
||||
.frame(height: 44)
|
||||
.padding(.horizontal, 12)
|
||||
.background(backgroundColor.opacity(0.95))
|
||||
Text("location unavailable")
|
||||
.font(.system(size: 14, design: .monospaced))
|
||||
.foregroundColor(secondaryTextColor)
|
||||
Button("enable location") {
|
||||
LocationChannelManager.shared.enableLocationChannels()
|
||||
LocationChannelManager.shared.refreshChannels()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
Spacer()
|
||||
}
|
||||
.background(backgroundColor)
|
||||
.foregroundColor(textColor)
|
||||
// per-sheet global onChange added below
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
// Ensure we are authorized and start live location updates (distance-filtered)
|
||||
LocationChannelManager.shared.enableLocationChannels()
|
||||
LocationChannelManager.shared.beginLiveRefresh()
|
||||
}
|
||||
.onDisappear {
|
||||
LocationChannelManager.shared.endLiveRefresh()
|
||||
sheetNotesCount = 0
|
||||
}
|
||||
.onChange(of: locationManager.availableChannels) { channels in
|
||||
if let current = channels.first(where: { $0.level == .building })?.geohash,
|
||||
notesGeohash != current {
|
||||
notesGeohash = current
|
||||
#if os(iOS)
|
||||
// Light taptic when geohash changes while the sheet is open
|
||||
let generator = UIImpactFeedbackGenerator(style: .light)
|
||||
generator.prepare()
|
||||
generator.impactOccurred()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
updateNotesCounterSubscription()
|
||||
if case .mesh = locationManager.selectedChannel,
|
||||
locationManager.permissionState == .authorized,
|
||||
LocationChannelManager.shared.availableChannels.isEmpty {
|
||||
LocationChannelManager.shared.refreshChannels()
|
||||
}
|
||||
}
|
||||
.onChange(of: locationManager.selectedChannel) { _ in
|
||||
updateNotesCounterSubscription()
|
||||
if case .mesh = locationManager.selectedChannel,
|
||||
locationManager.permissionState == .authorized,
|
||||
LocationChannelManager.shared.availableChannels.isEmpty {
|
||||
LocationChannelManager.shared.refreshChannels()
|
||||
}
|
||||
}
|
||||
.onChange(of: locationManager.availableChannels) { _ in updateNotesCounterSubscription() }
|
||||
.onChange(of: locationManager.permissionState) { _ in
|
||||
updateNotesCounterSubscription()
|
||||
if case .mesh = locationManager.selectedChannel,
|
||||
locationManager.permissionState == .authorized,
|
||||
LocationChannelManager.shared.availableChannels.isEmpty {
|
||||
LocationChannelManager.shared.refreshChannels()
|
||||
}
|
||||
}
|
||||
.alert("heads up", isPresented: $viewModel.showScreenshotPrivacyWarning) {
|
||||
Button("ok", role: .cancel) {}
|
||||
} message: {
|
||||
@@ -1399,6 +1515,26 @@ struct ContentView: View {
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Notes Counter Subscription Helper
|
||||
extension ContentView {
|
||||
private func updateNotesCounterSubscription() {
|
||||
switch locationManager.selectedChannel {
|
||||
case .mesh:
|
||||
// Ensure we have a fresh one-shot location fix so building geohash is current
|
||||
if locationManager.permissionState == .authorized {
|
||||
LocationChannelManager.shared.refreshChannels()
|
||||
}
|
||||
if let building = LocationChannelManager.shared.availableChannels.first(where: { $0.level == .building })?.geohash {
|
||||
LocationNotesCounter.shared.subscribe(geohash: building)
|
||||
} else {
|
||||
LocationNotesCounter.shared.cancel()
|
||||
}
|
||||
case .location:
|
||||
LocationNotesCounter.shared.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper Views
|
||||
|
||||
// Rounded payment chip button
|
||||
|
||||
Reference in New Issue
Block a user