diff --git a/bitchat/Models/PeerID.swift b/bitchat/Models/PeerID.swift index 5861156f..1248a520 100644 --- a/bitchat/Models/PeerID.swift +++ b/bitchat/Models/PeerID.swift @@ -35,7 +35,7 @@ struct PeerID: Equatable, Hashable { // Private so the callers have to go through a convenience init private init(prefix: Prefix, bare: any StringProtocol) { self.prefix = prefix - self.bare = String(bare) + self.bare = String(bare).lowercased() } } diff --git a/bitchat/Services/BLEService.swift b/bitchat/Services/BLEService.swift index 88c31965..76c24636 100644 --- a/bitchat/Services/BLEService.swift +++ b/bitchat/Services/BLEService.swift @@ -2360,7 +2360,6 @@ extension BLEService { private func configureNoiseServiceCallbacks(for service: NoiseEncryptionService) { service.onPeerAuthenticated = { [weak self] peerID, fingerprint in - let peerID = PeerID(str: peerID) SecureLogger.debug("🔐 Noise session authenticated with \(peerID), fingerprint: \(fingerprint.prefix(16))...") self?.messageQueue.async { [weak self] in self?.sendPendingMessagesAfterHandshake(for: peerID) @@ -3612,7 +3611,7 @@ extension BLEService { private func checkPeerConnectivity() { let now = Date() - var disconnectedPeers: [String] = [] + var disconnectedPeers: [PeerID] = [] let peerIDsForLinkState: [PeerID] = collectionsQueue.sync { Array(peers.keys) } var cachedLinkStates: [PeerID: (hasPeripheral: Bool, hasCentral: Bool)] = [:] for peerID in peerIDsForLinkState { @@ -3635,7 +3634,7 @@ extension BLEService { var updated = peer updated.isConnected = false peers[peerID] = updated - disconnectedPeers.append(peerID.id) + disconnectedPeers.append(peerID) } } // Cleanup: remove peers that are not connected and past reachability retention @@ -3660,7 +3659,7 @@ extension BLEService { let currentPeerIDs = self.collectionsQueue.sync { self.currentPeerIDs } for peerID in disconnectedPeers { - self.delegate?.didDisconnectFromPeer(PeerID(str: peerID)) + self.delegate?.didDisconnectFromPeer(peerID) } // Publish snapshots so UnifiedPeerService updates connection/reachability icons self.requestPeerDataPublish() diff --git a/bitchat/Services/NoiseEncryptionService.swift b/bitchat/Services/NoiseEncryptionService.swift index aa0d6ea6..5d99a227 100644 --- a/bitchat/Services/NoiseEncryptionService.swift +++ b/bitchat/Services/NoiseEncryptionService.swift @@ -177,18 +177,18 @@ final class NoiseEncryptionService { private let rekeyCheckInterval: TimeInterval = 60.0 // Check every minute // Callbacks - private var onPeerAuthenticatedHandlers: [((String, String) -> Void)] = [] // Array of handlers for peer authentication + private var onPeerAuthenticatedHandlers: [((PeerID, String) -> Void)] = [] // Array of handlers for peer authentication var onHandshakeRequired: ((PeerID) -> Void)? // peerID needs handshake // Add a handler for peer authentication - func addOnPeerAuthenticatedHandler(_ handler: @escaping (String, String) -> Void) { + func addOnPeerAuthenticatedHandler(_ handler: @escaping (PeerID, String) -> Void) { serviceQueue.async(flags: .barrier) { [weak self] in self?.onPeerAuthenticatedHandlers.append(handler) } } // Legacy support - setting this will add to the handlers array - var onPeerAuthenticated: ((String, String) -> Void)? { + var onPeerAuthenticated: ((PeerID, String) -> Void)? { get { nil } // Always return nil for backward compatibility set { if let handler = newValue { @@ -546,7 +546,7 @@ final class NoiseEncryptionService { // Notify all handlers about authentication serviceQueue.async { [weak self] in self?.onPeerAuthenticatedHandlers.forEach { handler in - handler(peerID.id, fingerprint) + handler(peerID, fingerprint) } } } diff --git a/bitchat/Services/NotificationService.swift b/bitchat/Services/NotificationService.swift index 1f5a972c..31d28c1c 100644 --- a/bitchat/Services/NotificationService.swift +++ b/bitchat/Services/NotificationService.swift @@ -61,11 +61,11 @@ final class NotificationService { sendLocalNotification(title: title, body: body, identifier: identifier) } - func sendPrivateMessageNotification(from sender: String, message: String, peerID: String) { + func sendPrivateMessageNotification(from sender: String, message: String, peerID: PeerID) { let title = "🔒 DM from \(sender)" let body = message let identifier = "private-\(UUID().uuidString)" - let userInfo = ["peerID": peerID, "senderName": sender] + let userInfo = ["peerID": peerID.id, "senderName": sender] sendLocalNotification(title: title, body: body, identifier: identifier, userInfo: userInfo) } diff --git a/bitchat/Sync/GossipSyncManager.swift b/bitchat/Sync/GossipSyncManager.swift index 7f3cebbb..e8b953b7 100644 --- a/bitchat/Sync/GossipSyncManager.swift +++ b/bitchat/Sync/GossipSyncManager.swift @@ -25,7 +25,7 @@ final class GossipSyncManager { // Storage: broadcast messages (ordered by insert), and latest announce per sender private var messages: [String: BitchatPacket] = [:] // idHex -> packet private var messageOrder: [String] = [] - private var latestAnnouncementByPeer: [String: (id: String, packet: BitchatPacket)] = [:] + private var latestAnnouncementByPeer: [PeerID: (id: String, packet: BitchatPacket)] = [:] // Timer private var periodicTimer: DispatchSourceTimer? @@ -101,8 +101,8 @@ final class GossipSyncManager { if isAnnounce { guard isAnnouncementFresh(packet) else { - let sender = packet.senderID.hexEncodedString().lowercased() - removeState(forNormalizedPeerID: sender) + let sender = PeerID(hexData: packet.senderID) + removeState(for: sender) return } } @@ -121,7 +121,7 @@ final class GossipSyncManager { } } } else if isAnnounce { - let sender = packet.senderID.hexEncodedString().lowercased() + let sender = PeerID(hexData: packet.senderID) latestAnnouncementByPeer[sender] = (id: idHex, packet: packet) } } @@ -270,33 +270,28 @@ final class GossipSyncManager { let nowMs = UInt64(now.timeIntervalSince1970 * 1000) guard nowMs >= timeoutMs else { return } let cutoff = nowMs - timeoutMs - let stalePeerIDs = latestAnnouncementByPeer.compactMap { (peerHex, pair) -> String? in - pair.packet.timestamp < cutoff ? peerHex.lowercased() : nil + let stalePeerIDs = latestAnnouncementByPeer.compactMap { peerID, pair in + pair.packet.timestamp < cutoff ? peerID : nil } guard !stalePeerIDs.isEmpty else { return } for peerKey in stalePeerIDs { - removeState(forNormalizedPeerID: peerKey) + removeState(for: peerKey) } } // Explicit removal hook for LEAVE/stale peer func removeAnnouncementForPeer(_ peerID: PeerID) { queue.async { [weak self] in - self?._removeAnnouncementForPeer(peerID) + self?.removeState(for: peerID) } } - private func _removeAnnouncementForPeer(_ peerID: PeerID) { - let normalizedPeerID = peerID.id.lowercased() - removeState(forNormalizedPeerID: normalizedPeerID) - } - - private func removeState(forNormalizedPeerID normalizedPeerID: String) { - _ = latestAnnouncementByPeer.removeValue(forKey: normalizedPeerID) + private func removeState(for peerID: PeerID) { + _ = latestAnnouncementByPeer.removeValue(forKey: peerID) // Remove messages from this peer // Collect IDs to remove first to avoid concurrent modification let messageIdsToRemove = messages.compactMap { (id, message) -> String? in - message.senderID.hexEncodedString().lowercased() == normalizedPeerID ? id : nil + PeerID(hexData: message.senderID) == peerID ? id : nil } // Remove messages and update messageOrder @@ -317,13 +312,13 @@ extension GossipSyncManager { func _hasAnnouncement(for peerID: PeerID) -> Bool { queue.sync { - latestAnnouncementByPeer[peerID.id.lowercased()] != nil + latestAnnouncementByPeer[peerID] != nil } } func _messageCount(for peerID: PeerID) -> Int { queue.sync { - messages.values.filter { $0.senderID.hexEncodedString().lowercased() == peerID.id.lowercased() }.count + messages.values.filter { PeerID(hexData: $0.senderID) == peerID }.count } } } diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index 65760e87..6984baa9 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -1155,7 +1155,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { NotificationService.shared.sendPrivateMessageNotification( from: senderName, message: pm.content, - peerID: convKey.id + peerID: convKey ) } } @@ -1876,7 +1876,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { NotificationService.shared.sendPrivateMessageNotification( from: senderName, message: pm.content, - peerID: convKey.id + peerID: convKey ) } @@ -1956,8 +1956,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { func isSelfSender(peerID: PeerID?, displayName: String?) -> Bool { guard let peerID else { return false } if peerID == meshService.myPeerID { return true } - let lowerPeer = peerID.id.lowercased() - guard lowerPeer.hasPrefix("nostr") else { return false } + guard peerID.isGeoDM || peerID.isGeoChat else { return false } if let mapped = nostrKeyMapping[peerID]?.lowercased(), let gh = currentGeohash, @@ -1967,10 +1966,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { if let gh = currentGeohash, let myIdentity = try? idBridge.deriveIdentity(forGeohash: gh) { - let myLower = myIdentity.publicKeyHex.lowercased() - let shortLen = TransportConfig.nostrShortKeyDisplayLength - let shortKey = "nostr:" + myLower.prefix(shortLen) - if lowerPeer == shortKey { return true } + if peerID == PeerID(nostr: myIdentity.publicKeyHex) { return true } let suffix = myIdentity.publicKeyHex.suffix(4) let expected = (nickname + "#" + suffix).lowercased() if let display = displayName?.lowercased(), display == expected { return true } @@ -2453,7 +2449,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } let targetPeer = selectedPrivateChatPeer - let message = enqueueMediaMessage(content: "[voice] \(url.lastPathComponent)", targetPeer: targetPeer?.id) + let message = enqueueMediaMessage(content: "[voice] \(url.lastPathComponent)", targetPeer: targetPeer) let messageID = message.id let transferId = makeTransferID(messageID: messageID) @@ -2532,7 +2528,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { ) guard packet.encode() != nil else { throw MediaSendError.encodingFailed } await MainActor.run { - let message = self.enqueueMediaMessage(content: "[image] \(outputURL.lastPathComponent)", targetPeer: targetPeer?.id) + let message = self.enqueueMediaMessage(content: "[image] \(outputURL.lastPathComponent)", targetPeer: targetPeer) let messageID = message.id let transferId = self.makeTransferID(messageID: messageID) self.registerTransfer(transferId: transferId, messageID: messageID) @@ -2574,7 +2570,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } @MainActor - private func enqueueMediaMessage(content: String, targetPeer: String?) -> BitchatMessage { + private func enqueueMediaMessage(content: String, targetPeer: PeerID?) -> BitchatMessage { let timestamp = Date() let message: BitchatMessage @@ -2591,7 +2587,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { deliveryStatus: .sending ) var chats = privateChats - chats[PeerID(str: peerID), default: []].append(message) + chats[peerID, default: []].append(message) privateChats = chats trimMessagesIfNeeded() } else { @@ -2604,7 +2600,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { originalSender: nil, isPrivate: false, recipientNickname: nil, - senderPeerID: PeerID(str: senderPeerID), + senderPeerID: senderPeerID, deliveryStatus: .sending ) messages.append(message) @@ -2629,29 +2625,28 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { return message } - private func currentPublicSender() -> (name: String, peerID: String) { + private func currentPublicSender() -> (name: String, peerID: PeerID) { var displaySender = nickname var senderPeerID = meshService.myPeerID if case .location(let ch) = activeChannel, let identity = try? idBridge.deriveIdentity(forGeohash: ch.geohash) { let suffix = String(identity.publicKeyHex.suffix(4)) displaySender = nickname + "#" + suffix - let shortKey = identity.publicKeyHex.prefix(TransportConfig.nostrShortKeyDisplayLength) - senderPeerID = PeerID(str: "nostr:\(shortKey)") + senderPeerID = PeerID(nostr: identity.publicKeyHex) } - return (displaySender, senderPeerID.id) + return (displaySender, senderPeerID) } @MainActor - private func nicknameForPeer(_ peerID: String) -> String { - if let name = meshService.peerNickname(peerID: PeerID(str: peerID)) { + private func nicknameForPeer(_ peerID: PeerID) -> String { + if let name = meshService.peerNickname(peerID: peerID) { return name } - if let favorite = FavoritesPersistenceService.shared.getFavoriteStatus(forPeerID: PeerID(str: peerID)), + if let favorite = FavoritesPersistenceService.shared.getFavoriteStatus(forPeerID: peerID), !favorite.peerNickname.isEmpty { return favorite.peerNickname } - if let noiseKey = Data(hexString: peerID), + if let noiseKey = Data(hexString: peerID.id), let favorite = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey), !favorite.peerNickname.isEmpty { return favorite.peerNickname @@ -4881,7 +4876,6 @@ 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) @@ -5874,7 +5868,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { NotificationService.shared.sendPrivateMessageNotification( from: senderNickname, message: messageContent, - peerID: targetPeerID.id + peerID: targetPeerID ) } } @@ -6267,7 +6261,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { NotificationService.shared.sendPrivateMessageNotification( from: message.sender, message: message.content, - peerID: peerID.id + peerID: peerID ) } } else { diff --git a/bitchat/Views/ContentView.swift b/bitchat/Views/ContentView.swift index 250e41fe..393659df 100644 --- a/bitchat/Views/ContentView.swift +++ b/bitchat/Views/ContentView.swift @@ -47,7 +47,7 @@ struct ContentView: View { @State private var commandSuggestions: [String] = [] @State private var showMessageActions = false @State private var selectedMessageSender: String? - @State private var selectedMessageSenderID: String? + @State private var selectedMessageSenderID: PeerID? @FocusState private var isNicknameFieldFocused: Bool @State private var isAtBottomPublic: Bool = true @State private var isAtBottomPrivate: Bool = true @@ -285,12 +285,12 @@ struct ContentView: View { Button("content.actions.direct_message") { if let peerID = selectedMessageSenderID { - if peerID.hasPrefix("nostr:") { - if let full = viewModel.fullNostrHex(forSenderPeerID: PeerID(str: peerID)) { + if peerID.isGeoChat { + if let full = viewModel.fullNostrHex(forSenderPeerID: peerID) { viewModel.startGeohashDM(withPubkeyHex: full) } } else { - viewModel.startPrivateChat(with: PeerID(str: peerID)) + viewModel.startPrivateChat(with: peerID) } withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) { showSidebar = true @@ -312,8 +312,8 @@ 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(str: peerID)), + if let peerID = selectedMessageSenderID, peerID.isGeoChat, + let full = viewModel.fullNostrHex(forSenderPeerID: peerID), let sender = selectedMessageSender { viewModel.blockGeohashUser(pubkeyHexLowercased: full, displayName: sender) } else if let sender = selectedMessageSender { @@ -794,7 +794,7 @@ struct ContentView: View { case "user": let id = url.path.trimmingCharacters(in: CharacterSet(charactersIn: "/")) let peerID = PeerID(str: id.removingPercentEncoding ?? id) - selectedMessageSenderID = peerID.id + selectedMessageSenderID = peerID if peerID.isGeoDM || peerID.isGeoChat { selectedMessageSender = viewModel.geohashDisplayName(for: peerID)