Giveaway improvements

This commit is contained in:
Ilya Laktyushin 2023-10-10 16:08:20 +04:00
parent d06e7af2ce
commit 91e7305050
6 changed files with 288 additions and 218 deletions

View File

@ -105,6 +105,7 @@ swift_library(
"//submodules/TelegramUI/Components/ItemListDatePickerItem:ItemListDatePickerItem", "//submodules/TelegramUI/Components/ItemListDatePickerItem:ItemListDatePickerItem",
"//submodules/TelegramUI/Components/ShareWithPeersScreen", "//submodules/TelegramUI/Components/ShareWithPeersScreen",
"//submodules/TelegramUI/Components/ButtonComponent", "//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -69,7 +69,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
case awardUsers(PresentationTheme, String, String, Bool) case awardUsers(PresentationTheme, String, String, Bool)
case prepaidHeader(PresentationTheme, String) case prepaidHeader(PresentationTheme, String)
case prepaid(PresentationTheme, String, String, Int32, Int32) case prepaid(PresentationTheme, String, String, PrepaidGiveaway)
case subscriptionsHeader(PresentationTheme, String, String) case subscriptionsHeader(PresentationTheme, String, String)
case subscriptions(PresentationTheme, Int32) case subscriptions(PresentationTheme, Int32)
@ -190,8 +190,8 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .prepaid(lhsTheme, lhsText, lhsSubtext, lhsBoosts, lhsMonths): case let .prepaid(lhsTheme, lhsText, lhsSubtext, lhsPrepaidGiveaway):
if case let .prepaid(rhsTheme, rhsText, rhsSubtext, rhsBoosts, rhsMonths) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsSubtext == rhsSubtext, lhsBoosts == rhsBoosts, lhsMonths == rhsMonths { if case let .prepaid(rhsTheme, rhsText, rhsSubtext, rhsPrepaidGiveaway) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsSubtext == rhsSubtext, lhsPrepaidGiveaway == rhsPrepaidGiveaway {
return true return true
} else { } else {
return false return false
@ -342,10 +342,9 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
}) })
case let .prepaidHeader(_, text): case let .prepaidHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .prepaid(_, title, subtitle, boosts, months): case let .prepaid(_, title, subtitle, prepaidGiveaway):
let _ = boosts
let color: GiftOptionItem.Icon.Color let color: GiftOptionItem.Icon.Color
switch months { switch prepaidGiveaway.months {
case 3: case 3:
color = .green color = .green
case 6: case 6:
@ -355,7 +354,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
default: default:
color = .blue color = .blue
} }
return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: GiftOptionItem.Icon(color: color, name: "Premium/Giveaway"), title: title, titleFont: .bold, subtitle: subtitle, label: .boosts(boosts), sectionId: self.section, action: nil) return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: GiftOptionItem.Icon(color: color, name: "Premium/Giveaway"), title: title, titleFont: .bold, subtitle: subtitle, label: .boosts(prepaidGiveaway.quantity), sectionId: self.section, action: nil)
case let .subscriptionsHeader(_, text, additionalText): case let .subscriptionsHeader(_, text, additionalText):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: ItemListSectionHeaderAccessoryText(value: additionalText, color: .generic), sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: ItemListSectionHeaderAccessoryText(value: additionalText, color: .generic), sectionId: self.section)
case let .subscriptions(_, value): case let .subscriptions(_, value):
@ -501,9 +500,9 @@ private func createGiveawayControllerEntries(peerId: EnginePeer.Id, subject: Cre
recipientsText = "select recipients" recipientsText = "select recipients"
} }
entries.append(.awardUsers(presentationData.theme, "Award Specific Users", recipientsText, state.mode == .gift)) entries.append(.awardUsers(presentationData.theme, "Award Specific Users", recipientsText, state.mode == .gift))
case let .prepaid(months, count): case let .prepaid(prepaidGiveaway):
entries.append(.prepaidHeader(presentationData.theme, "PREPAID GIVEAWAY")) entries.append(.prepaidHeader(presentationData.theme, "PREPAID GIVEAWAY"))
entries.append(.prepaid(presentationData.theme, "\(count) Telegram Premium", "\(months)-month subscriptions", count, months)) entries.append(.prepaid(presentationData.theme, "\(prepaidGiveaway.quantity) Telegram Premium", "\(prepaidGiveaway.months)-month subscriptions", prepaidGiveaway))
} }
if case .giveaway = state.mode { if case .giveaway = state.mode {
@ -607,15 +606,15 @@ private struct CreateGiveawayControllerState: Equatable {
public enum CreateGiveawaySubject { public enum CreateGiveawaySubject {
case generic case generic
case prepaid(months: Int32, count: Int32) case prepaid(PrepaidGiveaway)
} }
public func createGiveawayController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id, subject: CreateGiveawaySubject, completion: (() -> Void)? = nil) -> ViewController { public func createGiveawayController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id, subject: CreateGiveawaySubject, completion: (() -> Void)? = nil) -> ViewController {
let actionsDisposable = DisposableSet() let actionsDisposable = DisposableSet()
let initialSubscriptions: Int32 let initialSubscriptions: Int32
if case let .prepaid(_, count) = subject { if case let .prepaid(prepaidGiveaway) = subject {
initialSubscriptions = count initialSubscriptions = prepaidGiveaway.quantity
} else { } else {
initialSubscriptions = 5 initialSubscriptions = 5
} }
@ -793,7 +792,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
let purpose: AppStoreTransactionPurpose let purpose: AppStoreTransactionPurpose
switch state.mode { switch state.mode {
case .giveaway: case .giveaway:
purpose = .giveaway(boostPeer: peerId, additionalPeerIds: state.channels.filter { $0 != peerId}, onlyNewSubscribers: state.onlyNewEligible, randomId: Int64.random(in: .min ..< .max), untilDate: state.time, currency: currency, amount: amount) purpose = .giveaway(boostPeer: peerId, additionalPeerIds: state.channels.filter { $0 != peerId }, onlyNewSubscribers: state.onlyNewEligible, randomId: Int64.random(in: .min ..< .max), untilDate: state.time, currency: currency, amount: amount)
case .gift: case .gift:
purpose = .giftCode(peerIds: state.peers, boostPeer: peerId, currency: currency, amount: amount) purpose = .giftCode(peerIds: state.peers, boostPeer: peerId, currency: currency, amount: amount)
} }
@ -804,57 +803,59 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
return updatedState return updatedState
} }
let _ = (context.engine.payments.canPurchasePremium(purpose: purpose) switch subject {
|> deliverOnMainQueue).startStandalone(next: { [weak controller] available in case .generic:
if available, let inAppPurchaseManager = context.inAppPurchaseManager { let _ = (context.engine.payments.canPurchasePremium(purpose: purpose)
let _ = (inAppPurchaseManager.buyProduct(selectedProduct.storeProduct, quantity: selectedProduct.giftOption.storeQuantity, purpose: purpose) |> deliverOnMainQueue).startStandalone(next: { [weak controller] available in
|> deliverOnMainQueue).startStandalone(next: { [weak controller] status in if available, let inAppPurchaseManager = context.inAppPurchaseManager {
if case .purchased = status { let _ = (inAppPurchaseManager.buyProduct(selectedProduct.storeProduct, quantity: selectedProduct.giftOption.storeQuantity, purpose: purpose)
if let controller, let navigationController = controller.navigationController as? NavigationController { |> deliverOnMainQueue).startStandalone(next: { [weak controller] status in
var controllers = navigationController.viewControllers if case .purchased = status {
var count = 0 if let controller, let navigationController = controller.navigationController as? NavigationController {
for c in controllers.reversed() { var controllers = navigationController.viewControllers
if c is PeerInfoScreen { var count = 0
if case .giveaway = state.mode { for c in controllers.reversed() {
if c is PeerInfoScreen {
if case .giveaway = state.mode {
count += 1
}
break
} else {
count += 1 count += 1
} }
break
} else {
count += 1
} }
} controllers.removeLast(count)
controllers.removeLast(count) navigationController.setViewControllers(controllers, animated: true)
navigationController.setViewControllers(controllers, animated: true)
let title: String
let title: String let text: String
let text: String switch state.mode {
switch state.mode { case .giveaway:
case .giveaway: title = "Giveaway Created"
title = "Giveaway Created" text = "Check your channel's [Statistics]() to see how this giveaway boosted your channel."
text = "Check your channel's [Statistics]() to see how this giveaway boosted your channel." case .gift:
case .gift: title = "Premium Subscriptions Gifted"
title = "Premium Subscriptions Gifted" text = "Check your channel's [Statistics]() to see how gifts boosted your channel."
text = "Check your channel's [Statistics]() to see how gifts boosted your channel." }
}
let tooltipController = UndoOverlayController(presentationData: presentationData, content: .premiumPaywall(title: title, text: text, customUndoText: nil, timeout: nil, linkAction: { [weak navigationController] _ in
let tooltipController = UndoOverlayController(presentationData: presentationData, content: .premiumPaywall(title: title, text: text, customUndoText: nil, timeout: nil, linkAction: { [weak navigationController] _ in let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.StatsDatacenterId(id: peerId))
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.StatsDatacenterId(id: peerId)) |> deliverOnMainQueue).startStandalone(next: { [weak navigationController] statsDatacenterId in
|> deliverOnMainQueue).startStandalone(next: { [weak navigationController] statsDatacenterId in guard let statsDatacenterId else {
guard let statsDatacenterId else { return
return }
} let statsController = context.sharedContext.makeChannelStatsController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, boosts: true, boostStatus: nil, statsDatacenterId: statsDatacenterId)
let statsController = context.sharedContext.makeChannelStatsController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, boosts: true, boostStatus: nil, statsDatacenterId: statsDatacenterId) navigationController?.pushViewController(statsController)
navigationController?.pushViewController(statsController) })
}), elevatedLayout: false, action: { _ in
return true
}) })
}), elevatedLayout: false, action: { _ in (controllers.last as? ViewController)?.present(tooltipController, in: .current)
return true }
})
(controllers.last as? ViewController)?.present(tooltipController, in: .current)
} }
} }, error: { error in
}, error: { error in var errorText: String?
var errorText: String? switch error {
switch error {
case .generic: case .generic:
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
case .network: case .network:
@ -867,27 +868,66 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
case .cancelled: case .cancelled:
break break
} }
if let errorText = errorText { if let errorText = errorText {
let alertController = textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) let alertController = textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
presentControllerImpl?(alertController) presentControllerImpl?(alertController)
} }
updateState { state in
var updatedState = state
updatedState.updating = false
return updatedState
}
})
} else {
updateState { state in updateState { state in
var updatedState = state var updatedState = state
updatedState.updating = false updatedState.updating = false
return updatedState return updatedState
} }
})
} else {
updateState { state in
var updatedState = state
updatedState.updating = false
return updatedState
} }
} })
}) case let .prepaid(prepaidGiveaway):
let _ = (context.engine.payments.launchPrepaidGiveaway(peerId: peerId, id: prepaidGiveaway.id, additionalPeerIds: state.channels.filter { $0 != peerId }, onlyNewSubscribers: state.onlyNewEligible, randomId: Int64.random(in: .min ..< .max), untilDate: state.time)
|> deliverOnMainQueue).startStandalone(completed: {
if let controller, let navigationController = controller.navigationController as? NavigationController {
var controllers = navigationController.viewControllers
var count = 0
for c in controllers.reversed() {
if c is PeerInfoScreen {
if case .giveaway = state.mode {
count += 1
}
break
} else {
count += 1
}
}
controllers.removeLast(count)
navigationController.setViewControllers(controllers, animated: true)
let title = "Giveaway Created"
let text = "Check your channel's [Statistics]() to see how this giveaway boosted your channel."
let tooltipController = UndoOverlayController(presentationData: presentationData, content: .premiumPaywall(title: title, text: text, customUndoText: nil, timeout: nil, linkAction: { [weak navigationController] _ in
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.StatsDatacenterId(id: peerId))
|> deliverOnMainQueue).startStandalone(next: { [weak navigationController] statsDatacenterId in
guard let statsDatacenterId else {
return
}
let statsController = context.sharedContext.makeChannelStatsController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, boosts: true, boostStatus: nil, statsDatacenterId: statsDatacenterId)
navigationController?.pushViewController(statsController)
})
}), elevatedLayout: false, action: { _ in
return true
})
(controllers.last as? ViewController)?.present(tooltipController, in: .current)
}
})
break
}
} }
openPeersSelectionImpl = { openPeersSelectionImpl = {

View File

@ -18,6 +18,7 @@ import BalancedTextComponent
import ConfettiEffect import ConfettiEffect
import AvatarNode import AvatarNode
import TextFormat import TextFormat
import RoundedRectWithTailPath
func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
@ -40,131 +41,6 @@ func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor
}) })
} }
private func generateBadgePath(rectSize: CGSize, tailPosition: CGFloat? = 0.5) -> UIBezierPath {
let cornerRadius: CGFloat = rectSize.height / 2.0
let tailWidth: CGFloat = 20.0
let tailHeight: CGFloat = 9.0
let tailRadius: CGFloat = 4.0
let rect = CGRect(origin: CGPoint(x: 0.0, y: tailHeight), size: rectSize)
guard let tailPosition else {
return UIBezierPath(cgPath: CGPath(roundedRect: rect, cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil))
}
let path = UIBezierPath()
path.move(to: CGPoint(x: rect.minX, y: rect.minY + cornerRadius))
var leftArcEndAngle: CGFloat = .pi / 2.0
var leftConnectionArcRadius = tailRadius
var tailLeftHalfWidth: CGFloat = tailWidth / 2.0
var tailLeftArcStartAngle: CGFloat = -.pi / 4.0
var tailLeftHalfRadius = tailRadius
var rightArcStartAngle: CGFloat = -.pi / 2.0
var rightConnectionArcRadius = tailRadius
var tailRightHalfWidth: CGFloat = tailWidth / 2.0
var tailRightArcStartAngle: CGFloat = .pi / 4.0
var tailRightHalfRadius = tailRadius
if tailPosition < 0.5 {
let fraction = max(0.0, tailPosition - 0.15) / 0.35
leftArcEndAngle *= fraction
let connectionFraction = max(0.0, tailPosition - 0.35) / 0.15
leftConnectionArcRadius *= connectionFraction
if tailPosition < 0.27 {
let fraction = tailPosition / 0.27
tailLeftHalfWidth *= fraction
tailLeftArcStartAngle *= fraction
tailLeftHalfRadius *= fraction
}
} else if tailPosition > 0.5 {
let tailPosition = 1.0 - tailPosition
let fraction = max(0.0, tailPosition - 0.15) / 0.35
rightArcStartAngle *= fraction
let connectionFraction = max(0.0, tailPosition - 0.35) / 0.15
rightConnectionArcRadius *= connectionFraction
if tailPosition < 0.27 {
let fraction = tailPosition / 0.27
tailRightHalfWidth *= fraction
tailRightArcStartAngle *= fraction
tailRightHalfRadius *= fraction
}
}
path.addArc(
withCenter: CGPoint(x: rect.minX + cornerRadius, y: rect.minY + cornerRadius),
radius: cornerRadius,
startAngle: .pi,
endAngle: .pi + max(0.0001, leftArcEndAngle),
clockwise: true
)
let leftArrowStart = max(rect.minX, rect.minX + rectSize.width * tailPosition - tailLeftHalfWidth - leftConnectionArcRadius)
path.addArc(
withCenter: CGPoint(x: leftArrowStart, y: rect.minY - leftConnectionArcRadius),
radius: leftConnectionArcRadius,
startAngle: .pi / 2.0,
endAngle: .pi / 4.0,
clockwise: false
)
path.addLine(to: CGPoint(x: max(rect.minX, rect.minX + rectSize.width * tailPosition - tailLeftHalfRadius), y: rect.minY - tailHeight))
path.addArc(
withCenter: CGPoint(x: rect.minX + rectSize.width * tailPosition, y: rect.minY - tailHeight + tailRadius / 2.0),
radius: tailRadius,
startAngle: -.pi / 2.0 + tailLeftArcStartAngle,
endAngle: -.pi / 2.0 + tailRightArcStartAngle,
clockwise: true
)
path.addLine(to: CGPoint(x: min(rect.maxX, rect.minX + rectSize.width * tailPosition + tailRightHalfRadius), y: rect.minY - tailHeight))
let rightArrowStart = min(rect.maxX, rect.minX + rectSize.width * tailPosition + tailRightHalfWidth + rightConnectionArcRadius)
path.addArc(
withCenter: CGPoint(x: rightArrowStart, y: rect.minY - rightConnectionArcRadius),
radius: rightConnectionArcRadius,
startAngle: .pi - .pi / 4.0,
endAngle: .pi / 2.0,
clockwise: false
)
path.addArc(
withCenter: CGPoint(x: rect.minX + rectSize.width - cornerRadius, y: rect.minY + cornerRadius),
radius: cornerRadius,
startAngle: min(-0.0001, rightArcStartAngle),
endAngle: 0.0,
clockwise: true
)
path.addLine(to: CGPoint(x: rect.minX + rectSize.width, y: rect.minY + rectSize.height - cornerRadius))
path.addArc(
withCenter: CGPoint(x: rect.minX + rectSize.width - cornerRadius, y: rect.minY + rectSize.height - cornerRadius),
radius: cornerRadius,
startAngle: 0.0,
endAngle: .pi / 2.0,
clockwise: true
)
path.addLine(to: CGPoint(x: rect.minX + cornerRadius, y: rect.minY + rectSize.height))
path.addArc(
withCenter: CGPoint(x: rect.minX + cornerRadius, y: rect.minY + rectSize.height - cornerRadius),
radius: cornerRadius,
startAngle: .pi / 2.0,
endAngle: .pi,
clockwise: true
)
return path
}
public class PremiumLimitDisplayComponent: Component { public class PremiumLimitDisplayComponent: Component {
private let inactiveColor: UIColor private let inactiveColor: UIColor
private let activeColors: [UIColor] private let activeColors: [UIColor]
@ -658,7 +534,7 @@ public class PremiumLimitDisplayComponent: Component {
if badgePosition > 1.0 - 0.15 { if badgePosition > 1.0 - 0.15 {
progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 1.0, y: 1.0)) progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 1.0, y: 1.0))
progressTransition.setShapeLayerPath(layer: self.badgeShapeLayer, path: generateBadgePath(rectSize: badgeSize, tailPosition: component.isPremiumDisabled ? nil : 1.0).cgPath) progressTransition.setShapeLayerPath(layer: self.badgeShapeLayer, path: generateRoundedRectWithTailPath(rectSize: badgeSize, tailPosition: component.isPremiumDisabled ? nil : 1.0).cgPath)
if let _ = self.badgeView.layer.animation(forKey: "appearance1") { if let _ = self.badgeView.layer.animation(forKey: "appearance1") {
@ -667,7 +543,7 @@ public class PremiumLimitDisplayComponent: Component {
} }
} else if badgePosition < 0.15 { } else if badgePosition < 0.15 {
progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 0.0, y: 1.0)) progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 0.0, y: 1.0))
progressTransition.setShapeLayerPath(layer: self.badgeShapeLayer, path: generateBadgePath(rectSize: badgeSize, tailPosition: component.isPremiumDisabled ? nil : 0.0).cgPath) progressTransition.setShapeLayerPath(layer: self.badgeShapeLayer, path: generateRoundedRectWithTailPath(rectSize: badgeSize, tailPosition: component.isPremiumDisabled ? nil : 0.0).cgPath)
if let _ = self.badgeView.layer.animation(forKey: "appearance1") { if let _ = self.badgeView.layer.animation(forKey: "appearance1") {
@ -676,7 +552,7 @@ public class PremiumLimitDisplayComponent: Component {
} }
} else { } else {
progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 0.5, y: 1.0)) progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 0.5, y: 1.0))
progressTransition.setShapeLayerPath(layer: self.badgeShapeLayer, path: generateBadgePath(rectSize: badgeSize, tailPosition: component.isPremiumDisabled ? nil : 0.5).cgPath) progressTransition.setShapeLayerPath(layer: self.badgeShapeLayer, path: generateRoundedRectWithTailPath(rectSize: badgeSize, tailPosition: component.isPremiumDisabled ? nil : 0.5).cgPath)
if let _ = self.badgeView.layer.animation(forKey: "appearance1") { if let _ = self.badgeView.layer.animation(forKey: "appearance1") {

View File

@ -34,9 +34,9 @@ private final class ChannelStatsControllerArguments {
let openPeer: (EnginePeer) -> Void let openPeer: (EnginePeer) -> Void
let expandBoosters: () -> Void let expandBoosters: () -> Void
let openGifts: () -> Void let openGifts: () -> Void
let createPrepaidGiveaway: (Int32, Int32) -> Void let createPrepaidGiveaway: (PrepaidGiveaway) -> Void
init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>, openMessage: @escaping (MessageId) -> Void, contextAction: @escaping (MessageId, ASDisplayNode, ContextGesture?) -> Void, copyBoostLink: @escaping (String) -> Void, shareBoostLink: @escaping (String) -> Void, openPeer: @escaping (EnginePeer) -> Void, expandBoosters: @escaping () -> Void, openGifts: @escaping () -> Void, createPrepaidGiveaway: @escaping (Int32, Int32) -> Void) { init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>, openMessage: @escaping (MessageId) -> Void, contextAction: @escaping (MessageId, ASDisplayNode, ContextGesture?) -> Void, copyBoostLink: @escaping (String) -> Void, shareBoostLink: @escaping (String) -> Void, openPeer: @escaping (EnginePeer) -> Void, expandBoosters: @escaping () -> Void, openGifts: @escaping () -> Void, createPrepaidGiveaway: @escaping (PrepaidGiveaway) -> Void) {
self.context = context self.context = context
self.loadDetailedGraph = loadDetailedGraph self.loadDetailedGraph = loadDetailedGraph
self.openMessageStats = openMessage self.openMessageStats = openMessage
@ -110,7 +110,7 @@ private enum StatsEntry: ItemListNodeEntry {
case boostOverview(PresentationTheme, ChannelBoostStatus) case boostOverview(PresentationTheme, ChannelBoostStatus)
case boostPrepaidTitle(PresentationTheme, String) case boostPrepaidTitle(PresentationTheme, String)
case boostPrepaid(Int32, PresentationTheme, String, String, Int32, Int32) case boostPrepaid(Int32, PresentationTheme, String, String, PrepaidGiveaway)
case boostPrepaidInfo(PresentationTheme, String) case boostPrepaidInfo(PresentationTheme, String)
case boostersTitle(PresentationTheme, String) case boostersTitle(PresentationTheme, String)
@ -219,7 +219,7 @@ private enum StatsEntry: ItemListNodeEntry {
return 2002 return 2002
case .boostPrepaidTitle: case .boostPrepaidTitle:
return 2003 return 2003
case let .boostPrepaid(index, _, _, _, _, _): case let .boostPrepaid(index, _, _, _, _):
return 2004 + index return 2004 + index
case .boostPrepaidInfo: case .boostPrepaidInfo:
return 2100 return 2100
@ -404,8 +404,8 @@ private enum StatsEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .boostPrepaid(lhsIndex, lhsTheme, lhsTitle, lhsSubtitle, lhsMonths, lhsCount): case let .boostPrepaid(lhsIndex, lhsTheme, lhsTitle, lhsSubtitle, lhsPrepaidGiveaway):
if case let .boostPrepaid(rhsIndex, rhsTheme, rhsTitle, rhsSubtitle, rhsMonths, rhsCount) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsMonths == rhsMonths, lhsCount == rhsCount { if case let .boostPrepaid(rhsIndex, rhsTheme, rhsTitle, rhsSubtitle, rhsPrepaidGiveaway) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsPrepaidGiveaway == rhsPrepaidGiveaway {
return true return true
} else { } else {
return false return false
@ -561,9 +561,9 @@ private enum StatsEntry: ItemListNodeEntry {
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.addBoostsIcon(theme), title: title, sectionId: self.section, editing: false, action: { return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.addBoostsIcon(theme), title: title, sectionId: self.section, editing: false, action: {
arguments.openGifts() arguments.openGifts()
}) })
case let .boostPrepaid(_, _, title, subtitle, months, count): case let .boostPrepaid(_, _, title, subtitle, prepaidGiveaway):
let color: GiftOptionItem.Icon.Color let color: GiftOptionItem.Icon.Color
switch months { switch prepaidGiveaway.months {
case 3: case 3:
color = .green color = .green
case 6: case 6:
@ -573,8 +573,8 @@ private enum StatsEntry: ItemListNodeEntry {
default: default:
color = .blue color = .blue
} }
return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: GiftOptionItem.Icon(color: color, name: "Premium/Giveaway"), title: title, titleFont: .bold, subtitle: subtitle, label: .boosts(count), sectionId: self.section, action: { return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: GiftOptionItem.Icon(color: color, name: "Premium/Giveaway"), title: title, titleFont: .bold, subtitle: subtitle, label: .boosts(prepaidGiveaway.quantity), sectionId: self.section, action: {
arguments.createPrepaidGiveaway(months, count) arguments.createPrepaidGiveaway(prepaidGiveaway)
}) })
} }
} }
@ -701,10 +701,15 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p
entries.append(.boostOverview(presentationData.theme, boostData)) entries.append(.boostOverview(presentationData.theme, boostData))
//TODO:localize //TODO:localize
entries.append(.boostPrepaidTitle(presentationData.theme, "PREPAID GIVEAWAYS")) if !boostData.prepaidGiveaways.isEmpty {
entries.append(.boostPrepaid(0, presentationData.theme, "70 Telegram Premium", "3-month subscriptions", 3, 70)) entries.append(.boostPrepaidTitle(presentationData.theme, "PREPAID GIVEAWAYS"))
entries.append(.boostPrepaid(1, presentationData.theme, "200 Telegram Premium", "6-month subscriptions", 6, 200)) var i: Int32 = 0
entries.append(.boostPrepaidInfo(presentationData.theme, "Select a giveaway you already paid for to set it up.")) for giveaway in boostData.prepaidGiveaways {
entries.append(.boostPrepaid(i, presentationData.theme, "\(giveaway.quantity) Telegram Premium", "\(giveaway.months)-month subscriptions", giveaway))
i += 1
}
entries.append(.boostPrepaidInfo(presentationData.theme, "Select a giveaway you already paid for to set it up."))
}
let boostersTitle: String let boostersTitle: String
let boostersPlaceholder: String? let boostersPlaceholder: String?
@ -871,8 +876,8 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
let controller = createGiveawayController(context: context, peerId: peerId, subject: .generic) let controller = createGiveawayController(context: context, peerId: peerId, subject: .generic)
pushImpl?(controller) pushImpl?(controller)
}, },
createPrepaidGiveaway: { months, count in createPrepaidGiveaway: { prepaidGiveaway in
let controller = createGiveawayController(context: context, peerId: peerId, subject: .prepaid(months: months, count: count)) let controller = createGiveawayController(context: context, peerId: peerId, subject: .prepaid(prepaidGiveaway))
pushImpl?(controller) pushImpl?(controller)
}) })

View File

@ -0,0 +1,18 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "RoundedRectWithTailPath",
module_name = "RoundedRectWithTailPath",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,130 @@
import Foundation
import UIKit
public func generateRoundedRectWithTailPath(rectSize: CGSize, cornerRadius: CGFloat? = nil, tailSize: CGSize = CGSize(width: 20.0, height: 9.0), tailRadius: CGFloat = 4.0, tailPosition: CGFloat? = 0.5, transformTail: Bool = true) -> UIBezierPath {
let cornerRadius: CGFloat = cornerRadius ?? rectSize.height / 2.0
let tailWidth: CGFloat = tailSize.width
let tailHeight: CGFloat = tailSize.height
let rect = CGRect(origin: CGPoint(x: 0.0, y: tailHeight), size: rectSize)
guard let tailPosition else {
return UIBezierPath(cgPath: CGPath(roundedRect: rect, cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil))
}
let cutoff: CGFloat = 0.27
let path = UIBezierPath()
path.move(to: CGPoint(x: rect.minX, y: rect.minY + cornerRadius))
var leftArcEndAngle: CGFloat = .pi / 2.0
var leftConnectionArcRadius = tailRadius
var tailLeftHalfWidth: CGFloat = tailWidth / 2.0
var tailLeftArcStartAngle: CGFloat = -.pi / 4.0
var tailLeftHalfRadius = tailRadius
var rightArcStartAngle: CGFloat = -.pi / 2.0
var rightConnectionArcRadius = tailRadius
var tailRightHalfWidth: CGFloat = tailWidth / 2.0
var tailRightArcStartAngle: CGFloat = .pi / 4.0
var tailRightHalfRadius = tailRadius
if transformTail {
if tailPosition < 0.5 {
let fraction = max(0.0, tailPosition - 0.15) / 0.35
leftArcEndAngle *= fraction
let connectionFraction = max(0.0, tailPosition - 0.35) / 0.15
leftConnectionArcRadius *= connectionFraction
if tailPosition < cutoff {
let fraction = tailPosition / cutoff
tailLeftHalfWidth *= fraction
tailLeftArcStartAngle *= fraction
tailLeftHalfRadius *= fraction
}
} else if tailPosition > 0.5 {
let tailPosition = 1.0 - tailPosition
let fraction = max(0.0, tailPosition - 0.15) / 0.35
rightArcStartAngle *= fraction
let connectionFraction = max(0.0, tailPosition - 0.35) / 0.15
rightConnectionArcRadius *= connectionFraction
if tailPosition < cutoff {
let fraction = tailPosition / cutoff
tailRightHalfWidth *= fraction
tailRightArcStartAngle *= fraction
tailRightHalfRadius *= fraction
}
}
}
path.addArc(
withCenter: CGPoint(x: rect.minX + cornerRadius, y: rect.minY + cornerRadius),
radius: cornerRadius,
startAngle: .pi,
endAngle: .pi + max(0.0001, leftArcEndAngle),
clockwise: true
)
let leftArrowStart = max(rect.minX, rect.minX + rectSize.width * tailPosition - tailLeftHalfWidth - leftConnectionArcRadius)
path.addArc(
withCenter: CGPoint(x: leftArrowStart, y: rect.minY - leftConnectionArcRadius),
radius: leftConnectionArcRadius,
startAngle: .pi / 2.0,
endAngle: .pi / 4.0,
clockwise: false
)
path.addLine(to: CGPoint(x: max(rect.minX, rect.minX + rectSize.width * tailPosition - tailLeftHalfRadius), y: rect.minY - tailHeight))
path.addArc(
withCenter: CGPoint(x: rect.minX + rectSize.width * tailPosition, y: rect.minY - tailHeight + tailRadius / 2.0),
radius: tailRadius,
startAngle: -.pi / 2.0 + tailLeftArcStartAngle,
endAngle: -.pi / 2.0 + tailRightArcStartAngle,
clockwise: true
)
path.addLine(to: CGPoint(x: min(rect.maxX, rect.minX + rectSize.width * tailPosition + tailRightHalfRadius), y: rect.minY - tailHeight))
let rightArrowStart = min(rect.maxX, rect.minX + rectSize.width * tailPosition + tailRightHalfWidth + rightConnectionArcRadius)
path.addArc(
withCenter: CGPoint(x: rightArrowStart, y: rect.minY - rightConnectionArcRadius),
radius: rightConnectionArcRadius,
startAngle: .pi - .pi / 4.0,
endAngle: .pi / 2.0,
clockwise: false
)
path.addArc(
withCenter: CGPoint(x: rect.minX + rectSize.width - cornerRadius, y: rect.minY + cornerRadius),
radius: cornerRadius,
startAngle: min(-0.0001, rightArcStartAngle),
endAngle: 0.0,
clockwise: true
)
path.addLine(to: CGPoint(x: rect.minX + rectSize.width, y: rect.minY + rectSize.height - cornerRadius))
path.addArc(
withCenter: CGPoint(x: rect.minX + rectSize.width - cornerRadius, y: rect.minY + rectSize.height - cornerRadius),
radius: cornerRadius,
startAngle: 0.0,
endAngle: .pi / 2.0,
clockwise: true
)
path.addLine(to: CGPoint(x: rect.minX + cornerRadius, y: rect.minY + rectSize.height))
path.addArc(
withCenter: CGPoint(x: rect.minX + cornerRadius, y: rect.minY + rectSize.height - cornerRadius),
radius: cornerRadius,
startAngle: .pi / 2.0,
endAngle: .pi,
clockwise: true
)
return path
}