From 54c7eba8cb474d24e783308a18426efac7042c9b Mon Sep 17 00:00:00 2001 From: jack <212554440+jackjackbits@users.noreply.github.com> Date: Sat, 26 Jul 2025 00:11:23 +0200 Subject: [PATCH] Improve code organization and documentation (#325) * Add MARK headers to improve code organization in major files * Reorganize peer management functions in BluetoothMeshService - Removed duplicate getCurrentPeerID(for:) function - Consolidated peer identity functions in Peer Identity Mapping section - Moved getPeerFingerprint(), getFingerprint(for:), isPeerIDOurs() to proper location - Moved getCurrentPeerIDForFingerprint() and getCurrentPeerIDs() from Message Sending section - Moved notifyPeerIDChange() to Peer Management section * Consolidate peer management functions in BluetoothMeshService - Moved getCachedPublicKey() and getCachedSigningKey() from Identity Cache Methods to Peer Connection Management - Moved getPeerNicknames() and getPeerRSSI() to Peer Connection Management section - Moved getAllConnectedPeerIDs(), notifyPeerListUpdate(), and cleanupStalePeers() to Peer Connection Management - Removed duplicate function declarations after consolidation - Improved code organization by grouping all peer-related functions together * Consolidate message handling functions in ChatViewModel - Moved handleHandshakeRequest() from floating location to Message Reception section - Moved trimMessagesIfNeeded() and trimPrivateChatMessagesIfNeeded() to Message Batching section - Improved code organization by grouping related message handling functions together - Removed unnecessary comments from trim functions * Improve ContentView organization with better documentation - Added descriptive comments for complex inline computations - Documented message extraction logic for private vs public chats - Documented peer data computation and sorting logic - Improved code readability by explaining complex operations inline - Note: Attempted to extract complex computations into helper functions, but SwiftUI scope limitations made inline documentation a better approach --------- Co-authored-by: jack --- bitchat/Protocols/BitchatProtocol.swift | 22 ++ bitchat/Services/BluetoothMeshService.swift | 255 ++++++++++++-------- bitchat/ViewModels/ChatViewModel.swift | 140 +++++++---- bitchat/Views/ContentView.swift | 30 +++ 4 files changed, 310 insertions(+), 137 deletions(-) diff --git a/bitchat/Protocols/BitchatProtocol.swift b/bitchat/Protocols/BitchatProtocol.swift index ce78b360..c772f795 100644 --- a/bitchat/Protocols/BitchatProtocol.swift +++ b/bitchat/Protocols/BitchatProtocol.swift @@ -9,6 +9,8 @@ import Foundation import CryptoKit +// MARK: - Message Padding + // Privacy-preserving padding utilities struct MessagePadding { // Standard block sizes for padding @@ -75,6 +77,8 @@ struct MessagePadding { } } +// MARK: - Message Types + enum MessageType: UInt8 { case announce = 0x01 case leave = 0x03 @@ -127,6 +131,8 @@ enum MessageType: UInt8 { } } +// MARK: - Handshake State + // Lazy handshake state tracking enum LazyHandshakeState { case none // No session, no handshake attempted @@ -136,11 +142,15 @@ enum LazyHandshakeState { case failed(Error) // Handshake failed } +// MARK: - Special Recipients + // Special recipient ID for broadcast messages struct SpecialRecipients { static let broadcast = Data(repeating: 0xFF, count: 8) // All 0xFF = broadcast } +// MARK: - Core Protocol Structures + struct BitchatPacket: Codable { let version: UInt8 let type: UInt8 @@ -197,6 +207,8 @@ struct BitchatPacket: Codable { } } +// MARK: - Delivery Acknowledgments + // Delivery acknowledgment structure struct DeliveryAck: Codable { let originalMessageID: String @@ -287,6 +299,8 @@ struct DeliveryAck: Codable { } } +// MARK: - Read Receipts + // Read receipt structure struct ReadReceipt: Codable { let originalMessageID: String @@ -371,6 +385,8 @@ struct ReadReceipt: Codable { } } +// MARK: - Handshake Requests + // Handshake request for pending messages struct HandshakeRequest: Codable { let requestID: String @@ -1031,6 +1047,8 @@ struct VersionAck: Codable { } } +// MARK: - Delivery Status + // Delivery status for messages enum DeliveryStatus: Codable, Equatable { case sending @@ -1058,6 +1076,8 @@ enum DeliveryStatus: Codable, Equatable { } } +// MARK: - Message Model + class BitchatMessage: Codable { let id: String let sender: String @@ -1120,6 +1140,8 @@ extension BitchatMessage: Equatable { } } +// MARK: - Delegate Protocol + protocol BitchatDelegate: AnyObject { func didReceiveMessage(_ message: BitchatMessage) func didConnectToPeer(_ peerID: String) diff --git a/bitchat/Services/BluetoothMeshService.swift b/bitchat/Services/BluetoothMeshService.swift index f80e56bd..5f3af822 100644 --- a/bitchat/Services/BluetoothMeshService.swift +++ b/bitchat/Services/BluetoothMeshService.swift @@ -65,27 +65,39 @@ enum PeerConnectionState: CustomStringConvertible { } class BluetoothMeshService: NSObject { + // MARK: - Constants + static let serviceUUID = CBUUID(string: "F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5C") static let characteristicUUID = CBUUID(string: "A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D") + // MARK: - Core Bluetooth Properties private var centralManager: CBCentralManager? private var peripheralManager: CBPeripheralManager? private var discoveredPeripherals: [CBPeripheral] = [] private var connectedPeripherals: [String: CBPeripheral] = [:] private var peripheralCharacteristics: [CBPeripheral: CBCharacteristic] = [:] + + // MARK: - Connection Tracking + private var lastConnectionTime: [String: Date] = [:] // Track when peers last connected private var lastSuccessfulMessageTime: [String: Date] = [:] // Track last successful message exchange private var lastHeardFromPeer: [String: Date] = [:] // Track last time we received ANY packet from peer + // MARK: - Peer Availability + // Peer availability tracking private var peerAvailabilityState: [String: Bool] = [:] // true = available, false = unavailable private let peerAvailabilityTimeout: TimeInterval = 30.0 // Mark unavailable after 30s of no response private var availabilityCheckTimer: Timer? + // MARK: - Peripheral Management + private var characteristic: CBMutableCharacteristic? private var subscribedCentrals: [CBCentral] = [] - // Thread-safe collections using concurrent queues + + // MARK: - Thread-Safe Collections + private let collectionsQueue = DispatchQueue(label: "bitchat.collections", attributes: .concurrent) private var peerNicknames: [String: String] = [:] private var activePeers: Set = [] // Track all active peers @@ -93,6 +105,8 @@ class BluetoothMeshService: NSObject { private var peripheralRSSI: [String: NSNumber] = [:] // Track RSSI by peripheral ID during discovery private var rssiRetryCount: [String: Int] = [:] // Track RSSI retry attempts per peripheral + // MARK: - Encryption Queues + // Per-peer encryption queues to prevent nonce desynchronization private var peerEncryptionQueues: [String: DispatchQueue] = [:] private let encryptionQueuesLock = NSLock() @@ -115,10 +129,14 @@ class BluetoothMeshService: NSObject { private let identityCacheTTL: TimeInterval = 3600.0 // 1 hour TTL private var identityCacheTimestamps: [String: Date] = [:] // Track when entries were cached + // MARK: - Delegates and Services + weak var delegate: BitchatDelegate? private let noiseService = NoiseEncryptionService() private let handshakeCoordinator = NoiseHandshakeCoordinator() + // MARK: - Protocol Version Negotiation + // Protocol version negotiation state private var versionNegotiationState: [String: VersionNegotiationState] = [:] private var negotiatedVersions: [String: UInt8] = [:] // peerID -> agreed version @@ -147,29 +165,6 @@ class BluetoothMeshService: NSObject { return noiseService } - // MARK: - Identity Cache Methods - - func getCachedPublicKey(for peerID: String) -> Data? { - return collectionsQueue.sync { - // Check if cache entry exists and is not expired - if let timestamp = identityCacheTimestamps[peerID], - Date().timeIntervalSince(timestamp) < identityCacheTTL { - return peerPublicKeyCache[peerID] - } - return nil - } - } - - func getCachedSigningKey(for peerID: String) -> Data? { - return collectionsQueue.sync { - // Check if cache entry exists and is not expired - if let timestamp = identityCacheTimestamps[peerID], - Date().timeIntervalSince(timestamp) < identityCacheTTL { - return peerSigningKeyCache[peerID] - } - return nil - } - } private func cleanExpiredIdentityCache() { collectionsQueue.async(flags: .barrier) { [weak self] in @@ -314,6 +309,8 @@ class BluetoothMeshService: NSObject { category: SecureLogger.session, level: .debug) } } + // MARK: - Message Processing + private let messageQueue = DispatchQueue(label: "bitchat.messageQueue", attributes: .concurrent) // Concurrent queue with barriers // Message state tracking for better duplicate detection @@ -338,6 +335,8 @@ class BluetoothMeshService: NSObject { private var recentIdentityAnnounces = [String: Date]() // peerID -> last seen time private let identityAnnounceDuplicateWindow: TimeInterval = 30.0 // 30 second window + // MARK: - Network State Management + private let maxTTL: UInt8 = 7 // Maximum hops for long-distance delivery private var announcedToPeers = Set() // Track which peers we've announced to private var announcedPeers = Set() // Track peers who have already been announced @@ -353,6 +352,8 @@ class BluetoothMeshService: NSObject { private var peerLastSeenTimestamps = LRUCache(maxSize: 100) // Bounded cache for peer timestamps private var cleanupTimer: Timer? // Timer to clean up stale peers + // MARK: - Store-and-Forward Cache + // Store-and-forward message cache private struct StoredMessage { let packet: BitchatPacket @@ -372,6 +373,8 @@ class BluetoothMeshService: NSObject { private let lastMessageFromPeer = LRUCache(maxSize: 100) // Bounded cache private let processedNoiseMessages = BoundedSet(maxSize: 1000) // Bounded cache + // MARK: - Battery and Performance Optimization + // Battery and range optimizations private var scanDutyCycleTimer: Timer? private var isActivelyScanning = true @@ -400,18 +403,26 @@ class BluetoothMeshService: NSObject { // Pending private messages waiting for handshake private var pendingPrivateMessages: [String: [(content: String, recipientNickname: String, messageID: String)]] = [:] + // MARK: - Noise Protocol State + // Noise session state tracking for lazy handshakes private var noiseSessionStates: [String: LazyHandshakeState] = [:] + // MARK: - Cover Traffic + // Cover traffic for privacy private var coverTrafficTimer: Timer? private let coverTrafficPrefix = "☂DUMMY☂" // Prefix to identify dummy messages after decryption private var lastCoverTrafficTime = Date() + // MARK: - Connection State + // Connection state tracking private var peerConnectionStates: [String: PeerConnectionState] = [:] private let connectionStateQueue = DispatchQueue(label: "chat.bitchat.connectionState", attributes: .concurrent) + // MARK: - Protocol ACK Tracking + // Protocol-level ACK tracking private var pendingAcks: [String: (packet: BitchatPacket, timestamp: Date, retries: Int)] = [:] private let ackTimeout: TimeInterval = 5.0 // 5 seconds to receive ACK @@ -421,10 +432,14 @@ class BluetoothMeshService: NSObject { private var connectionKeepAliveTimer: Timer? // Timer to send keepalive pings private let keepAliveInterval: TimeInterval = 20.0 // Send keepalive every 20 seconds + // MARK: - Timing and Delays + // Timing randomization for privacy (now with exponential distribution) private let minMessageDelay: TimeInterval = 0.01 // 10ms minimum for faster sync private let maxMessageDelay: TimeInterval = 0.1 // 100ms maximum for faster sync + // MARK: - RSSI Management + // Dynamic RSSI threshold based on network size (smart compromise) private var dynamicRSSIThreshold: Int { let peerCount = activePeers.count @@ -437,6 +452,8 @@ class BluetoothMeshService: NSObject { } } + // MARK: - Fragment Handling + // Fragment handling with security limits private var incomingFragments: [String: [Int: Data]] = [:] // fragmentID -> [index: data] private var fragmentMetadata: [String: (originalType: UInt8, totalFragments: Int, timestamp: Date)] = [:] @@ -444,9 +461,11 @@ class BluetoothMeshService: NSObject { private let maxConcurrentFragmentSessions = 20 // Limit concurrent fragment sessions to prevent DoS private let fragmentTimeout: TimeInterval = 30 // 30 seconds timeout for incomplete fragments + // MARK: - Peer Identity + var myPeerID: String - // ===== SCALING OPTIMIZATIONS ===== + // MARK: - Scaling Optimizations // Connection pooling private var connectionPool: [String: CBPeripheral] = [:] @@ -457,6 +476,8 @@ class BluetoothMeshService: NSObject { private let maxConnectionAttempts = 3 private let baseBackoffInterval: TimeInterval = 1.0 + // MARK: - Peripheral Mapping + // Simplified peripheral mapping system private struct PeripheralMapping { let peripheral: CBPeripheral @@ -523,20 +544,28 @@ class BluetoothMeshService: NSObject { return nil } + // MARK: - Probabilistic Flooding + // Probabilistic flooding private var relayProbability: Double = 1.0 // Start at 100%, decrease with peer count private let minRelayProbability: Double = 0.4 // Minimum 40% relay chance - ensures coverage + // MARK: - Message Aggregation + // Message aggregation private var pendingMessages: [(message: BitchatPacket, destination: String?)] = [] private var aggregationTimer: Timer? private var aggregationWindow: TimeInterval = 0.1 // 100ms window private let maxAggregatedMessages = 5 + // MARK: - Bloom Filter + // Optimized Bloom filter for efficient duplicate detection private var messageBloomFilter = OptimizedBloomFilter(expectedItems: 2000, falsePositiveRate: 0.01) private var bloomFilterResetTimer: Timer? + // MARK: - Network Size Estimation + // Network size estimation private var estimatedNetworkSize: Int { return max(activePeers.count, connectedPeripherals.count) @@ -578,11 +607,15 @@ class BluetoothMeshService: NSObject { } } + // MARK: - Relay Cancellation + // Relay cancellation mechanism to prevent duplicate relays private var pendingRelays: [String: DispatchWorkItem] = [:] // messageID -> relay task private let pendingRelaysLock = NSLock() private let relayCancellationWindow: TimeInterval = 0.05 // 50ms window to detect other relays + // MARK: - Memory Management + // Global memory limits to prevent unbounded growth private let maxPendingPrivateMessages = 100 private let maxCachedMessagesSentToPeer = 1000 @@ -593,6 +626,8 @@ class BluetoothMeshService: NSObject { private var acknowledgedPackets = Set() private let acknowledgedPacketsLock = NSLock() + // MARK: - Rate Limiting + // Rate limiting for flood control with separate limits for different message types private var messageRateLimiter: [String: [Date]] = [:] // peerID -> recent message timestamps private var protocolMessageRateLimiter: [String: [Date]] = [:] // peerID -> protocol msg timestamps @@ -603,11 +638,13 @@ class BluetoothMeshService: NSObject { private let maxTotalMessagesPerMinute = 2000 // Increased for legitimate use private var totalMessageTimestamps: [Date] = [] + // MARK: - BLE Advertisement + // BLE advertisement for lightweight presence private var advertisementData: [String: Any] = [:] private var isAdvertising = false - // ===== MESSAGE AGGREGATION ===== + // MARK: - Message Aggregation Implementation private func startAggregationTimer() { aggregationTimer?.invalidate() @@ -673,12 +710,36 @@ class BluetoothMeshService: NSObject { // Removed getPublicKeyFingerprint - no longer needed with Noise + // MARK: - Peer Identity Mapping + // Get peer's fingerprint (replaces getPeerPublicKey) func getPeerFingerprint(_ peerID: String) -> String? { return noiseService.getPeerFingerprint(peerID) } - // MARK: - Peer Identity Mapping + // Get fingerprint for a peer ID + func getFingerprint(for peerID: String) -> String? { + return collectionsQueue.sync { + peerIDToFingerprint[peerID] + } + } + + // Check if a peer ID belongs to us (current or previous) + func isPeerIDOurs(_ peerID: String) -> Bool { + if peerID == myPeerID { + return true + } + + // Check if it's our previous ID within grace period + if let previousID = previousPeerID, + peerID == previousID, + let rotationTime = rotationTimestamp, + Date().timeIntervalSince(rotationTime) < rotationGracePeriod { + return true + } + + return false + } // Update peer identity binding when receiving announcements func updatePeerBinding(_ newPeerID: String, fingerprint: String, binding: PeerIdentityBinding) { @@ -773,38 +834,69 @@ class BluetoothMeshService: NSObject { } } - // Get current peer ID for a fingerprint - func getCurrentPeerID(for fingerprint: String) -> String? { + // Public method to get current peer ID for a fingerprint + func getCurrentPeerIDForFingerprint(_ fingerprint: String) -> String? { return collectionsQueue.sync { - fingerprintToPeerID[fingerprint] + return fingerprintToPeerID[fingerprint] } } - // Get fingerprint for a peer ID - func getFingerprint(for peerID: String) -> String? { + // Public method to get all current peer IDs for known fingerprints + func getCurrentPeerIDs() -> [String: String] { return collectionsQueue.sync { - peerIDToFingerprint[peerID] + return fingerprintToPeerID } } - // Check if a peer ID belongs to us (current or previous) - func isPeerIDOurs(_ peerID: String) -> Bool { - if peerID == myPeerID { - return true + // Notify delegate when peer ID changes + private func notifyPeerIDChange(oldPeerID: String, newPeerID: String, fingerprint: String) { + DispatchQueue.main.async { [weak self] in + // Remove old peer ID from active peers and announcedPeers + self?.collectionsQueue.sync(flags: .barrier) { + _ = self?.activePeers.remove(oldPeerID) + // Don't pre-insert the new peer ID - let the announce packet handle it + // This ensures the connect message logic works properly + } + + // Also remove from announcedPeers so the new ID can trigger a connect message + self?.announcedPeers.remove(oldPeerID) + + // Update peer list + self?.notifyPeerListUpdate(immediate: true) + + // Don't send disconnect/connect messages for peer ID rotation + // The peer didn't actually disconnect, they just rotated their ID + // This prevents confusing messages like "3a7e1c2c0d8943b9 disconnected" + + // Instead, notify the delegate about the peer ID change if needed + // (Could add a new delegate method for this in the future) + } + } + + // MARK: - Peer Connection Management + + func getCachedPublicKey(for peerID: String) -> Data? { + return collectionsQueue.sync { + // Check if cache entry exists and is not expired + if let timestamp = identityCacheTimestamps[peerID], + Date().timeIntervalSince(timestamp) < identityCacheTTL { + return peerPublicKeyCache[peerID] + } + return nil + } + } + + func getCachedSigningKey(for peerID: String) -> Data? { + return collectionsQueue.sync { + // Check if cache entry exists and is not expired + if let timestamp = identityCacheTimestamps[peerID], + Date().timeIntervalSince(timestamp) < identityCacheTTL { + return peerSigningKeyCache[peerID] + } + return nil } - - // Check if it's our previous ID within grace period - if let previousID = previousPeerID, - peerID == previousID, - let rotationTime = rotationTimestamp, - Date().timeIntervalSince(rotationTime) < rotationGracePeriod { - return true - } - - return false } - // Update peer connection state private func updatePeerConnectionState(_ peerID: String, state: PeerConnectionState) { connectionStateQueue.async(flags: .barrier) { [weak self] in guard let self = self else { return } @@ -956,6 +1048,8 @@ class BluetoothMeshService: NSObject { rotationLocked = false } + // MARK: - Initialization + override init() { // Generate ephemeral peer ID for each session to prevent tracking self.myPeerID = "" @@ -1080,6 +1174,8 @@ class BluetoothMeshService: NSObject { #endif } + // MARK: - Deinitialization and Cleanup + deinit { cleanup() scanDutyCycleTimer?.invalidate() @@ -1148,6 +1244,8 @@ class BluetoothMeshService: NSObject { lastHeardFromPeer.removeAll() } + // MARK: - Service Management + func startServices() { // Starting services // Start both central and peripheral services @@ -1176,6 +1274,8 @@ class BluetoothMeshService: NSObject { } } + // MARK: - Message Sending + func sendBroadcastAnnounce() { guard let vm = delegate as? ChatViewModel else { return } @@ -1370,46 +1470,6 @@ class BluetoothMeshService: NSObject { } } - // Public method to get current peer ID for a fingerprint - func getCurrentPeerIDForFingerprint(_ fingerprint: String) -> String? { - return collectionsQueue.sync { - return fingerprintToPeerID[fingerprint] - } - } - - // Public method to get all current peer IDs for known fingerprints - func getCurrentPeerIDs() -> [String: String] { - return collectionsQueue.sync { - return fingerprintToPeerID - } - } - - // Notify delegate when peer ID changes - private func notifyPeerIDChange(oldPeerID: String, newPeerID: String, fingerprint: String) { - DispatchQueue.main.async { [weak self] in - // Remove old peer ID from active peers and announcedPeers - self?.collectionsQueue.sync(flags: .barrier) { - _ = self?.activePeers.remove(oldPeerID) - // Don't pre-insert the new peer ID - let the announce packet handle it - // This ensures the connect message logic works properly - } - - // Also remove from announcedPeers so the new ID can trigger a connect message - self?.announcedPeers.remove(oldPeerID) - - // Update peer list - self?.notifyPeerListUpdate(immediate: true) - - // Don't send disconnect/connect messages for peer ID rotation - // The peer didn't actually disconnect, they just rotated their ID - // This prevents confusing messages like "3a7e1c2c0d8943b9 disconnected" - - // Instead, notify the delegate about the peer ID change if needed - // (Could add a new delegate method for this in the future) - } - } - - func sendDeliveryAck(_ ack: DeliveryAck, to recipientID: String) { // Use per-peer encryption queue to prevent nonce desynchronization let encryptionQueue = getEncryptionQueue(for: recipientID) @@ -1589,13 +1649,6 @@ class BluetoothMeshService: NSObject { broadcastPacket(packet) } - - func getPeerNicknames() -> [String: String] { - return collectionsQueue.sync { - return peerNicknames - } - } - // Get Noise session state for UI display func getNoiseSessionState(for peerID: String) -> LazyHandshakeState { return collectionsQueue.sync { @@ -1634,6 +1687,12 @@ class BluetoothMeshService: NSObject { initiateNoiseHandshake(with: peerID) } + func getPeerNicknames() -> [String: String] { + return collectionsQueue.sync { + return peerNicknames + } + } + func getPeerRSSI() -> [String: NSNumber] { var rssiValues = peerRSSI @@ -3602,6 +3661,8 @@ class BluetoothMeshService: NSObject { } // End of BluetoothMeshService class extension BluetoothMeshService: CBCentralManagerDelegate { + // MARK: - CBCentralManagerDelegate + func centralManagerDidUpdateState(_ central: CBCentralManager) { // Central manager state updated switch central.state { @@ -3968,6 +4029,8 @@ extension BluetoothMeshService: CBCentralManagerDelegate { } extension BluetoothMeshService: CBPeripheralDelegate { + // MARK: - CBPeripheralDelegate + func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { if let error = error { SecureLogger.log("Error discovering services: \(error)", @@ -4146,6 +4209,8 @@ extension BluetoothMeshService: CBPeripheralDelegate { } extension BluetoothMeshService: CBPeripheralManagerDelegate { + // MARK: - CBPeripheralManagerDelegate + func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) { // Peripheral manager state updated switch peripheral.state { diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index ce85d8d4..5f930d1d 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -16,10 +16,14 @@ import UIKit #endif class ChatViewModel: ObservableObject { + // MARK: - Published Properties + @Published var messages: [BitchatMessage] = [] private let maxMessages = 1337 // Maximum messages before oldest are removed @Published var connectedPeers: [String] = [] + // MARK: - Message Batching Properties + // Message batching for performance private var pendingMessages: [BitchatMessage] = [] private var pendingPrivateMessages: [String: [BitchatMessage]] = [:] // peerID -> messages @@ -44,6 +48,8 @@ class ChatViewModel: ObservableObject { @Published var autocompleteRange: NSRange? = nil @Published var selectedAutocompleteIndex: Int = 0 + // MARK: - Autocomplete Properties + // Autocomplete optimization private let mentionRegex = try? NSRegularExpression(pattern: "@([a-zA-Z0-9_]*)$", options: []) private var cachedNicknames: [String] = [] @@ -52,18 +58,26 @@ class ChatViewModel: ObservableObject { // Temporary property to fix compilation @Published var showPasswordPrompt = false + // MARK: - Services and Storage + var meshService = BluetoothMeshService() private let userDefaults = UserDefaults.standard private let nicknameKey = "bitchat.nickname" + // MARK: - Caches + // Caches for expensive computations private var rssiColorCache: [String: Color] = [:] // key: "\(rssi)_\(isDark)" private var encryptionStatusCache: [String: EncryptionStatus] = [:] // key: peerID + // MARK: - Social Features + @Published var favoritePeers: Set = [] // Now stores public key fingerprints instead of peer IDs private var peerIDToPublicKeyFingerprint: [String: String] = [:] // Maps ephemeral peer IDs to persistent fingerprints private var blockedUsers: Set = [] // Stores public key fingerprints of blocked users + // MARK: - Encryption and Security + // Noise Protocol encryption status @Published var peerEncryptionStatus: [String: EncryptionStatus] = [:] // peerID -> encryption status @Published var verifiedFingerprints: Set = [] // Set of verified fingerprints @@ -71,12 +85,16 @@ class ChatViewModel: ObservableObject { // Messages are naturally ephemeral - no persistent storage + // MARK: - Message Delivery Tracking + // Delivery tracking private var deliveryTrackerCancellable: AnyCancellable? // Track sent read receipts to avoid duplicates private var sentReadReceipts: Set = [] // messageID set + // MARK: - Initialization + init() { loadNickname() loadFavorites() @@ -174,6 +192,8 @@ class ChatViewModel: ObservableObject { #endif } + // MARK: - Deinitialization + deinit { // Clean up timer messageBatchTimer?.invalidate() @@ -182,6 +202,8 @@ class ChatViewModel: ObservableObject { userDefaults.synchronize() } + // MARK: - Nickname Management + private func loadNickname() { if let savedNickname = userDefaults.string(forKey: nicknameKey) { // Trim whitespace when loading @@ -213,6 +235,8 @@ class ChatViewModel: ObservableObject { saveNickname() } + // MARK: - Favorites Management + private func loadFavorites() { // Load favorites from secure storage favoritePeers = SecureIdentityStateManager.shared.getFavorites() @@ -223,6 +247,8 @@ class ChatViewModel: ObservableObject { // This method is kept for compatibility } + // MARK: - Blocked Users Management + private func loadBlockedUsers() { // Load blocked users from secure storage let allIdentities = SecureIdentityStateManager.shared.getAllSocialIdentities() @@ -275,6 +301,8 @@ class ChatViewModel: ObservableObject { return SecureIdentityStateManager.shared.isFavorite(fingerprint: fp) } + // MARK: - Public Key and Identity Management + // Called when we receive a peer's public key func registerPeerPublicKey(peerID: String, publicKeyData: Data) { // Create a fingerprint from the public key (full SHA256, not truncated) @@ -379,6 +407,8 @@ class ChatViewModel: ObservableObject { } } + // MARK: - Message Sending + func sendMessage(_ content: String) { guard !content.isEmpty else { return } @@ -479,6 +509,8 @@ class ChatViewModel: ObservableObject { meshService.sendPrivateMessage(content, to: peerID, recipientNickname: recipientNickname, messageID: message.id) } + // MARK: - Private Chat Management + func startPrivateChat(with peerID: String) { let peerNickname = meshService.getPeerNicknames()[peerID] ?? "unknown" @@ -581,6 +613,8 @@ class ChatViewModel: ObservableObject { selectedPrivateChatFingerprint = nil } + // MARK: - Message Retry Handling + @objc private func handleRetryMessage(_ notification: Notification) { guard let messageID = notification.userInfo?["messageID"] as? String else { return } @@ -605,6 +639,8 @@ class ChatViewModel: ObservableObject { } } + // MARK: - App Lifecycle + @objc private func appDidBecomeActive() { // When app becomes active, send read receipts for visible private chat if let peerID = selectedPrivateChatPeer { @@ -798,6 +834,8 @@ class ChatViewModel: ObservableObject { } + // MARK: - Emergency Functions + // PANIC: Emergency data clearing for activist safety func panicClearAllData() { // Flush any pending messages immediately before clearing @@ -868,6 +906,8 @@ class ChatViewModel: ObservableObject { + // MARK: - Formatting Helpers + func formatTimestamp(_ date: Date) -> String { let formatter = DateFormatter() formatter.dateFormat = "HH:mm:ss" @@ -909,6 +949,8 @@ class ChatViewModel: ObservableObject { return color } + // MARK: - Autocomplete + func updateAutocomplete(for text: String, cursorPosition: Int) { // Quick early exit for empty text guard cursorPosition > 0 else { @@ -1001,6 +1043,8 @@ class ChatViewModel: ObservableObject { return range.location + nickname.count + 2 } + // MARK: - Message Formatting + func getSenderColor(for message: BitchatMessage, colorScheme: ColorScheme) -> Color { let isDark = colorScheme == .dark let primaryColor = isDark ? Color.green : Color(red: 0, green: 0.5, blue: 0) @@ -1390,7 +1434,8 @@ class ChatViewModel: ObservableObject { rssiColorCache.removeAll() } - // Trim messages to keep only the most recent maxMessages + // MARK: - Message Batching + private func trimMessagesIfNeeded() { if messages.count > maxMessages { let removeCount = messages.count - maxMessages @@ -1398,7 +1443,6 @@ class ChatViewModel: ObservableObject { } } - // Trim private chat messages to keep only the most recent maxMessages private func trimPrivateChatMessagesIfNeeded(for peerID: String) { if let count = privateChats[peerID]?.count, count > maxMessages { let removeCount = count - maxMessages @@ -1406,8 +1450,6 @@ class ChatViewModel: ObservableObject { } } - // MARK: - Message Batching - private func addMessageToBatch(_ message: BitchatMessage) { pendingMessages.append(message) scheduleBatchFlush() @@ -1512,6 +1554,8 @@ class ChatViewModel: ObservableObject { } } + // MARK: - Fingerprint Management + func showFingerprint(for peerID: String) { showingFingerprintFor = peerID } @@ -1646,48 +1690,14 @@ class ChatViewModel: ObservableObject { } } } - - func handleHandshakeRequest(from peerID: String, nickname: String, pendingCount: UInt8) { - // Create a notification message - let notificationMessage = BitchatMessage( - sender: "system", - content: "📨 \(nickname) wants to send you \(pendingCount) message\(pendingCount == 1 ? "" : "s"). Open the conversation to receive.", - timestamp: Date(), - isRelay: false, - originalSender: nil, - isPrivate: false, - recipientNickname: nil, - senderPeerID: "system", - mentions: nil - ) - - // Add to messages - messages.append(notificationMessage) - trimMessagesIfNeeded() - - // Show system notification - if let fingerprint = getFingerprint(for: peerID) { - let isFavorite = favoritePeers.contains(fingerprint) - if isFavorite { - // Send favorite notification - NotificationService.shared.sendPrivateMessageNotification( - from: nickname, - message: "\(pendingCount) message\(pendingCount == 1 ? "" : "s") pending", - peerID: peerID - ) - } else { - // Send regular notification - NotificationService.shared.sendMentionNotification( - from: nickname, - message: "\(pendingCount) message\(pendingCount == 1 ? "" : "s") pending. Open conversation to receive." - ) - } - } - } } +// MARK: - BitchatDelegate + extension ChatViewModel: BitchatDelegate { + // MARK: - Command Handling + private func handleCommand(_ command: String) { let parts = command.split(separator: " ") guard let cmd = parts.first else { return } @@ -2042,6 +2052,46 @@ extension ChatViewModel: BitchatDelegate { } } + // MARK: - Message Reception + + func handleHandshakeRequest(from peerID: String, nickname: String, pendingCount: UInt8) { + // Create a notification message + let notificationMessage = BitchatMessage( + sender: "system", + content: "📨 \(nickname) wants to send you \(pendingCount) message\(pendingCount == 1 ? "" : "s"). Open the conversation to receive.", + timestamp: Date(), + isRelay: false, + originalSender: nil, + isPrivate: false, + recipientNickname: nil, + senderPeerID: "system", + mentions: nil + ) + + // Add to messages + messages.append(notificationMessage) + trimMessagesIfNeeded() + + // Show system notification + if let fingerprint = getFingerprint(for: peerID) { + let isFavorite = favoritePeers.contains(fingerprint) + if isFavorite { + // Send favorite notification + NotificationService.shared.sendPrivateMessageNotification( + from: nickname, + message: "\(pendingCount) message\(pendingCount == 1 ? "" : "s") pending", + peerID: peerID + ) + } else { + // Send regular notification + NotificationService.shared.sendMentionNotification( + from: nickname, + message: "\(pendingCount) message\(pendingCount == 1 ? "" : "s") pending. Open conversation to receive." + ) + } + } + } + func didReceiveMessage(_ message: BitchatMessage) { @@ -2342,6 +2392,8 @@ extension ChatViewModel: BitchatDelegate { #endif } + // MARK: - Peer Connection Events + func didConnectToPeer(_ peerID: String) { isConnected = true @@ -2435,6 +2487,8 @@ extension ChatViewModel: BitchatDelegate { } } + // MARK: - Helper Methods + private func parseMentions(from content: String) -> [String] { let pattern = "@([a-zA-Z0-9_]+)" let regex = try? NSRegularExpression(pattern: pattern, options: []) @@ -2461,6 +2515,8 @@ extension ChatViewModel: BitchatDelegate { return SecureIdentityStateManager.shared.isFavorite(fingerprint: fingerprint) } + // MARK: - Delivery Tracking + func didReceiveDeliveryAck(_ ack: DeliveryAck) { // Find the message and update its delivery status updateMessageDeliveryStatus(ack.originalMessageID, status: .delivered(to: ack.recipientNickname, at: ack.timestamp)) diff --git a/bitchat/Views/ContentView.swift b/bitchat/Views/ContentView.swift index 1bb00549..893d46a7 100644 --- a/bitchat/Views/ContentView.swift +++ b/bitchat/Views/ContentView.swift @@ -8,6 +8,8 @@ import SwiftUI +// MARK: - Supporting Types + // Pre-computed peer data for performance struct PeerDisplayData: Identifiable { let id: String @@ -19,6 +21,8 @@ struct PeerDisplayData: Identifiable { let encryptionStatus: EncryptionStatus } +// MARK: - Lazy Link Preview + // Lazy loading wrapper for link previews struct LazyLinkPreviewView: View { let url: URL @@ -44,7 +48,11 @@ struct LazyLinkPreviewView: View { } } +// MARK: - Main Content View + struct ContentView: View { + // MARK: - Properties + @EnvironmentObject var viewModel: ChatViewModel @State private var messageText = "" @State private var textFieldSelection: NSRange? = nil @@ -66,6 +74,8 @@ struct ContentView: View { @State private var scrollThrottleTimer: Timer? @State private var autocompleteDebounceTimer: Timer? + // MARK: - Computed Properties + private var backgroundColor: Color { colorScheme == .dark ? Color.black : Color.white } @@ -78,6 +88,8 @@ struct ContentView: View { colorScheme == .dark ? Color.green.opacity(0.8) : Color(red: 0, green: 0.5, blue: 0).opacity(0.8) } + // MARK: - Body + var body: some View { GeometryReader { geometry in ZStack { @@ -221,10 +233,13 @@ struct ContentView: View { } } + // MARK: - Message List View + private func messagesView(privatePeer: String?) -> some View { ScrollViewReader { proxy in ScrollView { VStack(alignment: .leading, spacing: 0) { + // Extract messages based on context (private or public chat) let messages: [BitchatMessage] = { if let privatePeer = privatePeer { let msgs = viewModel.getPrivateChatMessages(for: privatePeer) @@ -355,6 +370,8 @@ struct ContentView: View { } } + // MARK: - Input View + private var inputView: some View { VStack(spacing: 0) { // @mentions autocomplete @@ -544,11 +561,15 @@ struct ContentView: View { } } + // MARK: - Actions + private func sendMessage() { viewModel.sendMessage(messageText) messageText = "" } + // MARK: - Sidebar View + private var sidebarView: some View { HStack(spacing: 0) { // Grey vertical bar for visual continuity @@ -594,6 +615,7 @@ struct ContentView: View { .foregroundColor(secondaryTextColor) .padding(.horizontal) } else { + // Extract peer data for display let peerNicknames = viewModel.meshService.getPeerNicknames() let peerRSSI = viewModel.meshService.getPeerRSSI() let myPeerID = viewModel.meshService.myPeerID @@ -939,6 +961,8 @@ struct ContentView: View { } +// MARK: - Helper Views + // Helper view for rendering message content with clickable hashtags struct MessageContentView: View { let message: BitchatMessage @@ -993,6 +1017,8 @@ struct MessageContentView: View { .textSelection(.enabled) } + // MARK: - Helper Methods + private func buildTextSegments() -> [(text: String, type: String)] { var segments: [(text: String, type: String)] = [] let content = message.content @@ -1052,6 +1078,8 @@ struct DeliveryStatusView: View { let status: DeliveryStatus let colorScheme: ColorScheme + // MARK: - Computed Properties + private var textColor: Color { colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0) } @@ -1060,6 +1088,8 @@ struct DeliveryStatusView: View { colorScheme == .dark ? Color.green.opacity(0.8) : Color(red: 0, green: 0.5, blue: 0).opacity(0.8) } + // MARK: - Body + var body: some View { switch status { case .sending: