import AVFoundation import Combine import Foundation import Photos import UIKit final class SharingMiddleware { static let shared = SharingMiddleware() func middleware(state: AppState, action: AppAction) -> AnyPublisher { switch action { 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() 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() } default: return Empty().eraseToAnyPublisher() } } }