[WIP] Post suggestions

This commit is contained in:
Isaac 2025-04-28 23:34:03 +02:00
parent d6334b9748
commit 084bb5bcd5
21 changed files with 412 additions and 93 deletions

View File

@ -996,8 +996,13 @@ public enum SendInviteLinkScreenSubject {
}
public enum StarsWithdrawalScreenSubject {
public enum PaidMessageKind {
case privacy
case postSuggestion
}
case withdraw
case enterAmount(current: StarsAmount)
case enterAmount(current: StarsAmount, minValue: StarsAmount, fractionAfterCommission: Int, kind: PaidMessageKind)
}
public protocol SharedAccountContext: AnyObject {

View File

@ -376,7 +376,7 @@ public func incomingMessagePrivacyScreen(context: AccountContext, value: GlobalP
if case let .paidMessages(value) = stateValue.with({ $0 }).updatedValue {
currentAmount = value
}
let starsScreen = context.sharedContext.makeStarsWithdrawalScreen(context: context, subject: .enterAmount(current: currentAmount), completion: { amount in
let starsScreen = context.sharedContext.makeStarsWithdrawalScreen(context: context, subject: .enterAmount(current: currentAmount, minValue: StarsAmount(value: 1, nanos: 0), fractionAfterCommission: 80, kind: .privacy), completion: { amount in
updateState { state in
var state = state
state.updatedValue = .paidMessages(StarsAmount(value: amount, nanos: 0))

View File

@ -321,48 +321,88 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
return self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: transition, interfaceState: interfaceState, metrics: metrics, force: false)
}
private var displayedGiftTooltip = false
private func presentGiftTooltip() {
guard let context = self.context, !self.displayedGiftTooltip, let parentController = self.interfaceInteraction?.chatController() else {
private var displayedGiftOrSuggestTooltip = false
private func presentGiftOrSuggestTooltip() {
guard let context = self.context, !self.displayedGiftOrSuggestTooltip, let parentController = self.interfaceInteraction?.chatController() else {
return
}
self.displayedGiftTooltip = true
self.displayedGiftOrSuggestTooltip = true
let _ = (ApplicationSpecificNotice.getChannelSendGiftTooltip(accountManager: context.sharedContext.accountManager)
|> deliverOnMainQueue).start(next: { [weak self] count in
let _ = (combineLatest(queue: .mainQueue(),
ApplicationSpecificNotice.getChannelSendGiftTooltip(accountManager: context.sharedContext.accountManager),
ApplicationSpecificNotice.getChannelSuggestTooltip(accountManager: context.sharedContext.accountManager)
|> deliverOnMainQueue)).start(next: { [weak self] giftCount, suggestCount in
guard let self else {
return
}
guard count < 2 else {
return
/*#if DEBUG
var giftCount = giftCount
var suggestCount = suggestCount
if "".isEmpty {
giftCount = 2
suggestCount = 0
}
#endif*/
let _ = ApplicationSpecificNotice.incrementChannelSendGiftTooltip(accountManager: context.sharedContext.accountManager).start()
Queue.mainQueue().after(0.4, {
let absoluteFrame = self.giftButton.view.convert(self.giftButton.bounds, to: parentController.view)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY + 11.0), size: CGSize())
if giftCount < 2 && !self.giftButton.isHidden {
let _ = ApplicationSpecificNotice.incrementChannelSendGiftTooltip(accountManager: context.sharedContext.accountManager).start()
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let text: String = presentationData.strings.Chat_SendGiftTooltip
let tooltipController = TooltipScreen(
account: context.account,
sharedContext: context.sharedContext,
text: .plain(text: text),
balancedTextLayout: false,
style: .wide,
arrowStyle: .small,
icon: nil,
location: .point(location, .bottom),
displayDuration: .default,
inset: 8.0,
shouldDismissOnTouch: { _, _ in
return .ignore
}
)
self.interfaceInteraction?.presentControllerInCurrent(tooltipController, nil)
})
Queue.mainQueue().after(0.4, {
let absoluteFrame = self.giftButton.view.convert(self.giftButton.bounds, to: parentController.view)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY + 11.0), size: CGSize())
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let text: String = presentationData.strings.Chat_SendGiftTooltip
let tooltipController = TooltipScreen(
account: context.account,
sharedContext: context.sharedContext,
text: .plain(text: text),
balancedTextLayout: false,
style: .wide,
arrowStyle: .small,
icon: nil,
location: .point(location, .bottom),
displayDuration: .default,
inset: 8.0,
shouldDismissOnTouch: { _, _ in
return .ignore
}
)
self.interfaceInteraction?.presentControllerInCurrent(tooltipController, nil)
})
} else if suggestCount < 2 && !self.suggestedPostButton.isHidden {
let _ = ApplicationSpecificNotice.incrementChannelSuggestTooltip(accountManager: context.sharedContext.accountManager).start()
Queue.mainQueue().after(0.4, {
let absoluteFrame = self.suggestedPostButton.view.convert(self.suggestedPostButton.bounds, to: parentController.view)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY + 11.0), size: CGSize())
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let _ = presentationData
//TODO:localize
let text: String = "Tap here to suggest a message"
let tooltipController = TooltipScreen(
account: context.account,
sharedContext: context.sharedContext,
text: .plain(text: text),
textBadge: "NEW",
balancedTextLayout: false,
style: .wide,
arrowStyle: .small,
icon: nil,
location: .point(location, .bottom),
displayDuration: .default,
inset: 8.0,
shouldDismissOnTouch: { _, _ in
return .ignore
}
)
self.interfaceInteraction?.presentControllerInCurrent(tooltipController, nil)
})
}
})
}
@ -377,7 +417,7 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
if previousState?.theme !== interfaceState.theme {
self.badgeBackground.image = PresentationResourcesChatList.badgeBackgroundActive(interfaceState.theme, diameter: 20.0)
self.helpButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/Help"), color: interfaceState.theme.chat.inputPanel.panelControlAccentColor), for: .normal)
self.suggestedPostButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/Gift"), color: interfaceState.theme.chat.inputPanel.panelControlAccentColor), for: .normal)
self.suggestedPostButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/SuggestPost"), color: interfaceState.theme.chat.inputPanel.panelControlAccentColor), for: .normal)
self.giftButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/Gift"), color: interfaceState.theme.chat.inputPanel.panelControlAccentColor), for: .normal)
}
@ -431,11 +471,12 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
self.helpButton.isHidden = true
//TODO:release
self.suggestedPostButton.isHidden = false
self.presentGiftTooltip()
self.presentGiftOrSuggestTooltip()
} else if case .broadcast = peer.info {
self.giftButton.isHidden = true
self.helpButton.isHidden = true
self.suggestedPostButton.isHidden = false
self.presentGiftOrSuggestTooltip()
} else if peer.flags.contains(.isGigagroup), self.action == .muteNotifications || self.action == .unmuteNotifications {
self.giftButton.isHidden = true
self.helpButton.isHidden = false

View File

@ -32,6 +32,7 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatMediaInputStickerGridItem",
"//submodules/PremiumUI",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/Components/BundleIconComponent",
],
visibility = [
"//visibility:public",

View File

@ -23,6 +23,7 @@ import ChatMediaInputStickerGridItem
import UndoUI
import PremiumUI
import LottieComponent
import BundleIconComponent
private protocol ChatEmptyNodeContent {
func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize
@ -1367,16 +1368,27 @@ public final class ChatEmptyNodePremiumRequiredChatContent: ASDisplayNode, ChatE
contentsHeight += iconBackgroundSize
contentsHeight += iconTextSpacing
let iconSize = self.icon.update(
transition: .immediate,
component: AnyComponent(
let iconComponent: AnyComponent<Empty>
if case let .customChatContents(customChatContents) = interfaceState.subject, case .postSuggestions = customChatContents.kind {
iconComponent = AnyComponent(
BundleIconComponent(
name: "Chat/Empty Chat/PostSuggestions",
tintColor: serviceColor.primaryText
)
)
} else {
iconComponent = AnyComponent(
LottieComponent(
content: LottieComponent.AppBundleContent(name: "PremiumRequired"),
color: serviceColor.primaryText,
size: CGSize(width: 120.0, height: 120.0),
loop: true
)
),
)
}
let iconSize = self.icon.update(
transition: .immediate,
component: iconComponent,
environment: {},
containerSize: CGSize(width: maxWidth - sideInset * 2.0, height: 500.0)
)

View File

@ -26,6 +26,7 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, ASScrollViewDel
private let backgroundNode: ASDisplayNode
private let contentBackgroundNode: ASDisplayNode
private let titleNode: ASTextNode
private let textNode: ASTextNode?
private let cancelButton: HighlightableButtonNode
private let doneButton: SolidRoundedButtonNode
private let onlineButton: SolidRoundedButtonNode
@ -93,6 +94,7 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, ASScrollViewDel
self.contentBackgroundNode.backgroundColor = backgroundColor
let title: String
var text: String?
switch mode {
case .scheduledMessages:
title = self.presentationData.strings.Conversation_ScheduleMessage_Title
@ -101,6 +103,7 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, ASScrollViewDel
case .suggestPost:
//TODO:localize
title = "Time"
text = "Set the date and time you want\nyour message to be published."
}
self.titleNode = ASTextNode()
@ -108,6 +111,19 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, ASScrollViewDel
self.titleNode.accessibilityLabel = title
self.titleNode.accessibilityTraits = [.staticText]
if let text {
let textNode = ASTextNode()
textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: textColor)
textNode.maximumNumberOfLines = 0
textNode.textAlignment = .center
textNode.lineSpacing = 0.2
textNode.accessibilityLabel = text
textNode.accessibilityTraits = [.staticText]
self.textNode = textNode
} else {
self.textNode = nil
}
self.cancelButton = HighlightableButtonNode()
self.cancelButton.setTitle(self.presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: accentColor, for: .normal)
self.cancelButton.accessibilityLabel = self.presentationData.strings.Common_Cancel
@ -146,6 +162,9 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, ASScrollViewDel
self.backgroundNode.addSubnode(self.effectNode)
self.backgroundNode.addSubnode(self.contentBackgroundNode)
self.contentContainerNode.addSubnode(self.titleNode)
if let textNode = self.textNode {
self.contentContainerNode.addSubnode(textNode)
}
self.contentContainerNode.addSubnode(self.cancelButton)
self.contentContainerNode.addSubnode(self.doneButton)
if case .scheduledMessages(true) = self.mode {
@ -422,6 +441,13 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, ASScrollViewDel
}
let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0)
let textControlSpacing: CGFloat = -8.0
let textDoneSpacing: CGFloat = 21.0
let textSize = self.textNode?.measure(CGSize(width: width, height: 1000.0))
if let textSize {
contentHeight += textSize.height + textControlSpacing + textDoneSpacing
}
let sideInset = floor((layout.size.width - width) / 2.0)
let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - contentHeight), size: CGSize(width: width, height: contentHeight))
let contentFrame = contentContainerFrame
@ -446,7 +472,13 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, ASScrollViewDel
let buttonInset: CGFloat = 16.0
let doneButtonHeight = self.doneButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition)
transition.updateFrame(node: self.doneButton, frame: CGRect(x: buttonInset, y: contentHeight - doneButtonHeight - insets.bottom - 16.0 - buttonOffset, width: contentFrame.width, height: doneButtonHeight))
let doneButtonFrame = CGRect(x: buttonInset, y: contentHeight - doneButtonHeight - insets.bottom - 16.0 - buttonOffset, width: contentFrame.width, height: doneButtonHeight)
transition.updateFrame(node: self.doneButton, frame: doneButtonFrame)
if let textNode = self.textNode, let textSize {
let textFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - textSize.width) / 2.0), y: doneButtonFrame.minY - textDoneSpacing - textSize.height), size: textSize)
transition.updateFrame(node: textNode, frame: textFrame)
}
let onlineButtonHeight = self.onlineButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition)
transition.updateFrame(node: self.onlineButton, frame: CGRect(x: buttonInset, y: contentHeight - onlineButtonHeight - cleanInsets.bottom - 16.0, width: contentFrame.width, height: onlineButtonHeight))

View File

@ -23,6 +23,7 @@ swift_library(
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramUI/Components/ListItemComponentAdaptor",
],
visibility = [
"//visibility:public",

View File

@ -12,11 +12,12 @@ import ComponentFlow
import ButtonComponent
import BundleIconComponent
import MultilineTextComponent
import ListItemComponentAdaptor
private let textFont = Font.with(size: 17.0, traits: .monospacedNumbers)
private let smallTextFont = Font.with(size: 13.0, traits: .monospacedNumbers)
public final class MessagePriceItem: ListViewItem, ItemListItem {
public final class MessagePriceItem: Equatable, ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator {
let theme: PresentationTheme
let strings: PresentationStrings
let isEnabled: Bool
@ -75,11 +76,46 @@ public final class MessagePriceItem: ListViewItem, ItemListItem {
}
}
}
public func item() -> ListViewItem {
return self
}
public static func ==(lhs: MessagePriceItem, rhs: MessagePriceItem) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.isEnabled != rhs.isEnabled {
return false
}
if lhs.minValue != rhs.minValue {
return false
}
if lhs.value != rhs.value {
return false
}
if lhs.price != rhs.price {
return false
}
if (lhs.openSetCustom == nil) != (rhs.openSetCustom == nil) {
return false
}
if (lhs.openPremiumInfo == nil) != (rhs.openPremiumInfo == nil) {
return false
}
return true
}
}
private class MessagePriceItemNode: ListViewItemNode {
private struct Amount: Equatable {
private let sliderSteps: [Int]
private let minRealValue: Int
private let maxRealValue: Int
let maxSliderValue: Int
private let isLogarithmic: Bool
@ -87,9 +123,9 @@ private class MessagePriceItemNode: ListViewItemNode {
private(set) var realValue: Int
private(set) var sliderValue: Int
private static func makeSliderSteps(maxRealValue: Int, isLogarithmic: Bool) -> [Int] {
private static func makeSliderSteps(minRealValue: Int, maxRealValue: Int, isLogarithmic: Bool) -> [Int] {
if isLogarithmic {
var sliderSteps: [Int] = [ 1, 10, 50, 100, 500, 1_000, 2_000, 5_000, 7_500, 10_000 ]
var sliderSteps: [Int] = [ minRealValue, 10, 50, 100, 500, 1_000, 2_000, 5_000, 7_500, 10_000 ]
sliderSteps.removeAll(where: { $0 >= maxRealValue })
sliderSteps.append(maxRealValue)
return sliderSteps
@ -126,8 +162,9 @@ private class MessagePriceItemNode: ListViewItemNode {
}
}
init(realValue: Int, maxRealValue: Int, maxSliderValue: Int, isLogarithmic: Bool) {
self.sliderSteps = Amount.makeSliderSteps(maxRealValue: maxRealValue, isLogarithmic: isLogarithmic)
init(realValue: Int, minRealValue: Int, maxRealValue: Int, maxSliderValue: Int, isLogarithmic: Bool) {
self.sliderSteps = Amount.makeSliderSteps(minRealValue: minRealValue, maxRealValue: maxRealValue, isLogarithmic: isLogarithmic)
self.minRealValue = minRealValue
self.maxRealValue = maxRealValue
self.maxSliderValue = maxSliderValue
self.isLogarithmic = isLogarithmic
@ -136,8 +173,9 @@ private class MessagePriceItemNode: ListViewItemNode {
self.sliderValue = Amount.remapValueToSlider(realValue: self.realValue, maxSliderValue: self.maxSliderValue, steps: self.sliderSteps)
}
init(sliderValue: Int, maxRealValue: Int, maxSliderValue: Int, isLogarithmic: Bool) {
self.sliderSteps = Amount.makeSliderSteps(maxRealValue: maxRealValue, isLogarithmic: isLogarithmic)
init(sliderValue: Int, minRealValue: Int, maxRealValue: Int, maxSliderValue: Int, isLogarithmic: Bool) {
self.sliderSteps = Amount.makeSliderSteps(minRealValue: minRealValue, maxRealValue: maxRealValue, isLogarithmic: isLogarithmic)
self.minRealValue = minRealValue
self.maxRealValue = maxRealValue
self.maxSliderValue = maxSliderValue
self.isLogarithmic = isLogarithmic
@ -147,11 +185,11 @@ private class MessagePriceItemNode: ListViewItemNode {
}
func withRealValue(_ realValue: Int) -> Amount {
return Amount(realValue: realValue, maxRealValue: self.maxRealValue, maxSliderValue: self.maxSliderValue, isLogarithmic: self.isLogarithmic)
return Amount(realValue: realValue, minRealValue: self.minRealValue, maxRealValue: self.maxRealValue, maxSliderValue: self.maxSliderValue, isLogarithmic: self.isLogarithmic)
}
func withSliderValue(_ sliderValue: Int) -> Amount {
return Amount(sliderValue: sliderValue, maxRealValue: self.maxRealValue, maxSliderValue: self.maxSliderValue, isLogarithmic: self.isLogarithmic)
return Amount(sliderValue: sliderValue, minRealValue: self.minRealValue, maxRealValue: self.maxRealValue, maxSliderValue: self.maxSliderValue, isLogarithmic: self.isLogarithmic)
}
}
@ -171,7 +209,7 @@ private class MessagePriceItemNode: ListViewItemNode {
private let button: ComponentView<Empty>
private var amount: Amount = Amount(realValue: 1, maxRealValue: 1000, maxSliderValue: 1000, isLogarithmic: true)
private var amount: Amount = Amount(realValue: 1, minRealValue: 1, maxRealValue: 1000, maxSliderValue: 1000, isLogarithmic: true)
private var item: MessagePriceItem?
private var layoutParams: ListViewItemLayoutParams?
@ -195,7 +233,9 @@ private class MessagePriceItemNode: ListViewItemNode {
self.centerTextButtonBackground = UIImageView()
self.centerLeftTextNode = ImmediateTextNode()
self.centerLeftTextNode.isUserInteractionEnabled = false
self.centerLeftTextNode.displaysAsynchronously = false
self.centerRightTextNode = ImmediateTextNode()
self.centerRightTextNode.displaysAsynchronously = false
self.centerRightTextNode.isUserInteractionEnabled = false
self.lockIconNode = ASImageNode()
@ -226,7 +266,7 @@ private class MessagePriceItemNode: ListViewItemNode {
sliderView.lineSize = 4.0
sliderView.disablesInteractiveTransitionGestureRecognizer = true
if let item = self.item, let params = self.layoutParams {
self.amount = Amount(realValue: Int(item.value), maxRealValue: Int(item.maxValue), maxSliderValue: 999, isLogarithmic: true)
self.amount = Amount(realValue: Int(item.value), minRealValue: Int(item.minValue), maxRealValue: Int(item.maxValue), maxSliderValue: 999, isLogarithmic: true)
sliderView.minimumValue = 0
sliderView.startValue = 0
@ -328,7 +368,9 @@ private class MessagePriceItemNode: ListViewItemNode {
strongSelf.leftTextNode.attributedText = NSAttributedString(string: "\(item.minValue)", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor)
strongSelf.rightTextNode.attributedText = NSAttributedString(string: "\(item.maxValue)", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor)
let centralLeftText = item.strings.Privacy_Messages_Stars(Int32(item.value))
//TODO:localize
let centralLeftText = item.value == 0 ? "Free" : item.strings.Privacy_Messages_Stars(Int32(item.value))
strongSelf.centerLeftTextNode.attributedText = NSAttributedString(string: centralLeftText, font: textFont, textColor: item.openSetCustom != nil ? item.theme.list.itemAccentColor : item.theme.list.itemPrimaryTextColor)
strongSelf.centerRightTextNode.attributedText = NSAttributedString(string: item.price, font: smallTextFont, textColor: item.openSetCustom != nil ? item.theme.list.itemAccentColor.withMultipliedAlpha(0.5) : item.theme.list.itemSecondaryTextColor)
@ -336,7 +378,7 @@ private class MessagePriceItemNode: ListViewItemNode {
let rightTextSize = strongSelf.rightTextNode.updateLayout(CGSize(width: 100.0, height: 100.0))
let centerLeftTextSize = strongSelf.centerLeftTextNode.updateLayout(CGSize(width: 200.0, height: 100.0))
let centerRightTextSize = strongSelf.centerRightTextNode.updateLayout(CGSize(width: 200.0, height: 100.0))
let centerSpacing: CGFloat = 6.0
let centerSpacing: CGFloat = item.price.isEmpty ? 0.0 : 6.0
let sideInset: CGFloat = 18.0
@ -386,7 +428,7 @@ private class MessagePriceItemNode: ListViewItemNode {
}
if !sliderView.isTracking {
strongSelf.amount = Amount(realValue: Int(item.value), maxRealValue: Int(item.maxValue), maxSliderValue: 999, isLogarithmic: true)
strongSelf.amount = Amount(realValue: Int(item.value), minRealValue: Int(item.minValue), maxRealValue: Int(item.maxValue), maxSliderValue: 999, isLogarithmic: true)
sliderView.value = CGFloat(strongSelf.amount.sliderValue)
}
}

View File

@ -2207,7 +2207,7 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL
}))
//TODO:localize
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPostSuggestionsSettings, label: .text("Off"), additionalBadgeLabel: presentationData.strings.Settings_New, text: "Post Suggestions", icon: UIImage(bundleImageName: "Chat/Info/GroupDiscussionIcon"), action: {
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPostSuggestionsSettings, label: .text("Off"), additionalBadgeLabel: presentationData.strings.Settings_New, text: "Post Suggestions", icon: UIImage(bundleImageName: "Chat/Info/PostSuggestionsIcon"), action: {
interaction.editingOpenPostSuggestionsSetup()
}))
}

View File

@ -27,10 +27,11 @@ swift_library(
"//submodules/Components/BundleIconComponent",
"//submodules/TextFormat",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListItemSliderSelectorComponent",
"//submodules/TelegramUI/Components/ListSwitchItemComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramStringFormatting",
"//submodules/TelegramUI/Components/ListItemComponentAdaptor",
"//submodules/TelegramUI/Components/PeerInfo/MessagePriceItem",
],
visibility = [
"//visibility:public",

View File

@ -17,23 +17,27 @@ import ListSectionComponent
import BundleIconComponent
import LottieComponent
import ListSwitchItemComponent
import ListItemSliderSelectorComponent
import ListSwitchItemComponent
import ListActionItemComponent
import Markdown
import TelegramStringFormatting
import MessagePriceItem
import ListItemComponentAdaptor
final class PostSuggestionsSettingsScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let usdWithdrawRate: Int64
let completion: () -> Void
init(
context: AccountContext,
usdWithdrawRate: Int64,
completion: @escaping () -> Void
) {
self.context = context
self.usdWithdrawRate = usdWithdrawRate
self.completion = completion
}
@ -315,26 +319,50 @@ final class PostSuggestionsSettingsScreenComponent: Component {
var contentSectionItems: [AnyComponentWithIdentity<Empty>] = []
let sliderValueList = (0 ... 10000).map { i -> String in
return "\(i)"
}
//TODO:localize
let sliderTitle: String
let sliderSecondaryTitle: String?
let usdAmount = Double(self.starCount) * 0.013
let usdAmountString = formatCurrencyAmount(Int64(usdAmount * 100.0), currency: "USD")
if self.starCount == 0 {
sliderTitle = "Free"
sliderSecondaryTitle = nil
} else if self.starCount == 1 {
sliderTitle = "\(self.starCount) Star"
sliderSecondaryTitle = "~\(usdAmountString)"
} else {
sliderTitle = "\(self.starCount) Stars"
sliderSecondaryTitle = "~\(usdAmountString)"
}
let usdRate = Double(component.usdWithdrawRate) / 1000.0 / 100.0
let price = self.starCount == 0 ? "" : "\(formatTonUsdValue(Int64(self.starCount), divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))"
contentSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemSliderSelectorComponent(
contentSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemComponentAdaptor(
itemGenerator: MessagePriceItem(
theme: environment.theme,
strings: environment.strings,
isEnabled: true, minValue: 0, maxValue: 10000,
value: Int64(self.starCount),
price: price,
sectionId: 0,
updated: { [weak self] value, _ in
guard let self else {
return
}
self.starCount = Int(value)
if !self.isUpdating {
self.state?.updated(transition: .immediate)
}
},
openSetCustom: { [weak self] in
guard let self, let component = self.component, let environment = self.environment else {
return
}
let currentAmount: StarsAmount = StarsAmount(value: Int64(self.starCount), nanos: 0)
let starsScreen = component.context.sharedContext.makeStarsWithdrawalScreen(context: component.context, subject: .enterAmount(current: currentAmount, minValue: StarsAmount(value: 0, nanos: 0), fractionAfterCommission: 85, kind: .postSuggestion), completion: { [weak self] amount in
guard let self else {
return
}
self.starCount = Int(amount)
if !self.isUpdating {
self.state?.updated(transition: .immediate)
}
})
environment.controller()?.push(starsScreen)
},
openPremiumInfo: nil
),
params: ListViewItemLayoutParams(width: availableSize.width - sideInset * 2.0, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true)
))))
/*contentSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemSliderSelectorComponent(
theme: environment.theme,
content: .discrete(ListItemSliderSelectorComponent.Discrete(
values: sliderValueList.map { item in
@ -353,7 +381,7 @@ final class PostSuggestionsSettingsScreenComponent: Component {
self.state?.updated(transition: .immediate)
}
))
))))
))))*/
let contentSectionSize = self.contentSection.update(
transition: transition,
@ -445,8 +473,11 @@ public final class PostSuggestionsSettingsScreen: ViewControllerComponentContain
) {
self.context = context
let configuration = StarsSubscriptionConfiguration.with(appConfiguration: context.currentAppConfiguration.with({ $0 }))
super.init(context: context, component: PostSuggestionsSettingsScreenComponent(
context: context,
usdWithdrawRate: configuration.usdWithdrawRate,
completion: completion
), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil)

View File

@ -156,13 +156,18 @@ private final class SheetContent: CombinedComponent {
minAmount = StarsAmount(value: resaleConfiguration.starGiftResaleMinAmount, nanos: 0)
maxAmount = StarsAmount(value: resaleConfiguration.starGiftResaleMaxAmount, nanos: 0)
amountLabel = nil
case .paidMessages:
case let .paidMessages(_, minAmountValue, _, kind):
//TODO:localize
titleString = "Price per Message"
switch kind {
case .privacy:
titleString = "Price per Message"
case .postSuggestion:
titleString = "Price for each Suggestion"
}
amountTitle = "PRICE IN STARS"
amountPlaceholder = "Enter Price"
minAmount = StarsAmount(value: 1, nanos: 0)
minAmount = StarsAmount(value: minAmountValue, nanos: 0)
maxAmount = StarsAmount(value: resaleConfiguration.paidMessageMaxAmount, nanos: 0)
amountLabel = nil
}
@ -289,10 +294,10 @@ private final class SheetContent: CombinedComponent {
text: .plain(amountInfoString),
maximumNumberOfLines: 0
))
case .paidMessages:
case let .paidMessages(_, _, fractionAfterCommission, _):
let amountInfoString: NSAttributedString
if let value = state.amount?.value, value > 0 {
let fullValue: Int64 = Int64(value) * 1_000_000_000 * 80 / 100
let fullValue: Int64 = Int64(value) * 1_000_000_000 * Int64(fractionAfterCommission) / 100
let amountValue = StarsAmount(value: fullValue / 1_000_000_000, nanos: Int32(fullValue % 1_000_000_000))
amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString("You will receive **\(amountValue) Stars**.", attributes: amountMarkdownAttributes, textAlignment: .natural))
} else {
@ -457,7 +462,7 @@ private final class SheetContent: CombinedComponent {
amount = nil
case .starGiftResell:
amount = nil
case let .paidMessages(initialValue):
case let .paidMessages(initialValue, _, _, _):
amount = StarsAmount(value: initialValue, nanos: 0)
}
@ -580,7 +585,7 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer {
case paidMedia(Int64?)
case reaction(Int64?)
case starGiftResell(Bool)
case paidMessages(Int64)
case paidMessages(current: Int64, minValue: Int64, fractionAfterCommission: Int, kind: StarsWithdrawalScreenSubject.PaidMessageKind)
}
private let context: AccountContext

View File

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

View File

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

View File

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

View File

@ -2111,6 +2111,36 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
case .postSuggestions:
//TODO:release
actions.removeAll()
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_MessageDialogEdit, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.actionSheet.primaryTextColor)
}, action: { c, f in
interfaceInteraction.setupEditMessage(messages[0].id, { transition in
f(.custom(transition))
})
})))
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.ScheduledMessages_EditTime, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Schedule"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
controllerInteraction.editScheduledMessagesTime(messages.map { $0.id })
f(.dismissWithoutContent)
})))
actions.append(.action(ContextMenuActionItem(text: "Edit Price", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Tag"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
})))
actions.append(.action(ContextMenuActionItem(text: "Delete", textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { controller, f in
interfaceInteraction.deleteMessages(messages, controller, f)
})))
actions.append(.separator)
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let action: ((ContextControllerProtocol?, @escaping (ContextMenuActionResult) -> Void) -> Void)? = nil
actions.append(.action(ContextMenuActionItem(text: "Deleting suggested post will auto-refund your order.", textColor: .primary, textLayout: .multiline, textFont: .custom(font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 0.8)), height: nil, verticalOffset: nil), badge: nil, icon: { theme in
return nil
}, iconSource: nil, action: action)))
}
}

View File

@ -26,6 +26,7 @@ swift_library(
"//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent",
"//submodules/Components/BalancedTextComponent",
"//submodules/Components/MultilineTextWithEntitiesComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/ShimmerEffect",
],
visibility = [

View File

@ -17,6 +17,7 @@ import AvatarStoryIndicatorComponent
import AccountContext
import Markdown
import BalancedTextComponent
import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import ShimmerEffect
@ -113,6 +114,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
private let context: AccountContext?
private let text: TooltipScreen.Text
private let textBadge: String?
private let textAlignment: TooltipScreen.Alignment
private let balancedTextLayout: Bool
private let constrainWidth: CGFloat?
@ -148,6 +150,8 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
private var avatarNode: AvatarNode?
private var avatarStoryIndicator: ComponentView<Empty>?
private let textView = ComponentView<Empty>()
private var textBadgeView: ComponentView<Empty>?
private var textBadgeBackgroundView: ComponentView<Empty>?
private var closeButtonNode: HighlightableButtonNode?
private var actionButtonNode: HighlightableButtonNode?
@ -166,6 +170,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
account: Account,
sharedContext: SharedAccountContext,
text: TooltipScreen.Text,
textBadge: String?,
textAlignment: TooltipScreen.Alignment,
balancedTextLayout: Bool,
constrainWidth: CGFloat?,
@ -390,6 +395,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
self.fontSize = fontSize
self.text = text
self.textBadge = textBadge
self.textAlignment = textAlignment
self.balancedTextLayout = balancedTextLayout
self.constrainWidth = constrainWidth
@ -658,18 +664,48 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
)
}
let textBadgeSpacing: CGFloat = 9.0
let textBadgeRightInset: CGFloat = 5.0
var textContentSize = textSize
var textBadgeSize: CGSize?
if let textBadge = self.textBadge {
let textBadgeView: ComponentView<Empty>
if let current = self.textBadgeView {
textBadgeView = current
} else {
textBadgeView = ComponentView()
self.textBadgeView = textBadgeView
}
let textBadgeSizeValue = textBadgeView.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: textBadge, font: Font.semibold(floor(self.fontSize * 0.8)), textColor: textColor))
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
textBadgeSize = textBadgeSizeValue
textContentSize.width += textBadgeSpacing + textBadgeSizeValue.width + textBadgeRightInset
} else {
if let textBadgeView = self.textBadgeView {
self.textBadgeView = nil
textBadgeView.view?.removeFromSuperview()
}
}
var backgroundFrame: CGRect
var backgroundHeight: CGFloat
switch self.tooltipStyle {
case .default, .gradient:
backgroundHeight = max(animationSize.height, textSize.height) + contentVerticalInset * 2.0
backgroundHeight = max(animationSize.height, textContentSize.height) + contentVerticalInset * 2.0
case .wide:
backgroundHeight = max(animationSize.height, textSize.height) + contentVerticalInset * 2.0 + 4.0
backgroundHeight = max(animationSize.height, textContentSize.height) + contentVerticalInset * 2.0 + 4.0
case let .customBlur(_, inset):
backgroundHeight = max(animationSize.height, textSize.height) + contentVerticalInset * 2.0 + inset * 2.0
backgroundHeight = max(animationSize.height, textContentSize.height) + contentVerticalInset * 2.0 + inset * 2.0
case .light:
backgroundHeight = max(28.0, max(animationSize.height, textSize.height) + 4.0 * 2.0)
backgroundHeight = max(28.0, max(animationSize.height, textContentSize.height) + 4.0 * 2.0)
}
if self.actionButtonNode != nil {
backgroundHeight += 4.0
@ -678,7 +714,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
var invertArrow = false
switch self.location {
case let .point(rect, arrowPosition):
var backgroundWidth = textSize.width + contentInset * 2.0 + animationSize.width + animationSpacing
var backgroundWidth = textContentSize.width + contentInset * 2.0 + animationSize.width + animationSpacing
if self.closeButtonNode != nil || self.actionButtonNode != nil {
backgroundWidth += buttonInset
}
@ -769,7 +805,6 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
}
let textFrame = CGRect(origin: CGPoint(x: contentInset + animationSize.width + animationSpacing, y: floor((backgroundHeight - textSize.height) / 2.0)), size: textSize)
if let textComponentView = self.textView.view {
if textComponentView.superview == nil {
textComponentView.layer.anchorPoint = CGPoint()
@ -779,6 +814,49 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
transition.updateBounds(layer: textComponentView.layer, bounds: CGRect(origin: CGPoint(), size: textFrame.size))
}
if let textBadgeView = self.textBadgeView, let textBadgeSize {
let textBadgeFrame = CGRect(origin: CGPoint(x: textFrame.maxX + textBadgeSpacing, y: textFrame.minY + 2.0), size: textBadgeSize)
if let textBadgeComponentView = textBadgeView.view {
if textBadgeComponentView.superview == nil {
textBadgeComponentView.layer.anchorPoint = CGPoint()
self.containerNode.view.addSubview(textBadgeComponentView)
}
transition.updatePosition(layer: textBadgeComponentView.layer, position: textBadgeFrame.origin)
transition.updateBounds(layer: textBadgeComponentView.layer, bounds: CGRect(origin: CGPoint(), size: textBadgeFrame.size))
}
var textBadgeBackgroundFrame = textBadgeFrame.insetBy(dx: -4.0, dy: -3.0)
textBadgeBackgroundFrame.size.height -= UIScreenPixel
textBadgeBackgroundFrame.size.width -= UIScreenPixel
let textBadgeBackgroundView: ComponentView<Empty>
if let current = self.textBadgeBackgroundView {
textBadgeBackgroundView = current
} else {
textBadgeBackgroundView = ComponentView()
self.textBadgeBackgroundView = textBadgeBackgroundView
}
let _ = textBadgeBackgroundView.update(
transition: .immediate,
component: AnyComponent(FilledRoundedRectangleComponent(
color: UIColor(white: 1.0, alpha: 0.1),
cornerRadius: .value(5.0),
smoothCorners: true
)),
environment: {},
containerSize: textBadgeBackgroundFrame.size
)
if let textBadgeBackgroundComponentView = textBadgeBackgroundView.view {
if textBadgeBackgroundComponentView.superview == nil, let textBadgeComponentView = textBadgeView.view {
self.containerNode.view.insertSubview(textBadgeBackgroundComponentView, belowSubview: textBadgeComponentView)
}
textBadgeBackgroundComponentView.frame = textBadgeBackgroundFrame
}
} else if let textBadgeBackgroundView = self.textBadgeBackgroundView {
self.textBadgeBackgroundView = nil
textBadgeBackgroundView.view?.removeFromSuperview()
}
if let closeButtonNode = self.closeButtonNode {
let closeSize = CGSize(width: 44.0, height: 44.0)
transition.updateFrame(node: closeButtonNode, frame: CGRect(origin: CGPoint(x: textFrame.maxX - 6.0, y: floor((backgroundHeight - closeSize.height) / 2.0)), size: closeSize))
@ -1120,6 +1198,7 @@ public final class TooltipScreen: ViewController {
private let account: Account
private let sharedContext: SharedAccountContext
public let text: TooltipScreen.Text
private let textBadge: String?
public let textAlignment: TooltipScreen.Alignment
private let balancedTextLayout: Bool
private let constrainWidth: CGFloat?
@ -1160,6 +1239,7 @@ public final class TooltipScreen: ViewController {
account: Account,
sharedContext: SharedAccountContext,
text: TooltipScreen.Text,
textBadge: String? = nil,
textAlignment: TooltipScreen.Alignment = .natural,
balancedTextLayout: Bool = false,
constrainWidth: CGFloat? = nil,
@ -1179,6 +1259,7 @@ public final class TooltipScreen: ViewController {
self.account = account
self.sharedContext = sharedContext
self.text = text
self.textBadge = textBadge
self.textAlignment = textAlignment
self.balancedTextLayout = balancedTextLayout
self.constrainWidth = constrainWidth
@ -1250,7 +1331,7 @@ public final class TooltipScreen: ViewController {
}
override public func loadDisplayNode() {
self.displayNode = TooltipScreenNode(context: self.context, account: self.account, sharedContext: self.sharedContext, text: self.text, textAlignment: self.textAlignment, balancedTextLayout: self.balancedTextLayout, constrainWidth: self.constrainWidth, style: self.style, arrowStyle: self.arrowStyle, icon: self.icon, action: self.action, location: self.location, displayDuration: self.displayDuration, inset: self.inset, cornerRadius: self.cornerRadius, isShimmering: self.isShimmering, shouldDismissOnTouch: self.shouldDismissOnTouch, requestDismiss: { [weak self] in
self.displayNode = TooltipScreenNode(context: self.context, account: self.account, sharedContext: self.sharedContext, text: self.text, textBadge: self.textBadge, textAlignment: self.textAlignment, balancedTextLayout: self.balancedTextLayout, constrainWidth: self.constrainWidth, style: self.style, arrowStyle: self.arrowStyle, icon: self.icon, action: self.action, location: self.location, displayDuration: self.displayDuration, inset: self.inset, cornerRadius: self.cornerRadius, isShimmering: self.isShimmering, shouldDismissOnTouch: self.shouldDismissOnTouch, requestDismiss: { [weak self] in
guard let strongSelf = self else {
return
}