diff --git a/bitchat.xcodeproj/project.pbxproj b/bitchat.xcodeproj/project.pbxproj index f2453c9b..74d25e76 100644 --- a/bitchat.xcodeproj/project.pbxproj +++ b/bitchat.xcodeproj/project.pbxproj @@ -1112,7 +1112,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.3.4; + MARKETING_VERSION = 1.4.0; PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.ShareExtension; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1143,7 +1143,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.3.4; + MARKETING_VERSION = 1.4.0; PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat; PRODUCT_NAME = bitchat; SDKROOT = iphoneos; @@ -1198,7 +1198,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.3.4; + MARKETING_VERSION = 1.4.0; PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat; PRODUCT_NAME = bitchat; SDKROOT = iphoneos; @@ -1230,7 +1230,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.3.4; + MARKETING_VERSION = 1.4.0; PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat; PRODUCT_NAME = bitchat; REGISTER_APP_GROUPS = YES; @@ -1319,7 +1319,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.3.4; + MARKETING_VERSION = 1.4.0; PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat; PRODUCT_NAME = bitchat; REGISTER_APP_GROUPS = YES; @@ -1412,7 +1412,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.3.4; + MARKETING_VERSION = 1.4.0; PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.ShareExtension; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; diff --git a/bitchat/Nostr/NostrRelayManager.swift b/bitchat/Nostr/NostrRelayManager.swift index 0b94df13..2b8ec1f0 100644 --- a/bitchat/Nostr/NostrRelayManager.swift +++ b/bitchat/Nostr/NostrRelayManager.swift @@ -45,6 +45,14 @@ final class NostrRelayManager: ObservableObject { // Coalesce duplicate subscribe requests for the same id within a short window private var subscribeCoalesce: [String: Date] = [:] private var cancellables = Set() + + // Track EOSE per subscription to signal when initial stored events are done + private struct EOSETracker { + var pendingRelays: Set + var callback: () -> Void + var timer: Timer? + } + private var eoseTrackers: [String: EOSETracker] = [:] // Message queue for reliability // Pending sends held only for relays that are not yet connected. @@ -201,7 +209,8 @@ final class NostrRelayManager: ObservableObject { filter: NostrFilter, id: String = UUID().uuidString, relayUrls: [String]? = nil, - handler: @escaping (NostrEvent) -> Void + handler: @escaping (NostrEvent) -> Void, + onEOSE: (() -> Void)? = nil ) { // Coalesce rapid duplicate subscribe requests only if a handler already exists let now = Date() @@ -250,6 +259,20 @@ final class NostrRelayManager: ObservableObject { map[id] = messageString self.pendingSubscriptions[url] = map } + // Initialize EOSE tracking if requested + if let onEOSE = onEOSE { + var tracker = EOSETracker(pendingRelays: Set(urls), callback: onEOSE, timer: nil) + // Fallback timeout to avoid hanging if a relay never sends EOSE + tracker.timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { [weak self] _ in + guard let self = self else { return } + if let t = self.eoseTrackers[id] { + t.timer?.invalidate() + self.eoseTrackers.removeValue(forKey: id) + onEOSE() + } + } + eoseTrackers[id] = tracker + } SecureLogger.debug("📋 Queued subscription id=\(id) for \(urls.count) relay(s)", category: .session) // Ensure we actually have sockets opening to these relays so queued REQs can flush ensureConnections(to: urls) @@ -418,9 +441,17 @@ final class NostrRelayManager: ObservableObject { } else { SecureLogger.warning("⚠️ No handler for subscription \(subId)", category: .session) } - case .eose: - // No-op for now - break + case .eose(let subId): + if var tracker = eoseTrackers[subId] { + tracker.pendingRelays.remove(relayUrl) + if tracker.pendingRelays.isEmpty { + tracker.timer?.invalidate() + eoseTrackers.removeValue(forKey: subId) + tracker.callback() + } else { + eoseTrackers[subId] = tracker + } + } case .ok(let eventId, let success, let reason): if success { _ = Self.pendingGiftWrapIDs.remove(eventId) diff --git a/bitchat/Protocols/LocationChannel.swift b/bitchat/Protocols/LocationChannel.swift index 5aea0575..ac58946d 100644 --- a/bitchat/Protocols/LocationChannel.swift +++ b/bitchat/Protocols/LocationChannel.swift @@ -2,7 +2,6 @@ import Foundation /// Levels of location channels mapped to geohash precisions. enum GeohashChannelLevel: CaseIterable, Codable, Equatable { - case building case block case neighborhood case city @@ -12,7 +11,6 @@ enum GeohashChannelLevel: CaseIterable, Codable, Equatable { /// Geohash length used for this level. var precision: Int { switch self { - case .building: return 8 case .block: return 7 case .neighborhood: return 6 case .city: return 5 @@ -23,7 +21,6 @@ enum GeohashChannelLevel: CaseIterable, Codable, Equatable { var displayName: String { switch self { - case .building: return "Building" case .block: return "Block" case .neighborhood: return "Neighborhood" case .city: return "City" @@ -38,7 +35,6 @@ extension GeohashChannelLevel { let container = try decoder.singleValueContainer() if let raw = try? container.decode(String.self) { switch raw { - case "building": self = .building case "block": self = .block case "neighborhood": self = .neighborhood case "city": self = .city @@ -50,7 +46,6 @@ extension GeohashChannelLevel { } } else if let precision = try? container.decode(Int.self) { switch precision { - case 8: self = .building case 7: self = .block case 6: self = .neighborhood case 5: self = .city @@ -66,7 +61,6 @@ extension GeohashChannelLevel { func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() switch self { - case .building: try container.encode("building") case .block: try container.encode("block") case .neighborhood: try container.encode("neighborhood") case .city: try container.encode("city") diff --git a/bitchat/Services/LocationChannelManager.swift b/bitchat/Services/LocationChannelManager.swift index 5c188ebf..bff1c89f 100644 --- a/bitchat/Services/LocationChannelManager.swift +++ b/bitchat/Services/LocationChannelManager.swift @@ -291,12 +291,6 @@ final class LocationChannelManager: NSObject, CLLocationManagerDelegate, Observa } else if let locality = pm.locality, !locality.isEmpty { dict[.block] = locality } - // Building: prefer place name/street address without numbers where possible - if let name = pm.name, !name.isEmpty { - dict[.building] = name - } else if let thoroughfare = pm.thoroughfare, !thoroughfare.isEmpty { - dict[.building] = thoroughfare - } return dict } } diff --git a/bitchat/Services/LocationNotesCounter.swift b/bitchat/Services/LocationNotesCounter.swift index a4d776d9..90682eb4 100644 --- a/bitchat/Services/LocationNotesCounter.swift +++ b/bitchat/Services/LocationNotesCounter.swift @@ -7,6 +7,7 @@ final class LocationNotesCounter: ObservableObject { @Published private(set) var geohash: String? = nil @Published private(set) var count: Int? = nil + @Published private(set) var initialLoadComplete: Bool = false private var subscriptionID: String? = nil private var noteIDs = Set() @@ -20,12 +21,13 @@ final class LocationNotesCounter: ObservableObject { geohash = norm count = nil noteIDs.removeAll() + initialLoadComplete = false 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 + 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 } @@ -33,7 +35,9 @@ final class LocationNotesCounter: ObservableObject { self.noteIDs.insert(event.id) self.count = self.noteIDs.count } - } + }, onEOSE: { [weak self] in + self?.initialLoadComplete = true + }) } func cancel() { @@ -46,4 +50,3 @@ final class LocationNotesCounter: ObservableObject { noteIDs.removeAll() } } - diff --git a/bitchat/Services/LocationNotesManager.swift b/bitchat/Services/LocationNotesManager.swift index 222d4249..bbc9aab5 100644 --- a/bitchat/Services/LocationNotesManager.swift +++ b/bitchat/Services/LocationNotesManager.swift @@ -22,6 +22,7 @@ final class LocationNotesManager: ObservableObject { @Published private(set) var notes: [Note] = [] // reverse-chron sorted @Published private(set) var geohash: String + @Published private(set) var initialLoadComplete: Bool = false private var subscriptionID: String? init(geohash: String) { @@ -47,7 +48,8 @@ final class LocationNotesManager: ObservableObject { // For persistent notes, allow relays to return recent history without an aggressive time cutoff let filter = NostrFilter.geohashNotes(geohash, since: nil, limit: 200) let relays = GeoRelayDirectory.shared.closestRelays(toGeohash: geohash, count: TransportConfig.nostrGeoRelayCount) - NostrRelayManager.shared.subscribe(filter: filter, id: subID, relayUrls: relays) { [weak self] event in + initialLoadComplete = false + 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 tag @@ -58,7 +60,9 @@ final class LocationNotesManager: ObservableObject { let note = Note(id: event.id, pubkey: event.pubkey, content: event.content, createdAt: ts, nickname: nick) self.notes.append(note) self.notes.sort { $0.createdAt > $1.createdAt } - } + }, onEOSE: { [weak self] in + self?.initialLoadComplete = true + }) } /// Send a location note for the current geohash using the per-geohash identity.