Search filters improvements

This commit is contained in:
Ilya Laktyushin 2020-09-16 15:08:38 +03:00
parent da9cdbf433
commit a7408778d2
15 changed files with 331 additions and 152 deletions

View file

@ -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<MessageId>) -> Signal<ChatAvailableMessageActions, NoError>
func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messageIds: Set<MessageId>, messages: [MessageId: Message], peers: [PeerId: Peer]) -> Signal<ChatAvailableMessageActions, NoError>
func resolveUrl(account: Account, url: String) -> Signal<ResolvedUrl, NoError>
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)

View file

@ -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<ChatListSearchContainerNodeSearchState>
private let searchContextValue = Atomic<ChatListSearchMessagesContext?>(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<Bool>(false, ignoreRepeated: true)
override public var isSearching: Signal<Bool, NoError> {
@ -827,7 +831,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
self.addSubnode(self.emptyResultsAnimationNode)
self.addSubnode(self.emptyResultsTitleNode)
self.addSubnode(self.emptyResultsTextNode)
let searchContext = Promise<ChatListSearchMessagesContext?>(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<ChatAvailableMessageActions, NoError> 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)
}

View file

@ -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()
}
}
}

View file

@ -31,6 +31,7 @@ final class ChatListSearchMessageSelectionPanelNode: ASDisplayNode {
private var validLayout: ContainerViewLayout?
var chatAvailableMessageActions: ((Set<MessageId>) -> Signal<ChatAvailableMessageActions, NoError>)?
var selectedMessages = Set<MessageId>() {
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)
}
}
}
}))
}))
}
}
}
}

View file

@ -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 {

View file

@ -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
}
}

View file

@ -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<UITouch>, 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))
}
}

View file

@ -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 {

View file

@ -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<Api.messages.Messages, MTRpcError>
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<Api.messages.Messages?, NoError> in
return .single(nil)

View file

@ -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])
}

View file

@ -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

View file

@ -933,19 +933,40 @@ private func canPerformDeleteActions(limits: LimitsConfiguration, accountPeerId:
return false
}
func chatAvailableMessageActionsImpl(postbox: Postbox, accountPeerId: PeerId, messageIds: Set<MessageId>) -> Signal<ChatAvailableMessageActions, NoError> {
func chatAvailableMessageActionsImpl(postbox: Postbox, accountPeerId: PeerId, messageIds: Set<MessageId>, messages: [MessageId: Message] = [:], peers: [PeerId: Peer] = [:]) -> Signal<ChatAvailableMessageActions, NoError> {
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 {

View file

@ -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 {

View file

@ -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

View file

@ -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<MessageId>, messages: [MessageId: Message] = [:], peers: [PeerId: Peer] = [:]) -> Signal<ChatAvailableMessageActions, NoError> {
return chatAvailableMessageActionsImpl(postbox: postbox, accountPeerId: accountPeerId, messageIds: messageIds, messages: messages, peers: peers)
}
public func navigateToChatController(_ params: NavigateToChatControllerParams) {
navigateToChatControllerImpl(params)
}