diff --git a/Sources/Shared/Models/AppModel.swift b/Sources/Shared/Models/AppModel.swift index 2e03a8f..a083d39 100644 --- a/Sources/Shared/Models/AppModel.swift +++ b/Sources/Shared/Models/AppModel.swift @@ -96,6 +96,7 @@ final class AppModel: ObservableObject { private var lastSuccessfulMAMSync: Date? private var lastSuccessfulMAMCursor: String? private var mamCheckpoints: [MAMArchiveKey: MAMArchiveCheckpoint] = [:] + private var hasMoreOlderHistoryByConversation: [String: Bool] = [:] private var pendingRoomPasswords: [String: String] = [:] private var joiningRoomJIDs: Set = [] private var mediaSendActivity = MediaSendActivityTracker() @@ -510,22 +511,31 @@ final class AppModel: ObservableObject { // } // schedulePersist() // syncWatch() - hasMoreOlderHistory = true +// hasMoreOlderHistory = true + hasMoreOlderHistory = hasMoreOlderHistoryByConversation[normalized] ?? true } func loadOlderHistoryForSelectedConversation() { guard !isLoadingOlderHistory, hasMoreOlderHistory, let conversation = selectedConversation else { return } +// let oldestServerID = selectedMessages +// .sorted { lhs, rhs in +// if lhs.timestamp != rhs.timestamp { return lhs.timestamp < rhs.timestamp } +// return lhs.id < rhs.id +// } +// .compactMap(\.stanzaID) +// .first let oldestServerID = selectedMessages - .sorted { lhs, rhs in + .filter { $0.stanzaID != nil } + .min { lhs, rhs in if lhs.timestamp != rhs.timestamp { return lhs.timestamp < rhs.timestamp } return lhs.id < rhs.id - } - .compactMap(\.stanzaID) - .first + }? + .stanzaID isLoadingOlderHistory = true + let conversationID = conversation.id.lowercased() xmpp.loadOlderHistory( conversationJID: conversation.jid, isGroup: conversation.isGroup, @@ -535,7 +545,11 @@ final class AppModel: ObservableObject { self.isLoadingOlderHistory = false switch result { case .success(let hasMore): - self.hasMoreOlderHistory = hasMore +// self.hasMoreOlderHistory = hasMore + self.hasMoreOlderHistoryByConversation[conversationID] = hasMore + if self.selectedConversationID == conversationID { + self.hasMoreOlderHistory = hasMore + } case .failure(let error): self.errorMessage = error.localizedDescription } @@ -1926,6 +1940,9 @@ final class AppModel: ObservableObject { } private func consumeMessage(_ envelope: XMPPService.MessageEnvelope) { + if !envelope.isGroupMessage { + upsertConversation(jid: envelope.peerJID, name: nil) + } if let stanzaID = envelope.stanzaID, messageIndex(referenceID: stanzaID, conversationID: envelope.peerJID) != nil { diff --git a/Sources/Shared/UI/ChatView.swift b/Sources/Shared/UI/ChatView.swift index 722cd93..935c28a 100644 --- a/Sources/Shared/UI/ChatView.swift +++ b/Sources/Shared/UI/ChatView.swift @@ -45,6 +45,7 @@ struct ChatView: View { @State private var hasCompletedInitialScroll = false @State private var isNearTimelineBottom = true @State private var historyLoadAnchorID: String? + @State private var historyTopTriggerArmed = true @State private var activeCaptureMode: ComposerCaptureMode? @State private var preparingCaptureMode: ComposerCaptureMode? @State private var captureGestureIsActive = false @@ -239,6 +240,7 @@ struct ChatView: View { hasCompletedInitialScroll = false isNearTimelineBottom = true historyLoadAnchorID = nil + historyTopTriggerArmed = true } .onDisappear { model.endComposerActivity(in: liveConversation) @@ -332,10 +334,30 @@ struct ChatView: View { Color.clear .frame(height: 1) .accessibilityHidden(true) - .onAppear { - guard !model.isLoadingOlderHistory else { return } - historyLoadAnchorID = timelineEntries.first?.id - model.loadOlderHistoryForSelectedConversation() +// .onAppear { +//// guard !model.isLoadingOlderHistory else { return } +// guard historyTopTriggerArmed, +// !model.isLoadingOlderHistory +// else { +// return +// } +// +// historyTopTriggerArmed = false +// historyLoadAnchorID = timelineEntries.first?.id +// model.loadOlderHistoryForSelectedConversation() +// } + .background { + GeometryReader { geometry in + Color.clear.preference( + key: TimelineTopYPreferenceKey.self, + value: geometry.frame( + in: .named(Self.timelineCoordinateSpace) + ).minY + ) + } + } + .onDisappear { + historyTopTriggerArmed = true } if model.isLoadingOlderHistory { ProgressView() @@ -419,6 +441,16 @@ struct ChatView: View { ) } #endif + .onPreferenceChange(TimelineTopYPreferenceKey.self) { topY in + guard hasCompletedInitialScroll, + !timelineEntries.isEmpty, + model.hasMoreOlderHistory, + !model.isLoadingOlderHistory, + topY <= 24 + else { return } + historyLoadAnchorID = timelineEntries.first?.id + model.loadOlderHistoryForSelectedConversation() + } .task(id: timelineEntries.last?.id) { await performInitialScrollAfterLayout(using: proxy) } @@ -1576,6 +1608,14 @@ private struct TimelineBottomYPreferenceKey: PreferenceKey { } } +private struct TimelineTopYPreferenceKey: PreferenceKey { + static var defaultValue: CGFloat = .greatestFiniteMagnitude + + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value = nextValue() + } +} + #if os(iOS) /// Reads the real UIScrollView offset without replacing its delegate or /// installing another pan recognizer. SwiftUI geometry preferences can stop diff --git a/Sources/Shared/XMPP/XMPPService.swift b/Sources/Shared/XMPP/XMPPService.swift index 99696a3..d9ad528 100644 --- a/Sources/Shared/XMPP/XMPPService.swift +++ b/Sources/Shared/XMPP/XMPPService.swift @@ -296,6 +296,13 @@ final class XMPPService { private var mucCatchupLastCursor: String? private var mucCursorFallbackArchives: Set = [] private var delayedLiveByArchive: [MAMArchiveKey: [BufferedLiveDelivery]] = [:] + private struct PendingOlderHistoryRequest { + let conversationJID: String + let isGroup: Bool + let before: String? + let completion: (Result) -> Void + } + private var pendingOlderHistoryRequest: PendingOlderHistoryRequest? private let archiveStanzaInbox = ArchiveStanzaInbox( maximumCount: ArchiveMessageBatchPolicy.maximumBufferedStanzas ) @@ -825,12 +832,38 @@ final class XMPPService { completion(.failure(LumaXMPPError.notConnected)) return } - guard !archiveSyncStarted, activeMUCCatchup == nil else { - completion(.success(false)) + // guard !archiveSyncStarted, activeMUCCatchup == nil else { + // completion(.success(false)) + if archiveSyncStarted || activeMUCCatchup != nil { + pendingOlderHistoryRequest = PendingOlderHistoryRequest( + conversationJID: conversationJID, + isGroup: isGroup, + before: before, + completion: completion + ) + return + } + performOlderHistoryRequest( + conversationJID: conversationJID, + isGroup: isGroup, + before: before, + completion: completion + ) + } + + private func performOlderHistoryRequest( + conversationJID: String, + isGroup: Bool, + before: String?, + completion: @escaping (Result) -> Void + ) { + guard let client, client.state == .connected() else { + completion(.failure(LumaXMPPError.notConnected)) return } guard let version = client.module(.mam).availableVersions.first else { - completion(.success(false)) +// completion(.success(false)) + completion(.failure(LumaXMPPError.connection("MAM недоступен"))) return } @@ -893,7 +926,8 @@ final class XMPPService { message: stanza.message, timestamp: stanza.timestamp, archiveID: stanza.archiveID, - isArchived: true + isArchived: true, + archivedPeerJID: isGroup ? nil : BareJID(conversationJID.lowercased()) ) } } @@ -911,6 +945,20 @@ final class XMPPService { } } } + + private func startPendingOlderHistoryIfPossible() { + guard !archiveSyncStarted, + activeMUCCatchup == nil, + let pending = pendingOlderHistoryRequest + else { return } + pendingOlderHistoryRequest = nil + performOlderHistoryRequest( + conversationJID: pending.conversationJID, + isGroup: pending.isGroup, + before: pending.before, + completion: pending.completion + ) + } func sendRetraction( to recipient: String, @@ -1567,7 +1615,8 @@ final class XMPPService { message: Message, timestamp: Date, archiveID: String?, - isArchived: Bool = false + isArchived: Bool = false, + archivedPeerJID: BareJID? = nil ) { guard message.type != .error, message.type != .groupchat, @@ -1578,8 +1627,12 @@ final class XMPPService { let ownJID = client.userBareJid let outgoing = sender == ownJID // guard let peer = outgoing ? message.to?.bareJid : message.from?.bareJid else { return } +// let peer: BareJID? +// if outgoing { let peer: BareJID? - if outgoing { + if isArchived, let archivedPeerJID { + peer = archivedPeerJID + } else if outgoing { peer = message.to?.bareJid ?? message.from?.bareJid } else { peer = message.from?.bareJid @@ -2707,6 +2760,7 @@ final class XMPPService { mucCatchupLastCursor = nil replayDelayedLive(for: archive) startNextMUCCatchupIfPossible() + startPendingOlderHistoryIfPossible() } guard succeeded else { return } let checkpoint = MAMArchiveCheckpoint( @@ -3181,6 +3235,7 @@ final class XMPPService { } setArchiveSyncIndicator(false) archiveSyncStarted = false + startPendingOlderHistoryIfPossible() archiveIsBootstrapQuery = false archiveHighWatermark = nil archiveHasCompletedPage = false