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

View File

@ -352,7 +352,8 @@ final class BrowserAddressListComponent: Component {
highlighting: .default, highlighting: .default,
updateIsHighlighted: { view, _ in updateIsHighlighted: { view, _ in
}) }
)
), ),
environment: {}, environment: {},
containerSize: itemFrame.size 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) 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 var leftOrigin: Int = 0
(composedString.string as NSString).enumerateSubstrings(in: NSMakeRange(0, firstRangeOrigin), options: [.byWords, .reverse]) { (str, range1, _, _) in (composedString.string as NSString).enumerateSubstrings(in: NSMakeRange(0, firstRangeOrigin), options: [.byWords, .reverse]) { (str, range1, _, _) in
let distanceFromEnd = firstRangeOrigin - range1.location 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) { func updateFrameAdditive(node: ASDisplayNode, frame: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) {
if node.frame.equalTo(frame) && !force { if node.frame.equalTo(frame) && !force {
completion?(true) completion?(true)

View File

@ -262,6 +262,9 @@ final class StarsTransactionItemNode: ListViewItemNode, ItemListItemNode {
case .ads: case .ads:
itemTitle = item.presentationData.strings.Stars_Intro_Transaction_TelegramAds_Title itemTitle = item.presentationData.strings.Stars_Intro_Transaction_TelegramAds_Title
itemSubtitle = item.presentationData.strings.Stars_Intro_Transaction_TelegramAds_Subtitle 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: case .unsupported:
itemTitle = item.presentationData.strings.Stars_Intro_Transaction_Unsupported_Title itemTitle = item.presentationData.strings.Stars_Intro_Transaction_Unsupported_Title
itemSubtitle = nil itemSubtitle = nil

View File

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

View File

@ -1010,13 +1010,13 @@ public extension Api {
} }
public extension Api { public extension Api {
enum StarsTransaction: TypeConstructorDescription { 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) { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self { 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 { if boxed {
buffer.appendInt32(178185410) buffer.appendInt32(-1216644148)
} }
serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags, buffer: buffer, boxed: false)
serializeString(id, 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 << 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 << 13) != 0 {serializeInt32(giveawayPostId!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 14) != 0 {stargift!.serialize(buffer, true)} 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 break
} }
} }
public func descriptionFields() -> (String, [(String, Any)]) { public func descriptionFields() -> (String, [(String, Any)]) {
switch self { 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):
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)]) 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() { if Int(_1!) & Int(1 << 14) != 0 {if let signature = reader.readInt32() {
_16 = Api.parse(reader, signature: signature) as? Api.StarGift _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 _c1 = _1 != nil
let _c2 = _2 != nil let _c2 = _2 != nil
let _c3 = _3 != nil let _c3 = _3 != nil
@ -1106,8 +1112,10 @@ public extension Api {
let _c14 = (Int(_1!) & Int(1 << 12) == 0) || _14 != nil let _c14 = (Int(_1!) & Int(1 << 12) == 0) || _14 != nil
let _c15 = (Int(_1!) & Int(1 << 13) == 0) || _15 != nil let _c15 = (Int(_1!) & Int(1 << 13) == 0) || _15 != nil
let _c16 = (Int(_1!) & Int(1 << 14) == 0) || _16 != 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 { let _c17 = (Int(_1!) & Int(1 << 15) == 0) || _17 != nil
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 _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 { else {
return nil return nil

View File

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

View File

@ -490,7 +490,10 @@ private final class StarsContextImpl {
private extension StarsContext.State.Transaction { private extension StarsContext.State.Transaction {
init?(apiTransaction: Api.StarsTransaction, peerId: EnginePeer.Id?, transaction: Transaction) { init?(apiTransaction: Api.StarsTransaction, peerId: EnginePeer.Id?, transaction: Transaction) {
switch apiTransaction { 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 let parsedPeer: StarsContext.State.Transaction.Peer
var paidMessageId: MessageId? var paidMessageId: MessageId?
var giveawayMessageId: MessageId? var giveawayMessageId: MessageId?
@ -506,6 +509,8 @@ private extension StarsContext.State.Transaction {
parsedPeer = .premiumBot parsedPeer = .premiumBot
case .starsTransactionPeerAds: case .starsTransactionPeerAds:
parsedPeer = .ads parsedPeer = .ads
case .starsTransactionPeerAPI:
parsedPeer = .apiLimitExtension
case .starsTransactionPeerUnsupported: case .starsTransactionPeerUnsupported:
parsedPeer = .unsupported parsedPeer = .unsupported
case let .starsTransactionPeer(apiPeer): case let .starsTransactionPeer(apiPeer):
@ -595,6 +600,7 @@ public final class StarsContext {
case fragment case fragment
case premiumBot case premiumBot
case ads case ads
case apiLimitExtension
case unsupported case unsupported
case peer(EnginePeer) case peer(EnginePeer)
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -344,6 +344,9 @@ final class StarsTransactionsListPanelComponent: Component {
case .ads: case .ads:
itemTitle = environment.strings.Stars_Intro_Transaction_TelegramAds_Title itemTitle = environment.strings.Stars_Intro_Transaction_TelegramAds_Title
itemSubtitle = environment.strings.Stars_Intro_Transaction_TelegramAds_Subtitle 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: case .unsupported:
itemTitle = environment.strings.Stars_Intro_Transaction_Unsupported_Title itemTitle = environment.strings.Stars_Intro_Transaction_Unsupported_Title
itemSubtitle = nil 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 { switch result {
case let .stickers(items): case let .stickers(items):
return (0, !items.isEmpty) return (0, !items.isEmpty)
case let .hashtags(items): case let .hashtags(items, _):
return (1, !items.isEmpty) return (1, !items.isEmpty)
case let .mentions(items): case let .mentions(items):
return (2, !items.isEmpty) return (2, !items.isEmpty)
@ -98,15 +98,19 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa
return panel return panel
} }
} }
case let .hashtags(results): case let .hashtags(results, query):
if !results.isEmpty { 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 { if let currentPanel = currentPanel as? HashtagChatInputContextPanelNode {
currentPanel.updateResults(results) currentPanel.updateResults(results, query: query, peer: peer)
return currentPanel return currentPanel
} else { } else {
let panel = HashtagChatInputContextPanelNode(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, fontSize: chatPresentationInterfaceState.fontSize, chatPresentationContext: controllerInteraction.presentationContext) let panel = HashtagChatInputContextPanelNode(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, fontSize: chatPresentationInterfaceState.fontSize, chatPresentationContext: controllerInteraction.presentationContext)
panel.interfaceInteraction = interfaceInteraction panel.interfaceInteraction = interfaceInteraction
panel.updateResults(results) panel.updateResults(results, query: query, peer: peer)
return panel return panel
} }
} }

View File

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

View File

@ -16,33 +16,38 @@ import ChatContextQuery
import ChatInputContextPanelNode import ChatInputContextPanelNode
private struct HashtagChatInputContextPanelEntryStableId: Hashable { private struct HashtagChatInputContextPanelEntryStableId: Hashable {
let text: String let title: String
} }
private struct HashtagChatInputContextPanelEntry: Comparable, Identifiable { private struct HashtagChatInputContextPanelEntry: Comparable, Identifiable {
let index: Int let index: Int
let theme: PresentationTheme 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 revealed: Bool
let isAdditionalRecent: Bool
var stableId: HashtagChatInputContextPanelEntryStableId { var stableId: HashtagChatInputContextPanelEntryStableId {
return HashtagChatInputContextPanelEntryStableId(text: self.text) return HashtagChatInputContextPanelEntryStableId(title: self.title)
} }
func withUpdatedTheme(_ theme: PresentationTheme) -> HashtagChatInputContextPanelEntry { 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 { 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 { static func <(lhs: HashtagChatInputContextPanelEntry, rhs: HashtagChatInputContextPanelEntry) -> Bool {
return lhs.index < rhs.index return lhs.index < rhs.index
} }
func item(account: Account, presentationData: PresentationData, setHashtagRevealed: @escaping (String?) -> Void, hashtagSelected: @escaping (String) -> Void, removeRequested: @escaping (String) -> Void) -> ListViewItem { func item(context: AccountContext, 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) 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] 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 (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } 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 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(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(context: context, presentationData: presentationData, setHashtagRevealed: setHashtagRevealed, hashtagSelected: hashtagSelected, removeRequested: removeRequested), directionHint: nil) }
return HashtagChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates) return HashtagChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates)
} }
@ -67,6 +72,8 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode {
private var currentEntries: [HashtagChatInputContextPanelEntry]? private var currentEntries: [HashtagChatInputContextPanelEntry]?
private var currentResults: [String] = [] private var currentResults: [String] = []
private var currentQuery: String = ""
private var currentPeer: EnginePeer?
private var revealedHashtag: String? private var revealedHashtag: String?
private var enqueuedTransitions: [(HashtagChatInputContextPanelTransition, Bool)] = [] private var enqueuedTransitions: [(HashtagChatInputContextPanelTransition, Bool)] = []
@ -91,14 +98,72 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode {
self.addSubnode(self.listView) self.addSubnode(self.listView)
} }
func updateResults(_ results: [String]) { func updateResults(_ results: [String], query: String, peer: EnginePeer?) {
self.currentResults = results self.currentResults = results
self.currentQuery = query
self.currentPeer = peer
var entries: [HashtagChatInputContextPanelEntry] = [] var entries: [HashtagChatInputContextPanelEntry] = []
var index = 0 var index = 0
var stableIds = Set<HashtagChatInputContextPanelEntryStableId>() 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) { if stableIds.contains(entry.stableId) {
continue continue
} }
@ -112,10 +177,10 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode {
private func prepareTransition(from: [HashtagChatInputContextPanelEntry]? , to: [HashtagChatInputContextPanelEntry]) { private func prepareTransition(from: [HashtagChatInputContextPanelEntry]? , to: [HashtagChatInputContextPanelEntry]) {
let firstTime = from == nil let firstTime = from == nil
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } 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 { if let strongSelf = self {
strongSelf.revealedHashtag = text strongSelf.revealedHashtag = text
strongSelf.updateResults(strongSelf.currentResults) strongSelf.updateResults(strongSelf.currentResults, query: strongSelf.currentQuery, peer: strongSelf.currentPeer)
} }
}, hashtagSelected: { [weak self] text in }, hashtagSelected: { [weak self] text in
if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction {
@ -131,8 +196,7 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode {
if let range = hashtagQueryRange { if let range = hashtagQueryRange {
let inputText = NSMutableAttributedString(attributedString: textInputState.inputText) let inputText = NSMutableAttributedString(attributedString: textInputState.inputText)
let replacementText = text + " " let replacementText = text
inputText.replaceCharacters(in: range, with: replacementText) inputText.replaceCharacters(in: range, with: replacementText)
let selectionPosition = range.lowerBound + (replacementText as NSString).length let selectionPosition = range.lowerBound + (replacementText as NSString).length
@ -172,7 +236,11 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode {
//options.insert(.LowLatency) //options.insert(.LowLatency)
} else { } else {
options.insert(.AnimateTopItemPosition) 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() var insets = UIEdgeInsets()

View File

@ -8,21 +8,35 @@ import Postbox
import TelegramPresentationData import TelegramPresentationData
import TelegramUIPreferences import TelegramUIPreferences
import ItemListUI import ItemListUI
import AvatarNode
import AccountContext
final class HashtagChatInputPanelItem: ListViewItem { final class HashtagChatInputPanelItem: ListViewItem {
fileprivate let context: AccountContext
fileprivate let presentationData: ItemListPresentationData 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 revealed: Bool
fileprivate let isAdditionalRecent: Bool
fileprivate let setHashtagRevealed: (String?) -> Void fileprivate let setHashtagRevealed: (String?) -> Void
private let hashtagSelected: (String) -> Void private let hashtagSelected: (String) -> Void
fileprivate let removeRequested: (String) -> Void fileprivate let removeRequested: (String) -> Void
let selectable: Bool = true 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.presentationData = presentationData
self.peer = peer
self.title = title
self.text = text self.text = text
self.badge = badge
self.hashtag = hashtag
self.revealed = revealed self.revealed = revealed
self.isAdditionalRecent = isAdditionalRecent
self.setHashtagRevealed = setHashtagRevealed self.setHashtagRevealed = setHashtagRevealed
self.hashtagSelected = hashtagSelected self.hashtagSelected = hashtagSelected
self.removeRequested = removeRequested self.removeRequested = removeRequested
@ -79,14 +93,29 @@ final class HashtagChatInputPanelItem: ListViewItem {
if self.revealed { if self.revealed {
self.setHashtagRevealed(nil) self.setHashtagRevealed(nil)
} else { } 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 { final class HashtagChatInputPanelItemNode: ListViewItemNode {
static let itemHeight: CGFloat = 42.0 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 textNode: TextNode
private let badgeNode: TextNode
private let topSeparatorNode: ASDisplayNode private let topSeparatorNode: ASDisplayNode
private let separatorNode: ASDisplayNode private let separatorNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode
@ -105,7 +134,12 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode {
private var validLayout: (CGSize, CGFloat, CGFloat)? private var validLayout: (CGSize, CGFloat, CGFloat)?
init() { init() {
self.iconBackgroundLayer.cornerRadius = 15.0
self.badgeBackgroundLayer.cornerRadius = 4.0
self.titleNode = TextNode()
self.textNode = TextNode() self.textNode = TextNode()
self.badgeNode = TextNode()
self.topSeparatorNode = ASDisplayNode() self.topSeparatorNode = ASDisplayNode()
self.topSeparatorNode.isLayerBacked = true self.topSeparatorNode.isLayerBacked = true
@ -123,6 +157,7 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode {
self.addSubnode(self.topSeparatorNode) self.addSubnode(self.topSeparatorNode)
self.addSubnode(self.separatorNode) self.addSubnode(self.separatorNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode) self.addSubnode(self.textNode)
self.addSubnode(self.activateAreaNode) self.addSubnode(self.activateAreaNode)
@ -131,6 +166,12 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode {
override func didLoad() { override func didLoad() {
super.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(_:))) let recognizer = ItemListRevealOptionsGestureRecognizer(target: self, action: #selector(self.revealGesture(_:)))
self.recognizer = recognizer self.recognizer = recognizer
recognizer.allowAnyDirection = false recognizer.allowAnyDirection = false
@ -149,16 +190,24 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode {
} }
func asyncLayout() -> (_ item: HashtagChatInputPanelItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { 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 makeTextLayout = TextNode.asyncLayout(self.textNode)
return { [weak self] item, params, mergedTop, mergedBottom in let makeBadgeLayout = TextNode.asyncLayout(self.badgeNode)
let textFont = Font.medium(floor(item.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0))
let baseWidth = params.width - params.leftInset - params.rightInset return { [weak self] item, params, mergedTop, mergedBottom in
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 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 (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 (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 (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()) let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: HashtagChatInputPanelItemNode.itemHeight), insets: UIEdgeInsets())
@ -169,23 +218,67 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode {
let revealOffset = strongSelf.revealOffset 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.separatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor
strongSelf.topSeparatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor strongSelf.topSeparatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor
strongSelf.backgroundColor = item.presentationData.theme.list.plainBackgroundColor strongSelf.backgroundColor = item.presentationData.theme.list.plainBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
let _ = titleApply()
let _ = textApply() 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.topSeparatorNode.isHidden = mergedTop
strongSelf.separatorNode.isHidden = !mergedBottom 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.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.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.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.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)]) 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) { func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
if let (_, leftInset, _) = self.validLayout { 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 { override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if let item = self.item {
if let _ = item.text {
return false
}
}
return true return true
} }
@ -356,7 +455,7 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode {
guard let item = self.item else { guard let item = self.item else {
return return
} }
item.removeRequested(item.text) item.removeRequested(item.hashtag)
} }
private func setupAndAddRevealNode() { private func setupAndAddRevealNode() {