From 78f1bd3ad912c102ec576f33a46596171ad0a13d Mon Sep 17 00:00:00 2001 From: jack Date: Sat, 13 Sep 2025 12:19:44 +0200 Subject: [PATCH] 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 --- bitchat/Services/LocationNotesCounter.swift | 52 ++++++++------------- bitchat/ViewModels/ChatViewModel.swift | 2 +- bitchat/Views/ContentView.swift | 44 ++++++++++++----- 3 files changed, 53 insertions(+), 45 deletions(-) diff --git a/bitchat/Services/LocationNotesCounter.swift b/bitchat/Services/LocationNotesCounter.swift index d4417028..0741d381 100644 --- a/bitchat/Services/LocationNotesCounter.swift +++ b/bitchat/Services/LocationNotesCounter.swift @@ -9,8 +9,7 @@ final class LocationNotesCounter: ObservableObject { @Published private(set) var count: Int? = 0 @Published private(set) var initialLoadComplete: Bool = false - // Support multiple subscriptions (e.g., building + parent block fallback) - private var subscriptionIDs: Set = [] + private var subscriptionID: String? = nil private var noteIDs = Set() private init() {} @@ -24,40 +23,27 @@ final class LocationNotesCounter: ObservableObject { noteIDs.removeAll() initialLoadComplete = false - // Subscribe to both building and parent block to capture legacy notes - var targets: [String] = [norm] - if norm.count >= 8 { - let parent7 = String(norm.prefix(7)) - if parent7 != norm { targets.append(parent7) } - } - var pendingEOSE = Set() - for target in targets { - let subID = "locnotes-count-\(target)-\(UUID().uuidString.prefix(6))" - subscriptionIDs.insert(subID) - pendingEOSE.insert(subID) - let filter = NostrFilter.geohashNotes(target, since: nil, limit: 500) - let relays = GeoRelayDirectory.shared.closestRelays(toGeohash: target, count: TransportConfig.nostrGeoRelayCount) - NostrRelayManager.shared.subscribe(filter: filter, id: subID, relayUrls: relays, handler: { [weak self] event in - guard let self = self else { return } - 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 } - }) - } + // Subscribe only to the building geohash (precision 8) + 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, handler: { [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 + } + }, onEOSE: { [weak self] in + self?.initialLoadComplete = true + }) } func cancel() { - for sub in subscriptionIDs { NostrRelayManager.shared.unsubscribe(id: sub) } - subscriptionIDs.removeAll() + if let sub = subscriptionID { NostrRelayManager.shared.unsubscribe(id: sub) } + subscriptionID = nil geohash = nil count = 0 noteIDs.removeAll() diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index ff537ec1..266e2b2e 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -858,7 +858,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { Task { @MainActor in self.torRestartPending = true // Post only in geohash channels (queue if not active) - self.addGeohashOnlySystemMessage("tor restarting to recover connectivity…") + self.addGeohashOnlySystemMessage("tor restarting to recover connectivity...") } } diff --git a/bitchat/Views/ContentView.swift b/bitchat/Views/ContentView.swift index faf60f33..8b10833d 100644 --- a/bitchat/Views/ContentView.swift +++ b/bitchat/Views/ContentView.swift @@ -1095,7 +1095,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) @@ -1188,14 +1188,12 @@ struct ContentView: View { Image(systemName: "long.text.page.and.pencil") .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)) - .lineLimit(1) - .fixedSize(horizontal: true, vertical: false) - .alignmentGuide(.firstTextBaseline) { d in d[.firstTextBaseline] } - } + .padding(.top, 1) + Text("\(notesCounter.count ?? 0)") + .font(.system(size: 11, design: .monospaced)) + .foregroundColor(Color(hue: 0.60, saturation: 0.85, brightness: 0.82)) + .lineLimit(1) + .fixedSize(horizontal: true, vertical: false) } .fixedSize(horizontal: true, vertical: false) } @@ -1213,9 +1211,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() @@ -1285,9 +1286,30 @@ struct ContentView: View { LocationChannelManager.shared.endLiveRefresh() } } - .onAppear { updateNotesCounterSubscription() } - .onChange(of: locationManager.selectedChannel) { _ in updateNotesCounterSubscription() } + .onAppear { + 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.permissionState) { _ in + updateNotesCounterSubscription() + if case .mesh = locationManager.selectedChannel, locationManager.permissionState == .authorized { + LocationChannelManager.shared.beginLiveRefresh() + } else { + LocationChannelManager.shared.endLiveRefresh() + } + } .alert("heads up", isPresented: $viewModel.showScreenshotPrivacyWarning) { Button("ok", role: .cancel) {} } message: {