wip
This commit is contained in:
parent
50fba234b0
commit
e21610d425
|
@ -7,4 +7,6 @@ enum DatabaseAction: Codable {
|
||||||
case storedChatsLoaded(chats: [Chat])
|
case storedChatsLoaded(chats: [Chat])
|
||||||
|
|
||||||
case storeMessageFailed(reason: String)
|
case storeMessageFailed(reason: String)
|
||||||
|
|
||||||
|
case updateAttachmentFailed(id: String, reason: String)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
enum FileAction: Stateable {
|
enum FileAction: Stateable {
|
||||||
|
case downloadAttachmentFile(id: String, remotePath: URL)
|
||||||
case attachmentFileDownloaded(id: String, localUrl: URL)
|
case attachmentFileDownloaded(id: String, localUrl: URL)
|
||||||
|
case downloadingAttachmentFileFailed(id: String, reason: String)
|
||||||
|
|
||||||
|
case createAttachmentThumbnail(id: String, localUrl: URL)
|
||||||
case attachmentThumbnailCreated(id: String, thumbnailUrl: URL)
|
case attachmentThumbnailCreated(id: String, thumbnailUrl: URL)
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,6 +67,7 @@ extension Database {
|
||||||
table.column("localPath", .text)
|
table.column("localPath", .text)
|
||||||
table.column("remotePath", .text)
|
table.column("remotePath", .text)
|
||||||
table.column("localThumbnailPath", .text)
|
table.column("localThumbnailPath", .text)
|
||||||
|
table.column("downloadFailed", .boolean).notNull().defaults(to: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,30 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
final class DownloadManager {
|
final class DownloadManager {
|
||||||
|
static let shared = DownloadManager()
|
||||||
|
|
||||||
private let urlSession: URLSession
|
private let urlSession: URLSession
|
||||||
|
private let downloadQueue = DispatchQueue(label: "com.example.downloadQueue")
|
||||||
|
private var activeDownloads = Set<URL>()
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
let configuration = URLSessionConfiguration.default
|
let configuration = URLSessionConfiguration.default
|
||||||
urlSession = URLSession(configuration: configuration)
|
urlSession = URLSession(configuration: configuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
func download(from url: URL, to localUrl: URL, completion: @escaping (Error?) -> Void) {
|
func enqueueDownload(from url: URL, to localUrl: URL, completion: @escaping (Error?) -> Void) {
|
||||||
let task = urlSession.downloadTask(with: url) { tempLocalUrl, _, error in
|
downloadQueue.async {
|
||||||
|
if self.activeDownloads.contains(url) {
|
||||||
|
print("Download for this file is already in queue.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.activeDownloads.insert(url)
|
||||||
|
|
||||||
|
let task = self.urlSession.downloadTask(with: url) { tempLocalUrl, _, error in
|
||||||
|
self.downloadQueue.async {
|
||||||
|
self.activeDownloads.remove(url)
|
||||||
|
|
||||||
if let tempLocalUrl = tempLocalUrl, error == nil {
|
if let tempLocalUrl = tempLocalUrl, error == nil {
|
||||||
do {
|
do {
|
||||||
try FileManager.default.copyItem(at: tempLocalUrl, to: localUrl)
|
try FileManager.default.copyItem(at: tempLocalUrl, to: localUrl)
|
||||||
|
@ -21,6 +36,8 @@ final class DownloadManager {
|
||||||
completion(error)
|
completion(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
task.resume()
|
task.resume()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
import Foundation
|
|
||||||
|
|
||||||
final class FileManager {}
|
|
72
ConversationsClassic/AppCore/Files/FileProcessing.swift
Normal file
72
ConversationsClassic/AppCore/Files/FileProcessing.swift
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class FileProcessing {
|
||||||
|
static let shared = FileProcessing()
|
||||||
|
|
||||||
|
static var fileFolder: URL {
|
||||||
|
// swiftlint:disable:next force_unwrapping
|
||||||
|
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||||
|
return documentsURL.appendingPathComponent(Const.fileFolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createThumbnail(id: String, localUrl: URL) -> URL? {
|
||||||
|
// make path for thumbnail
|
||||||
|
let thumbnailUrl = FileProcessing.fileFolder.appendingPathComponent(id).appendingPathExtension("png")
|
||||||
|
|
||||||
|
// check if thumbnail already exists
|
||||||
|
if FileManager.default.fileExists(atPath: thumbnailUrl.path) {
|
||||||
|
return thumbnailUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
// create thumbnail if not exists
|
||||||
|
switch localUrl.lastPathComponent.attachmentType {
|
||||||
|
case .image:
|
||||||
|
guard let image = UIImage(contentsOfFile: localUrl.path) else { return nil }
|
||||||
|
let targetSize = CGSize(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize)
|
||||||
|
guard let thumbnail = scaleAndCropImage(image, targetSize) else { return nil }
|
||||||
|
guard let data = thumbnail.pngData() else { return nil }
|
||||||
|
do {
|
||||||
|
try data.write(to: thumbnailUrl)
|
||||||
|
return thumbnailUrl
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension FileProcessing {
|
||||||
|
func scaleAndCropImage(_ img: UIImage, _ size: CGSize) -> UIImage? {
|
||||||
|
guard let cgImage = img.cgImage else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let contextImage: UIImage = .init(cgImage: cgImage)
|
||||||
|
var contextSize: CGSize = contextImage.size
|
||||||
|
|
||||||
|
var posX: CGFloat = 0.0
|
||||||
|
var posY: CGFloat = 0.0
|
||||||
|
let cgwidth: CGFloat = size.width
|
||||||
|
let cgheight: CGFloat = size.height
|
||||||
|
|
||||||
|
// Check and handle if the image is wider than the requested size
|
||||||
|
if contextSize.width > contextSize.height {
|
||||||
|
posX = ((contextSize.width - contextSize.height) / 2)
|
||||||
|
contextSize.width = contextSize.height
|
||||||
|
} else if contextSize.width < contextSize.height {
|
||||||
|
// Check and handle if the image is taller than the requested size
|
||||||
|
posY = ((contextSize.height - contextSize.width) / 2)
|
||||||
|
contextSize.height = contextSize.width
|
||||||
|
}
|
||||||
|
|
||||||
|
let rect: CGRect = .init(x: posX, y: posY, width: cgwidth, height: cgheight)
|
||||||
|
guard let contextCg = contextImage.cgImage, let imgRef = contextCg.cropping(to: rect) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let image: UIImage = .init(cgImage: imgRef, scale: img.scale, orientation: img.imageOrientation)
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
import GRDB
|
import GRDB
|
||||||
|
|
||||||
|
// swiftlint:disable:next type_body_length
|
||||||
final class DatabaseMiddleware {
|
final class DatabaseMiddleware {
|
||||||
static let shared = DatabaseMiddleware()
|
static let shared = DatabaseMiddleware()
|
||||||
private let database = Database.shared
|
private let database = Database.shared
|
||||||
|
@ -277,11 +278,36 @@ final class DatabaseMiddleware {
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
|
|
||||||
|
// MARK: Attachments
|
||||||
|
case .fileAction(.downloadingAttachmentFileFailed(let id, _)):
|
||||||
|
return Future<AppAction, Never> { promise in
|
||||||
|
Task(priority: .background) { [weak self] in
|
||||||
|
guard let database = self?.database else {
|
||||||
|
promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: L10n.Global.Error.genericDbError)))
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
_ = try database._db.write { db in
|
||||||
|
try Attachment
|
||||||
|
.filter(Column("id") == id)
|
||||||
|
.updateAll(db, Column("downloadFailed").set(to: true))
|
||||||
|
}
|
||||||
|
promise(.success(.empty))
|
||||||
|
} catch {
|
||||||
|
promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: error.localizedDescription)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
|
||||||
case .fileAction(.attachmentFileDownloaded(let id, let localUrl)):
|
case .fileAction(.attachmentFileDownloaded(let id, let localUrl)):
|
||||||
return Future<AppAction, Never> { promise in
|
return Future<AppAction, Never> { promise in
|
||||||
Task(priority: .background) { [weak self] in
|
Task(priority: .background) { [weak self] in
|
||||||
guard let database = self?.database else {
|
guard let database = self?.database else {
|
||||||
promise(.success(.databaseAction(.storeMessageFailed(reason: L10n.Global.Error.genericDbError))))
|
promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: L10n.Global.Error.genericDbError)))
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
|
@ -292,7 +318,31 @@ final class DatabaseMiddleware {
|
||||||
}
|
}
|
||||||
promise(.success(.empty))
|
promise(.success(.empty))
|
||||||
} catch {
|
} catch {
|
||||||
promise(.success(.databaseAction(.storeMessageFailed(reason: L10n.Global.Error.genericDbError))))
|
promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: error.localizedDescription)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
|
||||||
|
case .fileAction(.attachmentThumbnailCreated(let id, let thumbnailUrl)):
|
||||||
|
return Future<AppAction, Never> { promise in
|
||||||
|
Task(priority: .background) { [weak self] in
|
||||||
|
guard let database = self?.database else {
|
||||||
|
promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: L10n.Global.Error.genericDbError)))
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
_ = try database._db.write { db in
|
||||||
|
try Attachment
|
||||||
|
.filter(Column("id") == id)
|
||||||
|
.updateAll(db, Column("localThumbnailPath").set(to: thumbnailUrl))
|
||||||
|
}
|
||||||
|
promise(.success(.empty))
|
||||||
|
} catch {
|
||||||
|
promise(.success(.databaseAction(.updateAttachmentFailed(id: id, reason: error.localizedDescription)))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -346,15 +396,3 @@ private extension DatabaseMiddleware {
|
||||||
.store(in: &conversationCancellables)
|
.store(in: &conversationCancellables)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// try db.write { db in
|
|
||||||
// // Update the attachment
|
|
||||||
// var attachment = try Attachment.fetchOne(db, key: attachmentId)!
|
|
||||||
// attachment.someField = newValue
|
|
||||||
// try attachment.update(db)
|
|
||||||
//
|
|
||||||
// // Update the message
|
|
||||||
// var message = try Message.fetchOne(db, key: messageId)!
|
|
||||||
// message.someField = newValue
|
|
||||||
// try message.update(db)
|
|
||||||
// }
|
|
||||||
|
|
|
@ -4,68 +4,51 @@ import UIKit
|
||||||
|
|
||||||
final class FileMiddleware {
|
final class FileMiddleware {
|
||||||
static let shared = FileMiddleware()
|
static let shared = FileMiddleware()
|
||||||
private var downloader = DownloadManager()
|
|
||||||
|
|
||||||
func middleware(state _: AppState, action: AppAction) -> AnyPublisher<AppAction, Never> {
|
func middleware(state _: AppState, action: AppAction) -> AnyPublisher<AppAction, Never> {
|
||||||
switch action {
|
switch action {
|
||||||
case .conversationAction(.attachmentsUpdated(let attachments)):
|
case .conversationAction(.attachmentsUpdated(let attachments)):
|
||||||
DispatchQueue.global(qos: .background).async {
|
return Future { promise in
|
||||||
for attachment in attachments where attachment.localPath == nil {
|
for attachment in attachments where attachment.localPath == nil && attachment.remotePath != nil {
|
||||||
if let remotePath = attachment.remotePath {
|
|
||||||
let localUrl = self.fileFolder.appendingPathComponent(attachment.id)
|
|
||||||
self.downloader.download(from: remotePath, to: localUrl) { error in
|
|
||||||
if error == nil {
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
store.dispatch(.fileAction(.attachmentFileDownloaded(id: attachment.id, localUrl: localUrl)))
|
// swiftlint:disable:next force_unwrapping
|
||||||
|
store.dispatch(.fileAction(.downloadAttachmentFile(id: attachment.id, remotePath: attachment.remotePath!)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
promise(.success(.empty))
|
||||||
|
}.eraseToAnyPublisher()
|
||||||
|
|
||||||
|
case .fileAction(.downloadAttachmentFile(let id, let remotePath)):
|
||||||
|
return Future { promise in
|
||||||
|
let localUrl = FileProcessing.fileFolder.appendingPathComponent(id).appendingPathExtension(remotePath.pathExtension)
|
||||||
|
DownloadManager.shared.enqueueDownload(from: remotePath, to: localUrl) { error in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let error {
|
||||||
|
store.dispatch(.fileAction(.downloadingAttachmentFileFailed(id: id, reason: error.localizedDescription)))
|
||||||
|
} else {
|
||||||
|
store.dispatch(.fileAction(.attachmentFileDownloaded(id: id, localUrl: localUrl)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
promise(.success(.empty))
|
||||||
}
|
}.eraseToAnyPublisher()
|
||||||
}
|
|
||||||
return Empty().eraseToAnyPublisher()
|
|
||||||
|
|
||||||
case .fileAction(.attachmentFileDownloaded(let id, let localUrl)):
|
case .fileAction(.attachmentFileDownloaded(let id, let localUrl)):
|
||||||
DispatchQueue.global(qos: .background).async {
|
return Just(.fileAction(.createAttachmentThumbnail(id: id, localUrl: localUrl)))
|
||||||
guard let attachment = store.state.conversationsState.currentAttachments.first(where: { $0.id == id }) else { return }
|
.eraseToAnyPublisher()
|
||||||
switch attachment.type {
|
|
||||||
case .image:
|
|
||||||
if let data = try? Data(contentsOf: localUrl), let image = UIImage(data: data) {
|
|
||||||
image.scaleAndCropImage(toExampleSize: CGSizeMake(Const.attachmentPreviewSize, Const.attachmentPreviewSize)) { img in
|
|
||||||
if let img = img, let data = img.jpegData(compressionQuality: 1.0) {
|
|
||||||
let thumbnailUrl = self.fileFolder.appendingPathComponent("\(id)_thumbnail.jpg")
|
|
||||||
do {
|
|
||||||
try data.write(to: thumbnailUrl)
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
store.dispatch(.fileAction(.attachmentThumbnailCreated(id: id, thumbnailUrl: thumbnailUrl)))
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
print("Error writing thumbnail: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case .movie:
|
case .fileAction(.createAttachmentThumbnail(let id, let localUrl)):
|
||||||
// self.downloadAndMakeThumbnail(for: attachment)
|
return Future { promise in
|
||||||
break
|
if let thumbnailUrl = FileProcessing.shared.createThumbnail(id: id, localUrl: localUrl) {
|
||||||
|
promise(.success(.fileAction(.attachmentThumbnailCreated(id: id, thumbnailUrl: thumbnailUrl))))
|
||||||
default:
|
} else {
|
||||||
break
|
promise(.success(.empty))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Empty().eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return Empty().eraseToAnyPublisher()
|
return Empty().eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension FileMiddleware {
|
|
||||||
var fileFolder: URL {
|
|
||||||
// swiftlint:disable:next force_unwrapping
|
|
||||||
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
||||||
return documentsURL.appendingPathComponent(Const.fileFolder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ struct Attachment: DBStorable {
|
||||||
let remotePath: URL?
|
let remotePath: URL?
|
||||||
let localThumbnailPath: URL?
|
let localThumbnailPath: URL?
|
||||||
let messageId: String
|
let messageId: String
|
||||||
|
var downloadFailed: Bool = false
|
||||||
|
|
||||||
static let message = belongsTo(Message.self)
|
static let message = belongsTo(Message.self)
|
||||||
var message: QueryInterfaceRequest<Message> {
|
var message: QueryInterfaceRequest<Message> {
|
||||||
|
@ -35,14 +36,18 @@ extension String {
|
||||||
switch ext {
|
switch ext {
|
||||||
case "mov", "mp4", "avi":
|
case "mov", "mp4", "avi":
|
||||||
return .movie
|
return .movie
|
||||||
|
|
||||||
case "jpg", "png", "gif":
|
case "jpg", "png", "gif":
|
||||||
return .image
|
return .image
|
||||||
|
|
||||||
case "mp3", "wav", "m4a":
|
case "mp3", "wav", "m4a":
|
||||||
return .audio
|
return .audio
|
||||||
|
|
||||||
case "txt", "doc", "pdf":
|
case "txt", "doc", "pdf":
|
||||||
return .file
|
return .file
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return .file // Default to .file if the extension is not recognized
|
return .file
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue