wip
This commit is contained in:
parent
485071162c
commit
e564ae5747
|
@ -4,20 +4,18 @@ enum SharingAction: Stateable {
|
||||||
case showSharing(Bool)
|
case showSharing(Bool)
|
||||||
|
|
||||||
case shareLocation(lat: Double, lon: Double)
|
case shareLocation(lat: Double, lon: Double)
|
||||||
|
|
||||||
case shareContact(jid: String)
|
case shareContact(jid: String)
|
||||||
|
|
||||||
case shareDocuments([Data])
|
case shareDocuments([Data])
|
||||||
|
|
||||||
// case sendAttachment([ShareItem])
|
case checkCameraAccess
|
||||||
// case sendAttachmentDone
|
case setCameraAccess(Bool)
|
||||||
// case sendAttachmentError(reason: String)
|
|
||||||
}
|
|
||||||
|
|
||||||
// struct ShareItem: Stateable {
|
case checkGalleryAccess
|
||||||
// let id: String
|
case setGalleryAccess(Bool)
|
||||||
// let type: AttachmentType
|
case fetchGallery
|
||||||
// let data: Data
|
case galleryFetched([SharingGalleryItem])
|
||||||
// let thumbnail: Data
|
case thumbnailUpdated(Data, String)
|
||||||
// let string: String
|
|
||||||
// }
|
case cameraCaptured(media: Data, type: SharingCameraMediaType)
|
||||||
|
case flushCameraCaptured
|
||||||
|
}
|
||||||
|
|
|
@ -44,6 +44,22 @@ extension Database {
|
||||||
table.column("type", .integer).notNull()
|
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
|
// messages
|
||||||
try db.create(table: "messages", options: [.ifNotExists]) { table in
|
try db.create(table: "messages", options: [.ifNotExists]) { table in
|
||||||
table.column("id", .text).notNull().primaryKey().unique(onConflict: .replace)
|
table.column("id", .text).notNull().primaryKey().unique(onConflict: .replace)
|
||||||
|
@ -60,22 +76,6 @@ extension Database {
|
||||||
table.column("sentError", .boolean).notNull()
|
table.column("sentError", .boolean).notNull()
|
||||||
table.column("attachment", .text).references("attachments", onDelete: .cascade)
|
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
|
// return migrator
|
||||||
|
|
|
@ -1,15 +1,354 @@
|
||||||
|
import AVFoundation
|
||||||
import Combine
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
import Martin
|
import Photos
|
||||||
|
import UIKit
|
||||||
|
|
||||||
final class SharingMiddleware {
|
final class SharingMiddleware {
|
||||||
static let shared = SharingMiddleware()
|
static let shared = SharingMiddleware()
|
||||||
private let gallery = GalleryService()
|
|
||||||
|
|
||||||
|
// swiftlint:disable:next function_body_length
|
||||||
func middleware(state _: AppState, action: AppAction) -> AnyPublisher<AppAction, Never> {
|
func middleware(state _: AppState, action: AppAction) -> AnyPublisher<AppAction, Never> {
|
||||||
switch action {
|
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:
|
default:
|
||||||
return Empty().eraseToAnyPublisher()
|
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 {
|
extension SharingState {
|
||||||
static func reducer(state: inout SharingState, action: SharingAction) {
|
static func reducer(state: inout SharingState, action: SharingAction) {
|
||||||
switch action {
|
switch action {
|
||||||
case .showSharing(let shown):
|
case .showSharing(let shown):
|
||||||
state.sharingShown = 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:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class CameraService {
|
||||||
|
var dumb = false
|
||||||
|
}
|
|
@ -4,7 +4,6 @@ struct ConversationState: Stateable {
|
||||||
var currentMessages: [Message]
|
var currentMessages: [Message]
|
||||||
|
|
||||||
var replyText: String
|
var replyText: String
|
||||||
var attachmentPickerVisible: Bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Init
|
// MARK: Init
|
||||||
|
@ -12,6 +11,5 @@ extension ConversationState {
|
||||||
init() {
|
init() {
|
||||||
currentMessages = []
|
currentMessages = []
|
||||||
replyText = ""
|
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 {
|
struct SharingState: Stateable {
|
||||||
var sharingShown: Bool
|
var sharingShown: Bool
|
||||||
|
var isCameraAccessGranted: Bool
|
||||||
|
var isGalleryAccessGranted: Bool
|
||||||
|
|
||||||
|
var cameraCapturedMedia: Data
|
||||||
|
var cameraCapturedMediaType: SharingCameraMediaType
|
||||||
|
|
||||||
|
var galleryItems: [SharingGalleryItem]
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Init
|
// MARK: Init
|
||||||
extension SharingState {
|
extension SharingState {
|
||||||
init() {
|
init() {
|
||||||
sharingShown = false
|
sharingShown = false
|
||||||
|
isCameraAccessGranted = false
|
||||||
|
isGalleryAccessGranted = false
|
||||||
|
|
||||||
|
cameraCapturedMedia = Data()
|
||||||
|
cameraCapturedMediaType = .photo
|
||||||
|
|
||||||
|
galleryItems = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
enum Const {
|
enum Const {
|
||||||
// // Network
|
// // Network
|
||||||
|
@ -28,6 +29,12 @@ enum Const {
|
||||||
case conversations = "conversations.im"
|
case conversations = "conversations.im"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Limit for video for sharing
|
||||||
|
static let videoDurationLimit = 60.0
|
||||||
|
|
||||||
// Upload/download file folder
|
// Upload/download file folder
|
||||||
static let fileFolder = "ConversationsClassic"
|
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 {
|
struct AttachmentMediaPickerView: View {
|
||||||
@State private var isCameraAccessGranted = AVCaptureDevice.authorizationStatus(for: .video) == .authorized
|
@EnvironmentObject var store: AppStore
|
||||||
@State private var isGalleryAccessGranted = PHPhotoLibrary.authorizationStatus() == .authorized
|
|
||||||
|
|
||||||
@State private var thumbnails = [ThumbnailView]()
|
|
||||||
@State private var selectedMedia = [SelectedMedia]()
|
@State private var selectedMedia = [SelectedMedia]()
|
||||||
|
|
||||||
@State private var showCameraPicker = false
|
@State private var showCameraPicker = false
|
||||||
|
|
||||||
let gridSize = UIScreen.main.bounds.width / 3
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
let columns = Array(repeating: GridItem(.flexible(), spacing: 0), count: 3)
|
let columns = Array(repeating: GridItem(.flexible(), spacing: 0), count: 3)
|
||||||
|
|
||||||
|
@ -26,7 +21,7 @@ struct AttachmentMediaPickerView: View {
|
||||||
ScrollView(showsIndicators: false) {
|
ScrollView(showsIndicators: false) {
|
||||||
LazyVGrid(columns: columns, spacing: 0) {
|
LazyVGrid(columns: columns, spacing: 0) {
|
||||||
// For camera
|
// For camera
|
||||||
if isCameraAccessGranted {
|
if store.state.sharingState.isCameraAccessGranted {
|
||||||
ZStack {
|
ZStack {
|
||||||
CameraView()
|
CameraView()
|
||||||
.aspectRatio(1, contentMode: .fit)
|
.aspectRatio(1, contentMode: .fit)
|
||||||
|
@ -67,9 +62,9 @@ struct AttachmentMediaPickerView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
// For gallery
|
// For gallery
|
||||||
if isGalleryAccessGranted {
|
if store.state.sharingState.isGalleryAccessGranted {
|
||||||
ForEach(thumbnails) { photo in
|
ForEach(store.state.sharingState.galleryItems) { item in
|
||||||
photo
|
GridViewItem(item: item)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Button {
|
Button {
|
||||||
|
@ -95,19 +90,12 @@ struct AttachmentMediaPickerView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.fullScreenCover(isPresented: $showCameraPicker) {
|
.fullScreenCover(isPresented: $showCameraPicker) {
|
||||||
// TODO: fix it
|
CameraPicker(sourceType: .camera) { data, type in
|
||||||
// CameraPicker(sourceType: .camera) { data, type in
|
store.dispatch(.sharingAction(.cameraCaptured(media: data, type: type)))
|
||||||
// store.dispatch(.conversationAction(.sendAttachment([.init(
|
showCameraPicker = false
|
||||||
// id: UUID().uuidString,
|
store.dispatch(.sharingAction(.showSharing(false)))
|
||||||
// type: type,
|
}
|
||||||
// data: data,
|
.edgesIgnoringSafeArea(.all)
|
||||||
// thumbnail: Data(),
|
|
||||||
// string: ""
|
|
||||||
// )])))
|
|
||||||
// showCameraPicker = false
|
|
||||||
// store.dispatch(.conversationAction(.showAttachmentPicker(false)))
|
|
||||||
// }
|
|
||||||
// .edgesIgnoringSafeArea(.all)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send panel
|
// Send panel
|
||||||
|
@ -130,190 +118,38 @@ struct AttachmentMediaPickerView: View {
|
||||||
.clipped()
|
.clipped()
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
let ids = selectedMedia.map { $0.id }
|
let ids = selectedMedia.map { $0.id }
|
||||||
sendGalleryMedia(ids: ids)
|
// sendGalleryMedia(ids: ids)
|
||||||
store.dispatch(.sharingAction(.showSharing(false)))
|
store.dispatch(.sharingAction(.showSharing(false)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 0.2) {
|
store.dispatch(.sharingAction(.checkCameraAccess))
|
||||||
checkCameraAccess()
|
store.dispatch(.sharingAction(.checkGalleryAccess))
|
||||||
checkGalleryAccess()
|
// DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 0.2) {
|
||||||
}
|
// checkCameraAccess()
|
||||||
}
|
// checkGalleryAccess()
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
// ))))
|
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
.onChange(of: store.state.sharingState.isGalleryAccessGranted) { granted in
|
||||||
|
if granted {
|
||||||
|
store.dispatch(.sharingAction(.fetchGallery))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct ThumbnailView: Identifiable, View {
|
private struct GridViewItem: View {
|
||||||
let id: String
|
let item: SharingGalleryItem
|
||||||
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 {
|
var body: some View {
|
||||||
if ready {
|
if let data = item.thumbnail {
|
||||||
ZStack {
|
ZStack {
|
||||||
Image(uiImage: image)
|
Image(uiImage: UIImage(data: data) ?? UIImage())
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fill)
|
.aspectRatio(contentMode: .fill)
|
||||||
.frame(width: gridSize, height: gridSize)
|
.frame(width: Const.galleryGridSize, height: Const.galleryGridSize)
|
||||||
.clipped()
|
.clipped()
|
||||||
if let duration {
|
if let duration = item.duration {
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
HStack {
|
HStack {
|
||||||
|
@ -326,34 +162,34 @@ private struct ThumbnailView: Identifiable, View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if selected {
|
// if selected {
|
||||||
VStack {
|
// VStack {
|
||||||
HStack {
|
// HStack {
|
||||||
Spacer()
|
// Spacer()
|
||||||
Circle()
|
// Circle()
|
||||||
.frame(width: 30, height: 30)
|
// .frame(width: 30, height: 30)
|
||||||
.shadow(color: .black, radius: 2)
|
// .shadow(color: .black, radius: 2)
|
||||||
.foregroundColor(.Material.Shape.white)
|
// .foregroundColor(.Material.Shape.white)
|
||||||
.overlay {
|
// .overlay {
|
||||||
Image(systemName: "checkmark")
|
// Image(systemName: "checkmark")
|
||||||
.foregroundColor(.Material.Elements.active)
|
// .foregroundColor(.Material.Elements.active)
|
||||||
.font(.body3)
|
// .font(.body3)
|
||||||
}
|
// }
|
||||||
.padding(4)
|
// .padding(4)
|
||||||
}
|
// }
|
||||||
Spacer()
|
// Spacer()
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
withAnimation {
|
// withAnimation {
|
||||||
selected.toggle()
|
// selected.toggle()
|
||||||
if selected {
|
// if selected {
|
||||||
selectedMedia.append(SelectedMedia(id: id))
|
// selectedMedia.append(SelectedMedia(id: id))
|
||||||
} else {
|
// } else {
|
||||||
selectedMedia.removeAll { $0.id == id }
|
// selectedMedia.removeAll { $0.id == id }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ZStack {
|
ZStack {
|
||||||
|
@ -362,15 +198,7 @@ private struct ThumbnailView: Identifiable, View {
|
||||||
.overlay {
|
.overlay {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
}
|
}
|
||||||
.frame(width: gridSize, height: gridSize)
|
.frame(width: Const.galleryGridSize, height: Const.galleryGridSize)
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
image.scaleAndCropImage(toExampleSize: CGSize(width: gridSize, height: gridSize), completion: { image in
|
|
||||||
if let image {
|
|
||||||
self.image = image
|
|
||||||
ready = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -411,7 +239,7 @@ struct CameraView: UIViewRepresentable {
|
||||||
|
|
||||||
struct CameraPicker: UIViewControllerRepresentable {
|
struct CameraPicker: UIViewControllerRepresentable {
|
||||||
var sourceType: UIImagePickerController.SourceType
|
var sourceType: UIImagePickerController.SourceType
|
||||||
var completionHandler: (Data, Data, AttachmentType) -> Void
|
var completionHandler: (Data, SharingCameraMediaType) -> Void
|
||||||
|
|
||||||
func makeUIViewController(context: Context) -> UIImagePickerController {
|
func makeUIViewController(context: Context) -> UIImagePickerController {
|
||||||
let picker = UIImagePickerController()
|
let picker = UIImagePickerController()
|
||||||
|
@ -419,7 +247,7 @@ struct CameraPicker: UIViewControllerRepresentable {
|
||||||
picker.delegate = context.coordinator
|
picker.delegate = context.coordinator
|
||||||
picker.mediaTypes = [UTType.movie.identifier, UTType.image.identifier]
|
picker.mediaTypes = [UTType.movie.identifier, UTType.image.identifier]
|
||||||
picker.videoQuality = .typeHigh
|
picker.videoQuality = .typeHigh
|
||||||
picker.videoMaximumDuration = 60 // 60 sec Limit
|
picker.videoMaximumDuration = Const.videoDurationLimit
|
||||||
picker.view.backgroundColor = .clear
|
picker.view.backgroundColor = .clear
|
||||||
return picker
|
return picker
|
||||||
}
|
}
|
||||||
|
@ -441,18 +269,17 @@ struct CameraPicker: UIViewControllerRepresentable {
|
||||||
// swiftlint:disable:next force_cast
|
// swiftlint:disable:next force_cast
|
||||||
let mediaType = info[.mediaType] as! String
|
let mediaType = info[.mediaType] as! String
|
||||||
|
|
||||||
// TODO: fix it
|
if mediaType == UTType.image.identifier {
|
||||||
// if mediaType == UTType.image.identifier {
|
if let image = info[.originalImage] as? UIImage {
|
||||||
// if let image = info[.originalImage] as? UIImage {
|
let data = image.jpegData(compressionQuality: 1.0) ?? Data()
|
||||||
// let data = image.jpegData(compressionQuality: 1.0) ?? Data()
|
parent.completionHandler(data, .photo)
|
||||||
// parent.completionHandler(data, .image)
|
}
|
||||||
// }
|
} else if mediaType == UTType.movie.identifier {
|
||||||
// } else if mediaType == UTType.movie.identifier {
|
if let url = info[.mediaURL] as? URL {
|
||||||
// if let url = info[.mediaURL] as? URL {
|
let data = try? Data(contentsOf: url)
|
||||||
// let data = try? Data(contentsOf: url)
|
parent.completionHandler(data ?? Data(), .video)
|
||||||
// parent.completionHandler(data ?? Data(), .movie)
|
}
|
||||||
// }
|
}
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,7 +89,7 @@ struct ConversationTextInput: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.fullScreenCover(isPresented: Binding<Bool>(
|
.fullScreenCover(isPresented: Binding<Bool>(
|
||||||
get: { store.state.conversationsState.attachmentPickerVisible },
|
get: { store.state.sharingState.sharingShown },
|
||||||
set: { _ in }
|
set: { _ in }
|
||||||
)) {
|
)) {
|
||||||
AttachmentPickerScreen()
|
AttachmentPickerScreen()
|
||||||
|
|
Loading…
Reference in a new issue