mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 21:05:20 +00:00
PeerID 16/n: BitchatPeer / PeerInfo / TransportPeer (#755)
* PeerID 15/n: Bitchat Message & Packet accept in init * PeerID 16/n: BitchatPeer / PeerInfo / TransportPeer
This commit is contained in:
@@ -2,8 +2,8 @@ import Foundation
|
|||||||
import CoreBluetooth
|
import CoreBluetooth
|
||||||
|
|
||||||
/// Represents a peer in the BitChat network with all associated metadata
|
/// Represents a peer in the BitChat network with all associated metadata
|
||||||
struct BitchatPeer: Identifiable, Equatable {
|
struct BitchatPeer: Equatable {
|
||||||
let id: String // Hex-encoded peer ID
|
let peerID: PeerID // Hex-encoded peer ID
|
||||||
let noisePublicKey: Data
|
let noisePublicKey: Data
|
||||||
let nickname: String
|
let nickname: String
|
||||||
let lastSeen: Date
|
let lastSeen: Date
|
||||||
@@ -51,7 +51,7 @@ struct BitchatPeer: Identifiable, Equatable {
|
|||||||
|
|
||||||
// Display helpers
|
// Display helpers
|
||||||
var displayName: String {
|
var displayName: String {
|
||||||
nickname.isEmpty ? String(id.prefix(8)) : nickname
|
nickname.isEmpty ? String(peerID.id.prefix(8)) : nickname
|
||||||
}
|
}
|
||||||
|
|
||||||
var statusIcon: String {
|
var statusIcon: String {
|
||||||
@@ -73,14 +73,14 @@ struct BitchatPeer: Identifiable, Equatable {
|
|||||||
|
|
||||||
// Initialize from mesh service data
|
// Initialize from mesh service data
|
||||||
init(
|
init(
|
||||||
id: String,
|
peerID: PeerID,
|
||||||
noisePublicKey: Data,
|
noisePublicKey: Data,
|
||||||
nickname: String,
|
nickname: String,
|
||||||
lastSeen: Date = Date(),
|
lastSeen: Date = Date(),
|
||||||
isConnected: Bool = false,
|
isConnected: Bool = false,
|
||||||
isReachable: Bool = false
|
isReachable: Bool = false
|
||||||
) {
|
) {
|
||||||
self.id = id
|
self.peerID = peerID
|
||||||
self.noisePublicKey = noisePublicKey
|
self.noisePublicKey = noisePublicKey
|
||||||
self.nickname = nickname
|
self.nickname = nickname
|
||||||
self.lastSeen = lastSeen
|
self.lastSeen = lastSeen
|
||||||
@@ -93,8 +93,6 @@ struct BitchatPeer: Identifiable, Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func == (lhs: BitchatPeer, rhs: BitchatPeer) -> Bool {
|
static func == (lhs: BitchatPeer, rhs: BitchatPeer) -> Bool {
|
||||||
lhs.id == rhs.id
|
lhs.peerID == rhs.peerID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ final class BLEService: NSObject {
|
|||||||
|
|
||||||
// 3. Peer Information (single source of truth)
|
// 3. Peer Information (single source of truth)
|
||||||
private struct PeerInfo {
|
private struct PeerInfo {
|
||||||
let id: String
|
let peerID: PeerID
|
||||||
var nickname: String
|
var nickname: String
|
||||||
var isConnected: Bool
|
var isConnected: Bool
|
||||||
var noisePublicKey: Data?
|
var noisePublicKey: Data?
|
||||||
@@ -380,13 +380,13 @@ final class BLEService: NSObject {
|
|||||||
collectionsQueue.sync {
|
collectionsQueue.sync {
|
||||||
let snapshot = Array(peers.values)
|
let snapshot = Array(peers.values)
|
||||||
let resolvedNames = PeerDisplayNameResolver.resolve(
|
let resolvedNames = PeerDisplayNameResolver.resolve(
|
||||||
snapshot.map { ($0.id, $0.nickname, $0.isConnected) },
|
snapshot.map { ($0.peerID, $0.nickname, $0.isConnected) },
|
||||||
selfNickname: myNickname
|
selfNickname: myNickname
|
||||||
)
|
)
|
||||||
return snapshot.map { info in
|
return snapshot.map { info in
|
||||||
TransportPeerSnapshot(
|
TransportPeerSnapshot(
|
||||||
id: info.id,
|
peerID: info.peerID,
|
||||||
nickname: resolvedNames[PeerID(str: info.id)] ?? info.nickname,
|
nickname: resolvedNames[info.peerID] ?? info.nickname,
|
||||||
isConnected: info.isConnected,
|
isConnected: info.isConnected,
|
||||||
noisePublicKey: info.noisePublicKey,
|
noisePublicKey: info.noisePublicKey,
|
||||||
lastSeen: info.lastSeen
|
lastSeen: info.lastSeen
|
||||||
@@ -528,7 +528,7 @@ final class BLEService: NSObject {
|
|||||||
func getPeerNicknames() -> [PeerID: String] {
|
func getPeerNicknames() -> [PeerID: String] {
|
||||||
return collectionsQueue.sync {
|
return collectionsQueue.sync {
|
||||||
let connected = peers.filter { $0.value.isConnected }
|
let connected = peers.filter { $0.value.isConnected }
|
||||||
let tuples = connected.map { ($0.key, $0.value.nickname, true) }
|
let tuples = connected.map { (PeerID(str: $0.key), $0.value.nickname, true) }
|
||||||
return PeerDisplayNameResolver.resolve(tuples, selfNickname: myNickname)
|
return PeerDisplayNameResolver.resolve(tuples, selfNickname: myNickname)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1032,7 +1032,7 @@ extension BLEService {
|
|||||||
collectionsQueue.sync(flags: .barrier) {
|
collectionsQueue.sync(flags: .barrier) {
|
||||||
if peers[normalizedID] == nil {
|
if peers[normalizedID] == nil {
|
||||||
peers[normalizedID] = PeerInfo(
|
peers[normalizedID] = PeerInfo(
|
||||||
id: normalizedID,
|
peerID: PeerID(str: normalizedID),
|
||||||
nickname: "TestPeer_\(fromPeerID.prefix(4))",
|
nickname: "TestPeer_\(fromPeerID.prefix(4))",
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
noisePublicKey: packet.senderID,
|
noisePublicKey: packet.senderID,
|
||||||
@@ -2464,7 +2464,7 @@ extension BLEService {
|
|||||||
if let existing = existingPeer, existing.isConnected {
|
if let existing = existingPeer, existing.isConnected {
|
||||||
// Update lastSeen and identity info
|
// Update lastSeen and identity info
|
||||||
peers[peerID] = PeerInfo(
|
peers[peerID] = PeerInfo(
|
||||||
id: existing.id,
|
peerID: existing.peerID,
|
||||||
nickname: announcement.nickname,
|
nickname: announcement.nickname,
|
||||||
isConnected: isDirectAnnounce || hasPeripheralConnection || hasCentralSubscription,
|
isConnected: isDirectAnnounce || hasPeripheralConnection || hasCentralSubscription,
|
||||||
noisePublicKey: announcement.noisePublicKey,
|
noisePublicKey: announcement.noisePublicKey,
|
||||||
@@ -2475,7 +2475,7 @@ extension BLEService {
|
|||||||
} else {
|
} else {
|
||||||
// New peer or reconnecting peer
|
// New peer or reconnecting peer
|
||||||
peers[peerID] = PeerInfo(
|
peers[peerID] = PeerInfo(
|
||||||
id: peerID,
|
peerID: PeerID(str: peerID),
|
||||||
nickname: announcement.nickname,
|
nickname: announcement.nickname,
|
||||||
isConnected: isDirectAnnounce || hasPeripheralConnection || hasCentralSubscription,
|
isConnected: isDirectAnnounce || hasPeripheralConnection || hasCentralSubscription,
|
||||||
noisePublicKey: announcement.noisePublicKey,
|
noisePublicKey: announcement.noisePublicKey,
|
||||||
@@ -2599,7 +2599,7 @@ extension BLEService {
|
|||||||
accepted = true
|
accepted = true
|
||||||
senderNickname = info.nickname
|
senderNickname = info.nickname
|
||||||
// Handle nickname collisions
|
// Handle nickname collisions
|
||||||
let hasCollision = peers.values.contains { $0.isConnected && $0.nickname == info.nickname && $0.id != peerID } || (myNickname == info.nickname)
|
let hasCollision = peers.values.contains { $0.isConnected && $0.nickname == info.nickname && $0.peerID != peerID } || (myNickname == info.nickname)
|
||||||
if hasCollision {
|
if hasCollision {
|
||||||
senderNickname += "#" + String(peerID.prefix(4))
|
senderNickname += "#" + String(peerID.prefix(4))
|
||||||
}
|
}
|
||||||
@@ -2917,10 +2917,10 @@ extension BLEService {
|
|||||||
return peers.values.map { info in
|
return peers.values.map { info in
|
||||||
var display = info.nickname
|
var display = info.nickname
|
||||||
if info.isConnected, (counts[info.nickname] ?? 0) > 1 {
|
if info.isConnected, (counts[info.nickname] ?? 0) > 1 {
|
||||||
display += "#" + String(info.id.prefix(4))
|
display += "#" + String(info.peerID.id.prefix(4))
|
||||||
}
|
}
|
||||||
return TransportPeerSnapshot(
|
return TransportPeerSnapshot(
|
||||||
id: info.id,
|
peerID: info.peerID,
|
||||||
nickname: display,
|
nickname: display,
|
||||||
isConnected: info.isConnected,
|
isConnected: info.isConnected,
|
||||||
noisePublicKey: info.noisePublicKey,
|
noisePublicKey: info.noisePublicKey,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Combine
|
|||||||
/// Abstract transport interface used by ChatViewModel and services.
|
/// Abstract transport interface used by ChatViewModel and services.
|
||||||
/// BLEService implements this protocol; a future Nostr transport can too.
|
/// BLEService implements this protocol; a future Nostr transport can too.
|
||||||
struct TransportPeerSnapshot: Equatable, Hashable {
|
struct TransportPeerSnapshot: Equatable, Hashable {
|
||||||
let id: String
|
let peerID: PeerID
|
||||||
let nickname: String
|
let nickname: String
|
||||||
let isConnected: Bool
|
let isConnected: Bool
|
||||||
let noisePublicKey: Data?
|
let noisePublicKey: Data?
|
||||||
|
|||||||
@@ -77,12 +77,12 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
|||||||
let favorites = favoritesService.favorites
|
let favorites = favoritesService.favorites
|
||||||
|
|
||||||
var enrichedPeers: [BitchatPeer] = []
|
var enrichedPeers: [BitchatPeer] = []
|
||||||
var connected: Set<String> = []
|
var connected: Set<PeerID> = []
|
||||||
var addedPeerIDs: Set<String> = []
|
var addedPeerIDs: Set<PeerID> = []
|
||||||
|
|
||||||
// Phase 1: Add all mesh peers (connected and reachable)
|
// Phase 1: Add all mesh peers (connected and reachable)
|
||||||
for peerInfo in meshPeers {
|
for peerInfo in meshPeers {
|
||||||
let peerID = peerInfo.id
|
let peerID = peerInfo.peerID
|
||||||
guard peerID != meshService.myPeerID else { continue } // Never add self
|
guard peerID != meshService.myPeerID else { continue } // Never add self
|
||||||
|
|
||||||
let peer = buildPeerFromMesh(
|
let peer = buildPeerFromMesh(
|
||||||
@@ -97,13 +97,13 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
|||||||
|
|
||||||
// Update fingerprint cache
|
// Update fingerprint cache
|
||||||
if let publicKey = peerInfo.noisePublicKey {
|
if let publicKey = peerInfo.noisePublicKey {
|
||||||
fingerprintCache[peerID] = publicKey.sha256Fingerprint()
|
fingerprintCache[peerID.id] = publicKey.sha256Fingerprint()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 2: Add offline favorites that we actively favorite
|
// Phase 2: Add offline favorites that we actively favorite
|
||||||
for (favoriteKey, favorite) in favorites where favorite.isFavorite {
|
for (favoriteKey, favorite) in favorites where favorite.isFavorite {
|
||||||
let peerID = favoriteKey.hexEncodedString()
|
let peerID = PeerID(hexData: favoriteKey)
|
||||||
|
|
||||||
// Skip if already added (connected peer)
|
// Skip if already added (connected peer)
|
||||||
if addedPeerIDs.contains(peerID) { continue }
|
if addedPeerIDs.contains(peerID) { continue }
|
||||||
@@ -114,12 +114,12 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
|||||||
}
|
}
|
||||||
if isConnectedByNickname { continue }
|
if isConnectedByNickname { continue }
|
||||||
|
|
||||||
let peer = buildPeerFromFavorite(favorite: favorite, peerID: peerID)
|
let peer = buildPeerFromFavorite(favorite: favorite, peerID: peerID.id)
|
||||||
enrichedPeers.append(peer)
|
enrichedPeers.append(peer)
|
||||||
addedPeerIDs.insert(peerID)
|
addedPeerIDs.insert(peerID)
|
||||||
|
|
||||||
// Update fingerprint cache
|
// Update fingerprint cache
|
||||||
fingerprintCache[peerID] = favoriteKey.sha256Fingerprint()
|
fingerprintCache[peerID.id] = favoriteKey.sha256Fingerprint()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 3: Sort peers
|
// Phase 3: Sort peers
|
||||||
@@ -140,7 +140,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
|||||||
var newIndex: [String: BitchatPeer] = [:]
|
var newIndex: [String: BitchatPeer] = [:]
|
||||||
|
|
||||||
for peer in enrichedPeers {
|
for peer in enrichedPeers {
|
||||||
newIndex[peer.id] = peer
|
newIndex[peer.peerID.id] = peer
|
||||||
|
|
||||||
if peer.isFavorite {
|
if peer.isFavorite {
|
||||||
favoritesList.append(peer)
|
favoritesList.append(peer)
|
||||||
@@ -155,7 +155,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
|||||||
p.isConnected || p.isReachable || p.isMutualFavorite
|
p.isConnected || p.isReachable || p.isMutualFavorite
|
||||||
}
|
}
|
||||||
self.peers = filtered
|
self.peers = filtered
|
||||||
self.connectedPeerIDs = connected
|
self.connectedPeerIDs = Set(connected.map(\.id))
|
||||||
self.favorites = favoritesList
|
self.favorites = favoritesList
|
||||||
self.mutualFavorites = mutualsList
|
self.mutualFavorites = mutualsList
|
||||||
self.peerIndex = newIndex
|
self.peerIndex = newIndex
|
||||||
@@ -184,7 +184,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
|||||||
let isReachable = peerInfo.isConnected ? true : (withinRetention && meshAttached)
|
let isReachable = peerInfo.isConnected ? true : (withinRetention && meshAttached)
|
||||||
|
|
||||||
var peer = BitchatPeer(
|
var peer = BitchatPeer(
|
||||||
id: peerInfo.id,
|
peerID: peerInfo.peerID,
|
||||||
noisePublicKey: peerInfo.noisePublicKey ?? Data(),
|
noisePublicKey: peerInfo.noisePublicKey ?? Data(),
|
||||||
nickname: peerInfo.nickname,
|
nickname: peerInfo.nickname,
|
||||||
lastSeen: peerInfo.lastSeen,
|
lastSeen: peerInfo.lastSeen,
|
||||||
@@ -207,7 +207,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
|||||||
peerID: String
|
peerID: String
|
||||||
) -> BitchatPeer {
|
) -> BitchatPeer {
|
||||||
var peer = BitchatPeer(
|
var peer = BitchatPeer(
|
||||||
id: peerID,
|
peerID: PeerID(str: peerID),
|
||||||
noisePublicKey: favorite.peerNoisePublicKey,
|
noisePublicKey: favorite.peerNoisePublicKey,
|
||||||
nickname: favorite.peerNickname,
|
nickname: favorite.peerNickname,
|
||||||
lastSeen: favorite.lastUpdated,
|
lastSeen: favorite.lastUpdated,
|
||||||
@@ -232,7 +232,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
|||||||
func getPeerID(for nickname: String) -> String? {
|
func getPeerID(for nickname: String) -> String? {
|
||||||
for peer in peers {
|
for peer in peers {
|
||||||
if peer.displayName == nickname || peer.nickname == nickname {
|
if peer.displayName == nickname || peer.nickname == nickname {
|
||||||
return peer.id
|
return peer.peerID.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -380,11 +380,11 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
|||||||
var allPeers: [BitchatPeer] { peers }
|
var allPeers: [BitchatPeer] { peers }
|
||||||
var connectedPeers: [String] { Array(connectedPeerIDs) }
|
var connectedPeers: [String] { Array(connectedPeerIDs) }
|
||||||
var favoritePeers: Set<String> {
|
var favoritePeers: Set<String> {
|
||||||
Set(favorites.compactMap { getFingerprint(for: $0.id) })
|
Set(favorites.compactMap { getFingerprint(for: $0.peerID.id) })
|
||||||
}
|
}
|
||||||
var blockedUsers: Set<String> {
|
var blockedUsers: Set<String> {
|
||||||
Set(peers.compactMap { peer in
|
Set(peers.compactMap { peer in
|
||||||
isBlocked(peer.id) ? getFingerprint(for: peer.id) : nil
|
isBlocked(peer.peerID.id) ? getFingerprint(for: peer.peerID.id) : nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import Foundation
|
|||||||
struct PeerDisplayNameResolver {
|
struct PeerDisplayNameResolver {
|
||||||
/// Computes display names with a `#xxxx` suffix for connected peers when nickname collisions occur.
|
/// Computes display names with a `#xxxx` suffix for connected peers when nickname collisions occur.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - peers: Array of tuples (id, nickname, isConnected).
|
/// - peers: Array of tuples (peerID, nickname, isConnected).
|
||||||
/// - selfNickname: The local user's current nickname, included in collision counts to suffix remotes matching it.
|
/// - selfNickname: The local user's current nickname, included in collision counts to suffix remotes matching it.
|
||||||
/// - Returns: Map of peerID -> displayName.
|
/// - Returns: Map of peerID -> displayName.
|
||||||
static func resolve(_ peers: [(id: String, nickname: String, isConnected: Bool)], selfNickname: String) -> [PeerID: String] {
|
static func resolve(_ peers: [(peerID: PeerID, nickname: String, isConnected: Bool)], selfNickname: String) -> [PeerID: String] {
|
||||||
// Count collisions among connected peers and include our own nickname
|
// Count collisions among connected peers and include our own nickname
|
||||||
var counts: [String: Int] = [:]
|
var counts: [String: Int] = [:]
|
||||||
for p in peers where p.isConnected {
|
for p in peers where p.isConnected {
|
||||||
@@ -19,9 +19,9 @@ struct PeerDisplayNameResolver {
|
|||||||
for p in peers {
|
for p in peers {
|
||||||
var name = p.nickname
|
var name = p.nickname
|
||||||
if p.isConnected, (counts[p.nickname] ?? 0) > 1 {
|
if p.isConnected, (counts[p.nickname] ?? 0) > 1 {
|
||||||
name += "#" + String(p.id.prefix(4))
|
name += "#" + String(p.peerID.id.prefix(4))
|
||||||
}
|
}
|
||||||
result[PeerID(str: p.id)] = name
|
result[p.peerID] = name
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -323,7 +323,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
func getShortIDForNoiseKey(_ fullNoiseKeyHex: String) -> String? {
|
func getShortIDForNoiseKey(_ fullNoiseKeyHex: String) -> String? {
|
||||||
// Check known peers for a noise key match
|
// Check known peers for a noise key match
|
||||||
if let match = allPeers.first(where: { $0.noisePublicKey.hexEncodedString() == fullNoiseKeyHex }) {
|
if let match = allPeers.first(where: { $0.noisePublicKey.hexEncodedString() == fullNoiseKeyHex }) {
|
||||||
return match.id
|
return match.peerID.id
|
||||||
}
|
}
|
||||||
// Also search cache mapping
|
// Also search cache mapping
|
||||||
if let pair = shortIDToNoiseKey.first(where: { $0.value == fullNoiseKeyHex }) {
|
if let pair = shortIDToNoiseKey.first(where: { $0.value == fullNoiseKeyHex }) {
|
||||||
@@ -586,10 +586,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
var uniquePeers: [String: BitchatPeer] = [:]
|
var uniquePeers: [String: BitchatPeer] = [:]
|
||||||
for peer in peers {
|
for peer in peers {
|
||||||
// Keep the first occurrence of each peer ID
|
// Keep the first occurrence of each peer ID
|
||||||
if uniquePeers[peer.id] == nil {
|
if uniquePeers[peer.peerID.id] == nil {
|
||||||
uniquePeers[peer.id] = peer
|
uniquePeers[peer.peerID.id] = peer
|
||||||
} else {
|
} else {
|
||||||
SecureLogger.warning("⚠️ Duplicate peer ID detected: \(peer.id) (\(peer.displayName))", category: .session)
|
SecureLogger.warning("⚠️ Duplicate peer ID detected: \(peer.peerID) (\(peer.displayName))", category: .session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.peerIndex = uniquePeers
|
self.peerIndex = uniquePeers
|
||||||
@@ -1220,11 +1220,11 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Find the ephemeral peer ID for this Noise key
|
// Find the ephemeral peer ID for this Noise key
|
||||||
let ephemeralPeerID = unifiedPeerService.peers.first { peer in
|
let ephemeralPeerID = unifiedPeerService.peers.first { peer in
|
||||||
peer.noisePublicKey == noisePublicKey
|
peer.noisePublicKey == noisePublicKey
|
||||||
}?.id
|
}?.peerID
|
||||||
|
|
||||||
if let ephemeralID = ephemeralPeerID {
|
if let ephemeralID = ephemeralPeerID {
|
||||||
// Found the ephemeral peer, use normal toggle
|
// Found the ephemeral peer, use normal toggle
|
||||||
unifiedPeerService.toggleFavorite(ephemeralID)
|
unifiedPeerService.toggleFavorite(ephemeralID.id)
|
||||||
// Also trigger UI update
|
// Also trigger UI update
|
||||||
objectWillChange.send()
|
objectWillChange.send()
|
||||||
} else {
|
} else {
|
||||||
@@ -3936,8 +3936,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Build current peer->seed map (excluding self)
|
// Build current peer->seed map (excluding self)
|
||||||
let myID = meshService.myPeerID
|
let myID = meshService.myPeerID
|
||||||
var currentSeeds: [String: String] = [:]
|
var currentSeeds: [String: String] = [:]
|
||||||
for p in allPeers where p.id != myID {
|
for p in allPeers where p.peerID != myID {
|
||||||
currentSeeds[p.id] = meshSeed(for: p.id)
|
currentSeeds[p.peerID.id] = meshSeed(for: p.peerID.id)
|
||||||
}
|
}
|
||||||
// If seeds unchanged and palette exists for both themes, skip
|
// If seeds unchanged and palette exists for both themes, skip
|
||||||
if currentSeeds == peerPaletteSeeds,
|
if currentSeeds == peerPaletteSeeds,
|
||||||
@@ -4322,7 +4322,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Also log any offline favorites and whether we consider them verified
|
// Also log any offline favorites and whether we consider them verified
|
||||||
let offlineFavorites = unifiedPeerService.favorites.filter { !$0.isConnected }
|
let offlineFavorites = unifiedPeerService.favorites.filter { !$0.isConnected }
|
||||||
for fav in offlineFavorites {
|
for fav in offlineFavorites {
|
||||||
let fp = unifiedPeerService.getFingerprint(for: fav.id)
|
let fp = unifiedPeerService.getFingerprint(for: fav.peerID.id)
|
||||||
let isVer = fp.flatMap { verifiedFingerprints.contains($0) } ?? false
|
let isVer = fp.flatMap { verifiedFingerprints.contains($0) } ?? false
|
||||||
let fpShort = fp?.prefix(8) ?? "nil"
|
let fpShort = fp?.prefix(8) ?? "nil"
|
||||||
SecureLogger.info("⭐️ Favorite offline: \(fav.nickname) fp=\(fpShort) verified=\(isVer)", category: .security)
|
SecureLogger.info("⭐️ Favorite offline: \(fav.nickname) fp=\(fpShort) verified=\(isVer)", category: .security)
|
||||||
@@ -4574,24 +4574,24 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
guard let peer = unifiedPeerService.peers.first(where: { $0.noisePublicKey.hexEncodedString().lowercased() == targetNoise }) else {
|
guard let peer = unifiedPeerService.peers.first(where: { $0.noisePublicKey.hexEncodedString().lowercased() == targetNoise }) else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
let peerID = peer.id
|
let peerID = peer.peerID
|
||||||
// If we already have a pending verification with this peer, don't send another
|
// If we already have a pending verification with this peer, don't send another
|
||||||
if pendingQRVerifications[peerID] != nil {
|
if pendingQRVerifications[peerID.id] != nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// Generate nonceA
|
// Generate nonceA
|
||||||
var nonce = Data(count: 16)
|
var nonce = Data(count: 16)
|
||||||
_ = nonce.withUnsafeMutableBytes { SecRandomCopyBytes(kSecRandomDefault, 16, $0.baseAddress!) }
|
_ = nonce.withUnsafeMutableBytes { SecRandomCopyBytes(kSecRandomDefault, 16, $0.baseAddress!) }
|
||||||
var pending = PendingVerification(noiseKeyHex: qr.noiseKeyHex, signKeyHex: qr.signKeyHex, nonceA: nonce, startedAt: Date(), sent: false)
|
var pending = PendingVerification(noiseKeyHex: qr.noiseKeyHex, signKeyHex: qr.signKeyHex, nonceA: nonce, startedAt: Date(), sent: false)
|
||||||
pendingQRVerifications[peerID] = pending
|
pendingQRVerifications[peerID.id] = pending
|
||||||
// If Noise session is established, send immediately; otherwise trigger handshake and send on auth
|
// If Noise session is established, send immediately; otherwise trigger handshake and send on auth
|
||||||
let noise = meshService.getNoiseService()
|
let noise = meshService.getNoiseService()
|
||||||
if noise.hasEstablishedSession(with: PeerID(str: peerID)) {
|
if noise.hasEstablishedSession(with: peerID) {
|
||||||
meshService.sendVerifyChallenge(to: PeerID(str: peerID), noiseKeyHex: qr.noiseKeyHex, nonceA: nonce)
|
meshService.sendVerifyChallenge(to: peerID, noiseKeyHex: qr.noiseKeyHex, nonceA: nonce)
|
||||||
pending.sent = true
|
pending.sent = true
|
||||||
pendingQRVerifications[peerID] = pending
|
pendingQRVerifications[peerID.id] = pending
|
||||||
} else {
|
} else {
|
||||||
meshService.triggerHandshake(with: PeerID(str: peerID))
|
meshService.triggerHandshake(with: peerID)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -4766,7 +4766,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
/// Clean up stale unread peer IDs that no longer exist in the peer list
|
/// Clean up stale unread peer IDs that no longer exist in the peer list
|
||||||
@MainActor
|
@MainActor
|
||||||
private func cleanupStaleUnreadPeerIDs() {
|
private func cleanupStaleUnreadPeerIDs() {
|
||||||
let currentPeerIDs = Set(unifiedPeerService.peers.map { $0.id })
|
let currentPeerIDs = Set(unifiedPeerService.peers.map { $0.peerID.id })
|
||||||
let staleIDs = unreadPrivateMessages.subtracting(currentPeerIDs)
|
let staleIDs = unreadPrivateMessages.subtracting(currentPeerIDs)
|
||||||
|
|
||||||
if !staleIDs.isEmpty {
|
if !staleIDs.isEmpty {
|
||||||
@@ -5253,7 +5253,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
@MainActor
|
@MainActor
|
||||||
private func mirrorToEphemeralIfNeeded(_ message: BitchatMessage, targetPeerID: String, key: Data?) {
|
private func mirrorToEphemeralIfNeeded(_ message: BitchatMessage, targetPeerID: String, key: Data?) {
|
||||||
guard let key,
|
guard let key,
|
||||||
let ephemeralPeerID = unifiedPeerService.peers.first(where: { $0.noisePublicKey == key })?.id,
|
let ephemeralPeerID = unifiedPeerService.peers.first(where: { $0.noisePublicKey == key })?.peerID.id,
|
||||||
ephemeralPeerID != targetPeerID
|
ephemeralPeerID != targetPeerID
|
||||||
else {
|
else {
|
||||||
return
|
return
|
||||||
@@ -5290,7 +5290,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
private func handleViewingThisChat(_ message: BitchatMessage, targetPeerID: String, key: Data?, senderPubkey: String) {
|
private func handleViewingThisChat(_ message: BitchatMessage, targetPeerID: String, key: Data?, senderPubkey: String) {
|
||||||
unreadPrivateMessages.remove(targetPeerID)
|
unreadPrivateMessages.remove(targetPeerID)
|
||||||
if let key,
|
if let key,
|
||||||
let ephemeralPeerID = unifiedPeerService.peers.first(where: { $0.noisePublicKey == key })?.id {
|
let ephemeralPeerID = unifiedPeerService.peers.first(where: { $0.noisePublicKey == key })?.peerID.id {
|
||||||
unreadPrivateMessages.remove(ephemeralPeerID)
|
unreadPrivateMessages.remove(ephemeralPeerID)
|
||||||
}
|
}
|
||||||
if !sentReadReceipts.contains(message.id) {
|
if !sentReadReceipts.contains(message.id) {
|
||||||
@@ -5322,7 +5322,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
unreadPrivateMessages.insert(targetPeerID)
|
unreadPrivateMessages.insert(targetPeerID)
|
||||||
if let key,
|
if let key,
|
||||||
let ephemeralPeerID = unifiedPeerService.peers.first(where: { $0.noisePublicKey == key })?.id,
|
let ephemeralPeerID = unifiedPeerService.peers.first(where: { $0.noisePublicKey == key })?.peerID.id,
|
||||||
ephemeralPeerID != targetPeerID {
|
ephemeralPeerID != targetPeerID {
|
||||||
unreadPrivateMessages.insert(ephemeralPeerID)
|
unreadPrivateMessages.insert(ephemeralPeerID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -989,7 +989,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.id(viewModel.allPeers.map { "\($0.id)-\($0.isConnected)" }.joined())
|
.id(viewModel.allPeers.map { "\($0.peerID)-\($0.isConnected)" }.joined())
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
@@ -1085,7 +1085,7 @@ struct ContentView: View {
|
|||||||
return (n, n > 0 ? standardGreen : Color.secondary)
|
return (n, n > 0 ? standardGreen : Color.secondary)
|
||||||
case .mesh:
|
case .mesh:
|
||||||
let counts = viewModel.allPeers.reduce(into: (others: 0, mesh: 0)) { counts, peer in
|
let counts = viewModel.allPeers.reduce(into: (others: 0, mesh: 0)) { counts, peer in
|
||||||
guard peer.id != viewModel.meshService.myPeerID else { return }
|
guard peer.peerID != viewModel.meshService.myPeerID else { return }
|
||||||
if peer.isConnected { counts.mesh += 1; counts.others += 1 }
|
if peer.isConnected { counts.mesh += 1; counts.others += 1 }
|
||||||
else if peer.isReachable { counts.others += 1 }
|
else if peer.isReachable { counts.others += 1 }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -443,7 +443,7 @@ struct LocationChannelsSheet: View {
|
|||||||
// Count mesh-connected OR mesh-reachable peers (exclude self)
|
// Count mesh-connected OR mesh-reachable peers (exclude self)
|
||||||
let myID = viewModel.meshService.myPeerID
|
let myID = viewModel.meshService.myPeerID
|
||||||
return viewModel.allPeers.reduce(0) { acc, peer in
|
return viewModel.allPeers.reduce(0) { acc, peer in
|
||||||
if peer.id != myID && (peer.isConnected || peer.isReachable) { return acc + 1 }
|
if peer.peerID != myID && (peer.isConnected || peer.isReachable) { return acc + 1 }
|
||||||
return acc
|
return acc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,20 @@ struct MeshPeerList: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
let myPeerID = viewModel.meshService.myPeerID
|
||||||
|
let mapped: [(peer: BitchatPeer, isMe: Bool, hasUnread: Bool, enc: EncryptionStatus)] = viewModel.allPeers.map { peer in
|
||||||
|
let isMe = peer.peerID == myPeerID
|
||||||
|
let hasUnread = viewModel.hasUnreadMessages(for: peer.peerID.id)
|
||||||
|
let enc = viewModel.getEncryptionStatus(for: peer.peerID.id)
|
||||||
|
return (peer, isMe, hasUnread, enc)
|
||||||
|
}
|
||||||
|
// Stable visual order without mutating state here
|
||||||
|
let currentIDs = mapped.map { $0.peer.peerID.id }
|
||||||
|
let displayIDs = orderedIDs.filter { currentIDs.contains($0) } + currentIDs.filter { !orderedIDs.contains($0) }
|
||||||
|
let peers: [(peer: BitchatPeer, isMe: Bool, hasUnread: Bool, enc: EncryptionStatus)] = displayIDs.compactMap { id in
|
||||||
|
mapped.first(where: { $0.peer.peerID.id == id })
|
||||||
|
}
|
||||||
|
|
||||||
if viewModel.allPeers.isEmpty {
|
if viewModel.allPeers.isEmpty {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
Text(Strings.noneNearby)
|
Text(Strings.noneNearby)
|
||||||
@@ -27,27 +41,13 @@ struct MeshPeerList: View {
|
|||||||
.padding(.top, 12)
|
.padding(.top, 12)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let myPeerID = viewModel.meshService.myPeerID
|
|
||||||
let mapped: [(peer: BitchatPeer, isMe: Bool, hasUnread: Bool, enc: EncryptionStatus)] = viewModel.allPeers.map { peer in
|
|
||||||
let isMe = peer.id == myPeerID
|
|
||||||
let hasUnread = viewModel.hasUnreadMessages(for: peer.id)
|
|
||||||
let enc = viewModel.getEncryptionStatus(for: peer.id)
|
|
||||||
return (peer, isMe, hasUnread, enc)
|
|
||||||
}
|
|
||||||
// Stable visual order without mutating state here
|
|
||||||
let currentIDs = mapped.map { $0.peer.id }
|
|
||||||
let displayIDs = orderedIDs.filter { currentIDs.contains($0) } + currentIDs.filter { !orderedIDs.contains($0) }
|
|
||||||
let peers: [(peer: BitchatPeer, isMe: Bool, hasUnread: Bool, enc: EncryptionStatus)] = displayIDs.compactMap { id in
|
|
||||||
mapped.first(where: { $0.peer.id == id })
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
ForEach(0..<peers.count, id: \.self) { idx in
|
ForEach(0..<peers.count, id: \.self) { idx in
|
||||||
let item = peers[idx]
|
let item = peers[idx]
|
||||||
let peer = item.peer
|
let peer = item.peer
|
||||||
let isMe = item.isMe
|
let isMe = item.isMe
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
let assigned = viewModel.colorForMeshPeer(id: peer.id, isDark: colorScheme == .dark)
|
let assigned = viewModel.colorForMeshPeer(id: peer.peerID.id, isDark: colorScheme == .dark)
|
||||||
let baseColor = isMe ? Color.orange : assigned
|
let baseColor = isMe ? Color.orange : assigned
|
||||||
if isMe {
|
if isMe {
|
||||||
Image(systemName: "person.fill")
|
Image(systemName: "person.fill")
|
||||||
@@ -89,7 +89,7 @@ struct MeshPeerList: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isMe, viewModel.isPeerBlocked(peer.id) {
|
if !isMe, viewModel.isPeerBlocked(peer.peerID.id) {
|
||||||
Image(systemName: "nosign")
|
Image(systemName: "nosign")
|
||||||
.font(.bitchatSystem(size: 10))
|
.font(.bitchatSystem(size: 10))
|
||||||
.foregroundColor(.red)
|
.foregroundColor(.red)
|
||||||
@@ -105,7 +105,7 @@ struct MeshPeerList: View {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Offline: prefer showing verified badge from persisted fingerprints
|
// Offline: prefer showing verified badge from persisted fingerprints
|
||||||
if let fp = viewModel.getFingerprint(for: peer.id),
|
if let fp = viewModel.getFingerprint(for: peer.peerID.id),
|
||||||
viewModel.verifiedFingerprints.contains(fp) {
|
viewModel.verifiedFingerprints.contains(fp) {
|
||||||
Image(systemName: "checkmark.seal.fill")
|
Image(systemName: "checkmark.seal.fill")
|
||||||
.font(.bitchatSystem(size: 10))
|
.font(.bitchatSystem(size: 10))
|
||||||
@@ -130,7 +130,7 @@ struct MeshPeerList: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !isMe {
|
if !isMe {
|
||||||
Button(action: { onToggleFavorite(peer.id) }) {
|
Button(action: { onToggleFavorite(peer.peerID.id) }) {
|
||||||
Image(systemName: (peer.favoriteStatus?.isFavorite ?? false) ? "star.fill" : "star")
|
Image(systemName: (peer.favoriteStatus?.isFavorite ?? false) ? "star.fill" : "star")
|
||||||
.font(.bitchatSystem(size: 12))
|
.font(.bitchatSystem(size: 12))
|
||||||
.foregroundColor((peer.favoriteStatus?.isFavorite ?? false) ? .yellow : secondaryTextColor)
|
.foregroundColor((peer.favoriteStatus?.isFavorite ?? false) ? .yellow : secondaryTextColor)
|
||||||
@@ -142,16 +142,16 @@ struct MeshPeerList: View {
|
|||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
.padding(.top, idx == 0 ? 10 : 0)
|
.padding(.top, idx == 0 ? 10 : 0)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture { if !isMe { onTapPeer(peer.id) } }
|
.onTapGesture { if !isMe { onTapPeer(peer.peerID.id) } }
|
||||||
.onTapGesture(count: 2) { if !isMe { onShowFingerprint(peer.id) } }
|
.onTapGesture(count: 2) { if !isMe { onShowFingerprint(peer.peerID.id) } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Seed and update order outside result builder
|
// Seed and update order outside result builder
|
||||||
.onAppear {
|
.onAppear {
|
||||||
let currentIDs = mapped.map { $0.peer.id }
|
let currentIDs = mapped.map { $0.peer.peerID.id }
|
||||||
orderedIDs = currentIDs
|
orderedIDs = currentIDs
|
||||||
}
|
}
|
||||||
.onChange(of: mapped.map { $0.peer.id }) { ids in
|
.onChange(of: mapped.map { $0.peer.peerID.id }) { ids in
|
||||||
var newOrder = orderedIDs
|
var newOrder = orderedIDs
|
||||||
newOrder.removeAll { !ids.contains($0) }
|
newOrder.removeAll { !ids.contains($0) }
|
||||||
for id in ids where !newOrder.contains(id) { newOrder.append(id) }
|
for id in ids where !newOrder.contains(id) { newOrder.append(id) }
|
||||||
|
|||||||
Reference in New Issue
Block a user