diff --git a/bitchat/BitchatApp.swift b/bitchat/BitchatApp.swift index 0cfe5dfc..0bf9012d 100644 --- a/bitchat/BitchatApp.swift +++ b/bitchat/BitchatApp.swift @@ -221,7 +221,7 @@ final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate { // Get peer ID from userInfo if let peerID = userInfo["peerID"] as? String { DispatchQueue.main.async { - self.chatViewModel?.startPrivateChat(with: peerID) + self.chatViewModel?.startPrivateChat(with: PeerID(str: peerID)) } } } diff --git a/bitchat/Services/CommandProcessor.swift b/bitchat/Services/CommandProcessor.swift index 0c6a73ba..3ac8d23c 100644 --- a/bitchat/Services/CommandProcessor.swift +++ b/bitchat/Services/CommandProcessor.swift @@ -148,9 +148,9 @@ final class CommandProcessor { if chatViewModel?.selectedPrivateChatPeer != nil { // In private chat - if let peerNickname = meshService?.peerNickname(peerID: PeerID(str: targetPeerID)) { + if let peerNickname = meshService?.peerNickname(peerID: targetPeerID) { let personalMessage = "* \(emoji) \(myNickname) \(action) you\(suffix) *" - meshService?.sendPrivateMessage(personalMessage, to: PeerID(str: targetPeerID), + meshService?.sendPrivateMessage(personalMessage, to: targetPeerID, recipientNickname: peerNickname, messageID: UUID().uuidString) // Also add a local system message so the sender sees a natural-language confirmation @@ -214,7 +214,7 @@ final class CommandProcessor { let nickname = targetName.hasPrefix("@") ? String(targetName.dropFirst()) : targetName if let peerID = chatViewModel?.getPeerIDForNickname(nickname), - let fingerprint = meshService?.getFingerprint(for: PeerID(str: peerID)) { + let fingerprint = meshService?.getFingerprint(for: peerID) { if identityManager.isBlocked(fingerprint: fingerprint) { return .success(message: "\(nickname) is already blocked") } @@ -258,7 +258,7 @@ final class CommandProcessor { let nickname = targetName.hasPrefix("@") ? String(targetName.dropFirst()) : targetName if let peerID = chatViewModel?.getPeerIDForNickname(nickname), - let fingerprint = meshService?.getFingerprint(for: PeerID(str: peerID)) { + let fingerprint = meshService?.getFingerprint(for: peerID) { if !identityManager.isBlocked(fingerprint: fingerprint) { return .success(message: "\(nickname) is not blocked") } @@ -285,7 +285,7 @@ final class CommandProcessor { let nickname = targetName.hasPrefix("@") ? String(targetName.dropFirst()) : targetName guard let peerID = chatViewModel?.getPeerIDForNickname(nickname), - let noisePublicKey = Data(hexString: peerID) else { + let noisePublicKey = Data(hexString: peerID.id) else { return .error(message: "can't find peer: \(nickname)") } diff --git a/bitchat/Services/UnifiedPeerService.swift b/bitchat/Services/UnifiedPeerService.swift index f21973f8..9ca8619e 100644 --- a/bitchat/Services/UnifiedPeerService.swift +++ b/bitchat/Services/UnifiedPeerService.swift @@ -235,10 +235,10 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate { } /// Get peer ID for nickname - func getPeerID(for nickname: String) -> String? { + func getPeerID(for nickname: String) -> PeerID? { for peer in peers { if peer.displayName == nickname || peer.nickname == nickname { - return peer.peerID.id + return peer.peerID } } return nil @@ -347,7 +347,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate { // MARK: - Compatibility Methods (for easy migration) var allPeers: [BitchatPeer] { peers } - var connectedPeers: [PeerID] { Array(connectedPeerIDs) } + var connectedPeers: Set { connectedPeerIDs } var favoritePeers: Set { Set(favorites.compactMap { getFingerprint(for: $0.peerID) }) } diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index c42b8b8a..82dd524d 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -154,19 +154,14 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { @MainActor private func normalizedSenderKey(for message: BitchatMessage) -> String { - if let spid = message.senderPeerID?.id { - if spid.hasPrefix("nostr:") || spid.hasPrefix("nostr_") { - let bare: String = { - if spid.hasPrefix("nostr:") { return String(spid.dropFirst(6)) } - if spid.hasPrefix("nostr_") { return String(spid.dropFirst(6)) } - return spid - }() - let full = (nostrKeyMapping[spid] ?? bare).lowercased() + if let spid = message.senderPeerID { + if spid.isGeoChat || spid.isGeoDM { + let full = (nostrKeyMapping[spid] ?? spid.bare).lowercased() return "nostr:" + full - } else if spid.count == 16, let full = getNoiseKeyForShortID(spid)?.lowercased() { + } else if spid.id.count == 16, let full = getNoiseKeyForShortID(spid)?.lowercased() { return "noise:" + full } else { - return "mesh:" + spid.lowercased() + return "mesh:" + spid.id.lowercased() } } return "name:" + message.sender.lowercased() @@ -252,7 +247,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // Computed properties for compatibility @MainActor - var connectedPeers: [String] { Array(unifiedPeerService.connectedPeerIDs.map(\.id)) } + var connectedPeers: Set { unifiedPeerService.connectedPeerIDs } @Published var allPeers: [BitchatPeer] = [] var privateChats: [PeerID: [BitchatMessage]] { get { privateChatManager.privateChats } @@ -287,7 +282,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { .map { ($0, privateChats[$0]?.last?.timestamp ?? Date.distantPast) } .sorted { $0.1 > $1.1 } if let target = unreadSorted.first?.0 { - startPrivateChat(with: target.id) + startPrivateChat(with: target) return } // Otherwise pick most recent private chat overall @@ -295,23 +290,23 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { .map { (id: $0.key, ts: $0.value.last?.timestamp ?? Date.distantPast) } .sorted { $0.ts > $1.ts } if let target = recent.first?.id { - startPrivateChat(with: target.id) + startPrivateChat(with: target) } } // - private var peerIDToPublicKeyFingerprint: [String: String] = [:] + private var peerIDToPublicKeyFingerprint: [PeerID: String] = [:] private var selectedPrivateChatFingerprint: String? = nil // Map stable short peer IDs (16-hex) to full Noise public key hex (64-hex) for session continuity - private var shortIDToNoiseKey: [String: String] = [:] + private var shortIDToNoiseKey: [PeerID: String] = [:] // Resolve full Noise key for a peer's short ID (used by UI header rendering) @MainActor - func getNoiseKeyForShortID(_ shortPeerID: String) -> String? { + private func getNoiseKeyForShortID(_ shortPeerID: PeerID) -> String? { if let mapped = shortIDToNoiseKey[shortPeerID] { return mapped } // Fallback: derive from active Noise session if available - if shortPeerID.count == 16, - let key = meshService.getNoiseService().getPeerPublicKeyData(PeerID(str: shortPeerID)) { + if shortPeerID.id.count == 16, + let key = meshService.getNoiseService().getPeerPublicKeyData(shortPeerID) { let stable = key.hexEncodedString() shortIDToNoiseKey[shortPeerID] = stable return stable @@ -321,10 +316,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // Resolve short mesh ID (16-hex) from a full Noise public key hex (64-hex) @MainActor - func getShortIDForNoiseKey(_ fullNoiseKeyHex: String) -> String? { + func getShortIDForNoiseKey(_ fullNoiseKeyHex: String) -> PeerID? { // Check known peers for a noise key match if let match = allPeers.first(where: { $0.noisePublicKey.hexEncodedString() == fullNoiseKeyHex }) { - return match.peerID.id + return match.peerID } // Also search cache mapping if let pair = shortIDToNoiseKey.first(where: { $0.value == fullNoiseKeyHex }) { @@ -332,7 +327,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } return nil } - private var peerIndex: [String: BitchatPeer] = [:] + private var peerIndex: [PeerID: BitchatPeer] = [:] // MARK: - Autocomplete Properties @@ -380,7 +375,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // MARK: - Caches // Caches for expensive computations - private var encryptionStatusCache: [String: EncryptionStatus] = [:] // key: peerID + private var encryptionStatusCache: [PeerID: EncryptionStatus] = [:] // MARK: - Social Features (Delegated to PeerStateManager) @@ -392,9 +387,9 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // MARK: - Encryption and Security // Noise Protocol encryption status - @Published var peerEncryptionStatus: [String: EncryptionStatus] = [:] // peerID -> encryption status + @Published var peerEncryptionStatus: [PeerID: EncryptionStatus] = [:] @Published var verifiedFingerprints: Set = [] // Set of verified fingerprints - @Published var showingFingerprintFor: String? = nil // Currently showing fingerprint sheet for peer + @Published var showingFingerprintFor: PeerID? = nil // Currently showing fingerprint sheet for peer // Bluetooth state management @Published var showBluetoothAlert = false @@ -443,9 +438,9 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { let startedAt: Date var sent: Bool } - private var pendingQRVerifications: [String: PendingVerification] = [:] // peerID -> pending + private var pendingQRVerifications: [PeerID: PendingVerification] = [:] // Last handled challenge nonce per peer to avoid duplicate responses - private var lastVerifyNonceByPeer: [String: Data] = [:] + private var lastVerifyNonceByPeer: [PeerID: Data] = [:] // Track when we last received a verify challenge from a peer (fingerprint-keyed) private var lastInboundVerifyChallengeAt: [String: Date] = [:] // key: fingerprint // Throttle mutual verification toasts per fingerprint @@ -491,7 +486,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { private var torInitialReadyAnnounced: Bool = false // Track Nostr pubkey mappings for unknown senders - private var nostrKeyMapping: [String: String] = [:] // senderPeerID -> nostrPubkey + private var nostrKeyMapping: [PeerID: String] = [:] // senderPeerID -> nostrPubkey // MARK: - Initialization @@ -599,11 +594,11 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { self.allPeers = peers // Update peer index for O(1) lookups // Deduplicate peers by ID to prevent crash from duplicate keys - var uniquePeers: [String: BitchatPeer] = [:] + var uniquePeers: [PeerID: BitchatPeer] = [:] for peer in peers { // Keep the first occurrence of each peer ID - if uniquePeers[peer.peerID.id] == nil { - uniquePeers[peer.peerID.id] = peer + if uniquePeers[peer.peerID] == nil { + uniquePeers[peer.peerID] = peer } else { SecureLogger.warning("⚠️ Duplicate peer ID detected: \(peer.peerID) (\(peer.displayName))", category: .session) } @@ -990,10 +985,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } // Store mapping for geohash sender IDs used in messages (ensures consistent colors) - let key16 = "nostr_" + String(event.pubkey.prefix(TransportConfig.nostrConvKeyPrefixLength)) - nostrKeyMapping[key16] = event.pubkey - let key8 = "nostr:" + String(event.pubkey.prefix(TransportConfig.nostrShortKeyDisplayLength)) - nostrKeyMapping[key8] = event.pubkey + nostrKeyMapping[PeerID(nostr_: event.pubkey)] = event.pubkey + nostrKeyMapping[PeerID(nostr: event.pubkey)] = event.pubkey // Update participants last-seen for this pubkey recordGeoParticipant(pubkeyHex: event.pubkey) @@ -1057,7 +1050,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } let messageTimestamp = Date(timeIntervalSince1970: TimeInterval(rumorTs)) - let convKey = "nostr_" + String(senderPubkey.prefix(TransportConfig.nostrConvKeyPrefixLength)) + let convKey = PeerID(nostr_: senderPubkey) nostrKeyMapping[convKey] = senderPubkey switch noisePayload.type { @@ -1076,13 +1069,12 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { private func handlePrivateMessage( payload: NoisePayload, senderPubkey: String, - convKey: String, + convKey: PeerID, id: NostrIdentity, messageTimestamp: Date ) { guard let pm = PrivateMessagePacket.decode(from: payload.data) else { return } let messageId = pm.messageID - let convKey = PeerID(str: convKey) sendDeliveryAckIfNeeded(to: messageId, senderPubKey: senderPubkey, from: id) @@ -1194,8 +1186,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { /// Check if a peer has unread messages, including messages stored under stable Noise keys and temporary Nostr peer IDs @MainActor - func hasUnreadMessages(for peerID: String) -> Bool { - let peerID = PeerID(str: peerID) + func hasUnreadMessages(for peerID: PeerID) -> Bool { // First check direct unread messages if unreadPrivateMessages.contains(peerID) { return true @@ -1235,11 +1226,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } @MainActor - func toggleFavorite(peerID: String) { + func toggleFavorite(peerID: PeerID) { // Distinguish between ephemeral peer IDs (16 hex chars) and Noise public keys (64 hex chars) // Ephemeral peer IDs are 8 bytes = 16 hex characters // Noise public keys are 32 bytes = 64 hex characters - let peerID = PeerID(str: peerID) if let noisePublicKey = peerID.noiseKey { // This is a stable Noise key hex (used in private chats) @@ -1302,16 +1292,16 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } @MainActor - func isFavorite(peerID: String) -> Bool { + func isFavorite(peerID: PeerID) -> Bool { // Distinguish between ephemeral peer IDs (16 hex chars) and Noise public keys (64 hex chars) - if peerID.count == 64, let noisePublicKey = Data(hexString: peerID) { + if let noisePublicKey = peerID.noiseKey { // This is a Noise public key if let status = FavoritesPersistenceService.shared.getFavoriteStatus(for: noisePublicKey) { return status.isFavorite } } else { // This is an ephemeral peer ID - check with UnifiedPeerService - if let peer = unifiedPeerService.getPeer(by: PeerID(str: peerID)) { + if let peer = unifiedPeerService.getPeer(by: peerID) { return peer.isFavorite } } @@ -1322,13 +1312,13 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // MARK: - Public Key and Identity Management @MainActor - func isPeerBlocked(_ peerID: String) -> Bool { - return unifiedPeerService.isBlocked(PeerID(str: peerID)) + func isPeerBlocked(_ peerID: PeerID) -> Bool { + return unifiedPeerService.isBlocked(peerID) } // Helper method to find current peer ID for a fingerprint @MainActor - private func getCurrentPeerIDForFingerprint(_ fingerprint: String) -> String? { + private func getCurrentPeerIDForFingerprint(_ fingerprint: String) -> PeerID? { // Search through all connected peers to find the one with matching fingerprint for peerID in connectedPeers { if let mappedFingerprint = peerIDToPublicKeyFingerprint[peerID], @@ -1345,7 +1335,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { guard let chatFingerprint = selectedPrivateChatFingerprint else { return } // Find current peer ID for the fingerprint - if let currentPeerID = PeerID(str: getCurrentPeerIDForFingerprint(chatFingerprint)) { + if let currentPeerID = getCurrentPeerIDForFingerprint(chatFingerprint) { // Update the selected peer if it's different if let oldPeerID = selectedPrivateChatPeer, oldPeerID != currentPeerID { @@ -1427,7 +1417,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { updatePrivateChatPeerIfNeeded() if let selectedPeer = selectedPrivateChatPeer { - sendPrivateMessage(content, to: selectedPeer.id) + sendPrivateMessage(content, to: selectedPeer) } return } @@ -1523,8 +1513,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // Track ourselves as active participant recordGeoParticipant(pubkeyHex: identity.publicKeyHex) - let shortKey = "nostr:" + identity.publicKeyHex.prefix(TransportConfig.nostrShortKeyDisplayLength) - nostrKeyMapping[shortKey] = identity.publicKeyHex + nostrKeyMapping[PeerID(nostr: identity.publicKeyHex)] = identity.publicKeyHex SecureLogger.debug("GeoTeleport: sent geo message pub=\(identity.publicKeyHex.prefix(8))… teleported=\(LocationChannelManager.shared.teleported)", category: .session) // If we tagged this as teleported, also mark our pubkey in teleportedGeo for UI @@ -1677,10 +1666,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } // Store mapping for geohash DM initiation - let key16 = "nostr_" + String(event.pubkey.prefix(TransportConfig.nostrConvKeyPrefixLength)) - nostrKeyMapping[key16] = event.pubkey - let key8 = "nostr:" + String(event.pubkey.prefix(TransportConfig.nostrShortKeyDisplayLength)) - nostrKeyMapping[key8] = event.pubkey + nostrKeyMapping[PeerID(nostr_: event.pubkey)] = event.pubkey + nostrKeyMapping[PeerID(nostr: event.pubkey)] = event.pubkey // Update participants last-seen for this pubkey recordGeoParticipant(pubkeyHex: event.pubkey) @@ -1754,7 +1741,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { return } - let convKey = "nostr_" + String(senderPubkey.prefix(16)) + let convKey = PeerID(nostr_: senderPubkey) nostrKeyMapping[convKey] = senderPubkey switch payload.type { @@ -1775,13 +1762,12 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { private func handlePrivateMessage( _ payload: NoisePayload, senderPubkey: String, - convKey: String, + convKey: PeerID, id: NostrIdentity, messageTimestamp: Date ) { guard let pm = PrivateMessagePacket.decode(from: payload.data) else { return } let messageId = pm.messageID - let convKey = PeerID(str: convKey) SecureLogger.info("GeoDM: recv PM <- sender=\(senderPubkey.prefix(8))… mid=\(messageId.prefix(8))…", category: .session) @@ -1841,9 +1827,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { objectWillChange.send() } - private func handleDelivered(_ payload: NoisePayload, senderPubkey: String, convKey: String) { + private func handleDelivered(_ payload: NoisePayload, senderPubkey: String, convKey: PeerID) { guard let messageID = String(data: payload.data, encoding: .utf8) else { return } - let convKey = PeerID(str: convKey) if let idx = privateChats[convKey]?.firstIndex(where: { $0.id == messageID }) { privateChats[convKey]?[idx].deliveryStatus = .delivered(to: displayNameForNostrPubkey(senderPubkey), at: Date()) @@ -1854,9 +1839,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } } - private func handleReadReceipt(_ payload: NoisePayload, senderPubkey: String, convKey: String) { + private func handleReadReceipt(_ payload: NoisePayload, senderPubkey: String, convKey: PeerID) { guard let messageID = String(data: payload.data, encoding: .utf8) else { return } - let convKey = PeerID(str: convKey) if let idx = privateChats[convKey]?.firstIndex(where: { $0.id == messageID }) { privateChats[convKey]?[idx].deliveryStatus = .read(by: displayNameForNostrPubkey(senderPubkey), at: Date()) @@ -1913,10 +1897,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } @MainActor - func isSelfSender(peerID: String?, displayName: String?) -> Bool { + func isSelfSender(peerID: PeerID?, displayName: String?) -> Bool { guard let peerID else { return false } if peerID == meshService.myPeerID { return true } - let lowerPeer = peerID.lowercased() + let lowerPeer = peerID.id.lowercased() guard lowerPeer.hasPrefix("nostr") else { return false } if let mapped = nostrKeyMapping[peerID]?.lowercased(), @@ -1996,7 +1980,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { if let gh = currentGeohash { if var arr = geoTimelines[gh] { arr.removeAll { msg in - if let spid = msg.senderPeerID?.id, spid.hasPrefix("nostr") { + if let spid = msg.senderPeerID, spid.isGeoDM || spid.isGeoChat { if let full = nostrKeyMapping[spid]?.lowercased() { return full == hex } } return false @@ -2007,7 +1991,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { switch activeChannel { case .location: messages.removeAll { msg in - if let spid = msg.senderPeerID?.id , spid.hasPrefix("nostr") { + if let spid = msg.senderPeerID , spid.isGeoDM || spid.isGeoChat { if let full = nostrKeyMapping[spid]?.lowercased() { return full == hex } } return false @@ -2222,9 +2206,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { /// - peerID: The recipient's peer ID /// - Note: Automatically establishes Noise encryption if not already active @MainActor - func sendPrivateMessage(_ content: String, to peerID: String) { + func sendPrivateMessage(_ content: String, to peerID: PeerID) { guard !content.isEmpty else { return } - let peerID = PeerID(str: peerID) // Check if blocked if unifiedPeerService.isBlocked(peerID) { @@ -2241,7 +2224,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // Geohash DM routing: conversation keys start with "nostr_" if peerID.isGeoDM { - sendGeohashDM(content, to: peerID.id) + sendGeohashDM(content, to: peerID) } // Determine routing method and recipient nickname @@ -2311,7 +2294,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } } - private func sendGeohashDM(_ content: String, to peerID: String) { + private func sendGeohashDM(_ content: String, to peerID: PeerID) { guard case .location(let ch) = activeChannel else { addSystemMessage( String(localized: "system.location.not_in_channel", comment: "System message when attempting to send without being in a location channel") @@ -2319,7 +2302,6 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { return } let messageID = UUID().uuidString - let peerID = PeerID(str: peerID) // Local echo in the DM thread let message = BitchatMessage( @@ -2342,7 +2324,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { objectWillChange.send() // Resolve recipient hex from mapping - guard let recipientHex = nostrKeyMapping[peerID.id] else { + guard let recipientHex = nostrKeyMapping[peerID] else { if let msgIdx = privateChats[peerID]?.firstIndex(where: { $0.id == messageID }) { privateChats[peerID]?[msgIdx].deliveryStatus = .failed( reason: String(localized: "content.delivery.reason.unknown_recipient", comment: "Failure reason when the recipient is unknown") @@ -2850,19 +2832,19 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { @MainActor func startGeohashDM(withPubkeyHex hex: String) { let convKey = PeerID(nostr_: hex) - nostrKeyMapping[convKey.id] = hex + nostrKeyMapping[convKey] = hex selectedPrivateChatPeer = convKey } @MainActor - func fullNostrHex(forSenderPeerID senderID: String) -> String? { + func fullNostrHex(forSenderPeerID senderID: PeerID) -> String? { return nostrKeyMapping[senderID] } @MainActor - func geohashDisplayName(for convKey: String) -> String { + func geohashDisplayName(for convKey: PeerID) -> String { guard let full = nostrKeyMapping[convKey] else { - let suffix = String(convKey.suffix(4)) + let suffix = String(convKey.id.suffix(4)) return "anon#\(suffix)" } let suffix = String(full.suffix(4)) @@ -2873,8 +2855,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } /// Add a local system message to a private chat (no network send) @MainActor - func addLocalPrivateSystemMessage(_ content: String, to peerID: String) { - let peerID = PeerID(str: peerID) + func addLocalPrivateSystemMessage(_ content: String, to peerID: PeerID) { let systemMessage = BitchatMessage( sender: "system", content: content, @@ -2926,12 +2907,11 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { /// - Parameter peerID: The peer's ID to start chatting with /// - Note: Switches the UI to private chat mode and loads message history @MainActor - func startPrivateChat(with peerID: String) { + func startPrivateChat(with peerID: PeerID) { // Safety check: Don't allow starting chat with ourselves if peerID == meshService.myPeerID { return } - let peerID = PeerID(str: peerID) let peerNickname = meshService.peerNickname(peerID: peerID) ?? "unknown" @@ -3147,7 +3127,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // Also mark messages as read for Nostr ACKs // This ensures read receipts are sent even for consolidated messages - markPrivateMessagesAsRead(from: peerID.id) + markPrivateMessagesAsRead(from: peerID) } func endPrivateChat() { @@ -3163,7 +3143,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // Store the Nostr pubkey if provided (for messages from unknown senders) if let nostrPubkey = notification.userInfo?["nostrPubkey"] as? String, - let senderPeerID = message.senderPeerID?.id { + let senderPeerID = message.senderPeerID { // Store mapping for read receipts nostrKeyMapping[senderPeerID] = nostrPubkey } @@ -3244,9 +3224,9 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { selectedPrivateChatPeer = newPeerID // Update fingerprint tracking if needed - if let fingerprint = peerIDToPublicKeyFingerprint[oldPeerID.id] { - peerIDToPublicKeyFingerprint.removeValue(forKey: oldPeerID.id) - peerIDToPublicKeyFingerprint[newPeerID.id] = fingerprint + if let fingerprint = peerIDToPublicKeyFingerprint[oldPeerID] { + peerIDToPublicKeyFingerprint.removeValue(forKey: oldPeerID) + peerIDToPublicKeyFingerprint[newPeerID] = fingerprint selectedPrivateChatFingerprint = fingerprint } @@ -3269,9 +3249,9 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } // Update fingerprint mapping - if let fingerprint = peerIDToPublicKeyFingerprint[oldPeerID.id] { - peerIDToPublicKeyFingerprint.removeValue(forKey: oldPeerID.id) - peerIDToPublicKeyFingerprint[newPeerID.id] = fingerprint + if let fingerprint = peerIDToPublicKeyFingerprint[oldPeerID] { + peerIDToPublicKeyFingerprint.removeValue(forKey: oldPeerID) + peerIDToPublicKeyFingerprint[newPeerID] = fingerprint } } } @@ -3281,12 +3261,12 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // Then handle favorite/unfavorite messages if applicable if let isFavorite = notification.userInfo?["isFavorite"] as? Bool { - let peerID = peerPublicKey.hexEncodedString() + let peerID = PeerID(hexData: peerPublicKey) let action = isFavorite ? "favorited" : "unfavorited" // Find peer nickname let peerNickname: String - if let nickname = meshService.peerNickname(peerID: PeerID(str: peerID)) { + if let nickname = meshService.peerNickname(peerID: peerID) { peerNickname = nickname } else if let favorite = FavoritesPersistenceService.shared.getFavoriteStatus(for: peerPublicKey) { peerNickname = favorite.peerNickname @@ -3330,10 +3310,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // When app becomes active, send read receipts for visible private chat if let peerID = selectedPrivateChatPeer { // Try immediately - self.markPrivateMessagesAsRead(from: peerID.id) + self.markPrivateMessagesAsRead(from: peerID) // And again with a delay DispatchQueue.main.asyncAfter(deadline: .now() + TransportConfig.uiAnimationMediumSeconds) { - self.markPrivateMessagesAsRead(from: peerID.id) + self.markPrivateMessagesAsRead(from: peerID) } } // Subscriptions will be resent after connections come back up @@ -3460,14 +3440,14 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } @MainActor - private func sendReadReceipt(_ receipt: ReadReceipt, to peerID: String, originalTransport: String? = nil) { + private func sendReadReceipt(_ receipt: ReadReceipt, to peerID: PeerID, originalTransport: String? = nil) { // First, try to resolve the current peer ID in case they reconnected with a new ID var actualPeerID = peerID // Check if this peer ID exists in current nicknames - if meshService.peerNickname(peerID: PeerID(str: peerID)) == nil { + if meshService.peerNickname(peerID: peerID) == nil { // Peer not found with this ID, try to find by fingerprint or nickname - if let oldNoiseKey = Data(hexString: peerID), + if let oldNoiseKey = Data(hexString: peerID.id), let favoriteStatus = FavoritesPersistenceService.shared.getFavoriteStatus(for: oldNoiseKey) { let peerNickname = favoriteStatus.peerNickname @@ -3475,7 +3455,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { for (currentPeerID, currentNickname) in meshService.getPeerNicknames() { if currentNickname == peerNickname { SecureLogger.info("📖 Resolved updated peer ID for read receipt: \(peerID) -> \(currentPeerID)", category: .session) - actualPeerID = currentPeerID.id + actualPeerID = currentPeerID break } } @@ -3487,17 +3467,16 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { return } // Use router to decide (mesh if reachable, else Nostr if available) - messageRouter.sendReadReceipt(receipt, to: PeerID(str: actualPeerID)) + messageRouter.sendReadReceipt(receipt, to: actualPeerID) } @MainActor - func markPrivateMessagesAsRead(from peerID: String) { - let peerID = PeerID(str: peerID) + func markPrivateMessagesAsRead(from peerID: PeerID) { privateChatManager.markAsRead(from: peerID) // Handle GeoDM (nostr_*) read receipts directly via per-geohash identity if peerID.isGeoDM, - let recipientHex = nostrKeyMapping[peerID.id], + let recipientHex = nostrKeyMapping[peerID], case .location(let ch) = LocationChannelManager.shared.selectedChannel, let id = try? idBridge.deriveIdentity(forGeohash: ch.geohash) { let messages = privateChats[peerID] ?? [] @@ -3538,7 +3517,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // Send Nostr read ACKs if peer has Nostr capability if peerNostrPubkey != nil { // Check messages under both ephemeral peer ID and stable Noise key - let messagesToAck = getPrivateChatMessages(for: peerID.id) + let messagesToAck = getPrivateChatMessages(for: peerID) for message in messagesToAck { // Only send read ACKs for messages from the peer (not our own) @@ -3558,9 +3537,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } @MainActor - func getPrivateChatMessages(for peerID: String) -> [BitchatMessage] { + func getPrivateChatMessages(for peerID: PeerID) -> [BitchatMessage] { var combined: [BitchatMessage] = [] - let peerID = PeerID(str: peerID) // Gather messages under the ephemeral peer ID if let ephemeralMessages = privateChats[peerID] { @@ -3608,14 +3586,14 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } @MainActor - func getPeerIDForNickname(_ nickname: String) -> String? { + func getPeerIDForNickname(_ nickname: String) -> PeerID? { // When in a geohash channel, allow resolving by geohash participant nickname switch LocationChannelManager.shared.selectedChannel { case .location: // If a disambiguation suffix is present (e.g., "name#abcd"), try exact displayName match first if nickname.contains("#") { if let person = visibleGeohashPeople().first(where: { $0.displayName == nickname }) { - let convKey = "nostr_" + String(person.id.prefix(TransportConfig.nostrConvKeyPrefixLength)) + let convKey = PeerID(nostr_: person.id) nostrKeyMapping[convKey] = person.id return convKey } @@ -3626,7 +3604,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { }().lowercased() // Try exact match against cached geoNicknames (pubkey -> nickname) if let pub = geoNicknames.first(where: { (_, nick) in nick.lowercased() == base })?.key { - let convKey = "nostr_" + String(pub.prefix(TransportConfig.nostrConvKeyPrefixLength)) + let convKey = PeerID(nostr_: pub) nostrKeyMapping[convKey] = pub return convKey } @@ -3816,9 +3794,9 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { func formatMessageAsText(_ message: BitchatMessage, colorScheme: ColorScheme) -> AttributedString { // Determine if this message was sent by self (mesh, geo, or DM) let isSelf: Bool = { - if let spid = message.senderPeerID?.id { + if let spid = message.senderPeerID { // In geohash channels, compare against our per-geohash nostr short ID - if case .location(let ch) = activeChannel, spid.hasPrefix("nostr:") { + if case .location(let ch) = activeChannel, spid.isGeoChat { // Use cached identity to avoid crypto during rendering let myGeo: NostrIdentity? = { if let cached = cachedGeohashIdentity, cached.geohash == ch.geohash { @@ -3832,7 +3810,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { return nil }() if let myGeo = myGeo { - return spid == "nostr:\(myGeo.publicKeyHex.prefix(TransportConfig.nostrShortKeyDisplayLength))" + return spid == PeerID(nostr: myGeo.publicKeyHex) } } return spid == meshService.myPeerID @@ -3863,7 +3841,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { let fontWeight: Font.Weight = isSelf ? .bold : .medium senderStyle.font = .bitchatSystem(size: 14, weight: fontWeight, design: .monospaced) // Make sender clickable: encode senderPeerID into a custom URL - if let spid = message.senderPeerID?.id, let url = URL(string: "bitchat://user/\(spid.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? spid)") { + if let spid = message.senderPeerID, let url = URL(string: "bitchat://user/\(spid.toPercentEncoded())") { senderStyle.link = url } @@ -4313,10 +4291,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } @MainActor - func updateEncryptionStatusForPeer(_ peerID: String) { + private func updateEncryptionStatusForPeer(_ peerID: PeerID) { let noiseService = meshService.getNoiseService() - if noiseService.hasEstablishedSession(with: PeerID(str: peerID)) { + if noiseService.hasEstablishedSession(with: peerID) { // Check if fingerprint is verified using our persisted data if let fingerprint = getFingerprint(for: peerID), verifiedFingerprints.contains(fingerprint) { @@ -4324,7 +4302,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } else { peerEncryptionStatus[peerID] = .noiseSecured } - } else if noiseService.hasSession(with: PeerID(str: peerID)) { + } else if noiseService.hasSession(with: peerID) { // Session exists but not established - handshaking peerEncryptionStatus[peerID] = .noiseHandshaking } else { @@ -4339,7 +4317,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } @MainActor - func getEncryptionStatus(for peerID: String) -> EncryptionStatus { + func getEncryptionStatus(for peerID: PeerID) -> EncryptionStatus { // Check cache first if let cachedStatus = encryptionStatusCache[peerID] { return cachedStatus @@ -4351,7 +4329,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // Check if we've ever established a session by looking for a fingerprint let hasEverEstablishedSession = getFingerprint(for: peerID) != nil - let sessionState = meshService.getNoiseSessionState(for: PeerID(str: peerID)) + let sessionState = meshService.getNoiseSessionState(for: peerID) let status: EncryptionStatus @@ -4422,8 +4400,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } // Clear caches when data changes - private func invalidateEncryptionCache(for peerID: String? = nil) { - if let peerID = peerID { + private func invalidateEncryptionCache(for peerID: PeerID? = nil) { + if let peerID { encryptionStatusCache.removeValue(forKey: peerID) } else { encryptionStatusCache.removeAll() @@ -4441,20 +4419,15 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { @MainActor private func peerColor(for message: BitchatMessage, isDark: Bool) -> Color { - if let spid = message.senderPeerID?.id { - if spid.hasPrefix("nostr:") || spid.hasPrefix("nostr_") { - let bare: String = { - if spid.hasPrefix("nostr:") { return String(spid.dropFirst(6)) } - if spid.hasPrefix("nostr_") { return String(spid.dropFirst(6)) } - return spid - }() - let full = nostrKeyMapping[spid]?.lowercased() ?? bare.lowercased() + if let spid = message.senderPeerID { + if spid.isGeoChat || spid.isGeoDM { + let full = nostrKeyMapping[spid]?.lowercased() ?? spid.bare.lowercased() return getNostrPaletteColor(for: full, isDark: isDark) - } else if spid.count == 16 { + } else if spid.id.count == 16 { // Mesh short ID return getPeerPaletteColor(for: spid, isDark: isDark) } else { - return getPeerPaletteColor(for: spid.lowercased(), isDark: isDark) + return getPeerPaletteColor(for: PeerID(str: spid.id.lowercased()), isDark: isDark) } } // Fallback when we only have a display name @@ -4468,7 +4441,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } @MainActor - func colorForMeshPeer(id peerID: String, isDark: Bool) -> Color { + func colorForMeshPeer(id peerID: PeerID, isDark: Bool) -> Color { return getPeerPaletteColor(for: peerID, isDark: isDark) } @@ -4484,19 +4457,19 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { private var peerPaletteSeeds: [String: String] = [:] // peerID -> seed used @MainActor - private func meshSeed(for peerID: String) -> String { + private func meshSeed(for peerID: PeerID) -> String { if let full = getNoiseKeyForShortID(peerID)?.lowercased() { return "noise:" + full } - return peerID.lowercased() + return peerID.id.lowercased() } @MainActor - private func getPeerPaletteColor(for peerID: String, isDark: Bool) -> Color { + private func getPeerPaletteColor(for peerID: PeerID, isDark: Bool) -> Color { // Ensure palette up to date for current peer set and seeds rebuildPeerPaletteIfNeeded() - let entry = (isDark ? peerPaletteDark[peerID] : peerPaletteLight[peerID]) + let entry = (isDark ? peerPaletteDark[peerID.id] : peerPaletteLight[peerID.id]) let orange = Color.orange if peerID == meshService.myPeerID { return orange } let saturation: Double = isDark ? 0.80 : 0.70 @@ -4516,7 +4489,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { let myID = meshService.myPeerID var currentSeeds: [String: String] = [:] for p in allPeers where p.peerID != myID { - currentSeeds[p.peerID.id] = meshSeed(for: p.peerID.id) + currentSeeds[p.peerID.id] = meshSeed(for: p.peerID) } // If seeds unchanged and palette exists for both themes, skip if currentSeeds == peerPaletteSeeds, @@ -4798,10 +4771,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // Update encryption status in appropriate places, not during view updates @MainActor - private func updateEncryptionStatus(for peerID: String) { + private func updateEncryptionStatus(for peerID: PeerID) { let noiseService = meshService.getNoiseService() - if noiseService.hasEstablishedSession(with: PeerID(str: peerID)) { + if noiseService.hasEstablishedSession(with: peerID) { if let fingerprint = getFingerprint(for: peerID) { if verifiedFingerprints.contains(fingerprint) { peerEncryptionStatus[peerID] = .noiseVerified @@ -4812,7 +4785,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // Session established but no fingerprint yet peerEncryptionStatus[peerID] = .noiseSecured } - } else if noiseService.hasSession(with: PeerID(str: peerID)) { + } else if noiseService.hasSession(with: peerID) { peerEncryptionStatus[peerID] = .noiseHandshaking } else { peerEncryptionStatus[peerID] = Optional.none @@ -4826,27 +4799,24 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // MARK: - Fingerprint Management - func showFingerprint(for peerID: String) { + func showFingerprint(for peerID: PeerID) { showingFingerprintFor = peerID } // MARK: - Peer Lookup Helpers - func getPeer(byID peerID: String) -> BitchatPeer? { + func getPeer(byID peerID: PeerID) -> BitchatPeer? { return peerIndex[peerID] } @MainActor - func getFingerprint(for peerID: String) -> String? { - return unifiedPeerService.getFingerprint(for: PeerID(str: peerID)) + func getFingerprint(for peerID: PeerID) -> String? { + return unifiedPeerService.getFingerprint(for: peerID) } - // - - - // Helper to resolve nickname for a peer ID through various sources + /// Helper to resolve nickname for a peer ID through various sources @MainActor - func resolveNickname(for peerID: String) -> String { + private func resolveNickname(for peerID: PeerID) -> String { // Guard against empty or very short peer IDs guard !peerID.isEmpty else { return "unknown" @@ -4854,15 +4824,14 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // Check if this might already be a nickname (not a hex peer ID) // Peer IDs are hex strings, so they only contain 0-9 and a-f - let isHexID = peerID.allSatisfy { $0.isHexDigit } - if !isHexID { + if !peerID.isHex { // If it's already a nickname, just return it - return peerID + return peerID.id } // First try direct peer nicknames from mesh service let peerNicknames = meshService.getPeerNicknames() - if let nickname = peerNicknames[PeerID(str: peerID)] { + if let nickname = peerNicknames[peerID] { return nickname } @@ -4880,8 +4849,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // Use anonymous with shortened peer ID // Ensure we have at least 4 characters for the prefix - let prefixLength = min(4, peerID.count) - let prefix = String(peerID.prefix(prefixLength)) + let prefixLength = min(4, peerID.id.count) + let prefix = String(peerID.id.prefix(prefixLength)) // Avoid "anonanon" by checking if ID already starts with "anon" if prefix.starts(with: "anon") { @@ -4896,7 +4865,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } @MainActor - func verifyFingerprint(for peerID: String) { + func verifyFingerprint(for peerID: PeerID) { guard let fingerprint = getFingerprint(for: peerID) else { return } // Update secure storage with verified status @@ -4910,7 +4879,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } @MainActor - func unverifyFingerprint(for peerID: String) { + func unverifyFingerprint(for peerID: PeerID) { guard let fingerprint = getFingerprint(for: peerID) else { return } identityManager.setVerified(fingerprint: fingerprint, verified: false) identityManager.forceSave() @@ -4946,6 +4915,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { noiseService.onPeerAuthenticated = { [weak self] peerID, fingerprint in DispatchQueue.main.async { guard let self = self else { return } + let peerID = PeerID(str: peerID) SecureLogger.debug("🔐 Authenticated: \(peerID)", category: .security) @@ -4963,7 +4933,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // Cache shortID -> full Noise key mapping as soon as session authenticates if self.shortIDToNoiseKey[peerID] == nil, - let keyData = self.meshService.getNoiseService().getPeerPublicKeyData(PeerID(str: peerID)) { + let keyData = self.meshService.getNoiseService().getPeerPublicKeyData(peerID) { let stable = keyData.hexEncodedString() self.shortIDToNoiseKey[peerID] = stable SecureLogger.debug("🗺️ Mapped short peerID to Noise key for header continuity: \(peerID) -> \(stable.prefix(8))…", category: .session) @@ -4971,7 +4941,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // If a QR verification is pending but not sent yet, send it now that session is authenticated if var pending = self.pendingQRVerifications[peerID], pending.sent == false { - self.meshService.sendVerifyChallenge(to: PeerID(str: peerID), noiseKeyHex: pending.noiseKeyHex, nonceA: pending.nonceA) + self.meshService.sendVerifyChallenge(to: peerID, noiseKeyHex: pending.noiseKeyHex, nonceA: pending.nonceA) pending.sent = true self.pendingQRVerifications[peerID] = pending SecureLogger.debug("📤 Sent deferred verify challenge to \(peerID) after handshake", category: .security) @@ -4986,10 +4956,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { noiseService.onHandshakeRequired = { [weak self] peerID in DispatchQueue.main.async { guard let self = self else { return } - self.peerEncryptionStatus[peerID.id] = .noiseHandshaking + self.peerEncryptionStatus[peerID] = .noiseHandshaking // Invalidate cache when encryption status changes - self.invalidateEncryptionCache(for: peerID.id) + self.invalidateEncryptionCache(for: peerID) } } } @@ -5087,10 +5057,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.id], last == tlv.nonceA { return } - lastVerifyNonceByPeer[peerID.id] = tlv.nonceA + if let last = lastVerifyNonceByPeer[peerID], last == tlv.nonceA { return } + lastVerifyNonceByPeer[peerID] = tlv.nonceA // Record inbound challenge time keyed by stable fingerprint if available - if let fp = getFingerprint(for: peerID.id) { + if let fp = getFingerprint(for: peerID) { lastInboundVerifyChallengeAt[fp] = Date() // If we've already verified this fingerprint locally, treat this as mutual and toast immediately (responder side) if verifiedFingerprints.contains(fp) { @@ -5098,7 +5068,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.id) + let name = unifiedPeerService.getPeer(by: peerID)?.nickname ?? resolveNickname(for: peerID) NotificationService.shared.sendLocalNotification( title: "Mutual verification", body: "You and \(name) verified each other", @@ -5112,19 +5082,19 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { case .verifyResponse: guard let resp = VerificationService.shared.parseVerifyResponse(payload) else { return } // Check pending for this peer - guard let pending = pendingQRVerifications[peerID.id] else { return } + guard let pending = pendingQRVerifications[peerID] 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.id) - if let fp = getFingerprint(for: peerID.id) { + pendingQRVerifications.removeValue(forKey: peerID) + if let fp = getFingerprint(for: peerID) { 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.id) + let name = unifiedPeerService.getPeer(by: peerID)?.nickname ?? resolveNickname(for: peerID) NotificationService.shared.sendLocalNotification( title: "Verified", body: "You verified \(name)", @@ -5143,7 +5113,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { ) } } - updateEncryptionStatus(for: peerID.id) + updateEncryptionStatus(for: peerID) } } } @@ -5182,20 +5152,20 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } let peerID = peer.peerID // If we already have a pending verification with this peer, don't send another - if pendingQRVerifications[peerID.id] != nil { + if pendingQRVerifications[peerID] != 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.id] = pending + pendingQRVerifications[peerID] = pending // If Noise session is established, send immediately; otherwise trigger handshake and send on auth let noise = meshService.getNoiseService() if noise.hasEstablishedSession(with: peerID) { meshService.sendVerifyChallenge(to: peerID, noiseKeyHex: qr.noiseKeyHex, nonceA: nonce) pending.sent = true - pendingQRVerifications[peerID.id] = pending + pendingQRVerifications[peerID] = pending } else { meshService.triggerHandshake(with: peerID) } @@ -5232,7 +5202,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // Cache mapping to full Noise key for session continuity on disconnect if let peer = unifiedPeerService.getPeer(by: peerID) { let noiseKeyHex = peer.noisePublicKey.hexEncodedString() - shortIDToNoiseKey[peerID.id] = noiseKeyHex + shortIDToNoiseKey[peerID] = noiseKeyHex } // Flush any queued messages for this peer via router @@ -5247,11 +5217,11 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { 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.id] + var derivedStableKeyHex: String? = shortIDToNoiseKey[peerID] if derivedStableKeyHex == nil, let key = meshService.getNoiseService().getPeerPublicKeyData(peerID) { derivedStableKeyHex = key.hexEncodedString() - shortIDToNoiseKey[peerID.id] = derivedStableKeyHex + shortIDToNoiseKey[peerID] = derivedStableKeyHex } if let current = selectedPrivateChatPeer, current == peerID, @@ -5719,7 +5689,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { noisePayload, actualSenderNoiseKey: actualSenderNoiseKey, senderNickname: senderNickname, - targetPeerID: targetPeerID.id, + targetPeerID: targetPeerID, messageTimestamp: messageTimestamp, senderPubkey: senderPubkey ) @@ -5755,7 +5725,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { _ payload: NoisePayload, actualSenderNoiseKey: Data?, senderNickname: String, - targetPeerID: String, + targetPeerID: PeerID, messageTimestamp: Date, senderPubkey: String ) { @@ -5766,7 +5736,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // Favorite/unfavorite notifications embedded as private messages if messageContent.hasPrefix("[FAVORITED]") || messageContent.hasPrefix("[UNFAVORITED]") { if let key = actualSenderNoiseKey { - handleFavoriteNotificationFromMesh(messageContent, from: key.hexEncodedString(), senderNickname: senderNickname) + handleFavoriteNotificationFromMesh(messageContent, from: PeerID(hexData: key), senderNickname: senderNickname) } return } @@ -5800,7 +5770,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { isRelay: false, isPrivate: true, recipientNickname: nickname, - senderPeerID: PeerID(str: targetPeerID), + senderPeerID: targetPeerID, deliveryStatus: .delivered(to: nickname, at: Date()) ) @@ -5837,8 +5807,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { objectWillChange.send() } - private func isDuplicateMessage(_ messageId: String, targetPeerID: String) -> Bool { - if privateChats[PeerID(str: targetPeerID)]?.contains(where: { $0.id == messageId }) == true { + private func isDuplicateMessage(_ messageId: String, targetPeerID: PeerID) -> Bool { + if privateChats[targetPeerID]?.contains(where: { $0.id == messageId }) == true { return true } for (_, messages) in privateChats where messages.contains(where: { $0.id == messageId }) { @@ -5847,8 +5817,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { return false } - private func addMessageToPrivateChatsIfNeeded(_ message: BitchatMessage, targetPeerID: String) { - let targetPeerID = PeerID(str: targetPeerID) + private func addMessageToPrivateChatsIfNeeded(_ message: BitchatMessage, targetPeerID: PeerID) { if privateChats[targetPeerID] == nil { privateChats[targetPeerID] = [] } @@ -5862,7 +5831,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } @MainActor - private func mirrorToEphemeralIfNeeded(_ message: BitchatMessage, targetPeerID: String, key: Data?) { + private func mirrorToEphemeralIfNeeded(_ message: BitchatMessage, targetPeerID: PeerID, key: Data?) { guard let key, let ephemeralPeerID = unifiedPeerService.peers.first(where: { $0.noisePublicKey == key })?.peerID, ephemeralPeerID != targetPeerID @@ -5898,8 +5867,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } @MainActor - private func handleViewingThisChat(_ message: BitchatMessage, targetPeerID: String, key: Data?, senderPubkey: String) { - let targetPeerID = PeerID(str: targetPeerID) + private func handleViewingThisChat(_ message: BitchatMessage, targetPeerID: PeerID, key: Data?, senderPubkey: String) { unreadPrivateMessages.remove(targetPeerID) if let key, let ephemeralPeerID = unifiedPeerService.peers.first(where: { $0.noisePublicKey == key })?.peerID { @@ -5924,14 +5892,14 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { @MainActor private func markAsUnreadIfNeeded( shouldMarkAsUnread: Bool, - targetPeerID: String, + targetPeerID: PeerID, key: Data?, isRecentMessage: Bool, senderNickname: String, messageContent: String ) { guard shouldMarkAsUnread else { return } - let targetPeerID = PeerID(str: targetPeerID) + unreadPrivateMessages.insert(targetPeerID) if let key, let ephemeralPeerID = unifiedPeerService.peers.first(where: { $0.noisePublicKey == key })?.peerID, @@ -6027,7 +5995,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // @MainActor - private func handleFavoriteNotificationFromMesh(_ content: String, from peerID: String, senderNickname: String) { + private func handleFavoriteNotificationFromMesh(_ content: String, from peerID: PeerID, senderNickname: String) { // Parse the message format: "[FAVORITED]:npub..." or "[UNFAVORITED]:npub..." let isFavorite = content.hasPrefix("[FAVORITED]") let parts = content.split(separator: ":") @@ -6041,17 +6009,9 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // Get the noise public key for this peer // Try both ephemeral ID and if that fails, get from peer service - var noiseKey: Data? = nil - // First try as hex-encoded Noise key (64 chars) - if peerID.count == 64 { - noiseKey = Data(hexString: peerID) - } - // If not a hex key, get from peer service (ephemeral ID) - if noiseKey == nil, let peer = unifiedPeerService.getPeer(by: PeerID(str: peerID)) { - noiseKey = peer.noisePublicKey - } + let noiseKey = peerID.noiseKey ?? unifiedPeerService.getPeer(by: peerID)?.noisePublicKey guard let finalNoiseKey = noiseKey else { SecureLogger.warning("⚠️ Cannot get Noise key for peer \(peerID)", category: .session) @@ -6233,28 +6193,27 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } @MainActor - func sendFavoriteNotification(to peerID: String, isFavorite: Bool) { + func sendFavoriteNotification(to peerID: PeerID, isFavorite: Bool) { // Handle both ephemeral peer IDs and Noise key hex strings var noiseKey: Data? // First check if peerID is a hex-encoded Noise key - if let hexKey = Data(hexString: peerID) { + if let hexKey = Data(hexString: peerID.id) { noiseKey = hexKey } else { // It's an ephemeral peer ID, get the Noise key from UnifiedPeerService - if let peer = unifiedPeerService.getPeer(by: PeerID(str: peerID)) { + if let peer = unifiedPeerService.getPeer(by: peerID) { noiseKey = peer.noisePublicKey } } // Try mesh first for connected peers - if meshService.isPeerConnected(PeerID(str: peerID)) { - messageRouter.sendFavoriteNotification(to: PeerID(str: peerID), isFavorite: isFavorite) + if meshService.isPeerConnected(peerID) { + messageRouter.sendFavoriteNotification(to: peerID, isFavorite: isFavorite) SecureLogger.debug("📤 Sent favorite notification via BLE to \(peerID)", category: .session) } else if let key = noiseKey { // Send via Nostr for offline peers (using router) - let recipientPeerID = key.hexEncodedString() - messageRouter.sendFavoriteNotification(to: PeerID(str: recipientPeerID), isFavorite: isFavorite) + messageRouter.sendFavoriteNotification(to: PeerID(hexData: key), isFavorite: isFavorite) } else { SecureLogger.warning("⚠️ Cannot send favorite notification - peer not connected and no Nostr pubkey", category: .session) } @@ -6265,11 +6224,11 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { /// Check if a message should be blocked based on sender @MainActor private func isMessageBlocked(_ message: BitchatMessage) -> Bool { - if let peerID = message.senderPeerID?.id ?? getPeerIDForNickname(message.sender) { + if let peerID = message.senderPeerID ?? getPeerIDForNickname(message.sender) { // Check mesh/known peers first if isPeerBlocked(peerID) { return true } // Check geohash (Nostr) blocks using mapping to full pubkey - if peerID.hasPrefix("nostr") { + if peerID.isGeoChat || peerID.isGeoDM { if let full = nostrKeyMapping[peerID]?.lowercased() { if identityManager.isNostrBlocked(pubkeyHexLowercased: full) { return true } } @@ -6315,9 +6274,9 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { /// Migrate private chats when peer reconnects with new ID @MainActor - private func migratePrivateChatsIfNeeded(for peerID: String, senderNickname: String) { + private func migratePrivateChatsIfNeeded(for peerID: PeerID, senderNickname: String) { let currentFingerprint = getFingerprint(for: peerID) - let peerID = PeerID(str: peerID) + if privateChats[peerID] == nil || privateChats[peerID]?.isEmpty == true { var migratedMessages: [BitchatMessage] = [] var oldPeerIDsToRemove: [PeerID] = [] @@ -6327,7 +6286,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { for (oldPeerID, messages) in privateChats { if oldPeerID != peerID { - let oldFingerprint = peerIDToPublicKeyFingerprint[oldPeerID.id] + let oldFingerprint = peerIDToPublicKeyFingerprint[oldPeerID] // Filter messages to only recent ones let recentMessages = messages.filter { $0.timestamp > cutoffTime } @@ -6413,7 +6372,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { @MainActor private func handlePrivateMessage(_ message: BitchatMessage) { SecureLogger.debug("📥 handlePrivateMessage called for message from \(message.sender)", category: .session) - let senderPeerID = message.senderPeerID ?? PeerID(str: getPeerIDForNickname(message.sender)) + let senderPeerID = message.senderPeerID ?? getPeerIDForNickname(message.sender) guard let peerID = senderPeerID else { SecureLogger.warning("⚠️ Could not get peer ID for sender \(message.sender)", category: .session) @@ -6422,12 +6381,12 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // Check if this is a favorite/unfavorite notification if message.content.hasPrefix("[FAVORITED]") || message.content.hasPrefix("[UNFAVORITED]") { - handleFavoriteNotificationFromMesh(message.content, from: peerID.id, senderNickname: message.sender) + handleFavoriteNotificationFromMesh(message.content, from: peerID, senderNickname: message.sender) return // Don't store as a regular message } // Migrate chats if needed - migratePrivateChatsIfNeeded(for: peerID.id, senderNickname: message.sender) + migratePrivateChatsIfNeeded(for: peerID, senderNickname: message.sender) // IMPORTANT: Also consolidate messages from stable Noise key if this is an ephemeral peer // This ensures Nostr messages appear in BLE chats @@ -6488,7 +6447,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // Handle fingerprint-based chat updates if let chatFingerprint = selectedPrivateChatFingerprint, - let senderFingerprint = peerIDToPublicKeyFingerprint[peerID.id], + let senderFingerprint = peerIDToPublicKeyFingerprint[peerID], chatFingerprint == senderFingerprint && selectedPrivateChatPeer != peerID { selectedPrivateChatPeer = peerID } @@ -6534,14 +6493,14 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { originalTransport = "nostr" } - self.sendReadReceipt(receipt, to: recipientID.id, originalTransport: originalTransport) + self.sendReadReceipt(receipt, to: recipientID, originalTransport: originalTransport) } sentReadReceipts.insert(message.id) } // Mark other messages as read DispatchQueue.main.asyncAfter(deadline: .now() + TransportConfig.uiReadReceiptRetryShortSeconds) { [weak self] in - self?.markPrivateMessagesAsRead(from: peerID.id) + self?.markPrivateMessagesAsRead(from: peerID) } } } diff --git a/bitchat/Views/ContentView.swift b/bitchat/Views/ContentView.swift index dd1635e6..260df51e 100644 --- a/bitchat/Views/ContentView.swift +++ b/bitchat/Views/ContentView.swift @@ -207,7 +207,7 @@ struct ContentView: View { set: { _ in viewModel.showingFingerprintFor = nil } )) { if let peerID = viewModel.showingFingerprintFor { - FingerprintView(viewModel: viewModel, peerID: peerID) + FingerprintView(viewModel: viewModel, peerID: peerID.id) } } #if os(iOS) @@ -262,11 +262,11 @@ struct ContentView: View { Button("content.actions.direct_message") { if let peerID = selectedMessageSenderID { if peerID.hasPrefix("nostr:") { - if let full = viewModel.fullNostrHex(forSenderPeerID: peerID) { + if let full = viewModel.fullNostrHex(forSenderPeerID: PeerID(str: peerID)) { viewModel.startGeohashDM(withPubkeyHex: full) } } else { - viewModel.startPrivateChat(with: peerID) + viewModel.startPrivateChat(with: PeerID(str: peerID)) } withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) { showSidebar = true @@ -289,7 +289,7 @@ struct ContentView: View { Button("content.actions.block", role: .destructive) { // Prefer direct geohash block when we have a Nostr sender ID if let peerID = selectedMessageSenderID, peerID.hasPrefix("nostr:"), - let full = viewModel.fullNostrHex(forSenderPeerID: peerID), + let full = viewModel.fullNostrHex(forSenderPeerID: PeerID(str: peerID)), let sender = selectedMessageSender { viewModel.blockGeohashUser(pubkeyHexLowercased: full, displayName: sender) } else if let sender = selectedMessageSender { @@ -322,8 +322,8 @@ struct ContentView: View { private func messagesView(privatePeer: String?, isAtBottom: Binding) -> some View { let messages: [BitchatMessage] = { - if let privatePeer = privatePeer { - return viewModel.getPrivateChatMessages(for: privatePeer) + if let privatePeer { + return viewModel.getPrivateChatMessages(for: PeerID(str: privatePeer)) } return viewModel.messages }() @@ -517,7 +517,7 @@ struct ContentView: View { } .onAppear { // Also check when view appears - if let peerID = privatePeer { + if let peerID = PeerID(str: privatePeer) { // Try multiple times to ensure read receipts are sent viewModel.markPrivateMessagesAsRead(from: peerID) @@ -767,20 +767,20 @@ struct ContentView: View { private func handleOpenURL(_ url: URL) { guard url.scheme == "bitchat", url.host == "user" else { return } let id = url.path.trimmingCharacters(in: CharacterSet(charactersIn: "/")) - let peerID = id.removingPercentEncoding ?? id - selectedMessageSenderID = peerID + let peerID = PeerID(str: id.removingPercentEncoding ?? id) + selectedMessageSenderID = peerID.id - if peerID.hasPrefix("nostr") { + if peerID.isGeoDM || peerID.isGeoChat { selectedMessageSender = viewModel.geohashDisplayName(for: peerID) } else { - if let name = viewModel.meshService.peerNickname(peerID: PeerID(str: peerID)) { + if let name = viewModel.meshService.peerNickname(peerID: peerID) { selectedMessageSender = name } else { selectedMessageSender = viewModel.messages.last(where: { $0.senderPeerID == peerID && $0.sender != "system" })?.sender } } - if viewModel.isSelfSender(peerID: selectedMessageSenderID, displayName: selectedMessageSender) { + if viewModel.isSelfSender(peerID: peerID, displayName: selectedMessageSender) { selectedMessageSender = nil selectedMessageSenderID = nil } else { @@ -793,7 +793,7 @@ struct ContentView: View { isAtBottom: Binding) { let targetID: String? = { if let peer = privatePeer, - let last = viewModel.getPrivateChatMessages(for: peer).suffix(300).last?.id { + let last = viewModel.getPrivateChatMessages(for: PeerID(str: peer)).suffix(300).last?.id { return "dm:\(peer)|\(last)" } let contextKey: String = { @@ -931,14 +931,14 @@ struct ContentView: View { textColor: textColor, secondaryTextColor: secondaryTextColor, onTapPeer: { peerID in - viewModel.startPrivateChat(with: peerID) + viewModel.startPrivateChat(with: PeerID(str: peerID)) showSidebar = true }, onToggleFavorite: { peerID in - viewModel.toggleFavorite(peerID: peerID) + viewModel.toggleFavorite(peerID: PeerID(str: peerID)) }, onShowFingerprint: { peerID in - viewModel.showFingerprint(for: peerID) + viewModel.showFingerprint(for: PeerID(str: peerID)) } ) } @@ -977,18 +977,20 @@ struct ContentView: View { HStack(spacing: 8) { privateHeaderInfo(context: headerContext, privatePeerID: privatePeerID) + let peerID = PeerID(str: headerContext.headerPeerID) + let isFavorite = viewModel.isFavorite(peerID: peerID) if !privatePeerID.hasPrefix("nostr_") { Button(action: { - viewModel.toggleFavorite(peerID: headerContext.headerPeerID) + viewModel.toggleFavorite(peerID: peerID) }) { - Image(systemName: viewModel.isFavorite(peerID: headerContext.headerPeerID) ? "star.fill" : "star") + Image(systemName: isFavorite ? "star.fill" : "star") .font(.bitchatSystem(size: 14)) - .foregroundColor(viewModel.isFavorite(peerID: headerContext.headerPeerID) ? Color.yellow : textColor) + .foregroundColor(isFavorite ? Color.yellow : textColor) } .buttonStyle(.plain) .accessibilityLabel( - viewModel.isFavorite(peerID: headerContext.headerPeerID) + isFavorite ? String(localized: "content.accessibility.remove_favorite", comment: "Accessibility label to remove a favorite") : String(localized: "content.accessibility.add_favorite", comment: "Accessibility label to add a favorite") ) @@ -1043,7 +1045,7 @@ struct ContentView: View { private func privateHeaderInfo(context: PrivateHeaderContext, privatePeerID: String) -> some View { Button(action: { - viewModel.showFingerprint(for: context.headerPeerID) + viewModel.showFingerprint(for: PeerID(str: context.headerPeerID)) }) { HStack(spacing: 6) { if let connectionState = context.peer?.connectionState { @@ -1076,7 +1078,7 @@ struct ContentView: View { .font(.bitchatSystem(size: 14)) .foregroundColor(.purple) .accessibilityLabel(String(localized: "content.accessibility.available_nostr", comment: "Accessibility label for Nostr-available peer indicator")) - } else if viewModel.meshService.isPeerConnected(PeerID(str: context.headerPeerID)) || viewModel.connectedPeers.contains(context.headerPeerID) { + } else if viewModel.meshService.isPeerConnected(PeerID(str: context.headerPeerID)) || viewModel.connectedPeers.contains(PeerID(str: context.headerPeerID)) { Image(systemName: "dot.radiowaves.left.and.right") .font(.bitchatSystem(size: 14)) .foregroundColor(textColor) @@ -1090,11 +1092,11 @@ struct ContentView: View { if !privatePeerID.hasPrefix("nostr_") { let statusPeerID: String = { if privatePeerID.count == 64, let short = viewModel.getShortIDForNoiseKey(privatePeerID) { - return short + return short.id } return context.headerPeerID }() - let encryptionStatus = viewModel.getEncryptionStatus(for: statusPeerID) + let encryptionStatus = viewModel.getEncryptionStatus(for: PeerID(str: statusPeerID)) if let icon = encryptionStatus.icon { Image(systemName: icon) .font(.bitchatSystem(size: 14)) @@ -1129,16 +1131,16 @@ struct ContentView: View { private func makePrivateHeaderContext(for privatePeerID: String) -> PrivateHeaderContext { let headerPeerID: String = { if privatePeerID.count == 64, let short = viewModel.getShortIDForNoiseKey(privatePeerID) { - return short + return short.id } return privatePeerID }() - let peer = viewModel.getPeer(byID: headerPeerID) + let peer = viewModel.getPeer(byID: PeerID(str: headerPeerID)) let displayName: String = { if privatePeerID.hasPrefix("nostr_"), case .location(let ch) = locationManager.selectedChannel { - let disp = viewModel.geohashDisplayName(for: privatePeerID) + let disp = viewModel.geohashDisplayName(for: PeerID(str: privatePeerID)) return "#\(ch.geohash)/@\(disp)" } if let name = peer?.displayName { return name } diff --git a/bitchat/Views/FingerprintView.swift b/bitchat/Views/FingerprintView.swift index 77f75745..f82ab04b 100644 --- a/bitchat/Views/FingerprintView.swift +++ b/bitchat/Views/FingerprintView.swift @@ -66,12 +66,12 @@ struct FingerprintView: View { VStack(alignment: .leading, spacing: 16) { // Prefer short mesh ID for session/encryption status let statusPeerID: String = { - if peerID.count == 64, let short = viewModel.getShortIDForNoiseKey(peerID) { return short } + if peerID.count == 64, let short = viewModel.getShortIDForNoiseKey(peerID) { return short.id } return peerID }() // Resolve a friendly name let peerNickname: String = { - if let p = viewModel.getPeer(byID: statusPeerID) { return p.displayName } + if let p = viewModel.getPeer(byID: PeerID(str: statusPeerID)) { return p.displayName } if let name = viewModel.meshService.peerNickname(peerID: PeerID(str: statusPeerID)) { return name } if peerID.count == 64, let data = Data(hexString: peerID) { if let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: data), !fav.peerNickname.isEmpty { return fav.peerNickname } @@ -84,7 +84,7 @@ struct FingerprintView: View { return Strings.unknownPeer() }() // Accurate encryption state based on short ID session - let encryptionStatus = viewModel.getEncryptionStatus(for: statusPeerID) + let encryptionStatus = viewModel.getEncryptionStatus(for: PeerID(str: statusPeerID)) HStack { if let icon = encryptionStatus.icon { @@ -115,7 +115,7 @@ struct FingerprintView: View { .font(.bitchatSystem(size: 12, weight: .bold, design: .monospaced)) .foregroundColor(textColor.opacity(0.7)) - if let fingerprint = viewModel.getFingerprint(for: statusPeerID) { + if let fingerprint = viewModel.getFingerprint(for: PeerID(str: statusPeerID)) { Text(formatFingerprint(fingerprint)) .font(.bitchatSystem(size: 14, design: .monospaced)) .foregroundColor(textColor) @@ -176,6 +176,7 @@ struct FingerprintView: View { // Verification status if encryptionStatus == .noiseSecured || encryptionStatus == .noiseVerified { let isVerified = encryptionStatus == .noiseVerified + let peerID = PeerID(str: peerID) VStack(spacing: 12) { Text(isVerified ? Strings.verifiedBadge : Strings.notVerifiedBadge) diff --git a/bitchat/Views/MeshPeerList.swift b/bitchat/Views/MeshPeerList.swift index c82dff17..25d0bcb3 100644 --- a/bitchat/Views/MeshPeerList.swift +++ b/bitchat/Views/MeshPeerList.swift @@ -21,8 +21,8 @@ struct MeshPeerList: 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) + let hasUnread = viewModel.hasUnreadMessages(for: peer.peerID) + let enc = viewModel.getEncryptionStatus(for: peer.peerID) return (peer, isMe, hasUnread, enc) } // Stable visual order without mutating state here @@ -47,7 +47,7 @@ struct MeshPeerList: View { let peer = item.peer let isMe = item.isMe HStack(spacing: 4) { - let assigned = viewModel.colorForMeshPeer(id: peer.peerID.id, isDark: colorScheme == .dark) + let assigned = viewModel.colorForMeshPeer(id: peer.peerID, 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.peerID.id) { + if !isMe, viewModel.isPeerBlocked(peer.peerID) { 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.peerID.id), + if let fp = viewModel.getFingerprint(for: peer.peerID), viewModel.verifiedFingerprints.contains(fp) { Image(systemName: "checkmark.seal.fill") .font(.bitchatSystem(size: 10)) diff --git a/bitchat/Views/VerificationViews.swift b/bitchat/Views/VerificationViews.swift index f5c007d7..40ac301e 100644 --- a/bitchat/Views/VerificationViews.swift +++ b/bitchat/Views/VerificationViews.swift @@ -373,7 +373,7 @@ struct VerificationSheetView: View { } // Optional: Remove verification for selected peer (if verified) - if let pid = viewModel.selectedPrivateChatPeer?.id, + if let pid = viewModel.selectedPrivateChatPeer, let fp = viewModel.getFingerprint(for: pid), viewModel.verifiedFingerprints.contains(fp) { Button(action: { viewModel.unverifyFingerprint(for: pid) }) {