diff --git a/ConversationsClassic/AppCore/Actions/FileActions.swift b/ConversationsClassic/AppCore/Actions/FileActions.swift index 84b1682..2e7e5c4 100644 --- a/ConversationsClassic/AppCore/Actions/FileActions.swift +++ b/ConversationsClassic/AppCore/Actions/FileActions.swift @@ -9,4 +9,7 @@ enum FileAction: Stateable { case attachmentThumbnailCreated(messageId: String, thumbnailName: String) case copyFileForUploading(messageId: String, fileData: Data, thumbnailData: Data?) + + case fetchItemsFromGallery + case itemsFromGalleryFetched(items: [SharingGalleryItem]) } diff --git a/ConversationsClassic/AppCore/Actions/SharingActions.swift b/ConversationsClassic/AppCore/Actions/SharingActions.swift index 3148cb3..522060f 100644 --- a/ConversationsClassic/AppCore/Actions/SharingActions.swift +++ b/ConversationsClassic/AppCore/Actions/SharingActions.swift @@ -13,9 +13,7 @@ enum SharingAction: Stateable { case checkGalleryAccess case setGalleryAccess(Bool) - case fetchGallery - case galleryFetched([SharingGalleryItem]) - case thumbnailUpdated(data: Data, id: String) + case galleryItemsUpdated(items: [SharingGalleryItem]) case cameraCaptured(media: Data, type: SharingCameraMediaType) case flushCameraCaptured diff --git a/ConversationsClassic/AppCore/Files/FileProcessing.swift b/ConversationsClassic/AppCore/Files/FileProcessing.swift index 6c6f102..16a6af5 100644 --- a/ConversationsClassic/AppCore/Files/FileProcessing.swift +++ b/ConversationsClassic/AppCore/Files/FileProcessing.swift @@ -1,4 +1,5 @@ import Foundation +import Photos import UIKit final class FileProcessing { @@ -42,25 +43,104 @@ final class FileProcessing { return nil } } -} -func scaleAndCropImage(_ img: UIImage, _ size: CGSize) -> UIImage? { - let aspect = img.size.width / img.size.height - let targetAspect = size.width / size.height - var newWidth: CGFloat - var newHeight: CGFloat - if aspect < targetAspect { - newWidth = size.width - newHeight = size.width / aspect - } else { - newHeight = size.height - newWidth = size.height * aspect + func fetchGallery() -> [SharingGalleryItem] { + 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)) + } + } + return items } - UIGraphicsBeginImageContextWithOptions(size, false, 0.0) - img.draw(in: CGRect(x: (size.width - newWidth) / 2, y: (size.height - newHeight) / 2, width: newWidth, height: newHeight)) - let newImage = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() + func fillGalleryItemsThumbnails(items: [SharingGalleryItem]) -> [SharingGalleryItem] { + var result: [SharingGalleryItem] = [] + let ids = items + .filter { $0.thumbnail == nil } + .map { $0.id } - return newImage + 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() + result.append(.init(id: asset.localIdentifier, type: .photo, thumbnail: data)) + } + } + } + } 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() + result.append(.init(id: asset.localIdentifier, type: .video, thumbnail: data)) + } + } + } catch { + print("Failed to create thumbnail image") + } + } + } + } + } + return result + } +} + +private extension FileProcessing { + func scaleAndCropImage(_ img: UIImage, _ size: CGSize) -> UIImage? { + let aspect = img.size.width / img.size.height + let targetAspect = size.width / size.height + var newWidth: CGFloat + var newHeight: CGFloat + if aspect < targetAspect { + newWidth = size.width + newHeight = size.width / aspect + } else { + newHeight = size.height + newWidth = size.height * aspect + } + + UIGraphicsBeginImageContextWithOptions(size, false, 0.0) + img.draw(in: CGRect(x: (size.width - newWidth) / 2, y: (size.height - newHeight) / 2, width: newWidth, height: newHeight)) + let newImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + return newImage + } + + func syncEnumrate(_ ids: [String]? = nil) -> [PHAsset] { + var result: [PHAsset] = [] + + let fetchOptions = PHFetchOptions() + fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] + if let ids { + fetchOptions.predicate = NSPredicate(format: "localIdentifier IN %@", ids) + } + let assets = PHAsset.fetchAssets(with: fetchOptions) + assets.enumerateObjects { asset, _, _ in + result.append(asset) + } + return result + } } diff --git a/ConversationsClassic/AppCore/Middlewares/FileMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/FileMiddleware.swift index 39cc82e..0f771bb 100644 --- a/ConversationsClassic/AppCore/Middlewares/FileMiddleware.swift +++ b/ConversationsClassic/AppCore/Middlewares/FileMiddleware.swift @@ -8,6 +8,7 @@ final class FileMiddleware { func middleware(state _: AppState, action: AppAction) -> AnyPublisher { switch action { + // MARK: - For incomig attachments case .conversationAction(.messagesUpdated(let messages)): return Future { [weak self] promise in guard let wSelf = self else { @@ -62,6 +63,21 @@ final class FileMiddleware { } .eraseToAnyPublisher() + // MARK: - For outgoing sharing + case .fileAction(.fetchItemsFromGallery): + return Future { promise in + let items = FileProcessing.shared.fetchGallery() + promise(.success(.fileAction(.itemsFromGalleryFetched(items: items)))) + } + .eraseToAnyPublisher() + + case .fileAction(.itemsFromGalleryFetched(let items)): + return Future { promise in + let newItems = FileProcessing.shared.fillGalleryItemsThumbnails(items: items) + promise(.success(.sharingAction(.galleryItemsUpdated(items: newItems)))) + } + .eraseToAnyPublisher() + case .fileAction(.copyFileForUploading(let messageId, let data, let thumbnail)): print("=====") print("copyFileForUploading") diff --git a/ConversationsClassic/AppCore/Middlewares/SharingMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/SharingMiddleware.swift index fd11849..e071dbf 100644 --- a/ConversationsClassic/AppCore/Middlewares/SharingMiddleware.swift +++ b/ConversationsClassic/AppCore/Middlewares/SharingMiddleware.swift @@ -53,68 +53,9 @@ final class SharingMiddleware { } .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 .fileAction(.itemsFromGalleryFetched(let items)): + return Just(.sharingAction(.galleryItemsUpdated(items: items))) + .eraseToAnyPublisher() // MARK: - Sharing case .sharingAction(.shareMedia(let ids)): diff --git a/ConversationsClassic/AppCore/Reducers/SharingReducer.swift b/ConversationsClassic/AppCore/Reducers/SharingReducer.swift index 4ab34cb..baddc95 100644 --- a/ConversationsClassic/AppCore/Reducers/SharingReducer.swift +++ b/ConversationsClassic/AppCore/Reducers/SharingReducer.swift @@ -20,15 +20,9 @@ extension SharingState { state.cameraCapturedMedia = Data() state.cameraCapturedMediaType = .photo - case .galleryFetched(let items): + case .galleryItemsUpdated(let items): state.galleryItems = items - case .thumbnailUpdated(let thumbnailData, let id): - guard let index = state.galleryItems.firstIndex(where: { $0.id == id }) else { - return - } - state.galleryItems[index].thumbnail = thumbnailData - default: break } diff --git a/ConversationsClassic/View/Screens/Sharing/SharingMediaPickerView.swift b/ConversationsClassic/View/Screens/Sharing/SharingMediaPickerView.swift index 905202b..28f9ab7 100644 --- a/ConversationsClassic/View/Screens/Sharing/SharingMediaPickerView.swift +++ b/ConversationsClassic/View/Screens/Sharing/SharingMediaPickerView.swift @@ -135,7 +135,7 @@ struct SharingMediaPickerView: View { } .onChange(of: store.state.sharingState.isGalleryAccessGranted) { granted in if granted { - store.dispatch(.sharingAction(.fetchGallery)) + store.dispatch(.fileAction(.fetchItemsFromGallery)) } } }