import Combine import Foundation import UIKit final class FileMiddleware { static let shared = FileMiddleware() private var downloadingMessageIDs = ThreadSafeSet() func middleware(state _: AppState, action: AppAction) -> AnyPublisher { switch action { // MARK: - For incomig attachments case .conversationAction(.messagesUpdated(let messages)): return Deferred { Future { [weak self] promise in guard let wSelf = self else { promise(.success(.info("FileMiddleware: on checking attachments/shares messages, middleware self is nil"))) return } // for incoming messages with attachments for message in messages where message.attachmentRemotePath != nil && message.attachmentLocalPath == nil { if wSelf.downloadingMessageIDs.contains(message.id) { continue } wSelf.downloadingMessageIDs.insert(message.id) DispatchQueue.main.async { // swiftlint:disable:next force_unwrapping store.dispatch(.fileAction(.downloadAttachmentFile(messageId: message.id, attachmentRemotePath: message.attachmentRemotePath!))) } } // for outgoing messages with shared attachments for message in messages where message.attachmentLocalPath != nil && message.attachmentRemotePath == nil && message.pending { DispatchQueue.main.async { store.dispatch(.xmppAction(.xmppSharingTryUpload(message))) } } // for outgoing messages with shared attachments which are already uploaded // but have no thumbnail (only for images) for message in messages where !message.pending && !message.sentError && message.attachmentType == .image { if message.attachmentLocalName != nil && message.attachmentRemotePath != nil && message.attachmentThumbnailName == nil { DispatchQueue.main.async { // swiftlint:disable:next force_unwrapping store.dispatch(.fileAction(.createAttachmentThumbnail(messageId: message.id, localName: message.attachmentLocalName!))) } } } promise(.success(.info("FileMiddleware: attachments/shares messages processed"))) } } .eraseToAnyPublisher() case .fileAction(.downloadAttachmentFile(let id, let attachmentRemotePath)): return Deferred { Future { promise in let localName = "\(id)_\(UUID().uuidString)\(attachmentRemotePath.lastPathComponent)" let localUrl = FileProcessing.fileFolder.appendingPathComponent(localName) DownloadManager.shared.enqueueDownload(from: attachmentRemotePath, to: localUrl) { error in DispatchQueue.main.async { if let error { store.dispatch(.fileAction(.downloadingAttachmentFileFailed(messageId: id, reason: error.localizedDescription))) } else { store.dispatch(.fileAction(.attachmentFileDownloaded(messageId: id, localName: localName))) } } } promise(.success(.info("FileMiddleware: started downloading attachment for message \(id)"))) } } .eraseToAnyPublisher() case .fileAction(.attachmentFileDownloaded(let id, let localName)): return Deferred { Future { [weak self] promise in self?.downloadingMessageIDs.remove(id) promise(.success(.fileAction(.createAttachmentThumbnail(messageId: id, localName: localName)))) } } .eraseToAnyPublisher() case .fileAction(.createAttachmentThumbnail(let id, let localName)): return Deferred { Future { [weak self] promise in if let thumbnailName = FileProcessing.shared.createThumbnail(localName: localName) { self?.downloadingMessageIDs.remove(id) promise(.success(.fileAction(.attachmentThumbnailCreated(messageId: id, thumbnailName: thumbnailName)))) } else { self?.downloadingMessageIDs.remove(id) promise(.success(.info("FileMiddleware: failed to create thumbnail from \(localName) for message \(id)"))) } } } .eraseToAnyPublisher() // MARK: - For outgoing sharing case .fileAction(.fetchItemsFromGallery): return Deferred { Future { promise in let items = FileProcessing.shared.fetchGallery() promise(.success(.fileAction(.itemsFromGalleryFetched(items: items)))) } } .eraseToAnyPublisher() case .fileAction(.itemsFromGalleryFetched(let items)): return Deferred { Future { promise in let newItems = FileProcessing.shared.fillGalleryItemsThumbnails(items: items) promise(.success(.sharingAction(.galleryItemsUpdated(items: newItems)))) } } .eraseToAnyPublisher() case .fileAction(.copyGalleryItemsForUploading(let items)): return Deferred { Future { promise in let ids = FileProcessing.shared.copyGalleryItemsForUploading(items: items) promise(.success(.fileAction(.itemsCopiedForUploading(newMessageIds: ids.map { $0.0 }, localNames: ids.map { $0.1 })))) } } .eraseToAnyPublisher() case .fileAction(.copyCameraCapturedForUploading(let media, let type)): return Deferred { Future { promise in if let (id, localName) = FileProcessing.shared.copyCameraCapturedForUploading(media: media, type: type) { promise(.success(.fileAction(.itemsCopiedForUploading(newMessageIds: [id], localNames: [localName])))) } else { promise(.success(.info("FileMiddleware: failed to copy camera captured media for uploading"))) } } } .eraseToAnyPublisher() default: return Empty().eraseToAnyPublisher() } } }