// // PrivateChatManager.swift // bitchat // // Manages private chat sessions and messages // This is free and unencumbered software released into the public domain. // import BitLogger import Foundation import SwiftUI /// Manages all private chat functionality final class PrivateChatManager: ObservableObject { @Published var privateChats: [PeerID: [BitchatMessage]] = [:] @Published var selectedPeer: PeerID? = nil @Published var unreadMessages: Set = [] private var selectedPeerFingerprint: String? = nil var sentReadReceipts: Set = [] // Made accessible for ChatViewModel weak var meshService: Transport? // Route acks/receipts via MessageRouter (chooses mesh or Nostr) weak var messageRouter: MessageRouter? init(meshService: Transport? = nil) { self.meshService = meshService } // Cap for messages stored per private chat private let privateChatCap = TransportConfig.privateChatCap /// Start a private chat with a peer func startChat(with peerID: PeerID) { selectedPeer = peerID // Store fingerprint for persistence across reconnections if let fingerprint = meshService?.getFingerprint(for: peerID) { selectedPeerFingerprint = fingerprint } // Mark messages as read markAsRead(from: peerID) // Initialize chat if needed if privateChats[peerID] == nil { privateChats[peerID] = [] } } /// End the current private chat func endChat() { selectedPeer = nil selectedPeerFingerprint = nil } /// Remove duplicate messages by ID and keep chronological order func sanitizeChat(for peerID: PeerID) { guard let arr = privateChats[peerID] else { return } if arr.count <= 1 { return } var indexByID: [String: Int] = [:] indexByID.reserveCapacity(arr.count) var deduped: [BitchatMessage] = [] deduped.reserveCapacity(arr.count) for msg in arr.sorted(by: { $0.timestamp < $1.timestamp }) { if let existing = indexByID[msg.id] { deduped[existing] = msg } else { indexByID[msg.id] = deduped.count deduped.append(msg) } } privateChats[peerID] = deduped } /// Mark messages from a peer as read func markAsRead(from peerID: PeerID) { unreadMessages.remove(peerID) // Send read receipts for unread messages that haven't been sent yet if let messages = privateChats[peerID] { for message in messages { if message.senderPeerID == peerID && !message.isRelay && !sentReadReceipts.contains(message.id) { sendReadReceipt(for: message) } } } } // MARK: - Private Methods private func sendReadReceipt(for message: BitchatMessage) { guard !sentReadReceipts.contains(message.id), let senderPeerID = message.senderPeerID else { return } sentReadReceipts.insert(message.id) // Create read receipt using the simplified method let receipt = ReadReceipt( originalMessageID: message.id, readerID: meshService?.myPeerID ?? PeerID(str: ""), readerNickname: meshService?.myNickname ?? "" ) // Route via MessageRouter to avoid handshakeRequired spam when session isn't established if let router = messageRouter { SecureLogger.debug("PrivateChatManager: sending READ ack for \(message.id.prefix(8))… to \(senderPeerID.id.prefix(8))… via router", category: .session) Task { @MainActor in router.sendReadReceipt(receipt, to: senderPeerID) } } else { // Fallback: preserve previous behavior meshService?.sendReadReceipt(receipt, to: senderPeerID) } } }