diff --git a/Sources/Shared/UI/ChatView.swift b/Sources/Shared/UI/ChatView.swift index 935c28a..9d2d532 100644 --- a/Sources/Shared/UI/ChatView.swift +++ b/Sources/Shared/UI/ChatView.swift @@ -442,12 +442,18 @@ struct ChatView: View { } #endif .onPreferenceChange(TimelineTopYPreferenceKey.self) { topY in + if topY > 24 { + historyTopTriggerArmed = true + return + } guard hasCompletedInitialScroll, !timelineEntries.isEmpty, model.hasMoreOlderHistory, !model.isLoadingOlderHistory, - topY <= 24 + historyTopTriggerArmed +// topY <= 24 else { return } + historyTopTriggerArmed = false historyLoadAnchorID = timelineEntries.first?.id model.loadOlderHistoryForSelectedConversation() } diff --git a/Sources/Shared/XMPP/XMPPService.swift b/Sources/Shared/XMPP/XMPPService.swift index d9ad528..96f3b68 100644 --- a/Sources/Shared/XMPP/XMPPService.swift +++ b/Sources/Shared/XMPP/XMPPService.swift @@ -303,6 +303,9 @@ final class XMPPService { let completion: (Result) -> Void } private var pendingOlderHistoryRequest: PendingOlderHistoryRequest? + private var olderHistoryQueryID: String? + private var olderHistoryTimeoutTask: Task? + private let olderHistoryQueryTimeoutNanoseconds: UInt64 = 15_000_000_000 private let archiveStanzaInbox = ArchiveStanzaInbox( maximumCount: ArchiveMessageBatchPolicy.maximumBufferedStanzas ) @@ -870,11 +873,22 @@ final class XMPPService { let archive = isGroup ? MAMArchiveKey.muc(conversationJID) : MAMArchiveKey.account(client.userBareJid.stringValue) + // Only one interactive backward-history query is allowed at a time. + // This is independent from the normal account/MUC catch-up query. + olderHistoryTimeoutTask?.cancel() + olderHistoryTimeoutTask = nil let queryID = UUID().uuidString + olderHistoryQueryID = queryID archiveStanzaInbox.begin( queryID: queryID, allowedSources: allowedMAMSources(for: archive, client: client) ) + scheduleOlderHistoryTimeout( + client: client, + queryID: queryID, + conversationJID: conversationJID, + completion: completion + ) // Martin 3.2.4's convenience overload accidentally clears `with`. // Construct MAMQueryForm explicitly so direct-chat history is truly @@ -897,9 +911,16 @@ final class XMPPService { ) { [weak self, weak client] result in DispatchQueue.main.async { guard let self, let client else { - completion(.success(false)) +// completion(.success(false)) + completion(.failure(LumaXMPPError.notConnected)) return } + guard self.olderHistoryQueryID == queryID else { + return + } + self.olderHistoryTimeoutTask?.cancel() + self.olderHistoryTimeoutTask = nil + self.olderHistoryQueryID = nil let page = self.archiveStanzaInbox.take(queryID: queryID) guard !page.overflowed, !page.rejectedSource else { completion(.failure(LumaXMPPError.connection("Некорректная MAM history page"))) @@ -940,12 +961,50 @@ final class XMPPService { case .success(let response): completion(.success(!response.complete)) case .failure(let error): + // Do not leave the UI in the loading state. A failed + // interactive query is recoverable by the next scroll. completion(.failure(error)) } } } } + private func scheduleOlderHistoryTimeout( + client: XMPPClient, + queryID: String, + conversationJID: String, + completion: @escaping (Result) -> Void + ) { + olderHistoryTimeoutTask?.cancel() + olderHistoryTimeoutTask = Task { @MainActor [weak self, weak client] in + try? await Task.sleep(nanoseconds: self?.olderHistoryQueryTimeoutNanoseconds ?? 15_000_000_000) + guard !Task.isCancelled, + let self, + let client, + self.client === client, + self.olderHistoryQueryID == queryID + else { + return + } + + self.olderHistoryQueryID = nil + self.olderHistoryTimeoutTask = nil + self.archiveStanzaInbox.cancel(queryID: queryID) + self.eventHandler?( + .recoverableError( + "Не удалось загрузить более старую историю чата \(conversationJID): MAM не ответил за 15 секунд." + ) + ) + completion( + .failure( + LumaXMPPError.connection( + "MAM не ответил вовремя. Повторите прокрутку вверх." + ) + ) + ) + } + } + private func startPendingOlderHistoryIfPossible() { guard !archiveSyncStarted, activeMUCCatchup == nil, @@ -3293,9 +3352,17 @@ final class XMPPService { private func suspendArchiveSyncForDisconnect() { setArchiveSyncIndicator(false) + olderHistoryTimeoutTask?.cancel() + olderHistoryTimeoutTask = nil + olderHistoryQueryID = nil + archiveStanzaInbox.cancel() + if let pending = pendingOlderHistoryRequest { + pendingOlderHistoryRequest = nil + pending.completion(.failure(LumaXMPPError.notConnected)) + } guard let client else { archiveSyncStarted = false - archiveStanzaInbox.cancel() +// archiveStanzaInbox.cancel() archiveActiveQueryID = nil archiveStanzaBuffer.removeAll(keepingCapacity: false) archiveBufferOverflowed = false @@ -3341,6 +3408,9 @@ final class XMPPService { archiveQueryCompletionTask?.cancel() archiveQueryCompletionTask = nil archiveStanzaInbox.cancel() + olderHistoryTimeoutTask?.cancel() + olderHistoryTimeoutTask = nil + olderHistoryQueryID = nil archiveResumeAfter = archiveSyncCheckpoint?.cursor archiveActiveQueryID = nil archiveSyncStarted = false