wip
This commit is contained in:
parent
485071162c
commit
e564ae5747
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<AppAction, Never> {
|
||||
switch action {
|
||||
case .sharingAction(.checkCameraAccess):
|
||||
return Future<AppAction, Never> { 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<AppAction, Never> { 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<AppAction, Never> { 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<AppAction, Never> { 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
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import Foundation
|
||||
|
||||
final class CameraService {
|
||||
var dumb = false
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = []
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -89,7 +89,7 @@ struct ConversationTextInput: View {
|
|||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: Binding<Bool>(
|
||||
get: { store.state.conversationsState.attachmentPickerVisible },
|
||||
get: { store.state.sharingState.sharingShown },
|
||||
set: { _ in }
|
||||
)) {
|
||||
AttachmentPickerScreen()
|
||||
|
|
Loading…
Reference in a new issue