From 9b4323ccd33c43b1c23d5c100fe50642d703a7f7 Mon Sep 17 00:00:00 2001 From: fmodf Date: Mon, 24 Jun 2024 15:28:26 +0200 Subject: [PATCH] wip --- .../AppCore/Actions/ConversationActions.swift | 1 + .../AppCore/Actions/MessagesActions.swift | 3 +- .../AppCore/Actions/XMPPActions.swift | 1 + .../Database/Database+Migrations.swift | 16 +-- .../Middlewares/ConversationMiddleware.swift | 13 +- .../Middlewares/DatabaseMiddleware.swift | 7 + .../AppCore/Middlewares/XMPPMiddleware.swift | 22 +-- .../AppCore/Models/Message.swift | 134 ++++++++---------- .../Reducers/ConversationReducer.swift | 3 + .../View/Components/MessageContainer.swift | 22 +++ .../View/Screens/ConversationScreen.swift | 123 +++++++++------- 11 files changed, 187 insertions(+), 158 deletions(-) create mode 100644 ConversationsClassic/View/Components/MessageContainer.swift diff --git a/ConversationsClassic/AppCore/Actions/ConversationActions.swift b/ConversationsClassic/AppCore/Actions/ConversationActions.swift index a50e8b4..09efa13 100644 --- a/ConversationsClassic/AppCore/Actions/ConversationActions.swift +++ b/ConversationsClassic/AppCore/Actions/ConversationActions.swift @@ -1,3 +1,4 @@ enum ConversationAction: Codable { case makeConversationActive(chat: Chat) + case messageForCurrentConversationReceived(Message) } diff --git a/ConversationsClassic/AppCore/Actions/MessagesActions.swift b/ConversationsClassic/AppCore/Actions/MessagesActions.swift index 8fb67d2..880c563 100644 --- a/ConversationsClassic/AppCore/Actions/MessagesActions.swift +++ b/ConversationsClassic/AppCore/Actions/MessagesActions.swift @@ -1,4 +1,3 @@ enum MessagesAction: Codable { - case newMessageReceived(Message) - case messageDraftUpdate(Message) + case dumb } diff --git a/ConversationsClassic/AppCore/Actions/XMPPActions.swift b/ConversationsClassic/AppCore/Actions/XMPPActions.swift index 3c2a3a8..3bc3670 100644 --- a/ConversationsClassic/AppCore/Actions/XMPPActions.swift +++ b/ConversationsClassic/AppCore/Actions/XMPPActions.swift @@ -1,3 +1,4 @@ enum XMPPAction: Codable { case clientConnectionChanged(jid: String, state: ConnectionStatus) + case xmppMessageReceived(Message) } diff --git a/ConversationsClassic/AppCore/Database/Database+Migrations.swift b/ConversationsClassic/AppCore/Database/Database+Migrations.swift index db3f446..902f652 100644 --- a/ConversationsClassic/AppCore/Database/Database+Migrations.swift +++ b/ConversationsClassic/AppCore/Database/Database+Migrations.swift @@ -47,16 +47,14 @@ extension Database { // messages try db.create(table: "messages", options: [.ifNotExists]) { table in table.column("id", .text).notNull().primaryKey().unique(onConflict: .replace) - table.column("chatId", .text).notNull().references("chats", onDelete: .cascade) - table.column("fromJid", .text).notNull() - table.column("toJid", .text).notNull() - table.column("timestamp", .datetime).notNull() - table.column("body", .text) table.column("type", .text).notNull() - // table.column("isReaded", .boolean).notNull().defaults(to: false) - // table.column("subject", .text) - // table.column("threadId", .text) - // table.column("errorType", .text) + table.column("contentType", .text).notNull() + table.column("from", .text).notNull() + table.column("to", .text) + table.column("body", .text) + table.column("subject", .text) + table.column("thread", .text) + table.column("oobUrl", .text) } } diff --git a/ConversationsClassic/AppCore/Middlewares/ConversationMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/ConversationMiddleware.swift index ddcd4bf..2bc44e9 100644 --- a/ConversationsClassic/AppCore/Middlewares/ConversationMiddleware.swift +++ b/ConversationsClassic/AppCore/Middlewares/ConversationMiddleware.swift @@ -3,7 +3,7 @@ import Combine final class ConversationMiddleware { static let shared = ConversationMiddleware() - func middleware(state _: AppState, action: AppAction) -> AnyPublisher { + func middleware(state: AppState, action: AppAction) -> AnyPublisher { switch action { case .chatsAction(.chatStarted(let chat)): return Just(AppAction.conversationAction(.makeConversationActive(chat: chat))).eraseToAnyPublisher() @@ -11,6 +11,17 @@ final class ConversationMiddleware { case .conversationAction(.makeConversationActive): return Just(AppAction.changeFlow(.conversation)).eraseToAnyPublisher() + case .xmppAction(.xmppMessageReceived(let message)): + return Future { promise in + let currentChat = state.conversationsState.currentChat + if message.from == currentChat?.participant, message.to == currentChat?.account { + promise(.success(.conversationAction(.messageForCurrentConversationReceived(message)))) + } else { + promise(.success(.empty)) + } + } + .eraseToAnyPublisher() + default: return Empty().eraseToAnyPublisher() } diff --git a/ConversationsClassic/AppCore/Middlewares/DatabaseMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/DatabaseMiddleware.swift index 362236a..959ab19 100644 --- a/ConversationsClassic/AppCore/Middlewares/DatabaseMiddleware.swift +++ b/ConversationsClassic/AppCore/Middlewares/DatabaseMiddleware.swift @@ -150,6 +150,13 @@ final class DatabaseMiddleware { } .eraseToAnyPublisher() + case .xmppAction(.xmppMessageReceived(let message)): + if message.type != .chat { + return Empty().eraseToAnyPublisher() + } + // TODO: Store msg here! + return Empty().eraseToAnyPublisher() + default: return Empty().eraseToAnyPublisher() } diff --git a/ConversationsClassic/AppCore/Middlewares/XMPPMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/XMPPMiddleware.swift index 7c2e5b2..fa61a10 100644 --- a/ConversationsClassic/AppCore/Middlewares/XMPPMiddleware.swift +++ b/ConversationsClassic/AppCore/Middlewares/XMPPMiddleware.swift @@ -18,23 +18,11 @@ final class XMPPMiddleware { } .store(in: &cancellables) - service.clientMessages.sink { client, martinMessage in - print("---") - print("Message received: \(martinMessage)") - print("In client: \(client)") - print("---") - // guard let message = Message.mapMartinMessage(martinMessage) else { - // return - // } - // if message.type == .writingProcessUpdate { - // DispatchQueue.main.async { - // store.dispatch(.messagesAction(.messageDraftUpdate(message))) - // } - // } else { - // DispatchQueue.main.async { - // store.dispatch(.messagesAction(.newMessageReceived(message))) - // } - // } + service.clientMessages.sink { _, martinMessage in + guard let message = Message.map(martinMessage) else { return } + DispatchQueue.main.async { + store.dispatch(.xmppAction(.xmppMessageReceived(message))) + } } .store(in: &cancellables) } diff --git a/ConversationsClassic/AppCore/Models/Message.swift b/ConversationsClassic/AppCore/Models/Message.swift index 4077c00..4065e83 100644 --- a/ConversationsClassic/AppCore/Models/Message.swift +++ b/ConversationsClassic/AppCore/Models/Message.swift @@ -4,95 +4,73 @@ import Martin enum MessageType: String, Codable, DatabaseValueConvertible { case chat - case channel case groupchat + case error } enum MessageContentType: String, Codable, DatabaseValueConvertible { case text - case image - case video - case audio - case file - case location case typing case invite } -struct MessageContainer: Stateable, DatabaseValueConvertible { +struct Message: Stateable, Identifiable, DatabaseValueConvertible { let id: String let type: MessageType - let content: any MessageContent + let contentType: MessageContentType + + let from: String + let to: String? + + let body: String? + let subject: String? + let thread: String? + let oobUrl: String? } -protocol MessageContent: Stateable, DatabaseValueConvertible { - var type: MessageContentType { get } -} +extension Message { + // Universal mapping from Martin's Message to App Message + static func map(_ martinMessage: Martin.Message) -> Message? { + #if DEBUG + print("---") + print("Message received: \(martinMessage)") + print("---") + #endif -// -// enum MessageType: String, Codable, DatabaseValueConvertible { -// case text -// case image -// case video -// case audio -// case file -// case location -// case writingProcessUpdate -// } -// -// struct Message: DBStorable, Equatable { -// static let databaseTableName = "messages" -// -// let id: String -// let chatId: String -// let fromJid: String -// let toJid: String -// let timestamp: Date -// let body: String? -// let type: MessageType -// } -// -// // Special extnesion for mapping Martin.Message to Message -// extension Message { -// static func mapMartinMessage(_ martinMessage: Martin.Message) -> Message? { -// // for draft messages -// if martinMessage.hints.contains(.noStore) { -// return Message( -// id: martinMessage.id ?? UUID().uuidString, -// chatId: "none", // chat id will be filled later -// fromJid: martinMessage.from?.bareJid.stringValue ?? "", -// toJid: martinMessage.to?.bareJid.stringValue ?? "", -// timestamp: Date(), -// body: nil, -// type: .writingProcessUpdate -// ) -// } -// -// // if regular message contains no body - return nil -// guard let body = martinMessage.body else { -// return nil -// } -// -// print("Message received: \(martinMessage)") -// print("From: \(martinMessage.from)") -// print("To: \(martinMessage.to)") -// print("Body: \(martinMessage.body)") -// print("Type: \(martinMessage.type)") -// print("Id: \(martinMessage.id)") -// print("Subject: \(martinMessage.subject)") -// print("----") -// print("!!!!!-----Message body: \(body)") -// -// // parse regular message -// return nil -// // Message( -// // id: message.id, -// // chatId: message.chatId, -// // fromJid: message.from, -// // toJid: message.to, -// // timestamp: message.timestamp, -// // body: message.body, -// // type: MessageType(rawValue: message.type) ?? .text -// // ) -// } -// } + // Check that the message type is supported + let chatTypes: [StanzaType] = [.chat, .groupchat] + guard let mType = martinMessage.type, chatTypes.contains(mType) else { + #if DEBUG + print("Unsupported message type: \(martinMessage.type?.rawValue ?? "nil")") + #endif + return nil + } + + // Type + let type = MessageType(rawValue: martinMessage.type?.rawValue ?? "") ?? .chat + + // Content type + var contentType: MessageContentType = .text + if martinMessage.hints.contains(.noStore) { + contentType = .typing + } + + // From/To + let from = martinMessage.from?.bareJid.stringValue ?? "" + let to = martinMessage.to?.bareJid.stringValue + + // Msg + let msg = Message( + id: martinMessage.id ?? UUID().uuidString, + type: type, + contentType: contentType, + from: from, + to: to, + body: martinMessage.body, + subject: martinMessage.subject, + thread: martinMessage.thread, + oobUrl: martinMessage.oob + ) + return msg + } +} diff --git a/ConversationsClassic/AppCore/Reducers/ConversationReducer.swift b/ConversationsClassic/AppCore/Reducers/ConversationReducer.swift index 4f1c5e5..4f167df 100644 --- a/ConversationsClassic/AppCore/Reducers/ConversationReducer.swift +++ b/ConversationsClassic/AppCore/Reducers/ConversationReducer.swift @@ -4,6 +4,9 @@ extension ConversationState { case .makeConversationActive(let chat): state.currentChat = chat + case .messageForCurrentConversationReceived(let message): + state.currentMessages.append(message) + default: break } diff --git a/ConversationsClassic/View/Components/MessageContainer.swift b/ConversationsClassic/View/Components/MessageContainer.swift new file mode 100644 index 0000000..8127904 --- /dev/null +++ b/ConversationsClassic/View/Components/MessageContainer.swift @@ -0,0 +1,22 @@ +import Foundation +import SwiftUI + +struct MessageContainer: View { + let message: Message + let isOutgoing: Bool + + var body: some View { + ZStack { + // bg + Color.Main.backgroundDark + .ignoresSafeArea() + + // TODO: make custom body for different message types + // body + Text(message.body ?? "...") + .multilineTextAlignment(.leading) + .foregroundColor(Color.Main.black) + .background(isOutgoing ? Color.Material.greenDark200 : Color.Main.white) + } + } +} diff --git a/ConversationsClassic/View/Screens/ConversationScreen.swift b/ConversationsClassic/View/Screens/ConversationScreen.swift index 9833e1e..a0b4062 100644 --- a/ConversationsClassic/View/Screens/ConversationScreen.swift +++ b/ConversationsClassic/View/Screens/ConversationScreen.swift @@ -7,21 +7,30 @@ struct ConversationScreen: View { @EnvironmentObject var store: AppStore var body: some View { - VStack(spacing: 0) { - // Header - ConversationScreenHeader() + ZStack { + // Background color + Color.Main.backgroundLight + .ignoresSafeArea() - // Msg list - // if !state.messages.isEmpty { - // List { - // ForEach(state.messages) { message in - // ChatMessageView(message: message) - // } - // } - // } else { - // Text("No messages") - // Spacer() - // } + // Content + VStack(spacing: 0) { + // Header + ConversationScreenHeader() + + // Msg list + let messages = store.state.conversationsState.currentMessages + if !messages.isEmpty { + List { + ForEach(messages) { message in + ConversationMessageRow(message: message) + } + } + .listStyle(.plain) + .background(Color.Main.backgroundLight) + } else { + Spacer() + } + } } } } @@ -63,50 +72,62 @@ private struct ConversationScreenHeader: View { } } -private struct ConversationMessageView: View { - // @EnvironmentObject var state: AppState +private struct ConversationMessageRow: View { + @EnvironmentObject var store: AppStore let message: Message var body: some View { - HStack { - Text(message.body ?? "--NO BODY?--") - // .padding(.all, 8) - // .background(.black) - // .clipShape(RoundedRectangle(cornerRadius: 8)) - .foregroundColor(Color.Main.black) - Spacer() + VStack { + if isIncoming() { + HStack { + MessageContainer(message: message, isOutgoing: false) + .padding(.all, 8) + Spacer() + .frame(minWidth: 48, maxWidth: .infinity, alignment: .leading) + } + } else { + HStack { + Spacer() + .frame(minWidth: 48, maxWidth: .infinity, alignment: .leading) + MessageContainer(message: message, isOutgoing: true) + .padding(.all, 8) + } + } + // HStack { // if isIncoming() { - // Image(systemName: "person.fill") - // .foregroundColor(Color.Main.black) - // .frame(width: 32, height: 32) - // .background(Color.Main.backgroundLight) - // .clipShape(Circle()) - // Text(message.body ?? "--NO BODY?--") - // .padding(.all, 8) - // .background(Color.Main.backgroundLight) - // .clipShape(RoundedRectangle(cornerRadius: 8)) - // .foregroundColor(Color.Main.black) - // } else { - // Text(message.body ?? "--NO BODY?--") - // .padding(.all, 8) - // .background(Color.Main.backgroundLight) - // .clipShape(RoundedRectangle(cornerRadius: 8)) - // .foregroundColor(Color.Main.black) - // Image(systemName: "person.fill") - // .foregroundColor(Color.Main.black) - // .frame(width: 32, height: 32) - // .background(Color.Main.backgroundLight) - // .clipShape(Circle()) + // HStack // } + // // if isIncoming() { + // // Image(systemName: "person.fill") + // // .foregroundColor(Color.Main.black) + // // .frame(width: 32, height: 32) + // // .background(Color.Main.backgroundLight) + // // .clipShape(Circle()) + // // Text(message.body ?? "...") + // // .padding(.all, 8) + // // .background(Color.Main.backgroundLight) + // // .clipShape(RoundedRectangle(cornerRadius: 8)) + // // .foregroundColor(Color.Main.black) + // // } else { + // // Text(message.body ?? "--NO BODY?--") + // // .padding(.all, 8) + // // .background(Color.Main.backgroundLight) + // // .clipShape(RoundedRectangle(cornerRadius: 8)) + // // .foregroundColor(Color.Main.black) + // // Image(systemName: "person.fill") + // // .foregroundColor(Color.Main.black) + // // .frame(width: 32, height: 32) + // // .background(Color.Main.backgroundLight) + // // .clipShape(Circle()) + // // } + // } + // .padding(.horizontal, 16) } - .padding(.horizontal, 16) - .background(Color.red) + .sharedListRow() } - // private func isIncoming() -> Bool { - // message.fromJid != state.currentChat?.account - // } + private func isIncoming() -> Bool { + message.from != store.state.conversationsState.currentChat?.account + } } - -// for test