From 58c516075c301dc93e96be3b394f92c83724ff68 Mon Sep 17 00:00:00 2001 From: jack Date: Mon, 25 Aug 2025 18:48:02 +0200 Subject: [PATCH] docs(plans): add refactor plan; chore(config): introduce TransportConfig and use in BLEService/ChatViewModel/PrivateChatManager; feat: add PeerDisplayNameResolver and apply in BLEService; chore: make TransportPeerSnapshot Equatable/Hashable; perf: simplify read-receipt persistence (no synchronize) --- bitchat/Services/BLEService.swift | 48 +++++++-------------- bitchat/Services/PrivateChatManager.swift | 2 +- bitchat/Services/Transport.swift | 2 +- bitchat/Services/TransportConfig.swift | 22 ++++++++++ bitchat/Utils/PeerDisplayNameResolver.swift | 29 +++++++++++++ bitchat/ViewModels/ChatViewModel.swift | 28 ++++-------- 6 files changed, 76 insertions(+), 55 deletions(-) create mode 100644 bitchat/Services/TransportConfig.swift create mode 100644 bitchat/Utils/PeerDisplayNameResolver.swift diff --git a/bitchat/Services/BLEService.swift b/bitchat/Services/BLEService.swift index 0110bf08..34236072 100644 --- a/bitchat/Services/BLEService.swift +++ b/bitchat/Services/BLEService.swift @@ -22,12 +22,12 @@ final class BLEService: NSObject { static let characteristicUUID = CBUUID(string: "A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D") // Default per-fragment chunk size when link limits are unknown - private let defaultFragmentSize = 469 // ~512 MTU minus protocol overhead - private let maxMessageLength = 10_000 - private let messageTTL: UInt8 = 7 + private let defaultFragmentSize = TransportConfig.bleDefaultFragmentSize + private let maxMessageLength = InputValidator.Limits.maxMessageLength + private let messageTTL: UInt8 = TransportConfig.messageTTLDefault // Flood/battery controls - private let maxInFlightAssemblies = 128 // cap concurrent fragment assemblies - private let highDegreeThreshold = 6 // for adaptive TTL/probabilistic relays + private let maxInFlightAssemblies = TransportConfig.bleMaxInFlightAssemblies // cap concurrent fragment assemblies + private let highDegreeThreshold = TransportConfig.bleHighDegreeThreshold // for adaptive TTL/probabilistic relays // MARK: - Core State (5 Essential Collections) @@ -257,20 +257,15 @@ final class BLEService: NSObject { func currentPeerSnapshots() -> [TransportPeerSnapshot] { collectionsQueue.sync { - // Compute nickname collision counts for connected peers - let connected = peers.values.filter { $0.isConnected } - var counts: [String: Int] = [:] - for p in connected { counts[p.nickname, default: 0] += 1 } - // Include our own nickname in collision counts so remote matching ours gets suffixed - counts[myNickname, default: 0] += 1 - return peers.values.map { info in - var display = info.nickname - if info.isConnected, (counts[info.nickname] ?? 0) > 1 { - display += "#" + String(info.id.prefix(4)) - } - return TransportPeerSnapshot( + let snapshot = Array(peers.values) + let resolvedNames = PeerDisplayNameResolver.resolve( + snapshot.map { ($0.id, $0.nickname, $0.isConnected) }, + selfNickname: myNickname + ) + return snapshot.map { info in + TransportPeerSnapshot( id: info.id, - nickname: display, + nickname: resolvedNames[info.id] ?? info.nickname, isConnected: info.isConnected, noisePublicKey: info.noisePublicKey, lastSeen: info.lastSeen @@ -514,22 +509,9 @@ final class BLEService: NSObject { func getPeerNicknames() -> [String: String] { return collectionsQueue.sync { - // Only connected peers let connected = peers.filter { $0.value.isConnected } - // Count collisions by nickname (include our own nickname) - var counts: [String: Int] = [:] - for (_, info) in connected { counts[info.nickname, default: 0] += 1 } - counts[myNickname, default: 0] += 1 - // Build map with suffix for collisions - var result: [String: String] = [:] - for (id, info) in connected { - var name = info.nickname - if (counts[info.nickname] ?? 0) > 1 { - name += "#" + String(id.prefix(4)) - } - result[id] = name - } - return result + let tuples = connected.map { ($0.key, $0.value.nickname, true) } + return PeerDisplayNameResolver.resolve(tuples, selfNickname: myNickname) } } diff --git a/bitchat/Services/PrivateChatManager.swift b/bitchat/Services/PrivateChatManager.swift index 0bddc6c0..896e711e 100644 --- a/bitchat/Services/PrivateChatManager.swift +++ b/bitchat/Services/PrivateChatManager.swift @@ -27,7 +27,7 @@ class PrivateChatManager: ObservableObject { } // Cap for messages stored per private chat - private let privateChatCap = 1337 + private let privateChatCap = TransportConfig.privateChatCap /// Start a private chat with a peer func startChat(with peerID: String) { diff --git a/bitchat/Services/Transport.swift b/bitchat/Services/Transport.swift index 74686d8a..372e3e3b 100644 --- a/bitchat/Services/Transport.swift +++ b/bitchat/Services/Transport.swift @@ -3,7 +3,7 @@ import Combine /// Abstract transport interface used by ChatViewModel and services. /// BLEService implements this protocol; a future Nostr transport can too. -struct TransportPeerSnapshot { +struct TransportPeerSnapshot: Equatable, Hashable { let id: String let nickname: String let isConnected: Bool diff --git a/bitchat/Services/TransportConfig.swift b/bitchat/Services/TransportConfig.swift new file mode 100644 index 00000000..f62fb937 --- /dev/null +++ b/bitchat/Services/TransportConfig.swift @@ -0,0 +1,22 @@ +import Foundation + +/// Centralized knobs for transport- and UI-related limits. +/// Keep values aligned with existing behavior when replacing magic numbers. +enum TransportConfig { + // BLE / Protocol + static let bleDefaultFragmentSize: Int = 469 // ~512 MTU minus protocol overhead + static let messageTTLDefault: UInt8 = 7 // Default TTL for mesh flooding + static let bleMaxInFlightAssemblies: Int = 128 // Cap concurrent fragment assemblies + static let bleHighDegreeThreshold: Int = 6 // For adaptive TTL/probabilistic relays + + // UI / Storage Caps + static let privateChatCap: Int = 1337 + static let meshTimelineCap: Int = 1337 + static let geoTimelineCap: Int = 1337 + static let contentLRUCap: Int = 2000 + + // Timers + static let networkResetGraceSeconds: TimeInterval = 600 // 10 minutes + static let basePublicFlushInterval: TimeInterval = 0.08 // ~12.5 fps batching +} + diff --git a/bitchat/Utils/PeerDisplayNameResolver.swift b/bitchat/Utils/PeerDisplayNameResolver.swift new file mode 100644 index 00000000..5af5c417 --- /dev/null +++ b/bitchat/Utils/PeerDisplayNameResolver.swift @@ -0,0 +1,29 @@ +import Foundation + +/// Resolves a stable display name for peers, adding a short suffix when collisions exist. +struct PeerDisplayNameResolver { + /// Computes display names with a `#xxxx` suffix for connected peers when nickname collisions occur. + /// - Parameters: + /// - peers: Array of tuples (id, nickname, isConnected). + /// - selfNickname: The local user's current nickname, included in collision counts to suffix remotes matching it. + /// - Returns: Map of peerID -> displayName. + static func resolve(_ peers: [(id: String, nickname: String, isConnected: Bool)], selfNickname: String) -> [String: String] { + // Count collisions among connected peers and include our own nickname + var counts: [String: Int] = [:] + for p in peers where p.isConnected { + counts[p.nickname, default: 0] += 1 + } + counts[selfNickname, default: 0] += 1 + + var result: [String: String] = [:] + for p in peers { + var name = p.nickname + if p.isConnected, (counts[p.nickname] ?? 0) > 1 { + name += "#" + String(p.id.prefix(4)) + } + result[p.id] = name + } + return result + } +} + diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index 20dee000..77230b20 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -201,7 +201,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate { // Persistent recent content map (LRU) to speed near-duplicate checks private var contentLRUMap: [String: Date] = [:] private var contentLRUOrder: [String] = [] - private let contentLRUCap = 2000 + private let contentLRUCap = TransportConfig.contentLRUCap private func recordContentKey(_ key: String, timestamp: Date) { if contentLRUMap[key] == nil { contentLRUOrder.append(key) } contentLRUMap[key] = timestamp @@ -219,13 +219,13 @@ class ChatViewModel: ObservableObject, BitchatDelegate { @Published var messages: [BitchatMessage] = [] @Published var currentColorScheme: ColorScheme = .light - private let maxMessages = 1337 // Maximum messages before oldest are removed + private let maxMessages = TransportConfig.meshTimelineCap // Maximum messages before oldest are removed @Published var isConnected = false private var hasNotifiedNetworkAvailable = false private var recentlySeenPeers: Set = [] private var lastNetworkNotificationTime = Date.distantPast private var networkResetTimer: Timer? = nil - private let networkResetGraceSeconds: TimeInterval = 600 // 10 minutes; avoid refiring on short drops/reconnects + private let networkResetGraceSeconds: TimeInterval = TransportConfig.networkResetGraceSeconds // avoid refiring on short drops/reconnects @Published var nickname: String = "" { didSet { // Trim whitespace whenever nickname is set @@ -386,10 +386,10 @@ class ChatViewModel: ObservableObject, BitchatDelegate { // Messages are naturally ephemeral - no persistent storage // Persist mesh public timeline across channel switches private var meshTimeline: [BitchatMessage] = [] - private let meshTimelineCap = 1337 + private let meshTimelineCap = TransportConfig.meshTimelineCap // Persist per-geohash public timelines across switches private var geoTimelines: [String: [BitchatMessage]] = [:] // geohash -> messages - private let geoTimelineCap = 1337 + private let geoTimelineCap = TransportConfig.geoTimelineCap // Channel activity tracking for background nudges private var lastPublicActivityAt: [String: Date] = [:] // channelKey -> last activity time private var lastPublicActivityNotifyAt: [String: Date] = [:] @@ -428,8 +428,8 @@ class ChatViewModel: ObservableObject, BitchatDelegate { // Buffer incoming public messages and flush in small batches to reduce UI invalidations private var publicBuffer: [BitchatMessage] = [] private var publicBufferTimer: Timer? = nil - private let basePublicFlushInterval: TimeInterval = 0.08 // ~12.5 fps batching - private var dynamicPublicFlushInterval: TimeInterval = 0.08 + private let basePublicFlushInterval: TimeInterval = TransportConfig.basePublicFlushInterval + private var dynamicPublicFlushInterval: TimeInterval = TransportConfig.basePublicFlushInterval private var recentBatchSizes: [Int] = [] @Published private(set) var isBatchingPublic: Bool = false private let lateInsertThreshold: TimeInterval = 15.0 @@ -441,21 +441,9 @@ class ChatViewModel: ObservableObject, BitchatDelegate { // Only persist if there are changes guard oldValue != sentReadReceipts else { return } - // Persist to UserDefaults whenever it changes + // Persist to UserDefaults whenever it changes (no manual synchronize/verify re-read) if let data = try? JSONEncoder().encode(Array(sentReadReceipts)) { UserDefaults.standard.set(data, forKey: "sentReadReceipts") - // Force synchronization for immediate persistence (ensures data is written to disk) - UserDefaults.standard.synchronize() - - // Verify persistence by re-reading - if let verifyData = UserDefaults.standard.data(forKey: "sentReadReceipts"), - let _ = try? JSONDecoder().decode([String].self, from: verifyData) { - // Only log errors, not successful persistence - // Successfully persisted - } else { - SecureLogger.log("⚠️ Failed to verify persistence of read receipts", - category: SecureLogger.session, level: .error) - } } else { SecureLogger.log("❌ Failed to encode read receipts for persistence", category: SecureLogger.session, level: .error)