import Foundation import UIKit import AsyncDisplayKit import Display import SwiftSignalKit import Postbox import TelegramCore import SyncCore import TelegramPresentationData import TelegramUIPreferences import MergeLists import AccountContext import SearchUI import ContactsPeerItem import ChatListSearchItemHeader import ContactListUI import ContextUI import PhoneNumberFormat import ItemListUI import SearchBarNode import ListMessageItem import TelegramBaseController import OverlayStatusController import UniversalMediaPlayer import PresentationDataUtils import AnimatedStickerNode import AppBundle import GalleryData import InstantPageUI import ChatInterfaceState import ShareController private enum ChatListTokenId: Int32 { case filter case peer case date } final class ChatListSearchInteraction { let openPeer: (Peer, Bool) -> Void let openDisabledPeer: (Peer) -> Void let openMessage: (Peer, MessageId) -> Void let openUrl: (String) -> Void let clearRecentSearch: () -> Void let addContact: (String) -> Void let toggleMessageSelection: (MessageId, Bool) -> Void let messageContextAction: ((Message, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void) let mediaMessageContextAction: ((Message, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void) let peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)? let present: (ViewController, Any?) -> Void let dismissInput: () -> Void let updateSuggestedPeers: ([Peer], ChatListSearchPaneKey) -> Void let getSelectedMessageIds: () -> Set? init(openPeer: @escaping (Peer, Bool) -> Void, openDisabledPeer: @escaping (Peer) -> Void, openMessage: @escaping (Peer, MessageId) -> Void, openUrl: @escaping (String) -> Void, clearRecentSearch: @escaping () -> Void, addContact: @escaping (String) -> Void, toggleMessageSelection: @escaping (MessageId, Bool) -> Void, messageContextAction: @escaping ((Message, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void), mediaMessageContextAction: @escaping ((Message, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void), peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, updateSuggestedPeers: @escaping ([Peer], ChatListSearchPaneKey) -> Void, getSelectedMessageIds: @escaping () -> Set?) { self.openPeer = openPeer self.openDisabledPeer = openDisabledPeer self.openMessage = openMessage self.openUrl = openUrl self.clearRecentSearch = clearRecentSearch self.addContact = addContact self.toggleMessageSelection = toggleMessageSelection self.messageContextAction = messageContextAction self.mediaMessageContextAction = mediaMessageContextAction self.peerContextAction = peerContextAction self.present = present self.dismissInput = dismissInput self.updateSuggestedPeers = updateSuggestedPeers self.getSelectedMessageIds = getSelectedMessageIds } } private struct ChatListSearchContainerNodeSearchState: Equatable { var selectedMessageIds: Set? func withUpdatedSelectedMessageIds(_ selectedMessageIds: Set?) -> ChatListSearchContainerNodeSearchState { return ChatListSearchContainerNodeSearchState(selectedMessageIds: selectedMessageIds) } } public final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { private let context: AccountContext private let peersFilter: ChatListNodePeersFilter private var interaction: ChatListSearchInteraction? private let openMessage: (Peer, MessageId) -> Void private let navigationController: NavigationController? let filterContainerNode: ChatListSearchFiltersContainerNode private let paneContainerNode: ChatListSearchPaneContainerNode private var selectionPanelNode: ChatListSearchMessageSelectionPanelNode? private var present: ((ViewController, Any?) -> Void)? private var presentInGlobalOverlay: ((ViewController, Any?) -> Void)? private let activeActionDisposable = MetaDisposable() private var searchQueryValue: String? private let searchQuery = Promise(nil) private var searchOptionsValue: ChatListSearchOptions? private let searchOptions = Promise(nil) private let searchDisposable = MetaDisposable() private var presentationData: PresentationData private var presentationDataDisposable: Disposable? private let suggestedDates = Promise<[(Date, String?)]>([]) private let suggestedPeers = Promise<[Peer]>([]) private var suggestedFilters: [ChatListSearchFilter]? private let suggestedFiltersDisposable = MetaDisposable() private var stateValue = ChatListSearchContainerNodeSearchState() private let statePromise = ValuePromise() private var selectedFilterKey: ChatListSearchFilterEntryId? = .filter(ChatListSearchFilter.chats.id) private var selectedFilterKeyPromise = Promise(.filter(ChatListSearchFilter.chats.id)) private var transitionFraction: CGFloat = 0.0 private var didSetReady: Bool = false private let _ready = Promise() public override func ready() -> Signal { return self._ready.get() } private var validLayout: (ContainerViewLayout, CGFloat)? public init(context: AccountContext, filter: ChatListNodePeersFilter, groupId: PeerGroupId, openPeer originalOpenPeer: @escaping (Peer, Bool) -> Void, openDisabledPeer: @escaping (Peer) -> Void, openRecentPeerOptions: @escaping (Peer) -> Void, openMessage originalOpenMessage: @escaping (Peer, MessageId) -> Void, addContact: ((String) -> Void)?, peerContextAction: ((Peer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, navigationController: NavigationController?) { self.context = context self.peersFilter = filter self.navigationController = navigationController self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.openMessage = originalOpenMessage self.present = present self.presentInGlobalOverlay = presentInGlobalOverlay self.filterContainerNode = ChatListSearchFiltersContainerNode() self.paneContainerNode = ChatListSearchPaneContainerNode(context: context, peersFilter: self.peersFilter, groupId: groupId, searchQuery: self.searchQuery.get(), searchOptions: self.searchOptions.get(), navigationController: navigationController) self.paneContainerNode.clipsToBounds = true super.init() self.backgroundColor = filter.contains(.excludeRecent) ? nil : self.presentationData.theme.chatList.backgroundColor self.addSubnode(self.paneContainerNode) let interaction = ChatListSearchInteraction(openPeer: { peer, value in originalOpenPeer(peer, value) if peer.id.namespace != Namespaces.Peer.SecretChat { addAppLogEvent(postbox: context.account.postbox, type: "search_global_open_peer", peerId: peer.id) } }, openDisabledPeer: { peer in openDisabledPeer(peer) }, openMessage: { peer, messageId in originalOpenMessage(peer, messageId) if peer.id.namespace != Namespaces.Peer.SecretChat { addAppLogEvent(postbox: context.account.postbox, type: "search_global_open_message", peerId: peer.id, data: .dictionary(["msg_id": .number(Double(messageId.id))])) } }, openUrl: { url in openUserGeneratedUrl(context: context, url: url, concealed: false, present: { c in present(c, nil) }, openResolved: { [weak self] resolved in context.sharedContext.openResolvedUrl(resolved, context: context, urlContext: .generic, navigationController: navigationController, openPeer: { peerId, navigation in // self?.openPeer(peerId: peerId, navigation: navigation) }, sendFile: nil, sendSticker: nil, present: { c, a in present(c, a) }, dismissInput: { self?.dismissInput() }, contentContext: nil) }) }, clearRecentSearch: { [weak self] in guard let strongSelf = self else { return } let presentationData = strongSelf.presentationData let actionSheet = ActionSheetController(presentationData: presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.WebSearch_RecentSectionClear, color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() guard let strongSelf = self else { return } let _ = (clearRecentlySearchedPeers(postbox: strongSelf.context.account.postbox) |> deliverOnMainQueue).start() }) ]), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) strongSelf.dismissInput() strongSelf.present?(actionSheet, nil) }, addContact: { phoneNumber in addContact?(phoneNumber) }, toggleMessageSelection: { [weak self] messageId, selected in if let strongSelf = self { strongSelf.updateState { state in var selectedMessageIds = state.selectedMessageIds ?? Set() if selected { selectedMessageIds.insert(messageId) } else { selectedMessageIds.remove(messageId) } return state.withUpdatedSelectedMessageIds(selectedMessageIds) } } }, messageContextAction: { [weak self] message, node, rect, gesture in self?.messageContextAction(message, node: node, rect: rect, gesture: gesture) }, mediaMessageContextAction: { [weak self] message, node, rect, gesture in self?.mediaMessageContextAction(message, node: node, rect: rect, gesture: gesture) }, peerContextAction: { peer, source, node, gesture in peerContextAction?(peer, source, node, gesture) }, present: { c, a in present(c, a) }, dismissInput: { [weak self] in self?.dismissInput() }, updateSuggestedPeers: { [weak self] peers, key in if let strongSelf = self, key == .chats { strongSelf.suggestedPeers.set(.single(peers)) } }, getSelectedMessageIds: { [weak self] () -> Set? in if let strongSelf = self { return strongSelf.stateValue.selectedMessageIds } else { return nil } }) self.paneContainerNode.interaction = interaction self.paneContainerNode.currentPaneUpdated = { [weak self] key, transitionFraction, transition in if let strongSelf = self { var filterKey: ChatListSearchFilter? if let key = key { switch key { case .chats: filterKey = .chats case .media: filterKey = .media case .links: filterKey = .links case .files: filterKey = .files case .music: filterKey = .music case .voice: filterKey = .voice } } strongSelf.selectedFilterKey = filterKey.flatMap { .filter($0.id) } strongSelf.selectedFilterKeyPromise.set(.single(strongSelf.selectedFilterKey)) strongSelf.transitionFraction = transitionFraction if let (layout, _) = strongSelf.validLayout { let filters: [ChatListSearchFilter] if let suggestedFilters = strongSelf.suggestedFilters, !suggestedFilters.isEmpty { filters = suggestedFilters } else { filters = [.chats, .media, .links, .files, .music, .voice] } strongSelf.filterContainerNode.update(size: CGSize(width: layout.size.width - 40.0, height: 38.0), sideInset: layout.safeInsets.left - 20.0, filters: filters.map { .filter($0) }, selectedFilter: strongSelf.selectedFilterKey, transitionFraction: strongSelf.transitionFraction, presentationData: strongSelf.presentationData, transition: transition) } } } self.filterContainerNode.filterPressed = { [weak self] filter in guard let strongSelf = self else { return } var key: ChatListSearchPaneKey? var maxDate = strongSelf.currentSearchOptions.maxDate var peer = strongSelf.currentSearchOptions.peer switch filter { case .chats: key = .chats case .media: key = .media case .links: key = .links case .files: key = .files case .music: key = .music case .voice: key = .voice case let .date(date, title): maxDate = (date, title) case let .peer(id, isGroup, _, compactDisplayTitle): peer = (id, isGroup, compactDisplayTitle) } if let key = key { strongSelf.paneContainerNode.requestSelectPane(key) } else { strongSelf.updateSearchOptions(strongSelf.currentSearchOptions.withUpdatedMaxDate(maxDate).withUpdatedPeer(peer), clearQuery: true) } } self.suggestedFiltersDisposable.set((combineLatest(self.suggestedPeers.get(), self.suggestedDates.get(), self.selectedFilterKeyPromise.get()) |> mapToSignal { peers, dates, selectedFilter -> Signal<([Peer], [(Date, String?)], ChatListSearchFilterEntryId?), NoError> in if (peers.isEmpty && dates.isEmpty) || peers.isEmpty { return .single((peers, dates, selectedFilter)) } else { return (.complete() |> delay(0.2, queue: Queue.mainQueue())) |> then(.single((peers, dates, selectedFilter))) } } |> map { peers, dates, selectedFilter -> [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 && selectedFilter != .filter(ChatListSearchFilter.chats.id) { 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 filteredFilters != previousFilters { if let (layout, navigationBarHeight) = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) } } })) self.presentationDataDisposable = (context.sharedContext.presentationData |> deliverOnMainQueue).start(next: { [weak self] presentationData in if let strongSelf = self { let previousTheme = strongSelf.presentationData.theme strongSelf.presentationData = presentationData if previousTheme !== presentationData.theme { strongSelf.updateTheme(theme: presentationData.theme) } } }) self._ready.set(self.paneContainerNode.isReady.get() |> map { _ in Void() }) } deinit { self.activeActionDisposable.dispose() self.searchDisposable.dispose() self.presentationDataDisposable?.dispose() self.suggestedFiltersDisposable.dispose() } private func updateState(_ f: (ChatListSearchContainerNodeSearchState) -> ChatListSearchContainerNodeSearchState) { let state = f(self.stateValue) if state != self.stateValue { self.stateValue = state self.statePromise.set(state) } self.paneContainerNode.currentPane?.node.updateSelectedMessages(animated: true) self.selectionPanelNode?.selectedMessages = self.stateValue.selectedMessageIds ?? [] } private var currentSearchOptions: ChatListSearchOptions { return self.searchOptionsValue ?? ChatListSearchOptions(peer: nil, minDate: nil, maxDate: nil) } public override func searchTokensUpdated(tokens: [SearchBarToken]) { var updatedOptions = self.searchOptionsValue var tokensIdSet = Set() for token in tokens { tokensIdSet.insert(token.id) } if !tokensIdSet.contains(ChatListTokenId.date.rawValue) && updatedOptions?.maxDate != nil { updatedOptions = updatedOptions?.withUpdatedMaxDate(nil) } if !tokensIdSet.contains(ChatListTokenId.peer.rawValue) && updatedOptions?.peer != nil { updatedOptions = updatedOptions?.withUpdatedPeer(nil) } self.updateSearchOptions(updatedOptions) } private func updateSearchOptions(_ options: ChatListSearchOptions?, clearQuery: Bool = false) { var options = options if options?.isEmpty ?? true { options = nil } self.searchOptionsValue = options self.searchOptions.set(.single(options)) var tokens: [SearchBarToken] = [] if let (peerId, isGroup, peerName) = options?.peer { let image: UIImage? if isGroup { image = UIImage(bundleImageName: "Chat List/Search/Group") } else if peerId.namespace == Namespaces.Peer.CloudChannel { image = UIImage(bundleImageName: "Chat List/Search/Channel") } else { image = UIImage(bundleImageName: "Chat List/Search/User") } tokens.append(SearchBarToken(id: ChatListTokenId.peer.rawValue, icon:image, title: peerName)) } if let (_, dateTitle) = options?.maxDate { tokens.append(SearchBarToken(id: ChatListTokenId.date.rawValue, icon: UIImage(bundleImageName: "Chat List/Search/Calendar"), title: dateTitle)) self.suggestedDates.set(.single([])) } if clearQuery { self.setQuery?(nil, tokens, "") } else { self.setQuery?(nil, tokens, self.searchQueryValue ?? "") } } private func updateTheme(theme: PresentationTheme) { self.backgroundColor = self.peersFilter.contains(.excludeRecent) ? nil : theme.chatList.backgroundColor if let (layout, navigationBarHeight) = self.validLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) } } override public func searchTextUpdated(text: String) { let searchQuery: String? = !text.isEmpty ? text : nil self.searchQuery.set(.single(searchQuery)) self.searchQueryValue = searchQuery self.suggestedDates.set(.single(suggestDates(for: text, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat))) } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) self.validLayout = (layout, navigationBarHeight) let topInset = navigationBarHeight transition.updateFrame(node: self.filterContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight + 6.0), size: CGSize(width: layout.size.width, height: 38.0))) let filters: [ChatListSearchFilter] if let suggestedFilters = self.suggestedFilters, !suggestedFilters.isEmpty { filters = suggestedFilters } else { filters = [.chats, .media, .links, .files, .music, .voice] } self.filterContainerNode.update(size: CGSize(width: layout.size.width, height: 38.0), sideInset: layout.safeInsets.left - 20.0, filters: filters.map { .filter($0) }, selectedFilter: self.selectedFilterKey, transitionFraction: self.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring)) if let selectedMessageIds = self.stateValue.selectedMessageIds { var wasAdded = false let selectionPanelNode: ChatListSearchMessageSelectionPanelNode if let current = self.selectionPanelNode { selectionPanelNode = current } else { wasAdded = true selectionPanelNode = ChatListSearchMessageSelectionPanelNode(context: self.context, deleteMessages: { [weak self] in guard let strongSelf = self else { return } strongSelf.deleteMessages(messageIds: nil) }, shareMessages: { [weak self] in guard let strongSelf = self, let messageIds = strongSelf.stateValue.selectedMessageIds, !messageIds.isEmpty else { return } let _ = (strongSelf.context.account.postbox.transaction { transaction -> [Message] in var messages: [Message] = [] for id in messageIds { if let message = transaction.getMessage(id) { messages.append(message) } } return messages } |> deliverOnMainQueue).start(next: { messages in if let strongSelf = self, !messages.isEmpty { let shareController = ShareController(context: strongSelf.context, subject: .messages(messages.sorted(by: { lhs, rhs in return lhs.index < rhs.index })), externalShare: true, immediateExternalShare: true) strongSelf.dismissInput() strongSelf.present?(shareController, nil) } }) }, forwardMessages: { [weak self] in guard let strongSelf = self else { return } strongSelf.forwardMessages(messageIds: nil) }) selectionPanelNode.chatAvailableMessageActions = { [weak self] messageIds -> Signal in guard let strongSelf = self else { return .complete() } let (peers, messages) = strongSelf.currentMessages 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) } selectionPanelNode.selectedMessages = selectedMessageIds let panelHeight = selectionPanelNode.update(layout: layout, presentationData: self.presentationData, transition: wasAdded ? .immediate : transition) let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - panelHeight), size: CGSize(width: layout.size.width, height: panelHeight)) if wasAdded { selectionPanelNode.frame = panelFrame transition.animatePositionAdditive(node: selectionPanelNode, offset: CGPoint(x: 0.0, y: panelHeight)) } else { transition.updateFrame(node: selectionPanelNode, frame: panelFrame) } } else if let selectionPanelNode = self.selectionPanelNode { self.selectionPanelNode = nil transition.updateFrame(node: selectionPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height), size: selectionPanelNode.bounds.size), completion: { [weak selectionPanelNode] _ in selectionPanelNode?.removeFromSupernode() }) } transition.updateFrame(node: self.paneContainerNode, frame: CGRect(x: 0.0, y: topInset, width: layout.size.width, height: layout.size.height - topInset)) self.paneContainerNode.update(size: CGSize(width: layout.size.width, height: layout.size.height - topInset), sideInset: layout.safeInsets.left, bottomInset: layout.inputHeight ?? 0.0, visibleHeight: layout.size.height - topInset, presentationData: self.presentationData, transition: transition) } private var currentMessages: ([PeerId: Peer], [MessageId: Message]) { var peers: [PeerId: Peer] = [:] let messages: [MessageId: Message] = self.paneContainerNode.allCurrentMessages() for (_, message) in messages { for (_, peer) in message.peers { peers[peer.id] = peer } } return (peers, messages) } override public func previewViewAndActionAtLocation(_ location: CGPoint) -> (UIView, CGRect, Any)? { if let node = self.paneContainerNode.currentPane?.node { let adjustedLocation = self.convert(location, to: node) return self.paneContainerNode.currentPane?.node.previewViewAndActionAtLocation(adjustedLocation) } else { return nil } } override public func scrollToTop() { let _ = self.paneContainerNode.scrollToTop() } private func messageContextAction(_ message: Message, node: ASDisplayNode?, rect: CGRect?, gesture anyRecognizer: UIGestureRecognizer?) { guard let node = node as? ContextExtractedContentContainingNode else { return } let _ = storedMessageFromSearch(account: self.context.account, message: message).start() var linkForCopying: String? var currentSupernode: ASDisplayNode? = node while true { if currentSupernode == nil { break } else if let currentSupernode = currentSupernode as? ListMessageSnippetItemNode { linkForCopying = currentSupernode.currentPrimaryUrl break } else { currentSupernode = currentSupernode?.supernode } } let gesture: ContextGesture? = anyRecognizer as? ContextGesture let (peers, messages) = self.currentMessages let items = context.sharedContext.chatAvailableMessageActions(postbox: context.account.postbox, accountPeerId: context.account.peerId, messageIds: [message.id], messages: messages, peers: peers) |> map { actions -> [ContextMenuItem] in var items: [ContextMenuItem] = [] if let linkForCopying = linkForCopying { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuCopyLink, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { c, _ in c.dismiss(completion: {}) UIPasteboard.general.string = linkForCopying }))) } items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuForward, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in c.dismiss(completion: { [weak self] in if let strongSelf = self { strongSelf.forwardMessages(messageIds: Set([message.id])) } }) }))) items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in c.dismiss(completion: { [weak self] in self?.openMessage(message.peers[message.id.peerId]!, message.id) }) }))) items.append(.separator) items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuMore, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/More"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in c.dismiss(completion: { if let strongSelf = self { strongSelf.dismissInput() strongSelf.updateState { state in return state.withUpdatedSelectedMessageIds([message.id]) } if let (layout, navigationBarHeight) = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) } } }) }))) return items } let controller = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node)), items: items, reactionItems: [], recognizer: nil, gesture: gesture) self.presentInGlobalOverlay?(controller, nil) } private func mediaMessageContextAction(_ message: Message, node: ASDisplayNode?, rect: CGRect?, gesture anyRecognizer: UIGestureRecognizer?) { let gesture: ContextGesture? = anyRecognizer as? ContextGesture let _ = (chatMediaListPreviewControllerData(context: self.context, chatLocation: .peer(message.id.peerId), chatLocationContextHolder: Atomic(value: nil), message: message, standalone: true, reverseMessageGalleryOrder: false, navigationController: self.navigationController) |> deliverOnMainQueue).start(next: { [weak self] previewData in guard let strongSelf = self else { gesture?.cancel() return } if let previewData = previewData { let context = strongSelf.context let strings = strongSelf.presentationData.strings let (peers, messages) = strongSelf.currentMessages let items = context.sharedContext.chatAvailableMessageActions(postbox: context.account.postbox, accountPeerId: context.account.peerId, messageIds: [message.id], messages: messages, peers: peers) |> map { actions -> [ContextMenuItem] in var items: [ContextMenuItem] = [] items.append(.action(ContextMenuActionItem(text: strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { c, f in c.dismiss(completion: { self?.openMessage(message.peers[message.id.peerId]!, message.id) }) }))) items.append(.action(ContextMenuActionItem(text: strings.Conversation_ContextMenuForward, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { c, f in c.dismiss(completion: { if let strongSelf = self { strongSelf.forwardMessages(messageIds: [message.id]) } }) }))) items.append(.separator) items.append(.action(ContextMenuActionItem(text: strings.Conversation_ContextMenuMore, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/More"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in if let strongSelf = self { strongSelf.dismissInput() strongSelf.updateState { state in return state.withUpdatedSelectedMessageIds([message.id]) } if let (layout, navigationBarHeight) = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.3, curve: .easeInOut)) } } f(.default) }))) return items } switch previewData { case let .gallery(gallery): gallery.setHintWillBePresentedInPreviewingContext(true) let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceNode: node)), items: items, reactionItems: [], gesture: gesture) strongSelf.presentInGlobalOverlay?(contextController, nil) case .instantPage: break } } }) } public override func searchTextClearTokens() { self.updateSearchOptions(nil) self.setQuery?(nil, [], self.searchQueryValue ?? "") } func deleteMessages(messageIds: Set?) { let messageIds = messageIds ?? self.stateValue.selectedMessageIds } func forwardMessages(messageIds: Set?) { let messageIds = messageIds ?? self.stateValue.selectedMessageIds if let messageIds = messageIds, !messageIds.isEmpty { let peerSelectionController = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: [.onlyWriteable, .excludeDisabled])) peerSelectionController.peerSelected = { [weak self, weak peerSelectionController] peerId in if let strongSelf = self, let _ = peerSelectionController { if peerId == strongSelf.context.account.peerId { let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: messageIds.map { id -> EnqueueMessage in return .forward(source: id, grouping: .auto, attributes: []) }) |> deliverOnMainQueue).start(next: { [weak self] messageIds in if let strongSelf = self { let signals: [Signal] = messageIds.compactMap({ id -> Signal? in guard let id = id else { return nil } return strongSelf.context.account.pendingMessageManager.pendingMessageStatus(id) |> mapToSignal { status, _ -> Signal in if status != nil { return .never() } else { return .single(true) } } |> take(1) }) strongSelf.activeActionDisposable.set((combineLatest(signals) |> deliverOnMainQueue).start(completed: { guard let strongSelf = self else { return } strongSelf.present?(OverlayStatusController(theme: strongSelf.presentationData.theme, type: .success), nil) })) } }) if let peerSelectionController = peerSelectionController { peerSelectionController.dismiss() } strongSelf.updateState { state in return state.withUpdatedSelectedMessageIds(nil) } if let (layout, navigationBarHeight) = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) } } else { let _ = (strongSelf.context.account.postbox.transaction({ transaction -> Void in transaction.updatePeerChatInterfaceState(peerId, update: { currentState in if let currentState = currentState as? ChatInterfaceState { return currentState.withUpdatedForwardMessageIds(Array(messageIds)) } else { return ChatInterfaceState().withUpdatedForwardMessageIds(Array(messageIds)) } }) }) |> deliverOnMainQueue).start(completed: { if let strongSelf = self { // strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone) let controller = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(peerId), subject: nil, botStart: nil, mode: .standard(previewing: false)) strongSelf.navigationController?.pushViewController(controller, animated: false, completion: { if let peerSelectionController = peerSelectionController { peerSelectionController.dismiss() } }) strongSelf.updateState { state in return state.withUpdatedSelectedMessageIds(nil) } if let (layout, navigationBarHeight) = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) } } }) } } } self.navigationController?.pushViewController(peerSelectionController) } } private func dismissInput() { self.view.window?.endEditing(true) } } private final class MessageContextExtractedContentSource: ContextExtractedContentSource { let keepInPlace: Bool = false let ignoreContentTouches: Bool = true private let sourceNode: ContextExtractedContentContainingNode init(sourceNode: ContextExtractedContentContainingNode) { self.sourceNode = sourceNode } func takeView() -> ContextControllerTakeViewInfo? { return ContextControllerTakeViewInfo(contentContainingNode: self.sourceNode, contentAreaInScreenSpace: UIScreen.main.bounds) } func putBack() -> ContextControllerPutBackViewInfo? { return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) } } private final class ContextControllerContentSourceImpl: ContextControllerContentSource { let controller: ViewController weak var sourceNode: ASDisplayNode? let navigationController: NavigationController? = nil let passthroughTouches: Bool = false init(controller: ViewController, sourceNode: ASDisplayNode?) { self.controller = controller self.sourceNode = sourceNode } func transitionInfo() -> ContextControllerTakeControllerInfo? { let sourceNode = self.sourceNode return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in if let sourceNode = sourceNode { return (sourceNode, sourceNode.bounds) } else { return nil } }) } func animatedIn() { self.controller.didAppearInContextPreview() } }