Merge commit '91e73050508ba2f4b0b201cbe9ab6584bb2e62f3'

This commit is contained in:
Ali 2023-10-11 00:41:02 +04:00
commit 50881b558f
19 changed files with 524 additions and 250 deletions

View File

@ -580,8 +580,8 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable
}
}
public func ensureItemNodeVisible(_ itemNode: ListViewItemNode, animated: Bool = true, curve: ListViewAnimationCurve = .Default(duration: 0.25)) {
(self.displayNode as! ItemListControllerNode).listNode.ensureItemNodeVisible(itemNode, animated: animated, curve: curve)
public func ensureItemNodeVisible(_ itemNode: ListViewItemNode, animated: Bool = true, overflow: CGFloat = 0.0, atTop: Bool = false, curve: ListViewAnimationCurve = .Default(duration: 0.25)) {
(self.displayNode as! ItemListControllerNode).listNode.ensureItemNodeVisible(itemNode, animated: animated, overflow: overflow, atTop: atTop, curve: curve)
}
public func afterLayout(_ f: @escaping () -> Void) {

View File

@ -389,7 +389,7 @@ open class ItemListControllerNode: ASDisplayNode {
}
case .unknown, .none:
if let headerItemNode = strongSelf.headerItemNode {
headerItemNode.updateContentOffset(0.0, transition: .immediate)
headerItemNode.updateContentOffset(1000.0, transition: .immediate)
strongSelf.navigationBar.updateBackgroundAlpha(0.0, transition: .immediate)
} else {
strongSelf.navigationBar.updateBackgroundAlpha(1.0, transition: .immediate)

View File

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

View File

@ -27,14 +27,16 @@ private final class CreateGiveawayControllerArguments {
let openPeersSelection: () -> Void
let openChannelsSelection: () -> Void
let openPremiumIntro: () -> Void
let scrollToDate: () -> Void
init(context: AccountContext, updateState: @escaping ((CreateGiveawayControllerState) -> CreateGiveawayControllerState) -> Void, dismissInput: @escaping () -> Void, openPeersSelection: @escaping () -> Void, openChannelsSelection: @escaping () -> Void, openPremiumIntro: @escaping () -> Void) {
init(context: AccountContext, updateState: @escaping ((CreateGiveawayControllerState) -> CreateGiveawayControllerState) -> Void, dismissInput: @escaping () -> Void, openPeersSelection: @escaping () -> Void, openChannelsSelection: @escaping () -> Void, openPremiumIntro: @escaping () -> Void, scrollToDate: @escaping () -> Void) {
self.context = context
self.updateState = updateState
self.dismissInput = dismissInput
self.openPeersSelection = openPeersSelection
self.openChannelsSelection = openChannelsSelection
self.openPremiumIntro = openPremiumIntro
self.scrollToDate = scrollToDate
}
}
@ -49,7 +51,7 @@ private enum CreateGiveawaySection: Int32 {
}
private enum CreateGiveawayEntryTag: ItemListItemTag {
case usage
case date
func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? CreateGiveawayEntryTag, self == other {
@ -67,7 +69,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
case awardUsers(PresentationTheme, String, String, Bool)
case prepaidHeader(PresentationTheme, String)
case prepaid(PresentationTheme, String, String, Int32, Int32)
case prepaid(PresentationTheme, String, String, PrepaidGiveaway)
case subscriptionsHeader(PresentationTheme, String, String)
case subscriptions(PresentationTheme, Int32)
@ -188,8 +190,8 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
} else {
return false
}
case let .prepaid(lhsTheme, lhsText, lhsSubtext, lhsBoosts, lhsMonths):
if case let .prepaid(rhsTheme, rhsText, rhsSubtext, rhsBoosts, rhsMonths) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsSubtext == rhsSubtext, lhsBoosts == rhsBoosts, lhsMonths == rhsMonths {
case let .prepaid(lhsTheme, lhsText, lhsSubtext, lhsPrepaidGiveaway):
if case let .prepaid(rhsTheme, rhsText, rhsSubtext, rhsPrepaidGiveaway) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsSubtext == rhsSubtext, lhsPrepaidGiveaway == rhsPrepaidGiveaway {
return true
} else {
return false
@ -340,10 +342,9 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
})
case let .prepaidHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .prepaid(_, title, subtitle, boosts, months):
let _ = boosts
case let .prepaid(_, title, subtitle, prepaidGiveaway):
let color: GiftOptionItem.Icon.Color
switch months {
switch prepaidGiveaway.months {
case 3:
color = .green
case 6:
@ -353,7 +354,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
default:
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):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: ItemListSectionHeaderAccessoryText(value: additionalText, color: .generic), sectionId: self.section)
case let .subscriptions(_, value):
@ -409,11 +410,20 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
}
return ItemListDisclosureItem(presentationData: presentationData, title: "Ends", label: text, labelStyle: active ? .coloredText(theme.list.itemAccentColor) : .text, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: {
arguments.dismissInput()
var focus = false
arguments.updateState { state in
var updatedState = state
updatedState.pickingTimeLimit = !state.pickingTimeLimit
if updatedState.pickingTimeLimit {
focus = true
}
return updatedState
}
if focus {
Queue.mainQueue().after(0.1) {
arguments.scrollToDate()
}
}
})
case let .timeCustomPicker(_, dateTimeFormat, date):
return ItemListDatePickerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, date: date, sectionId: self.section, style: .blocks, updated: { date in
@ -422,7 +432,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
updatedState.time = date
return updatedState
})
})
}, tag: CreateGiveawayEntryTag.date)
case let .timeInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .durationHeader(_, text):
@ -490,9 +500,9 @@ private func createGiveawayControllerEntries(peerId: EnginePeer.Id, subject: Cre
recipientsText = "select recipients"
}
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(.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 {
@ -596,15 +606,15 @@ private struct CreateGiveawayControllerState: Equatable {
public enum CreateGiveawaySubject {
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 {
let actionsDisposable = DisposableSet()
let initialSubscriptions: Int32
if case let .prepaid(_, count) = subject {
initialSubscriptions = count
if case let .prepaid(prepaidGiveaway) = subject {
initialSubscriptions = prepaidGiveaway.quantity
} else {
initialSubscriptions = 5
}
@ -626,6 +636,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
var openPremiumIntroImpl: (() -> Void)?
var presentControllerImpl: ((ViewController) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
var scrollToDateImpl: (() -> Void)?
var dismissImpl: (() -> Void)?
var dismissInputImpl: (() -> Void)?
@ -639,6 +650,8 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
openChannelsSelectionImpl?()
}, openPremiumIntro: {
openPremiumIntroImpl?()
}, scrollToDate: {
scrollToDateImpl?()
})
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
@ -779,7 +792,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
let purpose: AppStoreTransactionPurpose
switch state.mode {
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:
purpose = .giftCode(peerIds: state.peers, boostPeer: peerId, currency: currency, amount: amount)
}
@ -790,57 +803,59 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
return updatedState
}
let _ = (context.engine.payments.canPurchasePremium(purpose: purpose)
|> deliverOnMainQueue).startStandalone(next: { [weak controller] available in
if available, let inAppPurchaseManager = context.inAppPurchaseManager {
let _ = (inAppPurchaseManager.buyProduct(selectedProduct.storeProduct, quantity: selectedProduct.giftOption.storeQuantity, purpose: purpose)
|> deliverOnMainQueue).startStandalone(next: { [weak controller] status in
if case .purchased = status {
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 {
switch subject {
case .generic:
let _ = (context.engine.payments.canPurchasePremium(purpose: purpose)
|> deliverOnMainQueue).startStandalone(next: { [weak controller] available in
if available, let inAppPurchaseManager = context.inAppPurchaseManager {
let _ = (inAppPurchaseManager.buyProduct(selectedProduct.storeProduct, quantity: selectedProduct.giftOption.storeQuantity, purpose: purpose)
|> deliverOnMainQueue).startStandalone(next: { [weak controller] status in
if case .purchased = status {
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
}
break
} else {
count += 1
}
}
controllers.removeLast(count)
navigationController.setViewControllers(controllers, animated: true)
let title: String
let text: String
switch state.mode {
case .giveaway:
title = "Giveaway Created"
text = "Check your channel's [Statistics]() to see how this giveaway boosted your channel."
case .gift:
title = "Premium Subscriptions Gifted"
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 _ = (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)
controllers.removeLast(count)
navigationController.setViewControllers(controllers, animated: true)
let title: String
let text: String
switch state.mode {
case .giveaway:
title = "Giveaway Created"
text = "Check your channel's [Statistics]() to see how this giveaway boosted your channel."
case .gift:
title = "Premium Subscriptions Gifted"
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 _ = (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
})
}), elevatedLayout: false, action: { _ in
return true
})
(controllers.last as? ViewController)?.present(tooltipController, in: .current)
(controllers.last as? ViewController)?.present(tooltipController, in: .current)
}
}
}
}, error: { error in
var errorText: String?
switch error {
}, error: { error in
var errorText: String?
switch error {
case .generic:
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
case .network:
@ -853,30 +868,66 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
case .cancelled:
break
}
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: {})])
presentControllerImpl?(alertController)
}
}
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: {})])
presentControllerImpl?(alertController)
}
updateState { state in
var updatedState = state
updatedState.updating = false
return updatedState
}
})
} else {
updateState { state in
var updatedState = state
updatedState.updating = false
return updatedState
}
})
} else {
let alertController = textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Premium_Purchase_ErrorUnknown, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
presentControllerImpl?(alertController)
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 = {
@ -903,6 +954,15 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
}
}
)
controller.dismissed = {
updateState { state in
var updatedState = state
if updatedState.peers.isEmpty {
updatedState.mode = .giveaway
}
return updatedState
}
}
pushControllerImpl?(controller)
})
}
@ -937,5 +997,27 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
pushControllerImpl?(controller)
}
scrollToDateImpl = { [weak controller] in
controller?.afterLayout({
guard let controller = controller else {
return
}
var resultItemNode: ListViewItemNode?
let _ = controller.frameForItemNode({ listItemNode in
if let itemNode = listItemNode as? ItemListItemNode {
if let tag = itemNode.tag as? CreateGiveawayEntryTag, tag == .date {
resultItemNode = listItemNode
return true
}
}
return false
})
if let resultItemNode = resultItemNode {
controller.ensureItemNodeVisible(resultItemNode, overflow: 120.0, atTop: true)
}
})
}
return controller
}

View File

@ -260,7 +260,7 @@ class GiftOptionItemNode: ItemListRevealOptionsItemNode {
let titleAttributedString = NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)
let statusAttributedString = NSAttributedString(string: item.subtitle ?? "", font: statusFont, textColor: item.subtitleActive ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemSecondaryTextColor)
let badgeAttributedString = NSAttributedString(string: item.badge ?? "", font: Font.with(size: 14.0, design: .round, weight: .semibold), textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor)
let badgeAttributedString = NSAttributedString(string: item.badge ?? "", font: Font.with(size: 13.0, design: .round, weight: .semibold), textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor)
let labelColor: UIColor
let labelFont: UIFont
@ -450,7 +450,7 @@ class GiftOptionItemNode: ItemListRevealOptionsItemNode {
var badgeOffset: CGFloat = 0.0
if badgeLayout.size.width > 0.0 {
let badgeFrame = CGRect(origin: CGPoint(x: leftInset + editingOffset + avatarInset + 2.0, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: badgeLayout.size)
let badgeBackgroundFrame = badgeFrame.insetBy(dx: -2.0, dy: -2.0)
let badgeBackgroundFrame = badgeFrame.insetBy(dx: -3.0, dy: -2.0)
let badgeBackgroundNode: ASImageNode
if let current = strongSelf.badgeBackgroundNode {
@ -491,7 +491,7 @@ class GiftOptionItemNode: ItemListRevealOptionsItemNode {
strongSelf.containerNode.addSubnode(iconNode)
strongSelf.labelBackgroundNode = backgroundNode
strongSelf.labelIconNode = backgroundNode
strongSelf.labelIconNode = iconNode
}
if let icon = iconNode.image {

View File

@ -18,6 +18,7 @@ import BalancedTextComponent
import ConfettiEffect
import AvatarNode
import TextFormat
import RoundedRectWithTailPath
func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? {
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 {
private let inactiveColor: UIColor
private let activeColors: [UIColor]
@ -658,7 +534,7 @@ public class PremiumLimitDisplayComponent: Component {
if badgePosition > 1.0 - 0.15 {
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") {
@ -667,7 +543,7 @@ public class PremiumLimitDisplayComponent: Component {
}
} else if badgePosition < 0.15 {
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") {
@ -676,7 +552,7 @@ public class PremiumLimitDisplayComponent: Component {
}
} else {
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") {

View File

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

View File

@ -663,6 +663,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[629052971] = { return Api.PremiumGiftCodeOption.parse_premiumGiftCodeOption($0) }
dict[1958953753] = { return Api.PremiumGiftOption.parse_premiumGiftOption($0) }
dict[1596792306] = { return Api.PremiumSubscriptionOption.parse_premiumSubscriptionOption($0) }
dict[-1303143084] = { return Api.PrepaidGiveaway.parse_prepaidGiveaway($0) }
dict[-1534675103] = { return Api.PrivacyKey.parse_privacyKeyAbout($0) }
dict[1124062251] = { return Api.PrivacyKey.parse_privacyKeyAddedByPhone($0) }
dict[1343122938] = { return Api.PrivacyKey.parse_privacyKeyChatInvite($0) }
@ -1187,7 +1188,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1862033025] = { return Api.stories.AllStories.parse_allStories($0) }
dict[291044926] = { return Api.stories.AllStories.parse_allStoriesNotModified($0) }
dict[-203604707] = { return Api.stories.BoostersList.parse_boostersList($0) }
dict[-440292772] = { return Api.stories.BoostsStatus.parse_boostsStatus($0) }
dict[1911715597] = { return Api.stories.BoostsStatus.parse_boostsStatus($0) }
dict[-1021889145] = { return Api.stories.CanApplyBoostResult.parse_canApplyBoostOk($0) }
dict[1898726997] = { return Api.stories.CanApplyBoostResult.parse_canApplyBoostReplace($0) }
dict[-890861720] = { return Api.stories.PeerStories.parse_peerStories($0) }
@ -1673,6 +1674,8 @@ public extension Api {
_1.serialize(buffer, boxed)
case let _1 as Api.PremiumSubscriptionOption:
_1.serialize(buffer, boxed)
case let _1 as Api.PrepaidGiveaway:
_1.serialize(buffer, boxed)
case let _1 as Api.PrivacyKey:
_1.serialize(buffer, boxed)
case let _1 as Api.PrivacyRule:

View File

@ -926,6 +926,54 @@ public extension Api {
}
}
public extension Api {
enum PrepaidGiveaway: TypeConstructorDescription {
case prepaidGiveaway(id: Int64, months: Int32, quantity: Int32, date: Int32)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .prepaidGiveaway(let id, let months, let quantity, let date):
if boxed {
buffer.appendInt32(-1303143084)
}
serializeInt64(id, buffer: buffer, boxed: false)
serializeInt32(months, buffer: buffer, boxed: false)
serializeInt32(quantity, buffer: buffer, boxed: false)
serializeInt32(date, buffer: buffer, boxed: false)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .prepaidGiveaway(let id, let months, let quantity, let date):
return ("prepaidGiveaway", [("id", id as Any), ("months", months as Any), ("quantity", quantity as Any), ("date", date as Any)])
}
}
public static func parse_prepaidGiveaway(_ reader: BufferReader) -> PrepaidGiveaway? {
var _1: Int64?
_1 = reader.readInt64()
var _2: Int32?
_2 = reader.readInt32()
var _3: Int32?
_3 = reader.readInt32()
var _4: Int32?
_4 = reader.readInt32()
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = _4 != nil
if _c1 && _c2 && _c3 && _c4 {
return Api.PrepaidGiveaway.prepaidGiveaway(id: _1!, months: _2!, quantity: _3!, date: _4!)
}
else {
return nil
}
}
}
}
public extension Api {
enum PrivacyKey: TypeConstructorDescription {
case privacyKeyAbout

View File

@ -660,13 +660,13 @@ public extension Api.stories {
}
public extension Api.stories {
enum BoostsStatus: TypeConstructorDescription {
case boostsStatus(flags: Int32, level: Int32, currentLevelBoosts: Int32, boosts: Int32, nextLevelBoosts: Int32?, premiumAudience: Api.StatsPercentValue?, boostUrl: String)
case boostsStatus(flags: Int32, level: Int32, currentLevelBoosts: Int32, boosts: Int32, nextLevelBoosts: Int32?, premiumAudience: Api.StatsPercentValue?, boostUrl: String, prepaidGiveaways: [Api.PrepaidGiveaway]?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .boostsStatus(let flags, let level, let currentLevelBoosts, let boosts, let nextLevelBoosts, let premiumAudience, let boostUrl):
case .boostsStatus(let flags, let level, let currentLevelBoosts, let boosts, let nextLevelBoosts, let premiumAudience, let boostUrl, let prepaidGiveaways):
if boxed {
buffer.appendInt32(-440292772)
buffer.appendInt32(1911715597)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt32(level, buffer: buffer, boxed: false)
@ -675,14 +675,19 @@ public extension Api.stories {
if Int(flags) & Int(1 << 0) != 0 {serializeInt32(nextLevelBoosts!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 1) != 0 {premiumAudience!.serialize(buffer, true)}
serializeString(boostUrl, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261)
buffer.appendInt32(Int32(prepaidGiveaways!.count))
for item in prepaidGiveaways! {
item.serialize(buffer, true)
}}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .boostsStatus(let flags, let level, let currentLevelBoosts, let boosts, let nextLevelBoosts, let premiumAudience, let boostUrl):
return ("boostsStatus", [("flags", flags as Any), ("level", level as Any), ("currentLevelBoosts", currentLevelBoosts as Any), ("boosts", boosts as Any), ("nextLevelBoosts", nextLevelBoosts as Any), ("premiumAudience", premiumAudience as Any), ("boostUrl", boostUrl as Any)])
case .boostsStatus(let flags, let level, let currentLevelBoosts, let boosts, let nextLevelBoosts, let premiumAudience, let boostUrl, let prepaidGiveaways):
return ("boostsStatus", [("flags", flags as Any), ("level", level as Any), ("currentLevelBoosts", currentLevelBoosts as Any), ("boosts", boosts as Any), ("nextLevelBoosts", nextLevelBoosts as Any), ("premiumAudience", premiumAudience as Any), ("boostUrl", boostUrl as Any), ("prepaidGiveaways", prepaidGiveaways as Any)])
}
}
@ -703,6 +708,10 @@ public extension Api.stories {
} }
var _7: String?
_7 = parseString(reader)
var _8: [Api.PrepaidGiveaway]?
if Int(_1!) & Int(1 << 3) != 0 {if let _ = reader.readInt32() {
_8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PrepaidGiveaway.self)
} }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
@ -710,8 +719,9 @@ public extension Api.stories {
let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil
let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil
let _c7 = _7 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 {
return Api.stories.BoostsStatus.boostsStatus(flags: _1!, level: _2!, currentLevelBoosts: _3!, boosts: _4!, nextLevelBoosts: _5, premiumAudience: _6, boostUrl: _7!)
let _c8 = (Int(_1!) & Int(1 << 3) == 0) || _8 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 {
return Api.stories.BoostsStatus.boostsStatus(flags: _1!, level: _2!, currentLevelBoosts: _3!, boosts: _4!, nextLevelBoosts: _5, premiumAudience: _6, boostUrl: _7!, prepaidGiveaways: _8)
}
else {
return nil

View File

@ -7641,6 +7641,23 @@ public extension Api.functions.payments {
})
}
}
public extension Api.functions.payments {
static func launchPrepaidGiveaway(peer: Api.InputPeer, giveawayId: Int64, purpose: Api.InputStorePaymentPurpose) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
let buffer = Buffer()
buffer.appendInt32(1609928480)
peer.serialize(buffer, true)
serializeInt64(giveawayId, buffer: buffer, boxed: false)
purpose.serialize(buffer, true)
return (FunctionDescription(name: "payments.launchPrepaidGiveaway", parameters: [("peer", String(describing: peer)), ("giveawayId", String(describing: giveawayId)), ("purpose", String(describing: purpose))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
let reader = BufferReader(buffer)
var result: Api.Updates?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.Updates
}
return result
})
}
}
public extension Api.functions.payments {
static func sendPaymentForm(flags: Int32, formId: Int64, invoice: Api.InputInvoice, requestedInfoId: String?, shippingOptionId: String?, credentials: Api.InputPaymentCredentials, tipAmount: Int64?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.payments.PaymentResult>) {
let buffer = Buffer()

View File

@ -10,14 +10,16 @@ public final class ChannelBoostStatus: Equatable {
public let nextLevelBoosts: Int?
public let premiumAudience: StatsPercentValue?
public let url: String
public let prepaidGiveaways: [PrepaidGiveaway]
public init(level: Int, boosts: Int, currentLevelBoosts: Int, nextLevelBoosts: Int?, premiumAudience: StatsPercentValue?, url: String) {
public init(level: Int, boosts: Int, currentLevelBoosts: Int, nextLevelBoosts: Int?, premiumAudience: StatsPercentValue?, url: String, prepaidGiveaways: [PrepaidGiveaway]) {
self.level = level
self.boosts = boosts
self.currentLevelBoosts = currentLevelBoosts
self.nextLevelBoosts = nextLevelBoosts
self.premiumAudience = premiumAudience
self.url = url
self.prepaidGiveaways = prepaidGiveaways
}
public static func ==(lhs: ChannelBoostStatus, rhs: ChannelBoostStatus) -> Bool {
@ -39,6 +41,9 @@ public final class ChannelBoostStatus: Equatable {
if lhs.url != rhs.url {
return false
}
if lhs.prepaidGiveaways != rhs.prepaidGiveaways {
return false
}
return true
}
}
@ -62,8 +67,8 @@ func _internal_getChannelBoostStatus(account: Account, peerId: PeerId) -> Signal
}
switch result {
case let .boostsStatus(_, level, currentLevelBoosts, boosts, nextLevelBoosts, premiumAudience, url):
return ChannelBoostStatus(level: Int(level), boosts: Int(boosts), currentLevelBoosts: Int(currentLevelBoosts), nextLevelBoosts: nextLevelBoosts.flatMap(Int.init), premiumAudience: premiumAudience.flatMap({ StatsPercentValue(apiPercentValue: $0) }), url: url)
case let .boostsStatus(_, level, currentLevelBoosts, boosts, nextLevelBoosts, premiumAudience, url, prepaidGiveaways):
return ChannelBoostStatus(level: Int(level), boosts: Int(boosts), currentLevelBoosts: Int(currentLevelBoosts), nextLevelBoosts: nextLevelBoosts.flatMap(Int.init), premiumAudience: premiumAudience.flatMap({ StatsPercentValue(apiPercentValue: $0) }), url: url, prepaidGiveaways: prepaidGiveaways?.map({ PrepaidGiveaway(apiPrepaidGiveaway: $0) }) ?? [])
}
}
}

View File

@ -22,6 +22,7 @@ public struct UserLimitsConfiguration: Equatable {
public let maxStoriesWeeklyCount: Int32
public let maxStoriesMonthlyCount: Int32
public let maxStoriesSuggestedReactions: Int32
public let maxGiveawayChannelsCount: Int32
public static var defaultValue: UserLimitsConfiguration {
return UserLimitsConfiguration(
@ -44,7 +45,8 @@ public struct UserLimitsConfiguration: Equatable {
maxExpiringStoriesCount: 3,
maxStoriesWeeklyCount: 7,
maxStoriesMonthlyCount: 30,
maxStoriesSuggestedReactions: 1
maxStoriesSuggestedReactions: 1,
maxGiveawayChannelsCount: 10
)
}
@ -68,7 +70,8 @@ public struct UserLimitsConfiguration: Equatable {
maxExpiringStoriesCount: Int32,
maxStoriesWeeklyCount: Int32,
maxStoriesMonthlyCount: Int32,
maxStoriesSuggestedReactions: Int32
maxStoriesSuggestedReactions: Int32,
maxGiveawayChannelsCount: Int32
) {
self.maxPinnedChatCount = maxPinnedChatCount
self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount
@ -90,6 +93,7 @@ public struct UserLimitsConfiguration: Equatable {
self.maxStoriesWeeklyCount = maxStoriesWeeklyCount
self.maxStoriesMonthlyCount = maxStoriesMonthlyCount
self.maxStoriesSuggestedReactions = maxStoriesSuggestedReactions
self.maxGiveawayChannelsCount = maxGiveawayChannelsCount
}
}
@ -134,5 +138,6 @@ extension UserLimitsConfiguration {
self.maxStoriesWeeklyCount = getValue("stories_sent_weekly_limit", orElse: defaultValue.maxStoriesWeeklyCount)
self.maxStoriesMonthlyCount = getValue("stories_sent_monthly_limit", orElse: defaultValue.maxStoriesMonthlyCount)
self.maxStoriesSuggestedReactions = getValue("stories_suggested_reactions_limit", orElse: defaultValue.maxStoriesMonthlyCount)
self.maxGiveawayChannelsCount = getGeneralValue("giveaway_add_peers_max", orElse: defaultValue.maxGiveawayChannelsCount)
}
}

View File

@ -56,6 +56,7 @@ public enum EngineConfiguration {
public let maxStoriesWeeklyCount: Int32
public let maxStoriesMonthlyCount: Int32
public let maxStoriesSuggestedReactions: Int32
public let maxGiveawayChannelsCount: Int32
public static var defaultValue: UserLimits {
return UserLimits(UserLimitsConfiguration.defaultValue)
@ -81,7 +82,8 @@ public enum EngineConfiguration {
maxExpiringStoriesCount: Int32,
maxStoriesWeeklyCount: Int32,
maxStoriesMonthlyCount: Int32,
maxStoriesSuggestedReactions: Int32
maxStoriesSuggestedReactions: Int32,
maxGiveawayChannelsCount: Int32
) {
self.maxPinnedChatCount = maxPinnedChatCount
self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount
@ -103,6 +105,7 @@ public enum EngineConfiguration {
self.maxStoriesWeeklyCount = maxStoriesWeeklyCount
self.maxStoriesMonthlyCount = maxStoriesMonthlyCount
self.maxStoriesSuggestedReactions = maxStoriesSuggestedReactions
self.maxGiveawayChannelsCount = maxGiveawayChannelsCount
}
}
}
@ -159,7 +162,8 @@ public extension EngineConfiguration.UserLimits {
maxExpiringStoriesCount: userLimitsConfiguration.maxExpiringStoriesCount,
maxStoriesWeeklyCount: userLimitsConfiguration.maxStoriesWeeklyCount,
maxStoriesMonthlyCount: userLimitsConfiguration.maxStoriesMonthlyCount,
maxStoriesSuggestedReactions: userLimitsConfiguration.maxStoriesSuggestedReactions
maxStoriesSuggestedReactions: userLimitsConfiguration.maxStoriesSuggestedReactions,
maxGiveawayChannelsCount: userLimitsConfiguration.maxGiveawayChannelsCount
)
}
}

View File

@ -74,6 +74,13 @@ public enum PremiumGiveawayInfo: Equatable {
case finished(status: ResultStatus, finishDate: Int32, winnersCount: Int32, activatedCount: Int32)
}
public struct PrepaidGiveaway: Equatable {
public let id: Int64
public let months: Int32
public let quantity: Int32
public let date: Int32
}
func _internal_getPremiumGiveawayInfo(account: Account, peerId: EnginePeer.Id, messageId: EngineMessage.Id) -> Signal<PremiumGiveawayInfo?, NoError> {
return account.postbox.loadedPeerWithId(peerId)
|> mapToSignal { peer in
@ -177,6 +184,46 @@ func _internal_applyPremiumGiftCode(account: Account, slug: String) -> Signal<Ne
}
}
func _internal_launchPrepaidGiveaway(account: Account, peerId: EnginePeer.Id, id: Int64, additionalPeerIds: [EnginePeer.Id], onlyNewSubscribers: Bool, randomId: Int64, untilDate: Int32) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Signal<Never, NoError> in
var flags: Int32 = 0
if onlyNewSubscribers {
flags |= (1 << 0)
}
var inputPeer: Api.InputPeer?
if let peer = transaction.getPeer(peerId), let apiPeer = apiInputPeer(peer) {
inputPeer = apiPeer
}
var additionalPeers: [Api.InputPeer] = []
if !additionalPeerIds.isEmpty {
flags |= (1 << 1)
for peerId in additionalPeerIds {
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
additionalPeers.append(inputPeer)
}
}
}
guard let inputPeer = inputPeer else {
return .complete()
}
return account.network.request(Api.functions.payments.launchPrepaidGiveaway(peer: inputPeer, giveawayId: id, purpose: .inputStorePaymentPremiumGiveaway(flags: flags, boostPeer: inputPeer, additionalPeers: additionalPeers, randomId: randomId, untilDate: untilDate, currency: "", amount: 0)))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Never, NoError> in
if let updates = updates {
account.stateManager.addUpdates(updates)
}
return .complete()
}
}
|> switchToLatest
}
extension PremiumGiftCodeOption {
init(apiGiftCodeOption: Api.PremiumGiftCodeOption) {
switch apiGiftCodeOption {
@ -207,3 +254,15 @@ public extension PremiumGiftCodeInfo {
return self.usedDate != nil
}
}
extension PrepaidGiveaway {
init(apiPrepaidGiveaway: Api.PrepaidGiveaway) {
switch apiPrepaidGiveaway {
case let .prepaidGiveaway(id, months, quantity, date):
self.id = id
self.months = months
self.quantity = quantity
self.date = date
}
}
}

View File

@ -61,5 +61,9 @@ public extension TelegramEngine {
public func premiumGiveawayInfo(peerId: EnginePeer.Id, messageId: EngineMessage.Id) -> Signal<PremiumGiveawayInfo?, NoError> {
return _internal_getPremiumGiveawayInfo(account: self.account, peerId: peerId, messageId: messageId)
}
public func launchPrepaidGiveaway(peerId: EnginePeer.Id, id: Int64, additionalPeerIds: [EnginePeer.Id], onlyNewSubscribers: Bool, randomId: Int64, untilDate: Int32) -> Signal<Never, NoError> {
return _internal_launchPrepaidGiveaway(account: self.account, peerId: peerId, id: id, additionalPeerIds: additionalPeerIds, onlyNewSubscribers: onlyNewSubscribers, randomId: randomId, untilDate: untilDate)
}
}
}

View File

@ -1452,8 +1452,9 @@ final class ShareWithPeersScreenComponent: Component {
}
}
let index = self.selectedPeers.firstIndex(of: peer.id)
let togglePeer = {
if let index = self.selectedPeers.firstIndex(of: peer.id) {
if let index {
self.selectedPeers.remove(at: index)
self.updateSelectedGroupPeers()
} else {
@ -1462,11 +1463,12 @@ final class ShareWithPeersScreenComponent: Component {
update()
}
if peer.id.isGroupOrChannel {
if case .channels = component.stateContext.subject, self.selectedPeers.count >= 10 {
if case .channels = component.stateContext.subject, self.selectedPeers.count >= component.context.userLimits.maxGiveawayChannelsCount, index == nil {
self.hapticFeedback.error()
return
}
if case .channels = component.stateContext.subject {
if case let .channel(channel) = peer, channel.addressName == nil, !self.selectedPeers.contains(peer.id) {
if case let .channel(channel) = peer, channel.addressName == nil, index == nil {
let alertController = textAlertController(
context: component.context,
forceTheme: environment.theme,
@ -1488,7 +1490,8 @@ final class ShareWithPeersScreenComponent: Component {
update()
}
} else {
if case .members = component.stateContext.subject, self.selectedPeers.count >= 10 {
if case .members = component.stateContext.subject, self.selectedPeers.count >= 10, index == nil {
self.hapticFeedback.error()
return
}
togglePeer()
@ -2359,7 +2362,7 @@ final class ShareWithPeersScreenComponent: Component {
case .channels:
title = "Add Channels"
actionButtonTitle = "Save Channels"
subtitle = "select up to 10 channels"
subtitle = "select up to \(component.context.userLimits.maxGiveawayChannelsCount) channels"
}
let titleComponent: AnyComponent<Empty>
@ -2982,6 +2985,10 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
}
deinit {
if !self.isDismissed {
self.isDismissed = true
self.dismissed()
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {

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
}