mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Search filters improvements
This commit is contained in:
parent
da9cdbf433
commit
a7408778d2
15 changed files with 331 additions and 152 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
16
submodules/Display/Source/PassthroughContainerNode.swift
Normal file
16
submodules/Display/Source/PassthroughContainerNode.swift
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue