From 49b1413d851946c076e019cf708a695a4f4abd6e Mon Sep 17 00:00:00 2001 From: jack Date: Mon, 12 Jan 2026 10:58:02 -1000 Subject: [PATCH] fix: address Codex review feedback for BCH-01-002 P1: Wire didReceivePendingFileTransfer into ChatViewModel - Add implementation that creates system message for pending files - Route to appropriate chat (public/private) - Send local notification for incoming files - Auto-decline files from blocked users P2: Keep pending files in queue if save fails - Only remove file from pendingFiles after successful save - Allows user to retry accept if initial save failed Also adds test for save failure scenario. Co-Authored-By: Claude Opus 4.5 --- bitchat/Services/PendingFileManager.swift | 15 ++++++-- bitchat/ViewModels/ChatViewModel.swift | 44 ++++++++++++++++++++++ bitchatTests/PendingFileManagerTests.swift | 32 ++++++++++++++++ 3 files changed, 88 insertions(+), 3 deletions(-) diff --git a/bitchat/Services/PendingFileManager.swift b/bitchat/Services/PendingFileManager.swift index 9f8f5b95..bbeacbf9 100644 --- a/bitchat/Services/PendingFileManager.swift +++ b/bitchat/Services/PendingFileManager.swift @@ -165,9 +165,11 @@ final class PendingFileManager { /// Accepts a pending file, saving it to disk and removing from pending queue. /// Returns the saved file URL, or nil if the file was not found or save failed. + /// Note: File is only removed from queue after successful save to prevent data loss. func acceptFile(id: String, saveHandler: (PendingFileTransfer) -> URL?) -> URL? { - let pending = queue.sync(flags: .barrier) { () -> PendingFileTransfer? in - pendingFiles.removeValue(forKey: id) + // First, get the pending file without removing it + let pending = queue.sync { () -> PendingFileTransfer? in + pendingFiles[id] } guard let pending = pending else { @@ -175,11 +177,18 @@ final class PendingFileManager { return nil } + // Attempt to save the file guard let url = saveHandler(pending) else { - SecureLogger.error("Failed to save accepted file \(id.prefix(8))...", category: .session) + // Save failed - keep the file in queue so user can retry + SecureLogger.error("Failed to save accepted file \(id.prefix(8))... - keeping in queue for retry", category: .session) return nil } + // Only remove from queue after successful save + _ = queue.sync(flags: .barrier) { + pendingFiles.removeValue(forKey: id) + } + SecureLogger.debug("Accepted and saved pending file \(id.prefix(8))... to \(url.lastPathComponent)", category: .session) return url } diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index 39aff8df..6fb448ed 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -3218,6 +3218,50 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, CommandContextProv } } + // MARK: - Pending File Transfers (BCH-01-002) + + func didReceivePendingFileTransfer(_ pending: PendingFileTransfer) { + Task { @MainActor in + // Check if sender is blocked + if blockedUsers.contains(pending.senderNickname) { + SecureLogger.debug("Ignoring pending file from blocked user: \(pending.senderNickname)", category: .session) + PendingFileManager.shared.declineFile(id: pending.id) + return + } + + // Create a system message to notify user about the pending file + let fileDescription = "\(pending.displayName) (\(ByteCountFormatter.string(fromByteCount: Int64(pending.fileSize), countStyle: .file)))" + let message = BitchatMessage( + id: "pending-\(pending.id)", + sender: pending.senderNickname, + content: "[Pending file: \(fileDescription)] - Accept or decline in file transfers", + timestamp: pending.timestamp, + isRelay: false, + originalSender: nil, + isPrivate: pending.isPrivate, + recipientNickname: pending.isPrivate ? nickname : nil, + senderPeerID: pending.senderPeerID, + mentions: nil + ) + + // Route to appropriate chat + if pending.isPrivate { + handlePrivateMessage(message) + } else { + handlePublicMessage(message) + } + + // Send notification + NotificationService.shared.sendLocalNotification( + title: "Incoming file from \(pending.senderNickname)", + body: fileDescription, + identifier: "pending-file-\(pending.id)" + ) + + SecureLogger.debug("📁 Notified UI of pending file: \(pending.id.prefix(8))... from \(pending.senderNickname)", category: .session) + } + } + // MARK: - Peer Connection Events func didConnectToPeer(_ peerID: PeerID) { diff --git a/bitchatTests/PendingFileManagerTests.swift b/bitchatTests/PendingFileManagerTests.swift index 873617fc..a59fddfc 100644 --- a/bitchatTests/PendingFileManagerTests.swift +++ b/bitchatTests/PendingFileManagerTests.swift @@ -258,6 +258,38 @@ struct PendingFileManagerTests { } } + @Test("acceptFile keeps file in queue on save failure") + func acceptFile_keepsFileOnSaveFailure() { + let manager = PendingFileManager(config: .default) + defer { manager.clearAll() } + + let content = Data(repeating: 0x42, count: 128) + let pending = manager.addPendingFile( + senderPeerID: PeerID(str: "AABBCCDD11223344"), + senderNickname: "TestUser", + fileName: "test.bin", + mimeType: "application/octet-stream", + content: content, + isPrivate: false + ) + + guard let id = pending?.id else { + Issue.record("Failed to add pending file") + return + } + + #expect(manager.stats.count == 1) + + // Simulate save failure by returning nil + let resultURL = manager.acceptFile(id: id) { _ in + return nil // Save failed + } + + #expect(resultURL == nil) + #expect(manager.stats.count == 1) // Still in queue for retry + #expect(manager.getPendingFile(id: id) != nil) // Can still retrieve it + } + @Test("displayName returns fileName or generates default") func displayName_returnsFileNameOrGeneratesDefault() { let manager = PendingFileManager(config: .default)