This commit is contained in:
Isaac 2025-07-25 15:20:36 +02:00
parent 0d2cffb033
commit 46dad2eddc
12 changed files with 1001 additions and 112 deletions

19
MODULE.bazel.lock generated
View File

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

View File

@ -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, _):

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Api.messages.Messages>) {
static func searchPosts(flags: Int32, hashtag: String?, query: String?, offsetRate: Int32, offsetPeer: Api.InputPeer, offsetId: Int32, limit: Int32, allowPaidStars: Int64?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.Messages>) {
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() {

View File

@ -86,6 +86,8 @@ public extension TelegramEngine {
return false
}
}.map(EngineRenderedPeer.init)
case .globalPosts:
return []
}
}
}

View File

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

View File

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

View File

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