diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 169cf0ff99..cf536336c5 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -10,6 +10,8 @@ "https://bcr.bazel.build/modules/abseil-cpp/20230802.1/MODULE.bazel": "fa92e2eb41a04df73cdabeec37107316f7e5272650f81d6cc096418fe647b915", "https://bcr.bazel.build/modules/abseil-cpp/20240116.1/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed", "https://bcr.bazel.build/modules/abseil-cpp/20240116.1/source.json": "9be551b8d4e3ef76875c0d744b5d6a504a27e3ae67bc6b28f46415fd2d2957da", + "https://bcr.bazel.build/modules/aexml/4.7.0/MODULE.bazel": "4030ff1555ade0956c08c74722851fcca0dc02ec7b8e7c61d0bbc4806ec4b2de", + "https://bcr.bazel.build/modules/aexml/4.7.0/source.json": "641c9de95dc10b8bf3685b5de9f9a84e7470ec3e40a09d7ddc9e3c9f2289a931", "https://bcr.bazel.build/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd", "https://bcr.bazel.build/modules/bazel_features/1.10.0/MODULE.bazel": "f75e8807570484a99be90abcd52b5e1f390362c258bcb73106f4544957a48101", "https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8", @@ -48,6 +50,8 @@ "https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902", "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/MODULE.bazel": "6f7b417dcc794d9add9e556673ad25cb3ba835224290f4f848f8e2db1e1fca74", "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/source.json": "f448c6e8963fdfa7eb831457df83ad63d3d6355018f6574fb017e8169deb43a9", + "https://bcr.bazel.build/modules/pathkit/1.0.1/MODULE.bazel": "fae93989a10f8d90d5ac02453e6632ae7f71111687862c01f468858cef40bb5e", + "https://bcr.bazel.build/modules/pathkit/1.0.1/source.json": "3215e6b4b08f96f34024eaf186d247744ca255925d7ee3f50cf94f7cf885696b", "https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5", "https://bcr.bazel.build/modules/platforms/0.0.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f", "https://bcr.bazel.build/modules/platforms/0.0.11/source.json": "f7e188b79ebedebfe75e9e1d098b8845226c7992b307e28e1496f23112e8fc29", @@ -144,6 +148,8 @@ "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/MODULE.bazel": "5e463fbfba7b1701d957555ed45097d7f984211330106ccd1352c6e0af0dcf91", "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/source.json": "32bd87e5f4d7acc57c5b2ff7c325ae3061d5e242c0c4c214ae87e0f1c13e54cb", "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43", + "https://bcr.bazel.build/modules/xcodeproj/8.27.3/MODULE.bazel": "49276599207dae3df1e4336c2067505323dfb0606b53ef63e144087d1226e0eb", + "https://bcr.bazel.build/modules/xcodeproj/8.27.3/source.json": "bbbb718187dcbdfbb3a9a0ec7d49446cdf48c67657cafd79b5cf33aa8918f608", "https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0", "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/MODULE.bazel": "af322bc08976524477c79d1e45e241b6efbeb918c497e8840b8ab116802dda79", "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/source.json": "2be409ac3c7601245958cd4fcdff4288be79ed23bd690b4b951f500d54ee6e7d", @@ -284,7 +290,7 @@ }, "@@rules_xcodeproj+//xcodeproj:extensions.bzl%internal": { "general": { - "bzlTransitiveDigest": "m7EcAC1RuIDErOmw2fxoQ7OGzFPZJtVcXgIIqZMO3OI=", + "bzlTransitiveDigest": "+kmqZtEKFY8zgqpV6mrwdQkTJqGUZhL8b3ZMsxrqSyc=", "usagesDigest": "fvsnMonVwKDYnBfww4bXuYie3WU0d9VSqT2gePSdQco=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -306,7 +312,7 @@ }, "@@rules_xcodeproj+//xcodeproj:extensions.bzl%non_module_deps": { "general": { - "bzlTransitiveDigest": "m7EcAC1RuIDErOmw2fxoQ7OGzFPZJtVcXgIIqZMO3OI=", + "bzlTransitiveDigest": "+kmqZtEKFY8zgqpV6mrwdQkTJqGUZhL8b3ZMsxrqSyc=", "usagesDigest": "jzxYhnOC9BE0dJ0biFLfxWXi/+R19uAAZkJ0p9CY0JI=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -339,6 +345,15 @@ "url": "https://github.com/apple/swift-argument-parser/archive/refs/tags/1.2.3.tar.gz" } }, + "com_github_tadija_aexml": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "build_file_content": "load(\"@build_bazel_rules_swift//swift:swift.bzl\", \"swift_library\")\n\nswift_library(\n name = \"AEXML\",\n srcs = glob([\"Sources/AEXML/**/*.swift\"]),\n visibility = [\"//visibility:public\"],\n)\n", + "sha256": "5a76c28e4fa9dcc1cbfb87a8518652628e990e522ecfbc98bdad17eabf4631d5", + "strip_prefix": "AEXML-4.6.1", + "url": "https://github.com/tadija/AEXML/archive/refs/tags/4.6.1.tar.gz" + } + }, "com_github_michaeleisel_jjliso8601dateformatter": { "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", "attributes": { diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index bdbab5249c..1d3ab0132f 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -719,6 +719,7 @@ public enum ChatListSearchFilter: Equatable { case topics case channels case apps + case globalPosts case media case downloads case links @@ -740,22 +741,24 @@ public enum ChatListSearchFilter: Equatable { return 2 case .apps: return 3 - case .media: + case .globalPosts: return 4 - case .downloads: + case .media: return 5 - case .links: + case .downloads: return 6 - case .files: + case .links: return 7 - case .music: + case .files: return 8 - case .voice: + case .music: return 9 - case .instantVideo: + case .voice: return 10 - case .publicPosts: + case .instantVideo: return 11 + case .publicPosts: + return 12 case let .peer(peerId, _, _, _): return peerId.id._internalGetInt64Value() case let .date(_, date, _): diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index 808dfa9b0d..9d17f95213 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -115,6 +115,7 @@ swift_library( "//submodules/TelegramUI/Components/AvatarUploadToastScreen", "//submodules/TelegramUI/Components/Ads/AdsInfoScreen", "//submodules/TelegramUI/Components/Ads/AdsReportScreen", + "//submodules/TelegramUI/Components/ButtonComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index e7c445e693..9d90ac12a8 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -345,6 +345,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo key = .channels case .apps: key = .apps + case .globalPosts: + key = .globalPosts case .media: key = .media case .downloads: @@ -388,7 +390,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo switch filter { case let .filter(filter): switch filter { - case .downloads, .channels, .apps: + case .downloads, .channels, .apps, .globalPosts: return false default: return true @@ -675,6 +677,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo filterKey = .channels case .apps: filterKey = .apps + case .globalPosts: + filterKey = .globalPosts case .media: filterKey = .media case .downloads: diff --git a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift index 05495508a2..d26010892c 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift @@ -13,6 +13,7 @@ private final class ItemNode: ASDisplayNode { private let iconNode: ASImageNode private let titleNode: ImmediateTextNode private let titleActiveNode: ImmediateTextNode + private var titleBadgeView: UIImageView? private let buttonNode: HighlightTrackingButtonNode private var selectionFraction: CGFloat = 0.0 @@ -74,6 +75,7 @@ private final class ItemNode: ASDisplayNode { self.selectionFraction = selectionFraction let title: String + var titleBadge: String? let icon: UIImage? let color = presentationData.theme.list.itemSecondaryTextColor @@ -90,6 +92,11 @@ private final class ItemNode: ASDisplayNode { case .apps: title = presentationData.strings.ChatList_Search_FilterApps icon = nil + case .globalPosts: + //TODO:localize + title = "Posts" + titleBadge = "NEW" + icon = nil case .media: title = presentationData.strings.ChatList_Search_FilterMedia icon = nil @@ -133,6 +140,38 @@ private final class ItemNode: ASDisplayNode { self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: color) self.titleActiveNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: presentationData.theme.list.itemAccentColor) + if let titleBadge { + let titleBadgeView: UIImageView + if let current = self.titleBadgeView { + titleBadgeView = current + } else { + titleBadgeView = UIImageView() + self.titleBadgeView = titleBadgeView + self.view.addSubview(titleBadgeView) + + let labelText = NSAttributedString(string: titleBadge, font: Font.medium(11.0), textColor: presentationData.theme.list.itemCheckColors.foregroundColor) + let labelBounds = labelText.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: [.usesLineFragmentOrigin], context: nil) + let labelSize = CGSize(width: ceil(labelBounds.width), height: ceil(labelBounds.height)) + let badgeSize = CGSize(width: labelSize.width + 8.0, height: labelSize.height + 2.0 + 1.0) + titleBadgeView.image = generateImage(badgeSize, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + let rect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height - UIScreenPixel * 2.0)) + + context.addPath(UIBezierPath(roundedRect: rect, cornerRadius: 5.0).cgPath) + context.setFillColor(presentationData.theme.list.itemCheckColors.fillColor.cgColor) + context.fillPath() + + UIGraphicsPushContext(context) + labelText.draw(at: CGPoint(x: 4.0, y: 1.0 + UIScreenPixel)) + UIGraphicsPopContext() + }) + } + } else if let titleBadgeView = self.titleBadgeView { + self.titleBadgeView = nil + titleBadgeView.removeFromSuperview() + } + let selectionAlpha: CGFloat = selectionFraction * selectionFraction let deselectionAlpha: CGFloat = 1.0// - selectionFraction transition.updateAlpha(node: self.titleNode, alpha: deselectionAlpha) @@ -163,8 +202,15 @@ private final class ItemNode: ASDisplayNode { let titleFrame = CGRect(origin: CGPoint(x: -self.titleNode.insets.left + iconInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize) self.titleNode.frame = titleFrame self.titleActiveNode.frame = titleFrame - - return titleSize.width - self.titleNode.insets.left - self.titleNode.insets.right + iconInset + + var width = titleSize.width - self.titleNode.insets.left - self.titleNode.insets.right + iconInset + + if let titleBadgeView = self.titleBadgeView, let image = titleBadgeView.image { + width += 4.0 + image.size.width + titleBadgeView.frame = CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: titleFrame.minY + floorToScreenPixels((titleFrame.height - image.size.height) * 0.5) + 1.0), size: image.size) + } + + return width } func updateArea(size: CGSize, sideInset: CGFloat, transition: ContainedViewLayoutTransition) { diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index cb4f254746..24e109925c 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -33,6 +33,10 @@ import FetchManagerImpl import AnimationCache import MultiAnimationRenderer import AvatarNode +import ComponentFlow +import MultilineTextComponent +import ButtonComponent +import BundleIconComponent private enum ChatListRecentEntryStableId: Hashable { case topPeers @@ -285,6 +289,9 @@ private enum ChatListRecentEntry: Comparable, Identifiable { header = ChatListSearchItemHeader(type: .text(presentationData.strings.ChatList_Search_SectionRecentApps, 0), theme: theme, strings: strings, actionTitle: nil, action: nil) } } + } else if case .globalPosts = key { + //TODO:localize + header = ChatListSearchItemHeader(type: .text("Public Posts", 0), theme: theme, strings: strings, actionTitle: nil, action: nil) } else { header = ChatListSearchItemHeader(type: .recentPeers, theme: theme, strings: strings, actionTitle: strings.WebSearch_RecentSectionClear, action: { _ in clearRecentlySearchedPeers() @@ -1111,23 +1118,30 @@ public enum ChatListSearchEntry: Comparable, Identifiable { } } else { var headerType: ChatListSearchItemHeaderType = .messages(location: nil) - if case let .forum(peerId) = location, let peer = peer.peer, peer.id == peerId { - headerType = .messages(location: peer.compactDisplayTitle) - } var actionTitle: String? - if case .generic = section { - let filterTitle: String - switch searchScope { - case .everywhere: - filterTitle = presentationData.strings.ChatList_Search_Messages_AllChats - case .channels: - filterTitle = presentationData.strings.ChatList_Search_Messages_Channels - case .groups: - filterTitle = presentationData.strings.ChatList_Search_Messages_GroupChats - case .privateChats: - filterTitle = presentationData.strings.ChatList_Search_Messages_PrivateChats + if key == .globalPosts { + headerType = .text("Public Posts", 0) + } else { + if case let .forum(peerId) = location, let peer = peer.peer, peer.id == peerId { + headerType = .messages(location: peer.compactDisplayTitle) + } + if case .generic = section { + let filterTitle: String + switch searchScope { + case .everywhere: + filterTitle = presentationData.strings.ChatList_Search_Messages_AllChats + case .channels: + filterTitle = presentationData.strings.ChatList_Search_Messages_Channels + case .groups: + filterTitle = presentationData.strings.ChatList_Search_Messages_GroupChats + case .privateChats: + filterTitle = presentationData.strings.ChatList_Search_Messages_PrivateChats + case .globalPosts: + //TODO:localize + filterTitle = "Public Posts" + } + actionTitle = "\(filterTitle) <" } - actionTitle = "\(filterTitle) <" } header = ChatListSearchItemHeader(type: headerType, theme: presentationData.theme, strings: presentationData.strings, actionTitle: actionTitle, action: { sourceNode in openMessagesFilter(sourceNode) @@ -1214,12 +1228,20 @@ public enum ChatListSearchEntry: Comparable, Identifiable { filterTitle = presentationData.strings.ChatList_Search_Messages_GroupChats case .privateChats: filterTitle = presentationData.strings.ChatList_Search_Messages_PrivateChats + case .globalPosts: + filterTitle = "Public Posts" } actionTitle = "\(filterTitle) <" - let header = ChatListSearchItemHeader(type: .messages(location: nil), theme: presentationData.theme, strings: presentationData.strings, actionTitle: actionTitle, action: { sourceNode in - openMessagesFilter(sourceNode) - }) + let header: ChatListSearchItemHeader + if key == .globalPosts { + //TODO:localize + header = ChatListSearchItemHeader(type: .text("Public Posts", 0), theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) + } else { + header = ChatListSearchItemHeader(type: .messages(location: nil), theme: presentationData.theme, strings: presentationData.strings, actionTitle: actionTitle, action: { sourceNode in + openMessagesFilter(sourceNode) + }) + } return ChatListItem(presentationData: presentationData, context: context, chatListLocation: location, filterData: nil, index: EngineChatList.Item.Index.chatList(ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: PeerId(0), namespace: Namespaces.Message.Cloud, id: 0), timestamp: 0))), content: .loading, editing: false, hasActiveRevealControls: false, selected: false, header: header, enabledContextActions: nil, hiddenOffset: false, interaction: interaction) case let .emptyMessagesFooter(presentationData, searchScope, searchQuery): var actionTitle: String? @@ -1233,6 +1255,9 @@ public enum ChatListSearchEntry: Comparable, Identifiable { filterTitle = presentationData.strings.ChatList_Search_Messages_GroupChats case .privateChats: filterTitle = presentationData.strings.ChatList_Search_Messages_PrivateChats + case .globalPosts: + //TODO:localize + filterTitle = "Public Posts" } actionTitle = "\(filterTitle) <" @@ -1271,15 +1296,21 @@ public struct ChatListSearchContainerTransition { public let isEmpty: Bool public let isLoading: Bool public let query: String? + public let approvedGlobalPostQuery: String? + public let remainingGlobalSearches: Int + public let globalSearchUnlockTimestamp: Int32? public var animated: Bool - public init(deletions: [ListViewDeleteItem], insertions: [ListViewInsertItem], updates: [ListViewUpdateItem], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, query: String?, animated: Bool) { + public init(deletions: [ListViewDeleteItem], insertions: [ListViewInsertItem], updates: [ListViewUpdateItem], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, query: String?, approvedGlobalPostQuery: String?, remainingGlobalSearches: Int, globalSearchUnlockTimestamp: Int32?, animated: Bool) { self.deletions = deletions self.insertions = insertions self.updates = updates self.displayingResults = displayingResults self.isEmpty = isEmpty self.isLoading = isLoading + self.approvedGlobalPostQuery = approvedGlobalPostQuery + self.remainingGlobalSearches = remainingGlobalSearches + self.globalSearchUnlockTimestamp = globalSearchUnlockTimestamp self.query = query self.animated = animated } @@ -1343,6 +1374,9 @@ public func chatListSearchContainerPreparedTransition( toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, + approvedGlobalPostQuery: String?, + remainingGlobalSearches: Int, + globalSearchUnlockTimestamp: Int32?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?, openClearRecentlyDownloaded: @escaping () -> Void, @@ -1358,7 +1392,7 @@ public func chatListSearchContainerPreparedTransition( let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories, openPublicPosts: openPublicPosts, openMessagesFilter: openMessagesFilter, switchMessagesFilter: switchMessagesFilter), directionHint: nil) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories, openPublicPosts: openPublicPosts, openMessagesFilter: openMessagesFilter, switchMessagesFilter: switchMessagesFilter), directionHint: nil) } - return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults, isEmpty: isEmpty, isLoading: isLoading, query: searchQuery, animated: animated) + return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults, isEmpty: isEmpty, isLoading: isLoading, query: searchQuery, approvedGlobalPostQuery: approvedGlobalPostQuery, remainingGlobalSearches: remainingGlobalSearches, globalSearchUnlockTimestamp: globalSearchUnlockTimestamp, animated: animated) } private struct ChatListSearchListPaneNodeState: Equatable { @@ -1456,7 +1490,7 @@ private struct DownloadItem: Equatable { private func filteredPeerSearchQueryResults(value: ([FoundPeer], [FoundPeer]), scope: TelegramSearchPeersScope) -> ([FoundPeer], [FoundPeer]) { switch scope { - case .everywhere, .privateChats, .groups: + case .everywhere, .privateChats, .groups, .globalPosts: return value case .channels: return ( @@ -1575,6 +1609,16 @@ final class GlobalPeerSearchContext { } } +struct TelegramGlobalPostSearchState: Equatable { + var remainingSearches: Int + var unlockTimestamp: Int32? + + init(remainingSearches: Int, unlockTimestamp: Int32?) { + self.remainingSearches = remainingSearches + self.unlockTimestamp = unlockTimestamp + } +} + final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private let context: AccountContext private let animationCache: AnimationCache @@ -1620,6 +1664,10 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private var searchQueryValue: String? private var searchOptionsValue: ChatListSearchOptions? + private var approvedGlobalPostQueryValue: String? + private var globalPostSearchStateValue: TelegramGlobalPostSearchState + private var globalPostSearchUnlockTimer: Foundation.Timer? + private var isPremium: Bool = false var isCurrent: Bool = false @@ -1642,6 +1690,10 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private let emptyResultsTextNode: ImmediateTextNode private let emptyResultsAnimationNode: AnimatedStickerNode private var emptyResultsAnimationSize = CGSize() + private var emptyResultsButton: ComponentView? + private var emptyResultsButtonSubtitle: ComponentView? + private var emptyResultsButtonContent: EmptyResultsButton.Content? + private var emptyResultsButtonSubtitleText: String? private var recentEmptyNode: ASDisplayNode? private var emptyRecentTitleNode: ImmediateTextNode? @@ -1668,10 +1720,14 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private var hiddenMediaDisposable: Disposable? private var searchQueryDisposable: Disposable? + private var approvedSearchQueryDisposable: Disposable? private var searchOptionsDisposable: Disposable? private let searchScopePromise = ValuePromise(.everywhere) + private let approvedGlobalPostQuery = ValuePromise(nil, ignoreRepeated: true) + private let globalPostSearchState = Promise() + init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, interaction: ChatListSearchInteraction, key: ChatListSearchPaneKey, peersFilter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?, parentController: ViewController?, globalPeerSearchContext: GlobalPeerSearchContext?) { self.context = context self.animationCache = animationCache @@ -1685,6 +1741,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let globalPeerSearchContext = globalPeerSearchContext ?? GlobalPeerSearchContext() self.globalPeerSearchContext = globalPeerSearchContext + + self.globalPostSearchStateValue = TelegramGlobalPostSearchState(remainingSearches: 2, unlockTimestamp: nil) var peersFilter = peersFilter if case .forum = location { @@ -1707,6 +1765,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { tagMask = nil case .apps: tagMask = nil + case .globalPosts: + tagMask = nil case .media: tagMask = .photoOrVideo case .downloads: @@ -1778,6 +1838,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { self.emptyResultsTextNode.displaysAsynchronously = false self.emptyResultsTextNode.maximumNumberOfLines = 0 self.emptyResultsTextNode.textAlignment = .center + self.emptyResultsTextNode.lineSpacing = 0.2 self.emptyResultsTextNode.isHidden = true self.emptyResultsAnimationNode = DefaultAnimatedStickerNodeImpl() @@ -1971,8 +2032,62 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let adsHiddenPromise = self.adsHiddenPromise - let foundItems: Signal<([ChatListSearchEntry], Bool)?, NoError> = combineLatest(queue: .mainQueue(), searchQuery, searchOptions, self.searchScopePromise.get(), downloadItems) - |> mapToSignal { [weak self] query, options, searchScope, downloadItems -> Signal<([ChatListSearchEntry], Bool)?, NoError> in + let isPremium = context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId) + ) + |> map { peer -> Bool in + guard case let .user(user) = peer else { + return false + } + return user.isPremium + } + |> distinctUntilChanged + + let globalPostSearchStateType = self.globalPostSearchState.get() + |> map { state -> Bool in + return state.unlockTimestamp != nil + } + |> distinctUntilChanged + + struct FoundRemoteMessages { + var messages: [EngineMessage] + var readCounters: [EnginePeer.Id: EnginePeerReadCounters] + var threadsData: [EngineMessage.Id: MessageHistoryThreadData] + var totalCount: Int32 + + init(messages: [EngineMessage], readCounters: [EnginePeer.Id: EnginePeerReadCounters], threadsData: [EngineMessage.Id: MessageHistoryThreadData], totalCount: Int32) { + self.messages = messages + self.readCounters = readCounters + self.threadsData = threadsData + self.totalCount = totalCount + } + } + + var defaultFoundRemoteMessagesSignal: Signal<([FoundRemoteMessages], Bool), NoError> = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) + if key == .globalPosts { + let searchSignal = context.engine.messages.searchMessages(location: .general(scope: .globalPosts, tags: nil, minDate: nil, maxDate: nil), query: "", state: nil, limit: 50) + |> map { resultData -> ChatListSearchMessagesResult in + let (result, updatedState) = resultData + + return ChatListSearchMessagesResult(query: "", messages: result.messages.map({ EngineMessage($0) }).sorted(by: { $0.index > $1.index }), readStates: result.readStates.mapValues { EnginePeerReadCounters(state: $0, isMuted: false) }, threadInfo: result.threadInfo, hasMore: !result.completed, totalCount: result.totalCount, state: updatedState) + } + + defaultFoundRemoteMessagesSignal = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], true)) + |> then( + searchSignal + |> map { foundMessages -> ([FoundRemoteMessages], Bool) in + var result: [FoundRemoteMessages] = [] + result.append(FoundRemoteMessages(messages: foundMessages.messages, readCounters: foundMessages.readStates, threadsData: foundMessages.threadInfo, totalCount: foundMessages.totalCount)) + return (result, false) + } + ) + } + + let defaultFoundRemoteMessages = Promise<([FoundRemoteMessages], Bool)>() + defaultFoundRemoteMessages.set(defaultFoundRemoteMessagesSignal) + + let foundItems: Signal<([ChatListSearchEntry], Bool)?, NoError> = combineLatest(queue: .mainQueue(), searchQuery, self.approvedGlobalPostQuery.get(), searchOptions, self.searchScopePromise.get(), downloadItems, globalPostSearchStateType) + |> mapToSignal { [weak self] query, approvedGlobalPostQuery, options, searchScope, downloadItems, _ -> Signal<([ChatListSearchEntry], Bool)?, NoError> in if query == nil && options == nil && [.chats, .topics, .channels, .apps].contains(key) { let _ = currentRemotePeers.swap(nil) return .single(nil) @@ -2496,20 +2611,6 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { return ([:], true) } - struct FoundRemoteMessages { - var messages: [EngineMessage] - var readCounters: [EnginePeer.Id: EnginePeerReadCounters] - var threadsData: [EngineMessage.Id: MessageHistoryThreadData] - var totalCount: Int32 - - init(messages: [EngineMessage], readCounters: [EnginePeer.Id: EnginePeerReadCounters], threadsData: [EngineMessage.Id: MessageHistoryThreadData], totalCount: Int32) { - self.messages = messages - self.readCounters = readCounters - self.threadsData = threadsData - self.totalCount = totalCount - } - } - let foundPublicMessages: Signal<([FoundRemoteMessages], Bool), NoError> if key == .chats || key == .publicPosts, let query, query.hasPrefix("#") { let searchSignal = context.engine.messages.searchHashtagPosts(hashtag: finalQuery, state: nil, limit: 10) @@ -2584,13 +2685,29 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { foundRemoteMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) } else if key == .apps { foundRemoteMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) + } else if key == .globalPosts && (finalQuery.isEmpty || approvedGlobalPostQuery != finalQuery) { + if finalQuery.isEmpty { + foundRemoteMessages = defaultFoundRemoteMessages.get() + } else { + foundRemoteMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) + } } else { if !finalQuery.isEmpty { addAppLogEvent(postbox: context.account.postbox, type: "search_global_query") } - let searchSignals: [Signal<(SearchMessagesResult, SearchMessagesState), NoError>] = searchLocations.map { searchLocation in - return context.engine.messages.searchMessages(location: searchLocation, query: finalQuery, state: nil, limit: 50) + let searchSignals: [Signal<(SearchMessagesResult, SearchMessagesState), NoError>] + + if key == .globalPosts { + #if DEBUG && false + searchSignals = [context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: nil, minDate: nil, maxDate: nil), query: finalQuery, state: nil, limit: 50)] + #else + searchSignals = [context.engine.messages.searchMessages(location: .general(scope: .globalPosts, tags: nil, minDate: nil, maxDate: nil), query: finalQuery, state: nil, limit: 50)] + #endif + } else { + searchSignals = searchLocations.map { searchLocation in + return context.engine.messages.searchMessages(location: searchLocation, query: finalQuery, state: nil, limit: 50) + } } let searchSignal = combineLatest(searchSignals) @@ -3486,10 +3603,60 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { self.searchQueryDisposable = (searchQuery |> deliverOnMainQueue).startStrict(next: { [weak self, weak listInteraction, weak chatListInteraction] query in - self?.searchQueryValue = query + guard let self else { + return + } + + if let searchQueryValue = self.searchQueryValue, searchQueryValue == self.approvedGlobalPostQueryValue { + self.approvedGlobalPostQuery.set(nil) + } + self.searchQueryValue = query + listInteraction?.searchTextHighightState = query chatListInteraction?.searchTextHighightState = query }) + self.globalPostSearchState.set(.single(self.globalPostSearchStateValue)) + self.approvedSearchQueryDisposable = (combineLatest(queue: .mainQueue(), self.approvedGlobalPostQuery.get(), self.globalPostSearchState.get(), isPremium) + |> deliverOnMainQueue).startStrict(next: { [weak self] approvedGlobalPostQuery, globalPostSearchState, isPremium in + guard let self else { + return + } + self.approvedGlobalPostQueryValue = approvedGlobalPostQuery + self.globalPostSearchStateValue = globalPostSearchState + self.isPremium = isPremium + + if globalPostSearchState.unlockTimestamp != nil { + if self.globalPostSearchUnlockTimer == nil { + self.globalPostSearchUnlockTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self] _ in + guard let self else { + return + } + + if let unlockTimestamp = self.globalPostSearchStateValue.unlockTimestamp { + var remainingTime: Int32 = unlockTimestamp - Int32(Date().timeIntervalSince1970) + remainingTime = max(0, remainingTime) + if remainingTime == 0 { + if let globalPostSearchUnlockTimer = self.globalPostSearchUnlockTimer { + self.globalPostSearchUnlockTimer = nil + globalPostSearchUnlockTimer.invalidate() + } + + //TODO:localize + var globalPostSearchState = self.globalPostSearchStateValue + globalPostSearchState.unlockTimestamp = nil + globalPostSearchState.remainingSearches = 2 + self.globalPostSearchState.set(.single(globalPostSearchState)) + } + } + }) + } + } else { + if let globalPostSearchUnlockTimer = self.globalPostSearchUnlockTimer { + self.globalPostSearchUnlockTimer = nil + globalPostSearchUnlockTimer.invalidate() + } + } + }) self.searchOptionsDisposable = (searchOptions |> deliverOnMainQueue).startStrict(next: { [weak self] options in @@ -3635,7 +3802,12 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { return state } }, searchPeer: { peer in - }, searchQuery: strongSelf.searchQueryValue, searchOptions: strongSelf.searchOptionsValue, messageContextAction: { message, node, rect, gesture, paneKey, downloadResource in + }, + searchQuery: strongSelf.searchQueryValue, + approvedGlobalPostQuery: strongSelf.approvedGlobalPostQueryValue, + remainingGlobalSearches: strongSelf.globalPostSearchStateValue.remainingSearches, + globalSearchUnlockTimestamp: strongSelf.globalPostSearchStateValue.unlockTimestamp, + searchOptions: strongSelf.searchOptionsValue, messageContextAction: { message, node, rect, gesture, paneKey, downloadResource in interaction.messageContextAction(message, node, rect, gesture, paneKey, downloadResource) }, openClearRecentlyDownloaded: { guard let strongSelf = self else { @@ -4508,7 +4680,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { self.updatedRecentPeersDisposable.dispose() self.deletedMessagesDisposable?.dispose() self.searchQueryDisposable?.dispose() + self.approvedSearchQueryDisposable?.dispose() self.searchOptionsDisposable?.dispose() + self.globalPostSearchUnlockTimer?.invalidate() } override func didLoad() { @@ -4898,15 +5072,150 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let emptyAnimationHeight = self.emptyResultsAnimationSize.height let emptyAnimationSpacing: CGFloat = 8.0 - let emptyTextSpacing: CGFloat = 8.0 - let emptyTotalHeight = emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSize.height + emptyTextSpacing + let emptyTextSpacing: CGFloat + let emptyButtonSpacing: CGFloat = 15.0 + let emptyButtonSubtitleSpacing: CGFloat = 12.0 + + var displayEmptyAnimation = true + if self.key == .globalPosts && self.emptyResultsButtonContent != nil { + displayEmptyAnimation = false + emptyTextSpacing = 18.0 + } else { + emptyTextSpacing = 8.0 + } + + var emptyButtonSize: CGSize? + var emptyButtonSubtitleSize: CGSize? + + if let emptyResultsButtonContent = self.emptyResultsButtonContent { + let emptyResultsButton: ComponentView + if let current = self.emptyResultsButton { + emptyResultsButton = current + } else { + emptyResultsButton = ComponentView() + self.emptyResultsButton = emptyResultsButton + } + + let emptyResultsButtonSizeValue = emptyResultsButton.update( + transition: .immediate, + component: AnyComponent(EmptyResultsButton( + theme: self.presentationData.theme, + strings: self.presentationData.strings, + content: emptyResultsButtonContent, + action: { [weak self] in + guard let self else { + return + } + if !self.isPremium { + guard let navigationController = self.navigationController else { + return + } + let context = self.context + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .ads, forceDark: false, dismissed: nil) + navigationController.pushViewController(controller) + } else { + if let searchQueryValue = self.searchQueryValue, !searchQueryValue.isEmpty, self.approvedGlobalPostQueryValue != searchQueryValue { + self.approvedGlobalPostQuery.set(searchQueryValue) + + var globalPostSearchStateValue = self.globalPostSearchStateValue + if globalPostSearchStateValue.remainingSearches > 0 { + globalPostSearchStateValue.remainingSearches -= 1 + } + if globalPostSearchStateValue.remainingSearches == 0 { + globalPostSearchStateValue.unlockTimestamp = Int32(Date().timeIntervalSince1970) + 30 + } + self.globalPostSearchState.set(.single(globalPostSearchStateValue)) + } + } + } + )), + environment: {}, + containerSize: CGSize(width: min(282.0, size.width - sideInset - 24.0 * 2.0), height: 50.0) + ) + emptyButtonSize = emptyResultsButtonSizeValue + } else { + if let emptyResultsButton = self.emptyResultsButton { + self.emptyResultsButton = nil + emptyResultsButton.view?.removeFromSuperview() + } + } + + if let emptyResultsButtonSubtitleText = self.emptyResultsButtonSubtitleText { + let emptyResultsButtonSubtitle: ComponentView + if let current = self.emptyResultsButtonSubtitle { + emptyResultsButtonSubtitle = current + } else { + emptyResultsButtonSubtitle = ComponentView() + self.emptyResultsButtonSubtitle = emptyResultsButtonSubtitle + } + + let emptyResultsButtonSubtitleSizeValue = emptyResultsButtonSubtitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: emptyResultsButtonSubtitleText, + font: Font.regular(13.0), + textColor: self.presentationData.theme.list.freeTextColor + )), + horizontalAlignment: .center + )), + environment: {}, + containerSize: CGSize(width: size.width - sideInset - 24.0 * 2.0, height: 100.0) + ) + emptyButtonSubtitleSize = emptyResultsButtonSubtitleSizeValue + } else { + if let emptyResultsButtonSubtitle = self.emptyResultsButtonSubtitle { + self.emptyResultsButtonSubtitle = nil + emptyResultsButtonSubtitle.view?.removeFromSuperview() + } + } + + var emptyTotalHeight = emptyTitleSize.height + emptyTextSize.height + emptyTextSpacing + if displayEmptyAnimation { + emptyTotalHeight += emptyAnimationHeight + emptyAnimationSpacing + } + if let emptyButtonSize { + emptyTotalHeight += emptyButtonSize.height + emptyButtonSpacing + } + if let emptyButtonSubtitleSize { + emptyTotalHeight += emptyButtonSubtitleSize.height + emptyButtonSubtitleSpacing + } + let emptyAnimationY = topInset + floorToScreenPixels((visibleHeight - topInset - bottomInset - emptyTotalHeight) / 2.0) + let emptyTitleY: CGFloat + if displayEmptyAnimation { + emptyTitleY = emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing + } else { + emptyTitleY = emptyAnimationY + } let textTransition = ContainedViewLayoutTransition.immediate textTransition.updateFrame(node: self.emptyResultsAnimationNode, frame: CGRect(origin: CGPoint(x: sideInset + padding + (size.width - sideInset * 2.0 - padding * 2.0 - self.emptyResultsAnimationSize.width) / 2.0, y: emptyAnimationY), size: self.emptyResultsAnimationSize)) - textTransition.updateFrame(node: self.emptyResultsTitleNode, frame: CGRect(origin: CGPoint(x: sideInset + padding + (size.width - sideInset * 2.0 - padding * 2.0 - emptyTitleSize.width) / 2.0, y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing), size: emptyTitleSize)) - textTransition.updateFrame(node: self.emptyResultsTextNode, frame: CGRect(origin: CGPoint(x: sideInset + padding + (size.width - sideInset * 2.0 - padding * 2.0 - emptyTextSize.width) / 2.0, y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSpacing), size: emptyTextSize)) + textTransition.updateFrame(node: self.emptyResultsTitleNode, frame: CGRect(origin: CGPoint(x: sideInset + padding + (size.width - sideInset * 2.0 - padding * 2.0 - emptyTitleSize.width) / 2.0, y: emptyTitleY), size: emptyTitleSize)) + textTransition.updateFrame(node: self.emptyResultsTextNode, frame: CGRect(origin: CGPoint(x: sideInset + padding + (size.width - sideInset * 2.0 - padding * 2.0 - emptyTextSize.width) / 2.0, y: emptyTitleY + emptyTitleSize.height + emptyTextSpacing), size: emptyTextSize)) self.emptyResultsAnimationNode.updateLayout(size: self.emptyResultsAnimationSize) + + var nextY: CGFloat = emptyTitleY + emptyTitleSize.height + emptyTextSpacing + emptyTextSize.height + if let emptyButtonView = self.emptyResultsButton?.view, let emptyButtonSize { + nextY += emptyButtonSpacing + + if emptyButtonView.superview == nil { + self.view.insertSubview(emptyButtonView, aboveSubview: self.emptyResultsTextNode.view) + } + emptyButtonView.frame = CGRect(origin: CGPoint(x: floor((size.width - emptyButtonSize.width) * 0.5), y: nextY), size: emptyButtonSize) + + nextY += emptyButtonSize.height + } + if let emptyButtonSubtitleView = self.emptyResultsButtonSubtitle?.view, let emptyButtonSubtitleSize { + nextY += emptyButtonSubtitleSpacing + + if emptyButtonSubtitleView.superview == nil { + self.view.insertSubview(emptyButtonSubtitleView, aboveSubview: self.emptyResultsTextNode.view) + } + emptyButtonSubtitleView.frame = CGRect(origin: CGPoint(x: floor((size.width - emptyButtonSubtitleSize.width) * 0.5), y: nextY), size: emptyButtonSubtitleSize) + + nextY += emptyButtonSubtitleSize.height + } } if !hadValidLayout { @@ -5055,33 +5364,101 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { if emptyResults { let emptyResultsTitle: String let emptyResultsText: String - if let query = transition.query, !query.isEmpty { - emptyResultsTitle = strongSelf.presentationData.strings.ChatList_Search_NoResults - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsQueryDescription(query).string - } else { - if let searchOptions = searchOptions, searchOptions.date == nil && searchOptions.peer == nil { - emptyResultsTitle = strongSelf.presentationData.strings.ChatList_Search_NoResultsFilter - if strongSelf.tagMask == .photoOrVideo { - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerMedia - } else if strongSelf.tagMask == .webPage { - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerLinks - } else if strongSelf.tagMask == .file { - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerFiles - } else if strongSelf.tagMask == .music { - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerMusic - } else if strongSelf.tagMask == .voiceOrInstantVideo { - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerVoice + var emptyResultsButtonContent: EmptyResultsButton.Content? + var emptyResultsButtonSubtitleText: String? + + if strongSelf.key == .globalPosts { + if !strongSelf.isPremium { + emptyResultsButtonContent = .premiumRequired + emptyResultsTitle = "Global Search" + emptyResultsText = "Type a keyword to search all posts\nfrom public channels." + emptyResultsButtonSubtitleText = "Global search is a Premium feature." + } else if let query = transition.query, !query.isEmpty { + if transition.approvedGlobalPostQuery == query { + emptyResultsButtonContent = nil + emptyResultsTitle = strongSelf.presentationData.strings.ChatList_Search_NoResults + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsQueryDescription(query).string } else { - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsDescription + if transition.remainingGlobalSearches != 0 { + emptyResultsTitle = "Global Search" + emptyResultsText = "Type a keyword to search all posts\nfrom public channels." + if transition.remainingGlobalSearches == 1 { + emptyResultsButtonSubtitleText = "1 free search remaining today." + } else { + emptyResultsButtonSubtitleText = "\(transition.remainingGlobalSearches) free searches remaining today." + } + + emptyResultsButtonContent = .searchQuery(query) + } else { + emptyResultsTitle = "Limit Reached" + emptyResultsText = "You can make up to\n10 search queries per day." + + emptyResultsButtonContent = .paidSearch( + price: 10, + timestamp: transition.globalSearchUnlockTimestamp + ) + } } } else { + if transition.remainingGlobalSearches != 0 { + emptyResultsButtonContent = .searchEmpty + emptyResultsTitle = "Global Search" + emptyResultsText = "Type a keyword to search all posts\nfrom public channels." + + if transition.remainingGlobalSearches == 1 { + emptyResultsButtonSubtitleText = "1 free search remaining today." + } else { + emptyResultsButtonSubtitleText = "\(transition.remainingGlobalSearches) free searches remaining today." + } + } else { + emptyResultsTitle = "Limit Reached" + emptyResultsText = "You can make up to\n10 search queries per day." + + emptyResultsButtonContent = .paidSearch( + price: 10, + timestamp: transition.globalSearchUnlockTimestamp + ) + } + } + } else { + if let query = transition.query, !query.isEmpty { emptyResultsTitle = strongSelf.presentationData.strings.ChatList_Search_NoResults - emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsDescription + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsQueryDescription(query).string + } else { + if let searchOptions = searchOptions, searchOptions.date == nil && searchOptions.peer == nil { + emptyResultsTitle = strongSelf.presentationData.strings.ChatList_Search_NoResultsFilter + if strongSelf.tagMask == .photoOrVideo { + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerMedia + } else if strongSelf.tagMask == .webPage { + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerLinks + } else if strongSelf.tagMask == .file { + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerFiles + } else if strongSelf.tagMask == .music { + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerMusic + } else if strongSelf.tagMask == .voiceOrInstantVideo { + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsFitlerVoice + } else { + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsDescription + } + } else { + emptyResultsTitle = strongSelf.presentationData.strings.ChatList_Search_NoResults + emptyResultsText = strongSelf.presentationData.strings.ChatList_Search_NoResultsDescription + } } } - strongSelf.emptyResultsTitleNode.attributedText = NSAttributedString(string: emptyResultsTitle, font: Font.semibold(17.0), textColor: strongSelf.presentationData.theme.list.freeTextColor) - strongSelf.emptyResultsTextNode.attributedText = NSAttributedString(string: emptyResultsText, font: Font.regular(15.0), textColor: strongSelf.presentationData.theme.list.freeTextColor) + if strongSelf.key == .globalPosts { + strongSelf.emptyResultsTitleNode.attributedText = NSAttributedString(string: emptyResultsTitle, font: Font.semibold(17.0), textColor: strongSelf.presentationData.theme.list.itemPrimaryTextColor) + strongSelf.emptyResultsTextNode.attributedText = NSAttributedString(string: emptyResultsText, font: Font.regular(15.0), textColor: strongSelf.presentationData.theme.list.freeTextColor) + } else { + strongSelf.emptyResultsTitleNode.attributedText = NSAttributedString(string: emptyResultsTitle, font: Font.semibold(17.0), textColor: strongSelf.presentationData.theme.list.freeTextColor) + strongSelf.emptyResultsTextNode.attributedText = NSAttributedString(string: emptyResultsText, font: Font.regular(15.0), textColor: strongSelf.presentationData.theme.list.freeTextColor) + } + strongSelf.emptyResultsButtonContent = emptyResultsButtonContent + strongSelf.emptyResultsButtonSubtitleText = emptyResultsButtonSubtitleText + } else { + strongSelf.emptyResultsButtonContent = nil + strongSelf.emptyResultsButtonSubtitleText = nil } if let (size, sideInset, bottomInset, visibleHeight, presentationData) = strongSelf.currentParams { @@ -5093,11 +5470,22 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { strongSelf.emptyResultsTitleNode.isHidden = true strongSelf.emptyResultsTextNode.isHidden = true strongSelf.emptyResultsAnimationNode.visibility = false + strongSelf.emptyResultsButton?.view?.isHidden = true + strongSelf.emptyResultsButtonSubtitle?.view?.isHidden = true + } else if strongSelf.key == .globalPosts { + strongSelf.emptyResultsAnimationNode.isHidden = !emptyResults || strongSelf.emptyResultsButtonContent != nil + strongSelf.emptyResultsTitleNode.isHidden = !emptyResults + strongSelf.emptyResultsTextNode.isHidden = !emptyResults + strongSelf.emptyResultsAnimationNode.visibility = emptyResults + strongSelf.emptyResultsButton?.view?.isHidden = !emptyResults + strongSelf.emptyResultsButtonSubtitle?.view?.isHidden = !emptyResults } else { strongSelf.emptyResultsAnimationNode.isHidden = !emptyResults strongSelf.emptyResultsTitleNode.isHidden = !emptyResults strongSelf.emptyResultsTextNode.isHidden = !emptyResults strongSelf.emptyResultsAnimationNode.visibility = emptyResults + strongSelf.emptyResultsButton?.view?.isHidden = emptyResults + strongSelf.emptyResultsButtonSubtitle?.view?.isHidden = emptyResults } var displayPlaceholder = transition.isLoading && (![.chats, .topics, .channels, .apps].contains(strongSelf.key) || (strongSelf.currentEntries?.isEmpty ?? true)) @@ -5393,7 +5781,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode { let items = (0 ..< 2).compactMap { _ -> ListViewItem? in switch key { - case .chats, .topics, .channels, .apps, .downloads, .publicPosts: + case .chats, .topics, .channels, .apps, .globalPosts, .downloads, .publicPosts: let message = EngineMessage( stableId: 0, stableVersion: 0, @@ -5738,3 +6126,389 @@ private final class ChatListSearchReferenceContentSource: ContextReferenceConten return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: .bottom) } } + +private final class EmptyResultsButtonSearchContent: Component { + let theme: PresentationTheme + let strings: PresentationStrings + let query: String + + init( + theme: PresentationTheme, + strings: PresentationStrings, + query: String + ) { + self.theme = theme + self.strings = strings + self.query = query + } + + static func ==(lhs: EmptyResultsButtonSearchContent, rhs: EmptyResultsButtonSearchContent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.query != rhs.query { + return false + } + return true + } + + final class View: UIView { + private let icon = ComponentView() + private let text = ComponentView() + private let arrow = ComponentView() + + private var component: EmptyResultsButtonSearchContent? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: EmptyResultsButtonSearchContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let iconSpacing: CGFloat = 0.0 + let arrowSpacing: CGFloat = 0.0 + + self.component = component + + let iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent( + name: "Peer Info/ButtonSearch", + tintColor: component.theme.list.itemCheckColors.foregroundColor, + scaleFactor: 0.8 + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + + let arrowSize = self.arrow.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent( + name: "Item List/DisclosureArrow", + tintColor: component.theme.list.itemCheckColors.foregroundColor, + scaleFactor: 1.0 + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + + //TODO:localize + let string = NSMutableAttributedString() + string.append(NSAttributedString(string: "Search ", font: Font.semibold(17.0), textColor: component.theme.list.itemCheckColors.foregroundColor)) + string.append(NSAttributedString(string: component.query, font: Font.semibold(17.0), textColor: component.theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.7))) + + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(string) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - iconSize.width - iconSpacing - arrowSize.width - arrowSpacing, height: 100.0) + ) + let textFrame = CGRect(origin: CGPoint(x: iconSize.width + iconSpacing, y: 0.0), size: textSize) + if let textView = self.text.view { + if textView.superview == nil { + self.addSubview(textView) + } + textView.frame = textFrame + } + + if let iconView = self.icon.view { + if iconView.superview == nil { + self.addSubview(iconView) + } + iconView.frame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((textSize.height - iconSize.height) * 0.5)), size: iconSize) + } + + if let arrowView = self.arrow.view { + if arrowView.superview == nil { + self.addSubview(arrowView) + } + arrowView.frame = CGRect(origin: CGPoint(x: textFrame.maxX + arrowSpacing, y: floorToScreenPixels((textSize.height - arrowSize.height) * 0.5)), size: arrowSize) + } + + return CGSize(width: iconSize.width + iconSpacing + textSize.width + arrowSpacing + arrowSize.width, height: textSize.height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class EmptyResultsButtonPaidSearchContent: Component { + let theme: PresentationTheme + let strings: PresentationStrings + let price: Int + let unlockTimestamp: Int32? + + init( + theme: PresentationTheme, + strings: PresentationStrings, + price: Int, + unlockTimestamp: Int32? + ) { + self.theme = theme + self.strings = strings + self.price = price + self.unlockTimestamp = unlockTimestamp + } + + static func ==(lhs: EmptyResultsButtonPaidSearchContent, rhs: EmptyResultsButtonPaidSearchContent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.price != rhs.price { + return false + } + if lhs.unlockTimestamp != rhs.unlockTimestamp { + return false + } + return true + } + + final class View: UIView { + private let title = ComponentView() + private let subtitle = ComponentView() + + private var component: EmptyResultsButtonPaidSearchContent? + private var timer: Foundation.Timer? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: EmptyResultsButtonPaidSearchContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let subtitleSpacing: CGFloat = 1.0 + + self.component = component + self.state = state + + //TODO:localize + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "Search for \(component.price) Stars", font: Font.semibold(17.0), textColor: component.theme.list.itemCheckColors.foregroundColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + + var subtitleText = "" + if let unlockTimestamp = component.unlockTimestamp { + var remainingTime: Int32 = unlockTimestamp - Int32(Date().timeIntervalSince1970) + remainingTime = max(0, remainingTime) + subtitleText = "free search unlocks in \(stringForRemainingTime(remainingTime))" + + if self.timer == nil { + self.timer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self] _ in + guard let self else { + return + } + self.state?.updated(transition: .immediate) + }) + } + } else { + if let timer = self.timer { + self.timer = nil + timer.invalidate() + } + } + + let subtitleSize = self.subtitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: subtitleText, font: Font.regular(12.0), textColor: component.theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.7))) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + + var contentSize = CGSize() + if subtitleText.isEmpty { + contentSize = titleSize + } else { + contentSize.width = max(titleSize.width, subtitleSize.width) + contentSize.height = titleSize.height + subtitleSpacing + subtitleSize.height + } + + let titleFrame = CGRect(origin: CGPoint(x: floor((contentSize.width - titleSize.width)), y: 0.0), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.frame = titleFrame + } + + let subtitleFrame = CGRect(origin: CGPoint(x: floor((contentSize.width - subtitleSize.width)), y: titleFrame.maxY + subtitleSpacing), size: subtitleSize) + if let subtitleView = self.subtitle.view { + if subtitleView.superview == nil { + self.addSubview(subtitleView) + } + subtitleView.frame = subtitleFrame + } + + return contentSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class EmptyResultsButton: Component { + enum Content: Equatable { + case searchQuery(String) + case searchEmpty + case premiumRequired + case paidSearch(price: Int, timestamp: Int32?) + } + + let theme: PresentationTheme + let strings: PresentationStrings + let content: Content + let action: () -> Void + + init( + theme: PresentationTheme, + strings: PresentationStrings, + content: Content, + action: @escaping () -> Void + ) { + self.theme = theme + self.strings = strings + self.content = content + self.action = action + } + + static func ==(lhs: EmptyResultsButton, rhs: EmptyResultsButton) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.content != rhs.content { + return false + } + return true + } + + final class View: UIView { + private let button = ComponentView() + + private var component: EmptyResultsButton? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: EmptyResultsButton, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let buttonContent: AnyComponentWithIdentity + var isEnabled = true + switch component.content { + case .searchEmpty: + isEnabled = false + buttonContent = AnyComponentWithIdentity(id: "empty", component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Search", + font: Font.medium(17.0), + textColor: component.theme.list.itemCheckColors.foregroundColor + )) + ))) + case .premiumRequired: + buttonContent = AnyComponentWithIdentity(id: "premium", component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Subscribe to Premium", + font: Font.medium(17.0), + textColor: component.theme.list.itemCheckColors.foregroundColor + )) + ))) + case let .searchQuery(query): + buttonContent = AnyComponentWithIdentity(id: "query", component: AnyComponent(EmptyResultsButtonSearchContent(theme: component.theme, strings: component.strings, query: query))) + case let .paidSearch(price, unlockTimestamp): + buttonContent = AnyComponentWithIdentity(id: "paid", component: AnyComponent(EmptyResultsButtonPaidSearchContent(theme: component.theme, strings: component.strings, price: price, unlockTimestamp: unlockTimestamp))) + } + + let size = self.button.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: component.theme.list.itemCheckColors.fillColor, + foreground: component.theme.list.itemCheckColors.foregroundColor, + pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), + cornerRadius: 10.0 + ), + content: buttonContent, + isEnabled: isEnabled, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.action() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 50.0) + ) + if let buttonView = self.button.view { + if buttonView.superview == nil { + self.addSubview(buttonView) + } + transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(), size: size)) + } + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private func stringForRemainingTime(_ duration: Int32) -> String { + let hours = duration / 3600 + let minutes = duration / 60 % 60 + let seconds = duration % 60 + let durationString: String + if hours > 0 { + durationString = String(format: "%d:%02d", hours, minutes) + } else { + durationString = String(format: "%02d:%02d", minutes, seconds) + } + return durationString +} diff --git a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift index ef5021db86..ac48fb5c95 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift @@ -55,6 +55,7 @@ public enum ChatListSearchPaneKey { case publicPosts case channels case apps + case globalPosts case media case downloads case links @@ -77,6 +78,8 @@ extension ChatListSearchPaneKey { return .channels case .apps: return .apps + case .globalPosts: + return .globalPosts case .media: return .media case .downloads: @@ -107,6 +110,9 @@ func defaultAvailableSearchPanes(isForum: Bool, hasDownloads: Bool, hasPublicPos } result.append(.channels) result.append(.apps) + if !isForum { + result.append(.globalPosts) + } result.append(contentsOf: [.media, .downloads, .links, .files, .music, .voice]) if !hasDownloads { @@ -232,7 +238,10 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, ASGestureRecognizerD } return } + #if DEBUG + #else self.isAdjacentLoadingEnabled = true + #endif if self.currentPanes[key] != nil { self.currentPaneKey = key diff --git a/submodules/TelegramApi/Sources/Api39.swift b/submodules/TelegramApi/Sources/Api39.swift index 7df8e9d44c..309ca1feb1 100644 --- a/submodules/TelegramApi/Sources/Api39.swift +++ b/submodules/TelegramApi/Sources/Api39.swift @@ -3511,15 +3511,18 @@ public extension Api.functions.channels { } } public extension Api.functions.channels { - static func searchPosts(hashtag: String, offsetRate: Int32, offsetPeer: Api.InputPeer, offsetId: Int32, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func searchPosts(flags: Int32, hashtag: String?, query: String?, offsetRate: Int32, offsetPeer: Api.InputPeer, offsetId: Int32, limit: Int32, allowPaidStars: Int64?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-778069893) - serializeString(hashtag, buffer: buffer, boxed: false) + buffer.appendInt32(-221973939) + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(hashtag!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {serializeString(query!, buffer: buffer, boxed: false)} serializeInt32(offsetRate, buffer: buffer, boxed: false) offsetPeer.serialize(buffer, true) serializeInt32(offsetId, buffer: buffer, boxed: false) serializeInt32(limit, buffer: buffer, boxed: false) - return (FunctionDescription(name: "channels.searchPosts", parameters: [("hashtag", String(describing: hashtag)), ("offsetRate", String(describing: offsetRate)), ("offsetPeer", String(describing: offsetPeer)), ("offsetId", String(describing: offsetId)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.Messages? in + if Int(flags) & Int(1 << 2) != 0 {serializeInt64(allowPaidStars!, buffer: buffer, boxed: false)} + return (FunctionDescription(name: "channels.searchPosts", parameters: [("flags", String(describing: flags)), ("hashtag", String(describing: hashtag)), ("query", String(describing: query)), ("offsetRate", String(describing: offsetRate)), ("offsetPeer", String(describing: offsetPeer)), ("offsetId", String(describing: offsetId)), ("limit", String(describing: limit)), ("allowPaidStars", String(describing: allowPaidStars))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.Messages? in let reader = BufferReader(buffer) var result: Api.messages.Messages? if let signature = reader.readInt32() { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramEngineContacts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramEngineContacts.swift index 23c5789da7..bc9b34b249 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramEngineContacts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramEngineContacts.swift @@ -86,6 +86,8 @@ public extension TelegramEngine { return false } }.map(EngineRenderedPeer.init) + case .globalPosts: + return [] } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift index c8fd1ef16e..3309c17f61 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift @@ -473,38 +473,64 @@ func _internal_searchMessages(account: Account, location: SearchMessagesLocation folderId = nil } - if case let .general(scope, _, _, _) = location { - switch scope { - case .everywhere: - break - case .channels: - flags |= (1 << 1) - case .groups: - flags |= (1 << 2) - case .privateChats: - flags |= (1 << 3) + if case let .general(scope, _, _, _) = location, case .globalPosts = scope { + remoteSearchResult = account.postbox.transaction { transaction -> (Int32, MessageIndex?, Api.InputPeer) in + var lowerBound: MessageIndex? + if let state = state, let message = state.main.messages.last { + lowerBound = message.index + } + if let lowerBound = lowerBound, let peer = transaction.getPeer(lowerBound.id.peerId), let inputPeer = apiInputPeer(peer) { + return (state?.main.nextRate ?? 0, lowerBound, inputPeer) + } else { + return (0, lowerBound, .inputPeerEmpty) + } } - } - - let filter: Api.MessagesFilter = tags.flatMap { messageFilterForTagMask($0) } ?? .inputMessagesFilterEmpty - remoteSearchResult = account.postbox.transaction { transaction -> (Int32, MessageIndex?, Api.InputPeer) in - var lowerBound: MessageIndex? - if let state = state, let message = state.main.messages.last { - lowerBound = message.index + |> mapToSignal { (nextRate, lowerBound, inputPeer) in + var flags: Int32 = 0 + flags |= 1 << 1 + return account.network.request(Api.functions.channels.searchPosts(flags: flags, hashtag: nil, query: query, offsetRate: nextRate, offsetPeer: inputPeer, offsetId: lowerBound?.id.id ?? 0, limit: limit, allowPaidStars: nil), automaticFloodWait: false) + |> map { result -> (Api.messages.Messages?, Api.messages.Messages?) in + return (result, nil) + } + |> `catch` { _ -> Signal<(Api.messages.Messages?, Api.messages.Messages?), NoError> in + return .single((nil, nil)) + } } - if let lowerBound = lowerBound, let peer = transaction.getPeer(lowerBound.id.peerId), let inputPeer = apiInputPeer(peer) { - return (state?.main.nextRate ?? 0, lowerBound, inputPeer) - } else { - return (0, lowerBound, .inputPeerEmpty) + } else { + if case let .general(scope, _, _, _) = location { + switch scope { + case .everywhere: + break + case .channels: + flags |= (1 << 1) + case .groups: + flags |= (1 << 2) + case .privateChats: + flags |= (1 << 3) + case .globalPosts: + break + } } - } - |> mapToSignal { (nextRate, lowerBound, inputPeer) in - return account.network.request(Api.functions.messages.searchGlobal(flags: flags, folderId: folderId, q: query, filter: filter, minDate: minDate ?? 0, maxDate: maxDate ?? (Int32.max - 1), offsetRate: nextRate, offsetPeer: inputPeer, offsetId: lowerBound?.id.id ?? 0, limit: limit), automaticFloodWait: false) - |> map { result -> (Api.messages.Messages?, Api.messages.Messages?) in - return (result, nil) + let filter: Api.MessagesFilter = tags.flatMap { messageFilterForTagMask($0) } ?? .inputMessagesFilterEmpty + remoteSearchResult = account.postbox.transaction { transaction -> (Int32, MessageIndex?, Api.InputPeer) in + var lowerBound: MessageIndex? + if let state = state, let message = state.main.messages.last { + lowerBound = message.index + } + if let lowerBound = lowerBound, let peer = transaction.getPeer(lowerBound.id.peerId), let inputPeer = apiInputPeer(peer) { + return (state?.main.nextRate ?? 0, lowerBound, inputPeer) + } else { + return (0, lowerBound, .inputPeerEmpty) + } } - |> `catch` { _ -> Signal<(Api.messages.Messages?, Api.messages.Messages?), NoError> in - return .single((nil, nil)) + |> mapToSignal { (nextRate, lowerBound, inputPeer) in + return account.network.request(Api.functions.messages.searchGlobal(flags: flags, folderId: folderId, q: query, filter: filter, minDate: minDate ?? 0, maxDate: maxDate ?? (Int32.max - 1), offsetRate: nextRate, offsetPeer: inputPeer, offsetId: lowerBound?.id.id ?? 0, limit: limit), automaticFloodWait: false) + |> map { result -> (Api.messages.Messages?, Api.messages.Messages?) in + return (result, nil) + } + |> `catch` { _ -> Signal<(Api.messages.Messages?, Api.messages.Messages?), NoError> in + return .single((nil, nil)) + } } } case let .sentMedia(tags): @@ -594,7 +620,7 @@ func _internal_searchHashtagPosts(account: Account, hashtag: String, state: Sear } } |> mapToSignal { (nextRate, lowerBound, inputPeer) in - return account.network.request(Api.functions.channels.searchPosts(hashtag: hashtag, offsetRate: nextRate, offsetPeer: inputPeer, offsetId: lowerBound?.id.id ?? 0, limit: limit), automaticFloodWait: false) + return account.network.request(Api.functions.channels.searchPosts(flags: 1 << 0, hashtag: hashtag, query: nil, offsetRate: nextRate, offsetPeer: inputPeer, offsetId: lowerBound?.id.id ?? 0, limit: limit, allowPaidStars: nil), automaticFloodWait: false) |> map { result -> (Api.messages.Messages?, Api.messages.Messages?) in return (result, nil) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift index 9b3d384efc..d983b9b1f7 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift @@ -23,6 +23,7 @@ public enum TelegramSearchPeersScope { case channels case groups case privateChats + case globalPosts } public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, network: Network, query: String, scope: TelegramSearchPeersScope) -> Signal<([FoundPeer], [FoundPeer]), NoError> { @@ -138,6 +139,9 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo return false } } + case .globalPosts: + renderedMyPeers = [] + renderedPeers = [] } return (renderedMyPeers, renderedPeers) diff --git a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift index 464a016b74..8d08f8da01 100644 --- a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift +++ b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift @@ -510,6 +510,8 @@ public final class ButtonComponent: Component { animateIn = true contentView.isUserInteractionEnabled = false self.addSubview(contentView) + + contentItem.view.parentState = state } let contentFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) * 0.5), y: floorToScreenPixels((availableSize.height - contentSize.height) * 0.5)), size: contentSize)