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

This commit is contained in:
jack
2025-09-13 01:12:49 +02:00
parent bef4a30ea7
commit 52bdf6745b
3 changed files with 86 additions and 4 deletions
+7 -1
View File
@@ -43,6 +43,8 @@
0481A3AA2E74D28800FC845E /* LocationNotesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3A82E74D28800FC845E /* LocationNotesView.swift */; };
0481A3AC2E74D29400FC845E /* LocationNotesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3AB2E74D29400FC845E /* LocationNotesManager.swift */; };
0481A3AD2E74D29400FC845E /* LocationNotesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3AB2E74D29400FC845E /* LocationNotesManager.swift */; };
0481A3AF2E74E06300FC845E /* LocationNotesCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3AE2E74E06300FC845E /* LocationNotesCounter.swift */; };
0481A3B02E74E06300FC845E /* LocationNotesCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3AE2E74E06300FC845E /* LocationNotesCounter.swift */; };
048A4BE72E5CCCC300162C4A /* TransportConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 048A4BE62E5CCCC300162C4A /* TransportConfig.swift */; };
048A4BE82E5CCCC300162C4A /* TransportConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 048A4BE62E5CCCC300162C4A /* TransportConfig.swift */; };
048A4BE92E5CCCC300162C4B /* TransportConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 048A4BE62E5CCCC300162C4A /* TransportConfig.swift */; };
@@ -240,6 +242,7 @@
0481A38F2E734CAE00FC845E /* CommandProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandProcessorTests.swift; sourceTree = "<group>"; };
0481A3A82E74D28800FC845E /* LocationNotesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationNotesView.swift; sourceTree = "<group>"; };
0481A3AB2E74D29400FC845E /* LocationNotesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationNotesManager.swift; sourceTree = "<group>"; };
0481A3AE2E74E06300FC845E /* LocationNotesCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationNotesCounter.swift; sourceTree = "<group>"; };
048A4BE62E5CCCC300162C4A /* TransportConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportConfig.swift; sourceTree = "<group>"; };
048A4C272E5FCD6600162C4A /* GeohashBookmarksStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeohashBookmarksStore.swift; sourceTree = "<group>"; };
048A4C2A2E5FCE0300162C4A /* GeohashBookmarksStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeohashBookmarksStoreTests.swift; sourceTree = "<group>"; };
@@ -594,8 +597,9 @@
D98A3186D7E4C72E35BDF7FE /* Services */ = {
isa = PBXGroup;
children = (
0481A3AB2E74D29400FC845E /* LocationNotesManager.swift */,
0481A3452E6D869F00FC845E /* Tor */,
0481A3AE2E74E06300FC845E /* LocationNotesCounter.swift */,
0481A3AB2E74D29400FC845E /* LocationNotesManager.swift */,
048A4C272E5FCD6600162C4A /* GeohashBookmarksStore.swift */,
048A4BE62E5CCCC300162C4A /* TransportConfig.swift */,
AA77BB10CC22DD33EE44FF55 /* VerificationService.swift */,
@@ -836,6 +840,7 @@
047502B92E560F690083520F /* RelayController.swift in Sources */,
6E7761E21C99F28AE2F9BE5F /* BitchatApp.swift in Sources */,
84E3F9B64FB7FB4A140BD0A8 /* BitchatPeer.swift in Sources */,
0481A3B02E74E06300FC845E /* LocationNotesCounter.swift in Sources */,
923027D6F2F417AFA2488127 /* BitchatProtocol.swift in Sources */,
D450CF41F207BDE1A1AAA56E /* ChatViewModel.swift in Sources */,
B0CA7796B2B2AC2B33F84548 /* CompressionUtil.swift in Sources */,
@@ -903,6 +908,7 @@
047502BA2E560F690083520F /* RelayController.swift in Sources */,
10E68BB889356219189E38EC /* BitchatApp.swift in Sources */,
84D13329AB7EE1D65A37438A /* BitchatPeer.swift in Sources */,
0481A3AF2E74E06300FC845E /* LocationNotesCounter.swift in Sources */,
6DE056E1EE9850E9FBF50157 /* BitchatProtocol.swift in Sources */,
7576A357B278E5733E9D9F33 /* ChatViewModel.swift in Sources */,
7DCA0DBCB8884E3B31C7BCE3 /* CompressionUtil.swift in Sources */,
@@ -0,0 +1,49 @@
import Foundation
/// Lightweight background counter for location notes (kind 1) at block-level geohash.
@MainActor
final class LocationNotesCounter: ObservableObject {
static let shared = LocationNotesCounter()
@Published private(set) var geohash: String? = nil
@Published private(set) var count: Int? = nil
private var subscriptionID: String? = nil
private var noteIDs = Set<String>()
private init() {}
func subscribe(geohash gh: String) {
let norm = gh.lowercased()
if geohash == norm { return }
cancel()
geohash = norm
count = nil
noteIDs.removeAll()
let subID = "locnotes-count-\(norm)-\(UUID().uuidString.prefix(6))"
subscriptionID = subID
let filter = NostrFilter.geohashNotes(norm, since: nil, limit: 500)
let relays = GeoRelayDirectory.shared.closestRelays(toGeohash: norm, count: TransportConfig.nostrGeoRelayCount)
NostrRelayManager.shared.subscribe(filter: filter, id: subID, relayUrls: relays) { [weak self] event in
guard let self = self else { return }
guard event.kind == NostrProtocol.EventKind.textNote.rawValue else { return }
guard event.tags.contains(where: { $0.count >= 2 && $0[0].lowercased() == "g" && $0[1].lowercased() == norm }) else { return }
if !self.noteIDs.contains(event.id) {
self.noteIDs.insert(event.id)
self.count = self.noteIDs.count
}
}
}
func cancel() {
if let sub = subscriptionID {
NostrRelayManager.shared.unsubscribe(id: sub)
}
subscriptionID = nil
geohash = nil
count = nil
noteIDs.removeAll()
}
}
+30 -3
View File
@@ -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
@@ -1156,9 +1157,16 @@ struct ContentView: View {
notesGeohash = LocationChannelManager.shared.availableChannels.first(where: { $0.level == .block })?.geohash
showLocationNotes = true
}) {
Image(systemName: "note.text")
.font(.system(size: 12))
.foregroundColor(Color(hue: 0.60, saturation: 0.85, brightness: 0.82))
HStack(spacing: 4) {
Image(systemName: "note.text")
.font(.system(size: 12))
.foregroundColor(Color(hue: 0.60, saturation: 0.85, brightness: 0.82))
if let c = notesCounter.count {
Text("\(c)")
.font(.system(size: 11, design: .monospaced))
.foregroundColor(Color(hue: 0.60, saturation: 0.85, brightness: 0.82))
}
}
}
.buttonStyle(.plain)
.accessibilityLabel("Location notes for this place")
@@ -1269,6 +1277,9 @@ struct ContentView: View {
}
}
}
.onAppear { updateNotesCounterSubscription() }
.onChange(of: locationManager.selectedChannel) { _ in updateNotesCounterSubscription() }
.onChange(of: locationManager.availableChannels) { _ in updateNotesCounterSubscription() }
.alert("heads up", isPresented: $viewModel.showScreenshotPrivacyWarning) {
Button("ok", role: .cancel) {}
} message: {
@@ -1467,6 +1478,22 @@ struct ContentView: View {
}
// MARK: - Notes Counter Subscription Helper
extension ContentView {
private func updateNotesCounterSubscription() {
switch locationManager.selectedChannel {
case .mesh:
if let block = LocationChannelManager.shared.availableChannels.first(where: { $0.level == .block })?.geohash {
LocationNotesCounter.shared.subscribe(geohash: block)
} else {
LocationNotesCounter.shared.cancel()
}
case .location:
LocationNotesCounter.shared.cancel()
}
}
}
// MARK: - Helper Views
// Rounded payment chip button