mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 02:25:20 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -165,9 +165,11 @@ final class PendingFileManager {
|
|||||||
|
|
||||||
/// Accepts a pending file, saving it to disk and removing from pending queue.
|
/// 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.
|
/// 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? {
|
func acceptFile(id: String, saveHandler: (PendingFileTransfer) -> URL?) -> URL? {
|
||||||
let pending = queue.sync(flags: .barrier) { () -> PendingFileTransfer? in
|
// First, get the pending file without removing it
|
||||||
pendingFiles.removeValue(forKey: id)
|
let pending = queue.sync { () -> PendingFileTransfer? in
|
||||||
|
pendingFiles[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let pending = pending else {
|
guard let pending = pending else {
|
||||||
@@ -175,11 +177,18 @@ final class PendingFileManager {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attempt to save the file
|
||||||
guard let url = saveHandler(pending) else {
|
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
|
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)
|
SecureLogger.debug("Accepted and saved pending file \(id.prefix(8))... to \(url.lastPathComponent)", category: .session)
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
// MARK: - Peer Connection Events
|
||||||
|
|
||||||
func didConnectToPeer(_ peerID: PeerID) {
|
func didConnectToPeer(_ peerID: PeerID) {
|
||||||
|
|||||||
@@ -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")
|
@Test("displayName returns fileName or generates default")
|
||||||
func displayName_returnsFileNameOrGeneratesDefault() {
|
func displayName_returnsFileNameOrGeneratesDefault() {
|
||||||
let manager = PendingFileManager(config: .default)
|
let manager = PendingFileManager(config: .default)
|
||||||
|
|||||||
Reference in New Issue
Block a user