This commit is contained in:
fmodf 2024-07-14 21:22:46 +02:00
parent 1780360fb4
commit 7666b71ef9
7 changed files with 106 additions and 71 deletions

View file

@ -6,8 +6,7 @@ enum XMPPAction: Codable {
case xmppMessageSendFailed(msgId: String) case xmppMessageSendFailed(msgId: String)
case xmppMessageSendSuccess(msgId: String) case xmppMessageSendSuccess(msgId: String)
case xmppAttachmentUpload(Message) case xmppAttachmentTryUpload(Message)
// case xmppAttachmentSlotRequestDone(String) //TODO: ???
case xmppAttachmentUploadFailed(msgId: String, reason: String) case xmppAttachmentUploadFailed(msgId: String, reason: String)
case xmppAttachmentUploadSuccess(msgId: String, attachmentRemotePath: String) case xmppAttachmentUploadSuccess(msgId: String, attachmentRemotePath: String)
} }

View file

@ -47,7 +47,7 @@ private extension Database {
// verbose and debugging in DEBUG builds only. // verbose and debugging in DEBUG builds only.
config.publicStatementArguments = true config.publicStatementArguments = true
config.prepareDatabase { db in config.prepareDatabase { db in
db.trace { print("SQL> \($0)") } db.trace { print("SQL> \($0)\n") }
} }
#endif #endif
return config return config

View file

@ -355,6 +355,7 @@ final class DatabaseMiddleware {
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
// MARK: Sharing
case .conversationAction(.sendMediaMessages(let from, let to, let messageIds, let localFilesNames)): case .conversationAction(.sendMediaMessages(let from, let to, let messageIds, let localFilesNames)):
return Future<AppAction, Never> { promise in return Future<AppAction, Never> { promise in
Task(priority: .background) { [weak self] in Task(priority: .background) { [weak self] in
@ -378,6 +379,7 @@ final class DatabaseMiddleware {
date: Date(), date: Date(),
pending: true, pending: true,
sentError: false, sentError: false,
attachmentType: localFilesNames[index].attachmentType,
attachmentLocalName: localFilesNames[index] attachmentLocalName: localFilesNames[index]
) )
try database._db.write { db in try database._db.write { db in
@ -393,6 +395,52 @@ final class DatabaseMiddleware {
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
case .xmppAction(.xmppAttachmentUploadSuccess(let messageId, let remotePath)):
return Future<AppAction, Never> { 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<AppAction, Never> { 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: default:
return Empty().eraseToAnyPublisher() return Empty().eraseToAnyPublisher()
} }

View file

@ -15,6 +15,8 @@ final class FileMiddleware {
promise(.success(.empty)) promise(.success(.empty))
return return
} }
// for incoming messages with attachments
for message in messages where message.attachmentRemotePath != nil && message.attachmentLocalPath == nil { for message in messages where message.attachmentRemotePath != nil && message.attachmentLocalPath == nil {
if wSelf.downloadingMessageIDs.contains(message.id) { if wSelf.downloadingMessageIDs.contains(message.id) {
continue continue
@ -25,6 +27,27 @@ final class FileMiddleware {
store.dispatch(.fileAction(.downloadAttachmentFile(messageId: message.id, attachmentRemotePath: message.attachmentRemotePath!))) 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)) promise(.success(.empty))
}.eraseToAnyPublisher() }.eraseToAnyPublisher()

View file

@ -7,7 +7,6 @@ import UIKit
final class SharingMiddleware { final class SharingMiddleware {
static let shared = SharingMiddleware() static let shared = SharingMiddleware()
// swiftlint:disable:next function_body_length
func middleware(state: AppState, action: AppAction) -> AnyPublisher<AppAction, Never> { func middleware(state: AppState, action: AppAction) -> AnyPublisher<AppAction, Never> {
switch action { switch action {
// MARK: - Camera and Gallery Access // MARK: - Camera and Gallery Access

View file

@ -6,6 +6,7 @@ final class XMPPMiddleware {
static let shared = XMPPMiddleware() static let shared = XMPPMiddleware()
private let service = XMPPService(manager: Database.shared) private let service = XMPPService(manager: Database.shared)
private var cancellables: Set<AnyCancellable> = [] private var cancellables: Set<AnyCancellable> = []
private var uploadingMessageIDs = ThreadSafeSet<String>()
private init() { private init() {
service.clientState.sink { client, state in service.clientState.sink { client, state in
@ -100,14 +101,20 @@ final class XMPPMiddleware {
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
case .xmppAction(.xmppAttachmentUpload(let message)): case .xmppAction(.xmppAttachmentTryUpload(let message)):
return Future<AppAction, Never> { [weak self] promise in return Future<AppAction, Never> { [weak self] promise in
DispatchQueue.global().async { if self?.uploadingMessageIDs.contains(message.id) ?? false {
self?.service.uploadAttachment(message: message) { done, remotePath in return promise(.success(.empty))
if done {
promise(.success(.xmppAction(.xmppAttachmentUploadSuccess(msgId: message.id, attachmentRemotePath: remotePath))))
} else { } else {
promise(.success(.xmppAction(.xmppAttachmentUploadFailed(msgId: message.id, reason: "Upload failed")))) 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))))
}
} }
} }
} }

View file

@ -143,6 +143,10 @@ final class XMPPService: ObservableObject {
completion(XMPPError.bad_request("No such file"), "") completion(XMPPError.bad_request("No such file"), "")
return 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) let httpModule = client.module(HttpFileUploadModule.self)
httpModule.findHttpUploadComponent { res in httpModule.findHttpUploadComponent { res in
@ -173,73 +177,28 @@ final class XMPPService: ObservableObject {
if code == 200 { if code == 200 {
completion(XMPPError.bad_request("Invalid response code"), "") completion(XMPPError.bad_request("Invalid response code"), "")
} else { } else {
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) completion(nil, slot.getUri.absoluteString)
case .failure:
completion(XMPPError.bad_request("File uploaded, but message sent failed"), slot.getUri.absoluteString)
}
}
} }
}.resume() }.resume()
case .failure: case .failure(let error):
completion(XMPPError.bad_request("Upload failed"), "") completion(error, "")
} }
} }
case .failure: case .failure(let error):
completion(XMPPError.bad_request("No such component"), "") 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<URL,ShareError>)->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)
// }
// }