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