mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 01:45:20 +00:00
Add connected vs reachable model: retain peers after link drop, expire after reachability window; expose all peers in snapshots; compute isReachable in UI; add meshReachable state and sorting; avoid removing peers on link events; notify UI on stale removals
This commit is contained in:
@@ -8,6 +8,7 @@ struct BitchatPeer: Identifiable, Equatable {
|
||||
let nickname: String
|
||||
let lastSeen: Date
|
||||
let isConnected: Bool
|
||||
let isReachable: Bool
|
||||
|
||||
// Favorite-related properties
|
||||
var favoriteStatus: FavoritesPersistenceService.FavoriteRelationship?
|
||||
@@ -18,6 +19,7 @@ struct BitchatPeer: Identifiable, Equatable {
|
||||
// Connection state
|
||||
enum ConnectionState {
|
||||
case bluetoothConnected
|
||||
case meshReachable // Seen via mesh recently, not directly connected
|
||||
case nostrAvailable // Mutual favorite, reachable via Nostr
|
||||
case offline // Not connected via any transport
|
||||
}
|
||||
@@ -25,6 +27,8 @@ struct BitchatPeer: Identifiable, Equatable {
|
||||
var connectionState: ConnectionState {
|
||||
if isConnected {
|
||||
return .bluetoothConnected
|
||||
} else if isReachable {
|
||||
return .meshReachable
|
||||
} else if favoriteStatus?.isMutual == true {
|
||||
// Mutual favorites can communicate via Nostr when offline
|
||||
return .nostrAvailable
|
||||
@@ -54,6 +58,8 @@ struct BitchatPeer: Identifiable, Equatable {
|
||||
switch connectionState {
|
||||
case .bluetoothConnected:
|
||||
return "📻" // Radio icon for mesh connection
|
||||
case .meshReachable:
|
||||
return "📡" // Antenna for mesh reachable
|
||||
case .nostrAvailable:
|
||||
return "🌐" // Purple globe for Nostr
|
||||
case .offline:
|
||||
@@ -71,13 +77,15 @@ struct BitchatPeer: Identifiable, Equatable {
|
||||
noisePublicKey: Data,
|
||||
nickname: String,
|
||||
lastSeen: Date = Date(),
|
||||
isConnected: Bool = false
|
||||
isConnected: Bool = false,
|
||||
isReachable: Bool = false
|
||||
) {
|
||||
self.id = id
|
||||
self.noisePublicKey = noisePublicKey
|
||||
self.nickname = nickname
|
||||
self.lastSeen = lastSeen
|
||||
self.isConnected = isConnected
|
||||
self.isReachable = isReachable
|
||||
|
||||
// Load favorite status - will be set later by the manager
|
||||
self.favoriteStatus = nil
|
||||
|
||||
@@ -1831,28 +1831,39 @@ final class BLEService: NSObject {
|
||||
let now = Date()
|
||||
var disconnectedPeers: [String] = []
|
||||
|
||||
var removedOfflineCount = 0
|
||||
collectionsQueue.sync(flags: .barrier) {
|
||||
for (peerID, peer) in peers {
|
||||
if peer.isConnected && now.timeIntervalSince(peer.lastSeen) > TransportConfig.blePeerInactivityTimeoutSeconds {
|
||||
let age = now.timeIntervalSince(peer.lastSeen)
|
||||
let retention: TimeInterval = peer.isVerifiedNickname ? TransportConfig.bleReachabilityRetentionVerifiedSeconds : TransportConfig.bleReachabilityRetentionUnverifiedSeconds
|
||||
if peer.isConnected && age > TransportConfig.blePeerInactivityTimeoutSeconds {
|
||||
// Check if we still have an active BLE connection to this peer
|
||||
let hasPeripheralConnection = peerToPeripheralUUID[peerID] != nil &&
|
||||
peripherals[peerToPeripheralUUID[peerID]!]?.isConnected == true
|
||||
let hasCentralConnection = centralToPeerID.values.contains(peerID)
|
||||
|
||||
// Only remove if we don't have an active BLE connection
|
||||
// If direct link is gone, mark as not connected (retain entry for reachability)
|
||||
if !hasPeripheralConnection && !hasCentralConnection {
|
||||
// Remove the peer completely (they'll be re-added when they reconnect)
|
||||
SecureLogger.log("⏱️ Peer timed out (no packets for 20s): \(peerID) (\(peer.nickname))",
|
||||
category: SecureLogger.session, level: .debug)
|
||||
peers.removeValue(forKey: peerID)
|
||||
var updated = peer
|
||||
updated.isConnected = false
|
||||
peers[peerID] = updated
|
||||
disconnectedPeers.append(peerID)
|
||||
}
|
||||
}
|
||||
// Cleanup: remove peers that are not connected and past reachability retention
|
||||
if !peer.isConnected {
|
||||
if age > retention {
|
||||
SecureLogger.log("🗑️ Removing stale peer after reachability window: \(peerID) (\(peer.nickname))",
|
||||
category: SecureLogger.session, level: .debug)
|
||||
peers.removeValue(forKey: peerID)
|
||||
removedOfflineCount += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update UI if any peers were disconnected
|
||||
if !disconnectedPeers.isEmpty {
|
||||
// Update UI if there were direct disconnections or offline removals
|
||||
if !disconnectedPeers.isEmpty || removedOfflineCount > 0 {
|
||||
notifyUI { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
@@ -2198,9 +2209,12 @@ func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeriph
|
||||
if let peerID = peerID {
|
||||
peerToPeripheralUUID.removeValue(forKey: peerID)
|
||||
|
||||
// Remove peer completely (they'll be re-added when they reconnect and announce)
|
||||
_ = collectionsQueue.sync(flags: .barrier) {
|
||||
peers.removeValue(forKey: peerID)
|
||||
// Do not remove peer; mark as not connected but retain for reachability
|
||||
collectionsQueue.sync(flags: .barrier) {
|
||||
if var info = peers[peerID] {
|
||||
info.isConnected = false
|
||||
peers[peerID] = info
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2215,7 +2229,7 @@ func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeriph
|
||||
// Attempt to fill freed slot from queue
|
||||
bleQueue.async { [weak self] in self?.tryConnectFromQueue() }
|
||||
|
||||
// Notify delegate about disconnection on main thread
|
||||
// Notify delegate about disconnection on main thread (direct link dropped)
|
||||
notifyUI { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
@@ -2586,9 +2600,12 @@ extension BLEService: CBPeripheralManagerDelegate {
|
||||
// Find and disconnect the peer associated with this central
|
||||
let centralUUID = central.identifier.uuidString
|
||||
if let peerID = centralToPeerID[centralUUID] {
|
||||
// Remove peer completely (they'll be re-added when they reconnect)
|
||||
_ = collectionsQueue.sync(flags: .barrier) {
|
||||
peers.removeValue(forKey: peerID)
|
||||
// Mark peer as not connected; retain for reachability
|
||||
collectionsQueue.sync(flags: .barrier) {
|
||||
if var info = peers[peerID] {
|
||||
info.isConnected = false
|
||||
peers[peerID] = info
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up mappings
|
||||
|
||||
@@ -72,6 +72,9 @@ enum TransportConfig {
|
||||
static let bleRSSIConnectedThreshold: Int = -85
|
||||
static let bleRSSIHighTimeoutThreshold: Int = -80
|
||||
static let blePeerInactivityTimeoutSeconds: TimeInterval = 20.0
|
||||
// How long to retain a peer as "reachable" (not directly connected) since lastSeen
|
||||
static let bleReachabilityRetentionVerifiedSeconds: TimeInterval = 600.0 // 10 minutes for verified/favorites
|
||||
static let bleReachabilityRetentionUnverifiedSeconds: TimeInterval = 120.0 // 2 minutes for unknown/unverified
|
||||
static let bleFragmentLifetimeSeconds: TimeInterval = 30.0
|
||||
static let bleIngressRecordLifetimeSeconds: TimeInterval = 3.0
|
||||
static let bleConnectTimeoutBackoffWindowSeconds: TimeInterval = 120.0
|
||||
|
||||
@@ -74,8 +74,8 @@ class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
||||
var connected: Set<String> = []
|
||||
var addedPeerIDs: Set<String> = []
|
||||
|
||||
// Phase 1: Add all connected mesh peers
|
||||
for peerInfo in meshPeers where peerInfo.isConnected {
|
||||
// Phase 1: Add all mesh peers (connected and reachable)
|
||||
for peerInfo in meshPeers {
|
||||
let peerID = peerInfo.id
|
||||
guard peerID != meshService.myPeerID else { continue } // Never add self
|
||||
|
||||
@@ -85,7 +85,7 @@ class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
||||
)
|
||||
|
||||
enrichedPeers.append(peer)
|
||||
connected.insert(peerID)
|
||||
if peer.isConnected { connected.insert(peerID) }
|
||||
addedPeerIDs.insert(peerID)
|
||||
|
||||
// Update fingerprint cache
|
||||
@@ -117,14 +117,12 @@ class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
||||
|
||||
// Phase 3: Sort peers
|
||||
enrichedPeers.sort { lhs, rhs in
|
||||
// Connected first
|
||||
if lhs.isConnected != rhs.isConnected {
|
||||
return lhs.isConnected
|
||||
}
|
||||
// Then favorites
|
||||
if lhs.isFavorite != rhs.isFavorite {
|
||||
return lhs.isFavorite
|
||||
}
|
||||
// Connectivity rank: connected > reachable > others
|
||||
func rank(_ p: BitchatPeer) -> Int { p.isConnected ? 2 : (p.isReachable ? 1 : 0) }
|
||||
let lr = rank(lhs), rr = rank(rhs)
|
||||
if lr != rr { return lr > rr }
|
||||
// Then favorites inside same rank
|
||||
if lhs.isFavorite != rhs.isFavorite { return lhs.isFavorite }
|
||||
// Finally alphabetical
|
||||
return lhs.displayName < rhs.displayName
|
||||
}
|
||||
@@ -164,12 +162,21 @@ class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
||||
peerInfo: TransportPeerSnapshot,
|
||||
favorites: [Data: FavoritesPersistenceService.FavoriteRelationship]
|
||||
) -> BitchatPeer {
|
||||
// Determine reachability based on lastSeen and identity trust
|
||||
let now = Date()
|
||||
let fingerprint = peerInfo.noisePublicKey?.sha256Fingerprint()
|
||||
let isVerified = fingerprint.map { SecureIdentityStateManager.shared.isVerified(fingerprint: $0) } ?? false
|
||||
let isFav = peerInfo.noisePublicKey.flatMap { favorites[$0]?.isFavorite } ?? false
|
||||
let retention: TimeInterval = (isVerified || isFav) ? TransportConfig.bleReachabilityRetentionVerifiedSeconds : TransportConfig.bleReachabilityRetentionUnverifiedSeconds
|
||||
let isReachable = now.timeIntervalSince(peerInfo.lastSeen) <= retention
|
||||
|
||||
var peer = BitchatPeer(
|
||||
id: peerInfo.id,
|
||||
noisePublicKey: peerInfo.noisePublicKey ?? Data(),
|
||||
nickname: peerInfo.nickname,
|
||||
lastSeen: peerInfo.lastSeen,
|
||||
isConnected: true
|
||||
isConnected: peerInfo.isConnected,
|
||||
isReachable: isReachable
|
||||
)
|
||||
|
||||
// Check for favorite status
|
||||
@@ -216,7 +223,8 @@ class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
||||
noisePublicKey: favorite.peerNoisePublicKey,
|
||||
nickname: favorite.peerNickname,
|
||||
lastSeen: favorite.lastUpdated,
|
||||
isConnected: false
|
||||
isConnected: false,
|
||||
isReachable: false
|
||||
)
|
||||
|
||||
peer.favoriteStatus = favorite
|
||||
|
||||
Reference in New Issue
Block a user