diff --git a/bitchat/Nostr/GeoRelayDirectory.swift b/bitchat/Nostr/GeoRelayDirectory.swift index c55a3bed..a0dd0afd 100644 --- a/bitchat/Nostr/GeoRelayDirectory.swift +++ b/bitchat/Nostr/GeoRelayDirectory.swift @@ -33,13 +33,32 @@ final class GeoRelayDirectory { /// Returns up to `count` relay URLs (wss://) closest to the given coordinate. func closestRelays(toLat lat: Double, lon: Double, count: Int = 5) -> [String] { - guard !entries.isEmpty else { return [] } - let sorted = entries - .sorted { a, b in - haversineKm(lat, lon, a.lat, a.lon) < haversineKm(lat, lon, b.lat, b.lon) + guard !entries.isEmpty, count > 0 else { return [] } + + if entries.count <= count { + return entries + .sorted { a, b in + haversineKm(lat, lon, a.lat, a.lon) < haversineKm(lat, lon, b.lat, b.lon) + } + .map { "wss://\($0.host)" } + } + + var best: [(entry: Entry, distance: Double)] = [] + best.reserveCapacity(count) + + for entry in entries { + let distance = haversineKm(lat, lon, entry.lat, entry.lon) + if best.count < count { + let idx = best.firstIndex { $0.distance > distance } ?? best.count + best.insert((entry, distance), at: idx) + } else if let worstDistance = best.last?.distance, distance < worstDistance { + let idx = best.firstIndex { $0.distance > distance } ?? best.count + best.insert((entry, distance), at: idx) + best.removeLast() } - .prefix(count) - return sorted.map { "wss://\($0.host)" } + } + + return best.map { "wss://\($0.entry.host)" } } // MARK: - Remote Fetch diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index 4fc17af0..54132da5 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -197,20 +197,37 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // Persistent recent content map (LRU) to speed near-duplicate checks private var contentLRUMap: [String: Date] = [:] private var contentLRUOrder: [String] = [] + private var contentLRUHead = 0 private let contentLRUCap = TransportConfig.contentLRUCap private func recordContentKey(_ key: String, timestamp: Date) { if contentLRUMap[key] == nil { contentLRUOrder.append(key) } contentLRUMap[key] = timestamp - if contentLRUOrder.count > contentLRUCap { - let overflow = contentLRUOrder.count - contentLRUCap - for _ in 0.. contentLRUCap else { return } + + let overflow = activeCount - contentLRUCap + for _ in 0.. String? { + guard contentLRUHead < contentLRUOrder.count else { return nil } + let victim = contentLRUOrder[contentLRUHead] + contentLRUHead += 1 + + // Periodically compact the backing storage to avoid unbounded growth. + if contentLRUHead >= 32 && contentLRUHead * 2 >= contentLRUOrder.count { + contentLRUOrder.removeFirst(contentLRUHead) + contentLRUHead = 0 + } + return victim + } // MARK: - Published Properties @Published var messages: [BitchatMessage] = [] @@ -348,6 +365,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // PeerManager replaced by UnifiedPeerService private var processedNostrEvents = Set() // Simple deduplication private var processedNostrEventOrder: [String] = [] + private var processedNostrEventHead = 0 private let maxProcessedNostrEvents = TransportConfig.uiProcessedNostrEventsCap private let userDefaults = UserDefaults.standard private let keychain: KeychainManagerProtocol @@ -2168,16 +2186,31 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { private func recordProcessedEvent(_ id: String) { processedNostrEvents.insert(id) processedNostrEventOrder.append(id) - if processedNostrEventOrder.count > maxProcessedNostrEvents { - let overflow = processedNostrEventOrder.count - maxProcessedNostrEvents - for _ in 0.. maxProcessedNostrEvents else { return } + + let overflow = activeCount - maxProcessedNostrEvents + for _ in 0.. String? { + guard processedNostrEventHead < processedNostrEventOrder.count else { return nil } + let value = processedNostrEventOrder[processedNostrEventHead] + processedNostrEventHead += 1 + + if processedNostrEventHead >= 32 && processedNostrEventHead * 2 >= processedNostrEventOrder.count { + processedNostrEventOrder.removeFirst(processedNostrEventHead) + processedNostrEventHead = 0 + } + return value + } /// Sends an encrypted private message to a specific peer. /// - Parameters: @@ -4826,11 +4859,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { private func parseMentions(from content: String) -> [String] { // Allow optional disambiguation suffix '#abcd' for duplicate nicknames - let pattern = "@([\\p{L}0-9_]+(?:#[a-fA-F0-9]{4})?)" - let regex = try? NSRegularExpression(pattern: pattern, options: []) + let regex = Regexes.mention let nsContent = content as NSString let nsLen = nsContent.length - let matches = regex?.matches(in: content, options: [], range: NSRange(location: 0, length: nsLen)) ?? [] + let matches = regex.matches(in: content, options: [], range: NSRange(location: 0, length: nsLen)) var mentions: [String] = [] let peerNicknames = meshService.getPeerNicknames()