diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift index 9a4153f109..473fe28dcb 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift @@ -23,6 +23,7 @@ import PlainButtonComponent import MultilineTextComponent import AttachmentUI import SearchBarNode +import BalancedTextComponent final class QuickReplySetupScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -474,6 +475,7 @@ final class QuickReplySetupScreenComponent: Component { final class View: UIView { private var emptyState: ComponentView? private var contentListNode: ContentListNode? + private var emptySearchState: ComponentView? private let navigationBarView = ComponentView() private var navigationHeight: CGFloat? @@ -1171,6 +1173,41 @@ final class QuickReplySetupScreenComponent: Component { } contentListNode.setEntries(entries: entries, animated: !transition.animation.isImmediate) + if !self.searchQuery.isEmpty && entries.isEmpty { + var emptySearchStateTransition = transition + let emptySearchState: ComponentView + if let current = self.emptySearchState { + emptySearchState = current + } else { + emptySearchStateTransition = emptySearchStateTransition.withAnimation(.none) + emptySearchState = ComponentView() + self.emptySearchState = emptySearchState + } + let emptySearchStateSize = emptySearchState.update( + transition: .immediate, + component: AnyComponent(BalancedTextComponent( + text: .plain(NSAttributedString(string: environment.strings.Conversation_SearchNoResults, font: Font.regular(17.0), textColor: environment.theme.list.freeTextColor, paragraphAlignment: .center)), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: availableSize.height) + ) + var emptySearchStateBottomInset = listBottomInset + emptySearchStateBottomInset = max(emptySearchStateBottomInset, environment.inputHeight) + let emptySearchStateFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - emptySearchStateSize.width) * 0.5), y: navigationHeight + floor((availableSize.height - emptySearchStateBottomInset - navigationHeight) * 0.5)), size: emptySearchStateSize) + if let emptySearchStateView = emptySearchState.view { + if emptySearchStateView.superview == nil { + self.addSubview(emptySearchStateView) + } + emptySearchStateTransition.containedViewLayoutTransition.updatePosition(layer: emptySearchStateView.layer, position: emptySearchStateFrame.center) + emptySearchStateView.bounds = CGRect(origin: CGPoint(), size: emptySearchStateFrame.size) + } + } else if let emptySearchState = self.emptySearchState { + self.emptySearchState = nil + emptySearchState.view?.removeFromSuperview() + } + if let shortcutMessageList = self.shortcutMessageList, !shortcutMessageList.items.isEmpty { contentListNode.isHidden = false } else { diff --git a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessHoursSetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessHoursSetupScreen.swift index 057abbd998..7166538490 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessHoursSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessHoursSetupScreen.swift @@ -27,7 +27,9 @@ private func wrappedMinuteRange(range: Range, dayIndexOffset: Int = 0) -> I var result = IndexSet() if mappedRange.upperBound > 7 * 24 * 60 { - result.insert(integersIn: mappedRange.lowerBound ..< 7 * 24 * 60) + if mappedRange.lowerBound < 7 * 24 * 60 { + result.insert(integersIn: mappedRange.lowerBound ..< 7 * 24 * 60) + } result.insert(integersIn: 0 ..< (mappedRange.upperBound - 7 * 24 * 60)) } else { result.insert(integersIn: mappedRange) diff --git a/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/BUILD b/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/BUILD index 383288eeab..38fdb23587 100644 --- a/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/BUILD @@ -23,6 +23,8 @@ swift_library( "//submodules/PresentationDataUtils", "//submodules/SearchBarNode", "//submodules/TelegramUIPreferences", + "//submodules/ComponentFlow", + "//submodules/Components/BalancedTextComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/Sources/TimezoneSelectionScreenNode.swift b/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/Sources/TimezoneSelectionScreenNode.swift index b6630b46c2..e20cc58d88 100644 --- a/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/Sources/TimezoneSelectionScreenNode.swift +++ b/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/Sources/TimezoneSelectionScreenNode.swift @@ -13,6 +13,8 @@ import AccountContext import SearchBarNode import SearchUI import TelegramUIPreferences +import ComponentFlow +import BalancedTextComponent private struct TimezoneListEntry: Comparable, Identifiable { var id: String @@ -60,6 +62,7 @@ private struct TimezoneListSearchContainerTransition { let insertions: [ListViewInsertItem] let updates: [ListViewUpdateItem] let isSearching: Bool + let isEmptyResult: Bool } private func preparedLanguageListSearchContainerTransition(presentationData: PresentationData, from fromEntries: [TimezoneListEntry], to toEntries: [TimezoneListEntry], action: @escaping (String) -> Void, isSearching: Bool, forceUpdate: Bool) -> TimezoneListSearchContainerTransition { @@ -69,7 +72,7 @@ private func preparedLanguageListSearchContainerTransition(presentationData: Pre let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: true, action: action), directionHint: nil) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: true, action: action), directionHint: nil) } - return TimezoneListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching) + return TimezoneListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching, isEmptyResult: isSearching && toEntries.isEmpty) } private final class TimezoneListSearchContainerNode: SearchDisplayControllerContentNode { @@ -77,6 +80,8 @@ private final class TimezoneListSearchContainerNode: SearchDisplayControllerCont private let dimNode: ASDisplayNode private let listNode: ListView + private var notFoundText: ComponentView? + private var enqueuedTransitions: [TimezoneListSearchContainerTransition] = [] private var hasValidLayout = false @@ -88,6 +93,9 @@ private final class TimezoneListSearchContainerNode: SearchDisplayControllerCont private let presentationDataPromise: Promise + private var isEmptyResult: Bool = false + private var currentLayout: (layout: ContainerViewLayout, navigationBarHeight: CGFloat)? + public override var hasDim: Bool { return true } @@ -220,14 +228,26 @@ private final class TimezoneListSearchContainerNode: SearchDisplayControllerCont options.insert(.PreferSynchronousDrawing) let isSearching = transition.isSearching + let isEmptyResult = transition.isEmptyResult + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in - self?.listNode.isHidden = !isSearching - self?.dimNode.isHidden = isSearching + guard let self else { + return + } + self.listNode.isHidden = !isSearching + self.dimNode.isHidden = isSearching + self.isEmptyResult = isEmptyResult + + if let currentLayout = self.currentLayout { + self.containerLayoutUpdated(currentLayout.layout, navigationBarHeight: currentLayout.navigationBarHeight, transition: .immediate) + } }) } } override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.currentLayout = (layout, navigationBarHeight) + super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) let topInset = navigationBarHeight @@ -244,6 +264,36 @@ private final class TimezoneListSearchContainerNode: SearchDisplayControllerCont self.dequeueTransitions() } } + + if self.isEmptyResult { + let notFoundText: ComponentView + if let current = self.notFoundText { + notFoundText = current + } else { + notFoundText = ComponentView() + self.notFoundText = notFoundText + } + let notFoundTextSize = notFoundText.update( + transition: .immediate, + component: AnyComponent(BalancedTextComponent( + text: .plain(NSAttributedString(string: self.presentationData.strings.Conversation_SearchNoResults, font: Font.regular(17.0), textColor: self.presentationData.theme.list.freeTextColor, paragraphAlignment: .center)), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: layout.size.width - 16.0 * 2.0, height: layout.size.height) + ) + let notFoundTextFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - notFoundTextSize.width) * 0.5), y: navigationBarHeight + floor((layout.size.height - navigationBarHeight - notFoundTextSize.height) * 0.5)), size: notFoundTextSize) + if let notFoundTextView = notFoundText.view { + if notFoundTextView.superview == nil { + self.view.addSubview(notFoundTextView) + } + notFoundTextView.frame = notFoundTextFrame + } + } else if let notFoundText = self.notFoundText { + self.notFoundText = nil + notFoundText.view?.removeFromSuperview() + } } @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift index 9ac99003bb..b09109858c 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift @@ -28,10 +28,6 @@ private func inputQueryResultPriority(_ result: ChatPresentationInputQueryResult } func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: ChatInputContextPanelNode?, controllerInteraction: ChatControllerInteraction, interfaceInteraction: ChatPanelInterfaceInteraction?, chatPresentationContext: ChatPresentationContext) -> ChatInputContextPanelNode? { - guard let _ = chatPresentationInterfaceState.renderedPeer?.peer else { - return nil - } - if chatPresentationInterfaceState.showCommands, let renderedPeer = chatPresentationInterfaceState.renderedPeer { if let currentPanel = currentPanel as? CommandMenuChatInputContextPanelNode { return currentPanel diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift index 88c8f78e59..438efeff40 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift @@ -15,9 +15,6 @@ import ChatPresentationInterfaceState import ChatContextQuery func contextQueryResultStateForChatInterfacePresentationState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentQueryStates: inout [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)], requestBotLocationStatus: @escaping (PeerId) -> Void) -> [ChatPresentationInputQueryKind: ChatContextQueryUpdate] { - guard let peer = chatPresentationInterfaceState.renderedPeer?.peer else { - return [:] - } let inputQueries = inputContextQueriesForChatPresentationIntefaceState(chatPresentationInterfaceState).filter({ query in if chatPresentationInterfaceState.editMessageState != nil { switch query { @@ -36,7 +33,7 @@ func contextQueryResultStateForChatInterfacePresentationState(_ chatPresentation for query in inputQueries { let previousQuery = currentQueryStates[query.kind]?.0 if previousQuery != query { - let signal = updatedContextQueryResultStateForQuery(context: context, peer: peer, chatLocation: chatPresentationInterfaceState.chatLocation, inputQuery: query, previousQuery: previousQuery, requestBotLocationStatus: requestBotLocationStatus) + let signal = updatedContextQueryResultStateForQuery(context: context, peer: chatPresentationInterfaceState.renderedPeer?.peer, chatLocation: chatPresentationInterfaceState.chatLocation, inputQuery: query, previousQuery: previousQuery, requestBotLocationStatus: requestBotLocationStatus) updates[query.kind] = .update(query, signal) } } @@ -57,7 +54,7 @@ func contextQueryResultStateForChatInterfacePresentationState(_ chatPresentation return updates } -private func updatedContextQueryResultStateForQuery(context: AccountContext, peer: Peer, chatLocation: ChatLocation, inputQuery: ChatPresentationInputQuery, previousQuery: ChatPresentationInputQuery?, requestBotLocationStatus: @escaping (PeerId) -> Void) -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> { +private func updatedContextQueryResultStateForQuery(context: AccountContext, peer: Peer?, chatLocation: ChatLocation, inputQuery: ChatPresentationInputQuery, previousQuery: ChatPresentationInputQuery?, requestBotLocationStatus: @escaping (PeerId) -> Void) -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> { switch inputQuery { case let .emoji(query): var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = .complete() @@ -154,46 +151,64 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee let inlineBots: Signal<[(EnginePeer, Double)], NoError> = types.contains(.contextBots) ? context.engine.peers.recentlyUsedInlineBots() : .single([]) let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings - let participants = combineLatest(inlineBots, searchPeerMembers(context: context, peerId: peer.id, chatLocation: chatLocation, query: query, scope: .mention)) - |> map { inlineBots, peers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in - let filteredInlineBots = inlineBots.sorted(by: { $0.1 > $1.1 }).filter { peer, rating in - if rating < 0.14 { + + let participants: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> + if let peer { + participants = combineLatest( + inlineBots, + searchPeerMembers(context: context, peerId: peer.id, chatLocation: chatLocation, query: query, scope: .mention) + ) + |> map { inlineBots, peers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in + let filteredInlineBots = inlineBots.sorted(by: { $0.1 > $1.1 }).filter { peer, rating in + if rating < 0.14 { + return false + } + if peer.indexName.matchesByTokens(normalizedQuery) { + return true + } + if let addressName = peer.addressName, addressName.lowercased().hasPrefix(normalizedQuery) { + return true + } return false - } - if peer.indexName.matchesByTokens(normalizedQuery) { + }.map { $0.0 } + + let inlineBotPeerIds = Set(filteredInlineBots.map { $0.id }) + + let filteredPeers = peers.filter { peer in + if inlineBotPeerIds.contains(peer.id) { + return false + } + if !types.contains(.accountPeer) && peer.id == context.account.peerId { + return false + } return true } - if let addressName = peer.addressName, addressName.lowercased().hasPrefix(normalizedQuery) { - return true + var sortedPeers = filteredInlineBots + sortedPeers.append(contentsOf: filteredPeers.sorted(by: { lhs, rhs in + let result = lhs.indexName.stringRepresentation(lastNameFirst: true).compare(rhs.indexName.stringRepresentation(lastNameFirst: true)) + return result == .orderedAscending + })) + sortedPeers = sortedPeers.filter { peer in + return !peer.displayTitle(strings: strings, displayOrder: .firstLast).isEmpty } - return false - }.map { $0.0 } - - let inlineBotPeerIds = Set(filteredInlineBots.map { $0.id }) - - let filteredPeers = peers.filter { peer in - if inlineBotPeerIds.contains(peer.id) { - return false - } - if !types.contains(.accountPeer) && peer.id == context.account.peerId { - return false - } - return true + return { _ in return .mentions(sortedPeers) } } - var sortedPeers = filteredInlineBots - sortedPeers.append(contentsOf: filteredPeers.sorted(by: { lhs, rhs in - let result = lhs.indexName.stringRepresentation(lastNameFirst: true).compare(rhs.indexName.stringRepresentation(lastNameFirst: true)) - return result == .orderedAscending - })) - sortedPeers = sortedPeers.filter { peer in - return !peer.displayTitle(strings: strings, displayOrder: .firstLast).isEmpty - } - return { _ in return .mentions(sortedPeers) } + |> castError(ChatContextQueryError.self) + } else { + participants = .single({ _ in return nil }) } - |> castError(ChatContextQueryError.self) return signal |> then(participants) case let .command(query): + guard let peer else { + return .single({ _ in return .commands(ChatInputQueryCommandsResult( + commands: [], + accountPeer: nil, + hasShortcuts: false, + query: "" + )) }) + } + let normalizedQuery = query.lowercased() var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = .complete() @@ -258,6 +273,9 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee |> castError(ChatContextQueryError.self) return signal |> then(commands) case let .contextRequest(addressName, query): + guard let peer else { + return .single({ _ in return .contextRequestResult(nil, nil) }) + } var delayRequest = true var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = .complete() if let previousQuery = previousQuery {