mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 03:05:19 +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
|
||||
|
||||
/// Represents a peer in the BitChat network with all associated metadata
|
||||
struct BitchatPeer: Identifiable, Equatable {
|
||||
let id: String // Hex-encoded peer ID
|
||||
struct BitchatPeer: Equatable {
|
||||
let peerID: PeerID // Hex-encoded peer ID
|
||||
let noisePublicKey: Data
|
||||
let nickname: String
|
||||
let lastSeen: Date
|
||||
@@ -51,7 +51,7 @@ struct BitchatPeer: Identifiable, Equatable {
|
||||
|
||||
// Display helpers
|
||||
var displayName: String {
|
||||
nickname.isEmpty ? String(id.prefix(8)) : nickname
|
||||
nickname.isEmpty ? String(peerID.id.prefix(8)) : nickname
|
||||
}
|
||||
|
||||
var statusIcon: String {
|
||||
@@ -73,14 +73,14 @@ struct BitchatPeer: Identifiable, Equatable {
|
||||
|
||||
// Initialize from mesh service data
|
||||
init(
|
||||
id: String,
|
||||
peerID: PeerID,
|
||||
noisePublicKey: Data,
|
||||
nickname: String,
|
||||
lastSeen: Date = Date(),
|
||||
isConnected: Bool = false,
|
||||
isReachable: Bool = false
|
||||
) {
|
||||
self.id = id
|
||||
self.peerID = peerID
|
||||
self.noisePublicKey = noisePublicKey
|
||||
self.nickname = nickname
|
||||
self.lastSeen = lastSeen
|
||||
@@ -93,8 +93,6 @@ struct BitchatPeer: Identifiable, Equatable {
|
||||
}
|
||||
|
||||
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)
|
||||
private struct PeerInfo {
|
||||
let id: String
|
||||
let peerID: PeerID
|
||||
var nickname: String
|
||||
var isConnected: Bool
|
||||
var noisePublicKey: Data?
|
||||
@@ -380,13 +380,13 @@ final class BLEService: NSObject {
|
||||
collectionsQueue.sync {
|
||||
let snapshot = Array(peers.values)
|
||||
let resolvedNames = PeerDisplayNameResolver.resolve(
|
||||
snapshot.map { ($0.id, $0.nickname, $0.isConnected) },
|
||||
snapshot.map { ($0.peerID, $0.nickname, $0.isConnected) },
|
||||
selfNickname: myNickname
|
||||
)
|
||||
return snapshot.map { info in
|
||||
TransportPeerSnapshot(
|
||||
id: info.id,
|
||||
nickname: resolvedNames[PeerID(str: info.id)] ?? info.nickname,
|
||||
peerID: info.peerID,
|
||||
nickname: resolvedNames[info.peerID] ?? info.nickname,
|
||||
isConnected: info.isConnected,
|
||||
noisePublicKey: info.noisePublicKey,
|
||||
lastSeen: info.lastSeen
|
||||
@@ -528,7 +528,7 @@ final class BLEService: NSObject {
|
||||
func getPeerNicknames() -> [PeerID: String] {
|
||||
return collectionsQueue.sync {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1032,7 +1032,7 @@ extension BLEService {
|
||||
collectionsQueue.sync(flags: .barrier) {
|
||||
if peers[normalizedID] == nil {
|
||||
peers[normalizedID] = PeerInfo(
|
||||
id: normalizedID,
|
||||
peerID: PeerID(str: normalizedID),
|
||||
nickname: "TestPeer_\(fromPeerID.prefix(4))",
|
||||
isConnected: true,
|
||||
noisePublicKey: packet.senderID,
|
||||
@@ -2464,7 +2464,7 @@ extension BLEService {
|
||||
if let existing = existingPeer, existing.isConnected {
|
||||
// Update lastSeen and identity info
|
||||
peers[peerID] = PeerInfo(
|
||||
id: existing.id,
|
||||
peerID: existing.peerID,
|
||||
nickname: announcement.nickname,
|
||||
isConnected: isDirectAnnounce || hasPeripheralConnection || hasCentralSubscription,
|
||||
noisePublicKey: announcement.noisePublicKey,
|
||||
@@ -2475,7 +2475,7 @@ extension BLEService {
|
||||
} else {
|
||||
// New peer or reconnecting peer
|
||||
peers[peerID] = PeerInfo(
|
||||
id: peerID,
|
||||
peerID: PeerID(str: peerID),
|
||||
nickname: announcement.nickname,
|
||||
isConnected: isDirectAnnounce || hasPeripheralConnection || hasCentralSubscription,
|
||||
noisePublicKey: announcement.noisePublicKey,
|
||||
@@ -2599,7 +2599,7 @@ extension BLEService {
|
||||
accepted = true
|
||||
senderNickname = info.nickname
|
||||
// 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 {
|
||||
senderNickname += "#" + String(peerID.prefix(4))
|
||||
}
|
||||
@@ -2917,10 +2917,10 @@ extension BLEService {
|
||||
return peers.values.map { info in
|
||||
var display = info.nickname
|
||||
if info.isConnected, (counts[info.nickname] ?? 0) > 1 {
|
||||
display += "#" + String(info.id.prefix(4))
|
||||
display += "#" + String(info.peerID.id.prefix(4))
|
||||
}
|
||||
return TransportPeerSnapshot(
|
||||
id: info.id,
|
||||
peerID: info.peerID,
|
||||
nickname: display,
|
||||
isConnected: info.isConnected,
|
||||
noisePublicKey: info.noisePublicKey,
|
||||
|
||||
@@ -4,7 +4,7 @@ import Combine
|
||||
/// Abstract transport interface used by ChatViewModel and services.
|
||||
/// BLEService implements this protocol; a future Nostr transport can too.
|
||||
struct TransportPeerSnapshot: Equatable, Hashable {
|
||||
let id: String
|
||||
let peerID: PeerID
|
||||
let nickname: String
|
||||
let isConnected: Bool
|
||||
let noisePublicKey: Data?
|
||||
|
||||
@@ -77,12 +77,12 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
||||
let favorites = favoritesService.favorites
|
||||
|
||||
var enrichedPeers: [BitchatPeer] = []
|
||||
var connected: Set<String> = []
|
||||
var addedPeerIDs: Set<String> = []
|
||||
var connected: Set<PeerID> = []
|
||||
var addedPeerIDs: Set<PeerID> = []
|
||||
|
||||
// Phase 1: Add all mesh peers (connected and reachable)
|
||||
for peerInfo in meshPeers {
|
||||
let peerID = peerInfo.id
|
||||
let peerID = peerInfo.peerID
|
||||
guard peerID != meshService.myPeerID else { continue } // Never add self
|
||||
|
||||
let peer = buildPeerFromMesh(
|
||||
@@ -97,13 +97,13 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
||||
|
||||
// Update fingerprint cache
|
||||
if let publicKey = peerInfo.noisePublicKey {
|
||||
fingerprintCache[peerID] = publicKey.sha256Fingerprint()
|
||||
fingerprintCache[peerID.id] = publicKey.sha256Fingerprint()
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: Add offline favorites that we actively favorite
|
||||
for (favoriteKey, favorite) in favorites where favorite.isFavorite {
|
||||
let peerID = favoriteKey.hexEncodedString()
|
||||
let peerID = PeerID(hexData: favoriteKey)
|
||||
|
||||
// Skip if already added (connected peer)
|
||||
if addedPeerIDs.contains(peerID) { continue }
|
||||
@@ -114,12 +114,12 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
||||
}
|
||||
if isConnectedByNickname { continue }
|
||||
|
||||
let peer = buildPeerFromFavorite(favorite: favorite, peerID: peerID)
|
||||
let peer = buildPeerFromFavorite(favorite: favorite, peerID: peerID.id)
|
||||
enrichedPeers.append(peer)
|
||||
addedPeerIDs.insert(peerID)
|
||||
|
||||
// Update fingerprint cache
|
||||
fingerprintCache[peerID] = favoriteKey.sha256Fingerprint()
|
||||
fingerprintCache[peerID.id] = favoriteKey.sha256Fingerprint()
|
||||
}
|
||||
|
||||
// Phase 3: Sort peers
|
||||
@@ -140,7 +140,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
||||
var newIndex: [String: BitchatPeer] = [:]
|
||||
|
||||
for peer in enrichedPeers {
|
||||
newIndex[peer.id] = peer
|
||||
newIndex[peer.peerID.id] = peer
|
||||
|
||||
if peer.isFavorite {
|
||||
favoritesList.append(peer)
|
||||
@@ -155,7 +155,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
||||
p.isConnected || p.isReachable || p.isMutualFavorite
|
||||
}
|
||||
self.peers = filtered
|
||||
self.connectedPeerIDs = connected
|
||||
self.connectedPeerIDs = Set(connected.map(\.id))
|
||||
self.favorites = favoritesList
|
||||
self.mutualFavorites = mutualsList
|
||||
self.peerIndex = newIndex
|
||||
@@ -184,7 +184,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
||||
let isReachable = peerInfo.isConnected ? true : (withinRetention && meshAttached)
|
||||
|
||||
var peer = BitchatPeer(
|
||||
id: peerInfo.id,
|
||||
peerID: peerInfo.peerID,
|
||||
noisePublicKey: peerInfo.noisePublicKey ?? Data(),
|
||||
nickname: peerInfo.nickname,
|
||||
lastSeen: peerInfo.lastSeen,
|
||||
@@ -207,7 +207,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
||||
peerID: String
|
||||
) -> BitchatPeer {
|
||||
var peer = BitchatPeer(
|
||||
id: peerID,
|
||||
peerID: PeerID(str: peerID),
|
||||
noisePublicKey: favorite.peerNoisePublicKey,
|
||||
nickname: favorite.peerNickname,
|
||||
lastSeen: favorite.lastUpdated,
|
||||
@@ -232,7 +232,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
||||
func getPeerID(for nickname: String) -> String? {
|
||||
for peer in peers {
|
||||
if peer.displayName == nickname || peer.nickname == nickname {
|
||||
return peer.id
|
||||
return peer.peerID.id
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -380,11 +380,11 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
||||
var allPeers: [BitchatPeer] { peers }
|
||||
var connectedPeers: [String] { Array(connectedPeerIDs) }
|
||||
var favoritePeers: Set<String> {
|
||||
Set(favorites.compactMap { getFingerprint(for: $0.id) })
|
||||
Set(favorites.compactMap { getFingerprint(for: $0.peerID.id) })
|
||||
}
|
||||
var blockedUsers: Set<String> {
|
||||
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 {
|
||||
/// Computes display names with a `#xxxx` suffix for connected peers when nickname collisions occur.
|
||||
/// - 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.
|
||||
/// - 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
|
||||
var counts: [String: Int] = [:]
|
||||
for p in peers where p.isConnected {
|
||||
@@ -19,9 +19,9 @@ struct PeerDisplayNameResolver {
|
||||
for p in peers {
|
||||
var name = p.nickname
|
||||
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
|
||||
}
|
||||
|
||||
@@ -323,7 +323,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
func getShortIDForNoiseKey(_ fullNoiseKeyHex: String) -> String? {
|
||||
// Check known peers for a noise key match
|
||||
if let match = allPeers.first(where: { $0.noisePublicKey.hexEncodedString() == fullNoiseKeyHex }) {
|
||||
return match.id
|
||||
return match.peerID.id
|
||||
}
|
||||
// Also search cache mapping
|
||||
if let pair = shortIDToNoiseKey.first(where: { $0.value == fullNoiseKeyHex }) {
|
||||
@@ -586,10 +586,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
var uniquePeers: [String: BitchatPeer] = [:]
|
||||
for peer in peers {
|
||||
// Keep the first occurrence of each peer ID
|
||||
if uniquePeers[peer.id] == nil {
|
||||
uniquePeers[peer.id] = peer
|
||||
if uniquePeers[peer.peerID.id] == nil {
|
||||
uniquePeers[peer.peerID.id] = peer
|
||||
} 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
|
||||
@@ -1220,11 +1220,11 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
// Find the ephemeral peer ID for this Noise key
|
||||
let ephemeralPeerID = unifiedPeerService.peers.first { peer in
|
||||
peer.noisePublicKey == noisePublicKey
|
||||
}?.id
|
||||
}?.peerID
|
||||
|
||||
if let ephemeralID = ephemeralPeerID {
|
||||
// Found the ephemeral peer, use normal toggle
|
||||
unifiedPeerService.toggleFavorite(ephemeralID)
|
||||
unifiedPeerService.toggleFavorite(ephemeralID.id)
|
||||
// Also trigger UI update
|
||||
objectWillChange.send()
|
||||
} else {
|
||||
@@ -3936,8 +3936,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
// Build current peer->seed map (excluding self)
|
||||
let myID = meshService.myPeerID
|
||||
var currentSeeds: [String: String] = [:]
|
||||
for p in allPeers where p.id != myID {
|
||||
currentSeeds[p.id] = meshSeed(for: p.id)
|
||||
for p in allPeers where p.peerID != myID {
|
||||
currentSeeds[p.peerID.id] = meshSeed(for: p.peerID.id)
|
||||
}
|
||||
// If seeds unchanged and palette exists for both themes, skip
|
||||
if currentSeeds == peerPaletteSeeds,
|
||||
@@ -4322,7 +4322,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
// Also log any offline favorites and whether we consider them verified
|
||||
let offlineFavorites = unifiedPeerService.favorites.filter { !$0.isConnected }
|
||||
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 fpShort = fp?.prefix(8) ?? "nil"
|
||||
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 {
|
||||
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 pendingQRVerifications[peerID] != nil {
|
||||
if pendingQRVerifications[peerID.id] != nil {
|
||||
return true
|
||||
}
|
||||
// Generate nonceA
|
||||
var nonce = Data(count: 16)
|
||||
_ = nonce.withUnsafeMutableBytes { SecRandomCopyBytes(kSecRandomDefault, 16, $0.baseAddress!) }
|
||||
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
|
||||
let noise = meshService.getNoiseService()
|
||||
if noise.hasEstablishedSession(with: PeerID(str: peerID)) {
|
||||
meshService.sendVerifyChallenge(to: PeerID(str: peerID), noiseKeyHex: qr.noiseKeyHex, nonceA: nonce)
|
||||
if noise.hasEstablishedSession(with: peerID) {
|
||||
meshService.sendVerifyChallenge(to: peerID, noiseKeyHex: qr.noiseKeyHex, nonceA: nonce)
|
||||
pending.sent = true
|
||||
pendingQRVerifications[peerID] = pending
|
||||
pendingQRVerifications[peerID.id] = pending
|
||||
} else {
|
||||
meshService.triggerHandshake(with: PeerID(str: peerID))
|
||||
meshService.triggerHandshake(with: peerID)
|
||||
}
|
||||
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
|
||||
@MainActor
|
||||
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)
|
||||
|
||||
if !staleIDs.isEmpty {
|
||||
@@ -5253,7 +5253,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
@MainActor
|
||||
private func mirrorToEphemeralIfNeeded(_ message: BitchatMessage, targetPeerID: String, key: Data?) {
|
||||
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
|
||||
else {
|
||||
return
|
||||
@@ -5290,7 +5290,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
private func handleViewingThisChat(_ message: BitchatMessage, targetPeerID: String, key: Data?, senderPubkey: String) {
|
||||
unreadPrivateMessages.remove(targetPeerID)
|
||||
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)
|
||||
}
|
||||
if !sentReadReceipts.contains(message.id) {
|
||||
@@ -5322,7 +5322,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
|
||||
unreadPrivateMessages.insert(targetPeerID)
|
||||
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 {
|
||||
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()
|
||||
@@ -1085,7 +1085,7 @@ struct ContentView: View {
|
||||
return (n, n > 0 ? standardGreen : Color.secondary)
|
||||
case .mesh:
|
||||
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 }
|
||||
else if peer.isReachable { counts.others += 1 }
|
||||
}
|
||||
|
||||
@@ -443,7 +443,7 @@ struct LocationChannelsSheet: View {
|
||||
// Count mesh-connected OR mesh-reachable peers (exclude self)
|
||||
let myID = viewModel.meshService.myPeerID
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,20 @@ struct MeshPeerList: 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 {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(Strings.noneNearby)
|
||||
@@ -27,27 +41,13 @@ struct MeshPeerList: View {
|
||||
.padding(.top, 12)
|
||||
}
|
||||
} 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) {
|
||||
ForEach(0..<peers.count, id: \.self) { idx in
|
||||
let item = peers[idx]
|
||||
let peer = item.peer
|
||||
let isMe = item.isMe
|
||||
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
|
||||
if isMe {
|
||||
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")
|
||||
.font(.bitchatSystem(size: 10))
|
||||
.foregroundColor(.red)
|
||||
@@ -105,7 +105,7 @@ struct MeshPeerList: View {
|
||||
}
|
||||
} else {
|
||||
// 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) {
|
||||
Image(systemName: "checkmark.seal.fill")
|
||||
.font(.bitchatSystem(size: 10))
|
||||
@@ -130,7 +130,7 @@ struct MeshPeerList: View {
|
||||
}
|
||||
|
||||
if !isMe {
|
||||
Button(action: { onToggleFavorite(peer.id) }) {
|
||||
Button(action: { onToggleFavorite(peer.peerID.id) }) {
|
||||
Image(systemName: (peer.favoriteStatus?.isFavorite ?? false) ? "star.fill" : "star")
|
||||
.font(.bitchatSystem(size: 12))
|
||||
.foregroundColor((peer.favoriteStatus?.isFavorite ?? false) ? .yellow : secondaryTextColor)
|
||||
@@ -142,16 +142,16 @@ struct MeshPeerList: View {
|
||||
.padding(.vertical, 4)
|
||||
.padding(.top, idx == 0 ? 10 : 0)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture { if !isMe { onTapPeer(peer.id) } }
|
||||
.onTapGesture(count: 2) { if !isMe { onShowFingerprint(peer.id) } }
|
||||
.onTapGesture { if !isMe { onTapPeer(peer.peerID.id) } }
|
||||
.onTapGesture(count: 2) { if !isMe { onShowFingerprint(peer.peerID.id) } }
|
||||
}
|
||||
}
|
||||
// Seed and update order outside result builder
|
||||
.onAppear {
|
||||
let currentIDs = mapped.map { $0.peer.id }
|
||||
let currentIDs = mapped.map { $0.peer.peerID.id }
|
||||
orderedIDs = currentIDs
|
||||
}
|
||||
.onChange(of: mapped.map { $0.peer.id }) { ids in
|
||||
.onChange(of: mapped.map { $0.peer.peerID.id }) { ids in
|
||||
var newOrder = orderedIDs
|
||||
newOrder.removeAll { !ids.contains($0) }
|
||||
for id in ids where !newOrder.contains(id) { newOrder.append(id) }
|
||||
|
||||
Reference in New Issue
Block a user