mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 00:25:20 +00:00
PeerID 18/n: BitchatDelegate + Tests (#769)
This commit is contained in:
@@ -162,9 +162,9 @@ enum DeliveryStatus: Codable, Equatable, Hashable {
|
||||
|
||||
protocol BitchatDelegate: AnyObject {
|
||||
func didReceiveMessage(_ message: BitchatMessage)
|
||||
func didConnectToPeer(_ peerID: String)
|
||||
func didDisconnectFromPeer(_ peerID: String)
|
||||
func didUpdatePeerList(_ peers: [String])
|
||||
func didConnectToPeer(_ peerID: PeerID)
|
||||
func didDisconnectFromPeer(_ peerID: PeerID)
|
||||
func didUpdatePeerList(_ peers: [PeerID])
|
||||
|
||||
// Optional method to check if a fingerprint belongs to a favorite peer
|
||||
func isFavorite(fingerprint: String) -> Bool
|
||||
@@ -172,11 +172,11 @@ protocol BitchatDelegate: AnyObject {
|
||||
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus)
|
||||
|
||||
// Low-level events for better separation of concerns
|
||||
func didReceiveNoisePayload(from peerID: String, type: NoisePayloadType, payload: Data, timestamp: Date)
|
||||
func didReceiveNoisePayload(from peerID: PeerID, type: NoisePayloadType, payload: Data, timestamp: Date)
|
||||
|
||||
// Bluetooth state updates for user notifications
|
||||
func didUpdateBluetoothState(_ state: CBManagerState)
|
||||
func didReceivePublicMessage(from peerID: String, nickname: String, content: String, timestamp: Date)
|
||||
func didReceivePublicMessage(from peerID: PeerID, nickname: String, content: String, timestamp: Date)
|
||||
}
|
||||
|
||||
// Provide default implementation to make it effectively optional
|
||||
@@ -189,11 +189,11 @@ extension BitchatDelegate {
|
||||
// Default empty implementation
|
||||
}
|
||||
|
||||
func didReceiveNoisePayload(from peerID: String, type: NoisePayloadType, payload: Data, timestamp: Date) {
|
||||
func didReceiveNoisePayload(from peerID: PeerID, type: NoisePayloadType, payload: Data, timestamp: Date) {
|
||||
// Default empty implementation
|
||||
}
|
||||
|
||||
func didReceivePublicMessage(from peerID: String, nickname: String, content: String, timestamp: Date) {
|
||||
func didReceivePublicMessage(from peerID: PeerID, nickname: String, content: String, timestamp: Date) {
|
||||
// Default empty implementation
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,9 @@ final class BLEService: NSObject {
|
||||
var lastSeen: Date
|
||||
}
|
||||
private var peers: [String: PeerInfo] = [:]
|
||||
private var currentPeerIDs: [PeerID] {
|
||||
peers.keys.map { PeerID(str: $0) }
|
||||
}
|
||||
|
||||
// 4. Efficient Message Deduplication
|
||||
private let messageDeduplicator = MessageDeduplicator()
|
||||
@@ -934,7 +937,7 @@ func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeriph
|
||||
guard let self = self else { return }
|
||||
|
||||
// Get current peer list (after removal)
|
||||
let currentPeerIDs = self.collectionsQueue.sync { Array(self.peers.keys) }
|
||||
let currentPeerIDs = self.collectionsQueue.sync { self.currentPeerIDs }
|
||||
|
||||
if let peerID = peerID {
|
||||
self.notifyPeerDisconnectedDebounced(peerID.id)
|
||||
@@ -1355,7 +1358,7 @@ extension BLEService: CBPeripheralManagerDelegate {
|
||||
guard let self = self else { return }
|
||||
|
||||
// Get current peer list (after removal)
|
||||
let currentPeerIDs = self.collectionsQueue.sync { Array(self.peers.keys) }
|
||||
let currentPeerIDs = self.collectionsQueue.sync { self.currentPeerIDs }
|
||||
|
||||
self.notifyPeerDisconnectedDebounced(peerID.id)
|
||||
// Publish snapshots so UnifiedPeerService can refresh icons promptly
|
||||
@@ -2549,11 +2552,11 @@ extension BLEService {
|
||||
guard let self = self else { return }
|
||||
|
||||
// Get current peer list (after addition)
|
||||
let currentPeerIDs = self.collectionsQueue.sync { Array(self.peers.keys) }
|
||||
let currentPeerIDs = self.collectionsQueue.sync { self.currentPeerIDs }
|
||||
|
||||
// Only notify of connection for new or reconnected peers when it is a direct announce
|
||||
if (packet.ttl == self.messageTTL) && (isNewPeer || isReconnectedPeer) {
|
||||
self.delegate?.didConnectToPeer(peerID)
|
||||
self.delegate?.didConnectToPeer(PeerID(str: peerID))
|
||||
// Schedule initial unicast sync to this peer
|
||||
self.gossipSyncManager?.scheduleInitialSyncToPeer(PeerID(str: peerID), delaySeconds: 1.0)
|
||||
}
|
||||
@@ -2700,7 +2703,7 @@ extension BLEService {
|
||||
|
||||
let ts = Date(timeIntervalSince1970: Double(packet.timestamp) / 1000)
|
||||
notifyUI { [weak self] in
|
||||
self?.delegate?.didReceivePublicMessage(from: peerID, nickname: senderNickname, content: content, timestamp: ts)
|
||||
self?.delegate?.didReceivePublicMessage(from: PeerID(str: peerID), nickname: senderNickname, content: content, timestamp: ts)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2738,6 +2741,7 @@ extension BLEService {
|
||||
}
|
||||
|
||||
private func handleNoiseEncrypted(_ packet: BitchatPacket, from peerID: String) {
|
||||
let peerID = PeerID(str: peerID)
|
||||
SecureLogger.debug("🔐 handleNoiseEncrypted called for packet from \(peerID)")
|
||||
|
||||
guard let recipientID = packet.recipientID else {
|
||||
@@ -2752,10 +2756,10 @@ extension BLEService {
|
||||
}
|
||||
|
||||
// Update lastSeen for the peer we received from (important for private messages)
|
||||
updatePeerLastSeen(peerID)
|
||||
updatePeerLastSeen(peerID.id)
|
||||
|
||||
do {
|
||||
let decrypted = try noiseService.decrypt(packet.payload, from: PeerID(str: peerID))
|
||||
let decrypted = try noiseService.decrypt(packet.payload, from: peerID)
|
||||
guard decrypted.count > 0 else { return }
|
||||
|
||||
// First byte indicates the payload type
|
||||
@@ -2795,8 +2799,8 @@ extension BLEService {
|
||||
// We received an encrypted message before establishing a session with this peer.
|
||||
// Trigger a handshake so future messages can be decrypted.
|
||||
SecureLogger.debug("🔑 Encrypted message from \(peerID) without session; initiating handshake")
|
||||
if !noiseService.hasSession(with: PeerID(str: peerID)) {
|
||||
initiateNoiseHandshake(with: peerID)
|
||||
if !noiseService.hasSession(with: peerID) {
|
||||
initiateNoiseHandshake(with: peerID.id)
|
||||
}
|
||||
} catch {
|
||||
SecureLogger.error("❌ Failed to decrypt message from \(peerID): \(error)")
|
||||
@@ -2815,9 +2819,9 @@ extension BLEService {
|
||||
guard let self = self else { return }
|
||||
|
||||
// Get current peer list (after removal)
|
||||
let currentPeerIDs = self.collectionsQueue.sync { Array(self.peers.keys) }
|
||||
let currentPeerIDs = self.collectionsQueue.sync { self.currentPeerIDs }
|
||||
|
||||
self.delegate?.didDisconnectFromPeer(peerID)
|
||||
self.delegate?.didDisconnectFromPeer(PeerID(str: peerID))
|
||||
self.delegate?.didUpdatePeerList(currentPeerIDs)
|
||||
}
|
||||
}
|
||||
@@ -2937,7 +2941,7 @@ extension BLEService {
|
||||
let now = Date()
|
||||
let last = recentDisconnectNotifies[peerID]
|
||||
if last == nil || now.timeIntervalSince(last!) >= TransportConfig.bleDisconnectNotifyDebounceSeconds {
|
||||
delegate?.didDisconnectFromPeer(peerID)
|
||||
delegate?.didDisconnectFromPeer(PeerID(str: peerID))
|
||||
recentDisconnectNotifies[peerID] = now
|
||||
} else {
|
||||
// Suppressed duplicate disconnect notification
|
||||
@@ -3078,13 +3082,13 @@ extension BLEService {
|
||||
// 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 }
|
||||
guard let self else { return }
|
||||
|
||||
// Get current peer list (after removal)
|
||||
let currentPeerIDs = self.collectionsQueue.sync { Array(self.peers.keys) }
|
||||
let currentPeerIDs = self.collectionsQueue.sync { self.currentPeerIDs }
|
||||
|
||||
for peerID in disconnectedPeers {
|
||||
self.delegate?.didDisconnectFromPeer(peerID)
|
||||
self.delegate?.didDisconnectFromPeer(PeerID(str: peerID))
|
||||
}
|
||||
// Publish snapshots so UnifiedPeerService updates connection/reachability icons
|
||||
self.requestPeerDataPublish()
|
||||
|
||||
@@ -223,7 +223,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
private let maxMessages = TransportConfig.meshTimelineCap // Maximum messages before oldest are removed
|
||||
@Published var isConnected = false
|
||||
private var hasNotifiedNetworkAvailable = false
|
||||
private var recentlySeenPeers: Set<String> = []
|
||||
private var recentlySeenPeers: Set<PeerID> = []
|
||||
private var lastNetworkNotificationTime = Date.distantPast
|
||||
private var networkResetTimer: Timer? = nil
|
||||
private let networkResetGraceSeconds: TimeInterval = TransportConfig.networkResetGraceSeconds // avoid refiring on short drops/reconnects
|
||||
@@ -4449,12 +4449,12 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
}
|
||||
|
||||
// Low-level BLE events
|
||||
func didReceiveNoisePayload(from peerID: String, type: NoisePayloadType, payload: Data, timestamp: Date) {
|
||||
func didReceiveNoisePayload(from peerID: PeerID, type: NoisePayloadType, payload: Data, timestamp: Date) {
|
||||
Task { @MainActor in
|
||||
switch type {
|
||||
case .privateMessage:
|
||||
guard let pm = PrivateMessagePacket.decode(from: payload) else { return }
|
||||
let senderName = unifiedPeerService.getPeer(by: peerID)?.nickname ?? "Unknown"
|
||||
let senderName = unifiedPeerService.getPeer(by: peerID.id)?.nickname ?? "Unknown"
|
||||
let pmMentions = parseMentions(from: pm.content)
|
||||
let msg = BitchatMessage(
|
||||
id: pm.messageID,
|
||||
@@ -4465,27 +4465,27 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
originalSender: nil,
|
||||
isPrivate: true,
|
||||
recipientNickname: nickname,
|
||||
senderPeerID: PeerID(str: peerID),
|
||||
senderPeerID: peerID,
|
||||
mentions: pmMentions.isEmpty ? nil : pmMentions
|
||||
)
|
||||
handlePrivateMessage(msg)
|
||||
// Send delivery ACK back over BLE
|
||||
meshService.sendDeliveryAck(for: pm.messageID, to: PeerID(str: peerID))
|
||||
meshService.sendDeliveryAck(for: pm.messageID, to: peerID)
|
||||
|
||||
case .delivered:
|
||||
guard let messageID = String(data: payload, encoding: .utf8) else { return }
|
||||
if let name = unifiedPeerService.getPeer(by: peerID)?.nickname {
|
||||
if let messages = privateChats[peerID], let idx = messages.firstIndex(where: { $0.id == messageID }) {
|
||||
privateChats[peerID]?[idx].deliveryStatus = .delivered(to: name, at: Date())
|
||||
if let name = unifiedPeerService.getPeer(by: peerID.id)?.nickname {
|
||||
if let messages = privateChats[peerID.id], let idx = messages.firstIndex(where: { $0.id == messageID }) {
|
||||
privateChats[peerID.id]?[idx].deliveryStatus = .delivered(to: name, at: Date())
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
case .readReceipt:
|
||||
guard let messageID = String(data: payload, encoding: .utf8) else { return }
|
||||
if let name = unifiedPeerService.getPeer(by: peerID)?.nickname {
|
||||
if let messages = privateChats[peerID], let idx = messages.firstIndex(where: { $0.id == messageID }) {
|
||||
privateChats[peerID]?[idx].deliveryStatus = .read(by: name, at: Date())
|
||||
if let name = unifiedPeerService.getPeer(by: peerID.id)?.nickname {
|
||||
if let messages = privateChats[peerID.id], let idx = messages.firstIndex(where: { $0.id == messageID }) {
|
||||
privateChats[peerID.id]?[idx].deliveryStatus = .read(by: name, at: Date())
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
@@ -4496,10 +4496,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
let myNoiseHex = meshService.getNoiseService().getStaticPublicKeyData().hexEncodedString().lowercased()
|
||||
guard tlv.noiseKeyHex.lowercased() == myNoiseHex else { return }
|
||||
// Deduplicate: ignore if we've already responded to this nonce for this peer
|
||||
if let last = lastVerifyNonceByPeer[peerID], last == tlv.nonceA { return }
|
||||
lastVerifyNonceByPeer[peerID] = tlv.nonceA
|
||||
if let last = lastVerifyNonceByPeer[peerID.id], last == tlv.nonceA { return }
|
||||
lastVerifyNonceByPeer[peerID.id] = tlv.nonceA
|
||||
// Record inbound challenge time keyed by stable fingerprint if available
|
||||
if let fp = getFingerprint(for: peerID) {
|
||||
if let fp = getFingerprint(for: peerID.id) {
|
||||
lastInboundVerifyChallengeAt[fp] = Date()
|
||||
// If we've already verified this fingerprint locally, treat this as mutual and toast immediately (responder side)
|
||||
if verifiedFingerprints.contains(fp) {
|
||||
@@ -4507,7 +4507,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
let last = lastMutualToastAt[fp] ?? .distantPast
|
||||
if now.timeIntervalSince(last) > 60 { // 1-minute throttle
|
||||
lastMutualToastAt[fp] = now
|
||||
let name = unifiedPeerService.getPeer(by: peerID)?.nickname ?? resolveNickname(for: peerID)
|
||||
let name = unifiedPeerService.getPeer(by: peerID.id)?.nickname ?? resolveNickname(for: peerID.id)
|
||||
NotificationService.shared.sendLocalNotification(
|
||||
title: "Mutual verification",
|
||||
body: "You and \(name) verified each other",
|
||||
@@ -4516,24 +4516,24 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
}
|
||||
}
|
||||
}
|
||||
meshService.sendVerifyResponse(to: PeerID(str: peerID), noiseKeyHex: tlv.noiseKeyHex, nonceA: tlv.nonceA)
|
||||
meshService.sendVerifyResponse(to: peerID, noiseKeyHex: tlv.noiseKeyHex, nonceA: tlv.nonceA)
|
||||
// Silent response: no toast needed on responder
|
||||
case .verifyResponse:
|
||||
guard let resp = VerificationService.shared.parseVerifyResponse(payload) else { return }
|
||||
// Check pending for this peer
|
||||
guard let pending = pendingQRVerifications[peerID] else { return }
|
||||
guard let pending = pendingQRVerifications[peerID.id] else { return }
|
||||
guard resp.noiseKeyHex.lowercased() == pending.noiseKeyHex.lowercased(), resp.nonceA == pending.nonceA else { return }
|
||||
// Verify signature with expected sign key
|
||||
let ok = VerificationService.shared.verifyResponseSignature(noiseKeyHex: resp.noiseKeyHex, nonceA: resp.nonceA, signature: resp.signature, signerPublicKeyHex: pending.signKeyHex)
|
||||
if ok {
|
||||
pendingQRVerifications.removeValue(forKey: peerID)
|
||||
if let fp = getFingerprint(for: peerID) {
|
||||
pendingQRVerifications.removeValue(forKey: peerID.id)
|
||||
if let fp = getFingerprint(for: peerID.id) {
|
||||
let short = fp.prefix(8)
|
||||
SecureLogger.info("🔐 Marking verified fingerprint: \(short)", category: .security)
|
||||
identityManager.setVerified(fingerprint: fp, verified: true)
|
||||
identityManager.forceSave()
|
||||
verifiedFingerprints.insert(fp)
|
||||
let name = unifiedPeerService.getPeer(by: peerID)?.nickname ?? resolveNickname(for: peerID)
|
||||
let name = unifiedPeerService.getPeer(by: peerID.id)?.nickname ?? resolveNickname(for: peerID.id)
|
||||
NotificationService.shared.sendLocalNotification(
|
||||
title: "Verified",
|
||||
body: "You verified \(name)",
|
||||
@@ -4552,14 +4552,14 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
)
|
||||
}
|
||||
}
|
||||
updateEncryptionStatus(for: peerID)
|
||||
updateEncryptionStatus(for: peerID.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func didReceivePublicMessage(from peerID: String, nickname: String, content: String, timestamp: Date) {
|
||||
func didReceivePublicMessage(from peerID: PeerID, nickname: String, content: String, timestamp: Date) {
|
||||
Task { @MainActor in
|
||||
let normalized = content.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let publicMentions = parseMentions(from: normalized)
|
||||
@@ -4572,7 +4572,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
originalSender: nil,
|
||||
isPrivate: false,
|
||||
recipientNickname: nil,
|
||||
senderPeerID: PeerID(str: peerID),
|
||||
senderPeerID: peerID,
|
||||
mentions: publicMentions.isEmpty ? nil : publicMentions
|
||||
)
|
||||
handlePublicMessage(msg)
|
||||
@@ -4622,7 +4622,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
|
||||
// MARK: - Peer Connection Events
|
||||
|
||||
func didConnectToPeer(_ peerID: String) {
|
||||
func didConnectToPeer(_ peerID: PeerID) {
|
||||
SecureLogger.debug("🤝 Peer connected: \(peerID)", category: .session)
|
||||
|
||||
// Handle all main actor work async
|
||||
@@ -4630,7 +4630,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
isConnected = true
|
||||
|
||||
// Register ephemeral session with identity manager
|
||||
identityManager.registerEphemeralSession(peerID: PeerID(str: peerID), handshakeState: .none)
|
||||
identityManager.registerEphemeralSession(peerID: peerID, handshakeState: .none)
|
||||
|
||||
// Intentionally do not resend favorites on reconnect.
|
||||
// We only send our npub when a favorite is toggled on, or if our npub changes.
|
||||
@@ -4639,36 +4639,34 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
objectWillChange.send()
|
||||
|
||||
// Cache mapping to full Noise key for session continuity on disconnect
|
||||
if let peer = unifiedPeerService.getPeer(by: peerID) {
|
||||
if let peer = unifiedPeerService.getPeer(by: peerID.id) {
|
||||
let noiseKeyHex = peer.noisePublicKey.hexEncodedString()
|
||||
shortIDToNoiseKey[peerID] = noiseKeyHex
|
||||
shortIDToNoiseKey[peerID.id] = noiseKeyHex
|
||||
}
|
||||
|
||||
// Flush any queued messages for this peer via router
|
||||
messageRouter.flushOutbox(for: PeerID(str: peerID))
|
||||
messageRouter.flushOutbox(for: peerID)
|
||||
}
|
||||
|
||||
//
|
||||
}
|
||||
|
||||
func didDisconnectFromPeer(_ peerID: String) {
|
||||
func didDisconnectFromPeer(_ peerID: PeerID) {
|
||||
SecureLogger.debug("👋 Peer disconnected: \(peerID)", category: .session)
|
||||
|
||||
// Remove ephemeral session from identity manager
|
||||
identityManager.removeEphemeralSession(peerID: PeerID(str: peerID))
|
||||
identityManager.removeEphemeralSession(peerID: peerID)
|
||||
|
||||
// If the open PM is tied to this short peer ID, switch UI context to the full Noise key (offline favorite)
|
||||
var derivedStableKeyHex: String? = shortIDToNoiseKey[peerID]
|
||||
var derivedStableKeyHex: String? = shortIDToNoiseKey[peerID.id]
|
||||
if derivedStableKeyHex == nil,
|
||||
let key = meshService.getNoiseService().getPeerPublicKeyData(PeerID(str: peerID)) {
|
||||
let key = meshService.getNoiseService().getPeerPublicKeyData(peerID) {
|
||||
derivedStableKeyHex = key.hexEncodedString()
|
||||
shortIDToNoiseKey[peerID] = derivedStableKeyHex
|
||||
shortIDToNoiseKey[peerID.id] = derivedStableKeyHex
|
||||
}
|
||||
|
||||
if let current = selectedPrivateChatPeer, current == peerID,
|
||||
let stableKeyHex = derivedStableKeyHex {
|
||||
// Migrate messages view context to stable key so header shows favorite + Nostr globe
|
||||
if let messages = privateChats[peerID] {
|
||||
if let messages = privateChats[peerID.id] {
|
||||
if privateChats[stableKeyHex] == nil { privateChats[stableKeyHex] = [] }
|
||||
let existing = Set(privateChats[stableKeyHex]!.map { $0.id })
|
||||
for msg in messages where !existing.contains(msg.id) {
|
||||
@@ -4688,10 +4686,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
privateChats[stableKeyHex]?.append(updated)
|
||||
}
|
||||
privateChats[stableKeyHex]?.sort { $0.timestamp < $1.timestamp }
|
||||
privateChats.removeValue(forKey: peerID)
|
||||
privateChats.removeValue(forKey: peerID.id)
|
||||
}
|
||||
if unreadPrivateMessages.contains(peerID) {
|
||||
unreadPrivateMessages.remove(peerID)
|
||||
if unreadPrivateMessages.contains(peerID.id) {
|
||||
unreadPrivateMessages.remove(peerID.id)
|
||||
unreadPrivateMessages.insert(stableKeyHex)
|
||||
}
|
||||
selectedPrivateChatPeer = stableKeyHex
|
||||
@@ -4706,7 +4704,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
|
||||
// Clear sent read receipts for this peer since they'll need to be resent after reconnection
|
||||
// Only clear receipts for messages from this specific peer
|
||||
if let messages = privateChats[peerID] {
|
||||
if let messages = privateChats[peerID.id] {
|
||||
for message in messages {
|
||||
// Remove read receipts for messages FROM this peer (not TO this peer)
|
||||
if message.senderPeerID == peerID {
|
||||
@@ -4718,7 +4716,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
//
|
||||
}
|
||||
|
||||
func didUpdatePeerList(_ peers: [String]) {
|
||||
func didUpdatePeerList(_ peers: [PeerID]) {
|
||||
// UI updates must run on the main thread.
|
||||
// The delegate callback is not guaranteed to be on the main thread.
|
||||
DispatchQueue.main.async {
|
||||
@@ -4736,7 +4734,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
self.networkResetTimer = nil
|
||||
// Count mesh peers that are connected OR recently reachable via mesh relays
|
||||
let meshPeers = peers.filter { peerID in
|
||||
self.meshService.isPeerConnected(PeerID(str: peerID)) || self.meshService.isPeerReachable(PeerID(str: peerID))
|
||||
self.meshService.isPeerConnected(peerID) || self.meshService.isPeerReachable(peerID)
|
||||
}
|
||||
|
||||
// Rising-edge only: previously zero peers, now > 0 peers
|
||||
@@ -4762,7 +4760,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
|
||||
// Register ephemeral sessions for all connected peers
|
||||
for peerID in peers {
|
||||
self.identityManager.registerEphemeralSession(peerID: PeerID(str: peerID), handshakeState: .none)
|
||||
self.identityManager.registerEphemeralSession(peerID: peerID, handshakeState: .none)
|
||||
}
|
||||
|
||||
// Schedule UI refresh to ensure offline favorites are shown
|
||||
|
||||
Reference in New Issue
Block a user