From e564ae5747fd77a17f0d3744551c53229b82b17b Mon Sep 17 00:00:00 2001 From: fmodf Date: Wed, 10 Jul 2024 19:49:36 +0200 Subject: [PATCH] wip --- .../AppCore/Actions/SharingActions.swift | 24 +- .../Database/Database+Migrations.swift | 32 +- .../Middlewares/SharingMiddleware.swift | 343 +++++++++++++++++- .../AppCore/Reducers/SharingReducer.swift | 25 ++ .../AppCore/Services/CameraService.swift | 5 + .../AppCore/State/ConversationState.swift | 2 - .../AppCore/State/SharingState.swift | 28 ++ ConversationsClassic/Helpers/Const.swift | 7 + .../AttachmentMediaPickerView.swift | 309 ++++------------ .../Conversation/ConversationTextInput.swift | 2 +- 10 files changed, 502 insertions(+), 275 deletions(-) create mode 100644 ConversationsClassic/AppCore/Services/CameraService.swift diff --git a/ConversationsClassic/AppCore/Actions/SharingActions.swift b/ConversationsClassic/AppCore/Actions/SharingActions.swift index 64293e7..98d0578 100644 --- a/ConversationsClassic/AppCore/Actions/SharingActions.swift +++ b/ConversationsClassic/AppCore/Actions/SharingActions.swift @@ -4,20 +4,18 @@ enum SharingAction: Stateable { case showSharing(Bool) case shareLocation(lat: Double, lon: Double) - case shareContact(jid: String) - case shareDocuments([Data]) - // case sendAttachment([ShareItem]) - // case sendAttachmentDone - // case sendAttachmentError(reason: String) -} + case checkCameraAccess + case setCameraAccess(Bool) -// struct ShareItem: Stateable { -// let id: String -// let type: AttachmentType -// let data: Data -// let thumbnail: Data -// let string: String -// } + case checkGalleryAccess + case setGalleryAccess(Bool) + case fetchGallery + case galleryFetched([SharingGalleryItem]) + case thumbnailUpdated(Data, String) + + case cameraCaptured(media: Data, type: SharingCameraMediaType) + case flushCameraCaptured +} diff --git a/ConversationsClassic/AppCore/Database/Database+Migrations.swift b/ConversationsClassic/AppCore/Database/Database+Migrations.swift index 7813f15..35ff447 100644 --- a/ConversationsClassic/AppCore/Database/Database+Migrations.swift +++ b/ConversationsClassic/AppCore/Database/Database+Migrations.swift @@ -44,6 +44,22 @@ extension Database { table.column("type", .integer).notNull() } + // attachments + try db.create(table: "attachments", options: [.ifNotExists]) { table in + table.column("id", .text).notNull().primaryKey().unique(onConflict: .replace) + } + + // attachment items + try db.create(table: "attachment_items", options: [.ifNotExists]) { table in + table.column("id", .text).notNull().primaryKey().unique(onConflict: .replace) + table.belongsTo("attachments", onDelete: .cascade).notNull() + table.column("type", .integer).notNull() + table.column("localPath", .text) + table.column("remotePath", .text) + table.column("localThumbnailPath", .text) + table.column("string", .text) + } + // messages try db.create(table: "messages", options: [.ifNotExists]) { table in table.column("id", .text).notNull().primaryKey().unique(onConflict: .replace) @@ -60,22 +76,6 @@ extension Database { table.column("sentError", .boolean).notNull() table.column("attachment", .text).references("attachments", onDelete: .cascade) } - - // attachments - try db.create(table: "attachments", options: [.ifNotExists]) { table in - table.column("id", .text).notNull().primaryKey().unique(onConflict: .replace) - } - - // attachment items - try db.create(table: "attachment_items", options: [.ifNotExists]) { table in - table.column("id", .text).notNull().primaryKey().unique(onConflict: .replace) - table.belongsTo("attachments", onDelete: .cascade).notNull() - table.column("type", .integer).notNull() - table.column("localPath", .text) - table.column("remotePath", .text) - table.column("localThumbnailPath", .text) - table.column("string", .text) - } } // return migrator diff --git a/ConversationsClassic/AppCore/Middlewares/SharingMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/SharingMiddleware.swift index 78b9eb0..2ac71fe 100644 --- a/ConversationsClassic/AppCore/Middlewares/SharingMiddleware.swift +++ b/ConversationsClassic/AppCore/Middlewares/SharingMiddleware.swift @@ -1,15 +1,354 @@ +import AVFoundation import Combine import Foundation -import Martin +import Photos +import UIKit final class SharingMiddleware { static let shared = SharingMiddleware() - private let gallery = GalleryService() + // swiftlint:disable:next function_body_length 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)))) + // DispatchQueue.main.async { + // self.isCameraAccessGranted = 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)))) + // DispatchQueue.main.async { + // self.isGalleryAccessGranted = status == .authorized + // if self.isGalleryAccessGranted { + // self.fetchGallery() + // } + // } + } + + case .denied, .restricted: + promise(.success(.sharingAction(.setGalleryAccess(false)))) + + @unknown default: + promise(.success(.sharingAction(.setGalleryAccess(false)))) + } + } + .eraseToAnyPublisher() + + case .sharingAction(.fetchGallery): + return Future { promise in + DispatchQueue.global(qos: .background).async { + let fetchOptions = PHFetchOptions() + fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] + let assets = PHAsset.fetchAssets(with: fetchOptions) + + let manager = PHImageManager.default() + let option = PHImageRequestOptions() + option.isSynchronous = true + + var items: [SharingGalleryItem] = [] + let group = DispatchGroup() + assets.enumerateObjects { asset, _, _ in + if asset.mediaType == .image { + group.enter() + manager.requestImage( + for: asset, + targetSize: PHImageManagerMaximumSize, + contentMode: .aspectFill, + options: option + ) { image, _ in + if image != nil { + items.append(.init(id: asset.localIdentifier, type: .photo)) + group.leave() + } + } + } else if asset.mediaType == .video { + group.enter() + manager.requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in + if avAsset != nil { + items.append(.init(id: asset.localIdentifier, type: .video, duration: asset.duration.minAndSec)) + group.leave() + } + } + } + } + group.notify(queue: .main) { + promise(.success(.sharingAction(.galleryFetched(items)))) + } + } + } + .eraseToAnyPublisher() + + case .sharingAction(.galleryFetched(let items)): + return Future { promise in + DispatchQueue.global(qos: .background).async { + let ids = items + .filter { $0.thumbnail == nil } + .map { $0.id } + let assets = PHAsset.fetchAssets(withLocalIdentifiers: ids, options: nil) + assets.enumerateObjects { asset, _, _ in + let manager = PHImageManager.default() + if asset.mediaType == .image { + manager.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() + DispatchQueue.main.async { + store.dispatch(.sharingAction(.thumbnailUpdated(data, asset.localIdentifier))) + } + } + } + } + } else if asset.mediaType == .video { + manager.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), completion: { image in + if let image { + let data = image.jpegData(compressionQuality: 1.0) ?? Data() + DispatchQueue.main.async { + store.dispatch(.sharingAction(.thumbnailUpdated(data, asset.localIdentifier))) + } + } + }) + } catch { + print("Failed to create thumbnail image") + } + } + } + } + } + } + promise(.success(.empty)) + } + .eraseToAnyPublisher() + default: return Empty().eraseToAnyPublisher() } } } + +// private func fetchGallery() { +// let fetchOptions = PHFetchOptions() +// fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] +// let assets = PHAsset.fetchAssets(with: fetchOptions) +// +// let manager = PHImageManager.default() +// let option = PHImageRequestOptions() +// option.isSynchronous = true +// +// assets.enumerateObjects { asset, _, _ in +// if asset.mediaType == .image { +// manager.requestImage( +// for: asset, +// targetSize: PHImageManagerMaximumSize, +// contentMode: .aspectFill, +// options: option +// ) { image, _ in +// image?.scaleAndCropImage(toExampleSize: CGSize(width: gridSize, height: gridSize), completion: { image in +// if let image { +// DispatchQueue.main.async { +// self.thumbnails.append(ThumbnailView(id: asset.localIdentifier, image: image, gridSize: gridSize, selected: $selectedMedia)) +// } +// } +// }) +// } +// } else if asset.mediaType == .video { +// manager.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: gridSize, height: gridSize), completion: { image in +// if let image { +// DispatchQueue.main.async { +// self.thumbnails.append(ThumbnailView(id: asset.localIdentifier, image: image, gridSize: gridSize, selected: $selectedMedia, duration: asset.duration.minAndSec)) +// } +// } +// }) +// } catch { +// print("Failed to create thumbnail image") +// } +// } +// } +// } +// } +// } + +// private func sendGalleryMedia(ids _: [String]) { +// var media: [AttachmentItem] = [] +// let dispatchGroup = DispatchGroup() +// +// let asset = PHAsset.fetchAssets(withLocalIdentifiers: ids, options: nil) +// asset.enumerateObjects { asset, _, _ in +// dispatchGroup.enter() +// if asset.mediaType == .image { +// let manager = PHImageManager.default() +// let option = PHImageRequestOptions() +// option.isSynchronous = true +// +// manager.requestImage( +// for: asset, +// targetSize: PHImageManagerMaximumSize, +// contentMode: .aspectFill, +// options: option +// ) { image, _ in +// if let image { +// let data = image.jpegData(compressionQuality: 1.0) ?? Data() +// media.append(.init(type: .image, data: data, string: "")) +// } +// dispatchGroup.leave() +// } +// } else if asset.mediaType == .video { +// let manager = PHImageManager.default() +// let option = PHVideoRequestOptions() +// option.version = .current +// option.deliveryMode = .highQualityFormat +// +// manager.requestAVAsset(forVideo: asset, options: option) { avAsset, _, _ in +// if let avAsset { +// let url = (avAsset as? AVURLAsset)?.url +// let data = try? Data(contentsOf: url ?? URL(fileURLWithPath: "")) +// media.append(.init(type: .movie, data: data ?? Data(), string: "")) +// } +// dispatchGroup.leave() +// } +// } +// } +// dispatchGroup.notify(queue: .main) { +// store.dispatch(.conversationAction(.sendAttachment(.init( +// id: UUID().uuidString, +// items: media +// )))) +// } +// } +// } +// +// private struct ThumbnailView: Identifiable, View { +// let id: String +// let gridSize: CGFloat +// let duration: String? +// +// @State private var image: UIImage +// @State private var ready = false +// @State private var selected = false +// @Binding var selectedMedia: [SelectedMedia] +// +// init(id: String, image: UIImage, gridSize: CGFloat, selected: Binding<[SelectedMedia]>, duration: String? = nil) { +// self.id = id +// self.image = image +// self.gridSize = gridSize +// _selectedMedia = selected +// self.duration = duration +// } +// +// var body: some View { +// if ready { +// ZStack { +// Image(uiImage: image) +// .resizable() +// .aspectRatio(contentMode: .fill) +// .frame(width: gridSize, height: gridSize) +// .clipped() +// if let duration { +// VStack { +// Spacer() +// HStack { +// Spacer() +// Text(duration) +// .foregroundColor(.Material.Text.white) +// .font(.sub1) +// .shadow(color: .black, radius: 2) +// .padding(4) +// } +// } +// } +// if selected { +// VStack { +// HStack { +// Spacer() +// Circle() +// .frame(width: 30, height: 30) +// .shadow(color: .black, radius: 2) +// .foregroundColor(.Material.Shape.white) +// .overlay { +// Image(systemName: "checkmark") +// .foregroundColor(.Material.Elements.active) +// .font(.body3) +// } +// .padding(4) +// } +// Spacer() +// } +// } +// } +// .onTapGesture { +// withAnimation { +// selected.toggle() +// if selected { +// selectedMedia.append(SelectedMedia(id: id)) +// } else { +// selectedMedia.removeAll { $0.id == id } +// } +// } +// } +// } else { +// ZStack { +// Rectangle() +// .fill(Color.Material.Background.light) +// .overlay { +// ProgressView() +// } +// .frame(width: gridSize, height: gridSize) +// } +// .onAppear { +// image.scaleAndCropImage(toExampleSize: CGSize(width: gridSize, height: gridSize), completion: { image in +// if let image { +// self.image = image +// ready = true +// } +// }) +// } +// } +// } +// } diff --git a/ConversationsClassic/AppCore/Reducers/SharingReducer.swift b/ConversationsClassic/AppCore/Reducers/SharingReducer.swift index 94d4262..4ab34cb 100644 --- a/ConversationsClassic/AppCore/Reducers/SharingReducer.swift +++ b/ConversationsClassic/AppCore/Reducers/SharingReducer.swift @@ -1,9 +1,34 @@ +import Foundation + extension SharingState { static func reducer(state: inout SharingState, action: SharingAction) { switch action { case .showSharing(let shown): state.sharingShown = shown + case .setCameraAccess(let granted): + state.isCameraAccessGranted = granted + + case .setGalleryAccess(let granted): + state.isGalleryAccessGranted = granted + + case .cameraCaptured(let media, let type): + state.cameraCapturedMedia = media + state.cameraCapturedMediaType = type + + case .flushCameraCaptured: + state.cameraCapturedMedia = Data() + state.cameraCapturedMediaType = .photo + + case .galleryFetched(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/AppCore/Services/CameraService.swift b/ConversationsClassic/AppCore/Services/CameraService.swift new file mode 100644 index 0000000..aedeb5a --- /dev/null +++ b/ConversationsClassic/AppCore/Services/CameraService.swift @@ -0,0 +1,5 @@ +import Foundation + +final class CameraService { + var dumb = false +} diff --git a/ConversationsClassic/AppCore/State/ConversationState.swift b/ConversationsClassic/AppCore/State/ConversationState.swift index bfb03ce..9267bbd 100644 --- a/ConversationsClassic/AppCore/State/ConversationState.swift +++ b/ConversationsClassic/AppCore/State/ConversationState.swift @@ -4,7 +4,6 @@ struct ConversationState: Stateable { var currentMessages: [Message] var replyText: String - var attachmentPickerVisible: Bool } // MARK: Init @@ -12,6 +11,5 @@ extension ConversationState { init() { currentMessages = [] replyText = "" - attachmentPickerVisible = false } } diff --git a/ConversationsClassic/AppCore/State/SharingState.swift b/ConversationsClassic/AppCore/State/SharingState.swift index 93c606e..323d83a 100644 --- a/ConversationsClassic/AppCore/State/SharingState.swift +++ b/ConversationsClassic/AppCore/State/SharingState.swift @@ -1,10 +1,38 @@ +import Foundation + +enum SharingCameraMediaType: Stateable { + case video + case photo +} + +struct SharingGalleryItem: Stateable, Identifiable { + var id: String + var type: SharingCameraMediaType + var thumbnail: Data? + var duration: String? +} + struct SharingState: Stateable { var sharingShown: Bool + var isCameraAccessGranted: Bool + var isGalleryAccessGranted: Bool + + var cameraCapturedMedia: Data + var cameraCapturedMediaType: SharingCameraMediaType + + var galleryItems: [SharingGalleryItem] } // MARK: Init extension SharingState { init() { sharingShown = false + isCameraAccessGranted = false + isGalleryAccessGranted = false + + cameraCapturedMedia = Data() + cameraCapturedMediaType = .photo + + galleryItems = [] } } diff --git a/ConversationsClassic/Helpers/Const.swift b/ConversationsClassic/Helpers/Const.swift index 02c7b53..71e4d34 100644 --- a/ConversationsClassic/Helpers/Const.swift +++ b/ConversationsClassic/Helpers/Const.swift @@ -1,4 +1,5 @@ import Foundation +import UIKit enum Const { // // Network @@ -28,6 +29,12 @@ enum Const { case conversations = "conversations.im" } + // Limit for video for sharing + static let videoDurationLimit = 60.0 + // Upload/download file folder static let fileFolder = "ConversationsClassic" + + // Grid size for gallery preview (3 in a row) + static let galleryGridSize = UIScreen.main.bounds.width / 3 } diff --git a/ConversationsClassic/View/Screens/Attachments/AttachmentMediaPickerView.swift b/ConversationsClassic/View/Screens/Attachments/AttachmentMediaPickerView.swift index 3993c67..565d813 100644 --- a/ConversationsClassic/View/Screens/Attachments/AttachmentMediaPickerView.swift +++ b/ConversationsClassic/View/Screens/Attachments/AttachmentMediaPickerView.swift @@ -8,16 +8,11 @@ struct SelectedMedia { } struct AttachmentMediaPickerView: View { - @State private var isCameraAccessGranted = AVCaptureDevice.authorizationStatus(for: .video) == .authorized - @State private var isGalleryAccessGranted = PHPhotoLibrary.authorizationStatus() == .authorized + @EnvironmentObject var store: AppStore - @State private var thumbnails = [ThumbnailView]() @State private var selectedMedia = [SelectedMedia]() - @State private var showCameraPicker = false - let gridSize = UIScreen.main.bounds.width / 3 - var body: some View { let columns = Array(repeating: GridItem(.flexible(), spacing: 0), count: 3) @@ -26,7 +21,7 @@ struct AttachmentMediaPickerView: View { ScrollView(showsIndicators: false) { LazyVGrid(columns: columns, spacing: 0) { // For camera - if isCameraAccessGranted { + if store.state.sharingState.isCameraAccessGranted { ZStack { CameraView() .aspectRatio(1, contentMode: .fit) @@ -67,9 +62,9 @@ struct AttachmentMediaPickerView: View { } // For gallery - if isGalleryAccessGranted { - ForEach(thumbnails) { photo in - photo + if store.state.sharingState.isGalleryAccessGranted { + ForEach(store.state.sharingState.galleryItems) { item in + GridViewItem(item: item) } } else { Button { @@ -95,19 +90,12 @@ struct AttachmentMediaPickerView: View { } } .fullScreenCover(isPresented: $showCameraPicker) { - // TODO: fix it - // CameraPicker(sourceType: .camera) { data, type in - // store.dispatch(.conversationAction(.sendAttachment([.init( - // id: UUID().uuidString, - // type: type, - // data: data, - // thumbnail: Data(), - // string: "" - // )]))) - // showCameraPicker = false - // store.dispatch(.conversationAction(.showAttachmentPicker(false))) - // } - // .edgesIgnoringSafeArea(.all) + CameraPicker(sourceType: .camera) { data, type in + store.dispatch(.sharingAction(.cameraCaptured(media: data, type: type))) + showCameraPicker = false + store.dispatch(.sharingAction(.showSharing(false))) + } + .edgesIgnoringSafeArea(.all) } // Send panel @@ -130,190 +118,38 @@ struct AttachmentMediaPickerView: View { .clipped() .onTapGesture { let ids = selectedMedia.map { $0.id } - sendGalleryMedia(ids: ids) + // sendGalleryMedia(ids: ids) store.dispatch(.sharingAction(.showSharing(false))) } } .onAppear { - DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 0.2) { - checkCameraAccess() - checkGalleryAccess() + store.dispatch(.sharingAction(.checkCameraAccess)) + store.dispatch(.sharingAction(.checkGalleryAccess)) + // DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 0.2) { + // checkCameraAccess() + // checkGalleryAccess() + // } + } + .onChange(of: store.state.sharingState.isGalleryAccessGranted) { granted in + if granted { + store.dispatch(.sharingAction(.fetchGallery)) } } } - - private func checkCameraAccess() { - let status = AVCaptureDevice.authorizationStatus(for: .video) - switch status { - case .authorized: - isCameraAccessGranted = true - - case .notDetermined: - AVCaptureDevice.requestAccess(for: .video) { granted in - DispatchQueue.main.async { - self.isCameraAccessGranted = granted - } - } - - case .denied, .restricted: - isCameraAccessGranted = false - - @unknown default: - isCameraAccessGranted = false - } - } - - private func checkGalleryAccess() { - let status = PHPhotoLibrary.authorizationStatus() - switch status { - case .authorized, .limited: - isGalleryAccessGranted = true - fetchGallery() - - case .notDetermined: - PHPhotoLibrary.requestAuthorization { status in - DispatchQueue.main.async { - self.isGalleryAccessGranted = status == .authorized - if self.isGalleryAccessGranted { - self.fetchGallery() - } - } - } - - case .denied, .restricted: - isGalleryAccessGranted = false - - @unknown default: - isGalleryAccessGranted = false - } - } - - private func fetchGallery() { - let fetchOptions = PHFetchOptions() - fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] - let assets = PHAsset.fetchAssets(with: fetchOptions) - - let manager = PHImageManager.default() - let option = PHImageRequestOptions() - option.isSynchronous = true - - assets.enumerateObjects { asset, _, _ in - if asset.mediaType == .image { - manager.requestImage( - for: asset, - targetSize: PHImageManagerMaximumSize, - contentMode: .aspectFill, - options: option - ) { image, _ in - image?.scaleAndCropImage(toExampleSize: CGSize(width: gridSize, height: gridSize), completion: { image in - if let image { - DispatchQueue.main.async { - self.thumbnails.append(ThumbnailView(id: asset.localIdentifier, image: image, gridSize: gridSize, selected: $selectedMedia)) - } - } - }) - } - } else if asset.mediaType == .video { - manager.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: gridSize, height: gridSize), completion: { image in - if let image { - DispatchQueue.main.async { - self.thumbnails.append(ThumbnailView(id: asset.localIdentifier, image: image, gridSize: gridSize, selected: $selectedMedia, duration: asset.duration.minAndSec)) - } - } - }) - } catch { - print("Failed to create thumbnail image") - } - } - } - } - } - } - - private func sendGalleryMedia(ids _: [String]) { - // var media: [AttachmentItem] = [] - // let dispatchGroup = DispatchGroup() - // - // let asset = PHAsset.fetchAssets(withLocalIdentifiers: ids, options: nil) - // asset.enumerateObjects { asset, _, _ in - // dispatchGroup.enter() - // if asset.mediaType == .image { - // let manager = PHImageManager.default() - // let option = PHImageRequestOptions() - // option.isSynchronous = true - // - // manager.requestImage( - // for: asset, - // targetSize: PHImageManagerMaximumSize, - // contentMode: .aspectFill, - // options: option - // ) { image, _ in - // if let image { - // let data = image.jpegData(compressionQuality: 1.0) ?? Data() - // media.append(.init(type: .image, data: data, string: "")) - // } - // dispatchGroup.leave() - // } - // } else if asset.mediaType == .video { - // let manager = PHImageManager.default() - // let option = PHVideoRequestOptions() - // option.version = .current - // option.deliveryMode = .highQualityFormat - // - // manager.requestAVAsset(forVideo: asset, options: option) { avAsset, _, _ in - // if let avAsset { - // let url = (avAsset as? AVURLAsset)?.url - // let data = try? Data(contentsOf: url ?? URL(fileURLWithPath: "")) - // media.append(.init(type: .movie, data: data ?? Data(), string: "")) - // } - // dispatchGroup.leave() - // } - // } - // } - // dispatchGroup.notify(queue: .main) { - // store.dispatch(.conversationAction(.sendAttachment(.init( - // id: UUID().uuidString, - // items: media - // )))) - // } - } } -private struct ThumbnailView: Identifiable, View { - let id: String - let gridSize: CGFloat - let duration: String? - - @State private var image: UIImage - @State private var ready = false - @State private var selected = false - @Binding var selectedMedia: [SelectedMedia] - - init(id: String, image: UIImage, gridSize: CGFloat, selected: Binding<[SelectedMedia]>, duration: String? = nil) { - self.id = id - self.image = image - self.gridSize = gridSize - _selectedMedia = selected - self.duration = duration - } +private struct GridViewItem: View { + let item: SharingGalleryItem var body: some View { - if ready { + if let data = item.thumbnail { ZStack { - Image(uiImage: image) + Image(uiImage: UIImage(data: data) ?? UIImage()) .resizable() .aspectRatio(contentMode: .fill) - .frame(width: gridSize, height: gridSize) + .frame(width: Const.galleryGridSize, height: Const.galleryGridSize) .clipped() - if let duration { + if let duration = item.duration { VStack { Spacer() HStack { @@ -326,34 +162,34 @@ private struct ThumbnailView: Identifiable, View { } } } - if selected { - VStack { - HStack { - Spacer() - Circle() - .frame(width: 30, height: 30) - .shadow(color: .black, radius: 2) - .foregroundColor(.Material.Shape.white) - .overlay { - Image(systemName: "checkmark") - .foregroundColor(.Material.Elements.active) - .font(.body3) - } - .padding(4) - } - Spacer() - } - } + // if selected { + // VStack { + // HStack { + // Spacer() + // Circle() + // .frame(width: 30, height: 30) + // .shadow(color: .black, radius: 2) + // .foregroundColor(.Material.Shape.white) + // .overlay { + // Image(systemName: "checkmark") + // .foregroundColor(.Material.Elements.active) + // .font(.body3) + // } + // .padding(4) + // } + // Spacer() + // } + // } } .onTapGesture { - withAnimation { - selected.toggle() - if selected { - selectedMedia.append(SelectedMedia(id: id)) - } else { - selectedMedia.removeAll { $0.id == id } - } - } + // withAnimation { + // selected.toggle() + // if selected { + // selectedMedia.append(SelectedMedia(id: id)) + // } else { + // selectedMedia.removeAll { $0.id == id } + // } + // } } } else { ZStack { @@ -362,15 +198,7 @@ private struct ThumbnailView: Identifiable, View { .overlay { ProgressView() } - .frame(width: gridSize, height: gridSize) - } - .onAppear { - image.scaleAndCropImage(toExampleSize: CGSize(width: gridSize, height: gridSize), completion: { image in - if let image { - self.image = image - ready = true - } - }) + .frame(width: Const.galleryGridSize, height: Const.galleryGridSize) } } } @@ -411,7 +239,7 @@ struct CameraView: UIViewRepresentable { struct CameraPicker: UIViewControllerRepresentable { var sourceType: UIImagePickerController.SourceType - var completionHandler: (Data, Data, AttachmentType) -> Void + var completionHandler: (Data, SharingCameraMediaType) -> Void func makeUIViewController(context: Context) -> UIImagePickerController { let picker = UIImagePickerController() @@ -419,7 +247,7 @@ struct CameraPicker: UIViewControllerRepresentable { picker.delegate = context.coordinator picker.mediaTypes = [UTType.movie.identifier, UTType.image.identifier] picker.videoQuality = .typeHigh - picker.videoMaximumDuration = 60 // 60 sec Limit + picker.videoMaximumDuration = Const.videoDurationLimit picker.view.backgroundColor = .clear return picker } @@ -441,18 +269,17 @@ struct CameraPicker: UIViewControllerRepresentable { // swiftlint:disable:next force_cast let mediaType = info[.mediaType] as! String - // TODO: fix it - // if mediaType == UTType.image.identifier { - // if let image = info[.originalImage] as? UIImage { - // let data = image.jpegData(compressionQuality: 1.0) ?? Data() - // parent.completionHandler(data, .image) - // } - // } else if mediaType == UTType.movie.identifier { - // if let url = info[.mediaURL] as? URL { - // let data = try? Data(contentsOf: url) - // parent.completionHandler(data ?? Data(), .movie) - // } - // } + if mediaType == UTType.image.identifier { + if let image = info[.originalImage] as? UIImage { + let data = image.jpegData(compressionQuality: 1.0) ?? Data() + parent.completionHandler(data, .photo) + } + } else if mediaType == UTType.movie.identifier { + if let url = info[.mediaURL] as? URL { + let data = try? Data(contentsOf: url) + parent.completionHandler(data ?? Data(), .video) + } + } } } } diff --git a/ConversationsClassic/View/Screens/Conversation/ConversationTextInput.swift b/ConversationsClassic/View/Screens/Conversation/ConversationTextInput.swift index e084dbd..dd2c202 100644 --- a/ConversationsClassic/View/Screens/Conversation/ConversationTextInput.swift +++ b/ConversationsClassic/View/Screens/Conversation/ConversationTextInput.swift @@ -89,7 +89,7 @@ struct ConversationTextInput: View { } } .fullScreenCover(isPresented: Binding( - get: { store.state.conversationsState.attachmentPickerVisible }, + get: { store.state.sharingState.sharingShown }, set: { _ in } )) { AttachmentPickerScreen()