diff --git a/ConversationsClassic/AppCore/Actions/XMPPActions.swift b/ConversationsClassic/AppCore/Actions/XMPPActions.swift index 7bde93c..9195638 100644 --- a/ConversationsClassic/AppCore/Actions/XMPPActions.swift +++ b/ConversationsClassic/AppCore/Actions/XMPPActions.swift @@ -6,8 +6,7 @@ enum XMPPAction: Codable { case xmppMessageSendFailed(msgId: String) case xmppMessageSendSuccess(msgId: String) - case xmppAttachmentUpload(Message) - // case xmppAttachmentSlotRequestDone(String) //TODO: ??? + case xmppAttachmentTryUpload(Message) case xmppAttachmentUploadFailed(msgId: String, reason: String) case xmppAttachmentUploadSuccess(msgId: String, attachmentRemotePath: String) } diff --git a/ConversationsClassic/AppCore/Database/Database.swift b/ConversationsClassic/AppCore/Database/Database.swift index d334bb0..dc06f02 100644 --- a/ConversationsClassic/AppCore/Database/Database.swift +++ b/ConversationsClassic/AppCore/Database/Database.swift @@ -47,7 +47,7 @@ private extension Database { // verbose and debugging in DEBUG builds only. config.publicStatementArguments = true config.prepareDatabase { db in - db.trace { print("SQL> \($0)") } + db.trace { print("SQL> \($0)\n") } } #endif return config diff --git a/ConversationsClassic/AppCore/Middlewares/DatabaseMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/DatabaseMiddleware.swift index 5d9ff3e..0972ee5 100644 --- a/ConversationsClassic/AppCore/Middlewares/DatabaseMiddleware.swift +++ b/ConversationsClassic/AppCore/Middlewares/DatabaseMiddleware.swift @@ -355,6 +355,7 @@ final class DatabaseMiddleware { } .eraseToAnyPublisher() + // MARK: Sharing case .conversationAction(.sendMediaMessages(let from, let to, let messageIds, let localFilesNames)): return Future { promise in Task(priority: .background) { [weak self] in @@ -378,6 +379,7 @@ final class DatabaseMiddleware { date: Date(), pending: true, sentError: false, + attachmentType: localFilesNames[index].attachmentType, attachmentLocalName: localFilesNames[index] ) try database._db.write { db in @@ -393,6 +395,52 @@ final class DatabaseMiddleware { } .eraseToAnyPublisher() + case .xmppAction(.xmppAttachmentUploadSuccess(let messageId, let remotePath)): + return Future { promise in + Task(priority: .background) { [weak self] in + guard let database = self?.database else { + promise(.success(.databaseAction(.updateAttachmentFailed(id: messageId, reason: L10n.Global.Error.genericDbError))) + ) + return + } + do { + _ = try database._db.write { db in + try Message + .filter(Column("id") == messageId) + .updateAll(db, Column("attachmentRemotePath").set(to: remotePath), Column("pending").set(to: false)) + } + promise(.success(.empty)) + } catch { + promise(.success(.databaseAction(.updateAttachmentFailed(id: messageId, reason: error.localizedDescription))) + ) + } + } + } + .eraseToAnyPublisher() + + case .xmppAction(.xmppAttachmentUploadFailed(let messageId, _)): + return Future { promise in + Task(priority: .background) { [weak self] in + guard let database = self?.database else { + promise(.success(.databaseAction(.updateAttachmentFailed(id: messageId, reason: L10n.Global.Error.genericDbError))) + ) + return + } + do { + _ = try database._db.write { db in + try Message + .filter(Column("id") == messageId) + .updateAll(db, Column("pending").set(to: false), Column("sentError").set(to: true)) + } + promise(.success(.empty)) + } catch { + promise(.success(.databaseAction(.updateAttachmentFailed(id: messageId, reason: error.localizedDescription))) + ) + } + } + } + .eraseToAnyPublisher() + default: return Empty().eraseToAnyPublisher() } diff --git a/ConversationsClassic/AppCore/Middlewares/FileMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/FileMiddleware.swift index 8994c39..237002d 100644 --- a/ConversationsClassic/AppCore/Middlewares/FileMiddleware.swift +++ b/ConversationsClassic/AppCore/Middlewares/FileMiddleware.swift @@ -15,6 +15,8 @@ final class FileMiddleware { promise(.success(.empty)) return } + + // for incoming messages with attachments for message in messages where message.attachmentRemotePath != nil && message.attachmentLocalPath == nil { if wSelf.downloadingMessageIDs.contains(message.id) { continue @@ -25,6 +27,27 @@ final class FileMiddleware { store.dispatch(.fileAction(.downloadAttachmentFile(messageId: message.id, attachmentRemotePath: message.attachmentRemotePath!))) } } + + // for outgoing messages with shared attachments + for message in messages where message.attachmentLocalPath != nil && message.attachmentRemotePath == nil && message.pending { + if wSelf.downloadingMessageIDs.contains(message.id) { + continue + } + wSelf.downloadingMessageIDs.insert(message.id) + DispatchQueue.main.async { + store.dispatch(.xmppAction(.xmppAttachmentTryUpload(message))) + } + } + + // for outgoing messages with shared attachments which are already uploaded + // but have no thumbnail + for message in messages where message.attachmentLocalName != nil && message.attachmentRemotePath != nil && message.attachmentThumbnailName == nil && !message.pending && !message.sentError { + DispatchQueue.main.async { + // swiftlint:disable:next force_unwrapping + store.dispatch(.fileAction(.createAttachmentThumbnail(messageId: message.id, localName: message.attachmentLocalName!))) + } + } + promise(.success(.empty)) }.eraseToAnyPublisher() diff --git a/ConversationsClassic/AppCore/Middlewares/SharingMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/SharingMiddleware.swift index 3a88081..d31606d 100644 --- a/ConversationsClassic/AppCore/Middlewares/SharingMiddleware.swift +++ b/ConversationsClassic/AppCore/Middlewares/SharingMiddleware.swift @@ -7,7 +7,6 @@ import UIKit final class SharingMiddleware { static let shared = SharingMiddleware() - // swiftlint:disable:next function_body_length func middleware(state: AppState, action: AppAction) -> AnyPublisher { switch action { // MARK: - Camera and Gallery Access diff --git a/ConversationsClassic/AppCore/Middlewares/XMPPMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/XMPPMiddleware.swift index c7f2a5a..a3f677e 100644 --- a/ConversationsClassic/AppCore/Middlewares/XMPPMiddleware.swift +++ b/ConversationsClassic/AppCore/Middlewares/XMPPMiddleware.swift @@ -6,6 +6,7 @@ final class XMPPMiddleware { static let shared = XMPPMiddleware() private let service = XMPPService(manager: Database.shared) private var cancellables: Set = [] + private var uploadingMessageIDs = ThreadSafeSet() private init() { service.clientState.sink { client, state in @@ -100,14 +101,20 @@ final class XMPPMiddleware { } .eraseToAnyPublisher() - case .xmppAction(.xmppAttachmentUpload(let message)): + case .xmppAction(.xmppAttachmentTryUpload(let message)): return Future { [weak self] promise in - DispatchQueue.global().async { - self?.service.uploadAttachment(message: message) { done, remotePath in - if done { - promise(.success(.xmppAction(.xmppAttachmentUploadSuccess(msgId: message.id, attachmentRemotePath: remotePath)))) - } else { - promise(.success(.xmppAction(.xmppAttachmentUploadFailed(msgId: message.id, reason: "Upload failed")))) + if self?.uploadingMessageIDs.contains(message.id) ?? false { + return promise(.success(.empty)) + } else { + self?.uploadingMessageIDs.insert(message.id) + DispatchQueue.global().async { + self?.service.uploadAttachment(message: message) { error, remotePath in + self?.uploadingMessageIDs.remove(message.id) + if let error { + promise(.success(.xmppAction(.xmppAttachmentUploadFailed(msgId: message.id, reason: error.localizedDescription)))) + } else { + promise(.success(.xmppAction(.xmppAttachmentUploadSuccess(msgId: message.id, attachmentRemotePath: remotePath)))) + } } } } diff --git a/ConversationsClassic/AppCore/XMPP/XMPPService.swift b/ConversationsClassic/AppCore/XMPP/XMPPService.swift index 68991fd..012f17f 100644 --- a/ConversationsClassic/AppCore/XMPP/XMPPService.swift +++ b/ConversationsClassic/AppCore/XMPP/XMPPService.swift @@ -143,6 +143,10 @@ final class XMPPService: ObservableObject { completion(XMPPError.bad_request("No such file"), "") return } + guard let chat = client.module(MessageModule.self).chatManager.chat(for: client.context, with: BareJID(to)) else { + completion(XMPPError.bad_request("No such chat"), "") + return + } let httpModule = client.module(HttpFileUploadModule.self) httpModule.findHttpUploadComponent { res in @@ -173,73 +177,28 @@ final class XMPPService: ObservableObject { if code == 200 { completion(XMPPError.bad_request("Invalid response code"), "") } else { - completion(nil, slot.getUri.absoluteString) + let mesg = chat.createMessage(text: slot.getUri.absoluteString, id: message.id) + mesg.oob = slot.getUri.absoluteString + chat.send(message: mesg) { res in + switch res { + case .success: + completion(nil, slot.getUri.absoluteString) + + case .failure: + completion(XMPPError.bad_request("File uploaded, but message sent failed"), slot.getUri.absoluteString) + } + } } }.resume() - case .failure: - completion(XMPPError.bad_request("Upload failed"), "") + case .failure(let error): + completion(error, "") } } - case .failure: - completion(XMPPError.bad_request("No such component"), "") + case .failure(let error): + completion(error, "") } } } } - -// open class HTTPFileUploadHelper { -// -// private static let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "HTTPFileUploadHelper") -// -// public static func upload(for context: Context, filename: String, inputStream: InputStream, filesize size: Int, mimeType: String, delegate: URLSessionDelegate?, completionHandler: @escaping (Result)->Void) { -// let httpUploadModule = context.module(.httpFileUpload); -// httpUploadModule.findHttpUploadComponent(completionHandler: { result in -// switch result { -// case .success(let components): -// guard let component = components.first(where: { $0.maxSize > size }) else { -// completionHandler(.failure(.fileTooBig)); -// return; -// } -// httpUploadModule.requestUploadSlot(componentJid: component.jid, filename: filename, size: size, contentType: mimeType, completionHandler: { result in -// switch result { -// case .success(let slot): -// var request = URLRequest(url: slot.putUri); -// slot.putHeaders.forEach({ (k,v) in -// request.addValue(v, forHTTPHeaderField: k); -// }); -// request.httpMethod = "PUT"; -// request.httpBodyStream = inputStream; -// request.addValue(String(size), forHTTPHeaderField: "Content-Length"); -// request.addValue(mimeType, forHTTPHeaderField: "Content-Type"); -// let session = URLSession(configuration: URLSessionConfiguration.default, delegate: delegate, delegateQueue: OperationQueue.main); -// session.dataTask(with: request) { (data, response, error) in -// let code = (response as? HTTPURLResponse)?.statusCode ?? 500; -// guard error == nil && (code == 200 || code == 201) else { -// logger.error("upload of file \(filename) failed, error: \(error as Any), response: \(response as Any)"); -// completionHandler(.failure(.httpError)); -// return; -// } -// if code == 200 { -// completionHandler(.failure(.invalidResponseCode(url: slot.getUri))); -// } else { -// completionHandler(.success(slot.getUri)); -// } -// }.resume(); -// case .failure(let error): -// logger.error("upload of file \(filename) failed, upload component returned error: \(error as Any)"); -// completionHandler(.failure(.unknownError)); -// } -// }); -// case .failure(let error): -// completionHandler(.failure(error.errorCondition == .item_not_found ? .notSupported : .unknownError)); -// } -// }) -// } -// -// public enum UploadResult { -// case success(url: URL, filesize: Int, mimeType: String?) -// case failure(ShareError) -// } -// }