Various improvements

This commit is contained in:
Ilya Laktyushin 2025-02-24 19:08:50 +04:00
parent b2351194d4
commit 4aeab37c70
16 changed files with 250 additions and 33 deletions

View File

@ -13876,3 +13876,11 @@ Sorry for the inconvenience.";
"Chat.PanelCustomStatusShortInfo" = "%@ is a mark for [Premium subscribers >]()"; "Chat.PanelCustomStatusShortInfo" = "%@ is a mark for [Premium subscribers >]()";
"Chat.InputTextPaidMessagePlaceholder" = "Message for %@"; "Chat.InputTextPaidMessagePlaceholder" = "Message for %@";
"Privacy.Messages.Stars_1" = "%@ Star";
"Privacy.Messages.Stars_any" = "%@ Stars";
"Privacy.Messages.Unlock" = "Unlock with Telegram Premium";
"Premium.PaidMessages" = "Paid Messages";
"Premium.PaidMessagesInfo" = "Charge a fee for messages from non-contacts or new senders.";
"Premium.PaidMessages.Proceed" = "About Telegram Premium";

View File

@ -1357,25 +1357,29 @@ public struct StarsSubscriptionConfiguration {
maxFee: 2500, maxFee: 2500,
usdWithdrawRate: 1200, usdWithdrawRate: 1200,
paidMessageMaxAmount: 10000, paidMessageMaxAmount: 10000,
paidMessageCommissionPermille: 850 paidMessageCommissionPermille: 850,
paidMessagesAvailable: false
) )
} }
public let maxFee: Int64 public let maxFee: Int64
public let usdWithdrawRate: Int64 public let usdWithdrawRate: Int64
public let paidMessageMaxAmount: Int64 public let paidMessageMaxAmount: Int64
public let paidMessageCommissionPermille: Int32 public let paidMessageCommissionPermille: Int32
public let paidMessagesAvailable: Bool
fileprivate init( fileprivate init(
maxFee: Int64, maxFee: Int64,
usdWithdrawRate: Int64, usdWithdrawRate: Int64,
paidMessageMaxAmount: Int64, paidMessageMaxAmount: Int64,
paidMessageCommissionPermille: Int32 paidMessageCommissionPermille: Int32,
paidMessagesAvailable: Bool
) { ) {
self.maxFee = maxFee self.maxFee = maxFee
self.usdWithdrawRate = usdWithdrawRate self.usdWithdrawRate = usdWithdrawRate
self.paidMessageMaxAmount = paidMessageMaxAmount self.paidMessageMaxAmount = paidMessageMaxAmount
self.paidMessageCommissionPermille = paidMessageCommissionPermille self.paidMessageCommissionPermille = paidMessageCommissionPermille
self.paidMessagesAvailable = paidMessagesAvailable
} }
public static func with(appConfiguration: AppConfiguration) -> StarsSubscriptionConfiguration { public static func with(appConfiguration: AppConfiguration) -> StarsSubscriptionConfiguration {
@ -1384,11 +1388,14 @@ public struct StarsSubscriptionConfiguration {
let usdWithdrawRate = (data["stars_usd_withdraw_rate_x1000"] as? Double).flatMap(Int64.init) ?? StarsSubscriptionConfiguration.defaultValue.usdWithdrawRate let usdWithdrawRate = (data["stars_usd_withdraw_rate_x1000"] as? Double).flatMap(Int64.init) ?? StarsSubscriptionConfiguration.defaultValue.usdWithdrawRate
let paidMessageMaxAmount = (data["stars_paid_message_amount_max"] as? Double).flatMap(Int64.init) ?? StarsSubscriptionConfiguration.defaultValue.paidMessageMaxAmount let paidMessageMaxAmount = (data["stars_paid_message_amount_max"] as? Double).flatMap(Int64.init) ?? StarsSubscriptionConfiguration.defaultValue.paidMessageMaxAmount
let paidMessageCommissionPermille = (data["stars_paid_message_commission_permille"] as? Double).flatMap(Int32.init) ?? StarsSubscriptionConfiguration.defaultValue.paidMessageCommissionPermille let paidMessageCommissionPermille = (data["stars_paid_message_commission_permille"] as? Double).flatMap(Int32.init) ?? StarsSubscriptionConfiguration.defaultValue.paidMessageCommissionPermille
let paidMessagesAvailable = (data["stars_paid_messages_available"] as? Bool) ?? StarsSubscriptionConfiguration.defaultValue.paidMessagesAvailable
return StarsSubscriptionConfiguration( return StarsSubscriptionConfiguration(
maxFee: maxFee, maxFee: maxFee,
usdWithdrawRate: usdWithdrawRate, usdWithdrawRate: usdWithdrawRate,
paidMessageMaxAmount: paidMessageMaxAmount, paidMessageMaxAmount: paidMessageMaxAmount,
paidMessageCommissionPermille: paidMessageCommissionPermille paidMessageCommissionPermille: paidMessageCommissionPermille,
paidMessagesAvailable: paidMessagesAvailable
) )
} else { } else {
return .defaultValue return .defaultValue

View File

@ -42,6 +42,7 @@ public enum PremiumIntroSource {
case folderTags case folderTags
case animatedEmoji case animatedEmoji
case messageEffects case messageEffects
case paidMessages
} }
public enum PremiumGiftSource: Equatable { public enum PremiumGiftSource: Equatable {
@ -79,6 +80,7 @@ public enum PremiumDemoSubject {
case folderTags case folderTags
case business case business
case messageEffects case messageEffects
case paidMessages
case businessLocation case businessLocation
case businessHours case businessHours

View File

@ -347,6 +347,8 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode {
iconFrame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - iconSize.width) / 2.0), y: floor((contentSize.height - iconSize.height) / 2.0)), size: iconSize) iconFrame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - iconSize.width) / 2.0), y: floor((contentSize.height - iconSize.height) / 2.0)), size: iconSize)
} }
strongSelf.imageNode.frame = iconFrame strongSelf.imageNode.frame = iconFrame
} else {
strongSelf.imageNode.image = nil
} }
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: strongSelf.backgroundNode.frame.height + UIScreenPixel + UIScreenPixel)) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: strongSelf.backgroundNode.frame.height + UIScreenPixel + UIScreenPixel))

View File

@ -141,7 +141,7 @@ const CGFloat TGPhotoEditorSliderViewInternalMargin = 7.0f;
if (vertical) if (vertical)
startPosition = 2 * visualMargin + visualTotalLength - startPosition; startPosition = 2 * visualMargin + visualTotalLength - startPosition;
CGFloat endPosition = visualMargin + visualTotalLength / (_maximumValue - _minimumValue) * (ABS(_minimumValue) + 1.0); CGFloat endPosition = visualMargin + visualTotalLength / (_maximumValue - _minimumValue) * (ABS(_minimumValue) + _maximumValue);
if (vertical) if (vertical)
endPosition = 2 * visualMargin + visualTotalLength - endPosition; endPosition = 2 * visualMargin + visualTotalLength - endPosition;

View File

@ -93,7 +93,7 @@ private enum ChannelPermissionsEntry: ItemListNodeEntry {
case chargeForMessagesInfo(PresentationTheme, String) case chargeForMessagesInfo(PresentationTheme, String)
case messagePriceHeader(PresentationTheme, String) case messagePriceHeader(PresentationTheme, String)
case messagePrice(PresentationTheme, Int64, String) case messagePrice(PresentationTheme, Int64, Int64, String)
case messagePriceInfo(PresentationTheme, String) case messagePriceInfo(PresentationTheme, String)
case unrestrictBoostersSwitch(PresentationTheme, String, Bool) case unrestrictBoostersSwitch(PresentationTheme, String, Bool)
@ -241,8 +241,8 @@ private enum ChannelPermissionsEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .messagePrice(lhsTheme, lhsValue, lhsPrice): case let .messagePrice(lhsTheme, lhsValue, lhsMaxValue, lhsPrice):
if case let .messagePrice(rhsTheme, rhsValue, rhsPrice) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue, lhsPrice == rhsPrice { if case let .messagePrice(rhsTheme, rhsValue, rhsMaxValue, rhsPrice) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue, lhsMaxValue == rhsMaxValue, lhsPrice == rhsPrice {
return true return true
} else { } else {
return false return false
@ -424,8 +424,8 @@ private enum ChannelPermissionsEntry: ItemListNodeEntry {
return ItemListTextItem(presentationData: presentationData, text: .plain(value), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(value), sectionId: self.section)
case let .messagePriceHeader(_, value): case let .messagePriceHeader(_, value):
return ItemListSectionHeaderItem(presentationData: presentationData, text: value, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: value, sectionId: self.section)
case let .messagePrice(_, value, price): case let .messagePrice(_, value, maxValue, price):
return MessagePriceItem(theme: presentationData.theme, strings: presentationData.strings, minValue: 1, maxValue: 10000, value: value, price: price, sectionId: self.section, updated: { value in return MessagePriceItem(theme: presentationData.theme, strings: presentationData.strings, isEnabled: true, minValue: 1, maxValue: maxValue, value: value, price: price, sectionId: self.section, updated: { value in
arguments.updateStarsAmount(StarsAmount(value: value, nanos: 0)) arguments.updateStarsAmount(StarsAmount(value: value, nanos: 0))
}) })
case let .messagePriceInfo(_, value): case let .messagePriceInfo(_, value):
@ -733,7 +733,7 @@ private func channelPermissionsControllerEntries(context: AccountContext, presen
price = "\(formatTonUsdValue(sendPaidMessageStars, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))" price = "\(formatTonUsdValue(sendPaidMessageStars, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))"
entries.append(.messagePriceHeader(presentationData.theme, presentationData.strings.GroupInfo_Permissions_MessagePrice)) entries.append(.messagePriceHeader(presentationData.theme, presentationData.strings.GroupInfo_Permissions_MessagePrice))
entries.append(.messagePrice(presentationData.theme, sendPaidMessageStars, price)) entries.append(.messagePrice(presentationData.theme, sendPaidMessageStars, configuration.paidMessageMaxAmount, price))
entries.append(.messagePriceInfo(presentationData.theme, presentationData.strings.GroupInfo_Permissions_MessagePriceInfo("\(configuration.paidMessageCommissionPermille / 10)", price).string)) entries.append(.messagePriceInfo(presentationData.theme, presentationData.strings.GroupInfo_Permissions_MessagePriceInfo("\(configuration.paidMessageCommissionPermille / 10)", price).string))
} }
} }

View File

@ -1099,6 +1099,25 @@ private final class DemoSheetContent: CombinedComponent {
) )
) )
availableItems[.paidMessages] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.paidMessages,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: component.context,
position: .top,
videoFile: configuration.videos["paid_messages"],
decoration: .badgeStars
)),
title: strings.Premium_PaidMessages,
text: strings.Premium_PaidMessagesInfo,
textColor: textColor
)
)
)
)
let index: Int = 0 let index: Int = 0
var items: [DemoPagerComponent.Item] = [] var items: [DemoPagerComponent.Item] = []
if let item = availableItems.first(where: { $0.value.content.id == component.subject as AnyHashable }) { if let item = availableItems.first(where: { $0.value.content.id == component.subject as AnyHashable }) {
@ -1195,6 +1214,8 @@ private final class DemoSheetContent: CombinedComponent {
text = strings.Premium_FolderTagsStandaloneInfo text = strings.Premium_FolderTagsStandaloneInfo
case .messageEffects: case .messageEffects:
text = strings.Premium_MessageEffectsInfo text = strings.Premium_MessageEffectsInfo
case .paidMessages:
text = strings.Premium_PaidMessagesInfo
default: default:
text = "" text = ""
} }
@ -1279,6 +1300,9 @@ private final class DemoSheetContent: CombinedComponent {
case .emojiStatus: case .emojiStatus:
buttonText = strings.Premium_EmojiStatus_Proceed buttonText = strings.Premium_EmojiStatus_Proceed
buttonAnimationName = "premium_unlock" buttonAnimationName = "premium_unlock"
case .paidMessages:
buttonText = strings.Premium_PaidMessages_Proceed
buttonAnimationName = "premium_unlock"
default: default:
buttonText = strings.Common_OK buttonText = strings.Common_OK
} }
@ -1468,6 +1492,7 @@ public class PremiumDemoScreen: ViewControllerComponentContainer {
case business case business
case folderTags case folderTags
case messageEffects case messageEffects
case paidMessages
case businessLocation case businessLocation
case businessHours case businessHours
@ -1526,6 +1551,8 @@ public class PremiumDemoScreen: ViewControllerComponentContainer {
return .folderTags return .folderTags
case .messageEffects: case .messageEffects:
return .messageEffects return .messageEffects
case .paidMessages:
return .paidMessages
case .businessLocation: case .businessLocation:
return .businessLocation return .businessLocation
case .businessHours: case .businessHours:

View File

@ -433,6 +433,7 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
UIColor(rgb: 0xdb374b), UIColor(rgb: 0xdb374b),
UIColor(rgb: 0xcb3e6d), UIColor(rgb: 0xcb3e6d),
UIColor(rgb: 0xbc4395), UIColor(rgb: 0xbc4395),
UIColor(rgb: 0xbc4395),
UIColor(rgb: 0xab4ac4), UIColor(rgb: 0xab4ac4),
UIColor(rgb: 0xab4ac4), UIColor(rgb: 0xab4ac4),
UIColor(rgb: 0xa34cd7), UIColor(rgb: 0xa34cd7),
@ -538,6 +539,8 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
demoSubject = .messagePrivacy demoSubject = .messagePrivacy
case .messageEffects: case .messageEffects:
demoSubject = .messageEffects demoSubject = .messageEffects
case .paidMessages:
demoSubject = .paidMessages
case .business: case .business:
demoSubject = .business demoSubject = .business
default: default:

View File

@ -302,6 +302,12 @@ public enum PremiumSource: Equatable {
} else { } else {
return false return false
} }
case .paidMessages:
if case .messageEffects = rhs {
return true
} else {
return false
}
} }
} }
@ -349,6 +355,7 @@ public enum PremiumSource: Equatable {
case messageTags case messageTags
case folderTags case folderTags
case messageEffects case messageEffects
case paidMessages
var identifier: String? { var identifier: String? {
switch self { switch self {
@ -442,6 +449,8 @@ public enum PremiumSource: Equatable {
return "folder_tags" return "folder_tags"
case .messageEffects: case .messageEffects:
return "effects" return "effects"
case .paidMessages:
return "paid_messages"
} }
} }
} }
@ -470,6 +479,7 @@ public enum PremiumPerk: CaseIterable {
case business case business
case folderTags case folderTags
case messageEffects case messageEffects
case paidMessages
case businessLocation case businessLocation
case businessHours case businessHours
@ -504,7 +514,8 @@ public enum PremiumPerk: CaseIterable {
.messagePrivacy, .messagePrivacy,
.folderTags, .folderTags,
.business, .business,
.messageEffects .messageEffects,
.paidMessages
] ]
} }
@ -578,6 +589,8 @@ public enum PremiumPerk: CaseIterable {
return "folder_tags" return "folder_tags"
case .messageEffects: case .messageEffects:
return "effects" return "effects"
case .paidMessages:
return "paid_messages"
case .business: case .business:
return "business" return "business"
case .businessLocation: case .businessLocation:
@ -647,6 +660,8 @@ public enum PremiumPerk: CaseIterable {
return strings.Premium_Business return strings.Premium_Business
case .messageEffects: case .messageEffects:
return strings.Premium_MessageEffects return strings.Premium_MessageEffects
case .paidMessages:
return strings.Premium_PaidMessages
case .businessLocation: case .businessLocation:
return strings.Business_Location return strings.Business_Location
case .businessHours: case .businessHours:
@ -714,6 +729,8 @@ public enum PremiumPerk: CaseIterable {
return strings.Premium_BusinessInfo return strings.Premium_BusinessInfo
case .messageEffects: case .messageEffects:
return strings.Premium_MessageEffectsInfo return strings.Premium_MessageEffectsInfo
case .paidMessages:
return strings.Premium_PaidMessagesInfo
case .businessLocation: case .businessLocation:
return strings.Business_LocationInfo return strings.Business_LocationInfo
case .businessHours: case .businessHours:
@ -781,7 +798,8 @@ public enum PremiumPerk: CaseIterable {
return "Premium/Perk/Business" return "Premium/Perk/Business"
case .messageEffects: case .messageEffects:
return "Premium/Perk/MessageEffects" return "Premium/Perk/MessageEffects"
case .paidMessages:
return "Premium/Perk/PaidMessages"
case .businessLocation: case .businessLocation:
return "Premium/BusinessPerk/Location" return "Premium/BusinessPerk/Location"
case .businessHours: case .businessHours:
@ -819,6 +837,7 @@ struct PremiumIntroConfiguration {
.colors, .colors,
.wallpapers, .wallpapers,
.profileBadge, .profileBadge,
.paidMessages,
.messagePrivacy, .messagePrivacy,
.advancedChatManagement, .advancedChatManagement,
.noAds, .noAds,
@ -867,6 +886,12 @@ struct PremiumIntroConfiguration {
perks = PremiumIntroConfiguration.defaultValue.perks perks = PremiumIntroConfiguration.defaultValue.perks
} }
#if DEBUG
if !perks.contains(.paidMessages) {
perks.append(.paidMessages)
}
#endif
var businessPerks: [PremiumPerk] = [] var businessPerks: [PremiumPerk] = []
if let values = data["business_promo_order"] as? [String] { if let values = data["business_promo_order"] as? [String] {
for value in values { for value in values {
@ -1859,6 +1884,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
UIColor(rgb: 0xdb374b), UIColor(rgb: 0xdb374b),
UIColor(rgb: 0xcb3e6d), UIColor(rgb: 0xcb3e6d),
UIColor(rgb: 0xbc4395), UIColor(rgb: 0xbc4395),
UIColor(rgb: 0xbc4395),
UIColor(rgb: 0xab4ac4), UIColor(rgb: 0xab4ac4),
UIColor(rgb: 0xab4ac4), UIColor(rgb: 0xab4ac4),
UIColor(rgb: 0xa34cd7), UIColor(rgb: 0xa34cd7),
@ -2092,6 +2118,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
demoSubject = .messagePrivacy demoSubject = .messagePrivacy
case .messageEffects: case .messageEffects:
demoSubject = .messageEffects demoSubject = .messageEffects
case .paidMessages:
demoSubject = .paidMessages
case .business: case .business:
demoSubject = .business demoSubject = .business
let _ = ApplicationSpecificNotice.setDismissedBusinessBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() let _ = ApplicationSpecificNotice.setDismissedBusinessBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()

View File

@ -842,6 +842,24 @@ public class PremiumLimitsListScreen: ViewController {
) )
) )
) )
availableItems[.paidMessages] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.paidMessages,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: context,
position: .top,
videoFile: videos["paid_messages"],
decoration: .badgeStars
)),
title: strings.Premium_PaidMessages,
text: strings.Premium_PaidMessagesInfo,
textColor: textColor
)
)
)
)
availableItems[.business] = DemoPagerComponent.Item( availableItems[.business] = DemoPagerComponent.Item(
AnyComponentWithIdentity( AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.business, id: PremiumDemoScreen.Subject.business,

View File

@ -19,19 +19,22 @@ private final class IncomingMessagePrivacyScreenArguments {
let disabledValuePressed: () -> Void let disabledValuePressed: () -> Void
let infoLinkAction: () -> Void let infoLinkAction: () -> Void
let openExceptions: () -> Void let openExceptions: () -> Void
let openPremiumInfo: () -> Void
init( init(
context: AccountContext, context: AccountContext,
updateValue: @escaping (GlobalPrivacySettings.NonContactChatsPrivacy) -> Void, updateValue: @escaping (GlobalPrivacySettings.NonContactChatsPrivacy) -> Void,
disabledValuePressed: @escaping () -> Void, disabledValuePressed: @escaping () -> Void,
infoLinkAction: @escaping () -> Void, infoLinkAction: @escaping () -> Void,
openExceptions: @escaping () -> Void openExceptions: @escaping () -> Void,
openPremiumInfo: @escaping () -> Void
) { ) {
self.context = context self.context = context
self.updateValue = updateValue self.updateValue = updateValue
self.disabledValuePressed = disabledValuePressed self.disabledValuePressed = disabledValuePressed
self.infoLinkAction = infoLinkAction self.infoLinkAction = infoLinkAction
self.openExceptions = openExceptions self.openExceptions = openExceptions
self.openPremiumInfo = openPremiumInfo
} }
} }
@ -49,7 +52,7 @@ private enum GlobalAutoremoveEntry: ItemListNodeEntry {
case optionChargeForMessages(value: GlobalPrivacySettings.NonContactChatsPrivacy, isEnabled: Bool) case optionChargeForMessages(value: GlobalPrivacySettings.NonContactChatsPrivacy, isEnabled: Bool)
case footer(value: GlobalPrivacySettings.NonContactChatsPrivacy) case footer(value: GlobalPrivacySettings.NonContactChatsPrivacy)
case priceHeader case priceHeader
case price(value: Int64, price: String) case price(value: Int64, maxValue: Int64, price: String, isEnabled: Bool)
case priceInfo(commission: Int32, value: String) case priceInfo(commission: Int32, value: String)
case exceptionsHeader case exceptionsHeader
case exceptions(count: Int) case exceptions(count: Int)
@ -128,12 +131,8 @@ private enum GlobalAutoremoveEntry: ItemListNodeEntry {
if case .paidMessages = value { if case .paidMessages = value {
isChecked = true isChecked = true
} }
return ItemListCheckboxItem(presentationData: presentationData, icon: isEnabled ? nil : generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/Lock"), color: presentationData.theme.list.itemSecondaryTextColor), iconPlacement: .check, title: presentationData.strings.Privacy_Messages_ChargeForMessages, style: .left, checked: isChecked, zeroSeparatorInsets: false, sectionId: self.section, action: { return ItemListCheckboxItem(presentationData: presentationData, icon: isEnabled || isChecked ? nil : generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/Lock"), color: presentationData.theme.list.itemSecondaryTextColor), iconPlacement: .check, title: presentationData.strings.Privacy_Messages_ChargeForMessages, style: .left, checked: isChecked, zeroSeparatorInsets: false, sectionId: self.section, action: {
if isEnabled { arguments.updateValue(.paidMessages(StarsAmount(value: 400, nanos: 0)))
arguments.updateValue(.paidMessages(StarsAmount(value: 400, nanos: 0)))
} else {
arguments.disabledValuePressed()
}
}) })
case let .footer(value): case let .footer(value):
let text: String let text: String
@ -149,9 +148,11 @@ private enum GlobalAutoremoveEntry: ItemListNodeEntry {
}) })
case .priceHeader: case .priceHeader:
return ItemListSectionHeaderItem(presentationData: presentationData, text: presentationData.strings.Privacy_Messages_MessagePrice, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: presentationData.strings.Privacy_Messages_MessagePrice, sectionId: self.section)
case let .price(value, price): case let .price(value, maxValue, price, isEnabled):
return MessagePriceItem(theme: presentationData.theme, strings: presentationData.strings, minValue: 1, maxValue: 10000, value: value, price: price, sectionId: self.section, updated: { value in return MessagePriceItem(theme: presentationData.theme, strings: presentationData.strings, isEnabled: isEnabled, minValue: 1, maxValue: maxValue, value: value, price: price, sectionId: self.section, updated: { value in
arguments.updateValue(.paidMessages(StarsAmount(value: value, nanos: 0))) arguments.updateValue(.paidMessages(StarsAmount(value: value, nanos: 0)))
}, openPremiumInfo: {
arguments.openPremiumInfo()
}) })
case let .priceInfo(commission, value): case let .priceInfo(commission, value):
return ItemListTextItem(presentationData: presentationData, text: .markdown(presentationData.strings.Privacy_Messages_MessagePriceInfo("\(commission)", value).string), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .markdown(presentationData.strings.Privacy_Messages_MessagePriceInfo("\(commission)", value).string), sectionId: self.section)
@ -178,7 +179,9 @@ private func incomingMessagePrivacyScreenEntries(presentationData: PresentationD
entries.append(.header) entries.append(.header)
entries.append(.optionEverybody(value: state.updatedValue)) entries.append(.optionEverybody(value: state.updatedValue))
entries.append(.optionPremium(value: state.updatedValue, isEnabled: enableSetting)) entries.append(.optionPremium(value: state.updatedValue, isEnabled: enableSetting))
entries.append(.optionChargeForMessages(value: state.updatedValue, isEnabled: isPremium)) if configuration.paidMessagesAvailable {
entries.append(.optionChargeForMessages(value: state.updatedValue, isEnabled: isPremium))
}
if case let .paidMessages(amount) = state.updatedValue { if case let .paidMessages(amount) = state.updatedValue {
entries.append(.footer(value: state.updatedValue)) entries.append(.footer(value: state.updatedValue))
@ -188,11 +191,14 @@ private func incomingMessagePrivacyScreenEntries(presentationData: PresentationD
let price = "\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))" let price = "\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))"
entries.append(.price(value: amount.value, price: price)) entries.append(.price(value: amount.value, maxValue: configuration.paidMessageMaxAmount, price: price, isEnabled: isPremium))
entries.append(.priceInfo(commission: configuration.paidMessageCommissionPermille / 10, value: price)) entries.append(.priceInfo(commission: configuration.paidMessageCommissionPermille / 10, value: price))
entries.append(.exceptionsHeader)
entries.append(.exceptions(count: state.disableFor.count)) if isPremium {
entries.append(.exceptionsInfo) entries.append(.exceptionsHeader)
entries.append(.exceptions(count: state.disableFor.count))
entries.append(.exceptionsInfo)
}
} else { } else {
entries.append(.footer(value: state.updatedValue)) entries.append(.footer(value: state.updatedValue))
entries.append(.info) entries.append(.info)
@ -347,6 +353,17 @@ public func incomingMessagePrivacyScreen(context: AccountContext, value: GlobalP
}) })
pushControllerImpl?(controller) pushControllerImpl?(controller)
} }
},
openPremiumInfo: {
var replaceImpl: ((ViewController) -> Void)?
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .paidMessages, forceDark: false, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .paidMessages, forceDark: false, dismissed: nil)
replaceImpl?(controller)
}, dismissed: nil)
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
pushControllerImpl?(controller)
} }
) )
@ -414,7 +431,12 @@ public func incomingMessagePrivacyScreen(context: AccountContext, value: GlobalP
controller?.push(c) controller?.push(c)
} }
controller.attemptNavigation = { _ in controller.attemptNavigation = { _ in
update(stateValue.with({ $0 }).updatedValue) let updatedValue = stateValue.with({ $0 }).updatedValue
if !context.isPremium, case .paidMessages = updatedValue {
} else {
update(updatedValue)
}
return true return true
} }
dismissImpl = { [weak controller] in dismissImpl = { [weak controller] in

View File

@ -19,6 +19,10 @@ swift_library(
"//submodules/TelegramPresentationData", "//submodules/TelegramPresentationData",
"//submodules/ItemListUI", "//submodules/ItemListUI",
"//submodules/LegacyComponents", "//submodules/LegacyComponents",
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/Components/MultilineTextComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -8,6 +8,10 @@ import TelegramPresentationData
import LegacyComponents import LegacyComponents
import ItemListUI import ItemListUI
import PresentationDataUtils import PresentationDataUtils
import ComponentFlow
import ButtonComponent
import BundleIconComponent
import MultilineTextComponent
private let textFont = Font.with(size: 17.0, traits: .monospacedNumbers) private let textFont = Font.with(size: 17.0, traits: .monospacedNumbers)
private let smallTextFont = Font.with(size: 13.0, traits: .monospacedNumbers) private let smallTextFont = Font.with(size: 13.0, traits: .monospacedNumbers)
@ -15,22 +19,26 @@ private let smallTextFont = Font.with(size: 13.0, traits: .monospacedNumbers)
public final class MessagePriceItem: ListViewItem, ItemListItem { public final class MessagePriceItem: ListViewItem, ItemListItem {
let theme: PresentationTheme let theme: PresentationTheme
let strings: PresentationStrings let strings: PresentationStrings
let isEnabled: Bool
let minValue: Int64 let minValue: Int64
let maxValue: Int64 let maxValue: Int64
let value: Int64 let value: Int64
let price: String let price: String
public let sectionId: ItemListSectionId public let sectionId: ItemListSectionId
let updated: (Int64) -> Void let updated: (Int64) -> Void
let openPremiumInfo: (() -> Void)?
public init(theme: PresentationTheme, strings: PresentationStrings, minValue: Int64, maxValue: Int64, value: Int64, price: String, sectionId: ItemListSectionId, updated: @escaping (Int64) -> Void) { public init(theme: PresentationTheme, strings: PresentationStrings, isEnabled: Bool, minValue: Int64, maxValue: Int64, value: Int64, price: String, sectionId: ItemListSectionId, updated: @escaping (Int64) -> Void, openPremiumInfo: (() -> Void)? = nil) {
self.theme = theme self.theme = theme
self.strings = strings self.strings = strings
self.isEnabled = isEnabled
self.minValue = minValue self.minValue = minValue
self.maxValue = maxValue self.maxValue = maxValue
self.value = value self.value = value
self.price = price self.price = price
self.sectionId = sectionId self.sectionId = sectionId
self.updated = updated self.updated = updated
self.openPremiumInfo = openPremiumInfo
} }
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) { public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
@ -155,6 +163,9 @@ private class MessagePriceItemNode: ListViewItemNode {
private let rightTextNode: ImmediateTextNode private let rightTextNode: ImmediateTextNode
private let centerLeftTextNode: ImmediateTextNode private let centerLeftTextNode: ImmediateTextNode
private let centerRightTextNode: ImmediateTextNode private let centerRightTextNode: ImmediateTextNode
private let lockIconNode: ASImageNode
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, maxRealValue: 1000, maxSliderValue: 1000, isLogarithmic: true)
@ -178,12 +189,18 @@ private class MessagePriceItemNode: ListViewItemNode {
self.centerLeftTextNode = ImmediateTextNode() self.centerLeftTextNode = ImmediateTextNode()
self.centerRightTextNode = ImmediateTextNode() self.centerRightTextNode = ImmediateTextNode()
self.lockIconNode = ASImageNode()
self.lockIconNode.displaysAsynchronously = false
self.button = ComponentView<Empty>()
super.init(layerBacked: false, dynamicBounce: false) super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.leftTextNode) self.addSubnode(self.leftTextNode)
self.addSubnode(self.rightTextNode) self.addSubnode(self.rightTextNode)
self.addSubnode(self.centerLeftTextNode) self.addSubnode(self.centerLeftTextNode)
self.addSubnode(self.centerRightTextNode) self.addSubnode(self.centerRightTextNode)
self.addSubnode(self.lockIconNode)
} }
override func didLoad() { override func didLoad() {
@ -224,11 +241,15 @@ private class MessagePriceItemNode: ListViewItemNode {
themeUpdated = true themeUpdated = true
} }
let contentSize: CGSize var contentSize: CGSize
let insets: UIEdgeInsets let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel let separatorHeight = UIScreenPixel
contentSize = CGSize(width: params.width, height: 88.0) contentSize = CGSize(width: params.width, height: 88.0)
if !item.isEnabled {
contentSize.height = 166.0
}
insets = itemListNeighborsGroupedInsets(neighbors, params) insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
@ -290,8 +311,7 @@ private class MessagePriceItemNode: ListViewItemNode {
strongSelf.leftTextNode.attributedText = NSAttributedString(string: "\(item.minValue)", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor) 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) strongSelf.rightTextNode.attributedText = NSAttributedString(string: "\(item.maxValue)", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor)
//TODO:localize let centralLeftText = item.strings.Privacy_Messages_Stars(Int32(item.value))
let centralLeftText = "\(item.value) Stars"
strongSelf.centerLeftTextNode.attributedText = NSAttributedString(string: centralLeftText, font: textFont, textColor: item.theme.list.itemPrimaryTextColor) strongSelf.centerLeftTextNode.attributedText = NSAttributedString(string: centralLeftText, font: textFont, textColor: item.theme.list.itemPrimaryTextColor)
strongSelf.centerRightTextNode.attributedText = NSAttributedString(string: item.price, font: smallTextFont, textColor: item.theme.list.itemSecondaryTextColor) strongSelf.centerRightTextNode.attributedText = NSAttributedString(string: item.price, font: smallTextFont, textColor: item.theme.list.itemSecondaryTextColor)
@ -323,6 +343,66 @@ private class MessagePriceItemNode: ListViewItemNode {
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 18.0, y: 36.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 18.0 * 2.0, height: 44.0)) sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 18.0, y: 36.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 18.0 * 2.0, height: 44.0))
} }
strongSelf.lockIconNode.isHidden = item.isEnabled
if !item.isEnabled {
if themeUpdated {
strongSelf.lockIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/SmallLock"), color: item.theme.list.itemSecondaryTextColor.withMultipliedAlpha(0.5))
}
if let image = strongSelf.lockIconNode.image {
strongSelf.lockIconNode.frame = CGRect(origin: CGPoint(x: centerLeftFrame.minX - image.size.width - 1.0, y: 12.0 + UIScreenPixel), size: image.size)
}
let sideInset: CGFloat = 16.0
let buttonSize = CGSize(width: params.width - params.leftInset - params.rightInset - sideInset * 2.0, height: 50.0)
let _ = strongSelf.button.update(
transition: .immediate,
component: AnyComponent(
ButtonComponent(
background: ButtonComponent.Background(
color: item.theme.list.itemCheckColors.fillColor,
foreground: item.theme.list.itemCheckColors.foregroundColor,
pressedColor: item.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: AnyComponentWithIdentity(
id: AnyHashable("unlock"),
component: AnyComponent(
HStack([
AnyComponentWithIdentity(
id: AnyHashable("icon"),
component: AnyComponent(BundleIconComponent(name: "Chat/Stickers/Lock", tintColor: item.theme.list.itemCheckColors.foregroundColor))
),
AnyComponentWithIdentity(
id: AnyHashable("label"),
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: item.strings.Privacy_Messages_Unlock, font: Font.semibold(17.0), textColor: item.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center))))
)
], spacing: 3.0)
)
),
isEnabled: true,
tintWhenDisabled: false,
allowActionWhenDisabled: false,
displaysProgress: false,
action: { [weak self] in
guard let self, let item = self.item else {
return
}
item.openPremiumInfo?()
}
)
),
environment: {},
containerSize: buttonSize
)
if let buttonView = strongSelf.button.view {
if buttonView.superview == nil {
strongSelf.view.addSubview(buttonView)
}
buttonView.frame = CGRect(origin: CGPoint(x: params.leftInset + sideInset, y: contentSize.height - buttonSize.height - sideInset), size: buttonSize)
}
} else if let buttonView = strongSelf.button.view, buttonView.superview != nil {
buttonView.removeFromSuperview()
}
} }
}) })
} }

View File

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

View File

@ -2486,6 +2486,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
mappedSource = .messageEffects mappedSource = .messageEffects
case .animatedEmoji: case .animatedEmoji:
mappedSource = .animatedEmoji mappedSource = .animatedEmoji
case .paidMessages:
mappedSource = .paidMessages
} }
let controller = PremiumIntroScreen(context: context, source: mappedSource, modal: modal, forceDark: forceDark) let controller = PremiumIntroScreen(context: context, source: mappedSource, modal: modal, forceDark: forceDark)
controller.wasDismissed = dismissed controller.wasDismissed = dismissed
@ -2542,6 +2544,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
mappedSubject = .folderTags mappedSubject = .folderTags
case .messageEffects: case .messageEffects:
mappedSubject = .messageEffects mappedSubject = .messageEffects
case .paidMessages:
mappedSubject = .paidMessages
case .business: case .business:
mappedSubject = .business mappedSubject = .business
buttonText = presentationData.strings.Chat_EmptyStateIntroFooterPremiumActionButton buttonText = presentationData.strings.Chat_EmptyStateIntroFooterPremiumActionButton