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 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)
+160 -95
View File
@@ -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<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 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<String>() // Track which peers we've announced to
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 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<String, Date>(maxSize: 100) // Bounded cache
private let processedNoiseMessages = BoundedSet<String>(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<String>()
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 {
+98 -42
View File
@@ -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<String> = [] // 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<String> = [] // 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<String> = [] // 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<String> = [] // 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))
+30
View File
@@ -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: