diff --git a/ConversationsClassic/AppCore/Actions/XMPPActions.swift b/ConversationsClassic/AppCore/Actions/XMPPActions.swift index 3bc3670..bc3599e 100644 --- a/ConversationsClassic/AppCore/Actions/XMPPActions.swift +++ b/ConversationsClassic/AppCore/Actions/XMPPActions.swift @@ -1,4 +1,8 @@ enum XMPPAction: Codable { case clientConnectionChanged(jid: String, state: ConnectionStatus) case xmppMessageReceived(Message) + + case xmppMessageSent(Message) + case xmppMessageSendFailed(msgId: String) + case xmppMessageSendSuccess(msgId: String) } diff --git a/ConversationsClassic/AppCore/Database/Database+Migrations.swift b/ConversationsClassic/AppCore/Database/Database+Migrations.swift index 3b93a9a..5e0eb2a 100644 --- a/ConversationsClassic/AppCore/Database/Database+Migrations.swift +++ b/ConversationsClassic/AppCore/Database/Database+Migrations.swift @@ -57,6 +57,7 @@ extension Database { table.column("oobUrl", .text) table.column("date", .datetime).notNull() table.column("pending", .boolean).notNull() + table.column("sentError", .boolean).notNull() } } diff --git a/ConversationsClassic/AppCore/Middlewares/DatabaseMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/DatabaseMiddleware.swift index e5002ba..5ee476a 100644 --- a/ConversationsClassic/AppCore/Middlewares/DatabaseMiddleware.swift +++ b/ConversationsClassic/AppCore/Middlewares/DatabaseMiddleware.swift @@ -184,7 +184,83 @@ final class DatabaseMiddleware { .eraseToAnyPublisher() case .conversationAction(.sendMessage(let from, let to, let body)): - return Empty().eraseToAnyPublisher() + return Future { 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 { + let message = Message( + id: UUID().uuidString, + type: .chat, + contentType: .text, + from: from, + to: to, + body: body, + subject: nil, + thread: nil, + oobUrl: nil, + date: Date(), + pending: true, + sentError: false + ) + try database._db.write { db in + try message.insert(db) + } + promise(.success(.xmppAction(.xmppMessageSent(message)))) + } catch { + promise(.success(.databaseAction(.storeMessageFailed(reason: error.localizedDescription)))) + } + } + } + .eraseToAnyPublisher() + + case .xmppAction(.xmppMessageSendSuccess(let msgId)): + // mark message as pending false and sentError false + return Future { 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 Message + .filter(Column("id") == msgId) + .updateAll(db, Column("pending").set(to: false), Column("sentError").set(to: false)) + } + promise(.success(.empty)) + } catch { + promise(.success(.databaseAction(.storeMessageFailed(reason: error.localizedDescription))) + ) + } + } + } + .eraseToAnyPublisher() + + case .xmppAction(.xmppMessageSendFailed(let msgId)): + // mark message as pending false and sentError true + return Future { 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 Message + .filter(Column("id") == msgId) + .updateAll(db, Column("pending").set(to: false), Column("sentError").set(to: true)) + } + promise(.success(.empty)) + } catch { + promise(.success(.databaseAction(.storeMessageFailed(reason: error.localizedDescription))) + ) + } + } + } + .eraseToAnyPublisher() default: return Empty().eraseToAnyPublisher() @@ -206,9 +282,7 @@ private extension DatabaseMiddleware { .fetchAll ) .publisher(in: database._db, scheduling: .immediate) - .sink { res in - print("!!!---Messages received: \(res)") - // Handle completion + .sink { _ in } receiveValue: { messages in DispatchQueue.main.async { store.dispatch(.conversationAction(.messagesUpdated(messages: messages))) diff --git a/ConversationsClassic/AppCore/Middlewares/XMPPMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/XMPPMiddleware.swift index 18011b1..25e13c3 100644 --- a/ConversationsClassic/AppCore/Middlewares/XMPPMiddleware.swift +++ b/ConversationsClassic/AppCore/Middlewares/XMPPMiddleware.swift @@ -86,13 +86,19 @@ final class XMPPMiddleware { } .eraseToAnyPublisher() - // case .conversationAction(.sendMessage(let from, let to, let body)): - // return Future { [weak self] promise in - // // TODO: handle errors - // self?.service.sendMessage(from: from, to: to, body: body) - // promise(.success(.empty)) - // } - // .eraseToAnyPublisher() + case .xmppAction(.xmppMessageSent(let message)): + return Future { [weak self] promise in + DispatchQueue.global().async { + self?.service.sendMessage(message: message) { done in + if done { + promise(.success(.xmppAction(.xmppMessageSendSuccess(msgId: message.id)))) + } else { + promise(.success(.xmppAction(.xmppMessageSendFailed(msgId: message.id)))) + } + } + } + } + .eraseToAnyPublisher() default: return Empty().eraseToAnyPublisher() diff --git a/ConversationsClassic/AppCore/Models/Message.swift b/ConversationsClassic/AppCore/Models/Message.swift index 482fed4..8ca0992 100644 --- a/ConversationsClassic/AppCore/Models/Message.swift +++ b/ConversationsClassic/AppCore/Models/Message.swift @@ -31,6 +31,7 @@ struct Message: DBStorable, Equatable { let date: Date let pending: Bool + let sentError: Bool } extension Message { @@ -76,7 +77,8 @@ extension Message { thread: martinMessage.thread, oobUrl: martinMessage.oob, date: Date(), - pending: false + pending: false, + sentError: false ) return msg } diff --git a/ConversationsClassic/AppCore/XMPP/XMPPService.swift b/ConversationsClassic/AppCore/XMPP/XMPPService.swift index 4cfc3b6..c3c1904 100644 --- a/ConversationsClassic/AppCore/XMPP/XMPPService.swift +++ b/ConversationsClassic/AppCore/XMPP/XMPPService.swift @@ -104,16 +104,25 @@ final class XMPPService: ObservableObject { clients.first { $0.connectionConfiguration.userJid.stringValue == jid } } - // TODO: add handler - func sendMessage(from: String, to: String, body: String) { - guard let client = getClient(for: from) else { return } - let message = Martin.Message() - // message.from = client.connectionConfiguration.userJid - message.to = JID(to) - message.body = body - - client.writer.write(message) { res in - print(res) + func sendMessage(message: Message, completion: @escaping (Bool) -> Void) { + guard let client = getClient(for: message.from), let to = message.to else { + completion(false) + return } + let message = Martin.Message() + message.to = JID(to) + message.body = message.body + + client.module(MessageModule.self) + .chatManager + .chat(for: client.context, with: JID(to).bareJid)? + .send(message: message) { res in + switch res { + case .success: + completion(true) + case .failure: + completion(false) + } + } } } diff --git a/ConversationsClassic/View/Screens/ConversationScreen.swift b/ConversationsClassic/View/Screens/ConversationScreen.swift index c302d31..c7f13d7 100644 --- a/ConversationsClassic/View/Screens/ConversationScreen.swift +++ b/ConversationsClassic/View/Screens/ConversationScreen.swift @@ -129,21 +129,15 @@ private struct ConversationMessageRow: View { HStack(spacing: 0) { if isOutgoing() { Spacer() - VStack(spacing: 0) { - MessageTime(message: message) - Spacer() - } - .padding(.trailing, 4) + MessageAttr(message: message) + .padding(.trailing, 4) } MessageContainer(message: message, isOutgoing: isOutgoing()) .background(isOutgoing() ? Color.Material.greenDark100 : Color.Main.white) .clipShape(MessageBubble(isOutgoing: isOutgoing())) if !isOutgoing() { - VStack(spacing: 0) { - MessageTime(message: message) - Spacer() - } - .padding(.leading, 4) + MessageAttr(message: message) + .padding(.leading, 4) Spacer() } } @@ -184,13 +178,25 @@ struct MessageBubble: Shape { } } -struct MessageTime: View { +struct MessageAttr: View { let message: Message var body: some View { - Text(message.date, style: .time) - .font(.sub2) - .foregroundColor(Color.Main.gray) + VStack(alignment: .leading, spacing: 0) { + Text(message.date, style: .time) + .font(.sub2) + .foregroundColor(Color.Main.gray) + Spacer() + if message.sentError { + Image(systemName: "exclamationmark.circle") + .font(.body3) + .foregroundColor(Color.Tango.redLight) + } else if message.pending { + Image(systemName: "clock") + .font(.body3) + .foregroundColor(Color.Main.gray) + } + } } } @@ -226,13 +232,13 @@ struct MessageTime: View { thread: nil, oobUrl: nil, date: Date(), - pending: false + pending: true, sentError: false ), - Message(id: "2", type: .chat, contentType: .text, from: contact, to: acc, body: "this is for testsdfsdf sdfsdf sdfs sdf sdffsdf sdf sdf sdf sdf sdf sdff sdfffwwe ", subject: nil, thread: nil, oobUrl: nil, date: Date(), pending: false), - Message(id: "3", type: .chat, contentType: .text, from: contact, to: acc, body: "this is for test", subject: nil, thread: nil, oobUrl: nil, date: Date(), pending: false), - Message(id: "4", type: .chat, contentType: .text, from: acc, to: contact, body: "this is for test sdfkjwek jwkjfh jwerf jdfhskjdhf jsdhfjhwefh sjdhfh fsdjhfh sd ", subject: nil, thread: nil, oobUrl: nil, date: Date(), pending: false), - Message(id: "5", type: .chat, contentType: .text, from: contact, to: acc, body: "this is for test sdfjkkeke kekkddjw;; w;edkdjfj l kjwekrjfk wef", subject: nil, thread: nil, oobUrl: nil, date: Date(), pending: false), - Message(id: "6", type: .chat, contentType: .text, from: acc, to: contact, body: "this is for testsdf dsdkkekkddn wejkjfj ", subject: nil, thread: nil, oobUrl: nil, date: Date(), pending: false), + Message(id: "2", type: .chat, contentType: .text, from: contact, to: acc, body: "this is for testsdfsdf sdfsdf sdfs sdf sdffsdf sdf sdf sdf sdf sdf sdff sdfffwwe ", subject: nil, thread: nil, oobUrl: nil, date: Date(), pending: false, sentError: false), + Message(id: "3", type: .chat, contentType: .text, from: contact, to: acc, body: "this is for test", subject: nil, thread: nil, oobUrl: nil, date: Date(), pending: false, sentError: true), + Message(id: "4", type: .chat, contentType: .text, from: acc, to: contact, body: "this is for test sdfkjwek jwkjfh jwerf jdfhskjdhf jsdhfjhwefh sjdhfh fsdjhfh sd ", subject: nil, thread: nil, oobUrl: nil, date: Date(), pending: false, sentError: false), + Message(id: "5", type: .chat, contentType: .text, from: contact, to: acc, body: "this is for test sdfjkkeke kekkddjw;; w;edkdjfj l kjwekrjfk wef", subject: nil, thread: nil, oobUrl: nil, date: Date(), pending: false, sentError: false), + Message(id: "6", type: .chat, contentType: .text, from: acc, to: contact, body: "this is for testsdf dsdkkekkddn wejkjfj ", subject: nil, thread: nil, oobUrl: nil, date: Date(), pending: false, sentError: false), Message( id: "7", type: .chat, @@ -244,12 +250,12 @@ struct MessageTime: View { subject: nil, thread: nil, oobUrl: nil, - date: Date(), pending: false + date: Date(), pending: false, sentError: false ), - Message(id: "8", type: .chat, contentType: .text, from: acc, to: contact, body: "so test", subject: nil, thread: nil, oobUrl: nil, date: Date(), pending: false), - Message(id: "9", type: .chat, contentType: .text, from: contact, to: acc, body: "so test", subject: nil, thread: nil, oobUrl: nil, date: Date(), pending: false), - Message(id: "10", type: .chat, contentType: .text, from: acc, to: contact, body: "so test so test so test", subject: nil, thread: nil, oobUrl: nil, date: Date(), pending: false), - Message(id: "11", type: .chat, contentType: .text, from: contact, to: acc, body: "xD", subject: nil, thread: nil, oobUrl: nil, date: Date(), pending: false) + Message(id: "8", type: .chat, contentType: .text, from: acc, to: contact, body: "so test", subject: nil, thread: nil, oobUrl: nil, date: Date(), pending: false, sentError: false), + Message(id: "9", type: .chat, contentType: .text, from: contact, to: acc, body: "so test", subject: nil, thread: nil, oobUrl: nil, date: Date(), pending: false, sentError: false), + Message(id: "10", type: .chat, contentType: .text, from: acc, to: contact, body: "so test so test so test", subject: nil, thread: nil, oobUrl: nil, date: Date(), pending: false, sentError: false), + Message(id: "11", type: .chat, contentType: .text, from: contact, to: acc, body: "xD", subject: nil, thread: nil, oobUrl: nil, date: Date(), pending: false, sentError: false) ] return state }