diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 5d7e5dbc9b..78cdaa13c3 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -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"; diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index 048c815643..5c70a42f0b 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -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)? = nil, peerId: PeerId, section: ChannelStatsSection = .stats, boostStatus: ChannelBoostStatus? = nil, boostStatusUpdated: ((ChannelBoostStatus) -> Void)? = nil) -> ViewController { +public func channelStatsController( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = 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() 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 diff --git a/submodules/StatisticsUI/Sources/MonetizationIntroScreen.swift b/submodules/StatisticsUI/Sources/MonetizationIntroScreen.swift index cb130cd736..2dc7e1687b 100644 --- a/submodules/StatisticsUI/Sources/MonetizationIntroScreen.swift +++ b/submodules/StatisticsUI/Sources/MonetizationIntroScreen.swift @@ -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( content: AnyComponent(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, diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index d36eeccb24..f8ef324e33 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -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 diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift index 2eef259c90..584f6b142e 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift @@ -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) diff --git a/submodules/TelegramStringFormatting/Sources/TonFormat.swift b/submodules/TelegramStringFormatting/Sources/TonFormat.swift index 7f3f05d663..f5e49e1fd6 100644 --- a/submodules/TelegramStringFormatting/Sources/TonFormat.swift +++ b/submodules/TelegramStringFormatting/Sources/TonFormat.swift @@ -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 } diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index bf64620198..fc798022ac 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -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, 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) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 3396ef3132..6ac2b53c30 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -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, diff --git a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift index 70f131dbfb..abea4059e8 100644 --- a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift +++ b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift @@ -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 diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift index f190635001..482d271297 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift @@ -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() diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index bc9ec74e11..159e256163 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -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) diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/ContextResultPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/ContextResultPanelComponent.swift index 634c62295c..956f088e41 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/ContextResultPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/ContextResultPanelComponent.swift @@ -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 - 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 + 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 + 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 + 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) ) diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/HashtagListItemComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/HashtagListItemComponent.swift new file mode 100644 index 0000000000..46e7efd3c4 --- /dev/null +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/HashtagListItemComponent.swift @@ -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() + private var label = ComponentView() + 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, 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, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 3c678b4622..199fb10d6e 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -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? private var stickersResultPanel: ComponentView? @@ -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 { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index 16eafe257e..647c1d93a2 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -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: [] )) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 515a0fa010..98cd70ea86 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift index fe094d65b1..e3df2ee96a 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift @@ -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) } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift index 713d2533f1..0229863e8e 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index 276716b6cb..22e2aa4e50 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 5c26840dfd..d82ca5abaf 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -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 diff --git a/submodules/TelegramUI/Sources/HashtagChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/HashtagChatInputContextPanelNode.swift index 26ec77e4ea..943a4ab4c4 100644 --- a/submodules/TelegramUI/Sources/HashtagChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/HashtagChatInputContextPanelNode.swift @@ -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, diff --git a/submodules/TelegramUI/Sources/HashtagChatInputPanelItem.swift b/submodules/TelegramUI/Sources/HashtagChatInputPanelItem.swift index 594071f323..899cbe278a 100644 --- a/submodules/TelegramUI/Sources/HashtagChatInputPanelItem.swift +++ b/submodules/TelegramUI/Sources/HashtagChatInputPanelItem.swift @@ -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)) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index ab8f61436c..9a368f2e91 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -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 ) {