This commit is contained in:
fmodf 2024-07-12 13:43:14 +02:00
parent a28d60e128
commit 0ede68e39a
7 changed files with 181 additions and 41 deletions

View file

@ -11,4 +11,5 @@ enum AppAction: Codable {
case chatsAction(_ action: ChatsAction) case chatsAction(_ action: ChatsAction)
case conversationAction(_ action: ConversationAction) case conversationAction(_ action: ConversationAction)
case sharingAction(_ action: SharingAction) case sharingAction(_ action: SharingAction)
case fileAction(_ action: FileAction)
} }

View file

@ -0,0 +1,6 @@
import Foundation
enum FileAction: Stateable {
case attachmentFileDownloaded(id: String, localUrl: URL)
case attachmentThumbnailCreated(id: String, thumbnailUrl: URL)
}

View file

@ -271,8 +271,28 @@ final class DatabaseMiddleware {
} }
promise(.success(.empty)) promise(.success(.empty))
} catch { } catch {
promise(.success(.databaseAction(.storeMessageFailed(reason: error.localizedDescription))) promise(.success(.databaseAction(.storeMessageFailed(reason: error.localizedDescription))))
) }
}
}
.eraseToAnyPublisher()
case .fileAction(.attachmentFileDownloaded(let id, let localUrl)):
return Future<AppAction, Never> { promise in
Task(priority: .background) { [weak self] in
guard let database = self?.database else {
promise(.success(.databaseAction(.storeMessageFailed(reason: L10n.Global.Error.genericDbError))))
return
}
do {
_ = try database._db.write { db in
try Attachment
.filter(Column("id") == id)
.updateAll(db, Column("localPath").set(to: localUrl))
}
promise(.success(.empty))
} catch {
promise(.success(.databaseAction(.storeMessageFailed(reason: L10n.Global.Error.genericDbError))))
} }
} }
} }

View file

@ -1,15 +1,57 @@
import Combine import Combine
import Foundation
import UIKit
final class FileMiddleware { final class FileMiddleware {
static let shared = AccountsMiddleware() 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(.messagesUpdated(let messages)): case .conversationAction(.attachmentsUpdated(let attachments)):
for msg in messages { DispatchQueue.global(qos: .background).async {
if msg.attachment != nil { for attachment in attachments where attachment.localPath == nil {
print("Attachment found") 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 {
store.dispatch(.fileAction(.attachmentFileDownloaded(id: attachment.id, localUrl: localUrl)))
}
}
}
}
}
}
return Empty().eraseToAnyPublisher()
case .fileAction(.attachmentFileDownloaded(let id, let localUrl)):
DispatchQueue.global(qos: .background).async {
guard let attachment = store.state.conversationsState.currentAttachments.first(where: { $0.id == id }) else { return }
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:
// self.downloadAndMakeThumbnail(for: attachment)
break
default:
break
} }
} }
return Empty().eraseToAnyPublisher() return Empty().eraseToAnyPublisher()
@ -19,3 +61,36 @@ final class FileMiddleware {
} }
} }
} }
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)
}
}
private final class DownloadManager {
private let urlSession: URLSession
init() {
let configuration = URLSessionConfiguration.default
urlSession = URLSession(configuration: configuration)
}
func download(from url: URL, to localUrl: URL, completion: @escaping (Error?) -> Void) {
let task = urlSession.downloadTask(with: url) { tempLocalUrl, _, error in
if let tempLocalUrl = tempLocalUrl, error == nil {
do {
try FileManager.default.copyItem(at: tempLocalUrl, to: localUrl)
completion(nil)
} catch let writeError {
completion(writeError)
}
} else {
completion(error)
}
}
task.resume()
}
}

View file

@ -13,8 +13,8 @@ extension AppState {
case .startAction(let action): case .startAction(let action):
StartState.reducer(state: &state.startState, action: action) StartState.reducer(state: &state.startState, action: action)
case .databaseAction, .xmppAction, .empty: case .databaseAction, .xmppAction, .fileAction, .empty:
break // database and xmpp actions are processed by other middlewares break // this actions are processed by other middlewares
case .accountsAction(let action): case .accountsAction(let action):
AccountsState.reducer(state: &state.accountsState, action: action) AccountsState.reducer(state: &state.accountsState, action: action)

View file

@ -33,7 +33,7 @@ enum Const {
static let videoDurationLimit = 60.0 static let videoDurationLimit = 60.0
// Upload/download file folder // Upload/download file folder
static let fileFolder = "ConversationsClassic" static let fileFolder = "Downloads"
// Grid size for gallery preview (3 in a row) // Grid size for gallery preview (3 in a row)
static let galleryGridSize = UIScreen.main.bounds.width / 3 static let galleryGridSize = UIScreen.main.bounds.width / 3

View file

@ -1,3 +1,4 @@
import AVKit
import MapKit import MapKit
import SwiftUI import SwiftUI
@ -71,21 +72,59 @@ private struct AttachmentView: View {
let attachmentId: String let attachmentId: String
var body: some View { var body: some View {
if let attachment = store.state.conversationsState.currentAttachments.first(where: { $0.id == attachmentId }) { if let attachment {
if let localPath = attachment.localPath { switch attachment.type {
Image(systemName: "questionmark.square") case .image:
.resizable() if let thumbnail = thumbnail() {
.aspectRatio(contentMode: .fill) thumbnail
.frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize) .resizable()
.cornerRadius(10) .aspectRatio(contentMode: .fit)
} else { .frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize)
AttachmentPlaceholderView(placeholderImageName: progressImageName(attachment.type)) } else {
placeholder
}
case .movie:
if let file = attachment.localPath {
VideoPlayerView(url: file)
.frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize)
.cornerRadius(Const.attachmentPreviewSize / 10)
.overlay(RoundedRectangle(cornerRadius: Const.attachmentPreviewSize / 10).stroke(Color.Material.Shape.separator, lineWidth: 1))
} else {
placeholder
}
default:
placeholder
} }
} else { } else {
AttachmentPlaceholderView(placeholderImageName: nil) placeholder
} }
} }
@ViewBuilder private var placeholder: some View {
Rectangle()
.foregroundColor(.Material.Background.dark)
.frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize)
.overlay {
ZStack {
ProgressView()
.scaleEffect(1.5)
.progressViewStyle(CircularProgressViewStyle(tint: .Material.Elements.active))
if let attachment {
let imageName = progressImageName(attachment.type)
Image(systemName: imageName)
.font(.body1)
.foregroundColor(.Material.Elements.active)
}
}
}
}
private var attachment: Attachment? {
store.state.conversationsState.currentAttachments.first(where: { $0.id == attachmentId })
}
private func progressImageName(_ type: AttachmentType) -> String { private func progressImageName(_ type: AttachmentType) -> String {
switch type { switch type {
case .image: case .image:
@ -98,26 +137,25 @@ private struct AttachmentView: View {
return "doc" return "doc"
} }
} }
}
private struct AttachmentPlaceholderView: View { private func thumbnail() -> Image? {
let placeholderImageName: String? guard let attachment = attachment else { return nil }
guard let thumbnailPath = attachment.localThumbnailPath else { return nil }
var body: some View { guard let uiImage = UIImage(contentsOfFile: thumbnailPath.path()) else { return nil }
Rectangle() return Image(uiImage: uiImage)
.foregroundColor(.Material.Background.dark) }
.frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize) }
.overlay {
ZStack { private struct VideoPlayerView: UIViewControllerRepresentable {
ProgressView() let url: URL
.scaleEffect(1.5)
.progressViewStyle(CircularProgressViewStyle(tint: .Material.Elements.active)) func makeUIViewController(context _: Context) -> AVPlayerViewController {
if let imageName = placeholderImageName { let controller = AVPlayerViewController()
Image(systemName: imageName) controller.player = AVPlayer(url: url)
.font(.body1) return controller
.foregroundColor(.Material.Elements.active) }
}
} func updateUIViewController(_: AVPlayerViewController, context _: Context) {
} // Update the controller if needed.
} }
} }