Various improvements

This commit is contained in:
Ilya Laktyushin 2024-10-25 19:09:31 +04:00
parent 2fa059477c
commit 3d03fc94c6
23 changed files with 917 additions and 113 deletions

View File

@ -11753,9 +11753,11 @@ Sorry for the inconvenience.";
"Monetization.TransactionInfo.ViewInExplorer" = "View in Blockchain Explorer";
"Monetization.Intro.Title" = "Earn From Your Channel";
"Monetization.Intro.Bot.Title" = "Earn From Your Bot";
"Monetization.Intro.Ads.Title" = "Telegram Ads";
"Monetization.Intro.Ads.Text" = "Telegram can display ads in your channel.";
"Monetization.Intro.Bot.Ads.Text" = "Telegram can display ads in your bot.";
"Monetization.Intro.Split.Title" = "50:50 Revenue Split";
"Monetization.Intro.Split.Text" = "You receive 50% of the ad revenue in TON.";
@ -13000,6 +13002,9 @@ Sorry for the inconvenience.";
"Gift.View.Availability" = "Availability";
"Gift.View.Availability.Of" = "%1$@ of %2$@";
"Gift.View.Availability.NewOf" = "%1$@ of %2$@ left";
"Gift.View.Visibility" = "Visibility";
"Gift.View.Visibility.Visible" = "Visible on your page";
"Gift.View.Visibility.Hide" = "hide";
"Gift.View.Hide" = "Hide from My Page";
"Gift.View.Display" = "Display on My Page";
"Gift.View.Convert" = "Convert to %@";
@ -13141,3 +13146,6 @@ Sorry for the inconvenience.";
"Stars.Transaction.TelegramBotApi.Title" = "Paid Limit Extension";
"Stars.Transaction.TelegramBotApi.Subtitle" = "Bot API";
"Monetization.Bot.Header" = "Telegram shares 50% of the revenue from ads displayed in your bot. [Learn More >]()";
"Monetization.Bot.BalanceTitle" = "AVAILABLE BALANCE";

View File

@ -1559,8 +1559,13 @@ private func monetizationEntries(
) -> [StatsEntry] {
var entries: [StatsEntry] = []
var isBot = false
if case let .user(user) = peer, let _ = user.botInfo {
isBot = true
}
if canViewRevenue {
entries.append(.adsHeader(presentationData.theme, presentationData.strings.Monetization_Header))
entries.append(.adsHeader(presentationData.theme, isBot ? presentationData.strings.Monetization_Bot_Header : presentationData.strings.Monetization_Header))
if !data.topHoursGraph.isEmpty {
entries.append(.adsImpressionsTitle(presentationData.theme, presentationData.strings.Monetization_ImpressionsTitle))
@ -1602,7 +1607,7 @@ private func monetizationEntries(
}
if canViewRevenue {
entries.append(.adsTonBalanceTitle(presentationData.theme, presentationData.strings.Monetization_TonBalanceTitle))
entries.append(.adsTonBalanceTitle(presentationData.theme, isBot ? presentationData.strings.Monetization_Bot_BalanceTitle : presentationData.strings.Monetization_TonBalanceTitle))
entries.append(.adsTonBalance(presentationData.theme, data, isCreator && data.balances.availableBalance > 0, data.balances.withdrawEnabled))
if isCreator {
@ -1644,7 +1649,7 @@ private func monetizationEntries(
if displayTonTransactions {
if !addedTransactionsTabs {
entries.append(.adsTransactionsTitle(presentationData.theme, presentationData.strings.Monetization_TonTransactions.uppercased()))
entries.append(.adsTransactionsTitle(presentationData.theme, isBot ? presentationData.strings.Monetization_TransactionsTitle.uppercased() : presentationData.strings.Monetization_TonTransactions.uppercased()))
}
var transactions = transactionsInfo.transactions
@ -1788,7 +1793,15 @@ private func channelStatsControllerEntries(
return []
}
public func channelStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: PeerId, section: ChannelStatsSection = .stats, boostStatus: ChannelBoostStatus? = nil, boostStatusUpdated: ((ChannelBoostStatus) -> Void)? = nil) -> ViewController {
public func channelStatsController(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
peerId: PeerId,
section: ChannelStatsSection = .stats,
existingRevenueContext: RevenueStatsContext? = nil,
boostStatus: ChannelBoostStatus? = nil,
boostStatusUpdated: ((ChannelBoostStatus) -> Void)? = nil
) -> ViewController {
let statePromise = ValuePromise(ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false, starsSelected: false, transactionsExpanded: false, moreTransactionsDisplayed: 0), ignoreRepeated: true)
let stateValue = Atomic(value: ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false, starsSelected: false, transactionsExpanded: false, moreTransactionsDisplayed: 0))
let updateState: ((ChannelStatsControllerState) -> ChannelStatsControllerState) -> Void = { f in
@ -1845,7 +1858,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
let boostsContext = ChannelBoostersContext(account: context.account, peerId: peerId, gift: false)
let giftsContext = ChannelBoostersContext(account: context.account, peerId: peerId, gift: true)
let revenueContext = RevenueStatsContext(account: context.account, peerId: peerId)
let revenueContext = existingRevenueContext ?? RevenueStatsContext(account: context.account, peerId: peerId)
let revenueState = Promise<RevenueStatsContextState?>()
revenueState.set(.single(nil) |> then(revenueContext.state |> map(Optional.init)))
@ -2013,7 +2026,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
buyAdsImpl?()
},
openMonetizationIntro: {
let controller = MonetizationIntroScreen(context: context, openMore: {})
let controller = MonetizationIntroScreen(context: context, mode: existingRevenueContext != nil ? .bot : .channel, openMore: {})
pushImpl?(controller)
},
openMonetizationInfo: {
@ -2112,7 +2125,8 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
)
|> deliverOnMainQueue
|> map { presentationData, state, peer, data, messageView, stories, boostData, boostersState, giftsState, revenueState, revenueTransactions, starsState, starsTransactions, peerData, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in
let (canViewStats, adsRestricted, canViewRevenue, canViewStarsRevenue) = peerData
let (canViewStats, adsRestricted, _, canViewStarsRevenue) = peerData
var canViewRevenue = peerData.2
let _ = canViewStatsValue.swap(canViewStats)
@ -2167,7 +2181,11 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
var headerItem: BoostHeaderItem?
var leftNavigationButton: ItemListNavigationButton?
var boostsOnly = false
if section == .boosts {
if existingRevenueContext != nil {
//TODO:localize
title = .text("Toncoin Balance")
canViewRevenue = true
} else if section == .boosts {
title = .text("")
let headerTitle = isGroup ? presentationData.strings.GroupBoost_Title : presentationData.strings.ChannelBoost_Title

View File

@ -20,15 +20,18 @@ private final class SheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let mode: MonetizationIntroScreen.Mode
let openMore: () -> Void
let dismiss: () -> Void
init(
context: AccountContext,
mode: MonetizationIntroScreen.Mode,
openMore: @escaping () -> Void,
dismiss: @escaping () -> Void
) {
self.context = context
self.mode = mode
self.openMore = openMore
self.dismiss = dismiss
}
@ -37,6 +40,9 @@ private final class SheetContent: CombinedComponent {
if lhs.context !== rhs.context {
return false
}
if lhs.mode != rhs.mode {
return false
}
return true
}
@ -136,7 +142,7 @@ private final class SheetContent: CombinedComponent {
let title = title.update(
component: BalancedTextComponent(
text: .plain(NSAttributedString(string: strings.Monetization_Intro_Title, font: titleFont, textColor: textColor)),
text: .plain(NSAttributedString(string: component.mode == .bot ? strings.Monetization_Intro_Bot_Title : strings.Monetization_Intro_Title, font: titleFont, textColor: textColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.1
@ -157,7 +163,7 @@ private final class SheetContent: CombinedComponent {
component: AnyComponent(ParagraphComponent(
title: strings.Monetization_Intro_Ads_Title,
titleColor: textColor,
text: strings.Monetization_Intro_Ads_Text,
text: component.mode == .bot ? strings.Monetization_Intro_Bot_Ads_Text : strings.Monetization_Intro_Ads_Text,
textColor: secondaryTextColor,
iconName: "Ads/Ads",
iconColor: linkColor
@ -343,13 +349,16 @@ private final class SheetContainerComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let mode: MonetizationIntroScreen.Mode
let openMore: () -> Void
init(
context: AccountContext,
mode: MonetizationIntroScreen.Mode,
openMore: @escaping () -> Void
) {
self.context = context
self.mode = mode
self.openMore = openMore
}
@ -357,6 +366,9 @@ private final class SheetContainerComponent: CombinedComponent {
if lhs.context !== rhs.context {
return false
}
if lhs.mode != rhs.mode {
return false
}
return true
}
@ -375,6 +387,7 @@ private final class SheetContainerComponent: CombinedComponent {
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(SheetContent(
context: context.component.context,
mode: context.component.mode,
openMore: context.component.openMore,
dismiss: {
animateOut.invoke(Action { _ in
@ -444,9 +457,15 @@ private final class SheetContainerComponent: CombinedComponent {
final class MonetizationIntroScreen: ViewControllerComponentContainer {
private let context: AccountContext
private var openMore: (() -> Void)?
enum Mode: Equatable {
case channel
case bot
}
init(
context: AccountContext,
mode: Mode,
openMore: @escaping () -> Void
) {
self.context = context
@ -456,6 +475,7 @@ final class MonetizationIntroScreen: ViewControllerComponentContainer {
context: context,
component: SheetContainerComponent(
context: context,
mode: mode,
openMore: openMore
),
navigationBarAppearance: .none,

View File

@ -1116,7 +1116,9 @@ public extension TelegramEngine.EngineData.Item {
guard let view = view as? CachedPeerDataView else {
preconditionFailure()
}
if let cachedData = view.cachedPeerData as? CachedChannelData {
if let cachedData = view.cachedPeerData as? CachedUserData {
return cachedData.flags.contains(.canViewRevenue)
} else if let cachedData = view.cachedPeerData as? CachedChannelData {
return cachedData.flags.contains(.canViewRevenue)
} else {
return false

View File

@ -111,6 +111,24 @@ public struct PresentationResourcesSettings {
drawBorder(context: context, rect: bounds)
})
public static let ton = generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
let path = UIBezierPath(roundedRect: bounds, cornerRadius: 7.0)
context.addPath(path.cgPath)
context.clip()
context.setFillColor(UIColor(rgb: 0x32ade6).cgColor)
context.fill(bounds)
if let image = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonAbout"), color: UIColor(rgb: 0xffffff)), let cgImage = image.cgImage {
context.draw(cgImage, in: CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - image.size.width) / 2.0), y: floorToScreenPixels((bounds.height - image.size.height) / 2.0)), size: image.size))
}
drawBorder(context: context, rect: bounds)
})
public static let stars = generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)

View File

@ -46,11 +46,6 @@ public func formatTonAmountText(_ value: Int64, dateTimeFormat: PresentationDate
break
}
}
if value < 0 {
balanceText.insert("-", at: balanceText.startIndex)
} else if showPlus {
balanceText.insert("+", at: balanceText.startIndex)
}
if let dotIndex = balanceText.range(of: dateTimeFormat.decimalSeparator) {
balanceText = String(balanceText[balanceText.startIndex ..< min(balanceText.endIndex, balanceText.index(dotIndex.upperBound, offsetBy: 2))])
@ -59,12 +54,22 @@ public func formatTonAmountText(_ value: Int64, dateTimeFormat: PresentationDate
if let integerPart = Int32(integerPartString) {
let modifiedIntegerPart = presentationStringsFormattedNumber(integerPart, dateTimeFormat.groupingSeparator)
let resultString = "\(modifiedIntegerPart)\(balanceText[dotIndex.lowerBound...])"
var resultString = "\(modifiedIntegerPart)\(balanceText[dotIndex.lowerBound...])"
if value < 0 {
resultString.insert("-", at: resultString.startIndex)
} else if showPlus {
resultString.insert("+", at: resultString.startIndex)
}
return resultString
}
} else if let integerPart = Int32(balanceText) {
balanceText = presentationStringsFormattedNumber(integerPart, dateTimeFormat.groupingSeparator)
}
if value < 0 {
balanceText.insert("-", at: balanceText.startIndex)
} else if showPlus {
balanceText.insert("+", at: balanceText.startIndex)
}
return balanceText
}

View File

@ -24,7 +24,7 @@ public final class GiftItemComponent: Component {
case red
case blue
var colors: [UIColor] {
func colors(theme: PresentationTheme) -> [UIColor] {
switch self {
case .red:
return [
@ -33,10 +33,17 @@ public final class GiftItemComponent: Component {
]
case .blue:
return [
UIColor(rgb: 0x34a4fc),
UIColor(rgb: 0x6fd3ff)
]
if theme.overallDarkAppearance {
return [
UIColor(rgb: 0x025799),
UIColor(rgb: 0x29a8e2)
]
} else {
return [
UIColor(rgb: 0x34a4fc),
UIColor(rgb: 0x6fd3ff)
]
}
}
}
}
@ -166,10 +173,15 @@ public final class GiftItemComponent: Component {
func update(component: GiftItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let isFirstTime = self.component == nil
let previousComponent = self.component
self.component = component
self.componentState = state
var themeUpdated = false
if previousComponent?.theme !== component.theme {
themeUpdated = true
}
let size = CGSize(width: availableSize.width, height: component.title != nil ? 178.0 : 154.0)
if component.isLoading {
@ -339,8 +351,8 @@ public final class GiftItemComponent: Component {
}
ribbonTextView.bounds = CGRect(origin: .zero, size: ribbonTextSize)
if self.ribbon.image == nil {
self.ribbon.image = generateGradientTintedImage(image: UIImage(bundleImageName: "Premium/GiftRibbon"), colors: ribbon.color.colors, direction: .diagonal)
if self.ribbon.image == nil || themeUpdated {
self.ribbon.image = generateGradientTintedImage(image: UIImage(bundleImageName: "Premium/GiftRibbon"), colors: ribbon.color.colors(theme: component.theme), direction: .diagonal)
}
if let ribbonImage = self.ribbon.image {
self.ribbon.frame = CGRect(origin: CGPoint(x: size.width - ribbonImage.size.width + 2.0, y: -2.0), size: ribbonImage.size)

View File

@ -556,6 +556,35 @@ private final class GiftViewSheetContent: CombinedComponent {
)
))
}
if savedToProfile {
tableItems.append(.init(
id: "visibility",
title: strings.Gift_View_Visibility,
component: AnyComponent(
HStack([
AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_View_Visibility_Visible, font: tableFont, textColor: tableTextColor))))
),
AnyComponentWithIdentity(
id: AnyHashable(1),
component: AnyComponent(Button(
content: AnyComponent(ButtonContentComponent(
context: component.context,
text: strings.Gift_View_Visibility_Hide,
color: theme.list.itemAccentColor
)),
action: {
component.updateSavedToProfile(false)
}
))
)
], spacing: 4.0)
),
insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 12.0)
))
}
if let text {
let attributedText = stringWithAppliedEntities(text, entities: entities ?? [], baseColor: tableTextColor, linkColor: tableLinkColor, baseFont: tableFont, linkFont: tableFont, boldFont: tableBoldFont, italicFont: tableItalicFont, boldItalicFont: tableBoldItalicFont, fixedFont: tableMonospaceFont, blockQuoteFont: tableFont, message: nil)
@ -637,7 +666,9 @@ private final class GiftViewSheetContent: CombinedComponent {
)
originY += additionalText.size.height
originY += 16.0
}
if incoming && !converted && !savedToProfile {
let button = button.update(
component: SolidRoundedButtonComponent(
title: savedToProfile ? strings.Gift_View_Hide : strings.Gift_View_Display,

View File

@ -208,7 +208,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
style: .media,
placeholder: .plain(presentationData.strings.MediaPicker_AddCaption),
maxLength: Int(self.context.userLimits.maxCaptionLength),
queryTypes: [.mention],
queryTypes: [.mention, .hashtag],
alwaysDarkWhenHasText: false,
resetInputContents: resetInputContents,
nextInputMode: { [weak self] _ in

View File

@ -119,7 +119,7 @@ public final class DrawingMessageRenderer {
let size = self.updateMessagesLayout(layout: layout, presentationData: mockPresentationData)
let _ = self.updateMessagesLayout(layout: layout, presentationData: mockPresentationData)
Queue.mainQueue().after(0.2, {
Queue.mainQueue().after(0.35, {
var mediaRect: CGRect?
if let messageNode = self.messageNodes?.first {
if self.isOverlay {
@ -343,7 +343,7 @@ public final class DrawingMessageRenderer {
}
public func render(completion: @escaping (Result) -> Void) {
Queue.mainQueue().after(0.12) {
Queue.mainQueue().justDispatch {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let defaultPresentationData = defaultPresentationData()

View File

@ -1229,7 +1229,7 @@ final class MediaEditorScreenComponent: Component {
style: .editor,
placeholder: .plain(environment.strings.Story_Editor_InputPlaceholderAddCaption),
maxLength: Int(component.context.userLimits.maxStoryCaptionLength),
queryTypes: [.mention],
queryTypes: [.mention, .hashtag],
alwaysDarkWhenHasText: false,
resetInputContents: nil,
nextInputMode: { _ in return nextInputMode },
@ -1363,12 +1363,12 @@ final class MediaEditorScreenComponent: Component {
header: header,
isChannel: false,
storyItem: nil,
chatLocation: nil
chatLocation: controller.customTarget.flatMap { .peer(id: $0) }
)),
environment: {},
containerSize: CGSize(width: inputPanelAvailableWidth, height: inputPanelAvailableHeight)
)
if self.inputPanelExternalState.isEditing && controller.node.entitiesView.hasSelection {
Queue.mainQueue().justDispatch {
controller.node.entitiesView.selectEntity(nil)

View File

@ -11,14 +11,18 @@ import PeerListItemComponent
final class ContextResultPanelComponent: Component {
enum Results: Equatable {
case mentions([EnginePeer])
case hashtags([String])
case hashtags(EnginePeer?, [String], String)
var count: Int {
switch self {
case let .hashtags(hashtags):
return hashtags.count
case let .mentions(peers):
return peers.count
case let .hashtags(peer, hashtags, query):
var count = hashtags.count
if let _ = peer, query.count >= 4 {
count += 2
}
return count
}
}
}
@ -186,38 +190,23 @@ final class ContextResultPanelComponent: Component {
let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -200.0)
// var synchronousLoad = false
// if let hint = transition.userData(PeerListItemComponent.TransitionHint.self) {
// synchronousLoad = hint.synchronousLoad
// }
var validIds: [AnyHashable] = []
if let range = itemLayout.visibleItems(for: visibleBounds), case let .mentions(peers) = component.results {
if let range = itemLayout.visibleItems(for: visibleBounds) {
for index in range.lowerBound ..< range.upperBound {
guard index < peers.count else {
guard index < component.results.count else {
continue
}
let itemFrame = itemLayout.itemFrame(for: index)
var itemTransition = transition
let peer = peers[index]
validIds.append(peer.id)
let id: AnyHashable
let visibleItem: ComponentView<Empty>
if let current = self.visibleItems[peer.id] {
visibleItem = current
} else {
if !transition.animation.isImmediate {
itemTransition = .immediate
}
visibleItem = ComponentView()
self.visibleItems[peer.id] = visibleItem
}
let _ = visibleItem.update(
transition: itemTransition,
component: AnyComponent(PeerListItemComponent(
let itemComponent: AnyComponent<Empty>
switch component.results {
case let .mentions(peers):
let peer = peers[index]
id = peer.id
itemComponent = AnyComponent(PeerListItemComponent(
context: component.context,
theme: component.theme,
strings: component.strings,
@ -236,21 +225,100 @@ final class ContextResultPanelComponent: Component {
}
component.action(.mention(peer))
}
)),
))
case let .hashtags(peer, hashtags, query):
var hashtagIndex = index
if let _ = peer, query.count >= 4 {
hashtagIndex -= 2
}
if let peer, let addressName = peer.addressName, hashtagIndex < 0 {
//TODO: localize
var isGroup = false
if case let .channel(channel) = peer, case .group = channel.info {
isGroup = true
}
id = hashtagIndex
if hashtagIndex == -2 {
itemComponent = AnyComponent(HashtagListItemComponent(
context: component.context,
theme: component.theme,
strings: component.strings,
peer: nil,
title: "Use #\(query)",
subtitle: "searches posts from all channels",
hashtag: query,
hasNext: index != hashtags.count - 1,
action: { [weak self] hashtag, _ in
guard let self, let component = self.component else {
return
}
component.action(.hashtag(query))
}
))
} else {
itemComponent = AnyComponent(HashtagListItemComponent(
context: component.context,
theme: component.theme,
strings: component.strings,
peer: peer,
title: "Use #\(query)@\(addressName)",
subtitle: isGroup ? "searches only posts from this group" : "searches only posts from this channel",
hashtag: "\(query)@\(addressName)",
hasNext: index != hashtags.count - 1,
action: { [weak self] hashtag, _ in
guard let self, let component = self.component else {
return
}
component.action(.hashtag("\(query)@\(addressName)"))
}
))
}
} else {
let hashtag = hashtags[hashtagIndex]
id = hashtag
itemComponent = AnyComponent(HashtagListItemComponent(
context: component.context,
theme: component.theme,
strings: component.strings,
peer: nil,
title: "#\(hashtag)",
subtitle: nil,
hashtag: hashtag,
hasNext: index != hashtags.count - 1,
action: { [weak self] hashtag, _ in
guard let self, let component = self.component else {
return
}
component.action(.hashtag(hashtag))
}
))
}
}
validIds.append(id)
let visibleItem: ComponentView<Empty>
if let current = self.visibleItems[id] {
visibleItem = current
} else {
if !transition.animation.isImmediate {
itemTransition = .immediate
}
visibleItem = ComponentView()
self.visibleItems[id] = visibleItem
}
let _ = visibleItem.update(
transition: itemTransition,
component: itemComponent,
environment: {},
containerSize: itemFrame.size
)
if let itemView = visibleItem.view {
// var animateIn = false
if itemView.superview == nil {
// animateIn = true
self.scrollView.addSubview(itemView)
}
itemTransition.setFrame(view: itemView, frame: itemFrame)
// if animateIn {
// itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
// }
}
}
}
@ -284,9 +352,10 @@ final class ContextResultPanelComponent: Component {
let sideInset: CGFloat = 3.0
self.backgroundView.updateColor(color: UIColor(white: 0.0, alpha: 0.7), transition: transition.containedViewLayoutTransition)
let measureItemSize = self.measureItem.update(
transition: .immediate,
component: AnyComponent(PeerListItemComponent(
let itemComponent: AnyComponent<Empty>
switch component.results {
case .mentions:
itemComponent = AnyComponent(PeerListItemComponent(
context: component.context,
theme: component.theme,
strings: component.strings,
@ -301,7 +370,25 @@ final class ContextResultPanelComponent: Component {
hasNext: true,
action: { _, _, _ in
}
)),
))
case .hashtags:
itemComponent = AnyComponent(HashtagListItemComponent(
context: component.context,
theme: component.theme,
strings: component.strings,
peer: nil,
title: "AAAAAAAAAAAA",
subtitle: nil,
hashtag: "",
hasNext: true,
action: { _, _ in
}
))
}
let measureItemSize = self.measureItem.update(
transition: .immediate,
component: itemComponent,
environment: {},
containerSize: CGSize(width: availableSize.width, height: 1000.0)
)

View File

@ -0,0 +1,509 @@
import Foundation
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import AccountContext
import TelegramCore
import Postbox
import MultilineTextComponent
import AvatarNode
import TelegramPresentationData
import CheckNode
import TelegramStringFormatting
import AppBundle
import PeerPresenceStatusManager
import EmojiStatusComponent
import ContextUI
import EmojiTextAttachmentView
import TextFormat
import PhotoResources
import ListSectionComponent
import ListItemSwipeOptionContainer
private let avatarFont = avatarPlaceholderFont(size: 15.0)
public final class HashtagListItemComponent: Component {
public final class TransitionHint {
public let synchronousLoad: Bool
public init(synchronousLoad: Bool) {
self.synchronousLoad = synchronousLoad
}
}
public final class InlineAction: Equatable {
public enum Color: Equatable {
case destructive
}
public let id: AnyHashable
public let title: String
public let color: Color
public let action: () -> Void
public init(id: AnyHashable, title: String, color: Color, action: @escaping () -> Void) {
self.id = id
self.title = title
self.color = color
self.action = action
}
public static func ==(lhs: InlineAction, rhs: InlineAction) -> Bool {
if lhs === rhs {
return true
}
if lhs.id != rhs.id {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.color != rhs.color {
return false
}
return true
}
}
public final class InlineActionsState: Equatable {
public let actions: [InlineAction]
public init(actions: [InlineAction]) {
self.actions = actions
}
public static func ==(lhs: InlineActionsState, rhs: InlineActionsState) -> Bool {
if lhs === rhs {
return true
}
if lhs.actions != rhs.actions {
return false
}
return true
}
}
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let peer: EnginePeer?
let title: String
let subtitle: String?
let hashtag: String
let hasNext: Bool
let action: (String, HashtagListItemComponent.View) -> Void
let contextAction: ((String, ContextExtractedContentContainingView, ContextGesture) -> Void)?
let inlineActions: InlineActionsState?
public init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
peer: EnginePeer?,
title: String,
subtitle: String?,
hashtag: String,
hasNext: Bool,
action: @escaping (String, HashtagListItemComponent.View) -> Void,
contextAction: ((String, ContextExtractedContentContainingView, ContextGesture) -> Void)? = nil,
inlineActions: InlineActionsState? = nil
) {
self.context = context
self.theme = theme
self.strings = strings
self.peer = peer
self.title = title
self.subtitle = subtitle
self.hashtag = hashtag
self.hasNext = hasNext
self.action = action
self.contextAction = contextAction
self.inlineActions = inlineActions
}
public static func ==(lhs: HashtagListItemComponent, rhs: HashtagListItemComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.subtitle != rhs.subtitle {
return false
}
if lhs.hashtag != rhs.hashtag {
return false
}
if lhs.hasNext != rhs.hasNext {
return false
}
if lhs.inlineActions != rhs.inlineActions {
return false
}
return true
}
public final class View: ContextControllerSourceView, ListSectionComponent.ChildView {
public let extractedContainerView: ContextExtractedContentContainingView
private let containerButton: HighlightTrackingButton
private let swipeOptionContainer: ListItemSwipeOptionContainer
private let iconBackgroundLayer = SimpleLayer()
private let iconLayer = SimpleLayer()
private let title = ComponentView<Empty>()
private var label = ComponentView<Empty>()
private let separatorLayer: SimpleLayer
private var avatarNode: AvatarNode?
private let badgeBackgroundLayer = SimpleLayer()
private var component: HashtagListItemComponent?
private weak var state: EmptyComponentState?
public var avatarFrame: CGRect {
if let avatarNode = self.avatarNode {
return avatarNode.frame
} else {
return CGRect(origin: CGPoint(), size: CGSize())
}
}
public var titleFrame: CGRect? {
return self.title.view?.frame
}
public var labelFrame: CGRect? {
guard let value = self.label.view?.frame else {
return nil
}
return value
}
private var isExtractedToContextMenu: Bool = false
public var customUpdateIsHighlighted: ((Bool) -> Void)?
public private(set) var separatorInset: CGFloat = 0.0
override init(frame: CGRect) {
self.separatorLayer = SimpleLayer()
self.iconBackgroundLayer.cornerRadius = 15.0
self.badgeBackgroundLayer.cornerRadius = 4.0
self.extractedContainerView = ContextExtractedContentContainingView()
self.containerButton = HighlightTrackingButton()
self.containerButton.layer.anchorPoint = CGPoint()
self.containerButton.isExclusiveTouch = true
self.swipeOptionContainer = ListItemSwipeOptionContainer(frame: CGRect())
super.init(frame: frame)
self.addSubview(self.extractedContainerView)
self.targetViewForActivationProgress = self.extractedContainerView.contentView
self.extractedContainerView.contentView.addSubview(self.swipeOptionContainer)
self.swipeOptionContainer.addSubview(self.containerButton)
self.layer.addSublayer(self.separatorLayer)
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.extractedContainerView.isExtractedToContextPreviewUpdated = { [weak self] value in
guard let self else {
return
}
self.containerButton.clipsToBounds = value
self.containerButton.backgroundColor = nil
self.containerButton.layer.cornerRadius = value ? 10.0 : 0.0
}
self.extractedContainerView.willUpdateIsExtractedToContextPreview = { [weak self] value, transition in
guard let self else {
return
}
self.isExtractedToContextMenu = value
let mappedTransition: ComponentTransition
if value {
mappedTransition = ComponentTransition(transition)
} else {
mappedTransition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut))
}
self.state?.updated(transition: mappedTransition)
}
self.activated = { [weak self] gesture, _ in
guard let self, let component = self.component else {
gesture.cancel()
return
}
component.contextAction?(component.hashtag, self.extractedContainerView, gesture)
}
self.containerButton.highligthedChanged = { [weak self] highlighted in
guard let self else {
return
}
if let customUpdateIsHighlighted = self.customUpdateIsHighlighted {
customUpdateIsHighlighted(highlighted)
}
}
self.swipeOptionContainer.updateRevealOffset = { [weak self] offset, transition in
guard let self else {
return
}
transition.setBounds(view: self.containerButton, bounds: CGRect(origin: CGPoint(x: -offset, y: 0.0), size: self.containerButton.bounds.size))
}
self.swipeOptionContainer.revealOptionSelected = { [weak self] option, _ in
guard let self, let component = self.component else {
return
}
guard let inlineActions = component.inlineActions else {
return
}
self.swipeOptionContainer.setRevealOptionsOpened(false, animated: true)
if let inlineAction = inlineActions.actions.first(where: { $0.id == option.key }) {
inlineAction.action()
}
}
self.containerButton.layer.addSublayer(self.iconBackgroundLayer)
self.iconBackgroundLayer.addSublayer(self.iconLayer)
self.containerButton.layer.addSublayer(self.badgeBackgroundLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
guard let component = self.component else {
return
}
component.action(component.hashtag, self)
}
func update(component: HashtagListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
var synchronousLoad = false
if let hint = transition.userData(TransitionHint.self) {
synchronousLoad = hint.synchronousLoad
}
self.isGestureEnabled = false
let themeUpdated = self.component?.theme !== component.theme
self.component = component
self.state = state
let labelData: (String, UIColor)
if let subtitle = component.subtitle {
labelData = (subtitle, component.theme.list.itemSecondaryTextColor)
} else {
labelData = ("", .clear)
}
let contextInset: CGFloat
if self.isExtractedToContextMenu {
contextInset = 12.0
} else {
contextInset = 0.0
}
let height: CGFloat = 42.0
let titleFont: UIFont = Font.semibold(14.0)
let subtitleFont: UIFont = Font.regular(14.0)
let verticalInset: CGFloat = 1.0
let leftInset: CGFloat = 55.0
let rightInset: CGFloat = 16.0
let avatarSize: CGFloat = 30.0
let avatarFrame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((height - verticalInset * 2.0 - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
if let peer = component.peer {
let avatarNode: AvatarNode
if let current = self.avatarNode {
avatarNode = current
} else {
avatarNode = AvatarNode(font: avatarFont)
avatarNode.isLayerBacked = false
avatarNode.isUserInteractionEnabled = false
self.avatarNode = avatarNode
self.containerButton.layer.insertSublayer(avatarNode.layer, at: 0)
}
if avatarNode.bounds.isEmpty {
avatarNode.frame = avatarFrame
} else {
transition.setFrame(layer: avatarNode.layer, frame: avatarFrame)
}
if peer.smallProfileImage != nil {
avatarNode.setPeerV2(
context: component.context,
theme: component.theme,
peer: peer,
authorOfMessage: nil,
overrideImage: nil,
emptyColor: nil,
clipStyle: .round,
synchronousLoad: synchronousLoad,
displayDimensions: CGSize(width: avatarSize, height: avatarSize)
)
} else {
avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, clipStyle: .round, synchronousLoad: synchronousLoad, displayDimensions: CGSize(width: avatarSize, height: avatarSize))
}
self.iconBackgroundLayer.isHidden = true
} else {
self.iconBackgroundLayer.isHidden = false
}
let previousTitleFrame = self.title.view?.frame
let titleAvailableWidth = availableSize.width - leftInset - rightInset
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.title, font: titleFont, textColor: component.theme.list.itemPrimaryTextColor))
)),
environment: {},
containerSize: CGSize(width: titleAvailableWidth, height: 100.0)
)
let labelAvailableWidth = availableSize.width - leftInset - rightInset
let labelColor: UIColor = labelData.1
let labelSize = self.label.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: labelData.0, font: subtitleFont, textColor: labelColor))
)),
environment: {},
containerSize: CGSize(width: labelAvailableWidth, height: 100.0)
)
let titleVerticalOffset: CGFloat = 0.0
let centralContentHeight: CGFloat
if labelSize.height > 0.0 {
centralContentHeight = titleSize.height + labelSize.height
} else {
centralContentHeight = titleSize.height
}
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: titleVerticalOffset + floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.isUserInteractionEnabled = false
self.containerButton.addSubview(titleView)
}
titleView.frame = titleFrame
if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x {
transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true)
}
}
if let labelView = self.label.view {
let labelFrame = CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY), size: labelSize)
if labelView.superview == nil {
labelView.isUserInteractionEnabled = false
labelView.layer.anchorPoint = CGPoint()
self.containerButton.addSubview(labelView)
labelView.center = labelFrame.origin
} else {
transition.setPosition(view: labelView, position: labelFrame.origin)
}
labelView.bounds = CGRect(origin: CGPoint(), size: labelFrame.size)
}
if self.iconLayer.contents == nil {
self.iconLayer.contents = UIImage(bundleImageName: "Chat/Hashtag/SuggestHashtag")?.cgImage
}
if themeUpdated {
let accentColor = UIColor(rgb: 0x007aff)
self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
self.iconBackgroundLayer.backgroundColor = accentColor.cgColor
self.iconLayer.layerTintColor = UIColor.white.cgColor
self.badgeBackgroundLayer.backgroundColor = accentColor.cgColor
}
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel)))
self.separatorLayer.isHidden = !component.hasNext
let iconSize = CGSize(width: 30.0, height: 30.0)
self.iconBackgroundLayer.frame = CGRect(origin: CGPoint(x: 12.0, y: floor((height - 30.0) / 2.0)), size: iconSize)
self.iconLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 30.0, height: 30.0))
let resultBounds = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height))
transition.setFrame(view: self.extractedContainerView, frame: resultBounds)
transition.setFrame(view: self.extractedContainerView.contentView, frame: resultBounds)
self.extractedContainerView.contentRect = resultBounds
let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0))
let swipeOptionContainerFrame = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: height))
transition.setFrame(view: self.swipeOptionContainer, frame: swipeOptionContainerFrame)
transition.setPosition(view: self.containerButton, position: containerFrame.origin)
transition.setBounds(view: self.containerButton, bounds: CGRect(origin: self.containerButton.bounds.origin, size: containerFrame.size))
self.separatorInset = leftInset
self.swipeOptionContainer.updateLayout(size: swipeOptionContainerFrame.size, leftInset: 0.0, rightInset: 0.0)
var rightOptions: [ListItemSwipeOptionContainer.Option] = []
if let inlineActions = component.inlineActions {
rightOptions = inlineActions.actions.map { action in
let color: UIColor
let textColor: UIColor
switch action.color {
case .destructive:
color = component.theme.list.itemDisclosureActions.destructive.fillColor
textColor = component.theme.list.itemDisclosureActions.destructive.foregroundColor
}
return ListItemSwipeOptionContainer.Option(
key: action.id,
title: action.title,
icon: .none,
color: color,
textColor: textColor
)
}
}
self.swipeOptionContainer.setRevealOptions(([], rightOptions))
return CGSize(width: availableSize.width, height: height)
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -472,6 +472,7 @@ public final class MessageInputPanelComponent: Component {
private var contextQueryStates: [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)] = [:]
private var contextQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult] = [:]
private var contextQueryPeer: EnginePeer?
private var contextQueryResultPanel: ComponentView<Empty>?
private var stickersResultPanel: ComponentView<Empty>?
@ -643,6 +644,17 @@ public final class MessageInputPanelComponent: Component {
}
let contextQueryUpdates = contextQueryResultState(context: context, inputState: inputState, availableTypes: availableTypes, chatLocation: component.chatLocation, currentQueryStates: &self.contextQueryStates)
if self.contextQueryPeer == nil, let peerId = component.chatLocation?.peerId {
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, peer?.addressName != nil else {
return
}
self.contextQueryPeer = peer
self.state?.updated(transition: .immediate)
})
}
for (kind, update) in contextQueryUpdates {
switch update {
case .remove:
@ -1871,6 +1883,8 @@ public final class MessageInputPanelComponent: Component {
var contextResults: ContextResultPanelComponent.Results?
if let result = self.contextQueryResults[.mention], case let .mentions(mentions) = result, !mentions.isEmpty {
contextResults = .mentions(mentions)
} else if let result = self.contextQueryResults[.hashtag], case let .hashtags(hashtags, query) = result, !hashtags.isEmpty || (query.count >= 4 && self.contextQueryPeer != nil) {
contextResults = .hashtags(self.contextQueryPeer, hashtags, query)
}
if let result = self.contextQueryResults[.emoji], case let .stickers(stickers) = result, !stickers.isEmpty {

View File

@ -383,6 +383,7 @@ final class PeerInfoScreenData {
let starsRevenueStatsState: StarsRevenueStats?
let starsRevenueStatsContext: StarsRevenueStatsContext?
let revenueStatsState: RevenueStats?
let revenueStatsContext: RevenueStatsContext?
let profileGiftsContext: ProfileGiftsContext?
let premiumGiftOptions: [PremiumGiftCodeOption]
@ -431,6 +432,7 @@ final class PeerInfoScreenData {
starsRevenueStatsState: StarsRevenueStats?,
starsRevenueStatsContext: StarsRevenueStatsContext?,
revenueStatsState: RevenueStats?,
revenueStatsContext: RevenueStatsContext?,
profileGiftsContext: ProfileGiftsContext?,
premiumGiftOptions: [PremiumGiftCodeOption]
) {
@ -467,6 +469,7 @@ final class PeerInfoScreenData {
self.starsRevenueStatsState = starsRevenueStatsState
self.starsRevenueStatsContext = starsRevenueStatsContext
self.revenueStatsState = revenueStatsState
self.revenueStatsContext = revenueStatsContext
self.profileGiftsContext = profileGiftsContext
self.premiumGiftOptions = premiumGiftOptions
}
@ -962,6 +965,7 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
starsRevenueStatsState: nil,
starsRevenueStatsContext: nil,
revenueStatsState: nil,
revenueStatsContext: nil,
profileGiftsContext: nil,
premiumGiftOptions: []
)
@ -1009,6 +1013,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
starsRevenueStatsState: nil,
starsRevenueStatsContext: nil,
revenueStatsState: nil,
revenueStatsContext: nil,
profileGiftsContext: nil,
premiumGiftOptions: []
))
@ -1264,15 +1269,15 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
let starsRevenueContextAndState = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> mapToSignal { peer -> Signal<(StarsRevenueStatsContext?, StarsRevenueStats?), NoError> in
var showStarsState = false
var canViewStarsRevenue = false
if let peer, case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) || context.sharedContext.applicationBindings.appBuildType == .internal {
showStarsState = true
canViewStarsRevenue = true
}
#if DEBUG
showStarsState = "".isEmpty
canViewStarsRevenue = "".isEmpty
#endif
guard showStarsState else {
guard canViewStarsRevenue else {
return .single((nil, nil))
}
let starsRevenueStatsContext = StarsRevenueStatsContext(account: context.account, peerId: peerId)
@ -1281,6 +1286,30 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
return (starsRevenueStatsContext, state.stats)
}
}
let revenueContextAndState = combineLatest(
context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> distinctUntilChanged,
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.CanViewRevenue(id: peerId))
|> distinctUntilChanged
)
|> mapToSignal { peer, canViewRevenue -> Signal<(RevenueStatsContext?, RevenueStats?), NoError> in
var canViewRevenue = canViewRevenue
if let peer, case let .user(user) = peer, let _ = user.botInfo, context.sharedContext.applicationBindings.appBuildType == .internal {
canViewRevenue = true
}
#if DEBUG
canViewRevenue = "".isEmpty
#endif
guard canViewRevenue else {
return .single((nil, nil))
}
let revenueStatsContext = RevenueStatsContext(account: context.account, peerId: peerId)
return revenueStatsContext.state
|> map { state -> (RevenueStatsContext?, RevenueStats?) in
return (revenueStatsContext, state.stats)
}
}
return combineLatest(
context.account.viewTracker.peerView(peerId, updateData: true),
@ -1299,9 +1328,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
peerInfoPersonalChannel(context: context, peerId: peerId, isSettings: false),
privacySettings,
starsRevenueContextAndState,
revenueContextAndState,
premiumGiftOptions
)
|> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState, premiumGiftOptions -> PeerInfoScreenData in
|> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState, revenueContextAndState, premiumGiftOptions -> PeerInfoScreenData in
var availablePanes = availablePanes
if isMyProfile {
availablePanes?.insert(.stories, at: 0)
@ -1417,7 +1447,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
starsState: nil,
starsRevenueStatsState: starsRevenueContextAndState.1,
starsRevenueStatsContext: starsRevenueContextAndState.0,
revenueStatsState: nil,
revenueStatsState: revenueContextAndState.1,
revenueStatsContext: revenueContextAndState.0,
profileGiftsContext: profileGiftsContext,
premiumGiftOptions: premiumGiftOptions
)
@ -1629,6 +1660,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
starsRevenueStatsState: starsRevenueContextAndState.1,
starsRevenueStatsContext: starsRevenueContextAndState.0,
revenueStatsState: revenueContextAndState.1,
revenueStatsContext: revenueContextAndState.0,
profileGiftsContext: nil,
premiumGiftOptions: []
)
@ -1931,6 +1963,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
starsRevenueStatsState: nil,
starsRevenueStatsContext: nil,
revenueStatsState: nil,
revenueStatsContext: nil,
profileGiftsContext: nil,
premiumGiftOptions: []
))

View File

@ -562,6 +562,7 @@ private final class PeerInfoInteraction {
let editingOpenNameColorSetup: () -> Void
let editingOpenInviteLinksSetup: () -> Void
let editingOpenDiscussionGroupSetup: () -> Void
let editingOpenRevenue: () -> Void
let editingOpenStars: () -> Void
let openParticipantsSection: (PeerInfoParticipantsSection) -> Void
let openRecentActions: () -> Void
@ -629,6 +630,7 @@ private final class PeerInfoInteraction {
editingOpenNameColorSetup: @escaping () -> Void,
editingOpenInviteLinksSetup: @escaping () -> Void,
editingOpenDiscussionGroupSetup: @escaping () -> Void,
editingOpenRevenue: @escaping () -> Void,
editingOpenStars: @escaping () -> Void,
openParticipantsSection: @escaping (PeerInfoParticipantsSection) -> Void,
openRecentActions: @escaping () -> Void,
@ -695,6 +697,7 @@ private final class PeerInfoInteraction {
self.editingOpenNameColorSetup = editingOpenNameColorSetup
self.editingOpenInviteLinksSetup = editingOpenInviteLinksSetup
self.editingOpenDiscussionGroupSetup = editingOpenDiscussionGroupSetup
self.editingOpenRevenue = editingOpenRevenue
self.editingOpenStars = editingOpenStars
self.openParticipantsSection = openParticipantsSection
self.openRecentActions = openRecentActions
@ -1503,25 +1506,40 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
}))
}
let revenueBalance = data.revenueStatsState?.balances.currentBalance ?? 0
let overallRevenueBalance = data.revenueStatsState?.balances.overallRevenue ?? 0
let starsBalance = data.starsRevenueStatsState?.balances.currentBalance ?? 0
let overallStarsBalance = data.starsRevenueStatsState?.balances.overallRevenue ?? 0
if overallStarsBalance > 0 {
var string = ""
if overallStarsBalance > 0 {
string.append("*\(presentationStringsFormattedNumber(Int32(starsBalance), presentationData.dateTimeFormat.groupingSeparator))")
}
let attributedString = NSMutableAttributedString(string: string, font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), textColor: presentationData.theme.list.itemSecondaryTextColor)
if let range = attributedString.string.range(of: "*") {
attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: attributedString.string))
attributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedString.string))
}
if overallRevenueBalance > 0 || overallStarsBalance > 0 {
//TODO:localize
items[.balances]!.append(PeerInfoScreenHeaderItem(id: 20, text: "BALANCE"))
items[.balances]!.append(PeerInfoScreenDisclosureItem(id: 21, label: .attributedText(attributedString), text: "Stars", icon: PresentationResourcesSettings.stars, action: {
interaction.editingOpenStars()
}))
if overallRevenueBalance > 0 {
let string = "*\(formatTonAmountText(revenueBalance, dateTimeFormat: presentationData.dateTimeFormat))"
let attributedString = NSMutableAttributedString(string: string, font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), textColor: presentationData.theme.list.itemSecondaryTextColor)
if let range = attributedString.string.range(of: "*") {
attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .ton), range: NSRange(range, in: attributedString.string))
attributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedString.string))
}
items[.balances]!.append(PeerInfoScreenDisclosureItem(id: 21, label: .attributedText(attributedString), text: "Toncoin", icon: PresentationResourcesSettings.ton, action: {
interaction.editingOpenRevenue()
}))
}
if overallStarsBalance > 0 {
let string = "*\(presentationStringsFormattedNumber(Int32(starsBalance), presentationData.dateTimeFormat.groupingSeparator))"
let attributedString = NSMutableAttributedString(string: string, font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), textColor: presentationData.theme.list.itemSecondaryTextColor)
if let range = attributedString.string.range(of: "*") {
attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: attributedString.string))
attributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedString.string))
}
items[.balances]!.append(PeerInfoScreenDisclosureItem(id: 22, label: .attributedText(attributedString), text: "Stars", icon: PresentationResourcesSettings.stars, action: {
interaction.editingOpenStars()
}))
}
} else {
print()
}
if let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) {
@ -2772,6 +2790,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
editingOpenDiscussionGroupSetup: { [weak self] in
self?.editingOpenDiscussionGroupSetup()
},
editingOpenRevenue: { [weak self] in
self?.editingOpenRevenue()
},
editingOpenStars: { [weak self] in
self?.editingOpenStars()
},
@ -8524,6 +8545,15 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
self.controller?.push(channelDiscussionGroupSetupController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id))
}
private func editingOpenRevenue() {
guard let revenueContext = self.data?.revenueStatsContext else {
return
}
let controller = channelStatsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId, section: .monetization, existingRevenueContext: revenueContext, boostStatus: nil)
self.controller?.push(controller)
}
private func editingOpenStars() {
guard let revenueContext = self.data?.starsRevenueStatsContext else {
return

View File

@ -104,7 +104,11 @@ final class StarsStatisticsScreenComponent: Component {
}
if let view = gestureRecognizer.view?.hitTest(gestureRecognizer.location(in: gestureRecognizer.view), with: nil) as? UIControl {
return !view.isTracking
if view is UIButton {
return true
} else {
return !view.isTracking
}
}
return true
@ -181,8 +185,8 @@ final class StarsStatisticsScreenComponent: Component {
super.init(frame: frame)
self.scrollView.delaysContentTouches = false
// self.scrollView.canCancelContentTouches = true
self.scrollView.delaysContentTouches = true
self.scrollView.canCancelContentTouches = true
self.scrollView.clipsToBounds = false
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.scrollView.contentInsetAdjustmentBehavior = .never
@ -677,7 +681,7 @@ final class StarsStatisticsScreenComponent: Component {
panel: AnyComponent(StarsTransactionsListPanelComponent(
context: component.context,
transactionsContext: allTransactionsContext,
isAccount: true,
isAccount: false,
action: { transaction in
component.openTransaction(transaction)
}
@ -690,7 +694,7 @@ final class StarsStatisticsScreenComponent: Component {
panel: AnyComponent(StarsTransactionsListPanelComponent(
context: component.context,
transactionsContext: incomingTransactionsContext,
isAccount: true,
isAccount: false,
action: { transaction in
component.openTransaction(transaction)
}
@ -703,7 +707,7 @@ final class StarsStatisticsScreenComponent: Component {
panel: AnyComponent(StarsTransactionsListPanelComponent(
context: component.context,
transactionsContext: outgoingTransactionsContext,
isAccount: true,
isAccount: false,
action: { transaction in
component.openTransaction(transaction)
}

View File

@ -136,8 +136,8 @@ final class StarsTransactionsListPanelComponent: Component {
super.init(frame: frame)
self.scrollView.delaysContentTouches = false
// self.scrollView.canCancelContentTouches = true
self.scrollView.delaysContentTouches = true
self.scrollView.canCancelContentTouches = true
self.scrollView.clipsToBounds = false
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.scrollView.contentInsetAdjustmentBehavior = .never

View File

@ -152,8 +152,8 @@ final class StarsTransactionsScreenComponent: Component {
super.init(frame: frame)
self.scrollView.delaysContentTouches = false
// self.scrollView.canCancelContentTouches = true
self.scrollView.delaysContentTouches = true
self.scrollView.canCancelContentTouches = true
self.scrollView.clipsToBounds = false
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.scrollView.contentInsetAdjustmentBehavior = .never

View File

@ -2864,7 +2864,7 @@ public final class StoryItemSetContainerComponent: Component {
style: .story,
placeholder: inputPlaceholder,
maxLength: 4096,
queryTypes: [.mention, .emoji],
queryTypes: [.mention, .hashtag, .emoji],
alwaysDarkWhenHasText: component.metrics.widthClass == .regular,
resetInputContents: resetInputContents,
nextInputMode: { [weak self] hasText in

View File

@ -15,8 +15,10 @@ import ChatControllerInteraction
import ChatContextQuery
import ChatInputContextPanelNode
private struct HashtagChatInputContextPanelEntryStableId: Hashable {
let title: String
private enum HashtagChatInputContextPanelEntryStableId: Hashable {
case generic
case peer
case hashtag(String)
}
private struct HashtagChatInputContextPanelEntry: Comparable, Identifiable {
@ -31,7 +33,14 @@ private struct HashtagChatInputContextPanelEntry: Comparable, Identifiable {
let isAdditionalRecent: Bool
var stableId: HashtagChatInputContextPanelEntryStableId {
return HashtagChatInputContextPanelEntryStableId(title: self.title)
switch self.index {
case 0:
return .generic
case 1:
return .peer
default:
return .hashtag(self.title)
}
}
func withUpdatedTheme(_ theme: PresentationTheme) -> HashtagChatInputContextPanelEntry {
@ -128,12 +137,16 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode {
stableIds.insert(genericEntry.stableId)
entries.append(genericEntry)
var isGroup = false
if case let .channel(channel) = peer, case .group = channel.info {
isGroup = true
}
let peerEntry = HashtagChatInputContextPanelEntry(
index: 1,
theme: self.theme,
peer: peer,
title: "Use #\(query)@\(addressName)",
text: "searches only posts from this channel",
text: isGroup ? "searches only posts from this group" : "searches only posts from this channel",
badge: "NEW",
hashtag: "\(query)@\(addressName)",
revealed: false,

View File

@ -93,11 +93,11 @@ final class HashtagChatInputPanelItem: ListViewItem {
if self.revealed {
self.setHashtagRevealed(nil)
} else {
if self.isAdditionalRecent {
self.hashtagSelected(self.hashtag)
} else {
// if self.isAdditionalRecent {
// self.hashtagSelected(self.hashtag)
// } else {
self.hashtagSelected(self.hashtag + " ")
}
// }
}
}
}
@ -254,7 +254,7 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode {
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.iconBackgroundLayer.frame = CGRect(origin: CGPoint(x: params.leftInset + 12.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 {
@ -274,7 +274,7 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode {
}
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 + textLeftInset, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: params.width - leftInset - textLeftInset, height: UIScreenPixel))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: nodeLayout.size.height + UIScreenPixel))

View File

@ -2362,7 +2362,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
updatedPresentationData: nil,
peer: peer._asPeer(),
mode: .generic,
avatarInitiallyExpanded: true,
avatarInitiallyExpanded: peer.smallProfileImage != nil,
fromChat: false,
requestsContext: nil
) {