From 52bdf6745b71d71b908cd226894b895f5791f037 Mon Sep 17 00:00:00 2001 From: jack Date: Sat, 13 Sep 2025 01:12:49 +0200 Subject: [PATCH] Location notes: add background counter service and show count next to notes icon on mesh; auto-subscribe to current block geohash --- bitchat.xcodeproj/project.pbxproj | 8 +++- bitchat/Services/LocationNotesCounter.swift | 49 +++++++++++++++++++++ bitchat/Views/ContentView.swift | 33 ++++++++++++-- 3 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 bitchat/Services/LocationNotesCounter.swift diff --git a/bitchat.xcodeproj/project.pbxproj b/bitchat.xcodeproj/project.pbxproj index ebeca1c4..f2453c9b 100644 --- a/bitchat.xcodeproj/project.pbxproj +++ b/bitchat.xcodeproj/project.pbxproj @@ -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 = ""; }; 0481A3A82E74D28800FC845E /* LocationNotesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationNotesView.swift; sourceTree = ""; }; 0481A3AB2E74D29400FC845E /* LocationNotesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationNotesManager.swift; sourceTree = ""; }; + 0481A3AE2E74E06300FC845E /* LocationNotesCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationNotesCounter.swift; sourceTree = ""; }; 048A4BE62E5CCCC300162C4A /* TransportConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportConfig.swift; sourceTree = ""; }; 048A4C272E5FCD6600162C4A /* GeohashBookmarksStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeohashBookmarksStore.swift; sourceTree = ""; }; 048A4C2A2E5FCE0300162C4A /* GeohashBookmarksStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeohashBookmarksStoreTests.swift; sourceTree = ""; }; @@ -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 */, diff --git a/bitchat/Services/LocationNotesCounter.swift b/bitchat/Services/LocationNotesCounter.swift new file mode 100644 index 00000000..a4d776d9 --- /dev/null +++ b/bitchat/Services/LocationNotesCounter.swift @@ -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() + + 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() + } +} + diff --git a/bitchat/Views/ContentView.swift b/bitchat/Views/ContentView.swift index fe9d621d..934f76c4 100644 --- a/bitchat/Views/ContentView.swift +++ b/bitchat/Views/ContentView.swift @@ -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