This commit is contained in:
fmodf 2024-06-24 15:28:26 +02:00
parent 8ce21712b7
commit 9b4323ccd3
11 changed files with 187 additions and 158 deletions

View file

@ -1,3 +1,4 @@
enum ConversationAction: Codable { enum ConversationAction: Codable {
case makeConversationActive(chat: Chat) case makeConversationActive(chat: Chat)
case messageForCurrentConversationReceived(Message)
} }

View file

@ -1,4 +1,3 @@
enum MessagesAction: Codable { enum MessagesAction: Codable {
case newMessageReceived(Message) case dumb
case messageDraftUpdate(Message)
} }

View file

@ -1,3 +1,4 @@
enum XMPPAction: Codable { enum XMPPAction: Codable {
case clientConnectionChanged(jid: String, state: ConnectionStatus) case clientConnectionChanged(jid: String, state: ConnectionStatus)
case xmppMessageReceived(Message)
} }

View file

@ -47,16 +47,14 @@ extension Database {
// messages // messages
try db.create(table: "messages", options: [.ifNotExists]) { table in try db.create(table: "messages", options: [.ifNotExists]) { table in
table.column("id", .text).notNull().primaryKey().unique(onConflict: .replace) 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("type", .text).notNull()
// table.column("isReaded", .boolean).notNull().defaults(to: false) table.column("contentType", .text).notNull()
// table.column("subject", .text) table.column("from", .text).notNull()
// table.column("threadId", .text) table.column("to", .text)
// table.column("errorType", .text) table.column("body", .text)
table.column("subject", .text)
table.column("thread", .text)
table.column("oobUrl", .text)
} }
} }

View file

@ -3,7 +3,7 @@ import Combine
final class ConversationMiddleware { final class ConversationMiddleware {
static let shared = ConversationMiddleware() static let shared = ConversationMiddleware()
func middleware(state _: AppState, action: AppAction) -> AnyPublisher<AppAction, Never> { func middleware(state: AppState, action: AppAction) -> AnyPublisher<AppAction, Never> {
switch action { switch action {
case .chatsAction(.chatStarted(let chat)): case .chatsAction(.chatStarted(let chat)):
return Just(AppAction.conversationAction(.makeConversationActive(chat: chat))).eraseToAnyPublisher() return Just(AppAction.conversationAction(.makeConversationActive(chat: chat))).eraseToAnyPublisher()
@ -11,6 +11,17 @@ final class ConversationMiddleware {
case .conversationAction(.makeConversationActive): case .conversationAction(.makeConversationActive):
return Just(AppAction.changeFlow(.conversation)).eraseToAnyPublisher() return Just(AppAction.changeFlow(.conversation)).eraseToAnyPublisher()
case .xmppAction(.xmppMessageReceived(let message)):
return Future<AppAction, Never> { 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: default:
return Empty().eraseToAnyPublisher() return Empty().eraseToAnyPublisher()
} }

View file

@ -150,6 +150,13 @@ final class DatabaseMiddleware {
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
case .xmppAction(.xmppMessageReceived(let message)):
if message.type != .chat {
return Empty().eraseToAnyPublisher()
}
// TODO: Store msg here!
return Empty().eraseToAnyPublisher()
default: default:
return Empty().eraseToAnyPublisher() return Empty().eraseToAnyPublisher()
} }

View file

@ -18,23 +18,11 @@ final class XMPPMiddleware {
} }
.store(in: &cancellables) .store(in: &cancellables)
service.clientMessages.sink { client, martinMessage in service.clientMessages.sink { _, martinMessage in
print("---") guard let message = Message.map(martinMessage) else { return }
print("Message received: \(martinMessage)") DispatchQueue.main.async {
print("In client: \(client)") store.dispatch(.xmppAction(.xmppMessageReceived(message)))
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)))
// }
// }
} }
.store(in: &cancellables) .store(in: &cancellables)
} }

View file

@ -4,95 +4,73 @@ import Martin
enum MessageType: String, Codable, DatabaseValueConvertible { enum MessageType: String, Codable, DatabaseValueConvertible {
case chat case chat
case channel
case groupchat case groupchat
case error
} }
enum MessageContentType: String, Codable, DatabaseValueConvertible { enum MessageContentType: String, Codable, DatabaseValueConvertible {
case text case text
case image
case video
case audio
case file
case location
case typing case typing
case invite case invite
} }
struct MessageContainer: Stateable, DatabaseValueConvertible { struct Message: Stateable, Identifiable, DatabaseValueConvertible {
let id: String let id: String
let type: MessageType 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 { extension Message {
var type: MessageContentType { get } // 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
// 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
// enum MessageType: String, Codable, DatabaseValueConvertible { let type = MessageType(rawValue: martinMessage.type?.rawValue ?? "") ?? .chat
// case text
// case image // Content type
// case video var contentType: MessageContentType = .text
// case audio if martinMessage.hints.contains(.noStore) {
// case file contentType = .typing
// case location }
// case writingProcessUpdate
// } // From/To
// let from = martinMessage.from?.bareJid.stringValue ?? ""
// struct Message: DBStorable, Equatable { let to = martinMessage.to?.bareJid.stringValue
// static let databaseTableName = "messages"
// // Msg
// let id: String let msg = Message(
// let chatId: String id: martinMessage.id ?? UUID().uuidString,
// let fromJid: String type: type,
// let toJid: String contentType: contentType,
// let timestamp: Date from: from,
// let body: String? to: to,
// let type: MessageType body: martinMessage.body,
// } subject: martinMessage.subject,
// thread: martinMessage.thread,
// // Special extnesion for mapping Martin.Message to Message oobUrl: martinMessage.oob
// extension Message { )
// static func mapMartinMessage(_ martinMessage: Martin.Message) -> Message? { return msg
// // 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
// // )
// }
// }

View file

@ -4,6 +4,9 @@ extension ConversationState {
case .makeConversationActive(let chat): case .makeConversationActive(let chat):
state.currentChat = chat state.currentChat = chat
case .messageForCurrentConversationReceived(let message):
state.currentMessages.append(message)
default: default:
break break
} }

View file

@ -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)
}
}
}

View file

@ -7,21 +7,30 @@ struct ConversationScreen: View {
@EnvironmentObject var store: AppStore @EnvironmentObject var store: AppStore
var body: some View { var body: some View {
ZStack {
// Background color
Color.Main.backgroundLight
.ignoresSafeArea()
// Content
VStack(spacing: 0) { VStack(spacing: 0) {
// Header // Header
ConversationScreenHeader() ConversationScreenHeader()
// Msg list // Msg list
// if !state.messages.isEmpty { let messages = store.state.conversationsState.currentMessages
// List { if !messages.isEmpty {
// ForEach(state.messages) { message in List {
// ChatMessageView(message: message) ForEach(messages) { message in
// } ConversationMessageRow(message: message)
// } }
// } else { }
// Text("No messages") .listStyle(.plain)
// Spacer() .background(Color.Main.backgroundLight)
// } } else {
Spacer()
}
}
} }
} }
} }
@ -63,50 +72,62 @@ private struct ConversationScreenHeader: View {
} }
} }
private struct ConversationMessageView: View { private struct ConversationMessageRow: View {
// @EnvironmentObject var state: AppState @EnvironmentObject var store: AppStore
let message: Message let message: Message
var body: some View { var body: some View {
VStack {
if isIncoming() {
HStack { HStack {
Text(message.body ?? "--NO BODY?--") MessageContainer(message: message, isOutgoing: false)
// .padding(.all, 8) .padding(.all, 8)
// .background(.black)
// .clipShape(RoundedRectangle(cornerRadius: 8))
.foregroundColor(Color.Main.black)
Spacer() 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() { // if isIncoming() {
// Image(systemName: "person.fill") // HStack
// .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())
// } // }
// // 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) .sharedListRow()
.background(Color.red)
} }
// private func isIncoming() -> Bool { private func isIncoming() -> Bool {
// message.fromJid != state.currentChat?.account message.from != store.state.conversationsState.currentChat?.account
// } }
} }
// for test