diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 07d3a40d74..d5308d37a0 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -552,6 +552,7 @@ public protocol SharedAccountContext: class { func navigateToChatController(_ params: NavigateToChatControllerParams) func openExternalUrl(context: AccountContext, urlContext: OpenURLContext, url: String, forceExternal: Bool, presentationData: PresentationData, navigationController: NavigationController?, dismissInput: @escaping () -> Void) func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messageIds: Set) -> Signal + func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messageIds: Set, messages: [MessageId: Message], peers: [PeerId: Peer]) -> Signal func resolveUrl(account: Account, url: String) -> Signal func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?) func openAddContact(context: AccountContext, firstName: String, lastName: String, phoneNumber: String, label: String, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, completed: @escaping () -> Void) diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 5b3f0459cc..e5ab68aff5 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -30,19 +30,6 @@ import InstantPageUI import ChatInterfaceState import ShareController -private final class PassthroughContainerNode: ASDisplayNode { - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if let subnodes = self.subnodes { - for subnode in subnodes { - if let result = subnode.view.hitTest(self.view.convert(point, to: subnode.view), with: event) { - return result - } - } - } - return nil - } -} - private enum ChatListRecentEntryStableId: Hashable { case topPeers case peerId(PeerId) @@ -724,6 +711,23 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo private var searchStateValue = ChatListSearchContainerNodeSearchState() private let searchStatePromise: ValuePromise private let searchContextValue = Atomic(value: nil) + private var searchCurrentMessages: [Message]? + + private let suggestedDates = Promise<[(Date, String?)]>([]) + private var suggestedDatesValue: [(Date, String?)] = [] { + didSet { + self.suggestedDates.set(.single(self.suggestedDatesValue)) + } + } + private let suggestedPeers = Promise<[Peer]>([]) + private var suggestedPeersValue: [Peer] = [] { + didSet { + self.suggestedPeers.set(.single(self.suggestedPeersValue)) + } + } + + private var suggestedFilters: [ChatListSearchFilter]? + private let suggestedFiltersDisposable = MetaDisposable() private let _isSearching = ValuePromise(false, ignoreRepeated: true) override public var isSearching: Signal { @@ -827,7 +831,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo self.addSubnode(self.emptyResultsAnimationNode) self.addSubnode(self.emptyResultsTitleNode) self.addSubnode(self.emptyResultsTextNode) - + let searchContext = Promise(nil) let searchContextValue = self.searchContextValue let updateSearchContext: ((ChatListSearchMessagesContext?) -> (ChatListSearchMessagesContext?, Bool)) -> Void = { f in @@ -1583,13 +1587,15 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo }) strongSelf.enqueueTransition(transition, firstTime: firstTime) - let previousPossiblePeers = strongSelf.possiblePeers - strongSelf.possiblePeers = Array(peers.prefix(10)) - - strongSelf.updatedDisplayFiltersPanel?(strongSelf.searchOptionsValue?.messageTags == nil || strongSelf.hasSuggestions) - if let (layout, navigationBarHeight) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + var messages: [Message] = [] + for entry in newEntries { + if case let .message(message, _, _, _, _, _, _) = entry { + messages.append(message) + } } + strongSelf.searchCurrentMessages = messages + + strongSelf.suggestedPeersValue = Array(peers.prefix(8)) } })) @@ -1658,6 +1664,64 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo strongSelf.updateSearchOptions(strongSelf.currentSearchOptions.withUpdatedMessageTags(messageTags).withUpdatedMaxDate(maxDate).withUpdatedPeer(peer), clearQuery: clearQuery) } + self.suggestedFiltersDisposable.set((combineLatest(self.suggestedPeers.get(), self.suggestedDates.get()) + |> mapToSignal { peers, dates -> Signal<([Peer], [(Date, String?)]), NoError> in + if peers.isEmpty && dates.isEmpty { + return .single((peers, dates)) + } else { + return (.complete() |> delay(0.2, queue: Queue.mainQueue())) + |> then(.single((peers, dates))) + } + } |> map { peers, dates -> [ChatListSearchFilter] in + var suggestedFilters: [ChatListSearchFilter] = [] + if !dates.isEmpty { + let formatter = DateFormatter() + formatter.timeStyle = .none + formatter.dateStyle = .medium + + for (date, string) in dates { + let title = string ?? formatter.string(from: date) + suggestedFilters.append(.date(Int32(date.timeIntervalSince1970), title)) + } + } + if !peers.isEmpty { + for peer in peers { + let isGroup: Bool + if let channel = peer as? TelegramChannel, case .group = channel.info { + isGroup = true + } else if peer.id.namespace == Namespaces.Peer.CloudGroup { + isGroup = true + } else { + isGroup = false + } + suggestedFilters.append(.peer(peer.id, isGroup, peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), peer.compactDisplayTitle)) + } + } + return suggestedFilters + } |> deliverOnMainQueue).start(next: { [weak self] filters in + guard let strongSelf = self else { + return + } + var filteredFilters: [ChatListSearchFilter] = [] + for filter in filters { + if case .date = filter, strongSelf.searchOptionsValue?.maxDate == nil { + filteredFilters.append(filter) + } + if case .peer = filter, strongSelf.searchOptionsValue?.peer == nil { + filteredFilters.append(filter) + } + } + let previousFilters = strongSelf.suggestedFilters ?? [] + strongSelf.suggestedFilters = filteredFilters + + if previousFilters.isEmpty != filteredFilters.isEmpty { + strongSelf.updatedDisplayFiltersPanel?(strongSelf.searchOptionsValue?.messageTags == nil || strongSelf.hasSuggestions) + if let (layout, navigationBarHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + } + } + })) + self.mediaStatusDisposable = (combineLatest(context.sharedContext.mediaManager.globalMediaPlayerState, self.searchOptions.get()) |> mapToSignal { playlistStateAndType, searchOptions -> Signal<(Account, SharedMediaPlayerItemPlaybackState, MediaManagerPlayerType)?, NoError> in if let (account, state, type) = playlistStateAndType { @@ -1711,11 +1775,14 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo self.presentationDataDisposable?.dispose() self.mediaStatusDisposable?.dispose() self.playlistPreloadDisposable?.dispose() + self.suggestedFiltersDisposable.dispose() } override public func didLoad() { super.didLoad() self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + + self.emptyResultsAnimationNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.animationTapGesture(_:)))) } @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { @@ -1723,6 +1790,12 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo self.cancel?() } } + + @objc private func animationTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state, !self.emptyResultsAnimationNode.isPlaying { + let _ = self.emptyResultsAnimationNode.playIfNeeded() + } + } private var currentSearchOptions: ChatListSearchOptions { return self.searchOptionsValue ?? ChatListSearchOptions(peer: nil, minDate: nil, maxDate: nil, messageTags: nil) @@ -1795,7 +1868,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo if let (_, dateTitle) = options?.maxDate { tokens.append(SearchBarToken(id: ChatListTokenId.date.rawValue, icon: UIImage(bundleImageName: "Chat List/Search/Calendar"), title: dateTitle)) - self.possibleDates = [] + self.suggestedDatesValue = [] } if clearQuery { @@ -1816,6 +1889,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo self.listNode.forEachItemHeaderNode({ itemHeaderNode in if let itemHeaderNode = itemHeaderNode as? ChatListSearchItemHeaderNode { itemHeaderNode.updateTheme(theme: theme) + } else if let itemHeaderNode = itemHeaderNode as? ListMessageDateHeaderNode { + itemHeaderNode.updateThemeAndStrings(theme: theme, strings: self.presentationData.strings) } }) self.recentListNode.forEachItemHeaderNode({ itemHeaderNode in @@ -1844,30 +1919,18 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo self.selectionPanelNode?.selectedMessages = self.searchStateValue.selectedMessageIds ?? [] } - var possibleDates: [(Date, String?)] = [] - var possiblePeers: [Peer] = [] override public func searchTextUpdated(text: String) { let searchQuery: String? = !text.isEmpty ? text : nil self.interaction?.searchTextHighightState = searchQuery self.searchQuery.set(.single(searchQuery)) self.searchQueryValue = searchQuery - let previousPossibleDate = self.possibleDates - self.possibleDates = suggestDates(for: text, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat) - - if previousPossibleDate.isEmpty != self.possibleDates.isEmpty { - self.updatedDisplayFiltersPanel?(self.searchOptionsValue?.messageTags == nil || self.hasSuggestions) - if let (layout, navigationBarHeight) = self.validLayout { - self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) - } - } + self.suggestedDatesValue = suggestDates(for: text, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat) } private var hasSuggestions: Bool { - if !self.possibleDates.isEmpty && self.searchOptionsValue?.maxDate == nil { - return true - } else if !self.possiblePeers.isEmpty && self.searchOptionsValue?.peer == nil { - return true + if let suggestedFilters = self.suggestedFilters { + return !suggestedFilters.isEmpty } else { return false } @@ -1910,7 +1973,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } private func dequeueTransition() { - if let (transition, firstTime) = self.enqueuedTransitions.first { + if let (transition, _) = self.enqueuedTransitions.first { self.enqueuedTransitions.remove(at: 0) var options = ListViewDeleteAndInsertOptions() @@ -2213,40 +2276,14 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo self.loadingNode.frame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: 422.0)) let filters: [ChatListSearchFilter] - var customFilters: [ChatListSearchFilter] = [] - if !self.possibleDates.isEmpty && self.searchOptionsValue?.maxDate == nil { - let formatter = DateFormatter() - formatter.timeStyle = .none - formatter.dateStyle = .medium - - for (date, string) in self.possibleDates { - let title = string ?? formatter.string(from: date) - customFilters.append(.date(Int32(date.timeIntervalSince1970), title)) - } - } - if !self.possiblePeers.isEmpty && self.searchOptionsValue?.peer == nil { - for peer in self.possiblePeers { - let isGroup: Bool - if let channel = peer as? TelegramChannel, case .group = channel.info { - isGroup = true - } else if peer.id.namespace == Namespaces.Peer.CloudGroup { - isGroup = true - } else { - isGroup = false - } - customFilters.append(.peer(peer.id, isGroup, peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), peer.compactDisplayTitle)) - } - } - - if !customFilters.isEmpty { - filters = customFilters + if let suggestedFilters = self.suggestedFilters, !suggestedFilters.isEmpty { + filters = suggestedFilters } else { filters = [.media, .links, .files, .music, .voice] } self.filterContainerNode.update(size: CGSize(width: layout.size.width, height: 37.0), sideInset: layout.safeInsets.left, filters: filters.map { .filter($0) }, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring)) - if let selectedMessageIds = self.searchStateValue.selectedMessageIds { var wasAdded = false let selectionPanelNode: ChatListSearchMessageSelectionPanelNode @@ -2287,6 +2324,24 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } strongSelf.forwardMessages(messageIds: nil) }) + selectionPanelNode.chatAvailableMessageActions = { [weak self] messageIds -> Signal in + guard let strongSelf = self else { + return .complete() + } + + var peers: [PeerId: Peer] = [:] + var messages: [MessageId: Message] = [:] + if let currentMessages = strongSelf.searchCurrentMessages { + for message in currentMessages { + messages[message.id] = message + for (_, peer) in message.peers { + peers[peer.id] = peer + } + } + } + + return strongSelf.context.sharedContext.chatAvailableMessageActions(postbox: strongSelf.context.account.postbox, accountPeerId: strongSelf.context.account.peerId, messageIds: messageIds, messages: messages, peers: peers) + } self.selectionPanelNode = selectionPanelNode self.addSubnode(selectionPanelNode) } diff --git a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift index 3063bf34ce..de29892d35 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift @@ -232,10 +232,14 @@ final class ChatListSearchFiltersContainerNode: ASDisplayNode { itemNode.update(type: type, presentationData: presentationData, transition: itemNodeTransition) } } + + var updated = false + var removeKeys: [ChatListSearchFilterEntryId] = [] for (id, _) in self.itemNodes { if !filters.contains(where: { $0.id == id }) { removeKeys.append(id) + updated = true } } for id in removeKeys { @@ -267,7 +271,7 @@ final class ChatListSearchFiltersContainerNode: ASDisplayNode { } let minSpacing: CGFloat = 24.0 - var spacing = minSpacing + let spacing = minSpacing let resolvedSideInset: CGFloat = 16.0 + sideInset var leftOffset: CGFloat = resolvedSideInset @@ -310,5 +314,9 @@ final class ChatListSearchFiltersContainerNode: ASDisplayNode { leftOffset += resolvedSideInset self.scrollNode.view.contentSize = CGSize(width: leftOffset, height: size.height) + + if updated && self.scrollNode.view.contentOffset.x > 0.0 { + self.scrollNode.view.contentOffset = CGPoint() + } } } diff --git a/submodules/ChatListUI/Sources/ChatListSearchMessageSelectionPanelNode.swift b/submodules/ChatListUI/Sources/ChatListSearchMessageSelectionPanelNode.swift index 09ae186c2a..93d4b11ac2 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchMessageSelectionPanelNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchMessageSelectionPanelNode.swift @@ -31,6 +31,7 @@ final class ChatListSearchMessageSelectionPanelNode: ASDisplayNode { private var validLayout: ContainerViewLayout? + var chatAvailableMessageActions: ((Set) -> Signal)? var selectedMessages = Set() { didSet { if oldValue != self.selectedMessages { @@ -44,15 +45,17 @@ final class ChatListSearchMessageSelectionPanelNode: ASDisplayNode { } self.canDeleteMessagesDisposable.set(nil) } else { - self.canDeleteMessagesDisposable.set((self.context.sharedContext.chatAvailableMessageActions(postbox: context.account.postbox, accountPeerId: context.account.peerId, messageIds: self.selectedMessages) - |> deliverOnMainQueue).start(next: { [weak self] actions in - if let strongSelf = self { - strongSelf.actions = actions - if let layout = strongSelf.validLayout { - let _ = strongSelf.update(layout: layout, presentationData: presentationData, transition: .immediate) + if let chatAvailableMessageActions = self.chatAvailableMessageActions { + self.canDeleteMessagesDisposable.set((chatAvailableMessageActions(self.selectedMessages) + |> deliverOnMainQueue).start(next: { [weak self] actions in + if let strongSelf = self { + strongSelf.actions = actions + if let layout = strongSelf.validLayout { + let _ = strongSelf.update(layout: layout, presentationData: presentationData, transition: .immediate) + } } - } - })) + })) + } } } } diff --git a/submodules/ChatListUI/Sources/DateSuggestion.swift b/submodules/ChatListUI/Sources/DateSuggestion.swift index 79b709a0ef..7a40eedf10 100644 --- a/submodules/ChatListUI/Sources/DateSuggestion.swift +++ b/submodules/ChatListUI/Sources/DateSuggestion.swift @@ -80,29 +80,30 @@ func suggestDates(for string: String, strings: PresentationStrings, dateTimeForm if date > telegramReleaseDate { result.append((date, "\(value)")) } - } else if !dateSeparator.isEmpty && string.contains(dateSeparator) { - let stringComponents = string.components(separatedBy: dateSeparator) - if stringComponents.count > 1 { - let locale = Locale(identifier: strings.baseLanguageCode) - do { - let dd = try NSDataDetector(types: NSTextCheckingResult.CheckingType.date.rawValue) - if let match = dd.firstMatch(in: string, options: [], range: NSMakeRange(0, string.utf16.count)), let date = match.date, date > telegramReleaseDate { - var resultDate = date - if resultDate > now { - if let date = calendar.date(byAdding: .year, value: -1, to: resultDate) { - resultDate = date - } + } else { + do { + let dd = try NSDataDetector(types: NSTextCheckingResult.CheckingType.date.rawValue) + if let match = dd.firstMatch(in: string, options: [], range: NSMakeRange(0, string.utf16.count)), let date = match.date, date > telegramReleaseDate { + var resultDate = date + if resultDate > now && !calendar.isDate(resultDate, equalTo: now, toGranularity: .year) { + if let date = calendar.date(byAdding: .year, value: -1, to: resultDate) { + resultDate = date } - + } + + let stringComponents = string.components(separatedBy: dateSeparator) + if stringComponents.count < 3 { for i in 0..<5 { if let date = calendar.date(byAdding: .year, value: -i, to: resultDate) { result.append((date, nil)) } } + } else if resultDate < now { + result.append((resultDate, nil)) } - } catch { - } + } catch { + } } } else { diff --git a/submodules/Display/Source/PassthroughContainerNode.swift b/submodules/Display/Source/PassthroughContainerNode.swift new file mode 100644 index 0000000000..dfc1b03a66 --- /dev/null +++ b/submodules/Display/Source/PassthroughContainerNode.swift @@ -0,0 +1,16 @@ +import Foundation +import UIKit +import AsyncDisplayKit + +public final class PassthroughContainerNode: ASDisplayNode { + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let subnodes = self.subnodes { + for subnode in subnodes { + if let result = subnode.view.hitTest(self.view.convert(point, to: subnode.view), with: event) { + return result + } + } + } + return nil + } +} diff --git a/submodules/SearchBarNode/Sources/SearchBarNode.swift b/submodules/SearchBarNode/Sources/SearchBarNode.swift index b6900adf64..1c48adfc24 100644 --- a/submodules/SearchBarNode/Sources/SearchBarNode.swift +++ b/submodules/SearchBarNode/Sources/SearchBarNode.swift @@ -133,7 +133,10 @@ private final class TokenNode: ASDisplayNode { let strokeColor = isSelected ? backgroundColor : (token.style?.strokeColor ?? backgroundColor) self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 8.0, color: backgroundColor, strokeColor: strokeColor, strokeWidth: UIScreenPixel, backgroundColor: nil) - let foregroundColor = isSelected ? .white : (token.style?.foregroundColor ?? .white) + var foregroundColor = isSelected ? .white : (token.style?.foregroundColor ?? .white) + if foregroundColor.distance(to: backgroundColor) < 1 { + foregroundColor = .black + } if let image = token.icon { self.iconNode.image = generateTintedImage(image: image, color: foregroundColor) @@ -167,7 +170,7 @@ private final class TokenNode: ASDisplayNode { } } -private class SearchBarTextField: UITextField { +private class SearchBarTextField: UITextField, UIScrollViewDelegate { public var didDeleteBackward: (() -> Bool)? let placeholderLabel: ImmediateTextNode @@ -178,6 +181,8 @@ private class SearchBarTextField: UITextField { } } + var clippingNode: PassthroughContainerNode + var tokenContainerNode: PassthroughContainerNode var tokenNodes: [AnyHashable: TokenNode] = [:] var tokens: [SearchBarToken] = [] { didSet { @@ -242,6 +247,10 @@ private class SearchBarTextField: UITextField { tokenNode.tapped = { [weak self] in self?.selectedTokenIndex = i self?.becomeFirstResponder() + if let strongSelf = self { + let newPosition = strongSelf.beginningOfDocument + strongSelf.selectedTextRange = strongSelf.textRange(from: newPosition, to: newPosition) + } } let isSelected = i == self.selectedTokenIndex if i < self.tokens.count - 1 && isSelected { @@ -277,17 +286,18 @@ private class SearchBarTextField: UITextField { var tokenNodeTransition = transition if wasAdded { tokenNodeTransition = .immediate - self.addSubnode(tokenNode) + self.tokenContainerNode.addSubnode(tokenNode) } - let nodeSize = tokenNode.updateLayout(constrainedSize: self.bounds.size, transition: tokenNodeTransition) + let constrainedSize = CGSize(width: self.bounds.size.width - 60.0, height: self.bounds.size.height) + let nodeSize = tokenNode.updateLayout(constrainedSize: constrainedSize, transition: tokenNodeTransition) tokenSizes.append((token.id, nodeSize, tokenNode, wasAdded)) totalRawTabSize += nodeSize.width } let minSpacing: CGFloat = 6.0 - let resolvedSideInset: CGFloat = 10.0 + let resolvedSideInset: CGFloat = 0.0 var leftOffset: CGFloat = 0.0 if !tokenSizes.isEmpty { leftOffset += resolvedSideInset @@ -332,10 +342,22 @@ private class SearchBarTextField: UITextField { } if !tokenSizes.isEmpty { - leftOffset -= 6.0 + leftOffset += 4.0 } + let previousTokensWidth = self.tokensWidth self.tokensWidth = leftOffset + self.tokenContainerNode.frame = CGRect(origin: self.tokenContainerNode.frame.origin, size: CGSize(width: self.tokensWidth, height: self.bounds.height)) + + if let scrollView = self.scrollView { + scrollView.contentInset = UIEdgeInsets(top: 0.0, left: leftOffset, bottom: 0.0, right: 0.0) + if leftOffset.isZero { + scrollView.contentOffset = CGPoint() + } else if self.tokensWidth != previousTokensWidth { + scrollView.contentOffset = CGPoint(x: -leftOffset, y: 0.0) + } + self.updateTokenContainerPosition(transition: transition) + } } private var tokensWidth: CGFloat = 0.0 @@ -352,7 +374,7 @@ private class SearchBarTextField: UITextField { init(theme: SearchBarNodeTheme) { self.theme = theme - + self.placeholderLabel = ImmediateTextNode() self.placeholderLabel.isUserInteractionEnabled = false self.placeholderLabel.displaysAsynchronously = false @@ -371,16 +393,51 @@ private class SearchBarTextField: UITextField { self.prefixLabel.maximumNumberOfLines = 1 self.prefixLabel.truncationMode = .byTruncatingTail + self.clippingNode = PassthroughContainerNode() + self.clippingNode.clipsToBounds = true + + self.tokenContainerNode = PassthroughContainerNode() + super.init(frame: CGRect()) self.addSubnode(self.placeholderLabel) self.addSubnode(self.prefixLabel) + self.addSubnode(self.clippingNode) + self.clippingNode.addSubnode(self.tokenContainerNode) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + override func addSubview(_ view: UIView) { + super.addSubview(view) + + if let scrollView = view as? UIScrollView { + scrollView.delegate = self + self.bringSubviewToFront(self.clippingNode.view) + } + } + + var scrollView: UIScrollView? { + for view in self.subviews { + if let scrollView = view as? UIScrollView { + return scrollView + } + } + return nil + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.updateTokenContainerPosition() + } + + private func updateTokenContainerPosition(transition: ContainedViewLayoutTransition = .immediate) { + if let scrollView = self.scrollView { + transition.updateFrame(node: self.tokenContainerNode, frame: CGRect(origin: CGPoint(x: -scrollView.contentOffset.x - scrollView.contentInset.left, y: 0.0), size: self.tokenContainerNode.frame.size)) + } + } + override var keyboardAppearance: UIKeyboardAppearance { get { return super.keyboardAppearance @@ -410,10 +467,6 @@ private class SearchBarTextField: UITextField { rect.origin.x += prefixOffset rect.size.width -= prefixOffset } - if !self.tokensWidth.isZero { - rect.origin.x += self.tokensWidth - rect.size.width -= self.tokensWidth - } rect.size.width = max(rect.size.width, 10.0) return rect } @@ -426,6 +479,8 @@ private class SearchBarTextField: UITextField { super.layoutSubviews() let bounds = self.bounds + self.clippingNode.frame = CGRect(x: 10.0, y: 0.0, width: bounds.width - 20.0, height: bounds.height) + if bounds.size.width.isZero { return } @@ -448,7 +503,7 @@ private class SearchBarTextField: UITextField { var processed = false if let selectedRange = self.selectedTextRange { let cursorPosition = self.offset(from: self.beginningOfDocument, to: selectedRange.start) - if cursorPosition == 0 && !self.tokens.isEmpty && self.selectedTokenIndex == nil { + if cursorPosition == 0 && selectedRange.isEmpty && !self.tokens.isEmpty && self.selectedTokenIndex == nil { self.selectedTokenIndex = self.tokens.count - 1 processed = true } @@ -459,6 +514,36 @@ private class SearchBarTextField: UITextField { } if !processed { super.deleteBackward() + + if let scrollView = self.scrollView { + if scrollView.contentSize.width <= scrollView.frame.width && scrollView.contentOffset.x > -scrollView.contentInset.left { + scrollView.contentOffset = CGPoint(x: max(scrollView.contentOffset.x - 5.0, -scrollView.contentInset.left), y: 0.0) + self.updateTokenContainerPosition() + } + } + } + } + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + if let _ = self.selectedTokenIndex { + self.selectedTokenIndex = nil + if let touch = touches.first, let gestureRecognizers = touch.gestureRecognizers { + let point = touch.location(in: self.tokenContainerNode.view) + for (_, tokenNode) in self.tokenNodes { + if tokenNode.frame.contains(point) { + super.touchesBegan(touches, with: event) + return + } + } + for gesture in gestureRecognizers { + if gesture is UITapGestureRecognizer, gesture.isEnabled { + gesture.isEnabled = false + gesture.isEnabled = true + } + } + } + } else { + super.touchesBegan(touches, with: event) } } } @@ -956,6 +1041,12 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate { } public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + if let _ = self.textField.selectedTokenIndex { + self.textField.selectedTokenIndex = nil + if string.range(of: " ") != nil { + return false + } + } if string.range(of: "\n") != nil { return false } @@ -972,12 +1063,8 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate { @objc private func textFieldDidChange(_ textField: UITextField) { self.updateIsEmpty() - if let _ = self.textField.selectedTokenIndex { - self.textField.selectedTokenIndex = nil - } if let textUpdated = self.textUpdated { textUpdated(textField.text ?? "", textField.textInputMode?.primaryLanguage) - self.textField.layoutTokens(transition: .animated(duration: 0.2, curve: .easeInOut)) } } diff --git a/submodules/SearchUI/Sources/SearchDisplayController.swift b/submodules/SearchUI/Sources/SearchDisplayController.swift index 9843a4d8fd..57d9f4ee3e 100644 --- a/submodules/SearchUI/Sources/SearchDisplayController.swift +++ b/submodules/SearchUI/Sources/SearchDisplayController.swift @@ -28,6 +28,7 @@ public final class SearchDisplayController { self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: presentationData.theme, hasSeparator: hasSeparator), strings: presentationData.strings, fieldStyle: .modern, forceSeparator: hasSeparator) self.backgroundNode = ASDisplayNode() self.backgroundNode.backgroundColor = presentationData.theme.chatList.backgroundColor + self.backgroundNode.allowsGroupOpacity = true self.mode = mode self.contentNode = contentNode @@ -61,10 +62,10 @@ public final class SearchDisplayController { strongSelf.searchBar.prefixString = prefix let previousTokens = strongSelf.searchBar.tokens strongSelf.searchBar.tokens = tokens + strongSelf.searchBar.text = query if previousTokens.count < tokens.count { strongSelf.searchBar.selectLastToken() } - strongSelf.searchBar.text = query } } if let placeholder = placeholder { @@ -119,8 +120,9 @@ public final class SearchDisplayController { self.containerLayout = (layout, navigationBarFrame.maxY) - transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + let bounds = CGRect(origin: CGPoint(), size: layout.size) + transition.updateFrame(node: self.backgroundNode, frame: bounds.insetBy(dx: -20.0, dy: -20.0)) + transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(x: 20.0, y: 20.0), size: layout.size)) self.contentNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: LayoutMetrics(), deviceMetrics: layout.deviceMetrics, intrinsicInsets: layout.intrinsicInsets, safeInsets: layout.safeInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationBarHeight, transition: transition) } @@ -130,7 +132,7 @@ public final class SearchDisplayController { } insertSubnode(self.backgroundNode, false) - insertSubnode(self.contentNode, false) + self.backgroundNode.addSubnode(self.contentNode) self.contentNode.frame = CGRect(origin: CGPoint(), size: layout.size) @@ -148,8 +150,7 @@ public final class SearchDisplayController { // self.contentNode.layer.animatePosition(from: CGPoint(x: contentNodePosition.x, y: contentNodePosition.y + (initialTextBackgroundFrame.maxY + 8.0 - contentNavigationBarHeight)), to: contentNodePosition, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) - self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) - self.contentNode.layer.animateScale(from: 0.85, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + self.backgroundNode.layer.animateScale(from: 0.85, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) self.searchBar.placeholderString = placeholder.placeholderString } @@ -215,10 +216,7 @@ public final class SearchDisplayController { self.contentNode.layer.animatePosition(from: contentNodePosition, to: CGPoint(x: contentNodePosition.x, y: contentNodePosition.y + (targetTextBackgroundFrame.maxY + 8.0 - contentNavigationBarHeight)), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) } - contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak contentNode] _ in - contentNode?.removeFromSupernode() - }) - backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak backgroundNode] _ in + backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak backgroundNode] _ in backgroundNode?.removeFromSupernode() }) } else { diff --git a/submodules/TelegramCore/Sources/SearchMessages.swift b/submodules/TelegramCore/Sources/SearchMessages.swift index 4e0ce26fb7..6a77448943 100644 --- a/submodules/TelegramCore/Sources/SearchMessages.swift +++ b/submodules/TelegramCore/Sources/SearchMessages.swift @@ -237,7 +237,13 @@ public func searchMessages(account: Account, location: SearchMessagesLocation, q peerMessages = .single(nil) } else { let lowerBound = state?.main.messages.last.flatMap({ $0.index }) - peerMessages = account.network.request(Api.functions.messages.search(flags: flags, peer: inputPeer, q: query, fromId: fromInputUser, topMsgId: topMsgId?.id, filter: filter, minDate: minDate ?? 0, maxDate: maxDate ?? (Int32.max - 1), offsetId: lowerBound?.id.id ?? 0, addOffset: 0, limit: limit, maxId: Int32.max - 1, minId: 0, hash: 0)) + let signal: Signal + if peer.id.namespace == Namespaces.Peer.CloudChannel && tags == nil && minDate == nil && maxDate == nil { + signal = account.network.request(Api.functions.messages.getHistory(peer: inputPeer, offsetId: lowerBound?.id.id ?? 0, offsetDate: 0, addOffset: 0, limit: limit, maxId: Int32.max - 1, minId: 0, hash: 0)) + } else { + signal = account.network.request(Api.functions.messages.search(flags: flags, peer: inputPeer, q: query, fromId: fromInputUser, topMsgId: topMsgId?.id, filter: filter, minDate: minDate ?? 0, maxDate: maxDate ?? (Int32.max - 1), offsetId: lowerBound?.id.id ?? 0, addOffset: 0, limit: limit, maxId: Int32.max - 1, minId: 0, hash: 0)) + } + peerMessages = signal |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 725eaeb617..683d3ae391 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -3630,23 +3630,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G transformedMessages = strongSelf.transformEnqueueMessages(messages) } - for message in transformedMessages { - let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: [message]) - |> deliverOnMainQueue).start(next: { messageIds in - - }) - - } - -// let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: transformedMessages) -// |> deliverOnMainQueue).start(next: { messageIds in -// if let strongSelf = self { -// if strongSelf.presentationInterfaceState.isScheduledMessages { -// } else { -// strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() -// } -// } -// }) + let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: transformedMessages) + |> deliverOnMainQueue).start(next: { messageIds in + if let strongSelf = self { + if strongSelf.presentationInterfaceState.isScheduledMessages { + } else { + strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() + } + } + }) donateSendMessageIntent(account: strongSelf.context.account, sharedContext: strongSelf.context.sharedContext, intentContext: .chat, peerIds: [peerId]) } diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 0a39157275..deae37c2e9 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -2734,7 +2734,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } var forwardingToSameChat = false - if case let .peer(id) = self.chatPresentationInterfaceState.chatLocation, id.namespace == Namespaces.Peer.CloudUser, id != self.context.account.peerId, let forwardMessageIds = self.chatPresentationInterfaceState.interfaceState.forwardMessageIds { + if case let .peer(id) = self.chatPresentationInterfaceState.chatLocation, id.namespace == Namespaces.Peer.CloudUser, id != self.context.account.peerId, let forwardMessageIds = self.chatPresentationInterfaceState.interfaceState.forwardMessageIds, forwardMessageIds.count == 1 { for messageId in forwardMessageIds { if messageId.peerId == id { forwardingToSameChat = true diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 559b5454a1..557134b295 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -933,19 +933,40 @@ private func canPerformDeleteActions(limits: LimitsConfiguration, accountPeerId: return false } -func chatAvailableMessageActionsImpl(postbox: Postbox, accountPeerId: PeerId, messageIds: Set) -> Signal { +func chatAvailableMessageActionsImpl(postbox: Postbox, accountPeerId: PeerId, messageIds: Set, messages: [MessageId: Message] = [:], peers: [PeerId: Peer] = [:]) -> Signal { return postbox.transaction { transaction -> ChatAvailableMessageActions in let limitsConfiguration: LimitsConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.limitsConfiguration) as? LimitsConfiguration ?? LimitsConfiguration.defaultValue var optionsMap: [MessageId: ChatAvailableMessageActionOptions] = [:] var banPeer: Peer? var hadPersonalIncoming = false var hadBanPeerId = false + + func getPeer(_ peerId: PeerId) -> Peer? { + if let peer = transaction.getPeer(peerId) { + return peer + } else if let peer = peers[peerId] { + return peer + } else { + return nil + } + } + + func getMessage(_ messageId: MessageId) -> Message? { + if let message = transaction.getMessage(messageId) { + return message + } else if let message = messages[messageId] { + return message + } else { + return nil + } + } + for id in messageIds { let isScheduled = id.namespace == Namespaces.Message.ScheduledCloud if optionsMap[id] == nil { optionsMap[id] = [] } - if let message = transaction.getMessage(id) { + if let message = getMessage(id) { for media in message.media { if let file = media as? TelegramMediaFile, file.isSticker { for case let .Sticker(sticker) in file.attributes { @@ -963,7 +984,7 @@ func chatAvailableMessageActionsImpl(postbox: Postbox, accountPeerId: PeerId, me if canEditMessage(accountPeerId: accountPeerId, limitsConfiguration: limitsConfiguration, message: message, reschedule: true) { optionsMap[id]!.insert(.editScheduledTime) } - if let peer = transaction.getPeer(id.peerId), let channel = peer as? TelegramChannel { + if let peer = getPeer(id.peerId), let channel = peer as? TelegramChannel { if !message.flags.contains(.Incoming) { optionsMap[id]!.insert(.deleteLocally) } else { @@ -979,7 +1000,7 @@ func chatAvailableMessageActionsImpl(postbox: Postbox, accountPeerId: PeerId, me optionsMap[id]!.insert(.forward) } optionsMap[id]!.insert(.deleteLocally) - } else if let peer = transaction.getPeer(id.peerId) { + } else if let peer = getPeer(id.peerId) { var isAction = false var isDice = false for media in message.media { diff --git a/submodules/TelegramUI/Sources/PaneSearchBarNode.swift b/submodules/TelegramUI/Sources/PaneSearchBarNode.swift index 6e430c85ab..490394469a 100644 --- a/submodules/TelegramUI/Sources/PaneSearchBarNode.swift +++ b/submodules/TelegramUI/Sources/PaneSearchBarNode.swift @@ -293,7 +293,7 @@ class PaneSearchBarNode: ASDisplayNode, UITextFieldDelegate { if let iconImage = self.iconNode.image { let iconSize = iconImage.size - transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: textBackgroundFrame.minX + 11.0, y: textBackgroundFrame.minY + floor((textBackgroundFrame.size.height - iconSize.height) / 2.0)), size: iconSize)) + transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: textBackgroundFrame.minX + 5.0, y: textBackgroundFrame.minY + floor((textBackgroundFrame.size.height - iconSize.height) / 2.0)), size: iconSize)) } if let activityIndicator = self.activityIndicator { diff --git a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoListPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoListPaneNode.swift index 8bc01d9a22..de270ceb40 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoListPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoListPaneNode.swift @@ -15,19 +15,6 @@ import TelegramBaseController import OverlayStatusController import ListMessageItem -private final class PassthroughContainerNode: ASDisplayNode { - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if let subnodes = self.subnodes { - for subnode in subnodes { - if let result = subnode.view.hitTest(self.view.convert(point, to: subnode.view), with: event) { - return result - } - } - } - return nil - } -} - final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { private let context: AccountContext private let peerId: PeerId diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 682fe934b9..3b752156bb 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1072,6 +1072,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { return chatAvailableMessageActionsImpl(postbox: postbox, accountPeerId: accountPeerId, messageIds: messageIds) } + public func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messageIds: Set, messages: [MessageId: Message] = [:], peers: [PeerId: Peer] = [:]) -> Signal { + return chatAvailableMessageActionsImpl(postbox: postbox, accountPeerId: accountPeerId, messageIds: messageIds, messages: messages, peers: peers) + } + public func navigateToChatController(_ params: NavigateToChatControllerParams) { navigateToChatControllerImpl(params) }