Merge commit '943d22c54e0d4c97a0d842d5804a7cc2e58f952c'

This commit is contained in:
Ali 2023-09-29 16:13:46 +04:00
commit c55bbe6be3
91 changed files with 5974 additions and 1251 deletions

View File

@ -10020,6 +10020,9 @@ Sorry for the inconvenience.";
"Stats.Boosts.BoostersNone" = "BOOSTERS"; "Stats.Boosts.BoostersNone" = "BOOSTERS";
"Stats.Boosts.Boosters_1" = "%@ BOOSTER"; "Stats.Boosts.Boosters_1" = "%@ BOOSTER";
"Stats.Boosts.Boosters_any" = "%@ BOOSTERS"; "Stats.Boosts.Boosters_any" = "%@ BOOSTERS";
"Stats.Boosts.BoostsNone" = "BOOSTS";
"Stats.Boosts.Boosts_1" = "%@ BOOST";
"Stats.Boosts.Boosts_any" = "%@ BOOSTS";
"Stats.Boosts.NoBoostersYet" = "No users currently boost your channel"; "Stats.Boosts.NoBoostersYet" = "No users currently boost your channel";
"Stats.Boosts.BoostersInfo" = "Your channel is currently boosted by these users."; "Stats.Boosts.BoostersInfo" = "Your channel is currently boosted by these users.";
"Stats.Boosts.ExpiresOn" = "Boost expires on %@"; "Stats.Boosts.ExpiresOn" = "Boost expires on %@";
@ -10086,3 +10089,9 @@ Sorry for the inconvenience.";
"ChannelBoost.BoostLinkForwardTooltip.TwoChats.One" = "Boost link forwarded to **%@** and **%@**"; "ChannelBoost.BoostLinkForwardTooltip.TwoChats.One" = "Boost link forwarded to **%@** and **%@**";
"ChannelBoost.BoostLinkForwardTooltip.ManyChats.One" = "Boost link forwarded to **%@** and %@ others"; "ChannelBoost.BoostLinkForwardTooltip.ManyChats.One" = "Boost link forwarded to **%@** and %@ others";
"ChannelBoost.BoostLinkForwardTooltip.SavedMessages.One" = "Boost link forwarded to **Saved Messages**"; "ChannelBoost.BoostLinkForwardTooltip.SavedMessages.One" = "Boost link forwarded to **Saved Messages**";
"ChannelBoost.YouBoostedChannelText" = "You boosted %1$@!";
"ChannelBoost.YouBoostedOtherChannelText" = "You boosted this channel";
"PremiumGift.LabelRecipients_1" = "1 recipient";
"PremiumGift.LabelRecipients_any" = "%d recipients";

View File

@ -301,6 +301,7 @@ public enum ResolvedUrl {
case chatFolder(slug: String) case chatFolder(slug: String)
case story(peerId: PeerId, id: Int32) case story(peerId: PeerId, id: Int32)
case boost(peerId: PeerId, status: ChannelBoostStatus?, canApplyStatus: CanApplyBoostStatus) case boost(peerId: PeerId, status: ChannelBoostStatus?, canApplyStatus: CanApplyBoostStatus)
case premiumGiftCode(slug: String)
} }
public enum NavigateToChatKeepStack { public enum NavigateToChatKeepStack {

View File

@ -59,48 +59,6 @@ public final class PeerSelectionControllerParams {
public let selectForumThreads: Bool public let selectForumThreads: Bool
public let hasCreation: Bool public let hasCreation: Bool
/*public convenience init(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
filter: ChatListNodePeersFilter = [.onlyWriteable],
requestPeerType: [ReplyMarkupButtonRequestPeerType]? = nil,
forumPeerId: EnginePeer.Id? = nil,
hasFilters: Bool = false,
hasChatListSelector: Bool = true,
hasContactSelector: Bool = true,
hasGlobalSearch: Bool = true,
title: String? = nil,
attemptSelection: ((EnginePeer, Int64?) -> Void)? = nil,
createNewGroup: (() -> Void)? = nil,
pretendPresentedInModal: Bool = false,
multipleSelection: Bool = false,
forwardedMessageIds: [EngineMessage.Id] = [],
hasTypeHeaders: Bool = false,
selectForumThreads: Bool = false,
hasCreation: Bool = false
) {
self.init(
context: .account(context),
updatedPresentationData: updatedPresentationData,
filter: filter,
requestPeerType: requestPeerType,
forumPeerId: forumPeerId,
hasFilters: hasFilters,
hasChatListSelector: hasChatListSelector,
hasContactSelector: hasContactSelector,
hasGlobalSearch: hasGlobalSearch,
title: title,
attemptSelection: attemptSelection,
createNewGroup: createNewGroup,
pretendPresentedInModal: pretendPresentedInModal,
multipleSelection: multipleSelection,
forwardedMessageIds: forwardedMessageIds,
hasTypeHeaders: hasTypeHeaders,
selectForumThreads: selectForumThreads,
hasCreation: hasCreation
)
}*/
public init( public init(
context: AccountContext, context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,

View File

@ -5,7 +5,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "Crc32", name: "Crc32",
platforms: [.macOS(.v10_12)], platforms: [.macOS(.v10_13)],
products: [ products: [
// Products define the executables and libraries a package produces, and make them visible to other packages. // Products define the executables and libraries a package produces, and make them visible to other packages.
.library( .library(

View File

@ -5,7 +5,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "CryptoUtils", name: "CryptoUtils",
platforms: [.macOS(.v10_12)], platforms: [.macOS(.v10_13)],
products: [ products: [
// Products define the executables and libraries a package produces, and make them visible to other packages. // Products define the executables and libraries a package produces, and make them visible to other packages.
.library( .library(

View File

@ -17,8 +17,9 @@ public final class DatePickerTheme: Equatable {
public let selectionTextColor: UIColor public let selectionTextColor: UIColor
public let separatorColor: UIColor public let separatorColor: UIColor
public let segmentedControlTheme: SegmentedControlTheme public let segmentedControlTheme: SegmentedControlTheme
public let overallDarkAppearance: Bool
public init(backgroundColor: UIColor, textColor: UIColor, secondaryTextColor: UIColor, accentColor: UIColor, disabledColor: UIColor, selectionColor: UIColor, selectionTextColor: UIColor, separatorColor: UIColor, segmentedControlTheme: SegmentedControlTheme) { public init(backgroundColor: UIColor, textColor: UIColor, secondaryTextColor: UIColor, accentColor: UIColor, disabledColor: UIColor, selectionColor: UIColor, selectionTextColor: UIColor, separatorColor: UIColor, segmentedControlTheme: SegmentedControlTheme, overallDarkAppearance: Bool) {
self.backgroundColor = backgroundColor self.backgroundColor = backgroundColor
self.textColor = textColor self.textColor = textColor
self.secondaryTextColor = secondaryTextColor self.secondaryTextColor = secondaryTextColor
@ -28,6 +29,7 @@ public final class DatePickerTheme: Equatable {
self.selectionTextColor = selectionTextColor self.selectionTextColor = selectionTextColor
self.separatorColor = separatorColor self.separatorColor = separatorColor
self.segmentedControlTheme = segmentedControlTheme self.segmentedControlTheme = segmentedControlTheme
self.overallDarkAppearance = overallDarkAppearance
} }
public static func ==(lhs: DatePickerTheme, rhs: DatePickerTheme) -> Bool { public static func ==(lhs: DatePickerTheme, rhs: DatePickerTheme) -> Bool {
@ -52,13 +54,16 @@ public final class DatePickerTheme: Equatable {
if lhs.separatorColor != rhs.separatorColor { if lhs.separatorColor != rhs.separatorColor {
return false return false
} }
if lhs.overallDarkAppearance != rhs.overallDarkAppearance {
return false
}
return true return true
} }
} }
public extension DatePickerTheme { public extension DatePickerTheme {
convenience init(theme: PresentationTheme) { convenience init(theme: PresentationTheme) {
self.init(backgroundColor: theme.list.itemBlocksBackgroundColor, textColor: theme.list.itemPrimaryTextColor, secondaryTextColor: theme.list.itemSecondaryTextColor, accentColor: theme.list.itemAccentColor, disabledColor: theme.list.itemDisabledTextColor, selectionColor: theme.list.itemCheckColors.fillColor, selectionTextColor: theme.list.itemCheckColors.foregroundColor, separatorColor: theme.list.itemBlocksSeparatorColor, segmentedControlTheme: SegmentedControlTheme(theme: theme)) self.init(backgroundColor: theme.list.itemBlocksBackgroundColor, textColor: theme.list.itemPrimaryTextColor, secondaryTextColor: theme.list.itemSecondaryTextColor, accentColor: theme.list.itemAccentColor, disabledColor: theme.list.itemDisabledTextColor, selectionColor: theme.list.itemCheckColors.fillColor, selectionTextColor: theme.list.itemCheckColors.foregroundColor, separatorColor: theme.list.itemBlocksSeparatorColor, segmentedControlTheme: SegmentedControlTheme(theme: theme), overallDarkAppearance: theme.overallDarkAppearance)
} }
} }
@ -948,6 +953,7 @@ private class TimeInputView: UIView, UIKeyInput {
} }
var keyboardType: UIKeyboardType = .numberPad var keyboardType: UIKeyboardType = .numberPad
var keyboardAppearance: UIKeyboardAppearance = .default
var text: String = "" var text: String = ""
var hasText: Bool { var hasText: Bool {
@ -1284,7 +1290,7 @@ private final class TimePickerNode: ASDisplayNode {
self.update() self.update()
} }
private func updateTime() { private func updateTime() {
switch self.dateTimeFormat.timeFormat { switch self.dateTimeFormat.timeFormat {
case .military: case .military:
@ -1338,6 +1344,8 @@ private final class TimePickerNode: ASDisplayNode {
self.view.disablesInteractiveModalDismiss = true self.view.disablesInteractiveModalDismiss = true
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))) self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:))))
(self.inputNode.view as? TimeInputView)?.keyboardAppearance = self.theme.overallDarkAppearance ? .dark : .default
} }
private func handleTextInput(_ input: String) { private func handleTextInput(_ input: String) {

View File

@ -5,7 +5,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "Emoji", name: "Emoji",
platforms: [.macOS(.v10_12)], platforms: [.macOS(.v10_13)],
products: [ products: [
// Products define the executables and libraries a package produces, and make them visible to other packages. // Products define the executables and libraries a package produces, and make them visible to other packages.
.library( .library(

View File

@ -5,7 +5,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "EncryptionProvider", name: "EncryptionProvider",
platforms: [.macOS(.v10_12)], platforms: [.macOS(.v10_13)],
products: [ products: [
// Products define the executables and libraries a package produces, and make them visible to other packages. // Products define the executables and libraries a package produces, and make them visible to other packages.
.library( .library(

View File

@ -5,7 +5,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "FFMpegBinding", name: "FFMpegBinding",
platforms: [.macOS(.v10_12)], platforms: [.macOS(.v10_13)],
products: [ products: [
// Products define the executables and libraries a package produces, and make them visible to other packages. // Products define the executables and libraries a package produces, and make them visible to other packages.
.library( .library(

View File

@ -1055,9 +1055,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.fullscreenButton.isHidden = true self.fullscreenButton.isHidden = true
} }
var textFrame = CGRect()
var visibleTextHeight: CGFloat = 0.0
if !self.textNode.isHidden { if !self.textNode.isHidden {
var textFrame = CGRect()
var visibleTextHeight: CGFloat = 0.0
let sideInset: CGFloat = 8.0 + leftInset let sideInset: CGFloat = 8.0 + leftInset
let topInset: CGFloat = 8.0 let topInset: CGFloat = 8.0
let textBottomInset: CGFloat = 8.0 let textBottomInset: CGFloat = 8.0
@ -1085,7 +1086,11 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.scrollNode.frame = scrollNodeFrame self.scrollNode.frame = scrollNodeFrame
} }
textOffset = min(400.0, self.scrollNode.view.contentOffset.y) var maxTextOffset: CGFloat = size.height - bottomInset - 238.0 - UIScreenPixel
if let _ = self.scrubberView {
maxTextOffset -= 44.0
}
textOffset = min(maxTextOffset, self.scrollNode.view.contentOffset.y)
panelHeight = max(0.0, panelHeight + visibleTextPanelHeight + textOffset) panelHeight = max(0.0, panelHeight + visibleTextPanelHeight + textOffset)
if self.scrollNode.view.isScrollEnabled { if self.scrollNode.view.isScrollEnabled {

View File

@ -5,7 +5,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "GraphCore", name: "GraphCore",
platforms: [.macOS(.v10_12)], platforms: [.macOS(.v10_13)],
products: [ products: [
// Products define the executables and libraries a package produces, and make them visible to other packages. // Products define the executables and libraries a package produces, and make them visible to other packages.
.library( .library(

View File

@ -3,6 +3,7 @@ import CoreLocation
import SwiftSignalKit import SwiftSignalKit
import StoreKit import StoreKit
import TelegramCore import TelegramCore
import Postbox
import TelegramStringFormatting import TelegramStringFormatting
import TelegramUIPreferences import TelegramUIPreferences
import PersistentStringHash import PersistentStringHash
@ -13,12 +14,12 @@ private let productIdentifiers = [
"org.telegram.telegramPremium.monthly", "org.telegram.telegramPremium.monthly",
"org.telegram.telegramPremium.twelveMonths", "org.telegram.telegramPremium.twelveMonths",
"org.telegram.telegramPremium.sixMonths", "org.telegram.telegramPremium.sixMonths",
"org.telegram.telegramPremium.threeMonths" "org.telegram.telegramPremium.threeMonths",
]
private func isSubscriptionProductId(_ id: String) -> Bool { "org.telegram.telegramPremium.threeMonths.code_x1",
return id.hasSuffix(".monthly") || id.hasSuffix(".annual") || id.hasSuffix(".semiannual") "org.telegram.telegramPremium.sixMonths.code_x1",
} "org.telegram.telegramPremium.twelveMonths.code_x1"
]
private extension NSDecimalNumber { private extension NSDecimalNumber {
func round(_ decimals: Int) -> NSDecimalNumber { func round(_ decimals: Int) -> NSDecimalNumber {
@ -103,6 +104,25 @@ public final class InAppPurchaseManager: NSObject {
return self.numberFormatter.string(from: prettierPrice) ?? "" return self.numberFormatter.string(from: prettierPrice) ?? ""
} }
public func multipliedPrice(count: Int) -> String {
let price = self.skProduct.price.multiplying(by: NSDecimalNumber(value: count)).round(2)
let prettierPrice = price
.multiplying(by: NSDecimalNumber(value: 2))
.rounding(accordingToBehavior:
NSDecimalNumberHandler(
roundingMode: .up,
scale: Int16(0),
raiseOnExactness: false,
raiseOnOverflow: false,
raiseOnUnderflow: false,
raiseOnDivideByZero: false
)
)
.dividing(by: NSDecimalNumber(value: 2))
.subtracting(NSDecimalNumber(value: 0.01))
return self.numberFormatter.string(from: prettierPrice) ?? ""
}
public var priceValue: NSDecimalNumber { public var priceValue: NSDecimalNumber {
return self.skProduct.price return self.skProduct.price
} }
@ -151,13 +171,11 @@ public final class InAppPurchaseManager: NSObject {
private final class PaymentTransactionContext { private final class PaymentTransactionContext {
var state: SKPaymentTransactionState? var state: SKPaymentTransactionState?
var isUpgrade: Bool let purpose: PendingInAppPurchaseState.Purpose
var targetPeerId: EnginePeer.Id?
let subscriber: (TransactionState) -> Void let subscriber: (TransactionState) -> Void
init(isUpgrade: Bool, targetPeerId: EnginePeer.Id?, subscriber: @escaping (TransactionState) -> Void) { init(purpose: PendingInAppPurchaseState.Purpose, subscriber: @escaping (TransactionState) -> Void) {
self.isUpgrade = isUpgrade self.purpose = purpose
self.targetPeerId = targetPeerId
self.subscriber = subscriber self.subscriber = subscriber
} }
} }
@ -235,21 +253,20 @@ public final class InAppPurchaseManager: NSObject {
} }
} }
public func buyProduct(_ product: Product, isUpgrade: Bool = false, targetPeerId: EnginePeer.Id? = nil) -> Signal<PurchaseState, PurchaseError> { public func buyProduct(_ product: Product, purpose: AppStoreTransactionPurpose) -> Signal<PurchaseState, PurchaseError> {
if !self.canMakePayments { if !self.canMakePayments {
return .fail(.cantMakePayments) return .fail(.cantMakePayments)
} }
if !product.isSubscription && targetPeerId == nil {
return .fail(.cantMakePayments)
}
let accountPeerId = "\(self.engine.account.peerId.toInt64())" let accountPeerId = "\(self.engine.account.peerId.toInt64())"
Logger.shared.log("InAppPurchaseManager", "Buying: account \(accountPeerId), product \(product.skProduct.productIdentifier), price \(product.price)") Logger.shared.log("InAppPurchaseManager", "Buying: account \(accountPeerId), product \(product.skProduct.productIdentifier), price \(product.price)")
let purpose = PendingInAppPurchaseState.Purpose(appStorePurpose: purpose)
let payment = SKMutablePayment(product: product.skProduct) let payment = SKMutablePayment(product: product.skProduct)
payment.applicationUsername = accountPeerId payment.applicationUsername = accountPeerId
payment.quantity = purpose.quantity
SKPaymentQueue.default().add(payment) SKPaymentQueue.default().add(payment)
let productIdentifier = payment.productIdentifier let productIdentifier = payment.productIdentifier
@ -257,7 +274,7 @@ public final class InAppPurchaseManager: NSObject {
let disposable = MetaDisposable() let disposable = MetaDisposable()
self.stateQueue.async { self.stateQueue.async {
let paymentContext = PaymentTransactionContext(isUpgrade: isUpgrade, targetPeerId: targetPeerId, subscriber: { state in let paymentContext = PaymentTransactionContext(purpose: purpose, subscriber: { state in
switch state { switch state {
case let .purchased(transactionId), let .restored(transactionId): case let .purchased(transactionId), let .restored(transactionId):
if let transactionId = transactionId { if let transactionId = transactionId {
@ -381,8 +398,7 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver {
productId: transaction.payment.productIdentifier, productId: transaction.payment.productIdentifier,
content: PendingInAppPurchaseState( content: PendingInAppPurchaseState(
productId: transaction.payment.productIdentifier, productId: transaction.payment.productIdentifier,
isUpgrade: paymentContext.isUpgrade, purpose: paymentContext.purpose
targetPeerId: paymentContext.targetPeerId
) )
).start() ).start()
} }
@ -410,64 +426,61 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver {
var completion: Signal<Never, NoError> = .never() var completion: Signal<Never, NoError> = .never()
let purpose: Signal<AppStoreTransactionPurpose, NoError> let products = self.availableProducts
if !isSubscriptionProductId(productIdentifier) { |> filter { products in
let peerId: Signal<EnginePeer.Id, NoError> return !products.isEmpty
if let targetPeerId = paymentContexts[productIdentifier]?.targetPeerId { }
peerId = .single(targetPeerId) |> take(1)
let product: Signal<InAppPurchaseManager.Product?, NoError> = products
|> map { products in
if let product = products.first(where: { $0.id == productIdentifier }) {
return product
} else { } else {
peerId = pendingInAppPurchaseState(engine: self.engine, productId: productIdentifier) return nil
|> mapToSignal { state -> Signal<EnginePeer.Id, NoError> in
if let state = state, let peerId = state.targetPeerId {
return .single(peerId)
} else {
return .complete()
}
}
}
completion = updatePendingInAppPurchaseState(engine: self.engine, productId: productIdentifier, content: nil)
let products = self.availableProducts
|> filter { products in
return !products.isEmpty
}
|> take(1)
purpose = combineLatest(products, peerId)
|> map { products, peerId -> AppStoreTransactionPurpose in
if let product = products.first(where: { $0.id == productIdentifier }) {
let (currency, amount) = product.priceCurrencyAndAmount
return .gift(peerId: peerId, currency: currency, amount: amount)
} else {
return .gift(peerId: peerId, currency: "", amount: 0)
}
}
} else {
let isUpgrade: Signal<Bool, NoError>
if let isUpgradeValue = paymentContexts[productIdentifier]?.isUpgrade {
isUpgrade = .single(isUpgradeValue)
} else {
isUpgrade = pendingInAppPurchaseState(engine: self.engine, productId: productIdentifier)
|> mapToSignal { state -> Signal<Bool, NoError> in
if let state = state {
return .single(state.isUpgrade)
} else {
return .single(false)
}
}
}
purpose = isUpgrade
|> map { isUpgrade in
return isUpgrade ? .upgrade : .subscription
} }
} }
let purpose: Signal<AppStoreTransactionPurpose, NoError>
if let paymentContext = paymentContexts[productIdentifier] {
purpose = product
|> map { product in
return paymentContext.purpose.appStorePurpose(product: product)
}
} else {
purpose = combineLatest(
product,
pendingInAppPurchaseState(engine: self.engine, productId: productIdentifier)
)
|> mapToSignal { product, state -> Signal<AppStoreTransactionPurpose, NoError> in
if let state {
return .single(state.purpose.appStorePurpose(product: product))
} else {
return .complete()
}
}
}
completion = updatePendingInAppPurchaseState(engine: self.engine, productId: productIdentifier, content: nil)
let receiptData = getReceiptData() ?? Data() let receiptData = getReceiptData() ?? Data()
#if DEBUG
let id = Int64.random(in: Int64.min ... Int64.max)
let fileResource = LocalFileMediaResource(fileId: id, size: Int64(receiptData.count), isSecretRelated: false)
self.engine.account.postbox.mediaBox.storeResourceData(fileResource.id, data: receiptData)
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(receiptData.count), attributes: [.FileName(fileName: "Receipt.dat")])
let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])
let _ = enqueueMessages(account: self.engine.account, peerId: self.engine.account.peerId, messages: [message]).start()
#endif
self.disposableSet.set( self.disposableSet.set(
(purpose (purpose
|> castError(AssignAppStoreTransactionError.self) |> castError(AssignAppStoreTransactionError.self)
|> mapToSignal { purpose -> Signal<Never, AssignAppStoreTransactionError> in |> mapToSignal { purpose -> Signal<Never, AssignAppStoreTransactionError> in
self.engine.payments.sendAppStoreReceipt(receipt: receiptData, purpose: purpose) return self.engine.payments.sendAppStoreReceipt(receipt: receiptData, purpose: purpose)
}).start(error: { [weak self] _ in }).start(error: { [weak self] _ in
Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transactions [\(transactionIds)] failed to assign") Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transactions [\(transactionIds)] failed to assign")
for transaction in transactions { for transaction in transactions {
@ -535,32 +548,165 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver {
} }
private final class PendingInAppPurchaseState: Codable { private final class PendingInAppPurchaseState: Codable {
public let productId: String enum CodingKeys: String, CodingKey {
public let isUpgrade: Bool case productId
public let targetPeerId: EnginePeer.Id? case purpose
case storeProductId
}
enum Purpose: Codable {
enum DecodingError: Error {
case generic
}
public init(productId: String, isUpgrade: Bool, targetPeerId: EnginePeer.Id?) { enum CodingKeys: String, CodingKey {
case type
case peer
case peers
case boostPeer
case randomId
case untilDate
}
enum PurposeType: Int32 {
case subscription
case upgrade
case restore
case gift
case giftCode
case giveaway
}
case subscription
case upgrade
case restore
case gift(peerId: EnginePeer.Id)
case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?)
case giveaway(boostPeer: EnginePeer.Id, randomId: Int64, untilDate: Int32)
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = PurposeType(rawValue: try container.decode(Int32.self, forKey: .type))
switch type {
case .subscription:
self = .subscription
case .upgrade:
self = .upgrade
case .restore:
self = .restore
case .gift:
self = .gift(
peerId: EnginePeer.Id(try container.decode(Int64.self, forKey: .peer))
)
case .giftCode:
self = .giftCode(
peerIds: try container.decode([Int64].self, forKey: .peers).map { EnginePeer.Id($0) },
boostPeer: try container.decodeIfPresent(Int64.self, forKey: .boostPeer).flatMap({ EnginePeer.Id($0) })
)
case .giveaway:
self = .giveaway(
boostPeer: EnginePeer.Id(try container.decode(Int64.self, forKey: .boostPeer)),
randomId: try container.decode(Int64.self, forKey: .randomId),
untilDate: try container.decode(Int32.self, forKey: .untilDate)
)
default:
throw DecodingError.generic
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .subscription:
try container.encode(PurposeType.subscription.rawValue, forKey: .type)
case .upgrade:
try container.encode(PurposeType.upgrade.rawValue, forKey: .type)
case .restore:
try container.encode(PurposeType.restore.rawValue, forKey: .type)
case let .gift(peerId):
try container.encode(PurposeType.gift.rawValue, forKey: .type)
try container.encode(peerId.toInt64(), forKey: .peer)
case let .giftCode(peerIds, boostPeer):
try container.encode(PurposeType.giftCode.rawValue, forKey: .type)
try container.encode(peerIds.map { $0.toInt64() }, forKey: .peers)
try container.encodeIfPresent(boostPeer?.toInt64(), forKey: .boostPeer)
case let .giveaway(boostPeer, randomId, untilDate):
try container.encode(PurposeType.giveaway.rawValue, forKey: .type)
try container.encode(boostPeer.toInt64(), forKey: .boostPeer)
try container.encode(randomId, forKey: .randomId)
try container.encode(untilDate, forKey: .untilDate)
}
}
init(appStorePurpose: AppStoreTransactionPurpose) {
switch appStorePurpose {
case .subscription:
self = .subscription
case .upgrade:
self = .upgrade
case .restore:
self = .restore
case let .gift(peerId, _, _):
self = .gift(peerId: peerId)
case let .giftCode(peerIds, boostPeer, _, _):
self = .giftCode(peerIds: peerIds, boostPeer: boostPeer)
case let .giveaway(boostPeer, randomId, untilDate, _, _):
self = .giveaway(boostPeer: boostPeer, randomId: randomId, untilDate: untilDate)
}
}
func appStorePurpose(product: InAppPurchaseManager.Product?) -> AppStoreTransactionPurpose {
let (currency, amount) = product?.priceCurrencyAndAmount ?? ("", 0)
switch self {
case .subscription:
return .subscription
case .upgrade:
return .upgrade
case .restore:
return .restore
case let .gift(peerId):
return .gift(peerId: peerId, currency: currency, amount: amount)
case let .giftCode(peerIds, boostPeer):
return .giftCode(peerIds: peerIds, boostPeer: boostPeer, currency: currency, amount: amount)
case let .giveaway(boostPeer, randomId, untilDate):
return .giveaway(boostPeer: boostPeer, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount)
}
}
var quantity: Int {
switch self {
case .subscription, .upgrade, .restore, .gift:
return 1
case let .giftCode(peerIds, _):
return peerIds.count
case .giveaway:
return 1
}
}
}
public let productId: String
public let purpose: Purpose
public init(productId: String, purpose: Purpose) {
self.productId = productId self.productId = productId
self.isUpgrade = isUpgrade self.purpose = purpose
self.targetPeerId = targetPeerId
} }
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self) let container = try decoder.container(keyedBy: CodingKeys.self)
self.productId = try container.decode(String.self, forKey: "productId") self.productId = try container.decode(String.self, forKey: .productId)
self.isUpgrade = try container.decodeIfPresent(Bool.self, forKey: "isUpgrade") ?? false self.purpose = try container.decode(Purpose.self, forKey: .purpose)
self.targetPeerId = (try container.decodeIfPresent(Int64.self, forKey: "targetPeerId")).flatMap { EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value($0)) }
} }
public func encode(to encoder: Encoder) throws { public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self) var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.productId, forKey: "productId") try container.encode(self.productId, forKey: .productId)
try container.encode(self.isUpgrade, forKey: "isUpgrade") try container.encode(self.purpose, forKey: .purpose)
if let targetPeerId = self.targetPeerId {
try container.encode(targetPeerId.id._internalGetInt64Value(), forKey: "targetPeerId")
}
} }
} }

View File

@ -58,6 +58,7 @@ swift_library(
"//submodules/PeerInfoAvatarListNode:PeerInfoAvatarListNode", "//submodules/PeerInfoAvatarListNode:PeerInfoAvatarListNode",
"//submodules/QrCodeUI:QrCodeUI", "//submodules/QrCodeUI:QrCodeUI",
"//submodules/PromptUI", "//submodules/PromptUI",
"//submodules/TelegramUI/Components/ItemListDatePickerItem:ItemListDatePickerItem",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -16,6 +16,7 @@ import AppBundle
import ContextUI import ContextUI
import TelegramStringFormatting import TelegramStringFormatting
import UndoUI import UndoUI
import ItemListDatePickerItem
private final class InviteLinkEditControllerArguments { private final class InviteLinkEditControllerArguments {
let context: AccountContext let context: AccountContext

View File

@ -5,7 +5,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "ManagedFile", name: "ManagedFile",
platforms: [.macOS(.v10_12)], platforms: [.macOS(.v10_13)],
products: [ products: [
// Products define the executables and libraries a package produces, and make them visible to other packages. // Products define the executables and libraries a package produces, and make them visible to other packages.
.library( .library(

View File

@ -5,7 +5,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "MtProtoKit", name: "MtProtoKit",
platforms: [.macOS(.v10_12)], platforms: [.macOS(.v10_13)],
products: [ products: [
// Products define the executables and libraries a package produces, and make them visible to other packages. // Products define the executables and libraries a package produces, and make them visible to other packages.
.library( .library(

View File

@ -5,7 +5,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "MurMurHash32", name: "MurMurHash32",
platforms: [.macOS(.v10_12)], platforms: [.macOS(.v10_13)],
products: [ products: [
// Products define the executables and libraries a package produces, and make them visible to other packages. // Products define the executables and libraries a package produces, and make them visible to other packages.
.library( .library(

View File

@ -5,7 +5,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "NetworkLogging", name: "NetworkLogging",
platforms: [.macOS(.v10_12)], platforms: [.macOS(.v10_13)],
products: [ products: [
// Products define the executables and libraries a package produces, and make them visible to other packages. // Products define the executables and libraries a package produces, and make them visible to other packages.
.library( .library(

View File

@ -7,7 +7,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "OpenSSLEncryption", name: "OpenSSLEncryption",
platforms: [ platforms: [
.macOS(.v10_12) .macOS(.v10_13)
], ],
products: [ products: [
.library( .library(

View File

@ -5,7 +5,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "OpusBinding", name: "OpusBinding",
platforms: [.macOS(.v10_12)], platforms: [.macOS(.v10_13)],
products: [ products: [
// Products define the executables and libraries a package produces, and make them visible to other packages. // Products define the executables and libraries a package produces, and make them visible to other packages.
.library( .library(

View File

@ -5,7 +5,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "Postbox", name: "Postbox",
platforms: [.macOS(.v10_12)], platforms: [.macOS(.v10_13)],
products: [ products: [
// Products define the executables and libraries a package produces, and make them visible to other packages. // Products define the executables and libraries a package produces, and make them visible to other packages.
.library( .library(

View File

@ -100,7 +100,11 @@ swift_library(
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent", "//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent",
"//submodules/AttachmentUI:AttachmentUI", "//submodules/AttachmentUI:AttachmentUI",
"//submodules/Components/BalancedTextComponent" "//submodules/Components/BalancedTextComponent",
"//submodules/ItemListPeerItem:ItemListPeerItem",
"//submodules/ItemListPeerActionItem:ItemListPeerActionItem",
"//submodules/TelegramUI/Components/ItemListDatePickerItem:ItemListDatePickerItem",
"//submodules/TelegramUI/Components/ShareWithPeersScreen",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -0,0 +1,784 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import AlertUI
import PresentationDataUtils
import AppBundle
import TelegramStringFormatting
import ItemListPeerItem
import ItemListDatePickerItem
import ItemListPeerActionItem
import ShareWithPeersScreen
import InAppPurchaseManager
private final class CreateGiveawayControllerArguments {
let context: AccountContext
let updateState: ((CreateGiveawayControllerState) -> CreateGiveawayControllerState) -> Void
let dismissInput: () -> Void
let openPeersSelection: () -> Void
let openChannelsSelection: () -> Void
init(context: AccountContext, updateState: @escaping ((CreateGiveawayControllerState) -> CreateGiveawayControllerState) -> Void, dismissInput: @escaping () -> Void, openPeersSelection: @escaping () -> Void, openChannelsSelection: @escaping () -> Void) {
self.context = context
self.updateState = updateState
self.dismissInput = dismissInput
self.openPeersSelection = openPeersSelection
self.openChannelsSelection = openChannelsSelection
}
}
private enum CreateGiveawaySection: Int32 {
case header
case mode
case subscriptions
case channels
case users
case time
case duration
}
private enum CreateGiveawayEntryTag: ItemListItemTag {
case usage
func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? CreateGiveawayEntryTag, self == other {
return true
} else {
return false
}
}
}
private enum CreateGiveawayEntry: ItemListNodeEntry {
case header(PresentationTheme, String, String)
case createGiveaway(PresentationTheme, String, String, Bool)
case awardUsers(PresentationTheme, String, String, Bool)
case subscriptionsHeader(PresentationTheme, String)
case subscriptions(PresentationTheme, Int32)
case subscriptionsInfo(PresentationTheme, String)
case channelsHeader(PresentationTheme, String)
case channel(Int32, PresentationTheme, EnginePeer, Int32)
case channelAdd(PresentationTheme, String)
case channelsInfo(PresentationTheme, String)
case usersHeader(PresentationTheme, String)
case usersAll(PresentationTheme, String, Bool)
case usersNew(PresentationTheme, String, Bool)
case usersInfo(PresentationTheme, String)
case timeHeader(PresentationTheme, String)
case timeExpiryDate(PresentationTheme, PresentationDateTimeFormat, Int32?, Bool)
case timeCustomPicker(PresentationTheme, PresentationDateTimeFormat, Int32?)
case timeInfo(PresentationTheme, String)
case durationHeader(PresentationTheme, String)
case duration(Int32, PresentationTheme, String, String, String, String, String?, Bool)
case durationInfo(PresentationTheme, String)
var section: ItemListSectionId {
switch self {
case .header:
return CreateGiveawaySection.header.rawValue
case .createGiveaway, .awardUsers:
return CreateGiveawaySection.mode.rawValue
case .subscriptionsHeader, .subscriptions, .subscriptionsInfo:
return CreateGiveawaySection.subscriptions.rawValue
case .channelsHeader, .channel, .channelAdd, .channelsInfo:
return CreateGiveawaySection.channels.rawValue
case .usersHeader, .usersAll, .usersNew, .usersInfo:
return CreateGiveawaySection.users.rawValue
case .timeHeader, .timeExpiryDate, .timeCustomPicker, .timeInfo:
return CreateGiveawaySection.time.rawValue
case .durationHeader, .duration, .durationInfo:
return CreateGiveawaySection.duration.rawValue
}
}
var stableId: Int32 {
switch self {
case .header:
return -1
case .createGiveaway:
return 0
case .awardUsers:
return 1
case .subscriptionsHeader:
return 2
case .subscriptions:
return 3
case .subscriptionsInfo:
return 4
case .channelsHeader:
return 5
case let .channel(index, _, _, _):
return 6 + index
case .channelAdd:
return 100
case .channelsInfo:
return 101
case .usersHeader:
return 102
case .usersAll:
return 103
case .usersNew:
return 104
case .usersInfo:
return 105
case .timeHeader:
return 106
case .timeExpiryDate:
return 107
case .timeCustomPicker:
return 108
case .timeInfo:
return 109
case .durationHeader:
return 110
case let .duration(index, _, _, _, _, _, _, _):
return 111 + index
case .durationInfo:
return 120
}
}
static func ==(lhs: CreateGiveawayEntry, rhs: CreateGiveawayEntry) -> Bool {
switch lhs {
case let .header(lhsTheme, lhsTitle, lhsText):
if case let .header(rhsTheme, rhsTitle, rhsText) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsText == rhsText {
return true
} else {
return false
}
case let .createGiveaway(lhsTheme, lhsText, lhsSubtext, lhsSelected):
if case let .createGiveaway(rhsTheme, rhsText, rhsSubtext, rhsSelected) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsSubtext == rhsSubtext, lhsSelected == rhsSelected {
return true
} else {
return false
}
case let .awardUsers(lhsTheme, lhsText, lhsSubtext, lhsSelected):
if case let .awardUsers(rhsTheme, rhsText, rhsSubtext, rhsSelected) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsSubtext == rhsSubtext, lhsSelected == rhsSelected {
return true
} else {
return false
}
case let .subscriptionsHeader(lhsTheme, lhsText):
if case let .subscriptionsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .subscriptions(lhsTheme, lhsValue):
if case let .subscriptions(rhsTheme, rhsValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue {
return true
} else {
return false
}
case let .subscriptionsInfo(lhsTheme, lhsText):
if case let .subscriptionsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .channelsHeader(lhsTheme, lhsText):
if case let .channelsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .channel(lhsIndex, lhsTheme, lhsPeer, lhsBoosts):
if case let .channel(rhsIndex, rhsTheme, rhsPeer, rhsBoosts) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsPeer == rhsPeer, lhsBoosts == rhsBoosts {
return true
} else {
return false
}
case let .channelAdd(lhsTheme, lhsText):
if case let .channelAdd(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .channelsInfo(lhsTheme, lhsText):
if case let .channelsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .usersHeader(lhsTheme, lhsText):
if case let .usersHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .usersAll(lhsTheme, lhsText, lhsSelected):
if case let .usersAll(rhsTheme, rhsText, rhsSelected) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsSelected == rhsSelected {
return true
} else {
return false
}
case let .usersNew(lhsTheme, lhsText, lhsSelected):
if case let .usersNew(rhsTheme, rhsText, rhsSelected) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsSelected == rhsSelected {
return true
} else {
return false
}
case let .usersInfo(lhsTheme, lhsText):
if case let .usersInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .timeHeader(lhsTheme, lhsText):
if case let .timeHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .timeExpiryDate(lhsTheme, lhsDateTimeFormat, lhsDate, lhsActive):
if case let .timeExpiryDate(rhsTheme, rhsDateTimeFormat, rhsDate, rhsActive) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsDate == rhsDate, lhsActive == rhsActive {
return true
} else {
return false
}
case let .timeCustomPicker(lhsTheme, lhsDateTimeFormat, lhsDate):
if case let .timeCustomPicker(rhsTheme, rhsDateTimeFormat, rhsDate) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsDate == rhsDate {
return true
} else {
return false
}
case let .timeInfo(lhsTheme, lhsText):
if case let .timeInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .durationHeader(lhsTheme, lhsText):
if case let .durationHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .duration(lhsIndex, lhsTheme, lhsProductId, lhsTitle, lhsSubtitle, lhsLabel, lhsBadge, lhsIsSelected):
if case let .duration(rhsIndex, rhsTheme, rhsProductId, rhsTitle, rhsSubtitle, rhsLabel, rhsBadge, rhsIsSelected) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsProductId == rhsProductId, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsLabel == rhsLabel, lhsBadge == rhsBadge, lhsIsSelected == rhsIsSelected {
return true
} else {
return false
}
case let .durationInfo(lhsTheme, lhsText):
if case let .durationInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
}
}
static func <(lhs: CreateGiveawayEntry, rhs: CreateGiveawayEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! CreateGiveawayControllerArguments
switch self {
case let .header(_, title, text):
return CreateGiveawayHeaderItem(theme: presentationData.theme, title: title, text: text, sectionId: self.section)
case let .createGiveaway(_, title, subtitle, isSelected):
return GiftModeItem(presentationData: presentationData, context: arguments.context, iconName: "Premium/Giveaway", title: title, subtitle: subtitle, label: nil, badge: nil, isSelected: isSelected, sectionId: self.section, action: {
arguments.updateState { state in
var updatedState = state
updatedState.mode = .giveaway
return updatedState
}
})
case let .awardUsers(_, title, subtitle, isSelected):
return GiftModeItem(presentationData: presentationData, context: arguments.context, iconName: "Media Editor/Privacy/SelectedUsers", title: title, subtitle: subtitle, subtitleActive: true, label: nil, badge: nil, isSelected: isSelected, sectionId: self.section, action: {
var openSelection = false
arguments.updateState { state in
var updatedState = state
if state.mode == .gift || state.peers.isEmpty {
openSelection = true
}
updatedState.mode = .gift
return updatedState
}
if openSelection {
arguments.openPeersSelection()
}
})
case let .subscriptionsHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .subscriptions(_, value):
let text = "\(value) Subscriptions / Boosts"
return SubscriptionsCountItem(theme: presentationData.theme, strings: presentationData.strings, text: text, value: value, range: 1 ..< 11, sectionId: self.section, updated: { value in
arguments.updateState { state in
var updatedState = state
updatedState.subscriptions = value
return updatedState
}
})
case let .subscriptionsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .channelsHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .channel(_, _, peer, boosts):
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: presentationData.nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: .text("this channel will receive \(boosts) boosts", .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: peer.id != arguments.context.account.peerId, sectionId: self.section, action: {
// arguments.openPeer(peer)
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in })
case let .channelAdd(theme, text):
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.roundPlusIconImage(theme), title: text, alwaysPlain: false, hasSeparator: true, sectionId: self.section, height: .generic, color: .accent, editing: false, action: {
arguments.openChannelsSelection()
})
case let .channelsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .usersHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .usersAll(_, title, isSelected):
return GiftModeItem(presentationData: presentationData, context: arguments.context, title: title, subtitle: nil, label: nil, badge: nil, isSelected: isSelected, sectionId: self.section, action: {
arguments.updateState { state in
var updatedState = state
updatedState.onlyNewEligible = false
return updatedState
}
})
case let .usersNew(_, title, isSelected):
return GiftModeItem(presentationData: presentationData, context: arguments.context, title: title, subtitle: nil, label: nil, badge: nil, isSelected: isSelected, sectionId: self.section, action: {
arguments.updateState { state in
var updatedState = state
updatedState.onlyNewEligible = true
return updatedState
}
})
case let .usersInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .timeHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .timeExpiryDate(theme, dateTimeFormat, value, active):
let text: String
if let value = value {
text = stringForMediumDate(timestamp: value, strings: presentationData.strings, dateTimeFormat: dateTimeFormat)
} else {
text = presentationData.strings.InviteLink_Create_TimeLimitExpiryDateNever
}
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()
arguments.updateState { state in
var updatedState = state
updatedState.pickingTimeLimit = !state.pickingTimeLimit
return updatedState
}
})
case let .timeCustomPicker(_, dateTimeFormat, date):
return ItemListDatePickerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, date: date, sectionId: self.section, style: .blocks, updated: { date in
arguments.updateState({ state in
var updatedState = state
updatedState.time = date
return updatedState
})
})
case let .timeInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .durationHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .duration(_, _, productId, title, subtitle, label, badge, isSelected):
return GiftModeItem(presentationData: presentationData, context: arguments.context, title: title, subtitle: subtitle, label: label, badge: badge, isSelected: isSelected, sectionId: self.section, action: {
arguments.updateState { state in
var updatedState = state
updatedState.selectedProductId = productId
return updatedState
}
})
case let .durationInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
}
}
}
private struct PremiumGiftProduct: Equatable {
let giftOption: PremiumGiftCodeOption
let storeProduct: InAppPurchaseManager.Product
var id: String {
return self.storeProduct.id
}
var months: Int32 {
return self.giftOption.months
}
var price: String {
return self.storeProduct.price
}
var pricePerMonth: String {
return self.storeProduct.pricePerMonth(Int(self.months))
}
}
private func createGiveawayControllerEntries(state: CreateGiveawayControllerState, presentationData: PresentationData, peers: [EnginePeer.Id: EnginePeer], products: [PremiumGiftProduct]) -> [CreateGiveawayEntry] {
var entries: [CreateGiveawayEntry] = []
entries.append(.header(presentationData.theme, "Boosts via Gifts", "Get more boosts for your channel by gifting\nPremium to your subscribers."))
entries.append(.createGiveaway(presentationData.theme, "Create Giveaway", "winners are chosen randomly", state.mode == .giveaway))
let recipientsText: String
if !state.peers.isEmpty {
var peerNamesArray: [String] = []
let peersCount = state.peers.count
for peerId in state.peers.prefix(2) {
if let peer = peers[peerId] {
peerNamesArray.append(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))
}
}
let peerNames = String(peerNamesArray.map { $0 }.joined(separator: ", "))
if !peerNames.isEmpty {
recipientsText = peerNames
} else {
recipientsText = presentationData.strings.PremiumGift_LabelRecipients(Int32(peersCount))
}
} else {
recipientsText = "select recipients"
}
entries.append(.awardUsers(presentationData.theme, "Award Specific Users", recipientsText, state.mode == .gift))
if case .giveaway = state.mode {
entries.append(.subscriptionsHeader(presentationData.theme, "QUANTITY OF PRIZES / BOOSTS".uppercased()))
entries.append(.subscriptions(presentationData.theme, state.subscriptions))
entries.append(.subscriptionsInfo(presentationData.theme, "Choose how many Premium subscriptions to give away and boosts to receive."))
entries.append(.channelsHeader(presentationData.theme, "CHANNELS INCLUDED IN THE GIVEAWAY".uppercased()))
var index: Int32 = 0
for peerId in state.channels {
if let peer = peers[peerId] {
entries.append(.channel(index, presentationData.theme, peer, state.subscriptions))
}
index += 1
}
entries.append(.channelAdd(presentationData.theme, "Add Channel"))
entries.append(.channelsInfo(presentationData.theme, "Choose the channels users need to be subscribed to take part in the giveaway"))
entries.append(.usersHeader(presentationData.theme, "USERS ELIGIBLE FOR THE GIVEAWAY".uppercased()))
entries.append(.usersAll(presentationData.theme, "All subscribers", !state.onlyNewEligible))
entries.append(.usersNew(presentationData.theme, "Only new subscribers", state.onlyNewEligible))
entries.append(.usersInfo(presentationData.theme, "Choose if you want to limit the giveaway only to those who joined the channel after the giveaway started."))
entries.append(.timeHeader(presentationData.theme, "DATE WHEN GIVEAWAY ENDS".uppercased()))
entries.append(.timeExpiryDate(presentationData.theme, presentationData.dateTimeFormat, state.time, state.pickingTimeLimit))
if state.pickingTimeLimit {
entries.append(.timeCustomPicker(presentationData.theme, presentationData.dateTimeFormat, state.time))
}
entries.append(.timeInfo(presentationData.theme, "Choose when \(state.subscriptions) subscribers of your channel will be randomly selected to receive Telegram Premium."))
}
entries.append(.durationHeader(presentationData.theme, "DURATION OF PREMIUM SUBSCRIPTIONS".uppercased()))
let recipientCount: Int
switch state.mode {
case .giveaway:
recipientCount = Int(state.subscriptions)
case .gift:
recipientCount = state.peers.count
}
let shortestOptionPrice: (Int64, NSDecimalNumber)
if let product = products.last {
shortestOptionPrice = (Int64(Float(product.storeProduct.priceCurrencyAndAmount.amount) / Float(product.months)), product.storeProduct.priceValue.dividing(by: NSDecimalNumber(value: product.months)))
} else {
shortestOptionPrice = (1, NSDecimalNumber(decimal: 1))
}
var i: Int32 = 0
for product in products {
let giftTitle: String
if product.months == 12 {
giftTitle = presentationData.strings.Premium_Gift_Years(1)
} else {
giftTitle = presentationData.strings.Premium_Gift_Months(product.months)
}
let discountValue = Int((1.0 - Float(product.storeProduct.priceCurrencyAndAmount.amount) / Float(product.months) / Float(shortestOptionPrice.0)) * 100.0)
let discount: String?
if discountValue > 0 {
discount = "-\(discountValue)%"
} else {
discount = nil
}
let subtitle = "\(product.storeProduct.price) x \(recipientCount)"
let label = product.storeProduct.multipliedPrice(count: recipientCount)
var isSelected = false
if let selectedProductId = state.selectedProductId {
isSelected = product.id == selectedProductId
} else if i == 0 {
isSelected = true
}
entries.append(.duration(i, presentationData.theme, product.id, giftTitle, subtitle, label, discount, isSelected))
i += 1
}
// entries.append(.duration(0, presentationData.theme, "3 Months", "$13.99 x \(state.subscriptions)", "$41.99", nil, true))
// entries.append(.duration(1, presentationData.theme, "6 Months", "$15.99 x \(state.subscriptions)", "$47.99", nil, false))
// entries.append(.duration(2, presentationData.theme, "1 Year", "$29.99 x \(state.subscriptions)", "$89.99", nil, false))
entries.append(.durationInfo(presentationData.theme, "You can review the list of features and terms of use for Telegram Premium [here]()."))
return entries
}
private struct CreateGiveawayControllerState: Equatable {
enum Mode {
case giveaway
case gift
}
var mode: Mode
var subscriptions: Int32
var channels: [EnginePeer.Id]
var peers: [EnginePeer.Id]
var selectedProductId: String?
var onlyNewEligible: Bool
var time: Int32
var pickingTimeLimit = false
var updating = false
}
public func createGiveawayController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id, completion: (() -> Void)? = nil) -> ViewController {
let actionsDisposable = DisposableSet()
let expiryTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + 86400 * 5
let initialState: CreateGiveawayControllerState = CreateGiveawayControllerState(mode: .giveaway, subscriptions: 5, channels: [peerId], peers: [], onlyNewEligible: false, time: expiryTime)
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
let updateState: ((CreateGiveawayControllerState) -> CreateGiveawayControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
let productsValue = Atomic<[PremiumGiftProduct]?>(value: nil)
var buyActionImpl: (() -> Void)?
var openPeersSelectionImpl: (() -> Void)?
var openChannelsSelectionImpl: (() -> Void)?
var presentControllerImpl: ((ViewController) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
var dismissImpl: (() -> Void)?
var dismissInputImpl: (() -> Void)?
let arguments = CreateGiveawayControllerArguments(context: context, updateState: { f in
updateState(f)
}, dismissInput: {
dismissInputImpl?()
}, openPeersSelection: {
openPeersSelectionImpl?()
}, openChannelsSelection: {
openChannelsSelectionImpl?()
})
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
let products = combineLatest(
.single([]) |> then(context.engine.payments.premiumGiftCodeOptions(peerId: peerId)),
context.inAppPurchaseManager?.availableProducts ?? .single([])
)
|> map { options, products in
var gifts: [PremiumGiftProduct] = []
for option in options {
if let product = products.first(where: { $0.id == option.storeProductId }), !product.isSubscription {
gifts.append(PremiumGiftProduct(giftOption: option, storeProduct: product))
}
}
return gifts
}
let previousState = Atomic<CreateGiveawayControllerState?>(value: nil)
let signal = combineLatest(
presentationData,
statePromise.get()
|> mapToSignal { state in
return context.engine.data.get(EngineDataMap(
Set(state.channels + state.peers).map {
TelegramEngine.EngineData.Item.Peer.Peer(id: $0)
}
))
|> map { peers in
return (state, peers)
}
},
products
)
|> deliverOnMainQueue
|> map { presentationData, stateAndPeersMap, products -> (ItemListControllerState, (ItemListNodeState, Any)) in
var presentationData = presentationData
let updatedTheme = presentationData.theme.withModalBlocksBackground()
presentationData = presentationData.withUpdated(theme: updatedTheme)
let (state, peersMap) = stateAndPeersMap
let footerItem = CreateGiveawayFooterItem(theme: presentationData.theme, title: state.mode == .gift ? "Gift Premium" : "Start Giveaway", action: {
buyActionImpl?()
})
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
dismissImpl?()
})
let _ = productsValue.swap(products)
let previousState = previousState.swap(state)
var animateChanges = false
if let previousState = previousState, previousState.pickingTimeLimit != state.pickingTimeLimit || previousState.mode != state.mode {
animateChanges = true
}
var peers: [EnginePeer.Id: EnginePeer] = [:]
for (peerId, peer) in peersMap {
if let peer {
peers[peerId] = peer
}
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(""), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: createGiveawayControllerEntries(state: state, presentationData: presentationData, peers: peers, products: products), style: .blocks, emptyStateItem: nil, footerItem: footerItem, crossfadeState: false, animateChanges: animateChanges)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
controller.navigationPresentation = .modal
controller.beganInteractiveDragging = {
dismissInputImpl?()
}
presentControllerImpl = { [weak controller] c in
controller?.present(c, in: .window(.root))
}
pushControllerImpl = { [weak controller] c in
controller?.push(c)
}
dismissInputImpl = { [weak controller] in
controller?.view.endEditing(true)
}
dismissImpl = { [weak controller] in
controller?.dismiss()
}
buyActionImpl = {
let state = stateValue.with { $0 }
guard let products = productsValue.with({ $0 }) else {
return
}
let selectedProduct: PremiumGiftProduct
if let selectedProductId = state.selectedProductId, let product = products.first(where: { $0.id == selectedProductId }) {
selectedProduct = product
} else {
selectedProduct = products.first!
}
let (currency, amount) = selectedProduct.storeProduct.priceCurrencyAndAmount
let purpose: AppStoreTransactionPurpose
switch state.mode {
case .giveaway:
purpose = .giveaway(boostPeer: peerId, randomId: 1000, untilDate: state.time, currency: currency, amount: amount)
case .gift:
purpose = .giftCode(peerIds: state.peers, boostPeer: peerId, currency: currency, amount: amount)
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let _ = (context.engine.payments.canPurchasePremium(purpose: purpose)
|> deliverOnMainQueue).startStandalone(next: { available in
if available, let inAppPurchaseManager = context.inAppPurchaseManager {
let _ = (inAppPurchaseManager.buyProduct(selectedProduct.storeProduct, purpose: purpose)
|> deliverOnMainQueue).startStandalone(next: { status in
if case .purchased = status {
dismissImpl?()
}
}, error: { error in
var errorText: String?
switch error {
case .generic:
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
case .network:
errorText = presentationData.strings.Premium_Purchase_ErrorNetwork
case .notAllowed:
errorText = presentationData.strings.Premium_Purchase_ErrorNotAllowed
case .cantMakePayments:
errorText = presentationData.strings.Premium_Purchase_ErrorCantMakePayments
case .assignFailed:
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)
}
})
} 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)
}
})
}
openPeersSelectionImpl = {
let state = stateValue.with { $0 }
let stateContext = ShareWithPeersScreen.StateContext(
context: context,
subject: .members(peerId: peerId),
initialPeerIds: Set(state.peers)
)
let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).startStandalone(next: { _ in
let controller = ShareWithPeersScreen(
context: context,
initialPrivacy: EngineStoryPrivacy(base: .nobody, additionallyIncludePeers: state.peers),
stateContext: stateContext,
completion: { _, privacy ,_, _, _, _ in
updateState { state in
var updatedState = state
updatedState.peers = privacy.additionallyIncludePeers
return updatedState
}
}
)
pushControllerImpl?(controller)
})
}
openChannelsSelectionImpl = {
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, requestPeerType: [ReplyMarkupButtonRequestPeerType.channel(ReplyMarkupButtonRequestPeerType.Channel(isCreator: false, hasUsername: nil, userAdminRights: TelegramChatAdminRights(rights: [.canChangeInfo]), botAdminRights: nil))]))
controller.peerSelected = { [weak controller] peer, _ in
updateState { state in
var updatedState = state
var channels = state.channels
channels.append(peer.id)
updatedState.channels = channels
return updatedState
}
controller?.dismiss()
}
pushControllerImpl?(controller)
}
return controller
}

View File

@ -0,0 +1,125 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import SolidRoundedButtonNode
final class CreateGiveawayFooterItem: ItemListControllerFooterItem {
let theme: PresentationTheme
let title: String
let action: () -> Void
init(theme: PresentationTheme, title: String, action: @escaping () -> Void) {
self.theme = theme
self.title = title
self.action = action
}
func isEqual(to: ItemListControllerFooterItem) -> Bool {
if let item = to as? CreateGiveawayFooterItem {
return self.theme === item.theme && self.title == item.title
} else {
return false
}
}
func node(current: ItemListControllerFooterItemNode?) -> ItemListControllerFooterItemNode {
if let current = current as? CreateGiveawayFooterItemNode {
current.item = self
return current
} else {
return CreateGiveawayFooterItemNode(item: self)
}
}
}
final class CreateGiveawayFooterItemNode: ItemListControllerFooterItemNode {
private let backgroundNode: NavigationBackgroundNode
private let separatorNode: ASDisplayNode
private let buttonNode: SolidRoundedButtonNode
private var validLayout: ContainerViewLayout?
var item: CreateGiveawayFooterItem {
didSet {
self.updateItem()
if let layout = self.validLayout {
let _ = self.updateLayout(layout: layout, transition: .immediate)
}
}
}
init(item: CreateGiveawayFooterItem) {
self.item = item
self.backgroundNode = NavigationBackgroundNode(color: item.theme.rootController.tabBar.backgroundColor)
self.separatorNode = ASDisplayNode()
self.buttonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: .black, foregroundColor: .white), height: 50.0, cornerRadius: 11.0)
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.separatorNode)
self.addSubnode(self.buttonNode)
self.updateItem()
}
private func updateItem() {
self.backgroundNode.updateColor(color: self.item.theme.rootController.tabBar.backgroundColor, transition: .immediate)
self.separatorNode.backgroundColor = self.item.theme.rootController.tabBar.separatorColor
self.buttonNode.updateTheme(SolidRoundedButtonTheme(backgroundColor: self.item.theme.list.itemCheckColors.fillColor, foregroundColor: self.item.theme.list.itemCheckColors.foregroundColor))
self.buttonNode.title = self.item.title
self.buttonNode.pressed = { [weak self] in
self?.item.action()
}
}
override func updateBackgroundAlpha(_ alpha: CGFloat, transition: ContainedViewLayoutTransition) {
transition.updateAlpha(node: self.backgroundNode, alpha: alpha)
transition.updateAlpha(node: self.separatorNode, alpha: alpha)
}
override func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat {
self.validLayout = layout
let buttonInset: CGFloat = 16.0
let buttonWidth = layout.size.width - layout.safeInsets.left - layout.safeInsets.right - buttonInset * 2.0
let buttonHeight = self.buttonNode.updateLayout(width: buttonWidth, transition: transition)
let inset: CGFloat = 9.0
let insets = layout.insets(options: [.input])
var panelHeight: CGFloat = buttonHeight + inset * 2.0
let totalPanelHeight: CGFloat
if let inputHeight = layout.inputHeight, inputHeight > 0.0 {
totalPanelHeight = panelHeight + insets.bottom
} else {
panelHeight += insets.bottom
totalPanelHeight = panelHeight
}
let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - totalPanelHeight), size: CGSize(width: layout.size.width, height: panelHeight))
transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + buttonInset, y: panelFrame.minY + inset), size: CGSize(width: buttonWidth, height: buttonHeight)))
transition.updateFrame(node: self.backgroundNode, frame: panelFrame)
self.backgroundNode.update(size: panelFrame.size, transition: transition)
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: panelFrame.origin, size: CGSize(width: panelFrame.width, height: UIScreenPixel)))
return panelHeight
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if self.backgroundNode.frame.contains(point) {
return true
} else {
return false
}
}
}

View File

@ -0,0 +1,169 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import Markdown
import ComponentFlow
final class CreateGiveawayHeaderItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let title: String
let text: String
let sectionId: ItemListSectionId
init(theme: PresentationTheme, title: String, text: String, sectionId: ItemListSectionId) {
self.theme = theme
self.title = title
self.text = text
self.sectionId = sectionId
}
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) {
async {
let node = CreateGiveawayHeaderItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
guard let nodeValue = node() as? CreateGiveawayHeaderItemNode else {
assertionFailure()
return
}
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
private let titleFont = Font.semibold(20.0)
private let textFont = Font.regular(15.0)
class CreateGiveawayHeaderItemNode: ListViewItemNode {
private let titleNode: TextNode
private let textNode: TextNode
private var hostView: ComponentHostView<Empty>?
private var params: (AnyComponent<Empty>, CGSize, ListViewItemNodeLayout)?
private var item: CreateGiveawayHeaderItem?
init() {
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.contentMode = .left
self.textNode.contentsScale = UIScreen.main.scale
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
}
override func didLoad() {
super.didLoad()
let hostView = ComponentHostView<Empty>()
self.hostView = hostView
self.view.addSubview(hostView)
if let (component, containerSize, layout) = self.params {
let size = hostView.update(
transition: .immediate,
component: component,
environment: {},
containerSize: containerSize
)
hostView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - size.width) / 2.0), y: -121.0), size: size)
}
}
func asyncLayout() -> (_ item: CreateGiveawayHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeTextLayout = TextNode.asyncLayout(self.textNode)
return { item, params, neighbors in
let topInset: CGFloat = 2.0
let leftInset: CGFloat = 24.0 + params.leftInset
let attributedTitle = NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor, paragraphAlignment: .center)
let attributedText = NSAttributedString(string: item.text, font: textFont, textColor: item.theme.list.freeTextColor, paragraphAlignment: .center)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedTitle, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let contentSize = CGSize(width: params.width, height: topInset + titleLayout.size.height + textLayout.size.height + 80.0)
let insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
let component = AnyComponent(PremiumStarComponent(isIntro: true, isVisible: true, hasIdleAnimations: true))
let containerSize = CGSize(width: min(414.0, layout.size.width), height: 220.0)
if let hostView = strongSelf.hostView {
let size = hostView.update(
transition: .immediate,
component: component,
environment: {},
containerSize: containerSize
)
hostView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - size.width) / 2.0), y: -121.0), size: size)
}
var origin: CGFloat = 78.0
let _ = titleApply()
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleLayout.size.width) / 2.0), y: origin), size: titleLayout.size)
origin += titleLayout.size.height + 10.0
let _ = textApply()
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - textLayout.size.width) / 2.0), y: origin), size: textLayout.size)
strongSelf.params = (component, containerSize, layout)
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}

View File

@ -0,0 +1,410 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AccountContext
import AvatarNode
public final class GiftModeItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let context: AccountContext
let iconName: String?
let title: String
let subtitle: String?
let subtitleActive: Bool
let label: String?
let badge: String?
let isSelected: Bool
public let sectionId: ItemListSectionId
let action: (() -> Void)?
public init(presentationData: ItemListPresentationData, context: AccountContext, iconName: String? = nil, title: String, subtitle: String?, subtitleActive: Bool = false, label: String?, badge: String?, isSelected: Bool, sectionId: ItemListSectionId, action: (() -> Void)?) {
self.presentationData = presentationData
self.iconName = iconName
self.context = context
self.title = title
self.subtitle = subtitle
self.subtitleActive = subtitleActive
self.label = label
self.badge = badge
self.isSelected = isSelected
self.sectionId = sectionId
self.action = action
}
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) {
async {
let node = GiftModeItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply(false) })
})
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? GiftModeItemNode {
let makeLayout = nodeValue.asyncLayout()
var animated = true
if case .None = animation {
animated = false
}
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply(animated)
})
}
}
}
}
}
public var selectable: Bool = true
public func selected(listView: ListView){
listView.clearHighlightAnimated(true)
self.action?()
}
}
class GiftModeItemNode: ItemListRevealOptionsItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let maskNode: ASImageNode
private let containerNode: ASDisplayNode
override var controlsContainer: ASDisplayNode {
return self.containerNode
}
fileprivate var avatarNode: ASImageNode?
private let titleNode: TextNode
private let statusNode: TextNode
private let labelNode: TextNode
private let badgeTextNode: TextNode
private var badgeBackgroundNode: ASImageNode?
private var layoutParams: (GiftModeItem, ListViewItemLayoutParams, ItemListNeighbors)?
private var selectableControlNode: ItemListSelectableControlNode?
private let activateArea: AccessibilityAreaNode
private let fetchDisposable = MetaDisposable()
override var canBeSelected: Bool {
return true
}
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.containerNode = ASDisplayNode()
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.statusNode = TextNode()
self.statusNode.isUserInteractionEnabled = false
self.statusNode.contentMode = .left
self.statusNode.contentsScale = UIScreen.main.scale
self.labelNode = TextNode()
self.labelNode.isUserInteractionEnabled = false
self.labelNode.contentMode = .left
self.labelNode.contentsScale = UIScreen.main.scale
self.badgeTextNode = TextNode()
self.badgeTextNode.isUserInteractionEnabled = false
self.badgeTextNode.contentMode = .left
self.badgeTextNode.contentsScale = UIScreen.main.scale
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.activateArea = AccessibilityAreaNode()
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.titleNode)
self.containerNode.addSubnode(self.statusNode)
self.containerNode.addSubnode(self.labelNode)
self.addSubnode(self.activateArea)
}
override func tapped() {
guard let item = self.layoutParams?.0 else {
return
}
item.action?()
}
func asyncLayout() -> (_ item: GiftModeItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeStatusLayout = TextNode.asyncLayout(self.statusNode)
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let selectableControlLayout = ItemListSelectableControlNode.asyncLayout(self.selectableControlNode)
let currentItem = self.layoutParams?.0
return { item, params, neighbors in
let titleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 17.0 / 17.0))
let statusFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0))
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let rightInset: CGFloat = params.rightInset
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 labelAttributedString = NSAttributedString(string: item.label ?? "", font: titleFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
let leftInset: CGFloat = 17.0 + params.leftInset
var avatarInset: CGFloat = 0.0
if let _ = item.iconName {
avatarInset += 40.0
}
let verticalInset: CGFloat = 11.0
let titleSpacing: CGFloat = 2.0
let insets = itemListNeighborsGroupedInsets(neighbors, params)
let separatorHeight = UIScreenPixel
var selectableControlSizeAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)?
var editingOffset: CGFloat = 0.0
let sizeAndApply = selectableControlLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, item.isSelected, false)
selectableControlSizeAndApply = sizeAndApply
editingOffset = sizeAndApply.0
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: labelAttributedString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: .greatestFiniteMagnitude)))
let textConstrainedWidth = params.width - leftInset - 8.0 - editingOffset - rightInset - labelLayout.size.width - avatarInset
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: textConstrainedWidth, height: .greatestFiniteMagnitude)))
let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: textConstrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + statusLayout.size.height)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] animated in
if let strongSelf = self {
strongSelf.layoutParams = (item, params, neighbors)
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
strongSelf.activateArea.accessibilityLabel = titleAttributedString.string
strongSelf.activateArea.accessibilityValue = statusAttributedString.string
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
let transition: ContainedViewLayoutTransition
if animated {
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
let iconSize = CGSize(width: 40.0, height: 40.0)
if let iconName = item.iconName {
let iconNode: ASImageNode
if let current = strongSelf.avatarNode {
iconNode = current
} else {
iconNode = ASImageNode()
iconNode.displaysAsynchronously = false
strongSelf.addSubnode(iconNode)
strongSelf.avatarNode = iconNode
}
let colors: [UIColor]
if iconName.contains("away") {
colors = [UIColor(rgb: 0x4faaff), UIColor(rgb: 0x017aff)]
} else {
colors = [UIColor(rgb: 0xc36eff), UIColor(rgb: 0x8c61fa)]
}
if iconNode.image == nil {
iconNode.image = generateAvatarImage(size: iconSize, icon: generateTintedImage(image: UIImage(bundleImageName: iconName), color: .white), iconScale: 1.0, cornerRadius: 20.0, color: .blue, customColors: colors.reversed())
}
let iconFrame = CGRect(origin: CGPoint(x: leftInset + 38.0, y: floorToScreenPixels((layout.contentSize.height - iconSize.height) / 2.0)), size: iconSize)
iconNode.frame = iconFrame
}
if let selectableControlSizeAndApply = selectableControlSizeAndApply {
let selectableControlSize = CGSize(width: selectableControlSizeAndApply.0, height: layout.contentSize.height)
let selectableControlFrame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: selectableControlSize)
if strongSelf.selectableControlNode == nil {
let selectableControlNode = selectableControlSizeAndApply.1(selectableControlSize, false)
strongSelf.selectableControlNode = selectableControlNode
strongSelf.addSubnode(selectableControlNode)
selectableControlNode.frame = selectableControlFrame
transition.animatePosition(node: selectableControlNode, from: CGPoint(x: -selectableControlFrame.size.width / 2.0, y: selectableControlFrame.midY))
selectableControlNode.alpha = 0.0
transition.updateAlpha(node: selectableControlNode, alpha: 1.0)
} else if let selectableControlNode = strongSelf.selectableControlNode {
transition.updateFrame(node: selectableControlNode, frame: selectableControlFrame)
let _ = selectableControlSizeAndApply.1(selectableControlSize, true)
}
} else if let selectableControlNode = strongSelf.selectableControlNode {
var selectableControlFrame = selectableControlNode.frame
selectableControlFrame.origin.x = -selectableControlFrame.size.width
strongSelf.selectableControlNode = nil
transition.updateAlpha(node: selectableControlNode, alpha: 0.0)
transition.updateFrame(node: selectableControlNode, frame: selectableControlFrame, completion: { [weak selectableControlNode] _ in
selectableControlNode?.removeFromSupernode()
})
}
let _ = titleApply()
let _ = statusApply()
let _ = labelApply()
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.addSubnode(strongSelf.maskNode)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset + editingOffset
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: strongSelf.backgroundNode.frame.size)
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)))
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)))
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + editingOffset + avatarInset, y: verticalInset), size: titleLayout.size))
transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + editingOffset + avatarInset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: statusLayout.size))
transition.updateFrame(node: strongSelf.labelNode, frame: CGRect(origin: CGPoint(x: layoutSize.width - rightInset - labelLayout.size.width - 18.0, y: floorToScreenPixels((layout.contentSize.height - labelLayout.size.height) / 2.0)), size: labelLayout.size))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: strongSelf.backgroundNode.frame.height + UIScreenPixel + UIScreenPixel))
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
}
})
}
}
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}

View File

@ -0,0 +1,879 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramCore
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import PresentationDataUtils
import ComponentFlow
import ViewControllerComponent
import SheetComponent
import MultilineTextComponent
import BundleIconComponent
import SolidRoundedButtonComponent
import Markdown
import BalancedTextComponent
import ConfettiEffect
import AvatarNode
import TextFormat
import TelegramStringFormatting
import UndoUI
private final class PremiumGiftCodeSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let giftCode: PremiumGiftCodeInfo
let action: () -> Void
let cancel: () -> Void
let openPeer: (EnginePeer) -> Void
init(
context: AccountContext,
giftCode: PremiumGiftCodeInfo,
action: @escaping () -> Void,
cancel: @escaping () -> Void,
openPeer: @escaping (EnginePeer) -> Void
) {
self.context = context
self.giftCode = giftCode
self.action = action
self.cancel = cancel
self.openPeer = openPeer
}
static func ==(lhs: PremiumGiftCodeSheetContent, rhs: PremiumGiftCodeSheetContent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.giftCode != rhs.giftCode {
return false
}
return true
}
final class State: ComponentState {
private let context: AccountContext
private var disposable: Disposable?
var initialized = false
var peerMap: [EnginePeer.Id: EnginePeer] = [:]
var cachedCloseImage: (UIImage, PresentationTheme)?
init(context: AccountContext, giftCode: PremiumGiftCodeInfo) {
self.context = context
super.init()
var peerIds: [EnginePeer.Id] = []
peerIds.append(giftCode.fromPeerId)
if let toPeerId = giftCode.toPeerId {
peerIds.append(toPeerId)
}
self.disposable = (context.engine.data.get(
EngineDataMap(
peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in
return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
}
)
) |> deliverOnMainQueue).startStrict(next: { [weak self] peers in
if let strongSelf = self {
var peersMap: [EnginePeer.Id: EnginePeer] = [:]
for peerId in peerIds {
if let maybePeer = peers[peerId], let peer = maybePeer {
peersMap[peerId] = peer
}
}
strongSelf.peerMap = peersMap
strongSelf.initialized = true
strongSelf.updated(transition: .immediate)
}
})
}
deinit {
self.disposable?.dispose()
}
}
func makeState() -> State {
return State(context: self.context, giftCode: self.giftCode)
}
static var body: Body {
let closeButton = Child(Button.self)
let title = Child(MultilineTextComponent.self)
let star = Child(PremiumStarComponent.self)
let description = Child(BalancedTextComponent.self)
let linkButton = Child(Button.self)
let table = Child(TableComponent.self)
let additional = Child(BalancedTextComponent.self)
let button = Child(SolidRoundedButtonComponent.self)
return { context in
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let component = context.component
let theme = environment.theme
let strings = environment.strings
let dateTimeFormat = environment.dateTimeFormat
let state = context.state
let giftCode = component.giftCode
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let textSideInset: CGFloat = 32.0 + environment.safeInsets.left
let closeImage: UIImage
if let (image, theme) = state.cachedCloseImage, theme === environment.theme {
closeImage = image
} else {
closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: theme.actionSheet.inputClearButtonColor)!
state.cachedCloseImage = (closeImage, theme)
}
let closeButton = closeButton.update(
component: Button(
content: AnyComponent(Image(image: closeImage)),
action: { [weak component] in
component?.cancel()
}
),
availableSize: CGSize(width: 30.0, height: 30.0),
transition: .immediate
)
let titleText: String
let descriptionText: String
let additionalText: String
let buttonText: String
if let usedDate = giftCode.usedDate {
let dateString = stringForMediumDate(timestamp: usedDate, strings: strings, dateTimeFormat: dateTimeFormat)
titleText = "Used Gift Link"
descriptionText = "This link was used to activate a **Telegram Premium** subscription."
additionalText = "This link was used on \(dateString)."
buttonText = strings.Common_OK
} else {
titleText = "Gift Link"
descriptionText = "This link allows you to activate a **Telegram Premium** subscription."
additionalText = "You can also [send this link]() to a friend as a gift."
buttonText = "Use Link"
}
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: titleText,
font: Font.semibold(17.0),
textColor: theme.actionSheet.primaryTextColor,
paragraphAlignment: .center
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let star = star.update(
component: PremiumStarComponent(isIntro: false, isVisible: true, hasIdleAnimations: true),
availableSize: CGSize(width: context.availableSize.width, height: 200.0),
transition: .immediate
)
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let textColor = theme.actionSheet.primaryTextColor
let linkColor = theme.actionSheet.controlAccentColor
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})
let description = description.update(
component: BalancedTextComponent(
text: .markdown(text: descriptionText, attributes: markdownAttributes),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
let link = "https://t.me/giftcode/\(giftCode.slug)"
let linkButton = linkButton.update(
component: Button(
content: AnyComponent(
LinkButtonContentComponent(theme: environment.theme, text: link)
),
action: {
UIPasteboard.general.string = link
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
transition: .immediate
)
let tableFont = Font.regular(15.0)
let tableTextColor = theme.list.itemPrimaryTextColor
let tableLinkColor = theme.list.itemAccentColor
var tableItems: [TableComponent.Item] = []
let fromPeer = state.peerMap[giftCode.fromPeerId]
tableItems.append(.init(
id: "from",
title: "From",
component: AnyComponent(
Button(
content: AnyComponent(PeerCellComponent(context: context.component.context, textColor: tableLinkColor, peer: fromPeer)),
action: {
if let peer = fromPeer {
component.openPeer(peer)
}
}
)
)
))
if let toPeerId = giftCode.toPeerId {
let toPeer = state.peerMap[toPeerId]
tableItems.append(.init(
id: "to",
title: "To",
component: AnyComponent(
Button(
content: AnyComponent(PeerCellComponent(context: context.component.context, textColor: tableLinkColor, peer: toPeer)),
action: {
if let peer = toPeer {
component.openPeer(peer)
}
}
)
)
))
}
let giftTitle: String
if giftCode.months == 12 {
giftTitle = "Telegram Premium for 1 year"
} else {
giftTitle = "Telegram Premium for \(giftCode.months) months"
}
tableItems.append(.init(
id: "gift",
title: "Gift",
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: giftTitle, font: tableFont, textColor: tableTextColor)))
)
))
let giftReason = giftCode.isGiveaway ? "Giveaway" : "Gift"
tableItems.append(.init(
id: "reason",
title: "Reason",
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: giftReason, font: tableFont, textColor: tableLinkColor)))
)
))
tableItems.append(.init(
id: "date",
title: "Date",
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: giftCode.date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
)
))
let table = table.update(
component: TableComponent(
theme: environment.theme,
items: tableItems
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude),
transition: .immediate
)
let additional = additional.update(
component: BalancedTextComponent(
text: .markdown(text: additionalText, attributes: markdownAttributes),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.1
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
let button = button.update(
component: SolidRoundedButtonComponent(
title: buttonText,
theme: SolidRoundedButtonComponent.Theme(theme: theme),
font: .bold,
fontSize: 17.0,
height: 50.0,
cornerRadius: 10.0,
gloss: !giftCode.isUsed,
iconName: nil,
animationName: nil,
iconPosition: .left,
action: {
if giftCode.isUsed {
component.cancel()
} else {
component.action()
}
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
transition: context.transition
)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: 28.0))
)
context.add(star
.position(CGPoint(x: context.availableSize.width / 2.0, y: star.size.height / 2.0))
)
var originY: CGFloat = 0.0
originY += star.size.height - 32.0
context.add(description
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + description.size.height / 2.0))
)
originY += description.size.height + 21.0
context.add(linkButton
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + linkButton.size.height / 2.0))
)
originY += linkButton.size.height + 16.0
context.add(table
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + table.size.height / 2.0))
)
originY += table.size.height + 23.0
context.add(additional
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + additional.size.height / 2.0))
)
originY += additional.size.height + 23.0
let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: button.size)
context.add(button
.position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY))
)
context.add(closeButton
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - closeButton.size.width, y: 28.0))
)
let contentSize = CGSize(width: context.availableSize.width, height: buttonFrame.maxY + 5.0 + environment.safeInsets.bottom)
return contentSize
}
}
}
private final class PremiumGiftCodeSheetComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let giftCode: PremiumGiftCodeInfo
let action: () -> Void
let cancel: () -> Void
let openPeer: (EnginePeer) -> Void
init(
context: AccountContext,
giftCode: PremiumGiftCodeInfo,
action: @escaping () -> Void,
cancel: @escaping () -> Void,
openPeer: @escaping (EnginePeer) -> Void
) {
self.context = context
self.giftCode = giftCode
self.action = action
self.cancel = cancel
self.openPeer = openPeer
}
static func ==(lhs: PremiumGiftCodeSheetComponent, rhs: PremiumGiftCodeSheetComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.giftCode != rhs.giftCode {
return false
}
return true
}
static var body: Body {
let sheet = Child(SheetComponent<EnvironmentType>.self)
let animateOut = StoredActionSlot(Action<Void>.self)
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(PremiumGiftCodeSheetContent(
context: context.component.context,
giftCode: context.component.giftCode,
action: context.component.action,
cancel: {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
},
openPeer: context.component.openPeer
)),
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
animateOut: animateOut
),
environment: {
environment
SheetComponentEnvironment(
isDisplaying: environment.value.isVisible,
isCentered: environment.metrics.widthClass == .regular,
hasInputHeight: !environment.inputHeight.isZero,
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
dismiss: { animated in
if animated {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
} else {
if let controller = controller() {
controller.dismiss(completion: nil)
}
}
}
)
},
availableSize: context.availableSize,
transition: context.transition
)
context.add(sheet
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
return context.availableSize
}
}
}
public class PremiumGiftCodeScreen: ViewControllerComponentContainer {
private let context: AccountContext
public var disposed: () -> Void = {}
private let hapticFeedback = HapticFeedback()
public init(
context: AccountContext,
giftCode: PremiumGiftCodeInfo,
forceDark: Bool = false,
cancel: @escaping () -> Void = {},
action: @escaping () -> Void,
openPeer: @escaping (EnginePeer) -> Void = { _ in }
) {
self.context = context
super.init(context: context, component: PremiumGiftCodeSheetComponent(context: context, giftCode: giftCode, action: action, cancel: cancel, openPeer: openPeer), navigationBarAppearance: .none, statusBarStyle: .ignore, theme: forceDark ? .dark : .default)
self.navigationPresentation = .flatModal
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.disposed()
}
public override func viewDidLoad() {
super.viewDidLoad()
self.view.disablesInteractiveModalDismiss = true
}
}
private final class LinkButtonContentComponent: CombinedComponent {
let theme: PresentationTheme
let text: String
public init(
theme: PresentationTheme,
text: String
) {
self.theme = theme
self.text = text
}
static func ==(lhs: LinkButtonContentComponent, rhs: LinkButtonContentComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.text != rhs.text {
return false
}
return true
}
static var body: Body {
let background = Child(RoundedRectangle.self)
let text = Child(MultilineTextComponent.self)
let icon = Child(BundleIconComponent.self)
return { context in
let component = context.component
let sideInset: CGFloat = 38.0
let background = background.update(
component: RoundedRectangle(color: component.theme.list.itemInputField.backgroundColor, cornerRadius: 10.0),
availableSize: context.availableSize,
transition: context.transition
)
let text = text.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.text.replacingOccurrences(of: "https://", with: ""),
font: Font.regular(17.0),
textColor: component.theme.list.itemPrimaryTextColor,
paragraphAlignment: .natural
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - sideInset - sideInset, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let icon = icon.update(
component: BundleIconComponent(name: "Chat/Context Menu/Copy", tintColor: component.theme.list.itemAccentColor),
availableSize: context.availableSize,
transition: context.transition
)
context.add(background
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
context.add(text
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
context.add(icon
.position(CGPoint(x: context.availableSize.width - icon.size.width / 2.0 - 14.0, y: context.availableSize.height / 2.0))
)
return context.availableSize
}
}
}
private final class TableComponent: CombinedComponent {
class Item: Equatable {
public let id: AnyHashable
public let title: String
public let component: AnyComponent<Empty>
public init<IdType: Hashable>(id: IdType, title: String, component: AnyComponent<Empty>) {
self.id = AnyHashable(id)
self.title = title
self.component = component
}
public static func == (lhs: Item, rhs: Item) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.component != rhs.component {
return false
}
return true
}
}
private let theme: PresentationTheme
private let items: [Item]
public init(theme: PresentationTheme, items: [Item]) {
self.theme = theme
self.items = items
}
public static func ==(lhs: TableComponent, rhs: TableComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.items != rhs.items {
return false
}
return true
}
final class State: ComponentState {
var cachedBorderImage: (UIImage, PresentationTheme)?
}
func makeState() -> State {
return State()
}
public static var body: Body {
let leftColumnBackground = Child(Rectangle.self)
let verticalBorder = Child(Rectangle.self)
let titleChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self)
let valueChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self)
let borderChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self)
let outerBorder = Child(Image.self)
return { context in
let verticalPadding: CGFloat = 11.0
let horizontalPadding: CGFloat = 12.0
let borderWidth: CGFloat = 1.0
let backgroundColor = context.component.theme.actionSheet.opaqueItemBackgroundColor
let borderColor = backgroundColor.mixedWith(context.component.theme.list.itemBlocksSeparatorColor, alpha: 0.6)
var leftColumnWidth: CGFloat = 0.0
var updatedTitleChildren: [_UpdatedChildComponent] = []
var updatedValueChildren: [_UpdatedChildComponent] = []
var updatedBorderChildren: [_UpdatedChildComponent] = []
for item in context.component.items {
let titleChild = titleChildren[item.id].update(
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: item.title, font: Font.regular(15.0), textColor: context.component.theme.list.itemPrimaryTextColor))
)),
availableSize: context.availableSize,
transition: context.transition
)
updatedTitleChildren.append(titleChild)
if titleChild.size.width > leftColumnWidth {
leftColumnWidth = titleChild.size.width
}
}
leftColumnWidth = max(100.0, leftColumnWidth + horizontalPadding * 2.0)
let rightColumnWidth = context.availableSize.width - leftColumnWidth
var i = 0
var rowHeights: [Int: CGFloat] = [:]
var totalHeight: CGFloat = 0.0
for item in context.component.items {
let titleChild = updatedTitleChildren[i]
let valueChild = valueChildren[item.id].update(
component: item.component,
availableSize: CGSize(width: rightColumnWidth - horizontalPadding * 2.0, height: context.availableSize.height),
transition: context.transition
)
updatedValueChildren.append(valueChild)
let rowHeight = max(40.0, max(titleChild.size.height, valueChild.size.height) + verticalPadding * 2.0)
rowHeights[i] = rowHeight
totalHeight += rowHeight
if i < context.component.items.count - 1 {
let borderChild = borderChildren[item.id].update(
component: AnyComponent(Rectangle(color: borderColor)),
availableSize: CGSize(width: context.availableSize.width, height: borderWidth),
transition: context.transition
)
updatedBorderChildren.append(borderChild)
}
i += 1
}
let leftColumnBackground = leftColumnBackground.update(
component: Rectangle(color: context.component.theme.list.itemInputField.backgroundColor),
availableSize: CGSize(width: leftColumnWidth, height: totalHeight),
transition: context.transition
)
context.add(
leftColumnBackground
.position(CGPoint(x: leftColumnWidth / 2.0, y: totalHeight / 2.0))
)
let borderImage: UIImage
if let (currentImage, theme) = context.state.cachedBorderImage, theme === context.component.theme {
borderImage = currentImage
} else {
let borderRadius: CGFloat = 5.0
borderImage = generateImage(CGSize(width: 16.0, height: 16.0), rotatedContext: { size, context in
let bounds = CGRect(origin: .zero, size: size)
context.setFillColor(backgroundColor.cgColor)
context.fill(bounds)
let path = CGPath(roundedRect: bounds.insetBy(dx: borderWidth / 2.0, dy: borderWidth / 2.0), cornerWidth: borderRadius, cornerHeight: borderRadius, transform: nil)
context.setBlendMode(.clear)
context.addPath(path)
context.fillPath()
context.setBlendMode(.normal)
context.setStrokeColor(borderColor.cgColor)
context.setLineWidth(borderWidth)
context.addPath(path)
context.strokePath()
})!.stretchableImage(withLeftCapWidth: 5, topCapHeight: 5)
context.state.cachedBorderImage = (borderImage, context.component.theme)
}
let outerBorder = outerBorder.update(
component: Image(image: borderImage),
availableSize: CGSize(width: context.availableSize.width, height: totalHeight),
transition: context.transition
)
context.add(outerBorder
.position(CGPoint(x: context.availableSize.width / 2.0, y: totalHeight / 2.0))
)
let verticalBorder = verticalBorder.update(
component: Rectangle(color: borderColor),
availableSize: CGSize(width: borderWidth, height: totalHeight),
transition: context.transition
)
context.add(
verticalBorder
.position(CGPoint(x: leftColumnWidth - borderWidth / 2.0, y: totalHeight / 2.0))
)
i = 0
var originY: CGFloat = 0.0
for (titleChild, valueChild) in zip(updatedTitleChildren, updatedValueChildren) {
let rowHeight = rowHeights[i] ?? 0.0
let titleFrame = CGRect(origin: CGPoint(x: horizontalPadding, y: originY + verticalPadding), size: titleChild.size)
let valueFrame = CGRect(origin: CGPoint(x: leftColumnWidth + horizontalPadding, y: originY + verticalPadding), size: valueChild.size)
context.add(titleChild
.position(titleFrame.center)
)
context.add(valueChild
.position(valueFrame.center)
)
if i < updatedBorderChildren.count {
let borderChild = updatedBorderChildren[i]
context.add(borderChild
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + rowHeight - borderWidth / 2.0))
)
}
originY += rowHeight
i += 1
}
return CGSize(width: context.availableSize.width, height: totalHeight)
}
}
}
private final class PeerCellComponent: Component {
let context: AccountContext
let textColor: UIColor
let peer: EnginePeer?
init(context: AccountContext, textColor: UIColor, peer: EnginePeer?) {
self.context = context
self.textColor = textColor
self.peer = peer
}
static func ==(lhs: PeerCellComponent, rhs: PeerCellComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.textColor !== rhs.textColor {
return false
}
if lhs.peer != rhs.peer {
return false
}
return true
}
final class View: UIView {
private let avatarNode: AvatarNode
private let text = ComponentView<Empty>()
private var component: PeerCellComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 14.0))
super.init(frame: frame)
self.addSubnode(self.avatarNode)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: PeerCellComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
self.state = state
self.avatarNode.setPeer(
context: component.context,
theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme,
peer: component.peer,
synchronousLoad: true
)
let avatarSize = CGSize(width: 22.0, height: 22.0)
let spacing: CGFloat = 6.0
let textSize = self.text.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: component.peer?.compactDisplayTitle ?? "", font: Font.regular(15.0), textColor: component.textColor, paragraphAlignment: .left))
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - avatarSize.width - spacing, height: availableSize.height)
)
let size = CGSize(width: avatarSize.width + textSize.width + spacing, height: textSize.height)
let avatarFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - avatarSize.height) / 2.0)), size: avatarSize)
self.avatarNode.frame = avatarFrame
if let view = self.text.view {
if view.superview == nil {
self.addSubview(view)
}
let textFrame = CGRect(origin: CGPoint(x: avatarSize.width + spacing, y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize)
transition.setFrame(view: view, frame: textFrame)
}
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -670,11 +670,12 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
self.updateInProgress(true) self.updateInProgress(true)
self.updated(transition: .immediate) self.updated(transition: .immediate)
let _ = (self.context.engine.payments.canPurchasePremium(purpose: .gift(peerId: self.peerId, currency: currency, amount: amount)) let purpose: AppStoreTransactionPurpose = .gift(peerId: self.peerId, currency: currency, amount: amount)
let _ = (self.context.engine.payments.canPurchasePremium(purpose: purpose)
|> deliverOnMainQueue).start(next: { [weak self] available in |> deliverOnMainQueue).start(next: { [weak self] available in
if let strongSelf = self { if let strongSelf = self {
if available { if available {
strongSelf.paymentDisposable.set((inAppPurchaseManager.buyProduct(product.storeProduct, targetPeerId: strongSelf.peerId) strongSelf.paymentDisposable.set((inAppPurchaseManager.buyProduct(product.storeProduct, purpose: purpose)
|> deliverOnMainQueue).start(next: { [weak self] status in |> deliverOnMainQueue).start(next: { [weak self] status in
if let strongSelf = self, case .purchased = status { if let strongSelf = self, case .purchased = status {
Queue.mainQueue().after(2.0) { Queue.mainQueue().after(2.0) {

View File

@ -2293,11 +2293,12 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
self.updateInProgress(true) self.updateInProgress(true)
self.updated(transition: .immediate) self.updated(transition: .immediate)
let _ = (self.context.engine.payments.canPurchasePremium(purpose: isUpgrade ? .upgrade : .subscription) let purpose: AppStoreTransactionPurpose = isUpgrade ? .upgrade : .subscription
let _ = (self.context.engine.payments.canPurchasePremium(purpose: purpose)
|> deliverOnMainQueue).start(next: { [weak self] available in |> deliverOnMainQueue).start(next: { [weak self] available in
if let strongSelf = self { if let strongSelf = self {
if available { if available {
strongSelf.paymentDisposable.set((inAppPurchaseManager.buyProduct(premiumProduct.storeProduct, isUpgrade: isUpgrade) strongSelf.paymentDisposable.set((inAppPurchaseManager.buyProduct(premiumProduct.storeProduct, purpose: purpose)
|> deliverOnMainQueue).start(next: { [weak self] status in |> deliverOnMainQueue).start(next: { [weak self] status in
if let strongSelf = self, case .purchased = status { if let strongSelf = self, case .purchased = status {
strongSelf.activationDisposable.set((strongSelf.context.account.postbox.peerView(id: strongSelf.context.account.peerId) strongSelf.activationDisposable.set((strongSelf.context.account.postbox.peerView(id: strongSelf.context.account.peerId)

View File

@ -17,6 +17,7 @@ import Markdown
import BalancedTextComponent import BalancedTextComponent
import ConfettiEffect import ConfettiEffect
import AvatarNode import AvatarNode
import TextFormat
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
@ -810,8 +811,9 @@ private final class LimitSheetContent: CombinedComponent {
let dismiss: () -> Void let dismiss: () -> Void
let openPeer: (EnginePeer) -> Void let openPeer: (EnginePeer) -> Void
let openStats: (() -> Void)? let openStats: (() -> Void)?
let openGift: (() -> Void)?
init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, cancel: @escaping () -> Void, action: @escaping () -> Bool, dismiss: @escaping () -> Void, openPeer: @escaping (EnginePeer) -> Void, openStats: (() -> Void)?) { init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, cancel: @escaping () -> Void, action: @escaping () -> Bool, dismiss: @escaping () -> Void, openPeer: @escaping (EnginePeer) -> Void, openStats: (() -> Void)?, openGift: (() -> Void)?) {
self.context = context self.context = context
self.subject = subject self.subject = subject
self.count = count self.count = count
@ -820,6 +822,7 @@ private final class LimitSheetContent: CombinedComponent {
self.dismiss = dismiss self.dismiss = dismiss
self.openPeer = openPeer self.openPeer = openPeer
self.openStats = openStats self.openStats = openStats
self.openGift = openGift
} }
static func ==(lhs: LimitSheetContent, rhs: LimitSheetContent) -> Bool { static func ==(lhs: LimitSheetContent, rhs: LimitSheetContent) -> Bool {
@ -847,6 +850,7 @@ private final class LimitSheetContent: CombinedComponent {
var boosted = false var boosted = false
var cachedCloseImage: (UIImage, PresentationTheme)? var cachedCloseImage: (UIImage, PresentationTheme)?
var cachedChevronImage: (UIImage, PresentationTheme)?
init(context: AccountContext, subject: PremiumLimitScreen.Subject) { init(context: AccountContext, subject: PremiumLimitScreen.Subject) {
self.context = context self.context = context
@ -891,6 +895,11 @@ private final class LimitSheetContent: CombinedComponent {
let peerShortcut = Child(Button.self) let peerShortcut = Child(Button.self)
let statsButton = Child(Button.self) let statsButton = Child(Button.self)
let orLeftLine = Child(Rectangle.self)
let orRightLine = Child(Rectangle.self)
let orText = Child(MultilineTextComponent.self)
let giftText = Child(BalancedTextComponent.self)
return { context in return { context in
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let component = context.component let component = context.component
@ -942,6 +951,7 @@ private final class LimitSheetContent: CombinedComponent {
var titleText = strings.Premium_LimitReached var titleText = strings.Premium_LimitReached
var actionButtonText: String? var actionButtonText: String?
var actionButtonHasGloss = true var actionButtonHasGloss = true
var gradientedActionButton = true
var buttonAnimationName: String? = "premium_x2" var buttonAnimationName: String? = "premium_x2"
var buttonIconName: String? var buttonIconName: String?
let iconName: String let iconName: String
@ -1167,7 +1177,8 @@ private final class LimitSheetContent: CombinedComponent {
PeerShortcutComponent( PeerShortcutComponent(
context: component.context, context: component.context,
theme: environment.theme, theme: environment.theme,
peer: peer peer: peer,
badge: "X2"
) )
), ),
action: { action: {
@ -1182,6 +1193,10 @@ private final class LimitSheetContent: CombinedComponent {
) )
} }
if link != nil {
gradientedActionButton = false
}
if let _ = link, let openStats = component.openStats { if let _ = link, let openStats = component.openStats {
let _ = openStats let _ = openStats
let statsButton = statsButton.update( let statsButton = statsButton.update(
@ -1267,11 +1282,15 @@ private final class LimitSheetContent: CombinedComponent {
premiumTitle = "" premiumTitle = ""
if boosted { if boosted {
let prefixString = isCurrent ? strings.ChannelBoost_YouBoostedChannelText(peer.compactDisplayTitle).string : strings.ChannelBoost_YouBoostedOtherChannelText
let storiesString = strings.ChannelBoost_StoriesPerDay(level + 1) let storiesString = strings.ChannelBoost_StoriesPerDay(level + 1)
buttonIconName = nil
actionButtonText = environment.strings.Common_OK // buttonIconName = nil
// actionButtonText = environment.strings.Common_OK
actionButtonText = "Boost Again"
if let remaining { if let remaining {
titleText = isCurrent ? strings.ChannelBoost_YouBoostedChannel(peer.compactDisplayTitle).string : strings.ChannelBoost_YouBoostedOtherChannel
let boostsString = strings.ChannelBoost_MoreBoosts(remaining) let boostsString = strings.ChannelBoost_MoreBoosts(remaining)
if level == 0 { if level == 0 {
if remaining == 0 { if remaining == 0 {
@ -1288,9 +1307,10 @@ private final class LimitSheetContent: CombinedComponent {
} }
} }
} else { } else {
titleText = strings.ChannelBoost_MaxLevelReached
string = strings.ChannelBoost_BoostedChannelReachedLevel("\(level + 1)", storiesString).string string = strings.ChannelBoost_BoostedChannelReachedLevel("\(level + 1)", storiesString).string
} }
string = "**\(prefixString)**\n\(string)"
} }
let progress: CGFloat let progress: CGFloat
@ -1323,18 +1343,18 @@ private final class LimitSheetContent: CombinedComponent {
horizontalAlignment: .center, horizontalAlignment: .center,
maximumNumberOfLines: 1 maximumNumberOfLines: 1
), ),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate transition: .immediate
) )
let textFont = Font.regular(15.0) let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0) let boldTextFont = Font.semibold(15.0)
let textColor = theme.actionSheet.primaryTextColor let textColor = theme.actionSheet.primaryTextColor
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: textColor), linkAttribute: { _ in let linkColor = theme.actionSheet.controlAccentColor
return nil let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
}) })
var textChild: _UpdatedChildComponent? var textChild: _UpdatedChildComponent?
var alternateTextChild: _UpdatedChildComponent? var alternateTextChild: _UpdatedChildComponent?
if useAlternateText { if useAlternateText {
@ -1370,6 +1390,7 @@ private final class LimitSheetContent: CombinedComponent {
} }
let gradientColors: [UIColor] let gradientColors: [UIColor]
var buttonGradientColors: [UIColor]
if isPremiumDisabled { if isPremiumDisabled {
gradientColors = [ gradientColors = [
UIColor(rgb: 0x007afe), UIColor(rgb: 0x007afe),
@ -1383,6 +1404,14 @@ private final class LimitSheetContent: CombinedComponent {
UIColor(rgb: 0xe46ace) UIColor(rgb: 0xe46ace)
] ]
} }
if gradientedActionButton {
buttonGradientColors = gradientColors
} else {
buttonGradientColors = [
UIColor(rgb: 0x007afe),
UIColor(rgb: 0x5494ff)
]
}
var limitTransition: Transition = .immediate var limitTransition: Transition = .immediate
if boostUpdated { if boostUpdated {
@ -1395,7 +1424,7 @@ private final class LimitSheetContent: CombinedComponent {
title: actionButtonText ?? (isIncreaseButton ? strings.Premium_IncreaseLimit : strings.Common_OK), title: actionButtonText ?? (isIncreaseButton ? strings.Premium_IncreaseLimit : strings.Common_OK),
theme: SolidRoundedButtonComponent.Theme( theme: SolidRoundedButtonComponent.Theme(
backgroundColor: .black, backgroundColor: .black,
backgroundColors: gradientColors, backgroundColors: buttonGradientColors,
foregroundColor: .white foregroundColor: .white
), ),
font: .bold, font: .bold,
@ -1528,8 +1557,76 @@ private final class LimitSheetContent: CombinedComponent {
context.add(button context.add(button
.position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY)) .position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY))
) )
var additionalContentHeight: CGFloat = 0.0
if case let .storiesChannelBoost(_, _, _, _, _, link, _) = component.subject, link != nil {
let orText = orText.update(
component: MultilineTextComponent(text: .plain(NSAttributedString(string: "or", font: Font.regular(15.0), textColor: textColor.withAlphaComponent(0.8), paragraphAlignment: .center))),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
context.add(orText
.position(CGPoint(x: context.availableSize.width / 2.0, y: buttonFrame.maxY + 27.0))
)
let orLeftLine = orLeftLine.update(
component: Rectangle(color: environment.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3)),
availableSize: CGSize(width: 90.0, height: 1.0 - UIScreenPixel),
transition: .immediate
)
context.add(orLeftLine
.position(CGPoint(x: context.availableSize.width / 2.0 - orText.size.width / 2.0 - 11.0 - 45.0, y: buttonFrame.maxY + 27.0))
)
let orRightLine = orRightLine.update(
component: Rectangle(color: environment.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3)),
availableSize: CGSize(width: 90.0, height: 1.0 - UIScreenPixel),
transition: .immediate
)
context.add(orRightLine
.position(CGPoint(x: context.availableSize.width / 2.0 + orText.size.width / 2.0 + 11.0 + 45.0, y: buttonFrame.maxY + 27.0))
)
if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme {
state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, environment.theme)
}
let giftString = "Boost your channel by gifting your subscribers Telegram Premium. [Get boosts >]()"
let giftAttributedString = parseMarkdownIntoAttributedString(giftString, attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString
if let range = giftAttributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 {
giftAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: giftAttributedString.string))
}
let openGift = component.openGift
let giftText = giftText.update(
component: BalancedTextComponent(
text: .plain(giftAttributedString),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.1,
highlightColor: linkColor.withAlphaComponent(0.2),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { _, _ in
openGift?()
}
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
context.add(giftText
.position(CGPoint(x: context.availableSize.width / 2.0, y: buttonFrame.maxY + 50.0 + giftText.size.height / 2.0))
)
additionalContentHeight += giftText.size.height + 50.0
}
contentSize = CGSize(width: context.availableSize.width, height: buttonFrame.maxY + 5.0 + environment.safeInsets.bottom) contentSize = CGSize(width: context.availableSize.width, height: buttonFrame.maxY + additionalContentHeight + 5.0 + environment.safeInsets.bottom)
} else { } else {
var height: CGFloat = 351.0 var height: CGFloat = 351.0
if isPremiumDisabled { if isPremiumDisabled {
@ -1539,6 +1636,8 @@ private final class LimitSheetContent: CombinedComponent {
if case let .storiesChannelBoost(_, isCurrent, _, _, _, link, _) = component.subject { if case let .storiesChannelBoost(_, isCurrent, _, _, _, link, _) = component.subject {
if link != nil { if link != nil {
height += 66.0 height += 66.0
height += 100.0
} else { } else {
if isCurrent { if isCurrent {
height -= 53.0 height -= 53.0
@ -1566,8 +1665,9 @@ private final class LimitSheetComponent: CombinedComponent {
let action: () -> Bool let action: () -> Bool
let openPeer: (EnginePeer) -> Void let openPeer: (EnginePeer) -> Void
let openStats: (() -> Void)? let openStats: (() -> Void)?
let openGift: (() -> Void)?
init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, cancel: @escaping () -> Void, action: @escaping () -> Bool, openPeer: @escaping (EnginePeer) -> Void, openStats: (() -> Void)?) { init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, cancel: @escaping () -> Void, action: @escaping () -> Bool, openPeer: @escaping (EnginePeer) -> Void, openStats: (() -> Void)?, openGift: (() -> Void)?) {
self.context = context self.context = context
self.subject = subject self.subject = subject
self.count = count self.count = count
@ -1575,6 +1675,7 @@ private final class LimitSheetComponent: CombinedComponent {
self.action = action self.action = action
self.openPeer = openPeer self.openPeer = openPeer
self.openStats = openStats self.openStats = openStats
self.openGift = openGift
} }
static func ==(lhs: LimitSheetComponent, rhs: LimitSheetComponent) -> Bool { static func ==(lhs: LimitSheetComponent, rhs: LimitSheetComponent) -> Bool {
@ -1616,7 +1717,8 @@ private final class LimitSheetComponent: CombinedComponent {
}) })
}, },
openPeer: context.component.openPeer, openPeer: context.component.openPeer,
openStats: context.component.openStats openStats: context.component.openStats,
openGift: context.component.openGift
)), )),
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
animateOut: animateOut animateOut: animateOut
@ -1680,14 +1782,14 @@ public class PremiumLimitScreen: ViewControllerComponentContainer {
private let hapticFeedback = HapticFeedback() private let hapticFeedback = HapticFeedback()
public init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, forceDark: Bool = false, cancel: @escaping () -> Void = {}, action: @escaping () -> Bool, openPeer: @escaping (EnginePeer) -> Void = { _ in }, openStats: (() -> Void)? = nil) { public init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, forceDark: Bool = false, cancel: @escaping () -> Void = {}, action: @escaping () -> Bool, openPeer: @escaping (EnginePeer) -> Void = { _ in }, openStats: (() -> Void)? = nil, openGift: (() -> Void)? = nil) {
self.context = context self.context = context
self.openPeer = openPeer self.openPeer = openPeer
var actionImpl: (() -> Bool)? var actionImpl: (() -> Bool)?
super.init(context: context, component: LimitSheetComponent(context: context, subject: subject, count: count, cancel: {}, action: { super.init(context: context, component: LimitSheetComponent(context: context, subject: subject, count: count, cancel: {}, action: {
return actionImpl?() ?? true return actionImpl?() ?? true
}, openPeer: openPeer, openStats: openStats), navigationBarAppearance: .none, statusBarStyle: .ignore, theme: forceDark ? .dark : .default) }, openPeer: openPeer, openStats: openStats, openGift: openGift), navigationBarAppearance: .none, statusBarStyle: .ignore, theme: forceDark ? .dark : .default)
self.navigationPresentation = .flatModal self.navigationPresentation = .flatModal
@ -1721,7 +1823,7 @@ public class PremiumLimitScreen: ViewControllerComponentContainer {
public func updateSubject(_ subject: Subject, count: Int32) { public func updateSubject(_ subject: Subject, count: Int32) {
let component = LimitSheetComponent(context: self.context, subject: subject, count: count, cancel: {}, action: { let component = LimitSheetComponent(context: self.context, subject: subject, count: count, cancel: {}, action: {
return true return true
}, openPeer: self.openPeer, openStats: nil) }, openPeer: self.openPeer, openStats: nil, openGift: nil)
self.updateComponent(component: AnyComponent(component), transition: .easeInOut(duration: 0.2)) self.updateComponent(component: AnyComponent(component), transition: .easeInOut(duration: 0.2))
self.hapticFeedback.impact() self.hapticFeedback.impact()
@ -1734,11 +1836,13 @@ private final class PeerShortcutComponent: Component {
let context: AccountContext let context: AccountContext
let theme: PresentationTheme let theme: PresentationTheme
let peer: EnginePeer let peer: EnginePeer
let badge: String?
init(context: AccountContext, theme: PresentationTheme, peer: EnginePeer) { init(context: AccountContext, theme: PresentationTheme, peer: EnginePeer, badge: String?) {
self.context = context self.context = context
self.theme = theme self.theme = theme
self.peer = peer self.peer = peer
self.badge = badge
} }
static func ==(lhs: PeerShortcutComponent, rhs: PeerShortcutComponent) -> Bool { static func ==(lhs: PeerShortcutComponent, rhs: PeerShortcutComponent) -> Bool {
@ -1751,13 +1855,20 @@ private final class PeerShortcutComponent: Component {
if lhs.peer != rhs.peer { if lhs.peer != rhs.peer {
return false return false
} }
if lhs.badge != rhs.badge {
return false
}
return true return true
} }
final class View: UIView { final class View: UIView {
private let backgroundView = UIView()
private let avatarNode: AvatarNode private let avatarNode: AvatarNode
private let text = ComponentView<Empty>() private let text = ComponentView<Empty>()
private let badgeBackground = UIView()
private let badgeText = ComponentView<Empty>()
private var component: PeerShortcutComponent? private var component: PeerShortcutComponent?
private weak var state: EmptyComponentState? private weak var state: EmptyComponentState?
@ -1766,10 +1877,15 @@ private final class PeerShortcutComponent: Component {
super.init(frame: frame) super.init(frame: frame)
self.clipsToBounds = true self.backgroundView.clipsToBounds = true
self.layer.cornerRadius = 16.0 self.backgroundView.layer.cornerRadius = 16.0
self.badgeBackground.clipsToBounds = true
self.badgeBackground.backgroundColor = UIColor(rgb: 0x9671ff)
self.addSubview(self.backgroundView)
self.addSubnode(self.avatarNode) self.addSubnode(self.avatarNode)
self.addSubview(self.badgeBackground)
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -1780,7 +1896,7 @@ private final class PeerShortcutComponent: Component {
self.component = component self.component = component
self.state = state self.state = state
self.backgroundColor = component.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3) self.backgroundView.backgroundColor = component.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3)
self.avatarNode.frame = CGRect(origin: CGPoint(x: 1.0, y: 1.0), size: CGSize(width: 30.0, height: 30.0)) self.avatarNode.frame = CGRect(origin: CGPoint(x: 1.0, y: 1.0), size: CGSize(width: 30.0, height: 30.0))
self.avatarNode.setPeer( self.avatarNode.setPeer(
@ -1810,6 +1926,40 @@ private final class PeerShortcutComponent: Component {
view.frame = textFrame view.frame = textFrame
} }
if let badge = component.badge {
let badgeSize = self.badgeText.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: badge, font: Font.with(size: 11.0, design: .round, weight: .bold), textColor: .white, paragraphAlignment: .left))
)
),
environment: {},
containerSize: availableSize
)
if let view = self.badgeText.view {
if view.superview == nil {
self.addSubview(view)
}
let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(size.width - badgeSize.width / 2.0), y: -2.0), size: badgeSize)
view.frame = textFrame
let backgroundFrame = textFrame.insetBy(dx: -5.0, dy: -3.0)
self.badgeBackground.frame = backgroundFrame
self.badgeBackground.layer.cornerRadius = backgroundFrame.height / 2.0
self.badgeBackground.isHidden = false
}
} else {
if let view = self.badgeText.view {
view.removeFromSuperview()
}
self.badgeBackground.isHidden = true
}
self.backgroundView.frame = CGRect(origin: .zero, size: size)
return size return size
} }
} }

View File

@ -0,0 +1,260 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import LegacyComponents
import ItemListUI
import PresentationDataUtils
final class SubscriptionsCountItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let strings: PresentationStrings
let text: String
let value: Int32
let range: Range<Int32>?
let sectionId: ItemListSectionId
let updated: (Int32) -> Void
init(theme: PresentationTheme, strings: PresentationStrings, text: String, value: Int32, range: Range<Int32>?, sectionId: ItemListSectionId, updated: @escaping (Int32) -> Void) {
self.theme = theme
self.strings = strings
self.text = text
self.value = value
self.range = range
self.sectionId = sectionId
self.updated = updated
}
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) {
async {
let node = SubscriptionsCountItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? SubscriptionsCountItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
private final class SubscriptionsCountItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let minTextNode: TextNode
private let maxTextNode: TextNode
private let textNode: TextNode
private var sliderView: TGPhotoEditorSliderView?
private var item: SubscriptionsCountItem?
private var layoutParams: ListViewItemLayoutParams?
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = false
self.minTextNode = TextNode()
self.minTextNode.isUserInteractionEnabled = false
self.minTextNode.displaysAsynchronously = false
self.maxTextNode = TextNode()
self.maxTextNode.isUserInteractionEnabled = false
self.maxTextNode.displaysAsynchronously = false
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.textNode)
self.addSubnode(self.minTextNode)
self.addSubnode(self.maxTextNode)
}
override func didLoad() {
super.didLoad()
let sliderView = TGPhotoEditorSliderView()
sliderView.enablePanHandling = true
sliderView.trackCornerRadius = 2.0
sliderView.lineSize = 4.0
sliderView.dotSize = 5.0
sliderView.minimumValue = 0.0
sliderView.maximumValue = 10.0
sliderView.startValue = 0.0
sliderView.disablesInteractiveTransitionGestureRecognizer = true
if let item = self.item, let params = self.layoutParams {
sliderView.value = CGFloat(item.value)
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.itemSwitchColors.frameColor
sliderView.startColor = item.theme.list.itemSwitchColors.frameColor
sliderView.trackColor = item.theme.list.itemAccentColor
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme)
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 37.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0))
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
}
self.view.addSubview(sliderView)
sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
self.sliderView = sliderView
}
func asyncLayout() -> (_ item: SubscriptionsCountItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
let makeTextLayout = TextNode.asyncLayout(self.textNode)
let makeMinTextLayout = TextNode.asyncLayout(self.minTextNode)
let makeMaxTextLayout = TextNode.asyncLayout(self.maxTextNode)
return { item, params, neighbors in
var themeUpdated = false
if currentItem?.theme !== item.theme {
themeUpdated = true
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let range = item.range ?? (1 ..< 11)
let (minTextLayout, minTextApply) = makeMinTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "\(range.lowerBound)", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let (maxTextLayout, maxTextApply) = makeMaxTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "\(range.upperBound - 1)", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
contentSize = CGSize(width: params.width, height: 88.0)
insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = 0.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
let _ = textApply()
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floor((params.width - textLayout.size.width) / 2.0), y: 12.0), size: textLayout.size)
let _ = minTextApply()
strongSelf.minTextNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 16.0, y: 16.0), size: minTextLayout.size)
let _ = maxTextApply()
strongSelf.maxTextNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 16.0 - maxTextLayout.size.width, y: 16.0), size: maxTextLayout.size)
if let sliderView = strongSelf.sliderView {
if themeUpdated {
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.itemSwitchColors.frameColor
sliderView.trackColor = item.theme.list.itemAccentColor
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme)
}
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 37.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0))
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
}
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
@objc func sliderValueChanged() {
guard let sliderView = self.sliderView else {
return
}
self.item?.updated(Int32(sliderView.value))
}
}

View File

@ -5,7 +5,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "LegacyReachability", name: "LegacyReachability",
platforms: [.macOS(.v10_12)], platforms: [.macOS(.v10_13)],
products: [ products: [
// Products define the executables and libraries a package produces, and make them visible to other packages. // Products define the executables and libraries a package produces, and make them visible to other packages.
.library( .library(

View File

@ -5,7 +5,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "Reachability", name: "Reachability",
platforms: [.macOS(.v10_12)], platforms: [.macOS(.v10_13)],
products: [ products: [
// Products define the executables and libraries a package produces, and make them visible to other packages. // Products define the executables and libraries a package produces, and make them visible to other packages.
.library( .library(

View File

@ -5,7 +5,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "SSignalKit", name: "SSignalKit",
platforms: [.macOS(.v10_12)], platforms: [.macOS(.v10_13)],
products: [ products: [
// Products define the executables and libraries a package produces, and make them visible to other packages. // Products define the executables and libraries a package produces, and make them visible to other packages.
.library( .library(

View File

@ -20,6 +20,7 @@ import InviteLinksUI
import UndoUI import UndoUI
import ShareController import ShareController
import ItemListPeerActionItem import ItemListPeerActionItem
import PremiumUI
private let maxUsersDisplayedLimit: Int32 = 50 private let maxUsersDisplayedLimit: Int32 = 50
@ -32,8 +33,9 @@ private final class ChannelStatsControllerArguments {
let shareBoostLink: (String) -> Void let shareBoostLink: (String) -> Void
let openPeer: (EnginePeer) -> Void let openPeer: (EnginePeer) -> Void
let expandBoosters: () -> Void let expandBoosters: () -> Void
let openGifts: () -> 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) { 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) {
self.context = context self.context = context
self.loadDetailedGraph = loadDetailedGraph self.loadDetailedGraph = loadDetailedGraph
self.openMessageStats = openMessage self.openMessageStats = openMessage
@ -42,6 +44,7 @@ private final class ChannelStatsControllerArguments {
self.shareBoostLink = shareBoostLink self.shareBoostLink = shareBoostLink
self.openPeer = openPeer self.openPeer = openPeer
self.expandBoosters = expandBoosters self.expandBoosters = expandBoosters
self.openGifts = openGifts
} }
} }
@ -61,6 +64,7 @@ private enum StatsSection: Int32 {
case boostOverview case boostOverview
case boosters case boosters
case boostLink case boostLink
case gifts
} }
private enum StatsEntry: ItemListNodeEntry { private enum StatsEntry: ItemListNodeEntry {
@ -112,6 +116,9 @@ private enum StatsEntry: ItemListNodeEntry {
case boostLink(PresentationTheme, String) case boostLink(PresentationTheme, String)
case boostLinkInfo(PresentationTheme, String) case boostLinkInfo(PresentationTheme, String)
case gifts(PresentationTheme, String)
case giftsInfo(PresentationTheme, String)
var section: ItemListSectionId { var section: ItemListSectionId {
switch self { switch self {
case .overviewTitle, .overview: case .overviewTitle, .overview:
@ -140,10 +147,12 @@ private enum StatsEntry: ItemListNodeEntry {
return StatsSection.boostLevel.rawValue return StatsSection.boostLevel.rawValue
case .boostOverviewTitle, .boostOverview: case .boostOverviewTitle, .boostOverview:
return StatsSection.boostOverview.rawValue return StatsSection.boostOverview.rawValue
case .boostersTitle, .boostersPlaceholder, .booster, .boostersExpand, .boostersInfo: case .boostersTitle, .boostersPlaceholder, .booster, .boostersExpand, .boostersInfo:
return StatsSection.boosters.rawValue return StatsSection.boosters.rawValue
case .boostLinkTitle, .boostLink, .boostLinkInfo: case .boostLinkTitle, .boostLink, .boostLinkInfo:
return StatsSection.boostLink.rawValue return StatsSection.boostLink.rawValue
case .gifts, .giftsInfo:
return StatsSection.gifts.rawValue
} }
} }
@ -215,6 +224,10 @@ private enum StatsEntry: ItemListNodeEntry {
return 10003 return 10003
case .boostLinkInfo: case .boostLinkInfo:
return 10004 return 10004
case .gifts:
return 10005
case .giftsInfo:
return 10006
} }
} }
@ -418,6 +431,18 @@ private enum StatsEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .gifts(lhsTheme, lhsText):
if case let .gifts(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .giftsInfo(lhsTheme, lhsText):
if case let .giftsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
} }
} }
@ -445,7 +470,8 @@ private enum StatsEntry: ItemListNodeEntry {
let .boostLinkTitle(_, text): let .boostLinkTitle(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .boostersInfo(_, text), case let .boostersInfo(_, text),
let .boostLinkInfo(_, text): let .boostLinkInfo(_, text),
let .giftsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
case let .overview(_, stats): case let .overview(_, stats):
return StatsOverviewItem(presentationData: presentationData, stats: stats, sectionId: self.section, style: .blocks) return StatsOverviewItem(presentationData: presentationData, stats: stats, sectionId: self.section, style: .blocks)
@ -496,6 +522,10 @@ private enum StatsEntry: ItemListNodeEntry {
}, contextAction: nil, viewAction: nil, tag: nil) }, contextAction: nil, viewAction: nil, tag: nil)
case let .boostersPlaceholder(_, text): case let .boostersPlaceholder(_, text):
return ItemListPlaceholderItem(theme: presentationData.theme, text: text, sectionId: self.section, style: .blocks) return ItemListPlaceholderItem(theme: presentationData.theme, text: text, sectionId: self.section, style: .blocks)
case let .gifts(theme, title):
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(theme), title: title, sectionId: self.section, editing: false, action: {
arguments.openGifts()
})
} }
} }
} }
@ -624,11 +654,11 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p
let boostersPlaceholder: String? let boostersPlaceholder: String?
let boostersFooter: String? let boostersFooter: String?
if let boostersState, boostersState.count > 0 { if let boostersState, boostersState.count > 0 {
boostersTitle = presentationData.strings.Stats_Boosts_Boosters(boostersState.count) boostersTitle = presentationData.strings.Stats_Boosts_Boosts(boostersState.count)
boostersPlaceholder = nil boostersPlaceholder = nil
boostersFooter = presentationData.strings.Stats_Boosts_BoostersInfo boostersFooter = presentationData.strings.Stats_Boosts_BoostersInfo
} else { } else {
boostersTitle = presentationData.strings.Stats_Boosts_BoostersNone boostersTitle = presentationData.strings.Stats_Boosts_BoostsNone
boostersPlaceholder = presentationData.strings.Stats_Boosts_NoBoostersYet boostersPlaceholder = presentationData.strings.Stats_Boosts_NoBoostersYet
boostersFooter = nil boostersFooter = nil
} }
@ -664,10 +694,11 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p
} }
entries.append(.boostLinkTitle(presentationData.theme, presentationData.strings.Stats_Boosts_LinkHeader)) entries.append(.boostLinkTitle(presentationData.theme, presentationData.strings.Stats_Boosts_LinkHeader))
entries.append(.boostLink(presentationData.theme, boostData.url)) entries.append(.boostLink(presentationData.theme, boostData.url))
entries.append(.boostLinkInfo(presentationData.theme, presentationData.strings.Stats_Boosts_LinkInfo)) entries.append(.boostLinkInfo(presentationData.theme, presentationData.strings.Stats_Boosts_LinkInfo))
entries.append(.gifts(presentationData.theme, "Get Boosts via Gifts"))
entries.append(.giftsInfo(presentationData.theme, "Get more boosts for your channel by gifting Premium to your subscribers."))
} }
} }
@ -718,6 +749,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
let boostersContext = ChannelBoostersContext(account: context.account, peerId: peerId) let boostersContext = ChannelBoostersContext(account: context.account, peerId: peerId)
var presentImpl: ((ViewController) -> Void)? var presentImpl: ((ViewController) -> Void)?
var pushImpl: ((ViewController) -> Void)?
var navigateToProfileImpl: ((EnginePeer) -> Void)? var navigateToProfileImpl: ((EnginePeer) -> Void)?
let arguments = ChannelStatsControllerArguments(context: context, loadDetailedGraph: { graph, x -> Signal<StatsGraph?, NoError> in let arguments = ChannelStatsControllerArguments(context: context, loadDetailedGraph: { graph, x -> Signal<StatsGraph?, NoError> in
@ -778,6 +810,10 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
}, },
expandBoosters: { expandBoosters: {
updateState { $0.withUpdatedBoostersExpanded(true) } updateState { $0.withUpdatedBoostersExpanded(true) }
},
openGifts: {
let controller = createGiveawayController(context: context, peerId: peerId)
pushImpl?(controller)
}) })
let messageView = context.account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId: peerId, threadId: nil), index: .upperBound, anchorIndex: .upperBound, count: 100, fixedCombinedReadStates: nil) let messageView = context.account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId: peerId, threadId: nil), index: .upperBound, anchorIndex: .upperBound, count: 100, fixedCombinedReadStates: nil)
@ -893,6 +929,9 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
presentImpl = { [weak controller] c in presentImpl = { [weak controller] c in
controller?.present(c, in: .window(.root)) controller?.present(c, in: .window(.root))
} }
pushImpl = { [weak controller] c in
controller?.push(c)
}
navigateToProfileImpl = { [weak controller] peer in navigateToProfileImpl = { [weak controller] peer in
if let navigationController = controller?.navigationController as? NavigationController, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: peer.largeProfileImage != nil, fromChat: false, requestsContext: nil) { if let navigationController = controller?.navigationController as? NavigationController, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: peer.largeProfileImage != nil, fromChat: false, requestsContext: nil) {
navigationController.pushViewController(controller) navigationController.pushViewController(controller)

View File

@ -5,7 +5,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "StringTransliteration", name: "StringTransliteration",
platforms: [.macOS(.v10_12)], platforms: [.macOS(.v10_13)],
products: [ products: [
// Products define the executables and libraries a package produces, and make them visible to other packages. // Products define the executables and libraries a package produces, and make them visible to other packages.
.library( .library(

View File

@ -5,7 +5,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "TelegramApi", name: "TelegramApi",
platforms: [.macOS(.v10_12)], platforms: [.macOS(.v10_13)],
products: [ products: [
// Products define the executables and libraries a package produces, and make them visible to other packages. // Products define the executables and libraries a package produces, and make them visible to other packages.
.library( .library(

View File

@ -405,6 +405,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[70813275] = { return Api.InputStickeredMedia.parse_inputStickeredMediaDocument($0) } dict[70813275] = { return Api.InputStickeredMedia.parse_inputStickeredMediaDocument($0) }
dict[1251549527] = { return Api.InputStickeredMedia.parse_inputStickeredMediaPhoto($0) } dict[1251549527] = { return Api.InputStickeredMedia.parse_inputStickeredMediaPhoto($0) }
dict[1634697192] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentGiftPremium($0) } dict[1634697192] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentGiftPremium($0) }
dict[-1551868097] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiftCode($0) }
dict[-566640558] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiveaway($0) }
dict[-1502273946] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumSubscription($0) } dict[-1502273946] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumSubscription($0) }
dict[1012306921] = { return Api.InputTheme.parse_inputTheme($0) } dict[1012306921] = { return Api.InputTheme.parse_inputTheme($0) }
dict[-175567375] = { return Api.InputTheme.parse_inputThemeSlug($0) } dict[-175567375] = { return Api.InputTheme.parse_inputThemeSlug($0) }
@ -477,6 +479,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-1230047312] = { return Api.MessageAction.parse_messageActionEmpty($0) } dict[-1230047312] = { return Api.MessageAction.parse_messageActionEmpty($0) }
dict[-1834538890] = { return Api.MessageAction.parse_messageActionGameScore($0) } dict[-1834538890] = { return Api.MessageAction.parse_messageActionGameScore($0) }
dict[-1730095465] = { return Api.MessageAction.parse_messageActionGeoProximityReached($0) } dict[-1730095465] = { return Api.MessageAction.parse_messageActionGeoProximityReached($0) }
dict[-758129906] = { return Api.MessageAction.parse_messageActionGiftCode($0) }
dict[-935499028] = { return Api.MessageAction.parse_messageActionGiftPremium($0) } dict[-935499028] = { return Api.MessageAction.parse_messageActionGiftPremium($0) }
dict[2047704898] = { return Api.MessageAction.parse_messageActionGroupCall($0) } dict[2047704898] = { return Api.MessageAction.parse_messageActionGroupCall($0) }
dict[-1281329567] = { return Api.MessageAction.parse_messageActionGroupCallScheduled($0) } dict[-1281329567] = { return Api.MessageAction.parse_messageActionGroupCallScheduled($0) }
@ -531,6 +534,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-38694904] = { return Api.MessageMedia.parse_messageMediaGame($0) } dict[-38694904] = { return Api.MessageMedia.parse_messageMediaGame($0) }
dict[1457575028] = { return Api.MessageMedia.parse_messageMediaGeo($0) } dict[1457575028] = { return Api.MessageMedia.parse_messageMediaGeo($0) }
dict[-1186937242] = { return Api.MessageMedia.parse_messageMediaGeoLive($0) } dict[-1186937242] = { return Api.MessageMedia.parse_messageMediaGeoLive($0) }
dict[1202724576] = { return Api.MessageMedia.parse_messageMediaGiveaway($0) }
dict[-156940077] = { return Api.MessageMedia.parse_messageMediaInvoice($0) } dict[-156940077] = { return Api.MessageMedia.parse_messageMediaInvoice($0) }
dict[1766936791] = { return Api.MessageMedia.parse_messageMediaPhoto($0) } dict[1766936791] = { return Api.MessageMedia.parse_messageMediaPhoto($0) }
dict[1272375192] = { return Api.MessageMedia.parse_messageMediaPoll($0) } dict[1272375192] = { return Api.MessageMedia.parse_messageMediaPoll($0) }
@ -655,6 +659,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[2061444128] = { return Api.PollResults.parse_pollResults($0) } dict[2061444128] = { return Api.PollResults.parse_pollResults($0) }
dict[1558266229] = { return Api.PopularContact.parse_popularContact($0) } dict[1558266229] = { return Api.PopularContact.parse_popularContact($0) }
dict[512535275] = { return Api.PostAddress.parse_postAddress($0) } dict[512535275] = { return Api.PostAddress.parse_postAddress($0) }
dict[-713473172] = { return Api.PremiumGiftCodeOption.parse_premiumGiftCodeOption($0) }
dict[1958953753] = { return Api.PremiumGiftOption.parse_premiumGiftOption($0) } dict[1958953753] = { return Api.PremiumGiftOption.parse_premiumGiftOption($0) }
dict[1596792306] = { return Api.PremiumSubscriptionOption.parse_premiumSubscriptionOption($0) } dict[1596792306] = { return Api.PremiumSubscriptionOption.parse_premiumSubscriptionOption($0) }
dict[-1534675103] = { return Api.PrivacyKey.parse_privacyKeyAbout($0) } dict[-1534675103] = { return Api.PrivacyKey.parse_privacyKeyAbout($0) }
@ -1144,6 +1149,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1218005070] = { return Api.messages.VotesList.parse_votesList($0) } dict[1218005070] = { return Api.messages.VotesList.parse_votesList($0) }
dict[-44166467] = { return Api.messages.WebPage.parse_webPage($0) } dict[-44166467] = { return Api.messages.WebPage.parse_webPage($0) }
dict[1042605427] = { return Api.payments.BankCardData.parse_bankCardData($0) } dict[1042605427] = { return Api.payments.BankCardData.parse_bankCardData($0) }
dict[-9426548] = { return Api.payments.CheckedGiftCode.parse_checkedGiftCode($0) }
dict[-1362048039] = { return Api.payments.ExportedInvoice.parse_exportedInvoice($0) } dict[-1362048039] = { return Api.payments.ExportedInvoice.parse_exportedInvoice($0) }
dict[-1610250415] = { return Api.payments.PaymentForm.parse_paymentForm($0) } dict[-1610250415] = { return Api.payments.PaymentForm.parse_paymentForm($0) }
dict[1891958275] = { return Api.payments.PaymentReceipt.parse_paymentReceipt($0) } dict[1891958275] = { return Api.payments.PaymentReceipt.parse_paymentReceipt($0) }
@ -1658,6 +1664,8 @@ public extension Api {
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.PostAddress: case let _1 as Api.PostAddress:
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.PremiumGiftCodeOption:
_1.serialize(buffer, boxed)
case let _1 as Api.PremiumGiftOption: case let _1 as Api.PremiumGiftOption:
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.PremiumSubscriptionOption: case let _1 as Api.PremiumSubscriptionOption:
@ -2022,6 +2030,8 @@ public extension Api {
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.payments.BankCardData: case let _1 as Api.payments.BankCardData:
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.payments.CheckedGiftCode:
_1.serialize(buffer, boxed)
case let _1 as Api.payments.ExportedInvoice: case let _1 as Api.payments.ExportedInvoice:
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.payments.PaymentForm: case let _1 as Api.payments.PaymentForm:

View File

@ -579,6 +579,8 @@ public extension Api {
public extension Api { public extension Api {
indirect enum InputStorePaymentPurpose: TypeConstructorDescription { indirect enum InputStorePaymentPurpose: TypeConstructorDescription {
case inputStorePaymentGiftPremium(userId: Api.InputUser, currency: String, amount: Int64) case inputStorePaymentGiftPremium(userId: Api.InputUser, currency: String, amount: Int64)
case inputStorePaymentPremiumGiftCode(flags: Int32, users: [Api.InputUser], boostPeer: Api.InputPeer?, currency: String, amount: Int64)
case inputStorePaymentPremiumGiveaway(flags: Int32, boostPeer: Api.InputPeer, randomId: Int64, untilDate: Int32, currency: String, amount: Int64)
case inputStorePaymentPremiumSubscription(flags: Int32) case inputStorePaymentPremiumSubscription(flags: Int32)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
@ -591,6 +593,31 @@ public extension Api {
serializeString(currency, buffer: buffer, boxed: false) serializeString(currency, buffer: buffer, boxed: false)
serializeInt64(amount, buffer: buffer, boxed: false) serializeInt64(amount, buffer: buffer, boxed: false)
break break
case .inputStorePaymentPremiumGiftCode(let flags, let users, let boostPeer, let currency, let amount):
if boxed {
buffer.appendInt32(-1551868097)
}
serializeInt32(flags, buffer: buffer, boxed: false)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(users.count))
for item in users {
item.serialize(buffer, true)
}
if Int(flags) & Int(1 << 0) != 0 {boostPeer!.serialize(buffer, true)}
serializeString(currency, buffer: buffer, boxed: false)
serializeInt64(amount, buffer: buffer, boxed: false)
break
case .inputStorePaymentPremiumGiveaway(let flags, let boostPeer, let randomId, let untilDate, let currency, let amount):
if boxed {
buffer.appendInt32(-566640558)
}
serializeInt32(flags, buffer: buffer, boxed: false)
boostPeer.serialize(buffer, true)
serializeInt64(randomId, buffer: buffer, boxed: false)
serializeInt32(untilDate, buffer: buffer, boxed: false)
serializeString(currency, buffer: buffer, boxed: false)
serializeInt64(amount, buffer: buffer, boxed: false)
break
case .inputStorePaymentPremiumSubscription(let flags): case .inputStorePaymentPremiumSubscription(let flags):
if boxed { if boxed {
buffer.appendInt32(-1502273946) buffer.appendInt32(-1502273946)
@ -604,6 +631,10 @@ public extension Api {
switch self { switch self {
case .inputStorePaymentGiftPremium(let userId, let currency, let amount): case .inputStorePaymentGiftPremium(let userId, let currency, let amount):
return ("inputStorePaymentGiftPremium", [("userId", userId as Any), ("currency", currency as Any), ("amount", amount as Any)]) return ("inputStorePaymentGiftPremium", [("userId", userId as Any), ("currency", currency as Any), ("amount", amount as Any)])
case .inputStorePaymentPremiumGiftCode(let flags, let users, let boostPeer, let currency, let amount):
return ("inputStorePaymentPremiumGiftCode", [("flags", flags as Any), ("users", users as Any), ("boostPeer", boostPeer as Any), ("currency", currency as Any), ("amount", amount as Any)])
case .inputStorePaymentPremiumGiveaway(let flags, let boostPeer, let randomId, let untilDate, let currency, let amount):
return ("inputStorePaymentPremiumGiveaway", [("flags", flags as Any), ("boostPeer", boostPeer as Any), ("randomId", randomId as Any), ("untilDate", untilDate as Any), ("currency", currency as Any), ("amount", amount as Any)])
case .inputStorePaymentPremiumSubscription(let flags): case .inputStorePaymentPremiumSubscription(let flags):
return ("inputStorePaymentPremiumSubscription", [("flags", flags as Any)]) return ("inputStorePaymentPremiumSubscription", [("flags", flags as Any)])
} }
@ -628,6 +659,61 @@ public extension Api {
return nil return nil
} }
} }
public static func parse_inputStorePaymentPremiumGiftCode(_ reader: BufferReader) -> InputStorePaymentPurpose? {
var _1: Int32?
_1 = reader.readInt32()
var _2: [Api.InputUser]?
if let _ = reader.readInt32() {
_2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputUser.self)
}
var _3: Api.InputPeer?
if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() {
_3 = Api.parse(reader, signature: signature) as? Api.InputPeer
} }
var _4: String?
_4 = parseString(reader)
var _5: Int64?
_5 = reader.readInt64()
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil
let _c4 = _4 != nil
let _c5 = _5 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 {
return Api.InputStorePaymentPurpose.inputStorePaymentPremiumGiftCode(flags: _1!, users: _2!, boostPeer: _3, currency: _4!, amount: _5!)
}
else {
return nil
}
}
public static func parse_inputStorePaymentPremiumGiveaway(_ reader: BufferReader) -> InputStorePaymentPurpose? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Api.InputPeer?
if let signature = reader.readInt32() {
_2 = Api.parse(reader, signature: signature) as? Api.InputPeer
}
var _3: Int64?
_3 = reader.readInt64()
var _4: Int32?
_4 = reader.readInt32()
var _5: String?
_5 = parseString(reader)
var _6: Int64?
_6 = reader.readInt64()
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = _4 != nil
let _c5 = _5 != nil
let _c6 = _6 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 {
return Api.InputStorePaymentPurpose.inputStorePaymentPremiumGiveaway(flags: _1!, boostPeer: _2!, randomId: _3!, untilDate: _4!, currency: _5!, amount: _6!)
}
else {
return nil
}
}
public static func parse_inputStorePaymentPremiumSubscription(_ reader: BufferReader) -> InputStorePaymentPurpose? { public static func parse_inputStorePaymentPremiumSubscription(_ reader: BufferReader) -> InputStorePaymentPurpose? {
var _1: Int32? var _1: Int32?
_1 = reader.readInt32() _1 = reader.readInt32()
@ -948,57 +1034,3 @@ public extension Api {
} }
} }
public extension Api {
enum InputWebDocument: TypeConstructorDescription {
case inputWebDocument(url: String, size: Int32, mimeType: String, attributes: [Api.DocumentAttribute])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .inputWebDocument(let url, let size, let mimeType, let attributes):
if boxed {
buffer.appendInt32(-1678949555)
}
serializeString(url, buffer: buffer, boxed: false)
serializeInt32(size, buffer: buffer, boxed: false)
serializeString(mimeType, buffer: buffer, boxed: false)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(attributes.count))
for item in attributes {
item.serialize(buffer, true)
}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .inputWebDocument(let url, let size, let mimeType, let attributes):
return ("inputWebDocument", [("url", url as Any), ("size", size as Any), ("mimeType", mimeType as Any), ("attributes", attributes as Any)])
}
}
public static func parse_inputWebDocument(_ reader: BufferReader) -> InputWebDocument? {
var _1: String?
_1 = parseString(reader)
var _2: Int32?
_2 = reader.readInt32()
var _3: String?
_3 = parseString(reader)
var _4: [Api.DocumentAttribute]?
if let _ = reader.readInt32() {
_4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.DocumentAttribute.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = _4 != nil
if _c1 && _c2 && _c3 && _c4 {
return Api.InputWebDocument.inputWebDocument(url: _1!, size: _2!, mimeType: _3!, attributes: _4!)
}
else {
return nil
}
}
}
}

View File

@ -1,3 +1,57 @@
public extension Api {
enum InputWebDocument: TypeConstructorDescription {
case inputWebDocument(url: String, size: Int32, mimeType: String, attributes: [Api.DocumentAttribute])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .inputWebDocument(let url, let size, let mimeType, let attributes):
if boxed {
buffer.appendInt32(-1678949555)
}
serializeString(url, buffer: buffer, boxed: false)
serializeInt32(size, buffer: buffer, boxed: false)
serializeString(mimeType, buffer: buffer, boxed: false)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(attributes.count))
for item in attributes {
item.serialize(buffer, true)
}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .inputWebDocument(let url, let size, let mimeType, let attributes):
return ("inputWebDocument", [("url", url as Any), ("size", size as Any), ("mimeType", mimeType as Any), ("attributes", attributes as Any)])
}
}
public static func parse_inputWebDocument(_ reader: BufferReader) -> InputWebDocument? {
var _1: String?
_1 = parseString(reader)
var _2: Int32?
_2 = reader.readInt32()
var _3: String?
_3 = parseString(reader)
var _4: [Api.DocumentAttribute]?
if let _ = reader.readInt32() {
_4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.DocumentAttribute.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = _4 != nil
if _c1 && _c2 && _c3 && _c4 {
return Api.InputWebDocument.inputWebDocument(url: _1!, size: _2!, mimeType: _3!, attributes: _4!)
}
else {
return nil
}
}
}
}
public extension Api { public extension Api {
enum InputWebFileLocation: TypeConstructorDescription { enum InputWebFileLocation: TypeConstructorDescription {
case inputWebFileAudioAlbumThumbLocation(flags: Int32, document: Api.InputDocument?, title: String?, performer: String?) case inputWebFileAudioAlbumThumbLocation(flags: Int32, document: Api.InputDocument?, title: String?, performer: String?)
@ -1008,111 +1062,3 @@ public extension Api {
} }
} }
public extension Api {
enum LangPackString: TypeConstructorDescription {
case langPackString(key: String, value: String)
case langPackStringDeleted(key: String)
case langPackStringPluralized(flags: Int32, key: String, zeroValue: String?, oneValue: String?, twoValue: String?, fewValue: String?, manyValue: String?, otherValue: String)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .langPackString(let key, let value):
if boxed {
buffer.appendInt32(-892239370)
}
serializeString(key, buffer: buffer, boxed: false)
serializeString(value, buffer: buffer, boxed: false)
break
case .langPackStringDeleted(let key):
if boxed {
buffer.appendInt32(695856818)
}
serializeString(key, buffer: buffer, boxed: false)
break
case .langPackStringPluralized(let flags, let key, let zeroValue, let oneValue, let twoValue, let fewValue, let manyValue, let otherValue):
if boxed {
buffer.appendInt32(1816636575)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeString(key, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {serializeString(zeroValue!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 1) != 0 {serializeString(oneValue!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 2) != 0 {serializeString(twoValue!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 3) != 0 {serializeString(fewValue!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 4) != 0 {serializeString(manyValue!, buffer: buffer, boxed: false)}
serializeString(otherValue, buffer: buffer, boxed: false)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .langPackString(let key, let value):
return ("langPackString", [("key", key as Any), ("value", value as Any)])
case .langPackStringDeleted(let key):
return ("langPackStringDeleted", [("key", key as Any)])
case .langPackStringPluralized(let flags, let key, let zeroValue, let oneValue, let twoValue, let fewValue, let manyValue, let otherValue):
return ("langPackStringPluralized", [("flags", flags as Any), ("key", key as Any), ("zeroValue", zeroValue as Any), ("oneValue", oneValue as Any), ("twoValue", twoValue as Any), ("fewValue", fewValue as Any), ("manyValue", manyValue as Any), ("otherValue", otherValue as Any)])
}
}
public static func parse_langPackString(_ reader: BufferReader) -> LangPackString? {
var _1: String?
_1 = parseString(reader)
var _2: String?
_2 = parseString(reader)
let _c1 = _1 != nil
let _c2 = _2 != nil
if _c1 && _c2 {
return Api.LangPackString.langPackString(key: _1!, value: _2!)
}
else {
return nil
}
}
public static func parse_langPackStringDeleted(_ reader: BufferReader) -> LangPackString? {
var _1: String?
_1 = parseString(reader)
let _c1 = _1 != nil
if _c1 {
return Api.LangPackString.langPackStringDeleted(key: _1!)
}
else {
return nil
}
}
public static func parse_langPackStringPluralized(_ reader: BufferReader) -> LangPackString? {
var _1: Int32?
_1 = reader.readInt32()
var _2: String?
_2 = parseString(reader)
var _3: String?
if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) }
var _4: String?
if Int(_1!) & Int(1 << 1) != 0 {_4 = parseString(reader) }
var _5: String?
if Int(_1!) & Int(1 << 2) != 0 {_5 = parseString(reader) }
var _6: String?
if Int(_1!) & Int(1 << 3) != 0 {_6 = parseString(reader) }
var _7: String?
if Int(_1!) & Int(1 << 4) != 0 {_7 = parseString(reader) }
var _8: String?
_8 = parseString(reader)
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil
let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil
let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil
let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil
let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil
let _c8 = _8 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 {
return Api.LangPackString.langPackStringPluralized(flags: _1!, key: _2!, zeroValue: _3, oneValue: _4, twoValue: _5, fewValue: _6, manyValue: _7, otherValue: _8!)
}
else {
return nil
}
}
}
}

View File

@ -1,3 +1,111 @@
public extension Api {
enum LangPackString: TypeConstructorDescription {
case langPackString(key: String, value: String)
case langPackStringDeleted(key: String)
case langPackStringPluralized(flags: Int32, key: String, zeroValue: String?, oneValue: String?, twoValue: String?, fewValue: String?, manyValue: String?, otherValue: String)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .langPackString(let key, let value):
if boxed {
buffer.appendInt32(-892239370)
}
serializeString(key, buffer: buffer, boxed: false)
serializeString(value, buffer: buffer, boxed: false)
break
case .langPackStringDeleted(let key):
if boxed {
buffer.appendInt32(695856818)
}
serializeString(key, buffer: buffer, boxed: false)
break
case .langPackStringPluralized(let flags, let key, let zeroValue, let oneValue, let twoValue, let fewValue, let manyValue, let otherValue):
if boxed {
buffer.appendInt32(1816636575)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeString(key, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {serializeString(zeroValue!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 1) != 0 {serializeString(oneValue!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 2) != 0 {serializeString(twoValue!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 3) != 0 {serializeString(fewValue!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 4) != 0 {serializeString(manyValue!, buffer: buffer, boxed: false)}
serializeString(otherValue, buffer: buffer, boxed: false)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .langPackString(let key, let value):
return ("langPackString", [("key", key as Any), ("value", value as Any)])
case .langPackStringDeleted(let key):
return ("langPackStringDeleted", [("key", key as Any)])
case .langPackStringPluralized(let flags, let key, let zeroValue, let oneValue, let twoValue, let fewValue, let manyValue, let otherValue):
return ("langPackStringPluralized", [("flags", flags as Any), ("key", key as Any), ("zeroValue", zeroValue as Any), ("oneValue", oneValue as Any), ("twoValue", twoValue as Any), ("fewValue", fewValue as Any), ("manyValue", manyValue as Any), ("otherValue", otherValue as Any)])
}
}
public static func parse_langPackString(_ reader: BufferReader) -> LangPackString? {
var _1: String?
_1 = parseString(reader)
var _2: String?
_2 = parseString(reader)
let _c1 = _1 != nil
let _c2 = _2 != nil
if _c1 && _c2 {
return Api.LangPackString.langPackString(key: _1!, value: _2!)
}
else {
return nil
}
}
public static func parse_langPackStringDeleted(_ reader: BufferReader) -> LangPackString? {
var _1: String?
_1 = parseString(reader)
let _c1 = _1 != nil
if _c1 {
return Api.LangPackString.langPackStringDeleted(key: _1!)
}
else {
return nil
}
}
public static func parse_langPackStringPluralized(_ reader: BufferReader) -> LangPackString? {
var _1: Int32?
_1 = reader.readInt32()
var _2: String?
_2 = parseString(reader)
var _3: String?
if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) }
var _4: String?
if Int(_1!) & Int(1 << 1) != 0 {_4 = parseString(reader) }
var _5: String?
if Int(_1!) & Int(1 << 2) != 0 {_5 = parseString(reader) }
var _6: String?
if Int(_1!) & Int(1 << 3) != 0 {_6 = parseString(reader) }
var _7: String?
if Int(_1!) & Int(1 << 4) != 0 {_7 = parseString(reader) }
var _8: String?
_8 = parseString(reader)
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil
let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil
let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil
let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil
let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil
let _c8 = _8 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 {
return Api.LangPackString.langPackStringPluralized(flags: _1!, key: _2!, zeroValue: _3, oneValue: _4, twoValue: _5, fewValue: _6, manyValue: _7, otherValue: _8!)
}
else {
return nil
}
}
}
}
public extension Api { public extension Api {
enum MaskCoords: TypeConstructorDescription { enum MaskCoords: TypeConstructorDescription {
case maskCoords(n: Int32, x: Double, y: Double, zoom: Double) case maskCoords(n: Int32, x: Double, y: Double, zoom: Double)
@ -501,6 +609,7 @@ public extension Api {
case messageActionEmpty case messageActionEmpty
case messageActionGameScore(gameId: Int64, score: Int32) case messageActionGameScore(gameId: Int64, score: Int32)
case messageActionGeoProximityReached(fromId: Api.Peer, toId: Api.Peer, distance: Int32) case messageActionGeoProximityReached(fromId: Api.Peer, toId: Api.Peer, distance: Int32)
case messageActionGiftCode(flags: Int32, boostPeer: Api.Peer?, months: Int32, slug: String)
case messageActionGiftPremium(flags: Int32, currency: String, amount: Int64, months: Int32, cryptoCurrency: String?, cryptoAmount: Int64?) case messageActionGiftPremium(flags: Int32, currency: String, amount: Int64, months: Int32, cryptoCurrency: String?, cryptoAmount: Int64?)
case messageActionGroupCall(flags: Int32, call: Api.InputGroupCall, duration: Int32?) case messageActionGroupCall(flags: Int32, call: Api.InputGroupCall, duration: Int32?)
case messageActionGroupCallScheduled(call: Api.InputGroupCall, scheduleDate: Int32) case messageActionGroupCallScheduled(call: Api.InputGroupCall, scheduleDate: Int32)
@ -643,6 +752,15 @@ public extension Api {
toId.serialize(buffer, true) toId.serialize(buffer, true)
serializeInt32(distance, buffer: buffer, boxed: false) serializeInt32(distance, buffer: buffer, boxed: false)
break break
case .messageActionGiftCode(let flags, let boostPeer, let months, let slug):
if boxed {
buffer.appendInt32(-758129906)
}
serializeInt32(flags, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 1) != 0 {boostPeer!.serialize(buffer, true)}
serializeInt32(months, buffer: buffer, boxed: false)
serializeString(slug, buffer: buffer, boxed: false)
break
case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount): case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount):
if boxed { if boxed {
buffer.appendInt32(-935499028) buffer.appendInt32(-935499028)
@ -859,6 +977,8 @@ public extension Api {
return ("messageActionGameScore", [("gameId", gameId as Any), ("score", score as Any)]) return ("messageActionGameScore", [("gameId", gameId as Any), ("score", score as Any)])
case .messageActionGeoProximityReached(let fromId, let toId, let distance): case .messageActionGeoProximityReached(let fromId, let toId, let distance):
return ("messageActionGeoProximityReached", [("fromId", fromId as Any), ("toId", toId as Any), ("distance", distance as Any)]) return ("messageActionGeoProximityReached", [("fromId", fromId as Any), ("toId", toId as Any), ("distance", distance as Any)])
case .messageActionGiftCode(let flags, let boostPeer, let months, let slug):
return ("messageActionGiftCode", [("flags", flags as Any), ("boostPeer", boostPeer as Any), ("months", months as Any), ("slug", slug as Any)])
case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount): case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount):
return ("messageActionGiftPremium", [("flags", flags as Any), ("currency", currency as Any), ("amount", amount as Any), ("months", months as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any)]) return ("messageActionGiftPremium", [("flags", flags as Any), ("currency", currency as Any), ("amount", amount as Any), ("months", months as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any)])
case .messageActionGroupCall(let flags, let call, let duration): case .messageActionGroupCall(let flags, let call, let duration):
@ -1094,6 +1214,28 @@ public extension Api {
return nil return nil
} }
} }
public static func parse_messageActionGiftCode(_ reader: BufferReader) -> MessageAction? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Api.Peer?
if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() {
_2 = Api.parse(reader, signature: signature) as? Api.Peer
} }
var _3: Int32?
_3 = reader.readInt32()
var _4: String?
_4 = parseString(reader)
let _c1 = _1 != nil
let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil
let _c3 = _3 != nil
let _c4 = _4 != nil
if _c1 && _c2 && _c3 && _c4 {
return Api.MessageAction.messageActionGiftCode(flags: _1!, boostPeer: _2, months: _3!, slug: _4!)
}
else {
return nil
}
}
public static func parse_messageActionGiftPremium(_ reader: BufferReader) -> MessageAction? { public static func parse_messageActionGiftPremium(_ reader: BufferReader) -> MessageAction? {
var _1: Int32? var _1: Int32?
_1 = reader.readInt32() _1 = reader.readInt32()

View File

@ -741,6 +741,7 @@ public extension Api {
case messageMediaGame(game: Api.Game) case messageMediaGame(game: Api.Game)
case messageMediaGeo(geo: Api.GeoPoint) case messageMediaGeo(geo: Api.GeoPoint)
case messageMediaGeoLive(flags: Int32, geo: Api.GeoPoint, heading: Int32?, period: Int32, proximityNotificationRadius: Int32?) case messageMediaGeoLive(flags: Int32, geo: Api.GeoPoint, heading: Int32?, period: Int32, proximityNotificationRadius: Int32?)
case messageMediaGiveaway(channels: [Int64], quantity: Int32, months: Int32, untilDate: Int32)
case messageMediaInvoice(flags: Int32, title: String, description: String, photo: Api.WebDocument?, receiptMsgId: Int32?, currency: String, totalAmount: Int64, startParam: String, extendedMedia: Api.MessageExtendedMedia?) case messageMediaInvoice(flags: Int32, title: String, description: String, photo: Api.WebDocument?, receiptMsgId: Int32?, currency: String, totalAmount: Int64, startParam: String, extendedMedia: Api.MessageExtendedMedia?)
case messageMediaPhoto(flags: Int32, photo: Api.Photo?, ttlSeconds: Int32?) case messageMediaPhoto(flags: Int32, photo: Api.Photo?, ttlSeconds: Int32?)
case messageMediaPoll(poll: Api.Poll, results: Api.PollResults) case messageMediaPoll(poll: Api.Poll, results: Api.PollResults)
@ -805,6 +806,19 @@ public extension Api {
serializeInt32(period, buffer: buffer, boxed: false) serializeInt32(period, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 1) != 0 {serializeInt32(proximityNotificationRadius!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 1) != 0 {serializeInt32(proximityNotificationRadius!, buffer: buffer, boxed: false)}
break break
case .messageMediaGiveaway(let channels, let quantity, let months, let untilDate):
if boxed {
buffer.appendInt32(1202724576)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(channels.count))
for item in channels {
serializeInt64(item, buffer: buffer, boxed: false)
}
serializeInt32(quantity, buffer: buffer, boxed: false)
serializeInt32(months, buffer: buffer, boxed: false)
serializeInt32(untilDate, buffer: buffer, boxed: false)
break
case .messageMediaInvoice(let flags, let title, let description, let photo, let receiptMsgId, let currency, let totalAmount, let startParam, let extendedMedia): case .messageMediaInvoice(let flags, let title, let description, let photo, let receiptMsgId, let currency, let totalAmount, let startParam, let extendedMedia):
if boxed { if boxed {
buffer.appendInt32(-156940077) buffer.appendInt32(-156940077)
@ -885,6 +899,8 @@ public extension Api {
return ("messageMediaGeo", [("geo", geo as Any)]) return ("messageMediaGeo", [("geo", geo as Any)])
case .messageMediaGeoLive(let flags, let geo, let heading, let period, let proximityNotificationRadius): case .messageMediaGeoLive(let flags, let geo, let heading, let period, let proximityNotificationRadius):
return ("messageMediaGeoLive", [("flags", flags as Any), ("geo", geo as Any), ("heading", heading as Any), ("period", period as Any), ("proximityNotificationRadius", proximityNotificationRadius as Any)]) return ("messageMediaGeoLive", [("flags", flags as Any), ("geo", geo as Any), ("heading", heading as Any), ("period", period as Any), ("proximityNotificationRadius", proximityNotificationRadius as Any)])
case .messageMediaGiveaway(let channels, let quantity, let months, let untilDate):
return ("messageMediaGiveaway", [("channels", channels as Any), ("quantity", quantity as Any), ("months", months as Any), ("untilDate", untilDate as Any)])
case .messageMediaInvoice(let flags, let title, let description, let photo, let receiptMsgId, let currency, let totalAmount, let startParam, let extendedMedia): case .messageMediaInvoice(let flags, let title, let description, let photo, let receiptMsgId, let currency, let totalAmount, let startParam, let extendedMedia):
return ("messageMediaInvoice", [("flags", flags as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("receiptMsgId", receiptMsgId as Any), ("currency", currency as Any), ("totalAmount", totalAmount as Any), ("startParam", startParam as Any), ("extendedMedia", extendedMedia as Any)]) return ("messageMediaInvoice", [("flags", flags as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("receiptMsgId", receiptMsgId as Any), ("currency", currency as Any), ("totalAmount", totalAmount as Any), ("startParam", startParam as Any), ("extendedMedia", extendedMedia as Any)])
case .messageMediaPhoto(let flags, let photo, let ttlSeconds): case .messageMediaPhoto(let flags, let photo, let ttlSeconds):
@ -1017,6 +1033,28 @@ public extension Api {
return nil return nil
} }
} }
public static func parse_messageMediaGiveaway(_ reader: BufferReader) -> MessageMedia? {
var _1: [Int64]?
if let _ = reader.readInt32() {
_1 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self)
}
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.MessageMedia.messageMediaGiveaway(channels: _1!, quantity: _2!, months: _3!, untilDate: _4!)
}
else {
return nil
}
}
public static func parse_messageMediaInvoice(_ reader: BufferReader) -> MessageMedia? { public static func parse_messageMediaInvoice(_ reader: BufferReader) -> MessageMedia? {
var _1: Int32? var _1: Int32?
_1 = reader.readInt32() _1 = reader.readInt32()

View File

@ -750,6 +750,54 @@ public extension Api {
} }
} }
public extension Api {
enum PremiumGiftCodeOption: TypeConstructorDescription {
case premiumGiftCodeOption(flags: Int32, users: Int32, months: Int32, storeProduct: String?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .premiumGiftCodeOption(let flags, let users, let months, let storeProduct):
if boxed {
buffer.appendInt32(-713473172)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt32(users, buffer: buffer, boxed: false)
serializeInt32(months, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {serializeString(storeProduct!, buffer: buffer, boxed: false)}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .premiumGiftCodeOption(let flags, let users, let months, let storeProduct):
return ("premiumGiftCodeOption", [("flags", flags as Any), ("users", users as Any), ("months", months as Any), ("storeProduct", storeProduct as Any)])
}
}
public static func parse_premiumGiftCodeOption(_ reader: BufferReader) -> PremiumGiftCodeOption? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Int32?
_2 = reader.readInt32()
var _3: Int32?
_3 = reader.readInt32()
var _4: String?
if Int(_1!) & Int(1 << 0) != 0 {_4 = parseString(reader) }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil
if _c1 && _c2 && _c3 && _c4 {
return Api.PremiumGiftCodeOption.premiumGiftCodeOption(flags: _1!, users: _2!, months: _3!, storeProduct: _4)
}
else {
return nil
}
}
}
}
public extension Api { public extension Api {
enum PremiumGiftOption: TypeConstructorDescription { enum PremiumGiftOption: TypeConstructorDescription {
case premiumGiftOption(flags: Int32, months: Int32, currency: String, amount: Int64, botUrl: String, storeProduct: String?) case premiumGiftOption(flags: Int32, months: Int32, currency: String, amount: Int64, botUrl: String, storeProduct: String?)

View File

@ -536,6 +536,84 @@ public extension Api.payments {
} }
} }
public extension Api.payments {
enum CheckedGiftCode: TypeConstructorDescription {
case checkedGiftCode(flags: Int32, fromId: Api.Peer, toId: Int64?, date: Int32, months: Int32, usedDate: Int32?, chats: [Api.Chat], users: [Api.User])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .checkedGiftCode(let flags, let fromId, let toId, let date, let months, let usedDate, let chats, let users):
if boxed {
buffer.appendInt32(-9426548)
}
serializeInt32(flags, buffer: buffer, boxed: false)
fromId.serialize(buffer, true)
if Int(flags) & Int(1 << 0) != 0 {serializeInt64(toId!, buffer: buffer, boxed: false)}
serializeInt32(date, buffer: buffer, boxed: false)
serializeInt32(months, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 1) != 0 {serializeInt32(usedDate!, buffer: buffer, boxed: false)}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(chats.count))
for item in chats {
item.serialize(buffer, true)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(users.count))
for item in users {
item.serialize(buffer, true)
}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .checkedGiftCode(let flags, let fromId, let toId, let date, let months, let usedDate, let chats, let users):
return ("checkedGiftCode", [("flags", flags as Any), ("fromId", fromId as Any), ("toId", toId as Any), ("date", date as Any), ("months", months as Any), ("usedDate", usedDate as Any), ("chats", chats as Any), ("users", users as Any)])
}
}
public static func parse_checkedGiftCode(_ reader: BufferReader) -> CheckedGiftCode? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Api.Peer?
if let signature = reader.readInt32() {
_2 = Api.parse(reader, signature: signature) as? Api.Peer
}
var _3: Int64?
if Int(_1!) & Int(1 << 0) != 0 {_3 = reader.readInt64() }
var _4: Int32?
_4 = reader.readInt32()
var _5: Int32?
_5 = reader.readInt32()
var _6: Int32?
if Int(_1!) & Int(1 << 1) != 0 {_6 = reader.readInt32() }
var _7: [Api.Chat]?
if let _ = reader.readInt32() {
_7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
}
var _8: [Api.User]?
if let _ = reader.readInt32() {
_8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil
let _c4 = _4 != nil
let _c5 = _5 != nil
let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil
let _c7 = _7 != nil
let _c8 = _8 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 {
return Api.payments.CheckedGiftCode.checkedGiftCode(flags: _1!, fromId: _2!, toId: _3, date: _4!, months: _5!, usedDate: _6, chats: _7!, users: _8!)
}
else {
return nil
}
}
}
}
public extension Api.payments { public extension Api.payments {
enum ExportedInvoice: TypeConstructorDescription { enum ExportedInvoice: TypeConstructorDescription {
case exportedInvoice(url: String) case exportedInvoice(url: String)
@ -1586,153 +1664,3 @@ public extension Api.stats {
} }
} }
public extension Api.stats {
enum MegagroupStats: TypeConstructorDescription {
case megagroupStats(period: Api.StatsDateRangeDays, members: Api.StatsAbsValueAndPrev, messages: Api.StatsAbsValueAndPrev, viewers: Api.StatsAbsValueAndPrev, posters: Api.StatsAbsValueAndPrev, growthGraph: Api.StatsGraph, membersGraph: Api.StatsGraph, newMembersBySourceGraph: Api.StatsGraph, languagesGraph: Api.StatsGraph, messagesGraph: Api.StatsGraph, actionsGraph: Api.StatsGraph, topHoursGraph: Api.StatsGraph, weekdaysGraph: Api.StatsGraph, topPosters: [Api.StatsGroupTopPoster], topAdmins: [Api.StatsGroupTopAdmin], topInviters: [Api.StatsGroupTopInviter], users: [Api.User])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .megagroupStats(let period, let members, let messages, let viewers, let posters, let growthGraph, let membersGraph, let newMembersBySourceGraph, let languagesGraph, let messagesGraph, let actionsGraph, let topHoursGraph, let weekdaysGraph, let topPosters, let topAdmins, let topInviters, let users):
if boxed {
buffer.appendInt32(-276825834)
}
period.serialize(buffer, true)
members.serialize(buffer, true)
messages.serialize(buffer, true)
viewers.serialize(buffer, true)
posters.serialize(buffer, true)
growthGraph.serialize(buffer, true)
membersGraph.serialize(buffer, true)
newMembersBySourceGraph.serialize(buffer, true)
languagesGraph.serialize(buffer, true)
messagesGraph.serialize(buffer, true)
actionsGraph.serialize(buffer, true)
topHoursGraph.serialize(buffer, true)
weekdaysGraph.serialize(buffer, true)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(topPosters.count))
for item in topPosters {
item.serialize(buffer, true)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(topAdmins.count))
for item in topAdmins {
item.serialize(buffer, true)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(topInviters.count))
for item in topInviters {
item.serialize(buffer, true)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(users.count))
for item in users {
item.serialize(buffer, true)
}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .megagroupStats(let period, let members, let messages, let viewers, let posters, let growthGraph, let membersGraph, let newMembersBySourceGraph, let languagesGraph, let messagesGraph, let actionsGraph, let topHoursGraph, let weekdaysGraph, let topPosters, let topAdmins, let topInviters, let users):
return ("megagroupStats", [("period", period as Any), ("members", members as Any), ("messages", messages as Any), ("viewers", viewers as Any), ("posters", posters as Any), ("growthGraph", growthGraph as Any), ("membersGraph", membersGraph as Any), ("newMembersBySourceGraph", newMembersBySourceGraph as Any), ("languagesGraph", languagesGraph as Any), ("messagesGraph", messagesGraph as Any), ("actionsGraph", actionsGraph as Any), ("topHoursGraph", topHoursGraph as Any), ("weekdaysGraph", weekdaysGraph as Any), ("topPosters", topPosters as Any), ("topAdmins", topAdmins as Any), ("topInviters", topInviters as Any), ("users", users as Any)])
}
}
public static func parse_megagroupStats(_ reader: BufferReader) -> MegagroupStats? {
var _1: Api.StatsDateRangeDays?
if let signature = reader.readInt32() {
_1 = Api.parse(reader, signature: signature) as? Api.StatsDateRangeDays
}
var _2: Api.StatsAbsValueAndPrev?
if let signature = reader.readInt32() {
_2 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev
}
var _3: Api.StatsAbsValueAndPrev?
if let signature = reader.readInt32() {
_3 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev
}
var _4: Api.StatsAbsValueAndPrev?
if let signature = reader.readInt32() {
_4 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev
}
var _5: Api.StatsAbsValueAndPrev?
if let signature = reader.readInt32() {
_5 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev
}
var _6: Api.StatsGraph?
if let signature = reader.readInt32() {
_6 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _7: Api.StatsGraph?
if let signature = reader.readInt32() {
_7 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _8: Api.StatsGraph?
if let signature = reader.readInt32() {
_8 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _9: Api.StatsGraph?
if let signature = reader.readInt32() {
_9 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _10: Api.StatsGraph?
if let signature = reader.readInt32() {
_10 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _11: Api.StatsGraph?
if let signature = reader.readInt32() {
_11 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _12: Api.StatsGraph?
if let signature = reader.readInt32() {
_12 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _13: Api.StatsGraph?
if let signature = reader.readInt32() {
_13 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _14: [Api.StatsGroupTopPoster]?
if let _ = reader.readInt32() {
_14 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StatsGroupTopPoster.self)
}
var _15: [Api.StatsGroupTopAdmin]?
if let _ = reader.readInt32() {
_15 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StatsGroupTopAdmin.self)
}
var _16: [Api.StatsGroupTopInviter]?
if let _ = reader.readInt32() {
_16 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StatsGroupTopInviter.self)
}
var _17: [Api.User]?
if let _ = reader.readInt32() {
_17 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = _4 != nil
let _c5 = _5 != nil
let _c6 = _6 != nil
let _c7 = _7 != nil
let _c8 = _8 != nil
let _c9 = _9 != nil
let _c10 = _10 != nil
let _c11 = _11 != nil
let _c12 = _12 != nil
let _c13 = _13 != nil
let _c14 = _14 != nil
let _c15 = _15 != nil
let _c16 = _16 != nil
let _c17 = _17 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 {
return Api.stats.MegagroupStats.megagroupStats(period: _1!, members: _2!, messages: _3!, viewers: _4!, posters: _5!, growthGraph: _6!, membersGraph: _7!, newMembersBySourceGraph: _8!, languagesGraph: _9!, messagesGraph: _10!, actionsGraph: _11!, topHoursGraph: _12!, weekdaysGraph: _13!, topPosters: _14!, topAdmins: _15!, topInviters: _16!, users: _17!)
}
else {
return nil
}
}
}
}

View File

@ -1,3 +1,153 @@
public extension Api.stats {
enum MegagroupStats: TypeConstructorDescription {
case megagroupStats(period: Api.StatsDateRangeDays, members: Api.StatsAbsValueAndPrev, messages: Api.StatsAbsValueAndPrev, viewers: Api.StatsAbsValueAndPrev, posters: Api.StatsAbsValueAndPrev, growthGraph: Api.StatsGraph, membersGraph: Api.StatsGraph, newMembersBySourceGraph: Api.StatsGraph, languagesGraph: Api.StatsGraph, messagesGraph: Api.StatsGraph, actionsGraph: Api.StatsGraph, topHoursGraph: Api.StatsGraph, weekdaysGraph: Api.StatsGraph, topPosters: [Api.StatsGroupTopPoster], topAdmins: [Api.StatsGroupTopAdmin], topInviters: [Api.StatsGroupTopInviter], users: [Api.User])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .megagroupStats(let period, let members, let messages, let viewers, let posters, let growthGraph, let membersGraph, let newMembersBySourceGraph, let languagesGraph, let messagesGraph, let actionsGraph, let topHoursGraph, let weekdaysGraph, let topPosters, let topAdmins, let topInviters, let users):
if boxed {
buffer.appendInt32(-276825834)
}
period.serialize(buffer, true)
members.serialize(buffer, true)
messages.serialize(buffer, true)
viewers.serialize(buffer, true)
posters.serialize(buffer, true)
growthGraph.serialize(buffer, true)
membersGraph.serialize(buffer, true)
newMembersBySourceGraph.serialize(buffer, true)
languagesGraph.serialize(buffer, true)
messagesGraph.serialize(buffer, true)
actionsGraph.serialize(buffer, true)
topHoursGraph.serialize(buffer, true)
weekdaysGraph.serialize(buffer, true)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(topPosters.count))
for item in topPosters {
item.serialize(buffer, true)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(topAdmins.count))
for item in topAdmins {
item.serialize(buffer, true)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(topInviters.count))
for item in topInviters {
item.serialize(buffer, true)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(users.count))
for item in users {
item.serialize(buffer, true)
}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .megagroupStats(let period, let members, let messages, let viewers, let posters, let growthGraph, let membersGraph, let newMembersBySourceGraph, let languagesGraph, let messagesGraph, let actionsGraph, let topHoursGraph, let weekdaysGraph, let topPosters, let topAdmins, let topInviters, let users):
return ("megagroupStats", [("period", period as Any), ("members", members as Any), ("messages", messages as Any), ("viewers", viewers as Any), ("posters", posters as Any), ("growthGraph", growthGraph as Any), ("membersGraph", membersGraph as Any), ("newMembersBySourceGraph", newMembersBySourceGraph as Any), ("languagesGraph", languagesGraph as Any), ("messagesGraph", messagesGraph as Any), ("actionsGraph", actionsGraph as Any), ("topHoursGraph", topHoursGraph as Any), ("weekdaysGraph", weekdaysGraph as Any), ("topPosters", topPosters as Any), ("topAdmins", topAdmins as Any), ("topInviters", topInviters as Any), ("users", users as Any)])
}
}
public static func parse_megagroupStats(_ reader: BufferReader) -> MegagroupStats? {
var _1: Api.StatsDateRangeDays?
if let signature = reader.readInt32() {
_1 = Api.parse(reader, signature: signature) as? Api.StatsDateRangeDays
}
var _2: Api.StatsAbsValueAndPrev?
if let signature = reader.readInt32() {
_2 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev
}
var _3: Api.StatsAbsValueAndPrev?
if let signature = reader.readInt32() {
_3 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev
}
var _4: Api.StatsAbsValueAndPrev?
if let signature = reader.readInt32() {
_4 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev
}
var _5: Api.StatsAbsValueAndPrev?
if let signature = reader.readInt32() {
_5 = Api.parse(reader, signature: signature) as? Api.StatsAbsValueAndPrev
}
var _6: Api.StatsGraph?
if let signature = reader.readInt32() {
_6 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _7: Api.StatsGraph?
if let signature = reader.readInt32() {
_7 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _8: Api.StatsGraph?
if let signature = reader.readInt32() {
_8 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _9: Api.StatsGraph?
if let signature = reader.readInt32() {
_9 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _10: Api.StatsGraph?
if let signature = reader.readInt32() {
_10 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _11: Api.StatsGraph?
if let signature = reader.readInt32() {
_11 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _12: Api.StatsGraph?
if let signature = reader.readInt32() {
_12 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _13: Api.StatsGraph?
if let signature = reader.readInt32() {
_13 = Api.parse(reader, signature: signature) as? Api.StatsGraph
}
var _14: [Api.StatsGroupTopPoster]?
if let _ = reader.readInt32() {
_14 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StatsGroupTopPoster.self)
}
var _15: [Api.StatsGroupTopAdmin]?
if let _ = reader.readInt32() {
_15 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StatsGroupTopAdmin.self)
}
var _16: [Api.StatsGroupTopInviter]?
if let _ = reader.readInt32() {
_16 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StatsGroupTopInviter.self)
}
var _17: [Api.User]?
if let _ = reader.readInt32() {
_17 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = _4 != nil
let _c5 = _5 != nil
let _c6 = _6 != nil
let _c7 = _7 != nil
let _c8 = _8 != nil
let _c9 = _9 != nil
let _c10 = _10 != nil
let _c11 = _11 != nil
let _c12 = _12 != nil
let _c13 = _13 != nil
let _c14 = _14 != nil
let _c15 = _15 != nil
let _c16 = _16 != nil
let _c17 = _17 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 {
return Api.stats.MegagroupStats.megagroupStats(period: _1!, members: _2!, messages: _3!, viewers: _4!, posters: _5!, growthGraph: _6!, membersGraph: _7!, newMembersBySourceGraph: _8!, languagesGraph: _9!, messagesGraph: _10!, actionsGraph: _11!, topHoursGraph: _12!, weekdaysGraph: _13!, topPosters: _14!, topAdmins: _15!, topInviters: _16!, users: _17!)
}
else {
return nil
}
}
}
}
public extension Api.stats { public extension Api.stats {
enum MessageStats: TypeConstructorDescription { enum MessageStats: TypeConstructorDescription {
case messageStats(viewsGraph: Api.StatsGraph) case messageStats(viewsGraph: Api.StatsGraph)

View File

@ -7440,6 +7440,21 @@ public extension Api.functions.messages {
}) })
} }
} }
public extension Api.functions.payments {
static func applyGiftCode(slug: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
let buffer = Buffer()
buffer.appendInt32(-152934316)
serializeString(slug, buffer: buffer, boxed: false)
return (FunctionDescription(name: "payments.applyGiftCode", parameters: [("slug", String(describing: slug))]), 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 { public extension Api.functions.payments {
static func assignAppStoreTransaction(receipt: Buffer, purpose: Api.InputStorePaymentPurpose) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) { static func assignAppStoreTransaction(receipt: Buffer, purpose: Api.InputStorePaymentPurpose) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
let buffer = Buffer() let buffer = Buffer()
@ -7487,6 +7502,21 @@ public extension Api.functions.payments {
}) })
} }
} }
public extension Api.functions.payments {
static func checkGiftCode(slug: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.payments.CheckedGiftCode>) {
let buffer = Buffer()
buffer.appendInt32(-1907247935)
serializeString(slug, buffer: buffer, boxed: false)
return (FunctionDescription(name: "payments.checkGiftCode", parameters: [("slug", String(describing: slug))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.CheckedGiftCode? in
let reader = BufferReader(buffer)
var result: Api.payments.CheckedGiftCode?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.payments.CheckedGiftCode
}
return result
})
}
}
public extension Api.functions.payments { public extension Api.functions.payments {
static func clearSavedInfo(flags: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) { static func clearSavedInfo(flags: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer() let buffer = Buffer()
@ -7565,6 +7595,22 @@ public extension Api.functions.payments {
}) })
} }
} }
public extension Api.functions.payments {
static func getPremiumGiftCodeOptions(flags: Int32, boostPeer: Api.InputPeer?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Api.PremiumGiftCodeOption]>) {
let buffer = Buffer()
buffer.appendInt32(660060756)
serializeInt32(flags, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {boostPeer!.serialize(buffer, true)}
return (FunctionDescription(name: "payments.getPremiumGiftCodeOptions", parameters: [("flags", String(describing: flags)), ("boostPeer", String(describing: boostPeer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> [Api.PremiumGiftCodeOption]? in
let reader = BufferReader(buffer)
var result: [Api.PremiumGiftCodeOption]?
if let _ = reader.readInt32() {
result = Api.parseVector(reader, elementSignature: 0, elementType: Api.PremiumGiftCodeOption.self)
}
return result
})
}
}
public extension Api.functions.payments { public extension Api.functions.payments {
static func getSavedInfo() -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.payments.SavedInfo>) { static func getSavedInfo() -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.payments.SavedInfo>) {
let buffer = Buffer() let buffer = Buffer()

View File

@ -5,7 +5,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "TelegramCore", name: "TelegramCore",
platforms: [.macOS(.v10_12)], platforms: [.macOS(.v10_13)],
products: [ products: [
// Products define the executables and libraries a package produces, and make them visible to other packages. // Products define the executables and libraries a package produces, and make them visible to other packages.
.library( .library(

View File

@ -200,6 +200,7 @@ private var declaredEncodables: Void = {
declareEncodable(SynchronizeViewStoriesOperation.self, f: { SynchronizeViewStoriesOperation(decoder: $0) }) declareEncodable(SynchronizeViewStoriesOperation.self, f: { SynchronizeViewStoriesOperation(decoder: $0) })
declareEncodable(SynchronizePeerStoriesOperation.self, f: { SynchronizePeerStoriesOperation(decoder: $0) }) declareEncodable(SynchronizePeerStoriesOperation.self, f: { SynchronizePeerStoriesOperation(decoder: $0) })
declareEncodable(MapVenue.self, f: { MapVenue(decoder: $0) }) declareEncodable(MapVenue.self, f: { MapVenue(decoder: $0) })
declareEncodable(TelegramMediaGiveaway.self, f: { TelegramMediaGiveaway(decoder: $0) })
return return
}() }()

View File

@ -492,7 +492,7 @@ extension ChatContextResultMessage {
if let replyMarkup = replyMarkup { if let replyMarkup = replyMarkup {
parsedReplyMarkup = ReplyMarkupMessageAttribute(apiMarkup: replyMarkup) parsedReplyMarkup = ReplyMarkupMessageAttribute(apiMarkup: replyMarkup)
} }
self = .invoice(media: TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: nil, currency: currency, totalAmount: totalAmount, startParam: "", extendedMedia: nil, flags: parsedFlags, version: TelegramMediaInvoice.lastVersion), replyMarkup: parsedReplyMarkup) self = .invoice(media: TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: nil, currency: currency, totalAmount: totalAmount, startParam: "", extendedMedia: nil, flags: parsedFlags, version: TelegramMediaInvoice.lastVersion), replyMarkup: parsedReplyMarkup)
} }
} }
} }

View File

@ -244,6 +244,10 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] {
} }
case let .messageActionRequestedPeer(_, peer): case let .messageActionRequestedPeer(_, peer):
result.append(peer.peerId) result.append(peer.peerId)
case let .messageActionGiftCode(_, boostPeer, _, _):
if let boostPeer = boostPeer {
result.append(boostPeer.peerId)
}
} }
return result return result
@ -383,6 +387,8 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI
case let .messageMediaStory(flags, peerId, id, _): case let .messageMediaStory(flags, peerId, id, _):
let isMention = (flags & (1 << 1)) != 0 let isMention = (flags & (1 << 1)) != 0
return (TelegramMediaStory(storyId: StoryId(peerId: peerId.peerId, id: id), isMention: isMention), nil, nil, nil) return (TelegramMediaStory(storyId: StoryId(peerId: peerId.peerId, id: id), isMention: isMention), nil, nil, nil)
case let .messageMediaGiveaway(channels, quantity, months, untilDate):
return (TelegramMediaGiveaway(channelPeerIds: channels.map { PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value($0)) }, quantity: quantity, months: months, untilDate: untilDate), nil, nil, nil)
} }
} }

View File

@ -127,6 +127,8 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
return TelegramMediaAction(action: .setChatWallpaper(wallpaper: TelegramWallpaper(apiWallpaper: wallpaper))) return TelegramMediaAction(action: .setChatWallpaper(wallpaper: TelegramWallpaper(apiWallpaper: wallpaper)))
case let .messageActionSetSameChatWallPaper(wallpaper): case let .messageActionSetSameChatWallPaper(wallpaper):
return TelegramMediaAction(action: .setSameChatWallpaper(wallpaper: TelegramWallpaper(apiWallpaper: wallpaper))) return TelegramMediaAction(action: .setSameChatWallpaper(wallpaper: TelegramWallpaper(apiWallpaper: wallpaper)))
case let .messageActionGiftCode(flags, boostPeer, months, slug):
return TelegramMediaAction(action: .giftCode(slug: slug, fromGiveaway: (flags & (1 << 0)) != 0, boostPeerId: boostPeer?.peerId, months: months))
} }
} }

View File

@ -210,7 +210,7 @@ public class BoxedMessage: NSObject {
public class Serialization: NSObject, MTSerialization { public class Serialization: NSObject, MTSerialization {
public func currentLayer() -> UInt { public func currentLayer() -> UInt {
return 165 return 166
} }
public func parseMessage(_ data: Data!) -> Any! { public func parseMessage(_ data: Data!) -> Any! {

View File

@ -109,6 +109,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case requestedPeer(buttonId: Int32, peerId: PeerId) case requestedPeer(buttonId: Int32, peerId: PeerId)
case setChatWallpaper(wallpaper: TelegramWallpaper) case setChatWallpaper(wallpaper: TelegramWallpaper)
case setSameChatWallpaper(wallpaper: TelegramWallpaper) case setSameChatWallpaper(wallpaper: TelegramWallpaper)
case giftCode(slug: String, fromGiveaway: Bool, boostPeerId: PeerId?, months: Int32)
public init(decoder: PostboxDecoder) { public init(decoder: PostboxDecoder) {
let rawValue: Int32 = decoder.decodeInt32ForKey("_rawValue", orElse: 0) let rawValue: Int32 = decoder.decodeInt32ForKey("_rawValue", orElse: 0)
@ -203,6 +204,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
} }
case 35: case 35:
self = .botAppAccessGranted(appName: decoder.decodeOptionalStringForKey("app"), type: decoder.decodeOptionalInt32ForKey("atp").flatMap { BotSendMessageAccessGrantedType(rawValue: $0) }) self = .botAppAccessGranted(appName: decoder.decodeOptionalStringForKey("app"), type: decoder.decodeOptionalInt32ForKey("atp").flatMap { BotSendMessageAccessGrantedType(rawValue: $0) })
case 36:
self = .giftCode(slug: decoder.decodeStringForKey("slug", orElse: ""), fromGiveaway: decoder.decodeBoolForKey("give", orElse: false), boostPeerId: PeerId(decoder.decodeInt64ForKey("pi", orElse: 0)), months: decoder.decodeInt32ForKey("months", orElse: 0))
default: default:
self = .unknown self = .unknown
} }
@ -382,6 +385,16 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
} else { } else {
encoder.encodeNil(forKey: "atp") encoder.encodeNil(forKey: "atp")
} }
case let .giftCode(slug, fromGiveaway, boostPeerId, months):
encoder.encodeInt32(36, forKey: "_rawValue")
encoder.encodeString(slug, forKey: "slug")
encoder.encodeBool(fromGiveaway, forKey: "give")
if let boostPeerId = boostPeerId {
encoder.encodeInt64(boostPeerId.toInt64(), forKey: "pi")
} else {
encoder.encodeNil(forKey: "pi")
}
encoder.encodeInt32(months, forKey: "months")
} }
} }
@ -403,6 +416,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
return peerIds return peerIds
case let .requestedPeer(_, peerId): case let .requestedPeer(_, peerId):
return [peerId] return [peerId]
case let .giftCode(_, _, boostPeerId, _):
return boostPeerId.flatMap { [$0] } ?? []
default: default:
return [] return []
} }

View File

@ -0,0 +1,67 @@
import Postbox
public final class TelegramMediaGiveaway: Media, Equatable {
public var id: MediaId? {
return nil
}
public var peerIds: [PeerId] {
return self.channelPeerIds
}
public let channelPeerIds: [PeerId]
public let quantity: Int32
public let months: Int32
public let untilDate: Int32
public init(channelPeerIds: [PeerId], quantity: Int32, months: Int32, untilDate: Int32) {
self.channelPeerIds = channelPeerIds
self.quantity = quantity
self.months = months
self.untilDate = untilDate
}
public init(decoder: PostboxDecoder) {
self.channelPeerIds = decoder.decodeInt64ArrayForKey("cns").map { PeerId($0) }
self.quantity = decoder.decodeInt32ForKey("qty", orElse: 0)
self.months = decoder.decodeInt32ForKey("mts", orElse: 0)
self.untilDate = decoder.decodeInt32ForKey("unt", orElse: 0)
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt64Array(self.channelPeerIds.map { $0.toInt64() }, forKey: "cns")
encoder.encodeInt32(self.quantity, forKey: "qty")
encoder.encodeInt32(self.months, forKey: "mts")
encoder.encodeInt32(self.untilDate, forKey: "unt")
}
public func isLikelyToBeUpdated() -> Bool {
return false
}
public func isEqual(to other: Media) -> Bool {
guard let other = other as? TelegramMediaGiveaway else {
return false
}
if self.channelPeerIds != other.channelPeerIds {
return false
}
if self.quantity != other.quantity {
return false
}
if self.months != other.months {
return false
}
if self.untilDate != other.untilDate {
return false
}
return true
}
public func isSemanticallyEqual(to other: Media) -> Bool {
return self.isEqual(to: other)
}
public static func ==(lhs: TelegramMediaGiveaway, rhs: TelegramMediaGiveaway) -> Bool {
return lhs.isEqual(to: rhs)
}
}

View File

@ -13,12 +13,13 @@ public enum AssignAppStoreTransactionError {
public enum AppStoreTransactionPurpose { public enum AppStoreTransactionPurpose {
case subscription case subscription
case upgrade case upgrade
case gift(peerId: EnginePeer.Id, currency: String, amount: Int64)
case restore case restore
case gift(peerId: EnginePeer.Id, currency: String, amount: Int64)
case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?, currency: String, amount: Int64)
case giveaway(boostPeer: EnginePeer.Id, randomId: Int64, untilDate: Int32, currency: String, amount: Int64)
} }
func _internal_sendAppStoreReceipt(account: Account, receipt: Data, purpose: AppStoreTransactionPurpose) -> Signal<Never, AssignAppStoreTransactionError> { private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTransactionPurpose) -> Signal<Api.InputStorePaymentPurpose, NoError> {
var purposeSignal: Signal<Api.InputStorePaymentPurpose, NoError>
switch purpose { switch purpose {
case .subscription, .upgrade, .restore: case .subscription, .upgrade, .restore:
var flags: Int32 = 0 var flags: Int32 = 0
@ -30,19 +31,47 @@ func _internal_sendAppStoreReceipt(account: Account, receipt: Data, purpose: App
default: default:
break break
} }
purposeSignal = .single(.inputStorePaymentPremiumSubscription(flags: flags)) return .single(.inputStorePaymentPremiumSubscription(flags: flags))
case let .gift(peerId, currency, amount): case let .gift(peerId, currency, amount):
purposeSignal = account.postbox.loadedPeerWithId(peerId) return account.postbox.loadedPeerWithId(peerId)
|> mapToSignal { peer -> Signal<Api.InputStorePaymentPurpose, NoError> in |> mapToSignal { peer -> Signal<Api.InputStorePaymentPurpose, NoError> in
if let inputUser = apiInputUser(peer) { guard let inputUser = apiInputUser(peer) else {
return .single(.inputStorePaymentGiftPremium(userId: inputUser, currency: currency, amount: amount))
} else {
return .complete() return .complete()
} }
return .single(.inputStorePaymentGiftPremium(userId: inputUser, currency: currency, amount: amount))
}
case let .giftCode(peerIds, boostPeerId, currency, amount):
return account.postbox.transaction { transaction -> Api.InputStorePaymentPurpose in
var flags: Int32 = 0
var apiBoostPeer: Api.InputPeer?
var apiInputUsers: [Api.InputUser] = []
for peerId in peerIds {
if let user = transaction.getPeer(peerId), let apiUser = apiInputUser(user) {
apiInputUsers.append(apiUser)
}
}
if let boostPeerId = boostPeerId, let boostPeer = transaction.getPeer(boostPeerId), let apiPeer = apiInputPeer(boostPeer) {
apiBoostPeer = apiPeer
flags |= (1 << 0)
}
return .inputStorePaymentPremiumGiftCode(flags: flags, users: apiInputUsers, boostPeer: apiBoostPeer, currency: currency, amount: amount)
}
case let .giveaway(boostPeerId, randomId, untilDate, currency, amount):
return account.postbox.loadedPeerWithId(boostPeerId)
|> mapToSignal { peer in
guard let apiBoostPeer = apiInputPeer(peer) else {
return .complete()
}
return .single(.inputStorePaymentPremiumGiveaway(flags: 0, boostPeer: apiBoostPeer, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount))
} }
} }
}
return purposeSignal
func _internal_sendAppStoreReceipt(account: Account, receipt: Data, purpose: AppStoreTransactionPurpose) -> Signal<Never, AssignAppStoreTransactionError> {
return apiInputStorePaymentPurpose(account: account, purpose: purpose)
|> castError(AssignAppStoreTransactionError.self) |> castError(AssignAppStoreTransactionError.self)
|> mapToSignal { purpose -> Signal<Never, AssignAppStoreTransactionError> in |> mapToSignal { purpose -> Signal<Never, AssignAppStoreTransactionError> in
return account.network.request(Api.functions.payments.assignAppStoreTransaction(receipt: Buffer(data: receipt), purpose: purpose)) return account.network.request(Api.functions.payments.assignAppStoreTransaction(receipt: Buffer(data: receipt), purpose: purpose))
@ -65,31 +94,7 @@ public enum RestoreAppStoreReceiptError {
} }
func _internal_canPurchasePremium(account: Account, purpose: AppStoreTransactionPurpose) -> Signal<Bool, NoError> { func _internal_canPurchasePremium(account: Account, purpose: AppStoreTransactionPurpose) -> Signal<Bool, NoError> {
var purposeSignal: Signal<Api.InputStorePaymentPurpose, NoError> return apiInputStorePaymentPurpose(account: account, purpose: purpose)
switch purpose {
case .subscription, .restore, .upgrade:
var flags: Int32 = 0
switch purpose {
case .upgrade:
flags |= (1 << 1)
case .restore:
flags |= (1 << 0)
default:
break
}
purposeSignal = .single(.inputStorePaymentPremiumSubscription(flags: flags))
case let .gift(peerId, currency, amount):
purposeSignal = account.postbox.loadedPeerWithId(peerId)
|> mapToSignal { peer -> Signal<Api.InputStorePaymentPurpose, NoError> in
if let inputUser = apiInputUser(peer) {
return .single(.inputStorePaymentGiftPremium(userId: inputUser, currency: currency, amount: amount))
} else {
return .complete()
}
}
}
return purposeSignal
|> mapToSignal { purpose -> Signal<Bool, NoError> in |> mapToSignal { purpose -> Signal<Bool, NoError> in
return account.network.request(Api.functions.payments.canPurchasePremium(purpose: purpose)) return account.network.request(Api.functions.payments.canPurchasePremium(purpose: purpose))
|> map { result -> Bool in |> map { result -> Bool in

View File

@ -0,0 +1,135 @@
import Foundation
import MtProtoKit
import SwiftSignalKit
import TelegramApi
public struct PremiumGiftCodeInfo: Equatable {
public let slug: String
public let fromPeerId: EnginePeer.Id
public let toPeerId: EnginePeer.Id?
public let date: Int32
public let months: Int32
public let usedDate: Int32?
public let isGiveaway: Bool
}
public struct PremiumGiftCodeOption: Codable, Equatable {
enum CodingKeys: String, CodingKey {
case users
case months
case storeProductId
}
public let users: Int32
public let months: Int32
public let storeProductId: String?
public init(users: Int32, months: Int32, storeProductId: String?) {
self.users = users
self.months = months
self.storeProductId = storeProductId
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.users = try container.decode(Int32.self, forKey: .users)
self.months = try container.decode(Int32.self, forKey: .months)
self.storeProductId = try container.decodeIfPresent(String.self, forKey: .storeProductId)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.users, forKey: .users)
try container.encode(self.months, forKey: .months)
try container.encodeIfPresent(self.storeProductId, forKey: .storeProductId)
}
}
func _internal_premiumGiftCodeOptions(account: Account, peerId: EnginePeer.Id) -> Signal<[PremiumGiftCodeOption], NoError> {
let flags: Int32 = 1 << 0
return account.postbox.loadedPeerWithId(peerId)
|> mapToSignal { peer in
guard let inputPeer = apiInputPeer(peer) else {
return .complete()
}
return account.network.request(Api.functions.payments.getPremiumGiftCodeOptions(flags: flags, boostPeer: inputPeer))
|> map(Optional.init)
|> `catch` { _ -> Signal<[Api.PremiumGiftCodeOption]?, NoError> in
return .single(nil)
}
|> mapToSignal { results -> Signal<[PremiumGiftCodeOption], NoError> in
if let results = results {
return .single(results.map { PremiumGiftCodeOption(apiGiftCodeOption: $0) })
} else {
return .single([])
}
}
}
}
func _internal_checkPremiumGiftCode(account: Account, slug: String) -> Signal<PremiumGiftCodeInfo?, NoError> {
return account.network.request(Api.functions.payments.checkGiftCode(slug: slug))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.payments.CheckedGiftCode?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<PremiumGiftCodeInfo?, NoError> in
if let result = result {
switch result {
case let .checkedGiftCode(_, _, _, _, _, _, chats, users):
return account.postbox.transaction { transaction in
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: parsedPeers)
return PremiumGiftCodeInfo(apiCheckedGiftCode: result, slug: slug)
}
}
} else {
return .single(nil)
}
}
}
func _internal_applyPremiumGiftCode(account: Account, slug: String) -> Signal<Never, NoError> {
return account.network.request(Api.functions.payments.applyGiftCode(slug: slug))
|> 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()
}
}
extension PremiumGiftCodeOption {
init(apiGiftCodeOption: Api.PremiumGiftCodeOption) {
switch apiGiftCodeOption {
case let .premiumGiftCodeOption(_, users, months, storeProduct):
self.init(users: users, months: months, storeProductId: storeProduct)
}
}
}
extension PremiumGiftCodeInfo {
init(apiCheckedGiftCode: Api.payments.CheckedGiftCode, slug: String) {
switch apiCheckedGiftCode {
case let .checkedGiftCode(flags, fromId, toId, date, months, usedDate, _, _):
self.slug = slug
self.fromPeerId = fromId.peerId
self.toPeerId = toId.flatMap { EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value($0)) }
self.date = date
self.months = months
self.usedDate = usedDate
self.isGiveaway = (flags & (1 << 2)) != 0
}
}
}
public extension PremiumGiftCodeInfo {
var isUsed: Bool {
return self.usedDate != nil
}
}

View File

@ -45,5 +45,17 @@ public extension TelegramEngine {
public func canPurchasePremium(purpose: AppStoreTransactionPurpose) -> Signal<Bool, NoError> { public func canPurchasePremium(purpose: AppStoreTransactionPurpose) -> Signal<Bool, NoError> {
return _internal_canPurchasePremium(account: self.account, purpose: purpose) return _internal_canPurchasePremium(account: self.account, purpose: purpose)
} }
public func checkPremiumGiftCode(slug: String) -> Signal<PremiumGiftCodeInfo?, NoError> {
return _internal_checkPremiumGiftCode(account: self.account, slug: slug)
}
public func applyPremiumGiftCode(slug: String) -> Signal<Never, NoError> {
return _internal_applyPremiumGiftCode(account: self.account, slug: slug)
}
public func premiumGiftCodeOptions(peerId: EnginePeer.Id) -> Signal<[PremiumGiftCodeOption], NoError> {
return _internal_premiumGiftCodeOptions(account: self.account, peerId: peerId)
}
} }
} }

View File

@ -444,7 +444,7 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati
), ),
itemCheckColors: PresentationThemeFillStrokeForeground( itemCheckColors: PresentationThemeFillStrokeForeground(
fillColor: UIColor(rgb: 0xffffff), fillColor: UIColor(rgb: 0xffffff),
strokeColor: UIColor(rgb: 0xffffff, alpha: 0.5), strokeColor: UIColor(rgb: 0xffffff, alpha: 0.3),
foregroundColor: UIColor(rgb: 0x000000) foregroundColor: UIColor(rgb: 0x000000)
), ),
controlSecondaryColor: UIColor(rgb: 0xffffff, alpha: 0.5), controlSecondaryColor: UIColor(rgb: 0xffffff, alpha: 0.5),

View File

@ -41,6 +41,7 @@ public enum PresentationResourceKey: Int32 {
case itemListCheckIcon case itemListCheckIcon
case itemListSecondaryCheckIcon case itemListSecondaryCheckIcon
case itemListPlusIcon case itemListPlusIcon
case itemListRoundPlusIcon
case itemListDeleteIcon case itemListDeleteIcon
case itemListDeleteIndicatorIcon case itemListDeleteIndicatorIcon
case itemListReorderIndicatorIcon case itemListReorderIndicatorIcon

View File

@ -63,6 +63,12 @@ public struct PresentationResourcesItemList {
}) })
} }
public static func roundPlusIconImage(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.itemListRoundPlusIcon.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat List/AddRoundIcon"), color: theme.list.itemAccentColor)
})
}
public static func deleteIconImage(_ theme: PresentationTheme) -> UIImage? { public static func deleteIconImage(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.itemListDeleteIcon.rawValue, { theme in return theme.image(PresentationResourceKey.itemListDeleteIcon.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: theme.list.itemDestructiveColor) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: theme.list.itemDestructiveColor)

View File

@ -898,6 +898,8 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
let resultTitleString = strings.Notification_ChangedToSameWallpaper(compactAuthorName) let resultTitleString = strings.Notification_ChangedToSameWallpaper(compactAuthorName)
attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
} }
case .giftCode:
attributedString = NSAttributedString(string: "Gift code", font: titleFont, textColor: primaryTextColor)
case .unknown: case .unknown:
attributedString = nil attributedString = nil
} }

View File

@ -0,0 +1,23 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ItemListDatePickerItem",
module_name = "ItemListDatePickerItem",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/ItemListUI:ItemListUI",
"//submodules/DatePickerNode:DatePickerNode",
],
visibility = [
"//visibility:public",
],
)

View File

@ -3999,7 +3999,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
adminedChannels: self.adminedChannels.get(), adminedChannels: self.adminedChannels.get(),
blockedPeersContext: self.storiesBlockedPeers blockedPeersContext: self.storiesBlockedPeers
) )
let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).startStandalone(next: { [weak self] _ in
guard let self else { guard let self else {
return return
} }

View File

@ -40,6 +40,7 @@ swift_library(
"//submodules/TooltipUI", "//submodules/TooltipUI",
"//submodules/OverlayStatusController", "//submodules/OverlayStatusController",
"//submodules/UndoUI", "//submodules/UndoUI",
"//submodules/TemporaryCachedPeerDataManager",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -26,6 +26,7 @@ import OverlayStatusController
import Markdown import Markdown
import TelegramUIPreferences import TelegramUIPreferences
import UndoUI import UndoUI
import TelegramStringFormatting
final class ShareWithPeersScreenComponent: Component { final class ShareWithPeersScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -494,7 +495,7 @@ final class ShareWithPeersScreenComponent: Component {
} }
@objc private func dismissPanGesture(_ recognizer: UIPanGestureRecognizer) { @objc private func dismissPanGesture(_ recognizer: UIPanGestureRecognizer) {
guard let controller = self.environment?.controller() as? ShareWithPeersScreen else { guard let controller = self.environment?.controller() as? ShareWithPeersScreen, let component = self.component else {
return return
} }
switch recognizer.state { switch recognizer.state {
@ -518,8 +519,12 @@ final class ShareWithPeersScreenComponent: Component {
if translation.y > 100.0 || velocity.y > 10.0 { if translation.y > 100.0 || velocity.y > 10.0 {
controller.requestDismiss() controller.requestDismiss()
Queue.mainQueue().justDispatch { if case .members = component.stateContext.subject {
controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .spring))
} else {
Queue.mainQueue().justDispatch {
controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .spring))
}
} }
} else { } else {
let transition = Transition(animation: .curve(duration: 0.3, curve: .spring)) let transition = Transition(animation: .curve(duration: 0.3, curve: .spring))
@ -815,7 +820,11 @@ final class ShareWithPeersScreenComponent: Component {
} }
private func updateModalOverlayTransition(transition: Transition) { private func updateModalOverlayTransition(transition: Transition) {
guard let _ = self.component, let environment = self.environment, let itemLayout = self.itemLayout else { guard let component = self.component, let environment = self.environment, let itemLayout = self.itemLayout else {
return
}
if case .members = component.stateContext.subject {
return return
} }
@ -830,7 +839,7 @@ final class ShareWithPeersScreenComponent: Component {
topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) topOffsetFraction = max(0.0, min(1.0, topOffsetFraction))
let transitionFactor: CGFloat = 1.0 - topOffsetFraction let transitionFactor: CGFloat = 1.0 - topOffsetFraction
if let controller = environment.controller() { if let controller = environment.controller() as? ShareWithPeersScreen {
Queue.mainQueue().justDispatch { Queue.mainQueue().justDispatch {
var transition = transition var transition = transition
if controller.modalStyleOverlayTransitionFactor.isZero && transitionFactor > 0.0, transition.animation.isImmediate { if controller.modalStyleOverlayTransitionFactor.isZero && transitionFactor > 0.0, transition.animation.isImmediate {
@ -951,7 +960,11 @@ final class ShareWithPeersScreenComponent: Component {
} else if section.id == 2 { } else if section.id == 2 {
sectionTitle = environment.strings.Story_Privacy_WhoCanViewHeader sectionTitle = environment.strings.Story_Privacy_WhoCanViewHeader
} else if section.id == 1 { } else if section.id == 1 {
sectionTitle = environment.strings.Story_Privacy_ContactsHeader if case .members = component.stateContext.subject {
sectionTitle = "SUBSCRIBERS"
} else {
sectionTitle = environment.strings.Story_Privacy_ContactsHeader
}
} else { } else {
sectionTitle = "" sectionTitle = ""
} }
@ -1389,7 +1402,15 @@ final class ShareWithPeersScreenComponent: Component {
subtitle = nil subtitle = nil
} }
} else { } else {
subtitle = nil if case .members = component.stateContext.subject {
if let invitedAt = stateValue.invitedAt[peer.id] {
subtitle = "joined \(stringForMediumDate(timestamp: invitedAt, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat))"
} else {
subtitle = nil
}
} else {
subtitle = nil
}
} }
let isSelected = self.selectedPeers.contains(peer.id) || self.selectedGroups.contains(peer.id) let isSelected = self.selectedPeers.contains(peer.id) || self.selectedGroups.contains(peer.id)
@ -1734,10 +1755,17 @@ final class ShareWithPeersScreenComponent: Component {
} }
func animateOut(completion: @escaping () -> Void) { func animateOut(completion: @escaping () -> Void) {
guard let component = self.component else {
return
}
self.isDismissed = true self.isDismissed = true
if let controller = self.environment?.controller() { if let controller = self.environment?.controller() as? ShareWithPeersScreen {
controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .easeInOut)) if case .members = component.stateContext.subject {
} else {
controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .easeInOut))
}
} }
var animateOffset: CGFloat = self.bounds.height - self.backgroundView.frame.minY var animateOffset: CGFloat = self.bounds.height - self.backgroundView.frame.minY
@ -1798,6 +1826,8 @@ final class ShareWithPeersScreenComponent: Component {
contentTransition = .spring(duration: 0.4) contentTransition = .spring(duration: 0.4)
} }
self.currentHasChannels = hasChannels self.currentHasChannels = hasChannels
} else if case .members = component.stateContext.subject {
self.dismissPanGesture?.isEnabled = false
} }
let environment = environment[ViewControllerComponentContainer.Environment.self].value let environment = environment[ViewControllerComponentContainer.Environment.self].value
@ -1943,6 +1973,8 @@ final class ShareWithPeersScreenComponent: Component {
let placeholder: String let placeholder: String
switch component.stateContext.subject { switch component.stateContext.subject {
case .members:
placeholder = "Search Subscribers"
case .chats: case .chats:
placeholder = environment.strings.Story_Privacy_SearchChats placeholder = environment.strings.Story_Privacy_SearchChats
default: default:
@ -2009,6 +2041,11 @@ final class ShareWithPeersScreenComponent: Component {
} }
transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize))
if case .members = component.stateContext.subject {
self.dimView .isHidden = true
} else {
self.dimView .isHidden = false
}
let categoryItemSize = self.categoryTemplateItem.update( let categoryItemSize = self.categoryTemplateItem.update(
transition: .immediate, transition: .immediate,
@ -2179,7 +2216,11 @@ final class ShareWithPeersScreenComponent: Component {
} }
} }
let containerInset: CGFloat = environment.statusBarHeight + 10.0 var containerInset: CGFloat = environment.statusBarHeight
if case .members = component.stateContext.subject {
} else {
containerInset += 10.0
}
var navigationHeight: CGFloat = 56.0 var navigationHeight: CGFloat = 56.0
let navigationSideInset: CGFloat = 16.0 let navigationSideInset: CGFloat = 16.0
@ -2262,6 +2303,9 @@ final class ShareWithPeersScreenComponent: Component {
} }
case .search: case .search:
title = "" title = ""
case .members:
title = "Gift Premium"
actionButtonTitle = "Save Recipients"
} }
let navigationTitleSize = self.navigationTitle.update( let navigationTitleSize = self.navigationTitle.update(
transition: .immediate, transition: .immediate,
@ -2298,7 +2342,9 @@ final class ShareWithPeersScreenComponent: Component {
topInset = 0.0 topInset = 0.0
} else { } else {
var inset: CGFloat var inset: CGFloat
if case let .stories(editing) = component.stateContext.subject { if case .members = component.stateContext.subject {
inset = 1000.0
} else if case let .stories(editing) = component.stateContext.subject {
if editing { if editing {
inset = 351.0 inset = 351.0
inset += 10.0 + environment.safeInsets.bottom + 50.0 + footersTotalHeight inset += 10.0 + environment.safeInsets.bottom + 50.0 + footersTotalHeight
@ -2412,7 +2458,7 @@ final class ShareWithPeersScreenComponent: Component {
})) }))
let _ = (peers let _ = (peers
|> deliverOnMainQueue).start(next: { [weak controller, weak component] peers in |> deliverOnMainQueue).start(next: { [weak controller, weak component] peers in
guard let controller, let component else { guard let controller, let component else {
return return
} }
@ -2435,7 +2481,7 @@ final class ShareWithPeersScreenComponent: Component {
} }
if savePeers { if savePeers {
let _ = (updatePeersListStoredState(engine: component.context.engine, base: base, peerIds: self.selectedPeers) let _ = (updatePeersListStoredState(engine: component.context.engine, base: base, peerIds: self.selectedPeers)
|> deliverOnMainQueue).start(completed: { |> deliverOnMainQueue).start(completed: {
complete() complete()
}) })
} else { } else {
@ -2596,7 +2642,13 @@ final class ShareWithPeersScreenComponent: Component {
transition.setPosition(view: self.backgroundView, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) transition.setPosition(view: self.backgroundView, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0))
transition.setBounds(view: self.backgroundView, bounds: CGRect(origin: CGPoint(x: containerSideInset, y: 0.0), size: CGSize(width: containerWidth, height: availableSize.height))) transition.setBounds(view: self.backgroundView, bounds: CGRect(origin: CGPoint(x: containerSideInset, y: 0.0), size: CGSize(width: containerWidth, height: availableSize.height)))
let scrollClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: containerInset + 10.0), size: CGSize(width: availableSize.width, height: availableSize.height - 10.0)) var scrollClippingInset: CGFloat = 0.0
if case .members = component.stateContext.subject {
} else {
scrollClippingInset = 10.0
}
let scrollClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: containerInset + scrollClippingInset), size: CGSize(width: availableSize.width, height: availableSize.height - scrollClippingInset))
transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center) transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center)
transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size)) transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size))
@ -2647,534 +2699,10 @@ final class ShareWithPeersScreenComponent: Component {
} }
} }
public class ShareWithPeersScreen: ViewControllerComponentContainer { public class ShareWithPeersScreen: ViewControllerComponentContainer {
public final class State {
let sendAsPeers: [EnginePeer]
let peers: [EnginePeer]
let peersMap: [EnginePeer.Id: EnginePeer]
let savedSelectedPeers: [Stories.Item.Privacy.Base: [EnginePeer.Id]]
let presences: [EnginePeer.Id: EnginePeer.Presence]
let participants: [EnginePeer.Id: Int]
let closeFriendsPeers: [EnginePeer]
let grayListPeers: [EnginePeer]
fileprivate init(
sendAsPeers: [EnginePeer],
peers: [EnginePeer],
peersMap: [EnginePeer.Id: EnginePeer],
savedSelectedPeers: [Stories.Item.Privacy.Base: [EnginePeer.Id]],
presences: [EnginePeer.Id: EnginePeer.Presence],
participants: [EnginePeer.Id: Int],
closeFriendsPeers: [EnginePeer],
grayListPeers: [EnginePeer]
) {
self.sendAsPeers = sendAsPeers
self.peers = peers
self.peersMap = peersMap
self.savedSelectedPeers = savedSelectedPeers
self.presences = presences
self.participants = participants
self.closeFriendsPeers = closeFriendsPeers
self.grayListPeers = grayListPeers
}
}
public final class StateContext {
public enum Subject: Equatable {
case peers(peers: [EnginePeer], peerId: EnginePeer.Id?)
case stories(editing: Bool)
case chats(blocked: Bool)
case contacts(base: EngineStoryPrivacy.Base)
case search(query: String, onlyContacts: Bool)
}
fileprivate var stateValue: State?
public let subject: Subject
public let editing: Bool
public private(set) var initialPeerIds: Set<EnginePeer.Id> = Set()
fileprivate let blockedPeersContext: BlockedPeersContext?
private var stateDisposable: Disposable?
private let stateSubject = Promise<State>()
public var state: Signal<State, NoError> {
return self.stateSubject.get()
}
private let readySubject = ValuePromise<Bool>(false, ignoreRepeated: true)
public var ready: Signal<Bool, NoError> {
return self.readySubject.get()
}
public init(
context: AccountContext,
subject: Subject = .chats(blocked: false),
editing: Bool,
initialSelectedPeers: [EngineStoryPrivacy.Base: [EnginePeer.Id]] = [:],
initialPeerIds: Set<EnginePeer.Id> = Set(),
closeFriends: Signal<[EnginePeer], NoError> = .single([]),
adminedChannels: Signal<[EnginePeer], NoError> = .single([]),
blockedPeersContext: BlockedPeersContext? = nil
) {
self.subject = subject
self.editing = editing
self.initialPeerIds = initialPeerIds
self.blockedPeersContext = blockedPeersContext
let grayListPeers: Signal<[EnginePeer], NoError>
if let blockedPeersContext {
grayListPeers = blockedPeersContext.state
|> map { state -> [EnginePeer] in
return state.peers.compactMap { $0.peer.flatMap(EnginePeer.init) }
}
} else {
grayListPeers = .single([])
}
switch subject {
case let .peers(peers, _):
self.stateDisposable = (.single(peers)
|> mapToSignal { peers -> Signal<([EnginePeer], [EnginePeer.Id: Optional<Int>]), NoError> in
return context.engine.data.subscribe(
EngineDataMap(peers.map(\.id).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init))
)
|> map { participantCountMap -> ([EnginePeer], [EnginePeer.Id: Optional<Int>]) in
return (peers, participantCountMap)
}
}
|> deliverOnMainQueue).start(next: { [weak self] peers, participantCounts in
guard let self else {
return
}
var participants: [EnginePeer.Id: Int] = [:]
for (key, value) in participantCounts {
if let value {
participants[key] = value
}
}
let state = State(
sendAsPeers: peers,
peers: [],
peersMap: [:],
savedSelectedPeers: [:],
presences: [:],
participants: participants,
closeFriendsPeers: [],
grayListPeers: []
)
self.stateValue = state
self.stateSubject.set(.single(state))
self.readySubject.set(true)
})
case .stories:
let savedEveryoneExceptionPeers = peersListStoredState(engine: context.engine, base: .everyone)
let savedContactsExceptionPeers = peersListStoredState(engine: context.engine, base: .contacts)
let savedSelectedPeers = peersListStoredState(engine: context.engine, base: .nobody)
let savedPeers = combineLatest(
savedEveryoneExceptionPeers,
savedContactsExceptionPeers,
savedSelectedPeers
) |> mapToSignal { everyone, contacts, selected -> Signal<([EnginePeer.Id: EnginePeer], [EnginePeer.Id], [EnginePeer.Id], [EnginePeer.Id]), NoError> in
var everyone = everyone
if let initialPeerIds = initialSelectedPeers[.everyone] {
everyone = initialPeerIds
}
var everyonePeerSignals: [Signal<EnginePeer?, NoError>] = []
if everyone.count < 3 {
for peerId in everyone {
everyonePeerSignals.append(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)))
}
}
var contacts = contacts
if let initialPeerIds = initialSelectedPeers[.contacts] {
contacts = initialPeerIds
}
var contactsPeerSignals: [Signal<EnginePeer?, NoError>] = []
if contacts.count < 3 {
for peerId in contacts {
contactsPeerSignals.append(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)))
}
}
var selected = selected
if let initialPeerIds = initialSelectedPeers[.nobody] {
selected = initialPeerIds
}
var selectedPeerSignals: [Signal<EnginePeer?, NoError>] = []
if selected.count < 3 {
for peerId in selected {
selectedPeerSignals.append(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)))
}
}
return combineLatest(
combineLatest(everyonePeerSignals),
combineLatest(contactsPeerSignals),
combineLatest(selectedPeerSignals)
) |> map { everyonePeers, contactsPeers, selectedPeers -> ([EnginePeer.Id: EnginePeer], [EnginePeer.Id], [EnginePeer.Id], [EnginePeer.Id]) in
var peersMap: [EnginePeer.Id: EnginePeer] = [:]
for peer in everyonePeers {
if let peer {
peersMap[peer.id] = peer
}
}
for peer in contactsPeers {
if let peer {
peersMap[peer.id] = peer
}
}
for peer in selectedPeers {
if let peer {
peersMap[peer.id] = peer
}
}
return (
peersMap,
everyone,
contacts,
selected
)
}
}
let adminedChannelsWithParticipants = adminedChannels
|> mapToSignal { peers -> Signal<([EnginePeer], [EnginePeer.Id: Optional<Int>]), NoError> in
return context.engine.data.subscribe(
EngineDataMap(peers.map(\.id).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init))
)
|> map { participantCountMap -> ([EnginePeer], [EnginePeer.Id: Optional<Int>]) in
return (peers, participantCountMap)
}
}
self.stateDisposable = combineLatest(
queue: Queue.mainQueue(),
context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)),
adminedChannelsWithParticipants,
savedPeers,
closeFriends,
grayListPeers
)
.start(next: { [weak self] accountPeer, adminedChannelsWithParticipants, savedPeers, closeFriends, grayListPeers in
guard let self else {
return
}
let (adminedChannels, participantCounts) = adminedChannelsWithParticipants
var participants: [EnginePeer.Id: Int] = [:]
for (key, value) in participantCounts {
if let value {
participants[key] = value
}
}
var sendAsPeers: [EnginePeer] = []
if let accountPeer {
sendAsPeers.append(accountPeer)
}
for channel in adminedChannels {
if case let .channel(channel) = channel, channel.hasPermission(.postStories) {
if !sendAsPeers.contains(where: { $0.id == channel.id }) {
sendAsPeers.append(contentsOf: adminedChannels)
}
}
}
let (peersMap, everyonePeers, contactsPeers, selectedPeers) = savedPeers
var savedSelectedPeers: [Stories.Item.Privacy.Base: [EnginePeer.Id]] = [:]
savedSelectedPeers[.everyone] = everyonePeers
savedSelectedPeers[.contacts] = contactsPeers
savedSelectedPeers[.nobody] = selectedPeers
let state = State(
sendAsPeers: sendAsPeers,
peers: [],
peersMap: peersMap,
savedSelectedPeers: savedSelectedPeers,
presences: [:],
participants: participants,
closeFriendsPeers: closeFriends,
grayListPeers: grayListPeers
)
self.stateValue = state
self.stateSubject.set(.single(state))
self.readySubject.set(true)
})
case let .chats(isGrayList):
self.stateDisposable = (combineLatest(
context.engine.messages.chatList(group: .root, count: 200) |> take(1),
context.engine.data.get(TelegramEngine.EngineData.Item.Contacts.List(includePresences: true)),
context.engine.data.get(EngineDataMap(Array(self.initialPeerIds).map(TelegramEngine.EngineData.Item.Peer.Peer.init))),
grayListPeers
)
|> mapToSignal { chatList, contacts, initialPeers, grayListPeers -> Signal<(EngineChatList, EngineContactList, [EnginePeer.Id: Optional<EnginePeer>], [EnginePeer.Id: Optional<Int>], [EnginePeer]), NoError> in
return context.engine.data.subscribe(
EngineDataMap(chatList.items.map(\.renderedPeer.peerId).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init))
)
|> map { participantCountMap -> (EngineChatList, EngineContactList, [EnginePeer.Id: Optional<EnginePeer>], [EnginePeer.Id: Optional<Int>], [EnginePeer]) in
return (chatList, contacts, initialPeers, participantCountMap, grayListPeers)
}
}
|> deliverOnMainQueue).start(next: { [weak self] chatList, contacts, initialPeers, participantCounts, grayListPeers in
guard let self else {
return
}
var participants: [EnginePeer.Id: Int] = [:]
for (key, value) in participantCounts {
if let value {
participants[key] = value
}
}
var grayListPeersIds = Set<EnginePeer.Id>()
for peer in grayListPeers {
grayListPeersIds.insert(peer.id)
}
var existingIds = Set<EnginePeer.Id>()
var selectedPeers: [EnginePeer] = []
if isGrayList {
self.initialPeerIds = Set(grayListPeers.map { $0.id })
}
for item in chatList.items.reversed() {
if let peer = item.renderedPeer.peer {
if self.initialPeerIds.contains(peer.id) || isGrayList && grayListPeersIds.contains(peer.id) {
selectedPeers.append(peer)
existingIds.insert(peer.id)
}
}
}
for peerId in self.initialPeerIds {
if !existingIds.contains(peerId), let maybePeer = initialPeers[peerId], let peer = maybePeer {
selectedPeers.append(peer)
existingIds.insert(peerId)
}
}
if isGrayList {
for peer in grayListPeers {
if !existingIds.contains(peer.id) {
selectedPeers.append(peer)
existingIds.insert(peer.id)
}
}
}
var presences: [EnginePeer.Id: EnginePeer.Presence] = [:]
for item in chatList.items {
presences[item.renderedPeer.peerId] = item.presence
}
var peers: [EnginePeer] = []
peers = chatList.items.filter { peer in
if let peer = peer.renderedPeer.peer {
if self.initialPeerIds.contains(peer.id) {
return false
}
if peer.id == context.account.peerId {
return false
}
if peer.isService || peer.isDeleted {
return false
}
if case let .user(user) = peer {
if user.botInfo != nil {
return false
}
}
if case let .channel(channel) = peer {
if channel.isForum {
return false
}
if case .broadcast = channel.info {
return false
}
}
return true
} else {
return false
}
}.reversed().compactMap { $0.renderedPeer.peer }
for peer in peers {
existingIds.insert(peer.id)
}
peers.insert(contentsOf: selectedPeers, at: 0)
let state = State(
sendAsPeers: [],
peers: peers,
peersMap: [:],
savedSelectedPeers: [:],
presences: presences,
participants: participants,
closeFriendsPeers: [],
grayListPeers: grayListPeers
)
self.stateValue = state
self.stateSubject.set(.single(state))
self.readySubject.set(true)
})
case let .contacts(base):
self.stateDisposable = (context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Contacts.List(includePresences: true)
)
|> deliverOnMainQueue).start(next: { [weak self] contactList in
guard let self else {
return
}
var selectedPeers: [EnginePeer] = []
if case .closeFriends = base {
for peer in contactList.peers {
if case let .user(user) = peer, user.flags.contains(.isCloseFriend) {
selectedPeers.append(peer)
}
}
self.initialPeerIds = Set(selectedPeers.map { $0.id })
} else {
for peer in contactList.peers {
if case let .user(user) = peer, initialPeerIds.contains(user.id), !user.isDeleted {
selectedPeers.append(peer)
}
}
self.initialPeerIds = initialPeerIds
}
selectedPeers = selectedPeers.sorted(by: { lhs, rhs in
let result = lhs.indexName.isLessThan(other: rhs.indexName, ordering: .firstLast)
if result == .orderedSame {
return lhs.id < rhs.id
} else {
return result == .orderedAscending
}
})
var peers: [EnginePeer] = []
peers = contactList.peers.filter { !self.initialPeerIds.contains($0.id) && $0.id != context.account.peerId && !$0.isDeleted }.sorted(by: { lhs, rhs in
let result = lhs.indexName.isLessThan(other: rhs.indexName, ordering: .firstLast)
if result == .orderedSame {
return lhs.id < rhs.id
} else {
return result == .orderedAscending
}
})
peers.insert(contentsOf: selectedPeers, at: 0)
let state = State(
sendAsPeers: [],
peers: peers,
peersMap: [:],
savedSelectedPeers: [:],
presences: contactList.presences,
participants: [:],
closeFriendsPeers: [],
grayListPeers: []
)
self.stateValue = state
self.stateSubject.set(.single(state))
self.readySubject.set(true)
})
case let .search(query, onlyContacts):
let signal: Signal<([EngineRenderedPeer], [EnginePeer.Id: Optional<EnginePeer.Presence>], [EnginePeer.Id: Optional<Int>]), NoError>
if onlyContacts {
signal = combineLatest(
context.engine.contacts.searchLocalPeers(query: query),
context.engine.contacts.searchContacts(query: query)
)
|> map { peers, contacts in
let contactIds = Set(contacts.0.map { $0.id })
return (peers.filter { contactIds.contains($0.peerId) }, [:], [:])
}
} else {
signal = context.engine.contacts.searchLocalPeers(query: query)
|> mapToSignal { peers in
return context.engine.data.subscribe(
EngineDataMap(peers.map(\.peerId).map(TelegramEngine.EngineData.Item.Peer.Presence.init)),
EngineDataMap(peers.map(\.peerId).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init))
)
|> map { presenceMap, participantCountMap -> ([EngineRenderedPeer], [EnginePeer.Id: Optional<EnginePeer.Presence>], [EnginePeer.Id: Optional<Int>]) in
return (peers, presenceMap, participantCountMap)
}
}
}
self.stateDisposable = (signal
|> deliverOnMainQueue).start(next: { [weak self] peers, presenceMap, participantCounts in
guard let self else {
return
}
var presences: [EnginePeer.Id: EnginePeer.Presence] = [:]
for (key, value) in presenceMap {
if let value {
presences[key] = value
}
}
var participants: [EnginePeer.Id: Int] = [:]
for (key, value) in participantCounts {
if let value {
participants[key] = value
}
}
let state = State(
sendAsPeers: [],
peers: peers.compactMap { $0.peer }.filter { peer in
if case let .user(user) = peer {
if user.id == context.account.peerId {
return false
} else if user.botInfo != nil {
return false
} else if peer.isService {
return false
} else if user.isDeleted {
return false
} else {
return true
}
} else if case let .channel(channel) = peer {
if channel.isForum {
return false
}
if case .broadcast = channel.info {
return false
}
return true
} else {
return true
}
},
peersMap: [:],
savedSelectedPeers: [:],
presences: presences,
participants: participants,
closeFriendsPeers: [],
grayListPeers: []
)
self.stateValue = state
self.stateSubject.set(.single(state))
self.readySubject.set(true)
})
}
}
deinit {
self.stateDisposable?.dispose()
}
}
private let context: AccountContext private let context: AccountContext
private var isCustomModal = true
private var isDismissed: Bool = false private var isDismissed: Bool = false
public var dismissed: () -> Void = {} public var dismissed: () -> Void = {}
@ -3189,8 +2717,8 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
mentions: [String] = [], mentions: [String] = [],
stateContext: StateContext, stateContext: StateContext,
completion: @escaping (EnginePeer.Id?, EngineStoryPrivacy, Bool, Bool, [EnginePeer], Bool) -> Void, completion: @escaping (EnginePeer.Id?, EngineStoryPrivacy, Bool, Bool, [EnginePeer], Bool) -> Void,
editCategory: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void, editCategory: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void = { _, _, _ in },
editBlockedPeers: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void, editBlockedPeers: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void = { _, _, _ in },
peerCompletion: @escaping (EnginePeer.Id) -> Void = { _ in } peerCompletion: @escaping (EnginePeer.Id) -> Void = { _ in }
) { ) {
self.context = context self.context = context
@ -3334,6 +2862,10 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
} }
} }
var theme: ViewControllerComponentContainer.Theme = .dark
if case .members = stateContext.subject {
theme = .default
}
super.init(context: context, component: ShareWithPeersScreenComponent( super.init(context: context, component: ShareWithPeersScreenComponent(
context: context, context: context,
stateContext: stateContext, stateContext: stateContext,
@ -3349,10 +2881,15 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
editCategory: editCategory, editCategory: editCategory,
editBlockedPeers: editBlockedPeers, editBlockedPeers: editBlockedPeers,
peerCompletion: peerCompletion peerCompletion: peerCompletion
), navigationBarAppearance: .none, theme: .dark) ), navigationBarAppearance: .none, theme: theme)
self.statusBar.statusBarStyle = .Ignore self.statusBar.statusBarStyle = .Ignore
self.navigationPresentation = .flatModal if case .members = stateContext.subject {
self.navigationPresentation = .modal
self.isCustomModal = false
} else {
self.navigationPresentation = .flatModal
}
self.blocksBackgroundWhenInOverlay = true self.blocksBackgroundWhenInOverlay = true
self.automaticallyControlPresentationContextLayout = false self.automaticallyControlPresentationContextLayout = false
self.lockOrientation = true self.lockOrientation = true
@ -3372,10 +2909,12 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
override public func viewDidAppear(_ animated: Bool) { override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated) super.viewDidAppear(animated)
self.view.disablesInteractiveModalDismiss = true if self.isCustomModal {
self.view.disablesInteractiveModalDismiss = true
if let componentView = self.node.hostView.componentView as? ShareWithPeersScreenComponent.View {
componentView.animateIn() if let componentView = self.node.hostView.componentView as? ShareWithPeersScreenComponent.View {
componentView.animateIn()
}
} }
} }
@ -3404,57 +2943,19 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
self.isDismissed = true self.isDismissed = true
self.view.endEditing(true) self.view.endEditing(true)
if let componentView = self.node.hostView.componentView as? ShareWithPeersScreenComponent.View { if self.isCustomModal {
componentView.animateOut(completion: { [weak self] in if let componentView = self.node.hostView.componentView as? ShareWithPeersScreenComponent.View {
completion?() componentView.animateOut(completion: { [weak self] in
self?.dismiss(animated: false) completion?()
}) self?.dismiss(animated: false)
})
} else {
self.dismiss(animated: false)
}
} else { } else {
self.dismiss(animated: false) self.dismiss(animated: true)
} }
} }
} }
} }
final class PeersListStoredState: Codable {
private enum CodingKeys: String, CodingKey {
case peerIds
}
public let peerIds: [EnginePeer.Id]
public init(peerIds: [EnginePeer.Id]) {
self.peerIds = peerIds
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.peerIds = try container.decode([Int64].self, forKey: .peerIds).map { EnginePeer.Id($0) }
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.peerIds.map { $0.toInt64() }, forKey: .peerIds)
}
}
private func peersListStoredState(engine: TelegramEngine, base: Stories.Item.Privacy.Base) -> Signal<[EnginePeer.Id], NoError> {
let key = EngineDataBuffer(length: 4)
key.setInt32(0, value: base.rawValue)
return engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.shareWithPeersState, id: key))
|> map { entry -> [EnginePeer.Id] in
return entry?.get(PeersListStoredState.self)?.peerIds ?? []
}
}
private func updatePeersListStoredState(engine: TelegramEngine, base: Stories.Item.Privacy.Base, peerIds: [EnginePeer.Id]) -> Signal<Never, NoError> {
let key = EngineDataBuffer(length: 4)
key.setInt32(0, value: base.rawValue)
let state = PeersListStoredState(peerIds: peerIds)
return engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.shareWithPeersState, id: key, item: state)
}

View File

@ -0,0 +1,637 @@
import Foundation
import SwiftSignalKit
import TelegramCore
import AccountContext
import TelegramUIPreferences
import TemporaryCachedPeerDataManager
public extension ShareWithPeersScreen {
final class State {
let sendAsPeers: [EnginePeer]
let peers: [EnginePeer]
let peersMap: [EnginePeer.Id: EnginePeer]
let savedSelectedPeers: [Stories.Item.Privacy.Base: [EnginePeer.Id]]
let presences: [EnginePeer.Id: EnginePeer.Presence]
let invitedAt: [EnginePeer.Id: Int32]
let participants: [EnginePeer.Id: Int]
let closeFriendsPeers: [EnginePeer]
let grayListPeers: [EnginePeer]
fileprivate init(
sendAsPeers: [EnginePeer] = [],
peers: [EnginePeer],
peersMap: [EnginePeer.Id: EnginePeer] = [:],
savedSelectedPeers: [Stories.Item.Privacy.Base: [EnginePeer.Id]] = [:],
presences: [EnginePeer.Id: EnginePeer.Presence] = [:],
invitedAt: [EnginePeer.Id: Int32] = [:],
participants: [EnginePeer.Id: Int] = [:],
closeFriendsPeers: [EnginePeer] = [],
grayListPeers: [EnginePeer] = []
) {
self.sendAsPeers = sendAsPeers
self.peers = peers
self.peersMap = peersMap
self.savedSelectedPeers = savedSelectedPeers
self.presences = presences
self.invitedAt = invitedAt
self.participants = participants
self.closeFriendsPeers = closeFriendsPeers
self.grayListPeers = grayListPeers
}
}
final class StateContext {
public enum Subject: Equatable {
case peers(peers: [EnginePeer], peerId: EnginePeer.Id?)
case stories(editing: Bool)
case chats(blocked: Bool)
case contacts(base: EngineStoryPrivacy.Base)
case search(query: String, onlyContacts: Bool)
case members(peerId: EnginePeer.Id)
}
var stateValue: State?
public let subject: Subject
public let editing: Bool
public private(set) var initialPeerIds: Set<EnginePeer.Id> = Set()
let blockedPeersContext: BlockedPeersContext?
private var stateDisposable: Disposable?
private let stateSubject = Promise<State>()
public var state: Signal<State, NoError> {
return self.stateSubject.get()
}
private var listControl: PeerChannelMemberCategoryControl?
private let readySubject = ValuePromise<Bool>(false, ignoreRepeated: true)
public var ready: Signal<Bool, NoError> {
return self.readySubject.get()
}
public init(
context: AccountContext,
subject: Subject = .chats(blocked: false),
editing: Bool = false,
initialSelectedPeers: [EngineStoryPrivacy.Base: [EnginePeer.Id]] = [:],
initialPeerIds: Set<EnginePeer.Id> = Set(),
closeFriends: Signal<[EnginePeer], NoError> = .single([]),
adminedChannels: Signal<[EnginePeer], NoError> = .single([]),
blockedPeersContext: BlockedPeersContext? = nil
) {
self.subject = subject
self.editing = editing
self.initialPeerIds = initialPeerIds
self.blockedPeersContext = blockedPeersContext
let grayListPeers: Signal<[EnginePeer], NoError>
if let blockedPeersContext {
grayListPeers = blockedPeersContext.state
|> map { state -> [EnginePeer] in
return state.peers.compactMap { $0.peer.flatMap(EnginePeer.init) }
}
} else {
grayListPeers = .single([])
}
switch subject {
case let .peers(peers, _):
self.stateDisposable = (.single(peers)
|> mapToSignal { peers -> Signal<([EnginePeer], [EnginePeer.Id: Optional<Int>]), NoError> in
return context.engine.data.subscribe(
EngineDataMap(peers.map(\.id).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init))
)
|> map { participantCountMap -> ([EnginePeer], [EnginePeer.Id: Optional<Int>]) in
return (peers, participantCountMap)
}
}
|> deliverOnMainQueue).start(next: { [weak self] peers, participantCounts in
guard let self else {
return
}
var participants: [EnginePeer.Id: Int] = [:]
for (key, value) in participantCounts {
if let value {
participants[key] = value
}
}
let state = State(
sendAsPeers: peers,
peers: [],
participants: participants
)
self.stateValue = state
self.stateSubject.set(.single(state))
self.readySubject.set(true)
})
case .stories:
let savedEveryoneExceptionPeers = peersListStoredState(engine: context.engine, base: .everyone)
let savedContactsExceptionPeers = peersListStoredState(engine: context.engine, base: .contacts)
let savedSelectedPeers = peersListStoredState(engine: context.engine, base: .nobody)
let savedPeers = combineLatest(
savedEveryoneExceptionPeers,
savedContactsExceptionPeers,
savedSelectedPeers
) |> mapToSignal { everyone, contacts, selected -> Signal<([EnginePeer.Id: EnginePeer], [EnginePeer.Id], [EnginePeer.Id], [EnginePeer.Id]), NoError> in
var everyone = everyone
if let initialPeerIds = initialSelectedPeers[.everyone] {
everyone = initialPeerIds
}
var everyonePeerSignals: [Signal<EnginePeer?, NoError>] = []
if everyone.count < 3 {
for peerId in everyone {
everyonePeerSignals.append(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)))
}
}
var contacts = contacts
if let initialPeerIds = initialSelectedPeers[.contacts] {
contacts = initialPeerIds
}
var contactsPeerSignals: [Signal<EnginePeer?, NoError>] = []
if contacts.count < 3 {
for peerId in contacts {
contactsPeerSignals.append(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)))
}
}
var selected = selected
if let initialPeerIds = initialSelectedPeers[.nobody] {
selected = initialPeerIds
}
var selectedPeerSignals: [Signal<EnginePeer?, NoError>] = []
if selected.count < 3 {
for peerId in selected {
selectedPeerSignals.append(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)))
}
}
return combineLatest(
combineLatest(everyonePeerSignals),
combineLatest(contactsPeerSignals),
combineLatest(selectedPeerSignals)
) |> map { everyonePeers, contactsPeers, selectedPeers -> ([EnginePeer.Id: EnginePeer], [EnginePeer.Id], [EnginePeer.Id], [EnginePeer.Id]) in
var peersMap: [EnginePeer.Id: EnginePeer] = [:]
for peer in everyonePeers {
if let peer {
peersMap[peer.id] = peer
}
}
for peer in contactsPeers {
if let peer {
peersMap[peer.id] = peer
}
}
for peer in selectedPeers {
if let peer {
peersMap[peer.id] = peer
}
}
return (
peersMap,
everyone,
contacts,
selected
)
}
}
let adminedChannelsWithParticipants = adminedChannels
|> mapToSignal { peers -> Signal<([EnginePeer], [EnginePeer.Id: Optional<Int>]), NoError> in
return context.engine.data.subscribe(
EngineDataMap(peers.map(\.id).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init))
)
|> map { participantCountMap -> ([EnginePeer], [EnginePeer.Id: Optional<Int>]) in
return (peers, participantCountMap)
}
}
self.stateDisposable = combineLatest(
queue: Queue.mainQueue(),
context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)),
adminedChannelsWithParticipants,
savedPeers,
closeFriends,
grayListPeers
)
.start(next: { [weak self] accountPeer, adminedChannelsWithParticipants, savedPeers, closeFriends, grayListPeers in
guard let self else {
return
}
let (adminedChannels, participantCounts) = adminedChannelsWithParticipants
var participants: [EnginePeer.Id: Int] = [:]
for (key, value) in participantCounts {
if let value {
participants[key] = value
}
}
var sendAsPeers: [EnginePeer] = []
if let accountPeer {
sendAsPeers.append(accountPeer)
}
for channel in adminedChannels {
if case let .channel(channel) = channel, channel.hasPermission(.postStories) {
if !sendAsPeers.contains(where: { $0.id == channel.id }) {
sendAsPeers.append(contentsOf: adminedChannels)
}
}
}
let (peersMap, everyonePeers, contactsPeers, selectedPeers) = savedPeers
var savedSelectedPeers: [Stories.Item.Privacy.Base: [EnginePeer.Id]] = [:]
savedSelectedPeers[.everyone] = everyonePeers
savedSelectedPeers[.contacts] = contactsPeers
savedSelectedPeers[.nobody] = selectedPeers
let state = State(
sendAsPeers: sendAsPeers,
peers: [],
peersMap: peersMap,
savedSelectedPeers: savedSelectedPeers,
participants: participants,
closeFriendsPeers: closeFriends,
grayListPeers: grayListPeers
)
self.stateValue = state
self.stateSubject.set(.single(state))
self.readySubject.set(true)
})
case let .chats(isGrayList):
self.stateDisposable = (combineLatest(
context.engine.messages.chatList(group: .root, count: 200) |> take(1),
context.engine.data.get(TelegramEngine.EngineData.Item.Contacts.List(includePresences: true)),
context.engine.data.get(EngineDataMap(Array(self.initialPeerIds).map(TelegramEngine.EngineData.Item.Peer.Peer.init))),
grayListPeers
)
|> mapToSignal { chatList, contacts, initialPeers, grayListPeers -> Signal<(EngineChatList, EngineContactList, [EnginePeer.Id: Optional<EnginePeer>], [EnginePeer.Id: Optional<Int>], [EnginePeer]), NoError> in
return context.engine.data.subscribe(
EngineDataMap(chatList.items.map(\.renderedPeer.peerId).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init))
)
|> map { participantCountMap -> (EngineChatList, EngineContactList, [EnginePeer.Id: Optional<EnginePeer>], [EnginePeer.Id: Optional<Int>], [EnginePeer]) in
return (chatList, contacts, initialPeers, participantCountMap, grayListPeers)
}
}
|> deliverOnMainQueue).start(next: { [weak self] chatList, contacts, initialPeers, participantCounts, grayListPeers in
guard let self else {
return
}
var participants: [EnginePeer.Id: Int] = [:]
for (key, value) in participantCounts {
if let value {
participants[key] = value
}
}
var grayListPeersIds = Set<EnginePeer.Id>()
for peer in grayListPeers {
grayListPeersIds.insert(peer.id)
}
var existingIds = Set<EnginePeer.Id>()
var selectedPeers: [EnginePeer] = []
if isGrayList {
self.initialPeerIds = Set(grayListPeers.map { $0.id })
}
for item in chatList.items.reversed() {
if let peer = item.renderedPeer.peer {
if self.initialPeerIds.contains(peer.id) || isGrayList && grayListPeersIds.contains(peer.id) {
selectedPeers.append(peer)
existingIds.insert(peer.id)
}
}
}
for peerId in self.initialPeerIds {
if !existingIds.contains(peerId), let maybePeer = initialPeers[peerId], let peer = maybePeer {
selectedPeers.append(peer)
existingIds.insert(peerId)
}
}
if isGrayList {
for peer in grayListPeers {
if !existingIds.contains(peer.id) {
selectedPeers.append(peer)
existingIds.insert(peer.id)
}
}
}
var presences: [EnginePeer.Id: EnginePeer.Presence] = [:]
for item in chatList.items {
presences[item.renderedPeer.peerId] = item.presence
}
var peers: [EnginePeer] = []
peers = chatList.items.filter { peer in
if let peer = peer.renderedPeer.peer {
if self.initialPeerIds.contains(peer.id) {
return false
}
if peer.id == context.account.peerId {
return false
}
if peer.isService || peer.isDeleted {
return false
}
if case let .user(user) = peer {
if user.botInfo != nil {
return false
}
}
if case let .channel(channel) = peer {
if channel.isForum {
return false
}
if case .broadcast = channel.info {
return false
}
}
return true
} else {
return false
}
}.reversed().compactMap { $0.renderedPeer.peer }
for peer in peers {
existingIds.insert(peer.id)
}
peers.insert(contentsOf: selectedPeers, at: 0)
let state = State(
peers: peers,
presences: presences,
participants: participants,
grayListPeers: grayListPeers
)
self.stateValue = state
self.stateSubject.set(.single(state))
self.readySubject.set(true)
})
case let .contacts(base):
self.stateDisposable = (context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Contacts.List(includePresences: true)
)
|> deliverOnMainQueue).start(next: { [weak self] contactList in
guard let self else {
return
}
var selectedPeers: [EnginePeer] = []
if case .closeFriends = base {
for peer in contactList.peers {
if case let .user(user) = peer, user.flags.contains(.isCloseFriend) {
selectedPeers.append(peer)
}
}
self.initialPeerIds = Set(selectedPeers.map { $0.id })
} else {
for peer in contactList.peers {
if case let .user(user) = peer, initialPeerIds.contains(user.id), !user.isDeleted {
selectedPeers.append(peer)
}
}
self.initialPeerIds = initialPeerIds
}
selectedPeers = selectedPeers.sorted(by: { lhs, rhs in
let result = lhs.indexName.isLessThan(other: rhs.indexName, ordering: .firstLast)
if result == .orderedSame {
return lhs.id < rhs.id
} else {
return result == .orderedAscending
}
})
var peers: [EnginePeer] = []
peers = contactList.peers.filter { !self.initialPeerIds.contains($0.id) && $0.id != context.account.peerId && !$0.isDeleted }.sorted(by: { lhs, rhs in
let result = lhs.indexName.isLessThan(other: rhs.indexName, ordering: .firstLast)
if result == .orderedSame {
return lhs.id < rhs.id
} else {
return result == .orderedAscending
}
})
peers.insert(contentsOf: selectedPeers, at: 0)
let state = State(
peers: peers,
presences: contactList.presences
)
self.stateValue = state
self.stateSubject.set(.single(state))
self.readySubject.set(true)
})
case let .search(query, onlyContacts):
let signal: Signal<([EngineRenderedPeer], [EnginePeer.Id: Optional<EnginePeer.Presence>], [EnginePeer.Id: Optional<Int>]), NoError>
if onlyContacts {
signal = combineLatest(
context.engine.contacts.searchLocalPeers(query: query),
context.engine.contacts.searchContacts(query: query)
)
|> map { peers, contacts in
let contactIds = Set(contacts.0.map { $0.id })
return (peers.filter { contactIds.contains($0.peerId) }, [:], [:])
}
} else {
signal = context.engine.contacts.searchLocalPeers(query: query)
|> mapToSignal { peers in
return context.engine.data.subscribe(
EngineDataMap(peers.map(\.peerId).map(TelegramEngine.EngineData.Item.Peer.Presence.init)),
EngineDataMap(peers.map(\.peerId).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init))
)
|> map { presenceMap, participantCountMap -> ([EngineRenderedPeer], [EnginePeer.Id: Optional<EnginePeer.Presence>], [EnginePeer.Id: Optional<Int>]) in
return (peers, presenceMap, participantCountMap)
}
}
}
self.stateDisposable = (signal
|> deliverOnMainQueue).start(next: { [weak self] peers, presenceMap, participantCounts in
guard let self else {
return
}
var presences: [EnginePeer.Id: EnginePeer.Presence] = [:]
for (key, value) in presenceMap {
if let value {
presences[key] = value
}
}
var participants: [EnginePeer.Id: Int] = [:]
for (key, value) in participantCounts {
if let value {
participants[key] = value
}
}
let state = State(
peers: peers.compactMap { $0.peer }.filter { peer in
if case let .user(user) = peer {
if user.id == context.account.peerId {
return false
} else if user.botInfo != nil {
return false
} else if peer.isService {
return false
} else if user.isDeleted {
return false
} else {
return true
}
} else if case let .channel(channel) = peer {
if channel.isForum {
return false
}
if case .broadcast = channel.info {
return false
}
return true
} else {
return true
}
},
presences: presences,
participants: participants
)
self.stateValue = state
self.stateSubject.set(.single(state))
self.readySubject.set(true)
})
case let .members(peerId):
let membersState = Promise<ChannelMemberListState>()
let contactsState = Promise<ChannelMemberListState>()
let disposableAndLoadMoreControl: (Disposable, PeerChannelMemberCategoryControl?)
disposableAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.recent(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, updated: { state in
membersState.set(.single(state))
})
let contactsDisposableAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.contacts(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: nil, updated: { state in
contactsState.set(.single(state))
})
let dataDisposable = combineLatest(
queue: Queue.mainQueue(),
contactsState.get(),
membersState.get()
).startStrict(next: { [weak self] contactsState, memberState in
guard let self else {
return
}
var peers: [EnginePeer] = []
var invitedAt: [EnginePeer.Id: Int32] = [:]
var existingPeersIds = Set<EnginePeer.Id>()
for participant in contactsState.list {
if participant.peer.isDeleted || existingPeersIds.contains(participant.peer.id) || participant.participant.adminInfo != nil {
continue
}
if case let .member(_, date, _, _, _) = participant.participant {
invitedAt[participant.peer.id] = date
} else {
continue
}
peers.append(EnginePeer(participant.peer))
existingPeersIds.insert(participant.peer.id)
}
for participant in memberState.list {
if participant.peer.isDeleted || existingPeersIds.contains(participant.peer.id) || participant.participant.adminInfo != nil {
continue
}
if let user = participant.peer as? TelegramUser, user.botInfo != nil {
continue
}
if case let .member(_, date, _, _, _) = participant.participant {
invitedAt[participant.peer.id] = date
} else {
continue
}
peers.append(EnginePeer(participant.peer))
}
let state = State(
peers: peers,
invitedAt: invitedAt
)
self.stateValue = state
self.stateSubject.set(.single(state))
self.readySubject.set(true)
})
let combinedDisposable = DisposableSet()
combinedDisposable.add(contactsDisposableAndLoadMoreControl.0)
combinedDisposable.add(disposableAndLoadMoreControl.0)
combinedDisposable.add(dataDisposable)
self.stateDisposable = combinedDisposable
self.listControl = disposableAndLoadMoreControl.1
}
}
deinit {
self.stateDisposable?.dispose()
}
}
}
final class PeersListStoredState: Codable {
private enum CodingKeys: String, CodingKey {
case peerIds
}
public let peerIds: [EnginePeer.Id]
public init(peerIds: [EnginePeer.Id]) {
self.peerIds = peerIds
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.peerIds = try container.decode([Int64].self, forKey: .peerIds).map { EnginePeer.Id($0) }
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.peerIds.map { $0.toInt64() }, forKey: .peerIds)
}
}
private func peersListStoredState(engine: TelegramEngine, base: Stories.Item.Privacy.Base) -> Signal<[EnginePeer.Id], NoError> {
let key = EngineDataBuffer(length: 4)
key.setInt32(0, value: base.rawValue)
return engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.shareWithPeersState, id: key))
|> map { entry -> [EnginePeer.Id] in
return entry?.get(PeersListStoredState.self)?.peerIds ?? []
}
}
func updatePeersListStoredState(engine: TelegramEngine, base: Stories.Item.Privacy.Base, peerIds: [EnginePeer.Id]) -> Signal<Never, NoError> {
let key = EngineDataBuffer(length: 4)
key.setInt32(0, value: base.rawValue)
let state = PeersListStoredState(peerIds: peerIds)
return engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.shareWithPeersState, id: key, item: state)
}

View File

@ -0,0 +1,95 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 4.335022 4.335007 cm
1.000000 1.000000 1.000000 scn
1.330000 10.665002 m
1.330000 15.820580 5.509422 20.000002 10.665000 20.000002 c
15.820578 20.000002 20.000000 15.820580 20.000000 10.665002 c
20.000000 5.509424 15.820578 1.330002 10.665000 1.330002 c
5.509422 1.330002 1.330000 5.509424 1.330000 10.665002 c
h
10.665000 21.330002 m
4.774883 21.330002 0.000000 16.555119 0.000000 10.665002 c
0.000000 4.774885 4.774883 0.000000 10.665000 0.000000 c
16.555117 0.000000 21.330002 4.774885 21.330002 10.665002 c
21.330002 16.555119 16.555117 21.330002 10.665000 21.330002 c
h
10.665007 16.329979 m
11.032276 16.329979 11.330007 16.032249 11.330007 15.664980 c
11.330007 11.329980 l
15.665007 11.329980 l
16.032276 11.329980 16.330006 11.032249 16.330006 10.664980 c
16.330006 10.297710 16.032276 9.999980 15.665007 9.999980 c
11.330007 9.999980 l
11.330007 5.664980 l
11.330007 5.297710 11.032276 4.999981 10.665007 4.999981 c
10.297737 4.999981 10.000007 5.297710 10.000007 5.664980 c
10.000007 9.999980 l
5.665007 9.999980 l
5.297737 9.999980 5.000007 10.297710 5.000007 10.664980 c
5.000007 11.032249 5.297737 11.329980 5.665007 11.329980 c
10.000007 11.329980 l
10.000007 15.664980 l
10.000007 16.032249 10.297737 16.329979 10.665007 16.329979 c
h
f*
n
Q
endstream
endobj
3 0 obj
1325
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001415 00000 n
0000001438 00000 n
0000001611 00000 n
0000001685 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1744
%%EOF

View File

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

View File

@ -1,7 +1,7 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "AvatarBoost.pdf", "filename" : "Reassign.pdf",
"idiom" : "universal" "idiom" : "universal"
} }
], ],

View File

@ -16,7 +16,7 @@ endobj
endobj endobj
3 0 obj 3 0 obj
<< /Pattern << /P1 << /Matrix [ 105.698799 22.310228 -22.310228 105.698799 -4.867018 -86.125580 ] << /Pattern << /P1 << /Matrix [ 105.698799 22.310228 -22.310228 105.698799 -6.526991 -87.785576 ]
/Shading << /Coords [ 0.000000 0.000000 1.000000 0.000000 ] /Shading << /Coords [ 0.000000 0.000000 1.000000 0.000000 ]
/ColorSpace /DeviceRGB /ColorSpace /DeviceRGB
/Function 1 0 R /Function 1 0 R
@ -35,94 +35,78 @@ stream
/DeviceRGB CS /DeviceRGB CS
/DeviceRGB cs /DeviceRGB cs
q q
q 1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
1.000000 0.000000 -0.000000 1.000000 1.659973 -1.660000 cm
0.949020 0.949020 0.968627 scn
22.340000 15.320000 m
22.340000 9.609375 17.710625 4.980000 12.000000 4.980000 c
12.000000 1.660000 l
19.544210 1.660000 25.660000 7.775789 25.660000 15.320000 c
22.340000 15.320000 l
h
12.000000 4.980000 m
6.289376 4.980000 1.660000 9.609375 1.660000 15.320000 c
-1.660000 15.320000 l
-1.660000 7.775789 4.455791 1.660000 12.000000 1.660000 c
12.000000 4.980000 l
h
1.660000 15.320000 m
1.660000 21.030624 6.289376 25.660000 12.000000 25.660000 c
12.000000 28.980000 l
4.455791 28.980000 -1.660000 22.864208 -1.660000 15.320000 c
1.660000 15.320000 l
h
12.000000 25.660000 m
17.710625 25.660000 22.340000 21.030624 22.340000 15.320000 c
25.660000 15.320000 l
25.660000 22.864208 19.544210 28.980000 12.000000 28.980000 c
12.000000 25.660000 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 1.659973 -1.660000 cm
0.850980 0.850980 0.850980 scn
24.000000 15.320000 m
24.000000 8.692583 18.627417 3.320000 12.000000 3.320000 c
5.372583 3.320000 0.000000 8.692583 0.000000 15.320000 c
0.000000 21.947416 5.372583 27.320000 12.000000 27.320000 c
18.627417 27.320000 24.000000 21.947416 24.000000 15.320000 c
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 1.659973 -1.660000 cm
/Pattern cs /Pattern cs
/P1 scn /P1 scn
24.000000 15.320000 m 24.000000 12.000000 m
24.000000 8.692583 18.627417 3.320000 12.000000 3.320000 c 24.000000 5.372583 18.627417 0.000000 12.000000 0.000000 c
5.372583 3.320000 0.000000 8.692583 0.000000 15.320000 c 5.372583 0.000000 0.000000 5.372583 0.000000 12.000000 c
0.000000 21.947416 5.372583 27.320000 12.000000 27.320000 c 0.000000 18.627417 5.372583 24.000000 12.000000 24.000000 c
18.627417 27.320000 24.000000 21.947416 24.000000 15.320000 c 18.627417 24.000000 24.000000 18.627417 24.000000 12.000000 c
h h
f f
n n
Q Q
Q
q q
1.000000 0.000000 -0.000000 1.000000 8.793701 5.165123 cm 1.000000 0.000000 -0.000000 1.000000 7.133789 3.505161 cm
1.000000 1.000000 1.000000 scn 1.000000 1.000000 1.000000 scn
6.639333 9.995974 m 6.046674 10.689552 m
6.270643 9.995974 5.989172 10.325375 6.046674 10.689552 c 5.989172 10.325375 6.270643 9.995975 6.639333 9.995975 c
6.848394 15.767110 l 9.125995 9.995975 l
6.948168 16.399014 6.121798 16.727470 5.760551 16.199497 c 9.608492 9.995975 9.893639 9.455373 9.621180 9.057164 c
7.531634 6.003213 l
2.337814 11.197033 l
5.760551 16.199497 l
6.121798 16.727470 6.948168 16.399014 6.848394 15.767110 c
6.046674 10.689552 l
h
6.577924 4.609328 m
1.384104 9.803148 l
0.105843 7.934922 l 0.105843 7.934922 l
-0.166615 7.536714 0.118531 6.996113 0.601028 6.996113 c -0.166615 7.536714 0.118531 6.996113 0.601028 6.996113 c
3.087691 6.996113 l 3.087691 6.996113 l
3.456380 6.996113 3.737850 6.666713 3.680349 6.302535 c 3.456380 6.996113 3.737850 6.666713 3.680349 6.302535 c
2.878629 1.224977 l 2.878629 1.224977 l
2.778855 0.593074 3.605225 0.264616 3.966471 0.792590 c 2.778855 0.593074 3.605225 0.264616 3.966471 0.792590 c
9.621180 9.057164 l 6.577924 4.609328 l
9.893639 9.455373 9.608493 9.995974 9.125995 9.995974 c
6.639333 9.995974 l
h h
f* f*
n n
Q Q
q
1.000000 0.000000 -0.000000 1.000000 6.000000 4.540150 cm
1.000000 1.000000 1.000000 scn
0.470226 13.930077 m
0.210527 14.189775 -0.210527 14.189775 -0.470226 13.930077 c
-0.729925 13.670378 -0.729925 13.249323 -0.470226 12.989624 c
0.470226 13.930077 l
h
11.529774 0.989624 m
11.789473 0.729925 12.210527 0.729925 12.470226 0.989624 c
12.729925 1.249323 12.729925 1.670378 12.470226 1.930077 c
11.529774 0.989624 l
h
-0.470226 12.989624 m
11.529774 0.989624 l
12.470226 1.930077 l
0.470226 13.930077 l
-0.470226 12.989624 l
h
f
n
Q
endstream endstream
endobj endobj
5 0 obj 5 0 obj
2168 1598
endobj endobj
6 0 obj 6 0 obj
<< /Annots [] << /Annots []
/Type /Page /Type /Page
/MediaBox [ 0.000000 0.000000 27.319946 27.320000 ] /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 3 0 R /Resources 3 0 R
/Contents 4 0 R /Contents 4 0 R
/Parent 7 0 R /Parent 7 0 R
@ -149,15 +133,15 @@ xref
0000000727 00000 n 0000000727 00000 n
0000000749 00000 n 0000000749 00000 n
0000001379 00000 n 0000001379 00000 n
0000003603 00000 n 0000003033 00000 n
0000003626 00000 n 0000003056 00000 n
0000003799 00000 n 0000003229 00000 n
0000003873 00000 n 0000003303 00000 n
trailer trailer
<< /ID [ (some) (id) ] << /ID [ (some) (id) ]
/Root 8 0 R /Root 8 0 R
/Size 9 /Size 9
>> >>
startxref startxref
3932 3362
%%EOF %%EOF

View File

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

View File

@ -0,0 +1,186 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 12.595703 26.093201 cm
1.000000 1.000000 1.000000 scn
6.271209 0.000001 m
4.615478 0.000001 l
3.771815 0.000001 3.116881 0.213741 2.650675 0.641222 c
2.184469 1.068702 1.951365 1.570590 1.951365 2.146887 c
1.951365 2.710768 2.139227 3.152389 2.514951 3.471749 c
2.890676 3.791109 3.377159 3.950789 3.974400 3.950789 c
4.607714 3.950789 5.148844 3.728570 5.597790 3.284133 c
6.046736 2.839696 6.271209 2.243396 6.271209 1.495234 c
6.271209 0.000001 l
8.333720 0.000001 l
8.333720 1.495234 l
8.333720 2.243396 8.416081 2.839696 8.865445 3.284133 c
9.314810 3.728570 9.859194 3.950789 10.498600 3.950789 c
11.089033 3.950789 11.571902 3.791109 11.947208 3.471749 c
12.322515 3.152389 12.510168 2.710768 12.510168 2.146887 c
12.510168 1.570590 12.277244 1.068702 11.811396 0.641222 c
11.345549 0.213741 10.690793 0.000001 9.847131 0.000001 c
8.333720 0.000001 l
13.456915 0.000001 l
13.768794 0.289816 14.014379 0.629093 14.193670 1.017832 c
14.372962 1.406571 14.462607 1.832700 14.462607 2.296221 c
14.462607 2.976442 14.291677 3.581651 13.949817 4.111848 c
13.607956 4.642046 13.149544 5.056766 12.574580 5.356008 c
11.999616 5.655250 11.356059 5.804871 10.643909 5.804871 c
9.859492 5.804871 9.163199 5.604868 8.555029 5.204864 c
7.946858 4.804859 7.507379 4.239052 7.236590 3.507444 c
6.965801 4.239052 6.524559 4.804859 5.912865 5.204864 c
5.301171 5.604868 4.603175 5.804871 3.818878 5.804871 c
3.113536 5.804871 2.471682 5.655250 1.893314 5.356008 c
1.314945 5.056766 0.854801 4.642046 0.512880 4.111848 c
0.170960 3.581651 0.000000 2.976442 0.000000 2.296221 c
0.000000 1.832700 0.089646 1.406571 0.268938 1.017832 c
0.448229 0.629093 0.693875 0.289816 1.005873 0.000001 c
6.271209 0.000001 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 9.565918 9.928680 cm
1.000000 1.000000 1.000000 scn
9.297392 16.184357 m
9.297392 13.233227 l
9.297392 13.205360 9.304155 13.177911 9.317101 13.153234 c
9.361280 13.069031 9.465354 13.036585 9.549558 13.080763 c
10.010468 13.322584 l
10.210828 13.427706 10.450042 13.427706 10.650402 13.322584 c
11.111313 13.080763 l
11.135988 13.067817 11.163439 13.061052 11.191304 13.061052 c
11.286394 13.061052 11.363479 13.138138 11.363479 13.233227 c
11.363479 16.184357 l
19.283478 16.184357 l
20.044191 16.184357 20.660870 15.567677 20.660870 14.806966 c
20.660870 12.052183 l
20.660870 11.291471 20.044191 10.674791 19.283478 10.674791 c
18.594784 10.674791 l
18.594933 10.657540 l
18.539255 10.668853 18.481627 10.674791 18.422609 10.674791 c
15.055919 10.675303 l
15.696681 10.485133 16.147873 9.883364 16.129395 9.195287 c
16.125700 9.122532 l
16.121429 9.065328 16.113916 9.008718 16.103285 8.952941 c
18.422609 8.953053 l
18.481627 8.953053 18.539255 8.958992 18.594933 8.970304 c
18.594784 2.754792 l
18.594784 1.233368 17.361425 0.000010 15.840000 0.000010 c
11.363479 0.000010 l
11.363479 2.804179 l
11.363479 3.021442 11.261068 3.224711 11.089363 3.354117 c
11.040842 3.387533 l
10.513464 3.718466 l
10.401549 3.788692 10.259320 3.788692 10.147406 3.718466 c
9.620029 3.387533 l
9.419268 3.261555 9.297392 3.041194 9.297392 2.804179 c
9.297392 0.000010 l
4.820869 0.000010 l
3.299445 0.000010 2.066087 1.233368 2.066087 2.754792 c
2.066087 8.953053 l
4.572276 8.953043 l
4.497755 9.343508 4.577706 9.761137 4.823255 10.106102 c
4.874576 10.174096 l
5.066670 10.414385 5.326026 10.587822 5.615777 10.674959 c
1.893913 10.674791 l
1.377391 10.674791 l
0.616679 10.674791 0.000000 11.291471 0.000000 12.052183 c
0.000000 14.806966 l
-0.000000 15.567677 0.616679 16.184357 1.377391 16.184357 c
9.297392 16.184357 l
h
9.899277 12.092366 m
8.916199 9.828564 l
8.846663 9.668441 8.694118 9.560041 8.520034 9.547045 c
6.013629 9.359927 l
5.883370 9.350203 5.763032 9.286716 5.681469 9.184690 c
5.519364 8.981916 5.552333 8.686123 5.755107 8.524018 c
6.579829 7.864708 l
6.972448 7.550834 7.477788 7.414117 7.975103 7.487225 c
10.210711 7.815870 l
10.304756 7.829695 10.396884 7.780842 10.438204 7.695237 c
10.490416 7.587067 10.445051 7.457050 10.336881 7.404838 c
8.412390 6.475926 l
7.984262 6.269278 7.667592 5.886132 7.545246 5.426753 c
7.218948 4.201583 l
7.186707 4.080523 7.204124 3.951603 7.267334 3.843440 c
7.398319 3.619301 7.686204 3.543785 7.910343 3.674770 c
10.093266 4.950454 l
10.239786 5.036079 10.421084 5.036079 10.567604 4.950454 c
12.768568 3.664227 l
12.874888 3.602095 13.001345 3.584166 13.120747 3.614296 c
13.372462 3.677814 13.525026 3.933361 13.461508 4.185077 c
12.875005 6.509334 l
12.830278 6.686582 12.892370 6.873685 13.034194 6.989025 c
14.923353 8.525406 l
15.023630 8.606956 15.085902 8.726200 15.095525 8.855093 c
15.114852 9.113979 14.920650 9.339515 14.661765 9.358842 c
12.140836 9.547045 l
11.966751 9.560041 11.814207 9.668441 11.744673 9.828564 c
10.761594 12.092366 l
10.714136 12.201649 10.626952 12.288834 10.517670 12.336290 c
10.279548 12.439697 10.002684 12.330488 9.899277 12.092366 c
h
f*
n
Q
endstream
endobj
3 0 obj
4952
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 40.000000 40.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000005042 00000 n
0000005065 00000 n
0000005238 00000 n
0000005312 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
5371
%%EOF

View File

@ -930,6 +930,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let controller = PremiumIntroScreen(context: strongSelf.context, source: .gift(from: fromPeerId, to: toPeerId, duration: duration)) let controller = PremiumIntroScreen(context: strongSelf.context, source: .gift(from: fromPeerId, to: toPeerId, duration: duration))
strongSelf.push(controller) strongSelf.push(controller)
return true return true
case let .giftCode(slug, _, _, _):
strongSelf.openResolved(result: .premiumGiftCode(slug: slug), sourceMessageId: message.id)
return true
case let .suggestedProfilePhoto(image): case let .suggestedProfilePhoto(image):
strongSelf.chatDisplayNode.dismissInput() strongSelf.chatDisplayNode.dismissInput()
if let image = image { if let image = image {

View File

@ -101,12 +101,18 @@ final class ChatMessageAttachedContentButtonNode: HighlightTrackingButtonNode {
if let presentationLayer = strongSelf.layer.presentation() { if let presentationLayer = strongSelf.layer.presentation() {
strongSelf.layer.animateScale(from: CGFloat((presentationLayer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0), to: 1.0, duration: 0.25, removeOnCompletion: false) strongSelf.layer.animateScale(from: CGFloat((presentationLayer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0), to: 1.0, duration: 0.25, removeOnCompletion: false)
} }
UIView.transition(with: strongSelf.view, duration: 0.2, options: [.transitionCrossDissolve], animations: { if let snapshot = strongSelf.view.snapshotView(afterScreenUpdates: false) {
strongSelf.backgroundNode.image = strongSelf.regularImage strongSelf.view.addSubview(snapshot)
strongSelf.iconNode.image = strongSelf.regularIconImage
strongSelf.textNode.isHidden = false snapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
strongSelf.highlightedTextNode.isHidden = true snapshot.removeFromSuperview()
}, completion: nil) })
}
strongSelf.backgroundNode.image = strongSelf.regularImage
strongSelf.iconNode.image = strongSelf.regularIconImage
strongSelf.textNode.isHidden = false
strongSelf.highlightedTextNode.isHidden = true
} }
} }
} }

View File

@ -192,6 +192,8 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
result.append((message, ChatMessageProfilePhotoSuggestionContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) result.append((message, ChatMessageProfilePhotoSuggestionContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
} else if case .setChatWallpaper = action.action { } else if case .setChatWallpaper = action.action {
result.append((message, ChatMessageWallpaperBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) result.append((message, ChatMessageWallpaperBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
} else if case .giftCode = action.action {
result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
} else { } else {
result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
} }
@ -223,6 +225,9 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
} else if let _ = media as? TelegramMediaPoll { } else if let _ = media as? TelegramMediaPoll {
result.append((message, ChatMessagePollBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) result.append((message, ChatMessagePollBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
needReactions = false needReactions = false
} else if let _ = media as? TelegramMediaGiveaway {
result.append((message, ChatMessageGiveawayBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
needReactions = false
} else if let _ = media as? TelegramMediaUnsupported { } else if let _ = media as? TelegramMediaUnsupported {
isUnsupportedMedia = true isUnsupportedMedia = true
needReactions = false needReactions = false
@ -321,14 +326,16 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
result.last?.1 == ChatMessagePollBubbleContentNode.self || result.last?.1 == ChatMessagePollBubbleContentNode.self ||
result.last?.1 == ChatMessageContactBubbleContentNode.self || result.last?.1 == ChatMessageContactBubbleContentNode.self ||
result.last?.1 == ChatMessageGameBubbleContentNode.self || result.last?.1 == ChatMessageGameBubbleContentNode.self ||
result.last?.1 == ChatMessageInvoiceBubbleContentNode.self { result.last?.1 == ChatMessageInvoiceBubbleContentNode.self ||
result.last?.1 == ChatMessageGiveawayBubbleContentNode.self {
result.append((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default))) result.append((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default)))
needReactions = false needReactions = false
} else if result.last?.1 == ChatMessageCommentFooterContentNode.self { } else if result.last?.1 == ChatMessageCommentFooterContentNode.self {
if result.count >= 2 { if result.count >= 2 {
if result[result.count - 2].1 == ChatMessageWebpageBubbleContentNode.self || if result[result.count - 2].1 == ChatMessageWebpageBubbleContentNode.self ||
result[result.count - 2].1 == ChatMessagePollBubbleContentNode.self || result[result.count - 2].1 == ChatMessagePollBubbleContentNode.self ||
result[result.count - 2].1 == ChatMessageContactBubbleContentNode.self { result[result.count - 2].1 == ChatMessageContactBubbleContentNode.self ||
result[result.count - 2].1 == ChatMessageGiveawayBubbleContentNode.self {
result.insert((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default)), at: result.count - 1) result.insert((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default)), at: result.count - 1)
} }
} }

View File

@ -17,6 +17,7 @@ import AnimatedStickerNode
import TelegramAnimatedStickerNode import TelegramAnimatedStickerNode
import ChatControllerInteraction import ChatControllerInteraction
import ShimmerEffect import ShimmerEffect
import Markdown
private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id) -> NSAttributedString? { private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id) -> NSAttributedString? {
return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: false, forForumOverview: false) return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: false, forForumOverview: false)
@ -173,42 +174,76 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center)
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
let giftSize = CGSize(width: 220.0, height: 240.0) var giftSize = CGSize(width: 220.0, height: 240.0)
let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, message: EngineMessage(item.message), accountPeerId: item.context.account.peerId) let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, message: EngineMessage(item.message), accountPeerId: item.context.account.peerId)
let primaryTextColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText let primaryTextColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText
var duration: String = "" var months: Int32 = 3
var animationName: String = "" var animationName: String = ""
var title = item.presentationData.strings.Notification_PremiumGift_Title
var text = ""
var buttonTitle = item.presentationData.strings.Notification_PremiumGift_View
var hasServiceMessage = true
var textSpacing: CGFloat = 0.0
for media in item.message.media { for media in item.message.media {
if let action = media as? TelegramMediaAction { if let action = media as? TelegramMediaAction {
switch action.action { switch action.action {
case let .giftPremium(_, _, months, _, _): case let .giftPremium(_, _, monthsValue, _, _):
duration = item.presentationData.strings.Notification_PremiumGift_Subtitle(item.presentationData.strings.Notification_PremiumGift_Months(months)).string months = monthsValue
switch months { text = item.presentationData.strings.Notification_PremiumGift_Subtitle(item.presentationData.strings.Notification_PremiumGift_Months(months)).string
case 12: case let .giftCode(_, fromGiveaway, channelId, monthsValue):
animationName = "Gift12" if fromGiveaway {
case 6: giftSize.width += 34.0
animationName = "Gift6" giftSize.height += 84.0
case 3: textSpacing += 20.0
animationName = "Gift3"
default: title = "Congratulations!"
animationName = "Gift3" var peerName = ""
if let channelId, let channel = item.message.peers[channelId] {
peerName = EnginePeer(channel).compactDisplayTitle
}
text = "You won a prize in a giveaway organized by **\(peerName)**.\n\nYour prize is a **Telegram Premium** subscription for **\(monthsValue)** months."
} else {
text = item.presentationData.strings.Notification_PremiumGift_Subtitle(item.presentationData.strings.Notification_PremiumGift_Months(months)).string
} }
months = monthsValue
buttonTitle = "Open Gift Link"
hasServiceMessage = false
default: default:
break break
} }
} }
} }
switch months {
case 12:
animationName = "Gift12"
case 6:
animationName = "Gift6"
case 3:
animationName = "Gift3"
default:
animationName = "Gift3"
}
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Notification_PremiumGift_Title, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: duration, font: Font.regular(13.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: primaryTextColor),
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor),
linkAttribute: { url in
return ("URL", url)
}
), textAlignment: .center)
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (buttonTitleLayout, buttonTitleApply) = makeButtonTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Notification_PremiumGift_View, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) let (buttonTitleLayout, buttonTitleApply) = makeButtonTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: buttonTitle, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
var labelRects = labelLayout.linesRects() var labelRects = labelLayout.linesRects()
if labelRects.count > 1 { if labelRects.count > 1 {
@ -233,14 +268,23 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
let backgroundMaskImage: (CGPoint, UIImage)? let backgroundMaskImage: (CGPoint, UIImage)?
var backgroundMaskUpdated = false var backgroundMaskUpdated = false
if let (currentOffset, currentImage, currentRects) = cachedMaskBackgroundImage, currentRects == labelRects { if hasServiceMessage {
backgroundMaskImage = (currentOffset, currentImage) if let (currentOffset, currentImage, currentRects) = cachedMaskBackgroundImage, currentRects == labelRects {
backgroundMaskImage = (currentOffset, currentImage)
} else {
backgroundMaskImage = LinkHighlightingNode.generateImage(color: .black, inset: 0.0, innerRadius: 10.0, outerRadius: 10.0, rects: labelRects, useModernPathCalculation: false)
backgroundMaskUpdated = true
}
} else { } else {
backgroundMaskImage = LinkHighlightingNode.generateImage(color: .black, inset: 0.0, innerRadius: 10.0, outerRadius: 10.0, rects: labelRects, useModernPathCalculation: false) backgroundMaskImage = nil
backgroundMaskUpdated = true
} }
let backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: labelLayout.size.height + giftSize.height + 18.0) var backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: giftSize.height)
if hasServiceMessage {
backgroundSize.height += labelLayout.size.height + 18.0
} else {
backgroundSize.height += 4.0
}
return (backgroundSize.width, { boundingWidth in return (backgroundSize.width, { boundingWidth in
return (backgroundSize, { [weak self] animation, synchronousLoads, _ in return (backgroundSize, { [weak self] animation, synchronousLoads, _ in
@ -253,9 +297,11 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.updateVisibility() strongSelf.updateVisibility()
strongSelf.labelNode.isHidden = !hasServiceMessage
strongSelf.backgroundColorNode.backgroundColor = selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper) strongSelf.backgroundColorNode.backgroundColor = selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - giftSize.width) / 2.0), y: labelLayout.size.height + 16.0), size: giftSize) let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - giftSize.width) / 2.0), y: hasServiceMessage ? labelLayout.size.height + 16.0 : 0.0), size: giftSize)
let mediaBackgroundFrame = imageFrame.insetBy(dx: -2.0, dy: -2.0) let mediaBackgroundFrame = imageFrame.insetBy(dx: -2.0, dy: -2.0)
strongSelf.mediaBackgroundNode.frame = mediaBackgroundFrame strongSelf.mediaBackgroundNode.frame = mediaBackgroundFrame
@ -278,7 +324,7 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
let titleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - titleLayout.size.width) / 2.0) , y: mediaBackgroundFrame.minY + 151.0), size: titleLayout.size) let titleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - titleLayout.size.width) / 2.0) , y: mediaBackgroundFrame.minY + 151.0), size: titleLayout.size)
strongSelf.titleNode.frame = titleFrame strongSelf.titleNode.frame = titleFrame
let subtitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY - 1.0), size: subtitleLayout.size) let subtitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY + textSpacing), size: subtitleLayout.size)
strongSelf.subtitleNode.frame = subtitleFrame strongSelf.subtitleNode.frame = subtitleFrame
let buttonTitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonTitleLayout.size.width) / 2.0), y: subtitleFrame.maxY + 18.0), size: buttonTitleLayout.size) let buttonTitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonTitleLayout.size.width) / 2.0), y: subtitleFrame.maxY + 18.0), size: buttonTitleLayout.size)

View File

@ -0,0 +1,518 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import AvatarNode
import AccountContext
import PhoneNumberFormat
import TelegramStringFormatting
import Markdown
import ShimmerEffect
import AnimatedStickerNode
import TelegramAnimatedStickerNode
private let titleFont = Font.medium(15.0)
private let textFont = Font.regular(13.0)
private let boldTextFont = Font.semibold(13.0)
class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
private let dateAndStatusNode: ChatMessageDateAndStatusNode
private let placeholderNode: StickerShimmerEffectNode
private let animationNode: AnimatedStickerNode
private let prizeTitleNode: TextNode
private let prizeTextNode: TextNode
private let participantsTitleNode: TextNode
private let participantsTextNode: TextNode
private let dateTitleNode: TextNode
private let dateTextNode: TextNode
private var giveaway: TelegramMediaGiveaway?
private let buttonNode: ChatMessageAttachedContentButtonNode
override var visibility: ListViewItemNodeVisibility {
didSet {
let wasVisible = oldValue != .none
let isVisible = self.visibility != .none
if wasVisible != isVisible {
self.visibilityStatus = isVisible
}
}
}
private var visibilityStatus: Bool? {
didSet {
if self.visibilityStatus != oldValue {
self.updateVisibility()
}
}
}
private var setupTimestamp: Double?
required init() {
self.placeholderNode = StickerShimmerEffectNode()
self.placeholderNode.isUserInteractionEnabled = false
self.placeholderNode.alpha = 0.75
self.animationNode = DefaultAnimatedStickerNodeImpl()
self.dateAndStatusNode = ChatMessageDateAndStatusNode()
self.prizeTitleNode = TextNode()
self.prizeTextNode = TextNode()
self.participantsTitleNode = TextNode()
self.participantsTextNode = TextNode()
self.dateTitleNode = TextNode()
self.dateTextNode = TextNode()
self.buttonNode = ChatMessageAttachedContentButtonNode()
super.init()
self.addSubnode(self.prizeTitleNode)
self.addSubnode(self.prizeTextNode)
self.addSubnode(self.participantsTitleNode)
self.addSubnode(self.participantsTextNode)
self.addSubnode(self.dateTitleNode)
self.addSubnode(self.dateTextNode)
self.addSubnode(self.buttonNode)
self.addSubnode(self.animationNode)
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.dateAndStatusNode.reactionSelected = { [weak self] value in
guard let strongSelf = self, let item = strongSelf.item else {
return
}
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value))
}
self.dateAndStatusNode.openReactionPreview = { [weak self] gesture, sourceView, value in
guard let strongSelf = self, let item = strongSelf.item else {
gesture?.cancel()
return
}
item.controllerInteraction.openMessageReactionContextMenu(item.topMessage, sourceView, gesture, value)
}
}
override func accessibilityActivate() -> Bool {
self.buttonPressed()
return true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func didLoad() {
super.didLoad()
// let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.contactTap(_:)))
// self.view.addGestureRecognizer(tapRecognizer)
}
private func removePlaceholder(animated: Bool) {
self.placeholderNode.alpha = 0.0
if !animated {
self.placeholderNode.removeFromSupernode()
} else {
self.placeholderNode.layer.animateAlpha(from: self.placeholderNode.alpha, to: 0.0, duration: 0.2, completion: { [weak self] _ in
self?.placeholderNode.removeFromSupernode()
})
}
}
override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
let statusLayout = self.dateAndStatusNode.asyncLayout()
let makePrizeTitleLayout = TextNode.asyncLayout(self.prizeTitleNode)
let makePrizeTextLayout = TextNode.asyncLayout(self.prizeTextNode)
let makeParticipantsTitleLayout = TextNode.asyncLayout(self.participantsTitleNode)
let makeParticipantsTextLayout = TextNode.asyncLayout(self.participantsTextNode)
let makeDateTitleLayout = TextNode.asyncLayout(self.dateTitleNode)
let makeDateTextLayout = TextNode.asyncLayout(self.dateTextNode)
let makeButtonLayout = ChatMessageAttachedContentButtonNode.asyncLayout(self.buttonNode)
return { item, layoutConstants, _, _, constrainedSize, _ in
var giveaway: TelegramMediaGiveaway?
for media in item.message.media {
if let media = media as? TelegramMediaGiveaway {
giveaway = media;
}
}
var incoming = item.message.effectivelyIncoming(item.context.account.peerId)
if case .forwardedMessages = item.associatedData.subject {
incoming = false
}
let textColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.primaryTextColor : item.presentationData.theme.theme.chat.message.outgoing.primaryTextColor
let prizeTitleString = NSAttributedString(string: "Giveaway Prizes", font: titleFont, textColor: textColor)
var prizeTextString: NSAttributedString?
if let giveaway {
prizeTextString = parseMarkdownIntoAttributedString("**\(giveaway.quantity)** Telegram Premium Subscriptions for **\(giveaway.months)** months.", attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
link: MarkdownAttributeSet(font: textFont, textColor: textColor),
linkAttribute: { url in
return ("URL", url)
}
), textAlignment: .center)
}
let participantsTitleString = NSAttributedString(string: "Participants", font: titleFont, textColor: textColor)
let participantsTextString = NSAttributedString(string: "All subscribers of this channel:", font: textFont, textColor: textColor)
let dateTitleString = NSAttributedString(string: "Winners Selection Date", font: titleFont, textColor: textColor)
var dateTextString: NSAttributedString?
if let giveaway {
dateTextString = NSAttributedString(string: stringForFullDate(timestamp: giveaway.untilDate, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat), font: textFont, textColor: textColor)
}
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
let sideInsets = layoutConstants.text.bubbleInsets.right * 2.0
let maxTextWidth = min(200.0, max(1.0, constrainedSize.width - 7.0 - sideInsets))
let (prizeTitleLayout, prizeTitleApply) = makePrizeTitleLayout(TextNodeLayoutArguments(attributedString: prizeTitleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (prizeTextLayout, prizeTextApply) = makePrizeTextLayout(TextNodeLayoutArguments(attributedString: prizeTextString, backgroundColor: nil, maximumNumberOfLines: 5, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (participantsTitleLayout, participantsTitleApply) = makeParticipantsTitleLayout(TextNodeLayoutArguments(attributedString: participantsTitleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (participantsTextLayout, participantsTextApply) = makeParticipantsTextLayout(TextNodeLayoutArguments(attributedString: participantsTextString, backgroundColor: nil, maximumNumberOfLines: 5, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (dateTitleLayout, dateTitleApply) = makeDateTitleLayout(TextNodeLayoutArguments(attributedString: dateTitleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (dateTextLayout, dateTextApply) = makeDateTextLayout(TextNodeLayoutArguments(attributedString: dateTextString, backgroundColor: nil, maximumNumberOfLines: 5, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
var edited = false
if item.attributes.updatingMedia != nil {
edited = true
}
var viewCount: Int?
var dateReplies = 0
var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeer: item.associatedData.accountPeer, message: item.message)
if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) {
dateReactionsAndPeers = ([], [])
}
for attribute in item.message.attributes {
if let attribute = attribute as? EditedMessageAttribute {
edited = !attribute.isHidden
} else if let attribute = attribute as? ViewCountMessageAttribute {
viewCount = attribute.count
} else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = item.chatLocation {
if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info {
dateReplies = Int(attribute.count)
}
}
}
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, associatedData: item.associatedData)
let statusType: ChatMessageDateAndStatusType?
switch position {
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
if incoming {
statusType = .BubbleIncoming
} else {
if item.message.flags.contains(.Failed) {
statusType = .BubbleOutgoing(.Failed)
} else if (item.message.flags.isSending && !item.message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil {
statusType = .BubbleOutgoing(.Sending)
} else {
statusType = .BubbleOutgoing(.Sent(read: item.read))
}
}
default:
statusType = nil
}
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))?
if let statusType = statusType {
var isReplyThread = false
if case .replyThread = item.chatLocation {
isReplyThread = true
}
statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments(
context: item.context,
presentationData: item.presentationData,
edited: edited,
impressionCount: viewCount,
dateText: dateText,
type: statusType,
layoutInput: .trailingContent(contentWidth: 1000.0, reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions) ? ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: true, preferAdditionalInset: false) : nil),
constrainedSize: CGSize(width: constrainedSize.width - sideInsets, height: .greatestFiniteMagnitude),
availableReactions: item.associatedData.availableReactions,
reactions: dateReactionsAndPeers.reactions,
reactionPeers: dateReactionsAndPeers.peers,
displayAllReactionPeers: item.message.id.peerId.namespace == Namespaces.Peer.CloudUser,
replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread,
hasAutoremove: item.message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: item.message),
animationCache: item.controllerInteraction.presentationContext.animationCache,
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
))
}
let buttonImage: UIImage
let buttonHighlightedImage: UIImage
let titleColor: UIColor
let titleHighlightedColor: UIColor
if incoming {
buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonIncoming(item.presentationData.theme.theme)!
buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIncoming(item.presentationData.theme.theme)!
titleColor = item.presentationData.theme.theme.chat.message.incoming.accentTextColor
let bubbleColors = bubbleColorComponents(theme: item.presentationData.theme.theme, incoming: true, wallpaper: !item.presentationData.theme.wallpaper.isEmpty)
titleHighlightedColor = bubbleColors.fill[0]
} else {
buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonOutgoing(item.presentationData.theme.theme)!
buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonOutgoing(item.presentationData.theme.theme)!
titleColor = item.presentationData.theme.theme.chat.message.outgoing.accentTextColor
let bubbleColors = bubbleColorComponents(theme: item.presentationData.theme.theme, incoming: false, wallpaper: !item.presentationData.theme.wallpaper.isEmpty)
titleHighlightedColor = bubbleColors.fill[0]
}
let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, nil, nil, false, "HOW DOES IT WORK?", titleColor, titleHighlightedColor, false)
let months = giveaway?.months ?? 0
let animationName: String
switch months {
case 12:
animationName = "Gift12"
case 6:
animationName = "Gift6"
case 3:
animationName = "Gift3"
default:
animationName = "Gift3"
}
var maxContentWidth: CGFloat = 0.0
if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue {
maxContentWidth = max(maxContentWidth, statusSuggestedWidthAndContinue.0)
}
maxContentWidth = max(maxContentWidth, prizeTitleLayout.size.width)
maxContentWidth = max(maxContentWidth, prizeTextLayout.size.width)
maxContentWidth = max(maxContentWidth, participantsTitleLayout.size.width)
maxContentWidth = max(maxContentWidth, participantsTextLayout.size.width)
maxContentWidth = max(maxContentWidth, dateTitleLayout.size.width)
maxContentWidth = max(maxContentWidth, dateTextLayout.size.width)
maxContentWidth = max(maxContentWidth, buttonWidth)
maxContentWidth += 30.0
let contentWidth = maxContentWidth + layoutConstants.text.bubbleInsets.right * 2.0
return (contentWidth, { boundingWidth in
let (buttonSize, buttonApply) = continueLayout(boundingWidth - layoutConstants.text.bubbleInsets.right * 2.0)
let buttonSpacing: CGFloat = 4.0
let statusSizeAndApply = statusSuggestedWidthAndContinue?.1(boundingWidth - sideInsets)
var layoutSize = CGSize(width: contentWidth, height: 49.0 + prizeTitleLayout.size.height + prizeTextLayout.size.height + participantsTitleLayout.size.height + participantsTextLayout.size.height + dateTitleLayout.size.height + dateTextLayout.size.height + buttonSize.height + buttonSpacing + 100.0)
if let statusSizeAndApply = statusSizeAndApply {
layoutSize.height += statusSizeAndApply.0.height - 4.0
}
let buttonFrame = CGRect(origin: CGPoint(x: layoutConstants.text.bubbleInsets.right, y: layoutSize.height - 9.0 - buttonSize.height), size: buttonSize)
return (layoutSize, { [weak self] animation, synchronousLoads, _ in
if let strongSelf = self {
if strongSelf.item == nil {
strongSelf.animationNode.autoplay = true
strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: animationName), width: 384, height: 384, playbackMode: .still(.start), mode: .direct(cachePathPrefix: nil))
}
strongSelf.item = item
strongSelf.giveaway = giveaway
strongSelf.updateVisibility()
let _ = prizeTitleApply()
let _ = prizeTextApply()
let _ = participantsTitleApply()
let _ = participantsTextApply()
let _ = dateTitleApply()
let _ = dateTextApply()
let _ = buttonApply()
let smallSpacing: CGFloat = 2.0
let largeSpacing: CGFloat = 14.0
var originY: CGFloat = 0.0
let iconSize = CGSize(width: 140.0, height: 140.0)
strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - iconSize.width) / 2.0), y: originY - 50.0), size: iconSize)
strongSelf.animationNode.updateLayout(size: iconSize)
originY += 95.0
strongSelf.prizeTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - prizeTitleLayout.size.width) / 2.0), y: originY), size: prizeTitleLayout.size)
originY += prizeTitleLayout.size.height + smallSpacing
strongSelf.prizeTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - prizeTextLayout.size.width) / 2.0), y: originY), size: prizeTextLayout.size)
originY += prizeTextLayout.size.height + largeSpacing
strongSelf.participantsTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - participantsTitleLayout.size.width) / 2.0), y: originY), size: participantsTitleLayout.size)
originY += participantsTitleLayout.size.height + smallSpacing
strongSelf.participantsTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - participantsTextLayout.size.width) / 2.0), y: originY), size: participantsTextLayout.size)
originY += participantsTextLayout.size.height + largeSpacing
strongSelf.dateTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - dateTitleLayout.size.width) / 2.0), y: originY), size: dateTitleLayout.size)
originY += dateTitleLayout.size.height + smallSpacing
strongSelf.dateTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - dateTextLayout.size.width) / 2.0), y: originY), size: dateTextLayout.size)
originY += dateTextLayout.size.height + largeSpacing
strongSelf.buttonNode.frame = buttonFrame
if let statusSizeAndApply = statusSizeAndApply {
strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: layoutConstants.text.bubbleInsets.left, y: strongSelf.dateTextNode.frame.maxY + 2.0), size: statusSizeAndApply.0)
if strongSelf.dateAndStatusNode.supernode == nil {
strongSelf.addSubnode(strongSelf.dateAndStatusNode)
statusSizeAndApply.1(.None)
} else {
statusSizeAndApply.1(animation)
}
} else if strongSelf.dateAndStatusNode.supernode != nil {
strongSelf.dateAndStatusNode.removeFromSupernode()
}
if let forwardInfo = item.message.forwardInfo, forwardInfo.flags.contains(.isImported) {
strongSelf.dateAndStatusNode.pressed = {
guard let strongSelf = self else {
return
}
item.controllerInteraction.displayImportedMessageTooltip(strongSelf.dateAndStatusNode)
}
} else {
strongSelf.dateAndStatusNode.pressed = nil
}
if let (rect, size) = strongSelf.absoluteRect {
strongSelf.updateAbsoluteRect(rect, within: size)
}
}
})
})
})
}
}
private func updateVisibility() {
// guard let item = self.item else {
// return
// }
//
// let isPlaying = self.visibilityStatus == true
// if self.isPlaying != isPlaying {
// self.isPlaying = isPlaying
// self.animationNode.visibility = isPlaying
// }
//
// if isPlaying && self.setupTimestamp == nil {
// self.setupTimestamp = CACurrentMediaTime()
// }
//
// if isPlaying {
// var alreadySeen = true
//
// if item.message.flags.contains(.Incoming) {
// if let unreadRange = item.controllerInteraction.unreadMessageRange[UnreadMessageRangeKey(peerId: item.message.id.peerId, namespace: item.message.id.namespace)] {
// if unreadRange.contains(item.message.id.id) {
// alreadySeen = false
// }
// }
// } else {
// if item.controllerInteraction.playNextOutgoingGift && !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) {
// alreadySeen = false
// }
// }
//
// if !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) {
// item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id)
// self.animationNode.playOnce()
// }
//
// if !alreadySeen && self.animationNode.isPlaying {
// item.controllerInteraction.playNextOutgoingGift = false
// Queue.mainQueue().after(1.0) {
// item.controllerInteraction.animateDiceSuccess(false, true)
// }
// }
// }
}
private var absoluteRect: (CGRect, CGSize)?
override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
self.absoluteRect = (rect, containerSize)
self.placeholderNode.updateAbsoluteRect(CGRect(origin: CGPoint(x: rect.minX + self.placeholderNode.frame.minX, y: rect.minY + self.placeholderNode.frame.minY), size: self.placeholderNode.frame.size), within: containerSize)
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if self.buttonNode.frame.contains(point) {
return .openMessage
}
if self.dateAndStatusNode.supernode != nil, let _ = self.dateAndStatusNode.hitTest(self.view.convert(point, to: self.dateAndStatusNode.view), with: nil) {
return .ignore
}
return .none
}
@objc func contactTap(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
if let item = self.item {
let _ = item.controllerInteraction.openMessage(item.message, .default)
}
}
}
@objc private func buttonPressed() {
if let item = self.item {
let _ = item.controllerInteraction.openMessage(item.message, .default)
}
}
override func reactionTargetView(value: MessageReaction.Reaction) -> UIView? {
if !self.dateAndStatusNode.isHidden {
return self.dateAndStatusNode.reactionView(value: value)
}
return nil
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.dateAndStatusNode.supernode != nil, let result = self.dateAndStatusNode.hitTest(self.view.convert(point, to: self.dateAndStatusNode.view), with: event) {
return result
}
return super.hitTest(point, with: event)
}
}

View File

@ -478,7 +478,6 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible {
break loop break loop
case let .Video(_, _, flags, _): case let .Video(_, _, flags, _):
if flags.contains(.instantRoundVideo) { if flags.contains(.instantRoundVideo) {
// viewClassName = ChatMessageInstantVideoItemNode.self
viewClassName = ChatMessageBubbleItemNode.self viewClassName = ChatMessageBubbleItemNode.self
break loop break loop
} }

View File

@ -1068,6 +1068,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
break break
case .boost: case .boost:
break break
case .premiumGiftCode:
break
} }
} }
})) }))

View File

@ -148,6 +148,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
deinit { deinit {
self.createActionDisposable.dispose() self.createActionDisposable.dispose()
self.presentationDataDisposable?.dispose() self.presentationDataDisposable?.dispose()
self.confirmationDisposable.dispose()
} }
@objc private func beginSearch() { @objc private func beginSearch() {

View File

@ -881,8 +881,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
case .ok: case .ok:
updateImpl?() updateImpl?()
case let .replace(previousPeer): case let .replace(previousPeer):
let text = presentationData.strings.ChannelBoost_ReplaceBoost(previousPeer.compactDisplayTitle, peer.compactDisplayTitle).string let controller = replaceBoostConfirmationController(context: context, fromPeers: [previousPeer], toPeer: peer, commit: {
let controller = replaceBoostConfirmationController(context: context, fromPeer: previousPeer, toPeer: peer, text: text, commit: {
updateImpl?() updateImpl?()
}) })
present(controller, nil) present(controller, nil)
@ -953,5 +952,27 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
controller?.dismiss() controller?.dismiss()
} }
}) })
case let .premiumGiftCode(slug):
var forceDark = false
if let updatedPresentationData, updatedPresentationData.initial.theme.overallDarkAppearance {
forceDark = true
}
let _ = (context.engine.payments.checkPremiumGiftCode(slug: slug)
|> deliverOnMainQueue).startStandalone(next: { giftCode in
if let giftCode {
var dismissImpl: (() -> Void)?
let controller = PremiumGiftCodeScreen(context: context, giftCode: giftCode, forceDark: forceDark, action: {
dismissImpl?()
let _ = context.engine.payments.applyPremiumGiftCode(slug: slug).startStandalone()
})
dismissImpl = { [weak controller] in
controller?.dismiss()
}
navigationController?.pushViewController(controller)
} else {
present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
}
})
} }
} }

View File

@ -11,8 +11,82 @@ import AccountContext
import AppBundle import AppBundle
import AvatarNode import AvatarNode
import Markdown import Markdown
import CheckNode
private final class PhotoUpdateConfirmationAlertContentNode: AlertContentNode { private func generateBoostIcon(theme: PresentationTheme) -> UIImage? {
if let image = UIImage(bundleImageName: "Premium/AvatarBoost") {
let size = CGSize(width: image.size.width + 4.0, height: image.size.height + 4.0)
return generateImage(size, contextGenerator: { size, context in
let bounds = CGRect(origin: .zero, size: size)
context.clear(bounds)
if let cgImage = image.cgImage {
context.draw(cgImage, in: CGRect(origin: CGPoint(x: 2.0, y: 2.0), size: image.size))
}
let lineWidth = 2.0 - UIScreenPixel
context.setLineWidth(lineWidth)
context.setStrokeColor(theme.actionSheet.opaqueItemBackgroundColor.cgColor)
context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0 + UIScreenPixel, dy: lineWidth / 2.0 + UIScreenPixel))
}, opaque: false)
}
return nil
}
private final class PreviousBoostNode: ASDisplayNode {
let checkNode: InteractiveCheckNode
let avatarNode: AvatarNode
let labelNode: ImmediateTextNode
var pressed: (PreviousBoostNode) -> Void = { _ in }
init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, peer: EnginePeer, badge: String?) {
self.checkNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: theme.accentColor, strokeColor: theme.contrastColor, borderColor: theme.controlBorderColor, overlayBorder: false, hasInset: false, hasShadow: false))
self.checkNode.setSelected(false, animated: false)
self.labelNode = ImmediateTextNode()
self.labelNode.maximumNumberOfLines = 4
self.labelNode.isUserInteractionEnabled = true
self.labelNode.attributedText = NSAttributedString(string: peer.compactDisplayTitle, font: Font.semibold(13.0), textColor: theme.primaryColor)
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 13.0))
super.init()
self.addSubnode(self.checkNode)
self.addSubnode(self.avatarNode)
self.addSubnode(self.labelNode)
self.avatarNode.setPeer(context: context, theme: ptheme, peer: peer)
self.checkNode.valueChanged = { [weak self] value in
if let self {
if value {
self.pressed(self)
}
}
}
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let checkSize = CGSize(width: 22.0, height: 22.0)
let condensedSize = CGSize(width: size.width - 76.0, height: size.height)
let avatarSize = CGSize(width: 30.0, height: 30.0)
let labelSize = self.labelNode.updateLayout(condensedSize)
transition.updateFrame(node: self.checkNode, frame: CGRect(origin: CGPoint(x: 12.0, y: -2.0), size: checkSize))
transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: 46.0, y: -8.0), size: avatarSize))
transition.updateFrame(node: self.labelNode, frame: CGRect(origin: CGPoint(x: 84.0, y: 0.0), size: labelSize))
return CGSize(width: size.width, height: checkSize.height)
}
func setChecked(_ checked: Bool) {
self.checkNode.setSelected(checked, animated: false)
}
}
private final class ReplaceBoostConfirmationAlertContentNode: AlertContentNode {
private let strings: PresentationStrings private let strings: PresentationStrings
private let text: String private let text: String
@ -26,13 +100,15 @@ private final class PhotoUpdateConfirmationAlertContentNode: AlertContentNode {
private let actionNodes: [TextAlertContentActionNode] private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode] private let actionVerticalSeparators: [ASDisplayNode]
private var boostNodes: [PreviousBoostNode] = []
private var validLayout: CGSize? private var validLayout: CGSize?
override var dismissOnOutsideTap: Bool { override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled return self.isUserInteractionEnabled
} }
init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, fromPeer: EnginePeer, toPeer: EnginePeer, text: String, actions: [TextAlertAction]) { init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, fromPeers: [EnginePeer], toPeer: EnginePeer, text: String, actions: [TextAlertAction]) {
self.strings = strings self.strings = strings
self.text = text self.text = text
@ -49,7 +125,7 @@ private final class PhotoUpdateConfirmationAlertContentNode: AlertContentNode {
self.iconNode = ASImageNode() self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false self.iconNode.displaysAsynchronously = false
self.iconNode.image = UIImage(bundleImageName: "Premium/AvatarBoost") self.iconNode.image = generateBoostIcon(theme: ptheme)
self.actionNodesSeparator = ASDisplayNode() self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true self.actionNodesSeparator.isLayerBacked = true
@ -68,6 +144,18 @@ private final class PhotoUpdateConfirmationAlertContentNode: AlertContentNode {
} }
self.actionVerticalSeparators = actionVerticalSeparators self.actionVerticalSeparators = actionVerticalSeparators
var boostNodes: [PreviousBoostNode] = []
if fromPeers.count > 1 {
for peer in fromPeers {
let boostNode = PreviousBoostNode(context: context, theme: theme, ptheme: ptheme, peer: peer, badge: nil)
if boostNodes.isEmpty {
boostNode.setChecked(true)
}
boostNodes.append(boostNode)
}
}
self.boostNodes = boostNodes
super.init() super.init()
self.addSubnode(self.textNode) self.addSubnode(self.textNode)
@ -86,9 +174,20 @@ private final class PhotoUpdateConfirmationAlertContentNode: AlertContentNode {
self.addSubnode(separatorNode) self.addSubnode(separatorNode)
} }
for boostNode in self.boostNodes {
boostNode.pressed = { [weak self] sender in
if let self {
for node in self.boostNodes {
node.setChecked(node === sender)
}
}
}
self.addSubnode(boostNode)
}
self.updateTheme(theme) self.updateTheme(theme)
self.avatarNode.setPeer(context: context, theme: ptheme, peer: fromPeer) self.avatarNode.setPeer(context: context, theme: ptheme, peer: fromPeers.first!)
self.secondAvatarNode.setPeer(context: context, theme: ptheme, peer: toPeer) self.secondAvatarNode.setPeer(context: context, theme: ptheme, peer: toPeer)
} }
@ -145,8 +244,10 @@ private final class PhotoUpdateConfirmationAlertContentNode: AlertContentNode {
origin.y += avatarSize.height + 10.0 origin.y += avatarSize.height + 10.0
var entriesHeight: CGFloat = 0.0
let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height)) let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)) transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
origin.y += textSize.height + 10.0
let actionButtonHeight: CGFloat = 44.0 let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0 var minActionsWidth: CGFloat = 0.0
@ -171,6 +272,17 @@ private final class PhotoUpdateConfirmationAlertContentNode: AlertContentNode {
let contentWidth = max(size.width, minActionsWidth) let contentWidth = max(size.width, minActionsWidth)
if !self.boostNodes.isEmpty {
origin.y += 17.0
for boostNode in self.boostNodes {
let boostSize = boostNode.updateLayout(size: size, transition: transition)
transition.updateFrame(node: boostNode, frame: CGRect(origin: CGPoint(x: 36.0, y: origin.y), size: boostSize))
entriesHeight += boostSize.height + 20.0
origin.y += boostSize.height + 20.0
}
}
var actionsHeight: CGFloat = 0.0 var actionsHeight: CGFloat = 0.0
switch effectiveActionLayout { switch effectiveActionLayout {
case .horizontal: case .horizontal:
@ -179,8 +291,7 @@ private final class PhotoUpdateConfirmationAlertContentNode: AlertContentNode {
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
} }
let resultSize = CGSize(width: contentWidth, height: avatarSize.height + textSize.height + actionsHeight + 16.0 + insets.top + insets.bottom) let resultSize = CGSize(width: contentWidth, height: avatarSize.height + textSize.height + entriesHeight + actionsHeight + 16.0 + insets.top + insets.bottom)
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
var actionOffset: CGFloat = 0.0 var actionOffset: CGFloat = 0.0
@ -230,21 +341,31 @@ private final class PhotoUpdateConfirmationAlertContentNode: AlertContentNode {
} }
} }
func replaceBoostConfirmationController(context: AccountContext, fromPeer: EnginePeer, toPeer: EnginePeer, text: String, commit: @escaping () -> Void) -> AlertController { func replaceBoostConfirmationController(context: AccountContext, fromPeers: [EnginePeer], toPeer: EnginePeer, commit: @escaping () -> Void) -> AlertController {
let fromPeers = [fromPeers.first!, fromPeers.first!]
let theme = defaultDarkColorPresentationTheme let theme = defaultDarkColorPresentationTheme
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings let strings = presentationData.strings
let text: String
if fromPeers.count > 1 {
text = "To boost **\(toPeer.compactDisplayTitle)**, reassign a previous boost from:"
//strings.ChannelBoost_ReplaceBoost(previousPeer.compactDisplayTitle, toPeer.compactDisplayTitle).string
} else {
text = strings.ChannelBoost_ReplaceBoost(fromPeers.first!.compactDisplayTitle, toPeer.compactDisplayTitle).string
}
var dismissImpl: ((Bool) -> Void)? var dismissImpl: ((Bool) -> Void)?
var contentNode: PhotoUpdateConfirmationAlertContentNode? var contentNode: ReplaceBoostConfirmationAlertContentNode?
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?(true) dismissImpl?(true)
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.ChannelBoost_Replace, action: { }), TextAlertAction(type: .defaultAction, title: "Reassign", action: {
dismissImpl?(true) dismissImpl?(true)
commit() commit()
})] })]
contentNode = PhotoUpdateConfirmationAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, fromPeer: fromPeer, toPeer: toPeer, text: text, actions: actions) contentNode = ReplaceBoostConfirmationAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, fromPeers: fromPeers, toPeer: toPeer, text: text, actions: actions)
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!) let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!)
dismissImpl = { [weak controller] animated in dismissImpl = { [weak controller] animated in

View File

@ -5,7 +5,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "TelegramVoip", name: "TelegramVoip",
platforms: [.macOS(.v10_12)], platforms: [.macOS(.v10_13)],
products: [ products: [
// Products define the executables and libraries a package produces, and make them visible to other packages. // Products define the executables and libraries a package produces, and make them visible to other packages.
.library( .library(

View File

@ -100,6 +100,7 @@ public enum ParsedInternalUrl {
case startAttach(String, String?, String?) case startAttach(String, String?, String?)
case contactToken(String) case contactToken(String)
case chatFolder(slug: String) case chatFolder(slug: String)
case premiumGiftCode(slug: String)
} }
private enum ParsedUrl { private enum ParsedUrl {
@ -453,6 +454,8 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? {
return .chatFolder(slug: pathComponents[1]) return .chatFolder(slug: pathComponents[1])
} else if pathComponents[0] == "boost", pathComponents.count == 2 { } else if pathComponents[0] == "boost", pathComponents.count == 2 {
return .peer(.name(pathComponents[1]), .boost) return .peer(.name(pathComponents[1]), .boost)
} else if pathComponents[0] == "giftcode", pathComponents.count == 2 {
return .premiumGiftCode(slug: pathComponents[1])
} else if pathComponents.count == 3 && pathComponents[0] == "c" { } else if pathComponents.count == 3 && pathComponents[0] == "c" {
if let channelId = Int64(pathComponents[1]), let messageId = Int32(pathComponents[2]) { if let channelId = Int64(pathComponents[1]), let messageId = Int32(pathComponents[2]) {
var threadId: Int32? var threadId: Int32?
@ -899,6 +902,8 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl)
return .single(.inaccessiblePeer) return .single(.inaccessiblePeer)
} }
} }
case let .premiumGiftCode(slug):
return .single(.premiumGiftCode(slug: slug))
} }
} }

View File

@ -5,7 +5,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "DarwinDirStat", name: "DarwinDirStat",
platforms: [.macOS(.v10_12)], platforms: [.macOS(.v10_13)],
products: [ products: [
// Products define the executables and libraries a package produces, and make them visible to other packages. // Products define the executables and libraries a package produces, and make them visible to other packages.
.library( .library(

View File

@ -5,7 +5,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "RangeSet", name: "RangeSet",
platforms: [.macOS(.v10_12)], platforms: [.macOS(.v10_13)],
products: [ products: [
// Products define the executables and libraries a package produces, and make them visible to other packages. // Products define the executables and libraries a package produces, and make them visible to other packages.
.library( .library(

View File

@ -5,7 +5,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "YuvConversion", name: "YuvConversion",
platforms: [.macOS(.v10_12)], platforms: [.macOS(.v10_13)],
products: [ products: [
// Products define the executables and libraries a package produces, and make them visible to other packages. // Products define the executables and libraries a package produces, and make them visible to other packages.
.library( .library(

View File

@ -5,7 +5,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "libphonenumber", name: "libphonenumber",
platforms: [.macOS(.v10_12)], platforms: [.macOS(.v10_13)],
products: [ products: [
// Products define the executables and libraries a package produces, and make them visible to other packages. // Products define the executables and libraries a package produces, and make them visible to other packages.
.library( .library(

View File

@ -5,7 +5,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "sqlcipher", name: "sqlcipher",
platforms: [.macOS(.v10_12)], platforms: [.macOS(.v10_13)],
products: [ products: [
// Products define the executables and libraries a package produces, and make them visible to other packages. // Products define the executables and libraries a package produces, and make them visible to other packages.
.library( .library(

View File

@ -36,7 +36,7 @@ func replaceSymbols() -> [String] {
let package = Package( let package = Package(
name: "rnoise", name: "rnoise",
platforms: [.macOS(.v10_12)], platforms: [.macOS(.v10_13)],
products: [ products: [
// Products define the executables and libraries a package produces, and make them visible to other packages. // Products define the executables and libraries a package produces, and make them visible to other packages.
.library( .library(