diff --git a/ConversationsClassic/AppData/Client/Client+MartinMAM.swift b/ConversationsClassic/AppData/Client/Client+MartinMAM.swift index 412efc8..438f107 100644 --- a/ConversationsClassic/AppData/Client/Client+MartinMAM.swift +++ b/ConversationsClassic/AppData/Client/Client+MartinMAM.swift @@ -5,19 +5,70 @@ import Martin final class ClientMartinMAM { private var cancellables: Set = [] + private let messageProcessor: ArchivedMessageProcessor init(_ xmppConnection: XMPPClient) { + messageProcessor = ArchivedMessageProcessor() + // subscribe to archived messages xmppConnection.module(.mam).archivedMessagesPublisher .sink(receiveValue: { [weak self] archived in - let message = archived.message - message.attribute("archived_date", newValue: "\(archived.timestamp.timeIntervalSince1970)") - self?.handleMessage(archived) + guard let self = self else { return } + Task { + await self.messageProcessor.addMessage(archived) + } }) .store(in: &cancellables) } - private func handleMessage(_ received: Martin.MessageArchiveManagementModule.ArchivedMessageReceived) { + // func requestArchivedMessages(for roster: Roster, before: String? = nil, after: String? = nil, in module: MessageArchiveManagementModule) async { + // print(roster, before, after, module) + // + // // let endDate = Date() + // // let startDate = Calendar.current.date(byAdding: .day, value: -Const.mamRequestDaysLength, to: endDate) ?? Date() + // // let response = try? await module.queryItems( + // // componentJid: JID(credentials.bareJid), + // // with: JID(roster.bareJid), + // // start: startDate, + // // end: endDate, + // // queryId: UUID().uuidString + // // ) + // // let query: RSM.Query = .init(before: nil, after: nil, max: nil) + // } + + // func requestArchivedMessages(for roster: Roster, before: String? = nil, after: String? = nil) async { + // assert(before != nil || after != nil, "Either before or after must be provided") + // if !discoManager.features.map({ $0.xep }).contains("XEP-0313") { + // return + // } + // let module = connection.module(MessageArchiveManagementModule.self) + // await mamManager.requestArchivedMessages(for: roster, before: before, after: after, in: module) + // } +} + +private actor ArchivedMessageProcessor { + private var messageBuffer: [Martin.MessageArchiveManagementModule.ArchivedMessageReceived] = [] + private let batchSize = 20 + + func addMessage(_ message: Martin.MessageArchiveManagementModule.ArchivedMessageReceived) async { + messageBuffer.append(message) + if messageBuffer.count >= batchSize { + await processBatch() + } + } + + private func processBatch() async { + guard !messageBuffer.isEmpty else { return } + + let batch = messageBuffer.prefix(batchSize) + messageBuffer.removeFirst(min(batchSize, messageBuffer.count)) + + for archived in batch { + await handleMessage(archived) + } + } + + private func handleMessage(_ received: Martin.MessageArchiveManagementModule.ArchivedMessageReceived) async { let message = received.message let date = received.timestamp #if DEBUG @@ -27,16 +78,61 @@ final class ClientMartinMAM { print("---") #endif - if let msg = Message.map(message) { - Task { - do { - var msg = msg - msg.date = received.timestamp - try await msg.save() - } catch { - logIt(.error, "Error saving message: \(error)") + // Skip archived message if such message already exists in the database + if let archiveId = message.id { + try? await Database.shared.dbQueue.read { db in + if try Message.fetchOne(db, key: archiveId) != nil { + return } } } + + if let msg = Message.map(message) { + do { + var msg = msg + msg.date = received.timestamp + try await msg.save() + } catch { + logIt(.error, "Error saving message: \(error)") + } + } } } + +// final class ClientMartinMAM { +// private var cancellables: Set = [] +// +// init(_ xmppConnection: XMPPClient) { +// // subscribe to archived messages +// xmppConnection.module(.mam).archivedMessagesPublisher +// .sink(receiveValue: { [weak self] archived in +// let message = archived.message +// message.attribute("archived_date", newValue: "\(archived.timestamp.timeIntervalSince1970)") +// self?.handleMessage(archived) +// }) +// .store(in: &cancellables) +// } +// +// private func handleMessage(_ received: Martin.MessageArchiveManagementModule.ArchivedMessageReceived) { +// let message = received.message +// let date = received.timestamp +// #if DEBUG +// print("---") +// print("Archive message received: \(message)") +// print("Date: \(date)") +// print("---") +// #endif +// +// if let msg = Message.map(message) { +// Task { +// do { +// var msg = msg +// msg.date = received.timestamp +// try await msg.save() +// } catch { +// logIt(.error, "Error saving message: \(error)") +// } +// } +// } +// } +// } diff --git a/ConversationsClassic/AppData/Client/Client.swift b/ConversationsClassic/AppData/Client/Client.swift index f1cc07e..c6adf98 100644 --- a/ConversationsClassic/AppData/Client/Client.swift +++ b/ConversationsClassic/AppData/Client/Client.swift @@ -140,18 +140,6 @@ extension Client { throw URLError(.badServerResponse) } } - - func requestArchivedMessages(for roster: Roster) async { - print(roster) - - // if !discoManager.features.map({ $0.xep }).contains("XEP-0313") { - // return - // } - // let module = connection.module(MessageArchiveManagementModule.self) - // let endDate = Date() - // let startDate = Calendar.current.date(byAdding: .day, value: -Const.mamRequestDaysLength, to: endDate) ?? Date() - // let response = try? await module.queryItems(componentJid: JID(credentials.bareJid), with: JID(roster.bareJid), start: startDate, end: endDate, queryId: UUID().uuidString) - } } extension Client { diff --git a/ConversationsClassic/UIToolkit/View+Flip.swift b/ConversationsClassic/UIToolkit/View+Flip.swift new file mode 100644 index 0000000..e0d0df2 --- /dev/null +++ b/ConversationsClassic/UIToolkit/View+Flip.swift @@ -0,0 +1,15 @@ +import SwiftUI + +struct FlipView: ViewModifier { + func body(content: Content) -> some View { + content + .rotationEffect(.radians(Double.pi)) + .scaleEffect(x: -1, y: 1, anchor: .center) + } +} + +extension View { + func flip() -> some View { + modifier(FlipView()) + } +} diff --git a/ConversationsClassic/View/Main/Conversation/ConversationScreen.swift b/ConversationsClassic/View/Main/Conversation/ConversationScreen.swift index 29d4403..7a1671a 100644 --- a/ConversationsClassic/View/Main/Conversation/ConversationScreen.swift +++ b/ConversationsClassic/View/Main/Conversation/ConversationScreen.swift @@ -34,30 +34,29 @@ struct ConversationScreen: View { let messages = messages.messages if !messages.isEmpty { ScrollViewReader { proxy in - List { - ForEach(messages) { message in - ConversationMessageRow(message: message) - .id(message.id) - .onAppear { - if message.id == messages.first?.id { - firstIsVisible = true - autoScroll = true + ScrollView { + LazyVStack(spacing: 0) { + ForEach(messages) { message in + ConversationMessageRow(message: message) + .id(message.id) + .flip() + .onAppear { + if message.id == messages.first?.id { + firstIsVisible = true + autoScroll = true + } } - } - .onDisappear { - if message.id == messages.first?.id { - firstIsVisible = false - autoScroll = false + .onDisappear { + if message.id == messages.first?.id { + firstIsVisible = false + autoScroll = false + } } - } + } } - .rotationEffect(.degrees(180)) } - .rotationEffect(.degrees(180)) - .listStyle(.plain) - .background(Color.Material.Background.light) + .flip() .scrollDismissesKeyboard(.immediately) - .scrollIndicators(.hidden) .onChange(of: autoScroll) { new in if new, !firstIsVisible { withAnimation {