wip
This commit is contained in:
parent
a28d60e128
commit
0ede68e39a
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
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))
|
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))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
if let thumbnail = thumbnail() {
|
||||||
|
thumbnail
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fill)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize)
|
.frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize)
|
||||||
.cornerRadius(10)
|
|
||||||
} else {
|
} 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 {
|
} 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 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 {
|
private struct VideoPlayerView: UIViewControllerRepresentable {
|
||||||
let placeholderImageName: String?
|
let url: URL
|
||||||
|
|
||||||
var body: some View {
|
func makeUIViewController(context _: Context) -> AVPlayerViewController {
|
||||||
Rectangle()
|
let controller = AVPlayerViewController()
|
||||||
.foregroundColor(.Material.Background.dark)
|
controller.player = AVPlayer(url: url)
|
||||||
.frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize)
|
return controller
|
||||||
.overlay {
|
}
|
||||||
ZStack {
|
|
||||||
ProgressView()
|
func updateUIViewController(_: AVPlayerViewController, context _: Context) {
|
||||||
.scaleEffect(1.5)
|
// Update the controller if needed.
|
||||||
.progressViewStyle(CircularProgressViewStyle(tint: .Material.Elements.active))
|
|
||||||
if let imageName = placeholderImageName {
|
|
||||||
Image(systemName: imageName)
|
|
||||||
.font(.body1)
|
|
||||||
.foregroundColor(.Material.Elements.active)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue