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 {
case makeConversationActive(chat: Chat)
case messageForCurrentConversationReceived(Message)
}

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@ import Combine
final class 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 {
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<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:
return Empty().eraseToAnyPublisher()
}

View file

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

View file

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

View file

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

View file

@ -4,6 +4,9 @@ extension ConversationState {
case .makeConversationActive(let chat):
state.currentChat = chat
case .messageForCurrentConversationReceived(let message):
state.currentMessages.append(message)
default:
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
var body: some View {
ZStack {
// Background color
Color.Main.backgroundLight
.ignoresSafeArea()
// Content
VStack(spacing: 0) {
// Header
ConversationScreenHeader()
// Msg list
// if !state.messages.isEmpty {
// List {
// ForEach(state.messages) { message in
// ChatMessageView(message: message)
// }
// }
// } else {
// Text("No messages")
// Spacer()
// }
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 {
VStack {
if isIncoming() {
HStack {
Text(message.body ?? "--NO BODY?--")
// .padding(.all, 8)
// .background(.black)
// .clipShape(RoundedRectangle(cornerRadius: 8))
.foregroundColor(Color.Main.black)
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