Merge pull request #308 from permissionlesstech/bluetooth-efficiency-optimization

Fix Bluetooth disconnect messages and blank chat rows
This commit is contained in:
jack
2025-07-23 21:36:53 +02:00
committed by GitHub
2 changed files with 341 additions and 29 deletions
+328 -24
View File
@@ -107,6 +107,13 @@ class BluetoothMeshService: NSObject {
private var rotationLocked = false // Prevent rotation during critical operations private var rotationLocked = false // Prevent rotation during critical operations
private var rotationTimer: Timer? // Timer for scheduled rotations private var rotationTimer: Timer? // Timer for scheduled rotations
// MARK: - Identity Cache
// In-memory cache for peer public keys to avoid keychain lookups
private var peerPublicKeyCache: [String: Data] = [:] // PeerID -> Public Key Data
private var peerSigningKeyCache: [String: Data] = [:] // PeerID -> Signing Key Data
private let identityCacheTTL: TimeInterval = 3600.0 // 1 hour TTL
private var identityCacheTimestamps: [String: Date] = [:] // Track when entries were cached
weak var delegate: BitchatDelegate? weak var delegate: BitchatDelegate?
private let noiseService = NoiseEncryptionService() private let noiseService = NoiseEncryptionService()
private let handshakeCoordinator = NoiseHandshakeCoordinator() private let handshakeCoordinator = NoiseHandshakeCoordinator()
@@ -115,9 +122,197 @@ class BluetoothMeshService: NSObject {
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
// MARK: - Write Queue for Disconnected Peripherals
private struct QueuedWrite {
let data: Data
let peripheralID: String
let peerID: String?
let timestamp: Date
let retryCount: Int
}
private var writeQueue: [String: [QueuedWrite]] = [:] // PeripheralID -> Queue of writes
private let writeQueueLock = NSLock()
private let maxWriteQueueSize = 50 // Max queued writes per peripheral
private let maxWriteRetries = 3
private let writeQueueTTL: TimeInterval = 60.0 // Expire queued writes after 1 minute
private var writeQueueTimer: Timer? // Timer for processing expired writes
// MARK: - Connection Pooling
private let maxConnectedPeripherals = 10 // Limit simultaneous connections
private let maxScanningDuration: TimeInterval = 5.0 // Stop scanning after 5 seconds to save battery
func getNoiseService() -> NoiseEncryptionService { func getNoiseService() -> NoiseEncryptionService {
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() {
collectionsQueue.async(flags: .barrier) { [weak self] in
guard let self = self else { return }
let now = Date()
var expiredPeerIDs: [String] = []
for (peerID, timestamp) in self.identityCacheTimestamps {
if now.timeIntervalSince(timestamp) >= self.identityCacheTTL {
expiredPeerIDs.append(peerID)
}
}
for peerID in expiredPeerIDs {
self.peerPublicKeyCache.removeValue(forKey: peerID)
self.peerSigningKeyCache.removeValue(forKey: peerID)
self.identityCacheTimestamps.removeValue(forKey: peerID)
}
if !expiredPeerIDs.isEmpty {
SecureLogger.log("Cleaned \(expiredPeerIDs.count) expired identity cache entries",
category: SecureLogger.session, level: .debug)
}
}
}
// MARK: - Write Queue Management
private func writeToPeripheral(_ data: Data, peripheral: CBPeripheral, characteristic: CBCharacteristic, peerID: String? = nil) {
let peripheralID = peripheral.identifier.uuidString
// Double check the peripheral state to avoid API misuse
guard peripheral.state == .connected else {
// Queue write for disconnected peripheral
queueWrite(data: data, peripheralID: peripheralID, peerID: peerID)
return
}
// Verify characteristic is valid and writable
guard characteristic.properties.contains(.write) || characteristic.properties.contains(.writeWithoutResponse) else {
SecureLogger.log("Characteristic does not support writing for peripheral \(peripheralID)",
category: SecureLogger.session, level: .warning)
return
}
// Direct write if connected
let writeType: CBCharacteristicWriteType = data.count > 512 ? .withResponse : .withoutResponse
peripheral.writeValue(data, for: characteristic, type: writeType)
// Update activity tracking
updatePeripheralActivity(peripheralID)
}
private func queueWrite(data: Data, peripheralID: String, peerID: String?) {
writeQueueLock.lock()
defer { writeQueueLock.unlock() }
// Check backpressure - drop oldest if queue is full
var queue = writeQueue[peripheralID] ?? []
if queue.count >= maxWriteQueueSize {
// Remove oldest entries
let removeCount = queue.count - maxWriteQueueSize + 1
queue.removeFirst(removeCount)
SecureLogger.log("Write queue full for \(peripheralID), dropped \(removeCount) oldest writes",
category: SecureLogger.session, level: .warning)
}
let queuedWrite = QueuedWrite(
data: data,
peripheralID: peripheralID,
peerID: peerID,
timestamp: Date(),
retryCount: 0
)
queue.append(queuedWrite)
writeQueue[peripheralID] = queue
SecureLogger.log("Queued write for disconnected peripheral \(peripheralID), queue size: \(queue.count)",
category: SecureLogger.session, level: .debug)
}
private func processWriteQueue(for peripheral: CBPeripheral) {
guard peripheral.state == .connected,
let characteristic = peripheralCharacteristics[peripheral] else { return }
let peripheralID = peripheral.identifier.uuidString
writeQueueLock.lock()
let queue = writeQueue[peripheralID] ?? []
writeQueue[peripheralID] = []
writeQueueLock.unlock()
if !queue.isEmpty {
SecureLogger.log("Processing \(queue.count) queued writes for \(peripheralID)",
category: SecureLogger.session, level: .info)
}
// Process queued writes with small delay between them
for (index, queuedWrite) in queue.enumerated() {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.01) { [weak self] in
guard let self = self,
peripheral.state == .connected,
characteristic.properties.contains(.write) || characteristic.properties.contains(.writeWithoutResponse) else {
return
}
let writeType: CBCharacteristicWriteType = queuedWrite.data.count > 512 ? .withResponse : .withoutResponse
peripheral.writeValue(queuedWrite.data, for: characteristic, type: writeType)
// Update activity tracking
self.updatePeripheralActivity(peripheralID)
}
}
}
private func cleanExpiredWriteQueues() {
writeQueueLock.lock()
defer { writeQueueLock.unlock() }
let now = Date()
var expiredWrites = 0
for (peripheralID, queue) in writeQueue {
let filteredQueue = queue.filter { write in
now.timeIntervalSince(write.timestamp) < writeQueueTTL
}
expiredWrites += queue.count - filteredQueue.count
if filteredQueue.isEmpty {
writeQueue.removeValue(forKey: peripheralID)
} else {
writeQueue[peripheralID] = filteredQueue
}
}
if expiredWrites > 0 {
SecureLogger.log("Cleaned \(expiredWrites) expired queued writes",
category: SecureLogger.session, level: .debug)
}
}
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
private let processedMessages = BoundedSet<String>(maxSize: 1000) // Bounded to prevent memory growth private let processedMessages = BoundedSet<String>(maxSize: 1000) // Bounded to prevent memory growth
private let maxTTL: UInt8 = 7 // Maximum hops for long-distance delivery private let maxTTL: UInt8 = 7 // Maximum hops for long-distance delivery
@@ -214,6 +409,8 @@ class BluetoothMeshService: NSObject {
private var connectionPool: [String: CBPeripheral] = [:] private var connectionPool: [String: CBPeripheral] = [:]
private var connectionAttempts: [String: Int] = [:] private var connectionAttempts: [String: Int] = [:]
private var connectionBackoff: [String: TimeInterval] = [:] private var connectionBackoff: [String: TimeInterval] = [:]
private var lastActivityByPeripheralID: [String: Date] = [:] // Track last activity for LRU
private var peerIDByPeripheralID: [String: String] = [:] // Map peripheral ID to peer ID
private let maxConnectionAttempts = 3 private let maxConnectionAttempts = 3
private let baseBackoffInterval: TimeInterval = 1.0 private let baseBackoffInterval: TimeInterval = 1.0
@@ -304,10 +501,9 @@ class BluetoothMeshService: NSObject {
self.broadcastPacket(messages[0]) self.broadcastPacket(messages[0])
} else if let dest = destination, } else if let dest = destination,
let peripheral = self.connectedPeripherals[dest], let peripheral = self.connectedPeripherals[dest],
peripheral.state == .connected,
let characteristic = self.peripheralCharacteristics[peripheral] { let characteristic = self.peripheralCharacteristics[peripheral] {
if let data = messages[0].toBinaryData() { if let data = messages[0].toBinaryData() {
peripheral.writeValue(data, for: characteristic, type: .withoutResponse) self.writeToPeripheral(data, peripheral: peripheral, characteristic: characteristic, peerID: dest)
} }
} }
} else { } else {
@@ -323,7 +519,7 @@ class BluetoothMeshService: NSObject {
peripheral.state == .connected, peripheral.state == .connected,
let characteristic = self?.peripheralCharacteristics[peripheral] { let characteristic = self?.peripheralCharacteristics[peripheral] {
if let data = message.toBinaryData() { if let data = message.toBinaryData() {
peripheral.writeValue(data, for: characteristic, type: .withoutResponse) self?.writeToPeripheral(data, peripheral: peripheral, characteristic: characteristic, peerID: dest)
} }
} }
} }
@@ -397,6 +593,11 @@ class BluetoothMeshService: NSObject {
self.fingerprintToPeerID[fingerprint] = newPeerID self.fingerprintToPeerID[fingerprint] = newPeerID
self.peerIdentityBindings[fingerprint] = binding self.peerIdentityBindings[fingerprint] = binding
// Cache public keys to avoid keychain lookups
self.peerPublicKeyCache[newPeerID] = binding.publicKey
self.peerSigningKeyCache[newPeerID] = binding.signingPublicKey
self.identityCacheTimestamps[newPeerID] = Date()
// Also update nickname from binding // Also update nickname from binding
self.peerNicknames[newPeerID] = binding.nickname self.peerNicknames[newPeerID] = binding.nickname
@@ -643,6 +844,16 @@ class BluetoothMeshService: NSObject {
self?.checkPeerAvailability() self?.checkPeerAvailability()
} }
// Start write queue cleanup timer (every 30 seconds)
writeQueueTimer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { [weak self] _ in
self?.cleanExpiredWriteQueues()
}
// Start identity cache cleanup timer (every hour)
Timer.scheduledTimer(withTimeInterval: 3600.0, repeats: true) { [weak self] _ in
self?.cleanExpiredIdentityCache()
}
// Log handshake states periodically for debugging and clean up stale states // Log handshake states periodically for debugging and clean up stale states
#if DEBUG #if DEBUG
Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { [weak self] _ in Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { [weak self] _ in
@@ -1239,8 +1450,7 @@ class BluetoothMeshService: NSObject {
if let peripheral = connectedPeripherals[peerID], if let peripheral = connectedPeripherals[peerID],
peripheral.state == .connected, peripheral.state == .connected,
let characteristic = peripheral.services?.first(where: { $0.uuid == BluetoothMeshService.serviceUUID })?.characteristics?.first(where: { $0.uuid == BluetoothMeshService.characteristicUUID }) { let characteristic = peripheral.services?.first(where: { $0.uuid == BluetoothMeshService.serviceUUID })?.characteristics?.first(where: { $0.uuid == BluetoothMeshService.characteristicUUID }) {
let writeType: CBCharacteristicWriteType = characteristic.properties.contains(.write) ? .withResponse : .withoutResponse writeToPeripheral(data, peripheral: peripheral, characteristic: characteristic, peerID: peerID)
peripheral.writeValue(data, for: characteristic, type: writeType)
} else { } else {
} }
} else { } else {
@@ -1602,7 +1812,7 @@ class BluetoothMeshService: NSObject {
for (index, storedMessage) in messagesToSend.enumerated() { for (index, storedMessage) in messagesToSend.enumerated() {
let delay = Double(index) * 0.02 // 20ms between messages for faster sync let delay = Double(index) * 0.02 // 20ms between messages for faster sync
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak peripheral] in DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self, weak peripheral] in
guard let peripheral = peripheral, guard let peripheral = peripheral,
peripheral.state == .connected else { peripheral.state == .connected else {
return return
@@ -1613,7 +1823,7 @@ class BluetoothMeshService: NSObject {
if let data = packetToSend.toBinaryData(), if let data = packetToSend.toBinaryData(),
characteristic.properties.contains(.writeWithoutResponse) { characteristic.properties.contains(.writeWithoutResponse) {
peripheral.writeValue(data, for: characteristic, type: .withoutResponse) self?.writeToPeripheral(data, peripheral: peripheral, characteristic: characteristic, peerID: peerID)
} }
} }
} }
@@ -1691,18 +1901,14 @@ class BluetoothMeshService: NSObject {
// Send to connected peripherals (as central) // Send to connected peripherals (as central)
var sentToPeripherals = 0 var sentToPeripherals = 0
for (_, peripheral) in connectedPeripherals { for (peerID, peripheral) in connectedPeripherals {
if let characteristic = peripheralCharacteristics[peripheral] { if let characteristic = peripheralCharacteristics[peripheral] {
// Check if peripheral is connected before writing // Check if peripheral is connected before writing
if peripheral.state == .connected { if peripheral.state == .connected {
// Use withoutResponse for faster transmission when possible
// Only use withResponse for critical messages or when MTU negotiation needed
let writeType: CBCharacteristicWriteType = data.count > 512 ? .withResponse : .withoutResponse
// Additional safety check for characteristic properties // Additional safety check for characteristic properties
if characteristic.properties.contains(.write) || if characteristic.properties.contains(.write) ||
characteristic.properties.contains(.writeWithoutResponse) { characteristic.properties.contains(.writeWithoutResponse) {
peripheral.writeValue(data, for: characteristic, type: writeType) writeToPeripheral(data, peripheral: peripheral, characteristic: characteristic, peerID: peerID)
sentToPeripherals += 1 sentToPeripherals += 1
} }
} else { } else {
@@ -1757,6 +1963,27 @@ class BluetoothMeshService: NSObject {
// Update peer availability // Update peer availability
self.updatePeerAvailability(senderID) self.updatePeerAvailability(senderID)
// IMPORTANT: Update peripheral mapping for ALL message types including handshakes
// This ensures disconnect messages work even if peer disconnects right after handshake
if let peripheral = peripheral {
let peripheralID = peripheral.identifier.uuidString
// Check if we need to update the mapping
if peerIDByPeripheralID[peripheralID] != senderID {
SecureLogger.log("Updating peripheral mapping: \(peripheralID) -> \(senderID)",
category: SecureLogger.session, level: .debug)
peerIDByPeripheralID[peripheralID] = senderID
// Also ensure connectedPeripherals has the correct mapping
// Remove any temp ID mapping if it exists
let tempIDToRemove = connectedPeripherals.first(where: { $0.value == peripheral && $0.key != senderID })?.key
if let tempID = tempIDToRemove {
connectedPeripherals.removeValue(forKey: tempID)
}
connectedPeripherals[senderID] = peripheral
}
}
// Check if this is a reconnection after a long silence // Check if this is a reconnection after a long silence
let wasReconnection: Bool let wasReconnection: Bool
if let lastHeard = self.lastHeardFromPeer[senderID] { if let lastHeard = self.lastHeardFromPeer[senderID] {
@@ -2126,6 +2353,8 @@ class BluetoothMeshService: NSObject {
self.connectedPeripherals.removeValue(forKey: tempID) self.connectedPeripherals.removeValue(forKey: tempID)
// Add real peer ID mapping // Add real peer ID mapping
self.connectedPeripherals[senderID] = peripheral self.connectedPeripherals[senderID] = peripheral
// Update peripheral ID to peer ID mapping
self.peerIDByPeripheralID[peripheral.identifier.uuidString] = senderID
// IMPORTANT: Remove old peer ID from activePeers to prevent duplicates // IMPORTANT: Remove old peer ID from activePeers to prevent duplicates
collectionsQueue.sync(flags: .barrier) { collectionsQueue.sync(flags: .barrier) {
@@ -3008,6 +3237,20 @@ extension BluetoothMeshService: CBCentralManagerDelegate {
// New peripheral - add to pool and connect // New peripheral - add to pool and connect
if !discoveredPeripherals.contains(peripheral) { if !discoveredPeripherals.contains(peripheral) {
// Check connection pool limits
let connectedCount = connectionPool.values.filter { $0.state == .connected }.count
if connectedCount >= maxConnectedPeripherals {
// Connection pool is full - find least recently used peripheral to disconnect
if let lruPeripheralID = findLeastRecentlyUsedPeripheral() {
if let lruPeripheral = connectionPool[lruPeripheralID] {
SecureLogger.log("Connection pool full, disconnecting LRU peripheral: \(lruPeripheralID)",
category: SecureLogger.session, level: .debug)
central.cancelPeripheralConnection(lruPeripheral)
connectionPool.removeValue(forKey: lruPeripheralID)
}
}
}
discoveredPeripherals.append(peripheral) discoveredPeripherals.append(peripheral)
peripheral.delegate = self peripheral.delegate = self
connectionPool[peripheralID] = peripheral connectionPool[peripheralID] = peripheral
@@ -3034,6 +3277,10 @@ extension BluetoothMeshService: CBCentralManagerDelegate {
let tempID = peripheral.identifier.uuidString let tempID = peripheral.identifier.uuidString
SecureLogger.log("Peripheral connected: \(tempID)", category: SecureLogger.session, level: .info) SecureLogger.log("Peripheral connected: \(tempID)", category: SecureLogger.session, level: .info)
// Log current mapping state for debugging
SecureLogger.log("Current peerIDByPeripheralID mappings: \(peerIDByPeripheralID.keys.joined(separator: ", "))",
category: SecureLogger.session, level: .debug)
peripheral.delegate = self peripheral.delegate = self
peripheral.discoverServices([BluetoothMeshService.serviceUUID]) peripheral.discoverServices([BluetoothMeshService.serviceUUID])
@@ -3077,16 +3324,32 @@ extension BluetoothMeshService: CBCentralManagerDelegate {
// Find the real peer ID for this peripheral // Find the real peer ID for this peripheral
var realPeerID: String? = nil var realPeerID: String? = nil
// First check if we have a direct mapping from peripheral to peer ID // First check our peripheral ID to peer ID mapping
for (peerID, connectedPeripheral) in connectedPeripherals { if let peerID = peerIDByPeripheralID[peripheralID] {
if connectedPeripheral.identifier == peripheral.identifier { realPeerID = peerID
realPeerID = peerID.count == 16 ? peerID : nil SecureLogger.log("Found peer ID \(peerID) from peerIDByPeripheralID for peripheral \(peripheralID)",
break category: SecureLogger.session, level: .debug)
} else {
SecureLogger.log("No mapping in peerIDByPeripheralID for peripheral \(peripheralID), checking connectedPeripherals",
category: SecureLogger.session, level: .debug)
// Fallback: check if we have a direct mapping from peripheral to peer ID
for (peerID, connectedPeripheral) in connectedPeripherals {
if connectedPeripheral.identifier == peripheral.identifier {
// Check if this is a real peer ID (16 hex chars) not a temp ID
if peerID.count == 16 && peerID.allSatisfy({ $0.isHexDigit }) {
realPeerID = peerID
SecureLogger.log("Found peer ID \(peerID) from connectedPeripherals fallback",
category: SecureLogger.session, level: .debug)
break
} else {
SecureLogger.log("Skipping non-peer ID '\(peerID)' (length: \(peerID.count))",
category: SecureLogger.session, level: .debug)
}
}
} }
} }
// If not found in connected peripherals, we don't have a mapping
// Update connection state immediately if we have a real peer ID // Update connection state immediately if we have a real peer ID
if let peerID = realPeerID { if let peerID = realPeerID {
// Update peer connection state // Update peer connection state
@@ -3124,6 +3387,10 @@ extension BluetoothMeshService: CBCentralManagerDelegate {
connectionBackoff.removeValue(forKey: peripheralID) connectionBackoff.removeValue(forKey: peripheralID)
} }
// Clean up peripheral tracking
peerIDByPeripheralID.removeValue(forKey: peripheralID)
lastActivityByPeripheralID.removeValue(forKey: peripheralID)
// Find peer ID for this peripheral (could be temp ID or real ID) // Find peer ID for this peripheral (could be temp ID or real ID)
var foundPeerID: String? = nil var foundPeerID: String? = nil
for (id, per) in connectedPeripherals { for (id, per) in connectedPeripherals {
@@ -3272,8 +3539,7 @@ extension BluetoothMeshService: CBPeripheralDelegate {
payload: Data(vm.nickname.utf8) payload: Data(vm.nickname.utf8)
) )
if let data = announcePacket.toBinaryData() { if let data = announcePacket.toBinaryData() {
let writeType: CBCharacteristicWriteType = characteristic.properties.contains(.write) ? .withResponse : .withoutResponse self.writeToPeripheral(data, peripheral: peripheral, characteristic: characteristic, peerID: nil)
peripheral.writeValue(data, for: characteristic, type: writeType)
} }
} }
} }
@@ -3286,6 +3552,9 @@ extension BluetoothMeshService: CBPeripheralDelegate {
return return
} }
// Update activity tracking for this peripheral
updatePeripheralActivity(peripheral.identifier.uuidString)
guard let packet = BitchatPacket.from(data) else { guard let packet = BitchatPacket.from(data) else {
return return
@@ -4348,8 +4617,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
let characteristic = peripheralCharacteristics[peripheral] { let characteristic = peripheralCharacteristics[peripheral] {
// Send directly to specific peripheral // Send directly to specific peripheral
if let data = packet.toBinaryData() { if let data = packet.toBinaryData() {
let writeType: CBCharacteristicWriteType = characteristic.properties.contains(.write) ? .withResponse : .withoutResponse writeToPeripheral(data, peripheral: peripheral, characteristic: characteristic, peerID: nil)
peripheral.writeValue(data, for: characteristic, type: writeType)
} }
} else { } else {
// Broadcast to all // Broadcast to all
@@ -4898,4 +5166,40 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
} }
} // End of encryptionQueue.async } // End of encryptionQueue.async
} }
// MARK: - Connection Pool Management
private func findLeastRecentlyUsedPeripheral() -> String? {
var lruPeripheralID: String?
var oldestActivityTime = Date()
for (peripheralID, peripheral) in connectionPool {
// Only consider connected peripherals
guard peripheral.state == .connected else { continue }
// Skip if this peripheral has an active peer connection
if let peerID = peerIDByPeripheralID[peripheralID],
activePeers.contains(peerID) {
continue
}
// Find the least recently used peripheral based on last activity
if let lastActivity = lastActivityByPeripheralID[peripheralID],
lastActivity < oldestActivityTime {
oldestActivityTime = lastActivity
lruPeripheralID = peripheralID
} else if lastActivityByPeripheralID[peripheralID] == nil {
// If no activity recorded, it's a candidate for removal
lruPeripheralID = peripheralID
break
}
}
return lruPeripheralID
}
// Track activity for peripherals
private func updatePeripheralActivity(_ peripheralID: String) {
lastActivityByPeripheralID[peripheralID] = Date()
}
} }
+13 -5
View File
@@ -1953,9 +1953,11 @@ extension ChatViewModel: BitchatDelegate {
} }
} }
} else { } else {
// System message - always add // System message - check for empty content before adding
messages.append(finalMessage) if !finalMessage.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
messages.sort { $0.timestamp < $1.timestamp } messages.append(finalMessage)
messages.sort { $0.timestamp < $1.timestamp }
}
} }
} }
@@ -2067,9 +2069,12 @@ extension ChatViewModel: BitchatDelegate {
// Resolve nickname using helper // Resolve nickname using helper
let displayName = resolveNickname(for: peerID) let displayName = resolveNickname(for: peerID)
// Ensure we have a valid display name
let finalDisplayName = displayName.isEmpty ? "peer" : displayName
let systemMessage = BitchatMessage( let systemMessage = BitchatMessage(
sender: "system", sender: "system",
content: "\(displayName) connected", content: "\(finalDisplayName) connected",
timestamp: Date(), timestamp: Date(),
isRelay: false, isRelay: false,
originalSender: nil originalSender: nil
@@ -2098,9 +2103,12 @@ extension ChatViewModel: BitchatDelegate {
// Resolve nickname using helper // Resolve nickname using helper
let displayName = resolveNickname(for: peerID) let displayName = resolveNickname(for: peerID)
// Ensure we have a valid display name
let finalDisplayName = displayName.isEmpty ? "peer" : displayName
let systemMessage = BitchatMessage( let systemMessage = BitchatMessage(
sender: "system", sender: "system",
content: "\(displayName) disconnected", content: "\(finalDisplayName) disconnected",
timestamp: Date(), timestamp: Date(),
isRelay: false, isRelay: false,
originalSender: nil originalSender: nil