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

This commit is contained in:
jack
2025-09-13 12:19:44 +02:00
parent 6cf6e3c420
commit 78f1bd3ad9
3 changed files with 53 additions and 45 deletions
+19 -33
View File
@@ -9,8 +9,7 @@ final class LocationNotesCounter: ObservableObject {
@Published private(set) var count: Int? = 0 @Published private(set) var count: Int? = 0
@Published private(set) var initialLoadComplete: Bool = false @Published private(set) var initialLoadComplete: Bool = false
// Support multiple subscriptions (e.g., building + parent block fallback) private var subscriptionID: String? = nil
private var subscriptionIDs: Set<String> = []
private var noteIDs = Set<String>() private var noteIDs = Set<String>()
private init() {} private init() {}
@@ -24,40 +23,27 @@ final class LocationNotesCounter: ObservableObject {
noteIDs.removeAll() noteIDs.removeAll()
initialLoadComplete = false initialLoadComplete = false
// Subscribe to both building and parent block to capture legacy notes // Subscribe only to the building geohash (precision 8)
var targets: [String] = [norm] let subID = "locnotes-count-\(norm)-\(UUID().uuidString.prefix(6))"
if norm.count >= 8 { subscriptionID = subID
let parent7 = String(norm.prefix(7)) let filter = NostrFilter.geohashNotes(norm, since: nil, limit: 500)
if parent7 != norm { targets.append(parent7) } let relays = GeoRelayDirectory.shared.closestRelays(toGeohash: norm, count: TransportConfig.nostrGeoRelayCount)
} NostrRelayManager.shared.subscribe(filter: filter, id: subID, relayUrls: relays, handler: { [weak self] event in
var pendingEOSE = Set<String>() guard let self = self else { return }
for target in targets { guard event.kind == NostrProtocol.EventKind.textNote.rawValue else { return }
let subID = "locnotes-count-\(target)-\(UUID().uuidString.prefix(6))" guard event.tags.contains(where: { $0.count >= 2 && $0[0].lowercased() == "g" && $0[1].lowercased() == norm }) else { return }
subscriptionIDs.insert(subID) if !self.noteIDs.contains(event.id) {
pendingEOSE.insert(subID) self.noteIDs.insert(event.id)
let filter = NostrFilter.geohashNotes(target, since: nil, limit: 500) self.count = self.noteIDs.count
let relays = GeoRelayDirectory.shared.closestRelays(toGeohash: target, count: TransportConfig.nostrGeoRelayCount) }
NostrRelayManager.shared.subscribe(filter: filter, id: subID, relayUrls: relays, handler: { [weak self] event in }, onEOSE: { [weak self] in
guard let self = self else { return } self?.initialLoadComplete = true
guard event.kind == NostrProtocol.EventKind.textNote.rawValue else { return } })
// Ensure matching g-tag for either building or parent block
let matches = event.tags.contains(where: { $0.count >= 2 && $0[0].lowercased() == "g" && targets.contains($0[1].lowercased()) })
guard matches else { return }
if !self.noteIDs.contains(event.id) {
self.noteIDs.insert(event.id)
self.count = self.noteIDs.count
}
}, onEOSE: { [weak self] in
guard let self = self else { return }
pendingEOSE.remove(subID)
if pendingEOSE.isEmpty { self.initialLoadComplete = true }
})
}
} }
func cancel() { func cancel() {
for sub in subscriptionIDs { NostrRelayManager.shared.unsubscribe(id: sub) } if let sub = subscriptionID { NostrRelayManager.shared.unsubscribe(id: sub) }
subscriptionIDs.removeAll() subscriptionID = nil
geohash = nil geohash = nil
count = 0 count = 0
noteIDs.removeAll() noteIDs.removeAll()
+1 -1
View File
@@ -858,7 +858,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
Task { @MainActor in Task { @MainActor in
self.torRestartPending = true self.torRestartPending = true
// Post only in geohash channels (queue if not active) // Post only in geohash channels (queue if not active)
self.addGeohashOnlySystemMessage("tor restarting to recover connectivity") self.addGeohashOnlySystemMessage("tor restarting to recover connectivity...")
} }
} }
+33 -11
View File
@@ -1095,7 +1095,7 @@ struct ContentView: View {
TextField("nickname", text: $viewModel.nickname) TextField("nickname", text: $viewModel.nickname)
.textFieldStyle(.plain) .textFieldStyle(.plain)
.font(.system(size: 14, design: .monospaced)) .font(.system(size: 14, design: .monospaced))
.frame(maxWidth: 100) .frame(maxWidth: 80)
.foregroundColor(textColor) .foregroundColor(textColor)
.focused($isNicknameFieldFocused) .focused($isNicknameFieldFocused)
.autocorrectionDisabled(true) .autocorrectionDisabled(true)
@@ -1188,14 +1188,12 @@ struct ContentView: View {
Image(systemName: "long.text.page.and.pencil") Image(systemName: "long.text.page.and.pencil")
.font(.system(size: 12)) .font(.system(size: 12))
.foregroundColor(Color(hue: 0.60, saturation: 0.85, brightness: 0.82)) .foregroundColor(Color(hue: 0.60, saturation: 0.85, brightness: 0.82))
if let c = notesCounter.count { .padding(.top, 1)
Text("\(c)") Text("\(notesCounter.count ?? 0)")
.font(.system(size: 11, design: .monospaced)) .font(.system(size: 11, design: .monospaced))
.foregroundColor(Color(hue: 0.60, saturation: 0.85, brightness: 0.82)) .foregroundColor(Color(hue: 0.60, saturation: 0.85, brightness: 0.82))
.lineLimit(1) .lineLimit(1)
.fixedSize(horizontal: true, vertical: false) .fixedSize(horizontal: true, vertical: false)
.alignmentGuide(.firstTextBaseline) { d in d[.firstTextBaseline] }
}
} }
.fixedSize(horizontal: true, vertical: false) .fixedSize(horizontal: true, vertical: false)
} }
@@ -1213,9 +1211,12 @@ struct ContentView: View {
.accessibilityHidden(true) .accessibilityHidden(true)
} }
.foregroundColor(headerCountColor) .foregroundColor(headerCountColor)
.lineLimit(1)
.fixedSize(horizontal: true, vertical: false)
// QR moved to the PEOPLE header in the sidebar when on mesh channel // QR moved to the PEOPLE header in the sidebar when on mesh channel
} }
.layoutPriority(3)
.onTapGesture { .onTapGesture {
withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) { withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) {
showSidebar.toggle() showSidebar.toggle()
@@ -1285,9 +1286,30 @@ struct ContentView: View {
LocationChannelManager.shared.endLiveRefresh() LocationChannelManager.shared.endLiveRefresh()
} }
} }
.onAppear { updateNotesCounterSubscription() } .onAppear {
.onChange(of: locationManager.selectedChannel) { _ in updateNotesCounterSubscription() } updateNotesCounterSubscription()
// Keep live updates running while on mesh to follow building changes
if case .mesh = locationManager.selectedChannel, locationManager.permissionState == .authorized {
LocationChannelManager.shared.beginLiveRefresh()
}
}
.onChange(of: locationManager.selectedChannel) { _ in
updateNotesCounterSubscription()
if case .mesh = locationManager.selectedChannel, locationManager.permissionState == .authorized {
LocationChannelManager.shared.beginLiveRefresh()
} else {
LocationChannelManager.shared.endLiveRefresh()
}
}
.onChange(of: locationManager.availableChannels) { _ in updateNotesCounterSubscription() } .onChange(of: locationManager.availableChannels) { _ in updateNotesCounterSubscription() }
.onChange(of: locationManager.permissionState) { _ in
updateNotesCounterSubscription()
if case .mesh = locationManager.selectedChannel, locationManager.permissionState == .authorized {
LocationChannelManager.shared.beginLiveRefresh()
} else {
LocationChannelManager.shared.endLiveRefresh()
}
}
.alert("heads up", isPresented: $viewModel.showScreenshotPrivacyWarning) { .alert("heads up", isPresented: $viewModel.showScreenshotPrivacyWarning) {
Button("ok", role: .cancel) {} Button("ok", role: .cancel) {}
} message: { } message: {