wip
This commit is contained in:
parent
a28d60e128
commit
0ede68e39a
|
@ -11,4 +11,5 @@ enum AppAction: Codable {
|
|||
case chatsAction(_ action: ChatsAction)
|
||||
case conversationAction(_ action: ConversationAction)
|
||||
case sharingAction(_ action: SharingAction)
|
||||
case fileAction(_ action: FileAction)
|
||||
}
|
||||
|
|
6
ConversationsClassic/AppCore/Actions/FileActions.swift
Normal file
6
ConversationsClassic/AppCore/Actions/FileActions.swift
Normal file
|
@ -0,0 +1,6 @@
|
|||
import Foundation
|
||||
|
||||
enum FileAction: Stateable {
|
||||
case attachmentFileDownloaded(id: String, localUrl: URL)
|
||||
case attachmentThumbnailCreated(id: String, thumbnailUrl: URL)
|
||||
}
|
|
@ -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))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
if let attachment {
|
||||
switch attachment.type {
|
||||
case .image:
|
||||
if let thumbnail = thumbnail() {
|
||||
thumbnail
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize)
|
||||
.cornerRadius(10)
|
||||
} else {
|
||||
AttachmentPlaceholderView(placeholderImageName: progressImageName(attachment.type))
|
||||
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 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 AttachmentPlaceholderView: View {
|
||||
let placeholderImageName: String?
|
||||
private struct VideoPlayerView: UIViewControllerRepresentable {
|
||||
let url: URL
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
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.
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue