import AVFoundation import Combine import Foundation import Photos import UIKit final class SharingMiddleware { static let shared = SharingMiddleware() // swiftlint:disable:next function_body_length func middleware(state: AppState, action: AppAction) -> AnyPublisher { switch action { // MARK: - Camera and Gallery Access case .sharingAction(.checkCameraAccess): return Future { promise in let status = AVCaptureDevice.authorizationStatus(for: .video) switch status { case .authorized: promise(.success(.sharingAction(.setCameraAccess(true)))) case .notDetermined: AVCaptureDevice.requestAccess(for: .video) { granted in promise(.success(.sharingAction(.setCameraAccess(granted)))) } case .denied, .restricted: promise(.success(.sharingAction(.setCameraAccess(false)))) @unknown default: promise(.success(.sharingAction(.setCameraAccess(false)))) } } .eraseToAnyPublisher() case .sharingAction(.checkGalleryAccess): return Future { promise in let status = PHPhotoLibrary.authorizationStatus() switch status { case .authorized, .limited: promise(.success(.sharingAction(.setGalleryAccess(true)))) case .notDetermined: PHPhotoLibrary.requestAuthorization { status in promise(.success(.sharingAction(.setGalleryAccess(status == .authorized)))) } case .denied, .restricted: promise(.success(.sharingAction(.setGalleryAccess(false)))) @unknown default: promise(.success(.sharingAction(.setGalleryAccess(false)))) } } .eraseToAnyPublisher() case .sharingAction(.fetchGallery): return Future { promise in let fetchOptions = PHFetchOptions() fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] let assets = PHAsset.fetchAssets(with: fetchOptions) var items: [SharingGalleryItem] = [] assets.enumerateObjects { asset, _, _ in if asset.mediaType == .image { items.append(.init(id: asset.localIdentifier, type: .photo)) } else if asset.mediaType == .video { items.append(.init(id: asset.localIdentifier, type: .video)) } } promise(.success(.sharingAction(.galleryFetched(items)))) } .eraseToAnyPublisher() case .sharingAction(.galleryFetched(let items)): DispatchQueue.global().async { let ids = items .filter { $0.thumbnail == nil } .map { $0.id } let assets = PHAsset.fetchAssets(withLocalIdentifiers: ids, options: nil) assets.enumerateObjects { asset, _, _ in if asset.mediaType == .image { PHImageManager.default().requestImage( for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .aspectFill, options: nil ) { image, _ in image?.scaleAndCropImage(toExampleSize: CGSize(width: Const.galleryGridSize, height: Const.galleryGridSize)) { image in if let image { let data = image.jpegData(compressionQuality: 1.0) ?? Data() store.dispatch(.sharingAction(.thumbnailUpdated(data: data, id: asset.localIdentifier))) } } } } else if asset.mediaType == .video { PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in if let avAsset { let imageGenerator = AVAssetImageGenerator(asset: avAsset) imageGenerator.appliesPreferredTrackTransform = true let time = CMTimeMake(value: 1, timescale: 2) do { let imageRef = try imageGenerator.copyCGImage(at: time, actualTime: nil) let thumbnail = UIImage(cgImage: imageRef) thumbnail.scaleAndCropImage(toExampleSize: CGSize(width: Const.galleryGridSize, height: Const.galleryGridSize)) { image in if let image { let data = image.jpegData(compressionQuality: 1.0) ?? Data() store.dispatch(.sharingAction(.thumbnailUpdated(data: data, id: asset.localIdentifier))) } } } catch { print("Failed to create thumbnail image") } } } } } } return Empty().eraseToAnyPublisher() // MARK: - Sharing case .sharingAction(.shareMedia(let ids)): return Future { promise in let assets = PHAsset.fetchAssets(withLocalIdentifiers: ids, options: nil) assets.enumerateObjects { asset, _, _ in if asset.mediaType == .image { PHImageManager.default().requestImage( for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .aspectFill, options: nil ) { image, _ in if let data = image?.jpegData(compressionQuality: 1.0) { DispatchQueue.main.async { let newMessageId = UUID().uuidString store.dispatch(.fileAction(.copyFileForUploading( messageId: newMessageId, fileData: data, thumbnailData: store.state.sharingState.galleryItems.first(where: { $0.id == asset.localIdentifier })?.thumbnail ))) } } } } else if asset.mediaType == .video { let options = PHVideoRequestOptions() options.version = .original options.deliveryMode = .highQualityFormat PHImageManager.default().requestAVAsset(forVideo: asset, options: options) { avAsset, _, _ in guard let urlAsset = avAsset as? AVURLAsset else { return } let exporter = AVAssetExportSession(asset: urlAsset, presetName: AVAssetExportPresetHighestQuality) exporter?.outputFileType = .mp4 let outputURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".mp4") exporter?.outputURL = outputURL exporter?.exportAsynchronously { switch exporter?.status { case .completed: if let data = try? Data(contentsOf: outputURL) { DispatchQueue.main.async { let newMessageId = UUID().uuidString store.dispatch(.fileAction(.copyFileForUploading( messageId: newMessageId, fileData: data, thumbnailData: nil ))) } } default: break } } } } } promise(.success(.empty)) } .eraseToAnyPublisher() case .sharingAction(.cameraCaptured(let media, let type)): print("Camera captured: \(media.count)") return Empty().eraseToAnyPublisher() case .sharingAction(.shareLocation(let lat, let lon)): if let chat = state.conversationsState.currentChat { let msg = "geo:\(lat),\(lon)" return Just(.conversationAction(.sendMessage(from: chat.account, to: chat.participant, body: msg))) .eraseToAnyPublisher() } else { return Empty().eraseToAnyPublisher() } case .sharingAction(.shareDocuments(let data)): print("Sharing documents: \(data.count)") return Empty().eraseToAnyPublisher() case .sharingAction(.shareContact(let jid)): if let chat = state.conversationsState.currentChat { let msg = "contact:\(jid)" return Just(.conversationAction(.sendMessage(from: chat.account, to: chat.participant, body: msg))) .eraseToAnyPublisher() } else { return Empty().eraseToAnyPublisher() } default: return Empty().eraseToAnyPublisher() } } }