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 conversationAction(_ action: ConversationAction)
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))
} 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 Foundation
import UIKit
final class FileMiddleware {
static let shared = AccountsMiddleware()
static let shared = FileMiddleware()
private var downloader = DownloadManager()
func middleware(state _: AppState, action: AppAction) -> AnyPublisher<AppAction, Never> {
switch action {
case .conversationAction(.messagesUpdated(let messages)):
for msg in messages {
if msg.attachment != nil {
print("Attachment found")
case .conversationAction(.attachmentsUpdated(let attachments)):
DispatchQueue.global(qos: .background).async {
for attachment in attachments where attachment.localPath == 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 {
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()
@ -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):
StartState.reducer(state: &state.startState, action: action)
case .databaseAction, .xmppAction, .empty:
break // database and xmpp actions are processed by other middlewares
case .databaseAction, .xmppAction, .fileAction, .empty:
break // this actions are processed by other middlewares
case .accountsAction(let action):
AccountsState.reducer(state: &state.accountsState, action: action)

View file

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

View file

@ -1,3 +1,4 @@
import AVKit
import MapKit
import SwiftUI
@ -71,21 +72,59 @@ private struct AttachmentView: View {
let attachmentId: String
var body: some View {
if let attachment = store.state.conversationsState.currentAttachments.first(where: { $0.id == attachmentId }) {
if let localPath = attachment.localPath {
Image(systemName: "questionmark.square")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize)
.cornerRadius(10)
} else {
AttachmentPlaceholderView(placeholderImageName: progressImageName(attachment.type))
if let attachment {
switch attachment.type {
case .image:
if let thumbnail = thumbnail() {
thumbnail
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize)
} 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 {
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 {
switch type {
case .image:
@ -98,26 +137,25 @@ private struct AttachmentView: View {
return "doc"
}
}
}
private struct AttachmentPlaceholderView: View {
let placeholderImageName: String?
var body: 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 imageName = placeholderImageName {
Image(systemName: imageName)
.font(.body1)
.foregroundColor(.Material.Elements.active)
}
}
}
private func thumbnail() -> Image? {
guard let attachment = attachment else { return nil }
guard let thumbnailPath = attachment.localThumbnailPath else { return nil }
guard let uiImage = UIImage(contentsOfFile: thumbnailPath.path()) else { return nil }
return Image(uiImage: uiImage)
}
}
private struct VideoPlayerView: UIViewControllerRepresentable {
let url: URL
func makeUIViewController(context _: Context) -> AVPlayerViewController {
let controller = AVPlayerViewController()
controller.player = AVPlayer(url: url)
return controller
}
func updateUIViewController(_: AVPlayerViewController, context _: Context) {
// Update the controller if needed.
}
}