diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index f3579bd8..7ea21281 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -3075,6 +3075,38 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, CommandContextProv } } + /// Find message index trying both short (16-hex) and long (64-hex) peer ID formats. + /// Returns the peer ID where the message was found and its index, or nil if not found. + private func findMessageIndex(messageID: String, peerID: PeerID) -> (peerID: PeerID, index: Int)? { + // Try direct lookup first + if let messages = privateChats[peerID], + let idx = messages.firstIndex(where: { $0.id == messageID }) { + return (peerID, idx) + } + + // Try with full noise key if peerID is short (16 hex chars) + if peerID.bare.count == 16, + let peer = unifiedPeerService.getPeer(by: peerID), + !peer.noisePublicKey.isEmpty { + let longID = PeerID(hexData: peer.noisePublicKey) + if let messages = privateChats[longID], + let idx = messages.firstIndex(where: { $0.id == messageID }) { + return (longID, idx) + } + } + + // Try with short form if peerID is long (64 hex = noise key) + if peerID.bare.count == 64 { + let shortID = peerID.toShort() + if let messages = privateChats[shortID], + let idx = messages.firstIndex(where: { $0.id == messageID }) { + return (shortID, idx) + } + } + + return nil + } + // Low-level BLE events func didReceiveNoisePayload(from peerID: PeerID, type: NoisePayloadType, payload: Data, timestamp: Date) { Task { @MainActor in @@ -3108,20 +3140,26 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, CommandContextProv case .delivered: guard let messageID = String(data: payload, encoding: .utf8) else { return } - if let name = unifiedPeerService.getPeer(by: peerID)?.nickname { - if let messages = privateChats[peerID], let idx = messages.firstIndex(where: { $0.id == messageID }) { - privateChats[peerID]?[idx].deliveryStatus = .delivered(to: name, at: Date()) - objectWillChange.send() - } - } + guard let name = unifiedPeerService.getPeer(by: peerID)?.nickname, + let (foundPeerID, idx) = findMessageIndex(messageID: messageID, peerID: peerID) else { return } + + // Don't downgrade from .read to .delivered + if case .read = privateChats[foundPeerID]?[idx].deliveryStatus { return } + + privateChats[foundPeerID]?[idx].deliveryStatus = .delivered(to: name, at: Date()) + objectWillChange.send() case .readReceipt: guard let messageID = String(data: payload, encoding: .utf8) else { return } - if let name = unifiedPeerService.getPeer(by: peerID)?.nickname { - if let messages = privateChats[peerID], let idx = messages.firstIndex(where: { $0.id == messageID }) { - privateChats[peerID]?[idx].deliveryStatus = .read(by: name, at: Date()) - objectWillChange.send() - } + guard let name = unifiedPeerService.getPeer(by: peerID)?.nickname, + let (foundPeerID, idx) = findMessageIndex(messageID: messageID, peerID: peerID) else { return } + + // Explicitly unwrap and re-assign to ensure the @Published setter is called + if var messages = privateChats[foundPeerID], idx < messages.count { + messages[idx].deliveryStatus = .read(by: name, at: Date()) + privateChats[foundPeerID] = messages + privateChatManager.objectWillChange.send() + objectWillChange.send() } case .verifyChallenge: // Parse and respond diff --git a/bitchat/ViewModels/Extensions/ChatViewModel+PrivateChat.swift b/bitchat/ViewModels/Extensions/ChatViewModel+PrivateChat.swift index 5d3423a8..bc93c763 100644 --- a/bitchat/ViewModels/Extensions/ChatViewModel+PrivateChat.swift +++ b/bitchat/ViewModels/Extensions/ChatViewModel+PrivateChat.swift @@ -740,15 +740,11 @@ extension ChatViewModel { if isViewing { // Mark read immediately if viewing - // Use router to send read receipt + // Use the incoming peerID directly - it has the established Noise session. + // Don't use PeerID(hexData: noiseKey) as that creates a 64-hex ID without a session. + // Use meshService directly (not messageRouter) so it queues if peer disconnects. let receipt = ReadReceipt(originalMessageID: message.id, readerID: meshService.myPeerID, readerNickname: nickname) - if let key = noiseKey { - // Send via router to stable key if available (preferred for persistence/Nostr fallback) - messageRouter.sendReadReceipt(receipt, to: PeerID(hexData: key)) - } else { - // Fallback to mesh direct - meshService.sendReadReceipt(receipt, to: peerID) - } + meshService.sendReadReceipt(receipt, to: peerID) sentReadReceipts.insert(message.id) } else { // Notify