mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 22:25:20 +00:00
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:
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user