Hashtag search improvements

This commit is contained in:
Ilya Laktyushin 2024-10-23 15:53:21 +04:00
parent 04b25d7152
commit 4806840989
21 changed files with 304 additions and 60 deletions

View File

@ -13132,3 +13132,9 @@ Sorry for the inconvenience.";
"TopApps.Info.Title" = "Top Mini Apps";
"TopApps.Info.Text" = "This catalogue ranks mini apps based on their daily revenue, measured in Stars. To be listed, developers must set their main mini app in [@botfather]() (as described [here](https://core.telegram.org/bots/webapps#launching-the-main-mini-app)), have over **1,000** daily users, and earn a daily revenue above **1,000** Stars, based on weekly average.";
"TopApps.Info.Done" = "Understood";
"Stars.Intro.Transaction.TelegramBotApi.Title" = "Paid Limit Extension";
"Stars.Intro.Transaction.TelegramBotApi.Subtitle" = "Bot API";
"Stars.Transaction.TelegramBotApi.Title" = "Paid Limit Extension";
"Stars.Transaction.TelegramBotApi.Subtitle" = "Bot API";

View File

@ -870,7 +870,7 @@ public struct ChatInputQueryCommandsResult: Equatable {
public enum ChatPresentationInputQueryResult: Equatable {
case stickers([FoundStickerItem])
case hashtags([String])
case hashtags([String], String)
case mentions([EnginePeer])
case commands(ChatInputQueryCommandsResult)
case emojis([(String, TelegramMediaFile?, String)], NSRange)
@ -884,9 +884,9 @@ public enum ChatPresentationInputQueryResult: Equatable {
} else {
return false
}
case let .hashtags(lhsResults):
if case let .hashtags(rhsResults) = rhs {
return lhsResults == rhsResults
case let .hashtags(lhsResults, lhsQuery):
if case let .hashtags(rhsResults, rhsQuery) = rhs {
return lhsResults == rhsResults && lhsQuery == rhsQuery
} else {
return false
}

View File

@ -352,7 +352,8 @@ final class BrowserAddressListComponent: Component {
highlighting: .default,
updateIsHighlighted: { view, _ in
})
}
)
),
environment: {},
containerSize: itemFrame.size

View File

@ -2495,7 +2495,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
let firstRangeOrigin = chatListSearchResult.text.distance(from: chatListSearchResult.text.startIndex, to: firstRange.lowerBound)
if firstRangeOrigin > 24 {
if firstRangeOrigin > 24 && !chatListSearchResult.searchQuery.hasPrefix("#") {
var leftOrigin: Int = 0
(composedString.string as NSString).enumerateSubstrings(in: NSMakeRange(0, firstRangeOrigin), options: [.byWords, .reverse]) { (str, range1, _, _) in
let distanceFromEnd = firstRangeOrigin - range1.location

View File

@ -273,6 +273,24 @@ public extension ContainedViewLayoutTransition {
}
}
func updateFrameAdditive(layer: CALayer, frame: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) {
if layer.frame.equalTo(frame) && !force {
completion?(true)
} else {
switch self {
case .immediate:
layer.frame = frame
if let completion = completion {
completion(true)
}
case .animated:
let previousFrame = layer.frame
layer.frame = frame
self.animatePositionAdditive(layer: layer, offset: CGPoint(x: previousFrame.minX - frame.minX, y: previousFrame.minY - frame.minY))
}
}
}
func updateFrameAdditive(node: ASDisplayNode, frame: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) {
if node.frame.equalTo(frame) && !force {
completion?(true)

View File

@ -262,6 +262,9 @@ final class StarsTransactionItemNode: ListViewItemNode, ItemListItemNode {
case .ads:
itemTitle = item.presentationData.strings.Stars_Intro_Transaction_TelegramAds_Title
itemSubtitle = item.presentationData.strings.Stars_Intro_Transaction_TelegramAds_Subtitle
case .apiLimitExtension:
itemTitle = item.presentationData.strings.Stars_Intro_Transaction_TelegramBotApi_Title
itemSubtitle = item.presentationData.strings.Stars_Intro_Transaction_TelegramBotApi_Subtitle
case .unsupported:
itemTitle = item.presentationData.strings.Stars_Intro_Transaction_Unsupported_Title
itemSubtitle = nil

View File

@ -902,8 +902,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1401868056] = { return Api.StarsSubscription.parse_starsSubscription($0) }
dict[88173912] = { return Api.StarsSubscriptionPricing.parse_starsSubscriptionPricing($0) }
dict[198776256] = { return Api.StarsTopupOption.parse_starsTopupOption($0) }
dict[178185410] = { return Api.StarsTransaction.parse_starsTransaction($0) }
dict[-1216644148] = { return Api.StarsTransaction.parse_starsTransaction($0) }
dict[-670195363] = { return Api.StarsTransactionPeer.parse_starsTransactionPeer($0) }
dict[-110658899] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerAPI($0) }
dict[1617438738] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerAds($0) }
dict[-1269320843] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerAppStore($0) }
dict[-382740222] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerFragment($0) }

View File

@ -1010,13 +1010,13 @@ public extension Api {
}
public extension Api {
enum StarsTransaction: TypeConstructorDescription {
case starsTransaction(flags: Int32, id: String, stars: Int64, date: Int32, peer: Api.StarsTransactionPeer, title: String?, description: String?, photo: Api.WebDocument?, transactionDate: Int32?, transactionUrl: String?, botPayload: Buffer?, msgId: Int32?, extendedMedia: [Api.MessageMedia]?, subscriptionPeriod: Int32?, giveawayPostId: Int32?, stargift: Api.StarGift?)
case starsTransaction(flags: Int32, id: String, stars: Int64, date: Int32, peer: Api.StarsTransactionPeer, title: String?, description: String?, photo: Api.WebDocument?, transactionDate: Int32?, transactionUrl: String?, botPayload: Buffer?, msgId: Int32?, extendedMedia: [Api.MessageMedia]?, subscriptionPeriod: Int32?, giveawayPostId: Int32?, stargift: Api.StarGift?, floodskipDate: Int32?, floodskipNumber: Int32?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia, let subscriptionPeriod, let giveawayPostId, let stargift):
case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia, let subscriptionPeriod, let giveawayPostId, let stargift, let floodskipDate, let floodskipNumber):
if boxed {
buffer.appendInt32(178185410)
buffer.appendInt32(-1216644148)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeString(id, buffer: buffer, boxed: false)
@ -1038,14 +1038,16 @@ public extension Api {
if Int(flags) & Int(1 << 12) != 0 {serializeInt32(subscriptionPeriod!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 13) != 0 {serializeInt32(giveawayPostId!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 14) != 0 {stargift!.serialize(buffer, true)}
if Int(flags) & Int(1 << 15) != 0 {serializeInt32(floodskipDate!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 15) != 0 {serializeInt32(floodskipNumber!, buffer: buffer, boxed: false)}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia, let subscriptionPeriod, let giveawayPostId, let stargift):
return ("starsTransaction", [("flags", flags as Any), ("id", id as Any), ("stars", stars as Any), ("date", date as Any), ("peer", peer as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("transactionDate", transactionDate as Any), ("transactionUrl", transactionUrl as Any), ("botPayload", botPayload as Any), ("msgId", msgId as Any), ("extendedMedia", extendedMedia as Any), ("subscriptionPeriod", subscriptionPeriod as Any), ("giveawayPostId", giveawayPostId as Any), ("stargift", stargift as Any)])
case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia, let subscriptionPeriod, let giveawayPostId, let stargift, let floodskipDate, let floodskipNumber):
return ("starsTransaction", [("flags", flags as Any), ("id", id as Any), ("stars", stars as Any), ("date", date as Any), ("peer", peer as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("transactionDate", transactionDate as Any), ("transactionUrl", transactionUrl as Any), ("botPayload", botPayload as Any), ("msgId", msgId as Any), ("extendedMedia", extendedMedia as Any), ("subscriptionPeriod", subscriptionPeriod as Any), ("giveawayPostId", giveawayPostId as Any), ("stargift", stargift as Any), ("floodskipDate", floodskipDate as Any), ("floodskipNumber", floodskipNumber as Any)])
}
}
@ -1090,6 +1092,10 @@ public extension Api {
if Int(_1!) & Int(1 << 14) != 0 {if let signature = reader.readInt32() {
_16 = Api.parse(reader, signature: signature) as? Api.StarGift
} }
var _17: Int32?
if Int(_1!) & Int(1 << 15) != 0 {_17 = reader.readInt32() }
var _18: Int32?
if Int(_1!) & Int(1 << 15) != 0 {_18 = reader.readInt32() }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
@ -1106,8 +1112,10 @@ public extension Api {
let _c14 = (Int(_1!) & Int(1 << 12) == 0) || _14 != nil
let _c15 = (Int(_1!) & Int(1 << 13) == 0) || _15 != nil
let _c16 = (Int(_1!) & Int(1 << 14) == 0) || _16 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 {
return Api.StarsTransaction.starsTransaction(flags: _1!, id: _2!, stars: _3!, date: _4!, peer: _5!, title: _6, description: _7, photo: _8, transactionDate: _9, transactionUrl: _10, botPayload: _11, msgId: _12, extendedMedia: _13, subscriptionPeriod: _14, giveawayPostId: _15, stargift: _16)
let _c17 = (Int(_1!) & Int(1 << 15) == 0) || _17 != nil
let _c18 = (Int(_1!) & Int(1 << 15) == 0) || _18 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 {
return Api.StarsTransaction.starsTransaction(flags: _1!, id: _2!, stars: _3!, date: _4!, peer: _5!, title: _6, description: _7, photo: _8, transactionDate: _9, transactionUrl: _10, botPayload: _11, msgId: _12, extendedMedia: _13, subscriptionPeriod: _14, giveawayPostId: _15, stargift: _16, floodskipDate: _17, floodskipNumber: _18)
}
else {
return nil

View File

@ -1,6 +1,7 @@
public extension Api {
enum StarsTransactionPeer: TypeConstructorDescription {
case starsTransactionPeer(peer: Api.Peer)
case starsTransactionPeerAPI
case starsTransactionPeerAds
case starsTransactionPeerAppStore
case starsTransactionPeerFragment
@ -15,6 +16,12 @@ public extension Api {
buffer.appendInt32(-670195363)
}
peer.serialize(buffer, true)
break
case .starsTransactionPeerAPI:
if boxed {
buffer.appendInt32(-110658899)
}
break
case .starsTransactionPeerAds:
if boxed {
@ -59,6 +66,8 @@ public extension Api {
switch self {
case .starsTransactionPeer(let peer):
return ("starsTransactionPeer", [("peer", peer as Any)])
case .starsTransactionPeerAPI:
return ("starsTransactionPeerAPI", [])
case .starsTransactionPeerAds:
return ("starsTransactionPeerAds", [])
case .starsTransactionPeerAppStore:
@ -87,6 +96,9 @@ public extension Api {
return nil
}
}
public static func parse_starsTransactionPeerAPI(_ reader: BufferReader) -> StarsTransactionPeer? {
return Api.StarsTransactionPeer.starsTransactionPeerAPI
}
public static func parse_starsTransactionPeerAds(_ reader: BufferReader) -> StarsTransactionPeer? {
return Api.StarsTransactionPeer.starsTransactionPeerAds
}

View File

@ -490,7 +490,10 @@ private final class StarsContextImpl {
private extension StarsContext.State.Transaction {
init?(apiTransaction: Api.StarsTransaction, peerId: EnginePeer.Id?, transaction: Transaction) {
switch apiTransaction {
case let .starsTransaction(apiFlags, id, stars, date, transactionPeer, title, description, photo, transactionDate, transactionUrl, _, messageId, extendedMedia, subscriptionPeriod, giveawayPostId, starGift):
case let .starsTransaction(apiFlags, id, stars, date, transactionPeer, title, description, photo, transactionDate, transactionUrl, _, messageId, extendedMedia, subscriptionPeriod, giveawayPostId, starGift, floodskipDate, floodskipNumber):
let _ = floodskipDate
let _ = floodskipNumber
let parsedPeer: StarsContext.State.Transaction.Peer
var paidMessageId: MessageId?
var giveawayMessageId: MessageId?
@ -506,6 +509,8 @@ private extension StarsContext.State.Transaction {
parsedPeer = .premiumBot
case .starsTransactionPeerAds:
parsedPeer = .ads
case .starsTransactionPeerAPI:
parsedPeer = .apiLimitExtension
case .starsTransactionPeerUnsupported:
parsedPeer = .unsupported
case let .starsTransactionPeer(apiPeer):
@ -595,6 +600,7 @@ public final class StarsContext {
case fragment
case premiumBot
case ads
case apiLimitExtension
case unsupported
case peer(EnginePeer)
}

View File

@ -130,10 +130,10 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, cha
case .hashtag:
break
default:
signal = .single({ _ in return .hashtags([]) })
signal = .single({ _ in return .hashtags([], query) })
}
} else {
signal = .single({ _ in return .hashtags([]) })
signal = .single({ _ in return .hashtags([], query) })
}
let hashtags: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = context.engine.messages.recentlyUsedHashtags()
@ -145,7 +145,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, cha
result.append(hashtag)
}
}
return { _ in return .hashtags(result) }
return { _ in return .hashtags(result, query) }
}
|> castError(ChatContextQueryError.self)

View File

@ -274,7 +274,7 @@ public final class StarsAvatarComponent: Component {
self.iconView.isHidden = false
self.avatarNode.isHidden = true
self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white)
case .unsupported:
case .unsupported, .apiLimitExtension:
iconInset = 7.0
self.backgroundView.image = generateGradientFilledCircleImage(
diameter: size.width,

View File

@ -788,7 +788,7 @@ public final class StarsImageComponent: Component {
direction: .mirroredDiagonal
)
iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white)
case .peer, .unsupported:
case .peer, .unsupported, .apiLimitExtension:
iconInset = 15.0
iconBackgroundView.image = generateGradientFilledCircleImage(
diameter: imageSize.width,

View File

@ -410,6 +410,9 @@ private final class StarsTransactionSheetContent: CombinedComponent {
case .ads:
titleText = strings.Stars_Transaction_TelegramAds_Title
via = strings.Stars_Transaction_TelegramAds_Subtitle
case .apiLimitExtension:
titleText = strings.Stars_Transaction_TelegramBotApi_Title
via = strings.Stars_Transaction_TelegramBotApi_Subtitle
case .unsupported:
titleText = strings.Stars_Transaction_Unsupported_Title
}

View File

@ -344,6 +344,9 @@ final class StarsTransactionsListPanelComponent: Component {
case .ads:
itemTitle = environment.strings.Stars_Intro_Transaction_TelegramAds_Title
itemSubtitle = environment.strings.Stars_Intro_Transaction_TelegramAds_Subtitle
case .apiLimitExtension:
itemTitle = environment.strings.Stars_Intro_Transaction_TelegramBotApi_Title
itemSubtitle = environment.strings.Stars_Intro_Transaction_TelegramBotApi_Subtitle
case .unsupported:
itemTitle = environment.strings.Stars_Intro_Transaction_Unsupported_Title
itemSubtitle = nil

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "tagrecent.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -10,7 +10,7 @@ private func inputQueryResultPriority(_ result: ChatPresentationInputQueryResult
switch result {
case let .stickers(items):
return (0, !items.isEmpty)
case let .hashtags(items):
case let .hashtags(items, _):
return (1, !items.isEmpty)
case let .mentions(items):
return (2, !items.isEmpty)
@ -98,15 +98,19 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa
return panel
}
}
case let .hashtags(results):
if !results.isEmpty {
case let .hashtags(results, query):
var peer: EnginePeer?
if let chatPeer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, chatPeer.addressName != nil {
peer = EnginePeer(chatPeer)
}
if !results.isEmpty || (peer != nil && query.count >= 4) {
if let currentPanel = currentPanel as? HashtagChatInputContextPanelNode {
currentPanel.updateResults(results)
currentPanel.updateResults(results, query: query, peer: peer)
return currentPanel
} else {
let panel = HashtagChatInputContextPanelNode(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, fontSize: chatPresentationInterfaceState.fontSize, chatPresentationContext: controllerInteraction.presentationContext)
panel.interfaceInteraction = interfaceInteraction
panel.updateResults(results)
panel.updateResults(results, query: query, peer: peer)
return panel
}
}

View File

@ -114,10 +114,10 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee
case .hashtag:
break
default:
signal = .single({ _ in return .hashtags([]) })
signal = .single({ _ in return .hashtags([], query) })
}
} else {
signal = .single({ _ in return .hashtags([]) })
signal = .single({ _ in return .hashtags([], query) })
}
let hashtags: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = context.engine.messages.recentlyUsedHashtags()
@ -129,7 +129,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee
result.append(hashtag)
}
}
return { _ in return .hashtags(result) }
return { _ in return .hashtags(result, query) }
}
|> castError(ChatContextQueryError.self)

View File

@ -16,33 +16,38 @@ import ChatContextQuery
import ChatInputContextPanelNode
private struct HashtagChatInputContextPanelEntryStableId: Hashable {
let text: String
let title: String
}
private struct HashtagChatInputContextPanelEntry: Comparable, Identifiable {
let index: Int
let theme: PresentationTheme
let text: String
let peer: EnginePeer?
let title: String
let text: String?
let badge: String?
let hashtag: String
let revealed: Bool
let isAdditionalRecent: Bool
var stableId: HashtagChatInputContextPanelEntryStableId {
return HashtagChatInputContextPanelEntryStableId(text: self.text)
return HashtagChatInputContextPanelEntryStableId(title: self.title)
}
func withUpdatedTheme(_ theme: PresentationTheme) -> HashtagChatInputContextPanelEntry {
return HashtagChatInputContextPanelEntry(index: self.index, theme: theme, text: self.text, revealed: self.revealed)
return HashtagChatInputContextPanelEntry(index: self.index, theme: theme, peer: peer, title: self.title, text: self.text, badge: self.badge, hashtag: self.hashtag, revealed: self.revealed, isAdditionalRecent: self.isAdditionalRecent)
}
static func ==(lhs: HashtagChatInputContextPanelEntry, rhs: HashtagChatInputContextPanelEntry) -> Bool {
return lhs.index == rhs.index && lhs.text == rhs.text && lhs.theme === rhs.theme && lhs.revealed == rhs.revealed
return lhs.index == rhs.index && lhs.peer == rhs.peer && lhs.title == rhs.title && lhs.text == rhs.text && lhs.badge == rhs.badge && lhs.hashtag == rhs.hashtag && lhs.theme === rhs.theme && lhs.revealed == rhs.revealed && lhs.isAdditionalRecent == rhs.isAdditionalRecent
}
static func <(lhs: HashtagChatInputContextPanelEntry, rhs: HashtagChatInputContextPanelEntry) -> Bool {
return lhs.index < rhs.index
}
func item(account: Account, presentationData: PresentationData, setHashtagRevealed: @escaping (String?) -> Void, hashtagSelected: @escaping (String) -> Void, removeRequested: @escaping (String) -> Void) -> ListViewItem {
return HashtagChatInputPanelItem(presentationData: ItemListPresentationData(presentationData), text: self.text, revealed: self.revealed, setHashtagRevealed: setHashtagRevealed, hashtagSelected: hashtagSelected, removeRequested: removeRequested)
func item(context: AccountContext, presentationData: PresentationData, setHashtagRevealed: @escaping (String?) -> Void, hashtagSelected: @escaping (String) -> Void, removeRequested: @escaping (String) -> Void) -> ListViewItem {
return HashtagChatInputPanelItem(context: context, presentationData: ItemListPresentationData(presentationData), peer: self.peer, title: self.title, text: self.text, badge: self.badge, hashtag: self.hashtag, revealed: self.revealed, isAdditionalRecent: self.isAdditionalRecent, setHashtagRevealed: setHashtagRevealed, hashtagSelected: hashtagSelected, removeRequested: removeRequested)
}
}
@ -52,12 +57,12 @@ private struct HashtagChatInputContextPanelTransition {
let updates: [ListViewUpdateItem]
}
private func preparedTransition(from fromEntries: [HashtagChatInputContextPanelEntry], to toEntries: [HashtagChatInputContextPanelEntry], account: Account, presentationData: PresentationData, setHashtagRevealed: @escaping (String?) -> Void, hashtagSelected: @escaping (String) -> Void, removeRequested: @escaping (String) -> Void) -> HashtagChatInputContextPanelTransition {
private func preparedTransition(from fromEntries: [HashtagChatInputContextPanelEntry], to toEntries: [HashtagChatInputContextPanelEntry], context: AccountContext, presentationData: PresentationData, setHashtagRevealed: @escaping (String?) -> Void, hashtagSelected: @escaping (String) -> Void, removeRequested: @escaping (String) -> Void) -> HashtagChatInputContextPanelTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, presentationData: presentationData, setHashtagRevealed: setHashtagRevealed, hashtagSelected: hashtagSelected, removeRequested: removeRequested), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, presentationData: presentationData, setHashtagRevealed: setHashtagRevealed, hashtagSelected: hashtagSelected, removeRequested: removeRequested), directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, setHashtagRevealed: setHashtagRevealed, hashtagSelected: hashtagSelected, removeRequested: removeRequested), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, setHashtagRevealed: setHashtagRevealed, hashtagSelected: hashtagSelected, removeRequested: removeRequested), directionHint: nil) }
return HashtagChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates)
}
@ -67,6 +72,8 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode {
private var currentEntries: [HashtagChatInputContextPanelEntry]?
private var currentResults: [String] = []
private var currentQuery: String = ""
private var currentPeer: EnginePeer?
private var revealedHashtag: String?
private var enqueuedTransitions: [(HashtagChatInputContextPanelTransition, Bool)] = []
@ -91,14 +98,72 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode {
self.addSubnode(self.listView)
}
func updateResults(_ results: [String]) {
func updateResults(_ results: [String], query: String, peer: EnginePeer?) {
self.currentResults = results
self.currentQuery = query
self.currentPeer = peer
var entries: [HashtagChatInputContextPanelEntry] = []
var index = 0
var stableIds = Set<HashtagChatInputContextPanelEntryStableId>()
for text in results {
let entry = HashtagChatInputContextPanelEntry(index: index, theme: self.theme, text: text, revealed: text == self.revealedHashtag)
var isAdditionalRecent = false
if let peer, let _ = peer.addressName {
isAdditionalRecent = true
}
//TODO:localize
if query.count > 3 {
if let peer, let addressName = peer.addressName {
let genericEntry = HashtagChatInputContextPanelEntry(
index: 0,
theme: self.theme,
peer: nil,
title: "Use #\(query)",
text: "searches posts from all channels",
badge: nil,
hashtag: query,
revealed: false,
isAdditionalRecent: false
)
stableIds.insert(genericEntry.stableId)
entries.append(genericEntry)
let peerEntry = HashtagChatInputContextPanelEntry(
index: 1,
theme: self.theme,
peer: peer,
title: "Use #\(query)@\(addressName)",
text: "searches only posts from this channel",
badge: "NEW",
hashtag: "\(query)@\(addressName)",
revealed: false,
isAdditionalRecent: false
)
stableIds.insert(peerEntry.stableId)
entries.append(peerEntry)
}
}
index = 2
for hashtag in results {
if hashtag == query {
continue
}
if !hashtag.hasPrefix(query) {
continue
}
let entry = HashtagChatInputContextPanelEntry(
index: index,
theme: self.theme,
peer: hashtag.contains("@") ? peer : nil,
title: "#\(hashtag)",
text: nil,
badge: nil,
hashtag: hashtag,
revealed: hashtag == self.revealedHashtag,
isAdditionalRecent: isAdditionalRecent && !hashtag.contains("@")
)
if stableIds.contains(entry.stableId) {
continue
}
@ -112,10 +177,10 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode {
private func prepareTransition(from: [HashtagChatInputContextPanelEntry]? , to: [HashtagChatInputContextPanelEntry]) {
let firstTime = from == nil
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let transition = preparedTransition(from: from ?? [], to: to, account: self.context.account, presentationData: presentationData, setHashtagRevealed: { [weak self] text in
let transition = preparedTransition(from: from ?? [], to: to, context: self.context, presentationData: presentationData, setHashtagRevealed: { [weak self] text in
if let strongSelf = self {
strongSelf.revealedHashtag = text
strongSelf.updateResults(strongSelf.currentResults)
strongSelf.updateResults(strongSelf.currentResults, query: strongSelf.currentQuery, peer: strongSelf.currentPeer)
}
}, hashtagSelected: { [weak self] text in
if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction {
@ -131,8 +196,7 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode {
if let range = hashtagQueryRange {
let inputText = NSMutableAttributedString(attributedString: textInputState.inputText)
let replacementText = text + " "
let replacementText = text
inputText.replaceCharacters(in: range, with: replacementText)
let selectionPosition = range.lowerBound + (replacementText as NSString).length
@ -172,7 +236,11 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode {
//options.insert(.LowLatency)
} else {
options.insert(.AnimateTopItemPosition)
options.insert(.AnimateCrossfade)
if transition.insertions.isEmpty && transition.deletions.isEmpty && transition.updates.count <= 2 {
options.insert(.AnimateInsertion)
} else {
options.insert(.AnimateCrossfade)
}
}
var insets = UIEdgeInsets()

View File

@ -8,21 +8,35 @@ import Postbox
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import AvatarNode
import AccountContext
final class HashtagChatInputPanelItem: ListViewItem {
fileprivate let context: AccountContext
fileprivate let presentationData: ItemListPresentationData
fileprivate let text: String
fileprivate let peer: EnginePeer?
fileprivate let title: String
fileprivate let text: String?
fileprivate let badge: String?
fileprivate let hashtag: String
fileprivate let revealed: Bool
fileprivate let isAdditionalRecent: Bool
fileprivate let setHashtagRevealed: (String?) -> Void
private let hashtagSelected: (String) -> Void
fileprivate let removeRequested: (String) -> Void
let selectable: Bool = true
public init(presentationData: ItemListPresentationData, text: String, revealed: Bool, setHashtagRevealed: @escaping (String?) -> Void, hashtagSelected: @escaping (String) -> Void, removeRequested: @escaping (String) -> Void) {
public init(context: AccountContext, presentationData: ItemListPresentationData, peer: EnginePeer?, title: String, text: String?, badge: String? = nil, hashtag: String, revealed: Bool, isAdditionalRecent: Bool, setHashtagRevealed: @escaping (String?) -> Void, hashtagSelected: @escaping (String) -> Void, removeRequested: @escaping (String) -> Void) {
self.context = context
self.presentationData = presentationData
self.peer = peer
self.title = title
self.text = text
self.badge = badge
self.hashtag = hashtag
self.revealed = revealed
self.isAdditionalRecent = isAdditionalRecent
self.setHashtagRevealed = setHashtagRevealed
self.hashtagSelected = hashtagSelected
self.removeRequested = removeRequested
@ -79,14 +93,29 @@ final class HashtagChatInputPanelItem: ListViewItem {
if self.revealed {
self.setHashtagRevealed(nil)
} else {
self.hashtagSelected(self.text)
if self.isAdditionalRecent {
self.hashtagSelected(self.hashtag)
} else {
self.hashtagSelected(self.hashtag + " ")
}
}
}
}
private let avatarFont = avatarPlaceholderFont(size: 16.0)
final class HashtagChatInputPanelItemNode: ListViewItemNode {
static let itemHeight: CGFloat = 42.0
private let iconBackgroundLayer = SimpleLayer()
private let iconLayer = SimpleLayer()
private var avatarNode: AvatarNode?
private let badgeBackgroundLayer = SimpleLayer()
private let titleNode: TextNode
private let textNode: TextNode
private let badgeNode: TextNode
private let topSeparatorNode: ASDisplayNode
private let separatorNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
@ -105,7 +134,12 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode {
private var validLayout: (CGSize, CGFloat, CGFloat)?
init() {
self.iconBackgroundLayer.cornerRadius = 15.0
self.badgeBackgroundLayer.cornerRadius = 4.0
self.titleNode = TextNode()
self.textNode = TextNode()
self.badgeNode = TextNode()
self.topSeparatorNode = ASDisplayNode()
self.topSeparatorNode.isLayerBacked = true
@ -120,9 +154,10 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode {
self.activateAreaNode.accessibilityTraits = [.button]
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.topSeparatorNode)
self.addSubnode(self.separatorNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.activateAreaNode)
@ -131,6 +166,12 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode {
override func didLoad() {
super.didLoad()
self.view.layer.addSublayer(self.iconBackgroundLayer)
self.iconBackgroundLayer.addSublayer(self.iconLayer)
self.view.layer.addSublayer(self.badgeBackgroundLayer)
self.addSubnode(self.badgeNode)
let recognizer = ItemListRevealOptionsGestureRecognizer(target: self, action: #selector(self.revealGesture(_:)))
self.recognizer = recognizer
recognizer.allowAnyDirection = false
@ -149,16 +190,24 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode {
}
func asyncLayout() -> (_ item: HashtagChatInputPanelItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeTextLayout = TextNode.asyncLayout(self.textNode)
let makeBadgeLayout = TextNode.asyncLayout(self.badgeNode)
return { [weak self] item, params, mergedTop, mergedBottom in
let textFont = Font.medium(floor(item.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0))
let baseWidth = params.width - params.leftInset - params.rightInset
let titleFont = Font.semibold(floor(item.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0))
let textFont = Font.regular(floor(item.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0))
let badgeFont = Font.medium(floor(item.presentationData.fontSize.baseDisplaySize * 10.0 / 17.0))
let leftInset: CGFloat = 15.0 + params.leftInset
let textLeftInset: CGFloat = 40.0
let baseWidth = params.width - params.leftInset - params.rightInset - textLeftInset
let title = "#\(item.text)"
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: textFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: baseWidth, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (badgeLayout, badgeApply) = makeBadgeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.badge ?? "", font: badgeFont, textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: baseWidth, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: baseWidth - badgeLayout.size.width, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.text ?? "", font: textFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: baseWidth, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: HashtagChatInputPanelItemNode.itemHeight), insets: UIEdgeInsets())
@ -166,26 +215,70 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode {
if let strongSelf = self {
strongSelf.item = item
strongSelf.validLayout = (nodeLayout.contentSize, params.leftInset, params.rightInset)
let revealOffset = strongSelf.revealOffset
if strongSelf.iconLayer.contents == nil {
strongSelf.iconLayer.contents = UIImage(bundleImageName: "Chat/Hashtag/SuggestHashtag")?.cgImage
}
strongSelf.iconBackgroundLayer.backgroundColor = item.presentationData.theme.list.itemAccentColor.cgColor
strongSelf.iconLayer.layerTintColor = item.presentationData.theme.list.itemCheckColors.foregroundColor.cgColor
strongSelf.badgeBackgroundLayer.backgroundColor = item.presentationData.theme.list.itemAccentColor.cgColor
strongSelf.separatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor
strongSelf.topSeparatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor
strongSelf.backgroundColor = item.presentationData.theme.list.plainBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
let _ = titleApply()
let _ = textApply()
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: revealOffset + leftInset, y: floor((nodeLayout.contentSize.height - textLayout.size.height) / 2.0)), size: textLayout.size)
let _ = badgeApply()
if textLayout.size.height > 0.0 {
let combinedHeight = titleLayout.size.height + textLayout.size.height
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + textLeftInset, y: floor((nodeLayout.contentSize.height - combinedHeight) / 2.0)), size: titleLayout.size)
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset + textLeftInset, y: floor((nodeLayout.contentSize.height - combinedHeight) / 2.0) + combinedHeight - textLayout.size.height), size: textLayout.size)
} else {
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: revealOffset + leftInset + textLeftInset, y: floor((nodeLayout.contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size)
}
if badgeLayout.size.height > 0.0 {
let badgeFrame = CGRect(origin: CGPoint(x: strongSelf.titleNode.frame.maxX + 8.0, y: floorToScreenPixels(strongSelf.titleNode.frame.midY - badgeLayout.size.height / 2.0)), size: badgeLayout.size)
let badgeBackgroundFrame = badgeFrame.insetBy(dx: -3.0, dy: -2.0)
strongSelf.badgeNode.frame = badgeFrame
strongSelf.badgeBackgroundLayer.frame = badgeBackgroundFrame
}
strongSelf.topSeparatorNode.isHidden = mergedTop
strongSelf.separatorNode.isHidden = !mergedBottom
let iconSize = CGSize(width: 30.0, height: 30.0)
strongSelf.iconBackgroundLayer.frame = CGRect(origin: CGPoint(x: leftInset - 3.0, y: floor((nodeLayout.contentSize.height - 30.0) / 2.0)), size: iconSize)
strongSelf.iconLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 30.0, height: 30.0))
if let peer = item.peer {
strongSelf.iconBackgroundLayer.isHidden = true
let avatarNode: AvatarNode
if let current = strongSelf.avatarNode {
avatarNode = current
} else {
avatarNode = AvatarNode(font: avatarFont)
strongSelf.addSubnode(avatarNode)
strongSelf.avatarNode = avatarNode
}
avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer)
avatarNode.frame = strongSelf.iconBackgroundLayer.frame
} else {
strongSelf.iconBackgroundLayer.isHidden = false
}
strongSelf.topSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: UIScreenPixel))
strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: params.width - leftInset, height: UIScreenPixel))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: nodeLayout.size.height + UIScreenPixel))
strongSelf.activateAreaNode.accessibilityLabel = title
strongSelf.activateAreaNode.accessibilityLabel = item.title
strongSelf.activateAreaNode.frame = CGRect(origin: .zero, size: nodeLayout.size)
strongSelf.setRevealOptions([ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)])
@ -197,7 +290,8 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode {
func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
if let (_, leftInset, _) = self.validLayout {
transition.updateFrameAdditive(node: self.textNode, frame: CGRect(origin: CGPoint(x: min(offset, 0.0) + 15.0 + leftInset, y: self.textNode.frame.minY), size: self.textNode.frame.size))
transition.updateFrameAdditive(layer: self.iconBackgroundLayer, frame: CGRect(origin: CGPoint(x: min(offset, 0.0) + 12.0 + leftInset, y: self.iconBackgroundLayer.frame.minY), size: self.iconBackgroundLayer.frame.size))
transition.updateFrameAdditive(node: self.titleNode, frame: CGRect(origin: CGPoint(x: min(offset, 0.0) + 15.0 + 40.0 + leftInset, y: self.titleNode.frame.minY), size: self.titleNode.frame.size))
}
}
@ -276,6 +370,11 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode {
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if let item = self.item {
if let _ = item.text {
return false
}
}
return true
}
@ -356,7 +455,7 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode {
guard let item = self.item else {
return
}
item.removeRequested(item.text)
item.removeRequested(item.hashtag)
}
private func setupAndAddRevealNode() {