diff --git a/submodules/TelegramUI/TelegramUI/ChatController.swift b/submodules/TelegramUI/TelegramUI/ChatController.swift index f64d8fe31a..440387332c 100644 --- a/submodules/TelegramUI/TelegramUI/ChatController.swift +++ b/submodules/TelegramUI/TelegramUI/ChatController.swift @@ -187,7 +187,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private let startingBot = ValuePromise(false, ignoreRepeated: true) private let unblockingPeer = ValuePromise(false, ignoreRepeated: true) private let searching = ValuePromise(false, ignoreRepeated: true) - private let searchResult = Promise<(SearchMessagesResult, SearchMessagesState)?>() + private let searchResult = Promise<(SearchMessagesResult, SearchMessagesState, SearchMessagesLocation)?>() private let loadingMessage = ValuePromise(false, ignoreRepeated: true) private var preloadHistoryPeerId: PeerId? @@ -3227,9 +3227,33 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = (strongSelf.searchResult.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak self] searchResult in - if let strongSelf = self, let searchResult = searchResult?.0 { - let controller = ChatSearchResultsController(context: strongSelf.context, searchQuery: searchData.query, messages: searchResult.messages, navigateToMessageIndex: { index in + if let strongSelf = self, let (searchResult, searchState, searchLocation) = searchResult { + + let controller = ChatSearchResultsController(context: strongSelf.context, location: searchLocation, searchQuery: searchData.query, searchResult: searchResult, searchState: searchState, navigateToMessageIndex: { index in strongSelf.interfaceInteraction?.navigateMessageSearch(.index(index)) + }, resultsUpdated: { results, state in + guard let strongSelf = self else { + return + } + let updatedValue: (SearchMessagesResult, SearchMessagesState, SearchMessagesLocation)? = (results, state, searchLocation) + strongSelf.searchResult.set(.single(updatedValue)) + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in + if let data = current.search { + let messageIndices = results.messages.map({ $0.index }).sorted() + var currentIndex = messageIndices.last + if let previousResultId = data.resultsState?.currentId { + for index in messageIndices { + if index.id >= previousResultId { + currentIndex = index + break + } + } + } + return current.updatedSearch(data.withUpdatedResultsState(ChatSearchResultsState(messageIndices: messageIndices, currentId: currentIndex?.id, state: state, totalCount: results.totalCount, completed: results.completed))) + } else { + return current + } + }) }) strongSelf.chatDisplayNode.dismissInput() if case let .inline(navigationController) = strongSelf.presentationInterfaceState.mode { @@ -4958,15 +4982,37 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var items: [ActionSheetItem] = [] if self.presentationInterfaceState.isScheduledMessages { - items.append(ActionSheetButtonItem(title: self.presentationData.strings.ScheduledMessages_ClearAllConfirmation, color: .destructive, action: { [weak actionSheet] in + items.append(ActionSheetButtonItem(title: self.presentationData.strings.ScheduledMessages_ClearAllConfirmation, color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() - beginClear(.scheduledMessages) + + guard let strongSelf = self else { + return + } + + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [ + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { + }), + TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationAction, action: { + beginClear(.scheduledMessages) + }) + ], parseMarkdown: true), in: .window(.root)) })) } else if canRemoveGlobally { items.append(DeleteChatPeerActionSheetItem(context: self.context, peer: mainPeer, chatPeer: chatPeer, action: .clearHistory, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder)) - items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForEveryone(mainPeer.compactDisplayTitle).0, color: .destructive, action: { [weak actionSheet] in - beginClear(.forEveryone) + items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForEveryone(mainPeer.compactDisplayTitle).0, color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() + + guard let strongSelf = self else { + return + } + + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [ + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { + }), + TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationAction, action: { + beginClear(.forEveryone) + }) + ], parseMarkdown: true), in: .window(.root)) })) items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForCurrentUser, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() @@ -4974,9 +5020,20 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) } else { items.append(ActionSheetTextItem(title: text)) - items.append(ActionSheetButtonItem(title: self.presentationData.strings.Conversation_ClearAll, color: .destructive, action: { [weak actionSheet] in + items.append(ActionSheetButtonItem(title: self.presentationData.strings.Conversation_ClearAll, color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() - beginClear(.forLocalPeer) + + guard let strongSelf = self else { + return + } + + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [ + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { + }), + TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationAction, action: { + beginClear(.forLocalPeer) + }) + ], parseMarkdown: true), in: .window(.root)) })) } @@ -6094,7 +6151,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let search = searchMessages(account: self.context.account, location: searchState.location, query: searchState.query, state: nil, limit: limit) |> delay(0.2, queue: Queue.mainQueue()) - self.searchResult.set(search |> map(Optional.init)) + self.searchResult.set(search + |> map { (result, state) -> (SearchMessagesResult, SearchMessagesState, SearchMessagesLocation)? in + return (result, state, searchState.location) + }) searchDisposable.set((search |> deliverOnMainQueue).start(next: { [weak self] results, updatedState in diff --git a/submodules/TelegramUI/TelegramUI/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/TelegramUI/ChatSearchResultsContollerNode.swift index bf87953037..552d6f4d36 100644 --- a/submodules/TelegramUI/TelegramUI/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatSearchResultsContollerNode.swift @@ -105,7 +105,10 @@ private func chatListSearchContainerPreparedTransition(from fromEntries: [ChatLi class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { private let context: AccountContext private var presentationData: PresentationData - private let messages: [Message] + private let location: SearchMessagesLocation + private let searchQuery: String + private var searchResult: SearchMessagesResult + private var searchState: SearchMessagesState private var interaction: ChatListNodeInteraction? @@ -114,14 +117,23 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe private var enqueuedTransitions: [(ChatListSearchContainerTransition, Bool)] = [] private var validLayout: (ContainerViewLayout, CGFloat)? + var resultsUpdated: ((SearchMessagesResult, SearchMessagesState) -> Void)? var resultSelected: ((Int) -> Void)? private let presentationDataPromise: Promise private let disposable = MetaDisposable() - init(context: AccountContext, searchQuery: String, messages: [Message]) { + private var isLoadingMore = false + private let loadMoreDisposable = MetaDisposable() + + private let previousEntries = Atomic<[ChatListSearchEntry]?>(value: nil) + + init(context: AccountContext, location: SearchMessagesLocation, searchQuery: String, searchResult: SearchMessagesResult, searchState: SearchMessagesState) { self.context = context - self.messages = messages + self.location = location + self.searchQuery = searchQuery + self.searchResult = searchResult + self.searchState = searchState self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationDataPromise = Promise(ChatListPresentationData(theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: self.presentationData.disableAnimations)) @@ -138,7 +150,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe |> map { presentationData -> [ChatListSearchEntry] in var entries: [ChatListSearchEntry] = [] - for message in messages { + for message in searchResult.messages { var peer = RenderedPeer(message: message) if let group = message.peers[message.id.peerId] as? TelegramGroup, let migrationReference = group.migrationReference { if let channelPeer = message.peers[migrationReference.peerId] { @@ -156,8 +168,8 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe }, togglePeerSelected: { _ in }, messageSelected: { [weak self] peer, message, _ in if let strongSelf = self { - if let index = strongSelf.messages.firstIndex(where: { $0.index == message.index }) { - strongSelf.resultSelected?(strongSelf.messages.count - index - 1) + if let index = strongSelf.searchResult.messages.firstIndex(where: { $0.index == message.index }) { + strongSelf.resultSelected?(strongSelf.searchResult.messages.count - index - 1) } strongSelf.listNode.clearHighlightAnimated(true) } @@ -175,17 +187,86 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe interaction.searchTextHighightState = searchQuery self.interaction = interaction - let previousEntries = Atomic<[ChatListSearchEntry]?>(value: nil) self.disposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] entries in if let strongSelf = self { - let previousEntries = previousEntries.swap(entries) + let previousEntries = strongSelf.previousEntries.swap(entries) let firstTime = previousEntries == nil let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entries, context: context, interaction: interaction) strongSelf.enqueueTransition(transition, firstTime: firstTime) } })) + + self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in + guard let strongSelf = self else { + return + } + guard case let .known(value) = offset, value < 100.0 else { + return + } + if strongSelf.searchResult.completed { + return + } + if strongSelf.isLoadingMore { + return + } + strongSelf.loadMore() + } + } + + deinit { + self.disposable.dispose() + self.loadMoreDisposable.dispose() + } + + private func loadMore() { + self.isLoadingMore = true + + self.loadMoreDisposable.set((searchMessages(account: self.context.account, location: self.location, query: self.searchQuery, state: self.searchState) + |> deliverOnMainQueue).start(next: { [weak self] (updatedResult, updatedState) in + guard let strongSelf = self else { + return + } + guard let interaction = strongSelf.interaction else { + return + } + + strongSelf.isLoadingMore = false + strongSelf.searchResult = updatedResult + strongSelf.searchState = updatedState + strongSelf.resultsUpdated?(updatedResult, updatedState) + + let context = strongSelf.context + + let signal = strongSelf.presentationDataPromise.get() + |> map { presentationData -> [ChatListSearchEntry] in + var entries: [ChatListSearchEntry] = [] + + for message in updatedResult.messages { + var peer = RenderedPeer(message: message) + if let group = message.peers[message.id.peerId] as? TelegramGroup, let migrationReference = group.migrationReference { + if let channelPeer = message.peers[migrationReference.peerId] { + peer = RenderedPeer(peer: channelPeer) + } + } + entries.append(.message(message, peer, nil, presentationData)) + } + + return entries + } + + strongSelf.disposable.set((signal + |> deliverOnMainQueue).start(next: { entries in + if let strongSelf = self { + let previousEntries = strongSelf.previousEntries.swap(entries) + + let firstTime = previousEntries == nil + let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entries, context: context, interaction: interaction) + strongSelf.enqueueTransition(transition, firstTime: firstTime) + } + })) + })) } func updatePresentationData(_ presentationData: PresentationData) {