mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Merge commit '943d22c54e0d4c97a0d842d5804a7cc2e58f952c'
This commit is contained in:
commit
c55bbe6be3
@ -10020,6 +10020,9 @@ Sorry for the inconvenience.";
|
||||
"Stats.Boosts.BoostersNone" = "BOOSTERS";
|
||||
"Stats.Boosts.Boosters_1" = "%@ BOOSTER";
|
||||
"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.BoostersInfo" = "Your channel is currently boosted by these users.";
|
||||
"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.ManyChats.One" = "Boost link forwarded to **%@** and %@ others";
|
||||
"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";
|
||||
|
@ -301,6 +301,7 @@ public enum ResolvedUrl {
|
||||
case chatFolder(slug: String)
|
||||
case story(peerId: PeerId, id: Int32)
|
||||
case boost(peerId: PeerId, status: ChannelBoostStatus?, canApplyStatus: CanApplyBoostStatus)
|
||||
case premiumGiftCode(slug: String)
|
||||
}
|
||||
|
||||
public enum NavigateToChatKeepStack {
|
||||
|
@ -59,48 +59,6 @@ public final class PeerSelectionControllerParams {
|
||||
public let selectForumThreads: 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(
|
||||
context: AccountContext,
|
||||
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
|
||||
|
@ -5,7 +5,7 @@ import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "Crc32",
|
||||
platforms: [.macOS(.v10_12)],
|
||||
platforms: [.macOS(.v10_13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
|
@ -5,7 +5,7 @@ import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "CryptoUtils",
|
||||
platforms: [.macOS(.v10_12)],
|
||||
platforms: [.macOS(.v10_13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
|
@ -17,8 +17,9 @@ public final class DatePickerTheme: Equatable {
|
||||
public let selectionTextColor: UIColor
|
||||
public let separatorColor: UIColor
|
||||
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.textColor = textColor
|
||||
self.secondaryTextColor = secondaryTextColor
|
||||
@ -28,6 +29,7 @@ public final class DatePickerTheme: Equatable {
|
||||
self.selectionTextColor = selectionTextColor
|
||||
self.separatorColor = separatorColor
|
||||
self.segmentedControlTheme = segmentedControlTheme
|
||||
self.overallDarkAppearance = overallDarkAppearance
|
||||
}
|
||||
|
||||
public static func ==(lhs: DatePickerTheme, rhs: DatePickerTheme) -> Bool {
|
||||
@ -52,13 +54,16 @@ public final class DatePickerTheme: Equatable {
|
||||
if lhs.separatorColor != rhs.separatorColor {
|
||||
return false
|
||||
}
|
||||
if lhs.overallDarkAppearance != rhs.overallDarkAppearance {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public extension DatePickerTheme {
|
||||
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 keyboardAppearance: UIKeyboardAppearance = .default
|
||||
|
||||
var text: String = ""
|
||||
var hasText: Bool {
|
||||
@ -1284,7 +1290,7 @@ private final class TimePickerNode: ASDisplayNode {
|
||||
|
||||
self.update()
|
||||
}
|
||||
|
||||
|
||||
private func updateTime() {
|
||||
switch self.dateTimeFormat.timeFormat {
|
||||
case .military:
|
||||
@ -1338,6 +1344,8 @@ private final class TimePickerNode: ASDisplayNode {
|
||||
self.view.disablesInteractiveModalDismiss = true
|
||||
|
||||
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) {
|
||||
|
@ -5,7 +5,7 @@ import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "Emoji",
|
||||
platforms: [.macOS(.v10_12)],
|
||||
platforms: [.macOS(.v10_13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
|
@ -5,7 +5,7 @@ import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "EncryptionProvider",
|
||||
platforms: [.macOS(.v10_12)],
|
||||
platforms: [.macOS(.v10_13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
|
@ -5,7 +5,7 @@ import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "FFMpegBinding",
|
||||
platforms: [.macOS(.v10_12)],
|
||||
platforms: [.macOS(.v10_13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
|
@ -1055,9 +1055,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
||||
self.fullscreenButton.isHidden = true
|
||||
}
|
||||
|
||||
var textFrame = CGRect()
|
||||
var visibleTextHeight: CGFloat = 0.0
|
||||
if !self.textNode.isHidden {
|
||||
var textFrame = CGRect()
|
||||
var visibleTextHeight: CGFloat = 0.0
|
||||
|
||||
let sideInset: CGFloat = 8.0 + leftInset
|
||||
let topInset: CGFloat = 8.0
|
||||
let textBottomInset: CGFloat = 8.0
|
||||
@ -1085,7 +1086,11 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
||||
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)
|
||||
|
||||
if self.scrollNode.view.isScrollEnabled {
|
||||
|
@ -5,7 +5,7 @@ import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "GraphCore",
|
||||
platforms: [.macOS(.v10_12)],
|
||||
platforms: [.macOS(.v10_13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
|
@ -3,6 +3,7 @@ import CoreLocation
|
||||
import SwiftSignalKit
|
||||
import StoreKit
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import TelegramStringFormatting
|
||||
import TelegramUIPreferences
|
||||
import PersistentStringHash
|
||||
@ -13,12 +14,12 @@ private let productIdentifiers = [
|
||||
"org.telegram.telegramPremium.monthly",
|
||||
"org.telegram.telegramPremium.twelveMonths",
|
||||
"org.telegram.telegramPremium.sixMonths",
|
||||
"org.telegram.telegramPremium.threeMonths"
|
||||
]
|
||||
"org.telegram.telegramPremium.threeMonths",
|
||||
|
||||
private func isSubscriptionProductId(_ id: String) -> Bool {
|
||||
return id.hasSuffix(".monthly") || id.hasSuffix(".annual") || id.hasSuffix(".semiannual")
|
||||
}
|
||||
"org.telegram.telegramPremium.threeMonths.code_x1",
|
||||
"org.telegram.telegramPremium.sixMonths.code_x1",
|
||||
"org.telegram.telegramPremium.twelveMonths.code_x1"
|
||||
]
|
||||
|
||||
private extension NSDecimalNumber {
|
||||
func round(_ decimals: Int) -> NSDecimalNumber {
|
||||
@ -103,6 +104,25 @@ public final class InAppPurchaseManager: NSObject {
|
||||
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 {
|
||||
return self.skProduct.price
|
||||
}
|
||||
@ -151,13 +171,11 @@ public final class InAppPurchaseManager: NSObject {
|
||||
|
||||
private final class PaymentTransactionContext {
|
||||
var state: SKPaymentTransactionState?
|
||||
var isUpgrade: Bool
|
||||
var targetPeerId: EnginePeer.Id?
|
||||
let purpose: PendingInAppPurchaseState.Purpose
|
||||
let subscriber: (TransactionState) -> Void
|
||||
|
||||
init(isUpgrade: Bool, targetPeerId: EnginePeer.Id?, subscriber: @escaping (TransactionState) -> Void) {
|
||||
self.isUpgrade = isUpgrade
|
||||
self.targetPeerId = targetPeerId
|
||||
init(purpose: PendingInAppPurchaseState.Purpose, subscriber: @escaping (TransactionState) -> Void) {
|
||||
self.purpose = purpose
|
||||
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 {
|
||||
return .fail(.cantMakePayments)
|
||||
}
|
||||
|
||||
if !product.isSubscription && targetPeerId == nil {
|
||||
return .fail(.cantMakePayments)
|
||||
}
|
||||
|
||||
|
||||
let accountPeerId = "\(self.engine.account.peerId.toInt64())"
|
||||
|
||||
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)
|
||||
payment.applicationUsername = accountPeerId
|
||||
payment.quantity = purpose.quantity
|
||||
SKPaymentQueue.default().add(payment)
|
||||
|
||||
let productIdentifier = payment.productIdentifier
|
||||
@ -257,7 +274,7 @@ public final class InAppPurchaseManager: NSObject {
|
||||
let disposable = MetaDisposable()
|
||||
|
||||
self.stateQueue.async {
|
||||
let paymentContext = PaymentTransactionContext(isUpgrade: isUpgrade, targetPeerId: targetPeerId, subscriber: { state in
|
||||
let paymentContext = PaymentTransactionContext(purpose: purpose, subscriber: { state in
|
||||
switch state {
|
||||
case let .purchased(transactionId), let .restored(transactionId):
|
||||
if let transactionId = transactionId {
|
||||
@ -381,8 +398,7 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver {
|
||||
productId: transaction.payment.productIdentifier,
|
||||
content: PendingInAppPurchaseState(
|
||||
productId: transaction.payment.productIdentifier,
|
||||
isUpgrade: paymentContext.isUpgrade,
|
||||
targetPeerId: paymentContext.targetPeerId
|
||||
purpose: paymentContext.purpose
|
||||
)
|
||||
).start()
|
||||
}
|
||||
@ -410,64 +426,61 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver {
|
||||
|
||||
var completion: Signal<Never, NoError> = .never()
|
||||
|
||||
let purpose: Signal<AppStoreTransactionPurpose, NoError>
|
||||
if !isSubscriptionProductId(productIdentifier) {
|
||||
let peerId: Signal<EnginePeer.Id, NoError>
|
||||
if let targetPeerId = paymentContexts[productIdentifier]?.targetPeerId {
|
||||
peerId = .single(targetPeerId)
|
||||
let products = self.availableProducts
|
||||
|> filter { products in
|
||||
return !products.isEmpty
|
||||
}
|
||||
|> take(1)
|
||||
|
||||
|
||||
let product: Signal<InAppPurchaseManager.Product?, NoError> = products
|
||||
|> map { products in
|
||||
if let product = products.first(where: { $0.id == productIdentifier }) {
|
||||
return product
|
||||
} else {
|
||||
peerId = pendingInAppPurchaseState(engine: self.engine, productId: productIdentifier)
|
||||
|> 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
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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()
|
||||
|
||||
#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(
|
||||
(purpose
|
||||
|> castError(AssignAppStoreTransactionError.self)
|
||||
|> 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
|
||||
Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transactions [\(transactionIds)] failed to assign")
|
||||
for transaction in transactions {
|
||||
@ -535,32 +548,165 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver {
|
||||
}
|
||||
|
||||
private final class PendingInAppPurchaseState: Codable {
|
||||
public let productId: String
|
||||
public let isUpgrade: Bool
|
||||
public let targetPeerId: EnginePeer.Id?
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case productId
|
||||
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.isUpgrade = isUpgrade
|
||||
self.targetPeerId = targetPeerId
|
||||
self.purpose = purpose
|
||||
}
|
||||
|
||||
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.isUpgrade = try container.decodeIfPresent(Bool.self, forKey: "isUpgrade") ?? false
|
||||
self.targetPeerId = (try container.decodeIfPresent(Int64.self, forKey: "targetPeerId")).flatMap { EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value($0)) }
|
||||
self.productId = try container.decode(String.self, forKey: .productId)
|
||||
self.purpose = try container.decode(Purpose.self, forKey: .purpose)
|
||||
}
|
||||
|
||||
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.isUpgrade, forKey: "isUpgrade")
|
||||
if let targetPeerId = self.targetPeerId {
|
||||
try container.encode(targetPeerId.id._internalGetInt64Value(), forKey: "targetPeerId")
|
||||
}
|
||||
try container.encode(self.productId, forKey: .productId)
|
||||
try container.encode(self.purpose, forKey: .purpose)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -58,6 +58,7 @@ swift_library(
|
||||
"//submodules/PeerInfoAvatarListNode:PeerInfoAvatarListNode",
|
||||
"//submodules/QrCodeUI:QrCodeUI",
|
||||
"//submodules/PromptUI",
|
||||
"//submodules/TelegramUI/Components/ItemListDatePickerItem:ItemListDatePickerItem",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -16,6 +16,7 @@ import AppBundle
|
||||
import ContextUI
|
||||
import TelegramStringFormatting
|
||||
import UndoUI
|
||||
import ItemListDatePickerItem
|
||||
|
||||
private final class InviteLinkEditControllerArguments {
|
||||
let context: AccountContext
|
||||
|
@ -5,7 +5,7 @@ import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "ManagedFile",
|
||||
platforms: [.macOS(.v10_12)],
|
||||
platforms: [.macOS(.v10_13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
|
@ -5,7 +5,7 @@ import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "MtProtoKit",
|
||||
platforms: [.macOS(.v10_12)],
|
||||
platforms: [.macOS(.v10_13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
|
@ -5,7 +5,7 @@ import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "MurMurHash32",
|
||||
platforms: [.macOS(.v10_12)],
|
||||
platforms: [.macOS(.v10_13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
|
@ -5,7 +5,7 @@ import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "NetworkLogging",
|
||||
platforms: [.macOS(.v10_12)],
|
||||
platforms: [.macOS(.v10_13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
|
@ -7,7 +7,7 @@ import PackageDescription
|
||||
let package = Package(
|
||||
name: "OpenSSLEncryption",
|
||||
platforms: [
|
||||
.macOS(.v10_12)
|
||||
.macOS(.v10_13)
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
|
2
submodules/OpusBinding/Package.swift
vendored
2
submodules/OpusBinding/Package.swift
vendored
@ -5,7 +5,7 @@ import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "OpusBinding",
|
||||
platforms: [.macOS(.v10_12)],
|
||||
platforms: [.macOS(.v10_13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
|
@ -5,7 +5,7 @@ import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "Postbox",
|
||||
platforms: [.macOS(.v10_12)],
|
||||
platforms: [.macOS(.v10_13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
|
@ -100,7 +100,11 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
||||
"//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent",
|
||||
"//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:public",
|
||||
|
784
submodules/PremiumUI/Sources/CreateGiveawayController.swift
Normal file
784
submodules/PremiumUI/Sources/CreateGiveawayController.swift
Normal 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
|
||||
}
|
125
submodules/PremiumUI/Sources/CreateGiveawayFooterItem.swift
Normal file
125
submodules/PremiumUI/Sources/CreateGiveawayFooterItem.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
169
submodules/PremiumUI/Sources/CreateGiveawayHeaderItem.swift
Normal file
169
submodules/PremiumUI/Sources/CreateGiveawayHeaderItem.swift
Normal 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)
|
||||
}
|
||||
}
|
410
submodules/PremiumUI/Sources/GiftModeItem.swift
Normal file
410
submodules/PremiumUI/Sources/GiftModeItem.swift
Normal 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)
|
||||
}
|
||||
}
|
879
submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift
Normal file
879
submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift
Normal 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)
|
||||
}
|
||||
}
|
@ -670,11 +670,12 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
|
||||
self.updateInProgress(true)
|
||||
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
|
||||
if let strongSelf = self {
|
||||
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
|
||||
if let strongSelf = self, case .purchased = status {
|
||||
Queue.mainQueue().after(2.0) {
|
||||
|
@ -2293,11 +2293,12 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
self.updateInProgress(true)
|
||||
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
|
||||
if let strongSelf = self {
|
||||
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
|
||||
if let strongSelf = self, case .purchased = status {
|
||||
strongSelf.activationDisposable.set((strongSelf.context.account.postbox.peerView(id: strongSelf.context.account.peerId)
|
||||
|
@ -17,6 +17,7 @@ import Markdown
|
||||
import BalancedTextComponent
|
||||
import ConfettiEffect
|
||||
import AvatarNode
|
||||
import TextFormat
|
||||
|
||||
func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? {
|
||||
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 openPeer: (EnginePeer) -> 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.subject = subject
|
||||
self.count = count
|
||||
@ -820,6 +822,7 @@ private final class LimitSheetContent: CombinedComponent {
|
||||
self.dismiss = dismiss
|
||||
self.openPeer = openPeer
|
||||
self.openStats = openStats
|
||||
self.openGift = openGift
|
||||
}
|
||||
|
||||
static func ==(lhs: LimitSheetContent, rhs: LimitSheetContent) -> Bool {
|
||||
@ -847,6 +850,7 @@ private final class LimitSheetContent: CombinedComponent {
|
||||
var boosted = false
|
||||
|
||||
var cachedCloseImage: (UIImage, PresentationTheme)?
|
||||
var cachedChevronImage: (UIImage, PresentationTheme)?
|
||||
|
||||
init(context: AccountContext, subject: PremiumLimitScreen.Subject) {
|
||||
self.context = context
|
||||
@ -891,6 +895,11 @@ private final class LimitSheetContent: CombinedComponent {
|
||||
let peerShortcut = 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
|
||||
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
|
||||
let component = context.component
|
||||
@ -942,6 +951,7 @@ private final class LimitSheetContent: CombinedComponent {
|
||||
var titleText = strings.Premium_LimitReached
|
||||
var actionButtonText: String?
|
||||
var actionButtonHasGloss = true
|
||||
var gradientedActionButton = true
|
||||
var buttonAnimationName: String? = "premium_x2"
|
||||
var buttonIconName: String?
|
||||
let iconName: String
|
||||
@ -1167,7 +1177,8 @@ private final class LimitSheetContent: CombinedComponent {
|
||||
PeerShortcutComponent(
|
||||
context: component.context,
|
||||
theme: environment.theme,
|
||||
peer: peer
|
||||
peer: peer,
|
||||
badge: "X2"
|
||||
)
|
||||
),
|
||||
action: {
|
||||
@ -1182,6 +1193,10 @@ private final class LimitSheetContent: CombinedComponent {
|
||||
)
|
||||
}
|
||||
|
||||
if link != nil {
|
||||
gradientedActionButton = false
|
||||
}
|
||||
|
||||
if let _ = link, let openStats = component.openStats {
|
||||
let _ = openStats
|
||||
let statsButton = statsButton.update(
|
||||
@ -1267,11 +1282,15 @@ private final class LimitSheetContent: CombinedComponent {
|
||||
premiumTitle = ""
|
||||
|
||||
if boosted {
|
||||
let prefixString = isCurrent ? strings.ChannelBoost_YouBoostedChannelText(peer.compactDisplayTitle).string : strings.ChannelBoost_YouBoostedOtherChannelText
|
||||
|
||||
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 {
|
||||
titleText = isCurrent ? strings.ChannelBoost_YouBoostedChannel(peer.compactDisplayTitle).string : strings.ChannelBoost_YouBoostedOtherChannel
|
||||
let boostsString = strings.ChannelBoost_MoreBoosts(remaining)
|
||||
if level == 0 {
|
||||
if remaining == 0 {
|
||||
@ -1288,9 +1307,10 @@ private final class LimitSheetContent: CombinedComponent {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
titleText = strings.ChannelBoost_MaxLevelReached
|
||||
string = strings.ChannelBoost_BoostedChannelReachedLevel("\(level + 1)", storiesString).string
|
||||
}
|
||||
|
||||
string = "**\(prefixString)**\n\(string)"
|
||||
}
|
||||
|
||||
let progress: CGFloat
|
||||
@ -1323,18 +1343,18 @@ private final class LimitSheetContent: CombinedComponent {
|
||||
horizontalAlignment: .center,
|
||||
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
|
||||
)
|
||||
|
||||
let textFont = Font.regular(15.0)
|
||||
let boldTextFont = Font.semibold(15.0)
|
||||
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
|
||||
return nil
|
||||
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)
|
||||
})
|
||||
|
||||
|
||||
var textChild: _UpdatedChildComponent?
|
||||
var alternateTextChild: _UpdatedChildComponent?
|
||||
if useAlternateText {
|
||||
@ -1370,6 +1390,7 @@ private final class LimitSheetContent: CombinedComponent {
|
||||
}
|
||||
|
||||
let gradientColors: [UIColor]
|
||||
var buttonGradientColors: [UIColor]
|
||||
if isPremiumDisabled {
|
||||
gradientColors = [
|
||||
UIColor(rgb: 0x007afe),
|
||||
@ -1383,6 +1404,14 @@ private final class LimitSheetContent: CombinedComponent {
|
||||
UIColor(rgb: 0xe46ace)
|
||||
]
|
||||
}
|
||||
if gradientedActionButton {
|
||||
buttonGradientColors = gradientColors
|
||||
} else {
|
||||
buttonGradientColors = [
|
||||
UIColor(rgb: 0x007afe),
|
||||
UIColor(rgb: 0x5494ff)
|
||||
]
|
||||
}
|
||||
|
||||
var limitTransition: Transition = .immediate
|
||||
if boostUpdated {
|
||||
@ -1395,7 +1424,7 @@ private final class LimitSheetContent: CombinedComponent {
|
||||
title: actionButtonText ?? (isIncreaseButton ? strings.Premium_IncreaseLimit : strings.Common_OK),
|
||||
theme: SolidRoundedButtonComponent.Theme(
|
||||
backgroundColor: .black,
|
||||
backgroundColors: gradientColors,
|
||||
backgroundColors: buttonGradientColors,
|
||||
foregroundColor: .white
|
||||
),
|
||||
font: .bold,
|
||||
@ -1528,8 +1557,76 @@ private final class LimitSheetContent: CombinedComponent {
|
||||
context.add(button
|
||||
.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 {
|
||||
var height: CGFloat = 351.0
|
||||
if isPremiumDisabled {
|
||||
@ -1539,6 +1636,8 @@ private final class LimitSheetContent: CombinedComponent {
|
||||
if case let .storiesChannelBoost(_, isCurrent, _, _, _, link, _) = component.subject {
|
||||
if link != nil {
|
||||
height += 66.0
|
||||
|
||||
height += 100.0
|
||||
} else {
|
||||
if isCurrent {
|
||||
height -= 53.0
|
||||
@ -1566,8 +1665,9 @@ private final class LimitSheetComponent: CombinedComponent {
|
||||
let action: () -> Bool
|
||||
let openPeer: (EnginePeer) -> 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.subject = subject
|
||||
self.count = count
|
||||
@ -1575,6 +1675,7 @@ private final class LimitSheetComponent: CombinedComponent {
|
||||
self.action = action
|
||||
self.openPeer = openPeer
|
||||
self.openStats = openStats
|
||||
self.openGift = openGift
|
||||
}
|
||||
|
||||
static func ==(lhs: LimitSheetComponent, rhs: LimitSheetComponent) -> Bool {
|
||||
@ -1616,7 +1717,8 @@ private final class LimitSheetComponent: CombinedComponent {
|
||||
})
|
||||
},
|
||||
openPeer: context.component.openPeer,
|
||||
openStats: context.component.openStats
|
||||
openStats: context.component.openStats,
|
||||
openGift: context.component.openGift
|
||||
)),
|
||||
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
|
||||
animateOut: animateOut
|
||||
@ -1680,14 +1782,14 @@ public class PremiumLimitScreen: ViewControllerComponentContainer {
|
||||
|
||||
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.openPeer = openPeer
|
||||
|
||||
var actionImpl: (() -> Bool)?
|
||||
super.init(context: context, component: LimitSheetComponent(context: context, subject: subject, count: count, cancel: {}, action: {
|
||||
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
|
||||
|
||||
@ -1721,7 +1823,7 @@ public class PremiumLimitScreen: ViewControllerComponentContainer {
|
||||
public func updateSubject(_ subject: Subject, count: Int32) {
|
||||
let component = LimitSheetComponent(context: self.context, subject: subject, count: count, cancel: {}, action: {
|
||||
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.hapticFeedback.impact()
|
||||
@ -1734,11 +1836,13 @@ private final class PeerShortcutComponent: Component {
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
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.theme = theme
|
||||
self.peer = peer
|
||||
self.badge = badge
|
||||
}
|
||||
|
||||
static func ==(lhs: PeerShortcutComponent, rhs: PeerShortcutComponent) -> Bool {
|
||||
@ -1751,13 +1855,20 @@ private final class PeerShortcutComponent: Component {
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
if lhs.badge != rhs.badge {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let backgroundView = UIView()
|
||||
private let avatarNode: AvatarNode
|
||||
private let text = ComponentView<Empty>()
|
||||
|
||||
private let badgeBackground = UIView()
|
||||
private let badgeText = ComponentView<Empty>()
|
||||
|
||||
private var component: PeerShortcutComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
@ -1766,10 +1877,15 @@ private final class PeerShortcutComponent: Component {
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.clipsToBounds = true
|
||||
self.layer.cornerRadius = 16.0
|
||||
self.backgroundView.clipsToBounds = true
|
||||
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.addSubview(self.badgeBackground)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -1780,7 +1896,7 @@ private final class PeerShortcutComponent: Component {
|
||||
self.component = component
|
||||
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.setPeer(
|
||||
@ -1810,6 +1926,40 @@ private final class PeerShortcutComponent: Component {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
260
submodules/PremiumUI/Sources/SubscriptionsCountItem.swift
Normal file
260
submodules/PremiumUI/Sources/SubscriptionsCountItem.swift
Normal 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))
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@ import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "LegacyReachability",
|
||||
platforms: [.macOS(.v10_12)],
|
||||
platforms: [.macOS(.v10_13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
|
@ -5,7 +5,7 @@ import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "Reachability",
|
||||
platforms: [.macOS(.v10_12)],
|
||||
platforms: [.macOS(.v10_13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
|
@ -5,7 +5,7 @@ import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "SSignalKit",
|
||||
platforms: [.macOS(.v10_12)],
|
||||
platforms: [.macOS(.v10_13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
|
@ -20,6 +20,7 @@ import InviteLinksUI
|
||||
import UndoUI
|
||||
import ShareController
|
||||
import ItemListPeerActionItem
|
||||
import PremiumUI
|
||||
|
||||
private let maxUsersDisplayedLimit: Int32 = 50
|
||||
|
||||
@ -32,8 +33,9 @@ private final class ChannelStatsControllerArguments {
|
||||
let shareBoostLink: (String) -> Void
|
||||
let openPeer: (EnginePeer) -> 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.loadDetailedGraph = loadDetailedGraph
|
||||
self.openMessageStats = openMessage
|
||||
@ -42,6 +44,7 @@ private final class ChannelStatsControllerArguments {
|
||||
self.shareBoostLink = shareBoostLink
|
||||
self.openPeer = openPeer
|
||||
self.expandBoosters = expandBoosters
|
||||
self.openGifts = openGifts
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,6 +64,7 @@ private enum StatsSection: Int32 {
|
||||
case boostOverview
|
||||
case boosters
|
||||
case boostLink
|
||||
case gifts
|
||||
}
|
||||
|
||||
private enum StatsEntry: ItemListNodeEntry {
|
||||
@ -112,6 +116,9 @@ private enum StatsEntry: ItemListNodeEntry {
|
||||
case boostLink(PresentationTheme, String)
|
||||
case boostLinkInfo(PresentationTheme, String)
|
||||
|
||||
case gifts(PresentationTheme, String)
|
||||
case giftsInfo(PresentationTheme, String)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
case .overviewTitle, .overview:
|
||||
@ -140,10 +147,12 @@ private enum StatsEntry: ItemListNodeEntry {
|
||||
return StatsSection.boostLevel.rawValue
|
||||
case .boostOverviewTitle, .boostOverview:
|
||||
return StatsSection.boostOverview.rawValue
|
||||
case .boostersTitle, .boostersPlaceholder, .booster, .boostersExpand, .boostersInfo:
|
||||
case .boostersTitle, .boostersPlaceholder, .booster, .boostersExpand, .boostersInfo:
|
||||
return StatsSection.boosters.rawValue
|
||||
case .boostLinkTitle, .boostLink, .boostLinkInfo:
|
||||
return StatsSection.boostLink.rawValue
|
||||
case .gifts, .giftsInfo:
|
||||
return StatsSection.gifts.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
@ -215,6 +224,10 @@ private enum StatsEntry: ItemListNodeEntry {
|
||||
return 10003
|
||||
case .boostLinkInfo:
|
||||
return 10004
|
||||
case .gifts:
|
||||
return 10005
|
||||
case .giftsInfo:
|
||||
return 10006
|
||||
}
|
||||
}
|
||||
|
||||
@ -418,6 +431,18 @@ private enum StatsEntry: ItemListNodeEntry {
|
||||
} else {
|
||||
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):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .boostersInfo(_, text),
|
||||
let .boostLinkInfo(_, text):
|
||||
let .boostLinkInfo(_, text),
|
||||
let .giftsInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
|
||||
case let .overview(_, stats):
|
||||
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)
|
||||
case let .boostersPlaceholder(_, text):
|
||||
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 boostersFooter: String?
|
||||
if let boostersState, boostersState.count > 0 {
|
||||
boostersTitle = presentationData.strings.Stats_Boosts_Boosters(boostersState.count)
|
||||
boostersTitle = presentationData.strings.Stats_Boosts_Boosts(boostersState.count)
|
||||
boostersPlaceholder = nil
|
||||
boostersFooter = presentationData.strings.Stats_Boosts_BoostersInfo
|
||||
} else {
|
||||
boostersTitle = presentationData.strings.Stats_Boosts_BoostersNone
|
||||
boostersTitle = presentationData.strings.Stats_Boosts_BoostsNone
|
||||
boostersPlaceholder = presentationData.strings.Stats_Boosts_NoBoostersYet
|
||||
boostersFooter = nil
|
||||
}
|
||||
@ -664,10 +694,11 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p
|
||||
}
|
||||
|
||||
entries.append(.boostLinkTitle(presentationData.theme, presentationData.strings.Stats_Boosts_LinkHeader))
|
||||
|
||||
entries.append(.boostLink(presentationData.theme, boostData.url))
|
||||
|
||||
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)
|
||||
|
||||
var presentImpl: ((ViewController) -> Void)?
|
||||
var pushImpl: ((ViewController) -> Void)?
|
||||
var navigateToProfileImpl: ((EnginePeer) -> Void)?
|
||||
|
||||
let arguments = ChannelStatsControllerArguments(context: context, loadDetailedGraph: { graph, x -> Signal<StatsGraph?, NoError> in
|
||||
@ -778,6 +810,10 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
|
||||
},
|
||||
expandBoosters: {
|
||||
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)
|
||||
@ -893,6 +929,9 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
|
||||
presentImpl = { [weak controller] c in
|
||||
controller?.present(c, in: .window(.root))
|
||||
}
|
||||
pushImpl = { [weak controller] c in
|
||||
controller?.push(c)
|
||||
}
|
||||
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) {
|
||||
navigationController.pushViewController(controller)
|
||||
|
@ -5,7 +5,7 @@ import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "StringTransliteration",
|
||||
platforms: [.macOS(.v10_12)],
|
||||
platforms: [.macOS(.v10_13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
|
@ -5,7 +5,7 @@ import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "TelegramApi",
|
||||
platforms: [.macOS(.v10_12)],
|
||||
platforms: [.macOS(.v10_13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
|
@ -405,6 +405,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[70813275] = { return Api.InputStickeredMedia.parse_inputStickeredMediaDocument($0) }
|
||||
dict[1251549527] = { return Api.InputStickeredMedia.parse_inputStickeredMediaPhoto($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[1012306921] = { return Api.InputTheme.parse_inputTheme($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[-1834538890] = { return Api.MessageAction.parse_messageActionGameScore($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[2047704898] = { return Api.MessageAction.parse_messageActionGroupCall($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[1457575028] = { return Api.MessageMedia.parse_messageMediaGeo($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[1766936791] = { return Api.MessageMedia.parse_messageMediaPhoto($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[1558266229] = { return Api.PopularContact.parse_popularContact($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[1596792306] = { return Api.PremiumSubscriptionOption.parse_premiumSubscriptionOption($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[-44166467] = { return Api.messages.WebPage.parse_webPage($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[-1610250415] = { return Api.payments.PaymentForm.parse_paymentForm($0) }
|
||||
dict[1891958275] = { return Api.payments.PaymentReceipt.parse_paymentReceipt($0) }
|
||||
@ -1658,6 +1664,8 @@ public extension Api {
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.PostAddress:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.PremiumGiftCodeOption:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.PremiumGiftOption:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.PremiumSubscriptionOption:
|
||||
@ -2022,6 +2030,8 @@ public extension Api {
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.payments.BankCardData:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.payments.CheckedGiftCode:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.payments.ExportedInvoice:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.payments.PaymentForm:
|
||||
|
@ -579,6 +579,8 @@ public extension Api {
|
||||
public extension Api {
|
||||
indirect enum InputStorePaymentPurpose: TypeConstructorDescription {
|
||||
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)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
@ -591,6 +593,31 @@ public extension Api {
|
||||
serializeString(currency, buffer: buffer, boxed: false)
|
||||
serializeInt64(amount, buffer: buffer, boxed: false)
|
||||
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):
|
||||
if boxed {
|
||||
buffer.appendInt32(-1502273946)
|
||||
@ -604,6 +631,10 @@ public extension Api {
|
||||
switch self {
|
||||
case .inputStorePaymentGiftPremium(let userId, let currency, let amount):
|
||||
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):
|
||||
return ("inputStorePaymentPremiumSubscription", [("flags", flags as Any)])
|
||||
}
|
||||
@ -628,6 +659,61 @@ public extension Api {
|
||||
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? {
|
||||
var _1: Int32?
|
||||
_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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
enum InputWebFileLocation: TypeConstructorDescription {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
enum MaskCoords: TypeConstructorDescription {
|
||||
case maskCoords(n: Int32, x: Double, y: Double, zoom: Double)
|
||||
@ -501,6 +609,7 @@ public extension Api {
|
||||
case messageActionEmpty
|
||||
case messageActionGameScore(gameId: Int64, score: 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 messageActionGroupCall(flags: Int32, call: Api.InputGroupCall, duration: Int32?)
|
||||
case messageActionGroupCallScheduled(call: Api.InputGroupCall, scheduleDate: Int32)
|
||||
@ -643,6 +752,15 @@ public extension Api {
|
||||
toId.serialize(buffer, true)
|
||||
serializeInt32(distance, buffer: buffer, boxed: false)
|
||||
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):
|
||||
if boxed {
|
||||
buffer.appendInt32(-935499028)
|
||||
@ -859,6 +977,8 @@ public extension Api {
|
||||
return ("messageActionGameScore", [("gameId", gameId as Any), ("score", score as Any)])
|
||||
case .messageActionGeoProximityReached(let fromId, let toId, let distance):
|
||||
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):
|
||||
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):
|
||||
@ -1094,6 +1214,28 @@ public extension Api {
|
||||
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? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
|
@ -741,6 +741,7 @@ public extension Api {
|
||||
case messageMediaGame(game: Api.Game)
|
||||
case messageMediaGeo(geo: Api.GeoPoint)
|
||||
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 messageMediaPhoto(flags: Int32, photo: Api.Photo?, ttlSeconds: Int32?)
|
||||
case messageMediaPoll(poll: Api.Poll, results: Api.PollResults)
|
||||
@ -805,6 +806,19 @@ public extension Api {
|
||||
serializeInt32(period, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 1) != 0 {serializeInt32(proximityNotificationRadius!, buffer: buffer, boxed: false)}
|
||||
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):
|
||||
if boxed {
|
||||
buffer.appendInt32(-156940077)
|
||||
@ -885,6 +899,8 @@ public extension Api {
|
||||
return ("messageMediaGeo", [("geo", geo as Any)])
|
||||
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)])
|
||||
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):
|
||||
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):
|
||||
@ -1017,6 +1033,28 @@ public extension Api {
|
||||
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? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
|
@ -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 {
|
||||
enum PremiumGiftOption: TypeConstructorDescription {
|
||||
case premiumGiftOption(flags: Int32, months: Int32, currency: String, amount: Int64, botUrl: String, storeProduct: String?)
|
||||
|
@ -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 {
|
||||
enum ExportedInvoice: TypeConstructorDescription {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
enum MessageStats: TypeConstructorDescription {
|
||||
case messageStats(viewsGraph: Api.StatsGraph)
|
||||
|
@ -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 {
|
||||
static func assignAppStoreTransaction(receipt: Buffer, purpose: Api.InputStorePaymentPurpose) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
|
||||
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 {
|
||||
static func clearSavedInfo(flags: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||
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 {
|
||||
static func getSavedInfo() -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.payments.SavedInfo>) {
|
||||
let buffer = Buffer()
|
||||
|
@ -5,7 +5,7 @@ import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "TelegramCore",
|
||||
platforms: [.macOS(.v10_12)],
|
||||
platforms: [.macOS(.v10_13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
|
@ -200,6 +200,7 @@ private var declaredEncodables: Void = {
|
||||
declareEncodable(SynchronizeViewStoriesOperation.self, f: { SynchronizeViewStoriesOperation(decoder: $0) })
|
||||
declareEncodable(SynchronizePeerStoriesOperation.self, f: { SynchronizePeerStoriesOperation(decoder: $0) })
|
||||
declareEncodable(MapVenue.self, f: { MapVenue(decoder: $0) })
|
||||
declareEncodable(TelegramMediaGiveaway.self, f: { TelegramMediaGiveaway(decoder: $0) })
|
||||
return
|
||||
}()
|
||||
|
||||
|
@ -492,7 +492,7 @@ extension ChatContextResultMessage {
|
||||
if let replyMarkup = 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -244,6 +244,10 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] {
|
||||
}
|
||||
case let .messageActionRequestedPeer(_, peer):
|
||||
result.append(peer.peerId)
|
||||
case let .messageActionGiftCode(_, boostPeer, _, _):
|
||||
if let boostPeer = boostPeer {
|
||||
result.append(boostPeer.peerId)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
@ -383,6 +387,8 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI
|
||||
case let .messageMediaStory(flags, peerId, id, _):
|
||||
let isMention = (flags & (1 << 1)) != 0
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,6 +127,8 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
|
||||
return TelegramMediaAction(action: .setChatWallpaper(wallpaper: TelegramWallpaper(apiWallpaper: wallpaper)))
|
||||
case let .messageActionSetSameChatWallPaper(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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -210,7 +210,7 @@ public class BoxedMessage: NSObject {
|
||||
|
||||
public class Serialization: NSObject, MTSerialization {
|
||||
public func currentLayer() -> UInt {
|
||||
return 165
|
||||
return 166
|
||||
}
|
||||
|
||||
public func parseMessage(_ data: Data!) -> Any! {
|
||||
|
@ -109,6 +109,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
|
||||
case requestedPeer(buttonId: Int32, peerId: PeerId)
|
||||
case setChatWallpaper(wallpaper: TelegramWallpaper)
|
||||
case setSameChatWallpaper(wallpaper: TelegramWallpaper)
|
||||
case giftCode(slug: String, fromGiveaway: Bool, boostPeerId: PeerId?, months: Int32)
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
let rawValue: Int32 = decoder.decodeInt32ForKey("_rawValue", orElse: 0)
|
||||
@ -203,6 +204,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
|
||||
}
|
||||
case 35:
|
||||
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:
|
||||
self = .unknown
|
||||
}
|
||||
@ -382,6 +385,16 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
|
||||
} else {
|
||||
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
|
||||
case let .requestedPeer(_, peerId):
|
||||
return [peerId]
|
||||
case let .giftCode(_, _, boostPeerId, _):
|
||||
return boostPeerId.flatMap { [$0] } ?? []
|
||||
default:
|
||||
return []
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -13,12 +13,13 @@ public enum AssignAppStoreTransactionError {
|
||||
public enum AppStoreTransactionPurpose {
|
||||
case subscription
|
||||
case upgrade
|
||||
case gift(peerId: EnginePeer.Id, currency: String, amount: Int64)
|
||||
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> {
|
||||
var purposeSignal: Signal<Api.InputStorePaymentPurpose, NoError>
|
||||
private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTransactionPurpose) -> Signal<Api.InputStorePaymentPurpose, NoError> {
|
||||
switch purpose {
|
||||
case .subscription, .upgrade, .restore:
|
||||
var flags: Int32 = 0
|
||||
@ -30,19 +31,47 @@ func _internal_sendAppStoreReceipt(account: Account, receipt: Data, purpose: App
|
||||
default:
|
||||
break
|
||||
}
|
||||
purposeSignal = .single(.inputStorePaymentPremiumSubscription(flags: flags))
|
||||
return .single(.inputStorePaymentPremiumSubscription(flags: flags))
|
||||
case let .gift(peerId, currency, amount):
|
||||
purposeSignal = account.postbox.loadedPeerWithId(peerId)
|
||||
return 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 {
|
||||
guard let inputUser = apiInputUser(peer) else {
|
||||
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)
|
||||
|> mapToSignal { purpose -> Signal<Never, AssignAppStoreTransactionError> in
|
||||
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> {
|
||||
var purposeSignal: Signal<Api.InputStorePaymentPurpose, NoError>
|
||||
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
|
||||
return apiInputStorePaymentPurpose(account: account, purpose: purpose)
|
||||
|> mapToSignal { purpose -> Signal<Bool, NoError> in
|
||||
return account.network.request(Api.functions.payments.canPurchasePremium(purpose: purpose))
|
||||
|> map { result -> Bool in
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -45,5 +45,17 @@ public extension TelegramEngine {
|
||||
public func canPurchasePremium(purpose: AppStoreTransactionPurpose) -> Signal<Bool, NoError> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -444,7 +444,7 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati
|
||||
),
|
||||
itemCheckColors: PresentationThemeFillStrokeForeground(
|
||||
fillColor: UIColor(rgb: 0xffffff),
|
||||
strokeColor: UIColor(rgb: 0xffffff, alpha: 0.5),
|
||||
strokeColor: UIColor(rgb: 0xffffff, alpha: 0.3),
|
||||
foregroundColor: UIColor(rgb: 0x000000)
|
||||
),
|
||||
controlSecondaryColor: UIColor(rgb: 0xffffff, alpha: 0.5),
|
||||
|
@ -41,6 +41,7 @@ public enum PresentationResourceKey: Int32 {
|
||||
case itemListCheckIcon
|
||||
case itemListSecondaryCheckIcon
|
||||
case itemListPlusIcon
|
||||
case itemListRoundPlusIcon
|
||||
case itemListDeleteIcon
|
||||
case itemListDeleteIndicatorIcon
|
||||
case itemListReorderIndicatorIcon
|
||||
|
@ -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? {
|
||||
return theme.image(PresentationResourceKey.itemListDeleteIcon.rawValue, { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: theme.list.itemDestructiveColor)
|
||||
|
@ -898,6 +898,8 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
||||
let resultTitleString = strings.Notification_ChangedToSameWallpaper(compactAuthorName)
|
||||
attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
|
||||
}
|
||||
case .giftCode:
|
||||
attributedString = NSAttributedString(string: "Gift code", font: titleFont, textColor: primaryTextColor)
|
||||
case .unknown:
|
||||
attributedString = nil
|
||||
}
|
||||
|
@ -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",
|
||||
],
|
||||
)
|
@ -3999,7 +3999,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
adminedChannels: self.adminedChannels.get(),
|
||||
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 {
|
||||
return
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ swift_library(
|
||||
"//submodules/TooltipUI",
|
||||
"//submodules/OverlayStatusController",
|
||||
"//submodules/UndoUI",
|
||||
"//submodules/TemporaryCachedPeerDataManager",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -26,6 +26,7 @@ import OverlayStatusController
|
||||
import Markdown
|
||||
import TelegramUIPreferences
|
||||
import UndoUI
|
||||
import TelegramStringFormatting
|
||||
|
||||
final class ShareWithPeersScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
@ -494,7 +495,7 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
}
|
||||
|
||||
@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
|
||||
}
|
||||
switch recognizer.state {
|
||||
@ -518,8 +519,12 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
|
||||
if translation.y > 100.0 || velocity.y > 10.0 {
|
||||
controller.requestDismiss()
|
||||
Queue.mainQueue().justDispatch {
|
||||
controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .spring))
|
||||
if case .members = component.stateContext.subject {
|
||||
|
||||
} else {
|
||||
Queue.mainQueue().justDispatch {
|
||||
controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .spring))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let transition = Transition(animation: .curve(duration: 0.3, curve: .spring))
|
||||
@ -815,7 +820,11 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -830,7 +839,7 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
topOffsetFraction = max(0.0, min(1.0, topOffsetFraction))
|
||||
|
||||
let transitionFactor: CGFloat = 1.0 - topOffsetFraction
|
||||
if let controller = environment.controller() {
|
||||
if let controller = environment.controller() as? ShareWithPeersScreen {
|
||||
Queue.mainQueue().justDispatch {
|
||||
var transition = transition
|
||||
if controller.modalStyleOverlayTransitionFactor.isZero && transitionFactor > 0.0, transition.animation.isImmediate {
|
||||
@ -951,7 +960,11 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
} else if section.id == 2 {
|
||||
sectionTitle = environment.strings.Story_Privacy_WhoCanViewHeader
|
||||
} 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 {
|
||||
sectionTitle = ""
|
||||
}
|
||||
@ -1389,7 +1402,15 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
subtitle = nil
|
||||
}
|
||||
} 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)
|
||||
@ -1734,10 +1755,17 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
self.isDismissed = true
|
||||
|
||||
if let controller = self.environment?.controller() {
|
||||
controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .easeInOut))
|
||||
if let controller = self.environment?.controller() as? ShareWithPeersScreen {
|
||||
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
|
||||
@ -1798,6 +1826,8 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
contentTransition = .spring(duration: 0.4)
|
||||
}
|
||||
self.currentHasChannels = hasChannels
|
||||
} else if case .members = component.stateContext.subject {
|
||||
self.dismissPanGesture?.isEnabled = false
|
||||
}
|
||||
|
||||
let environment = environment[ViewControllerComponentContainer.Environment.self].value
|
||||
@ -1943,6 +1973,8 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
|
||||
let placeholder: String
|
||||
switch component.stateContext.subject {
|
||||
case .members:
|
||||
placeholder = "Search Subscribers"
|
||||
case .chats:
|
||||
placeholder = environment.strings.Story_Privacy_SearchChats
|
||||
default:
|
||||
@ -2009,6 +2041,11 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
}
|
||||
|
||||
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(
|
||||
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
|
||||
let navigationSideInset: CGFloat = 16.0
|
||||
@ -2262,6 +2303,9 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
}
|
||||
case .search:
|
||||
title = ""
|
||||
case .members:
|
||||
title = "Gift Premium"
|
||||
actionButtonTitle = "Save Recipients"
|
||||
}
|
||||
let navigationTitleSize = self.navigationTitle.update(
|
||||
transition: .immediate,
|
||||
@ -2298,7 +2342,9 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
topInset = 0.0
|
||||
} else {
|
||||
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 {
|
||||
inset = 351.0
|
||||
inset += 10.0 + environment.safeInsets.bottom + 50.0 + footersTotalHeight
|
||||
@ -2412,7 +2458,7 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
}))
|
||||
|
||||
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 {
|
||||
return
|
||||
}
|
||||
@ -2435,7 +2481,7 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
}
|
||||
if savePeers {
|
||||
let _ = (updatePeersListStoredState(engine: component.context.engine, base: base, peerIds: self.selectedPeers)
|
||||
|> deliverOnMainQueue).start(completed: {
|
||||
|> deliverOnMainQueue).start(completed: {
|
||||
complete()
|
||||
})
|
||||
} 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.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.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 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()
|
||||
}
|
||||
}
|
||||
|
||||
public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
private let context: AccountContext
|
||||
|
||||
private var isCustomModal = true
|
||||
private var isDismissed: Bool = false
|
||||
|
||||
public var dismissed: () -> Void = {}
|
||||
@ -3189,8 +2717,8 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
mentions: [String] = [],
|
||||
stateContext: StateContext,
|
||||
completion: @escaping (EnginePeer.Id?, EngineStoryPrivacy, Bool, Bool, [EnginePeer], Bool) -> Void,
|
||||
editCategory: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void,
|
||||
editBlockedPeers: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void,
|
||||
editCategory: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void = { _, _, _ in },
|
||||
editBlockedPeers: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void = { _, _, _ in },
|
||||
peerCompletion: @escaping (EnginePeer.Id) -> Void = { _ in }
|
||||
) {
|
||||
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(
|
||||
context: context,
|
||||
stateContext: stateContext,
|
||||
@ -3349,10 +2881,15 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
editCategory: editCategory,
|
||||
editBlockedPeers: editBlockedPeers,
|
||||
peerCompletion: peerCompletion
|
||||
), navigationBarAppearance: .none, theme: .dark)
|
||||
), navigationBarAppearance: .none, theme: theme)
|
||||
|
||||
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.automaticallyControlPresentationContextLayout = false
|
||||
self.lockOrientation = true
|
||||
@ -3372,10 +2909,12 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
self.view.disablesInteractiveModalDismiss = true
|
||||
|
||||
if let componentView = self.node.hostView.componentView as? ShareWithPeersScreenComponent.View {
|
||||
componentView.animateIn()
|
||||
if self.isCustomModal {
|
||||
self.view.disablesInteractiveModalDismiss = true
|
||||
|
||||
if let componentView = self.node.hostView.componentView as? ShareWithPeersScreenComponent.View {
|
||||
componentView.animateIn()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3404,57 +2943,19 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
self.isDismissed = true
|
||||
|
||||
self.view.endEditing(true)
|
||||
|
||||
if let componentView = self.node.hostView.componentView as? ShareWithPeersScreenComponent.View {
|
||||
componentView.animateOut(completion: { [weak self] in
|
||||
completion?()
|
||||
self?.dismiss(animated: false)
|
||||
})
|
||||
|
||||
if self.isCustomModal {
|
||||
if let componentView = self.node.hostView.componentView as? ShareWithPeersScreenComponent.View {
|
||||
componentView.animateOut(completion: { [weak self] in
|
||||
completion?()
|
||||
self?.dismiss(animated: false)
|
||||
})
|
||||
} else {
|
||||
self.dismiss(animated: false)
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
95
submodules/TelegramUI/Images.xcassets/Chat List/AddRoundIcon.imageset/AddPlus.pdf
vendored
Normal file
95
submodules/TelegramUI/Images.xcassets/Chat List/AddRoundIcon.imageset/AddPlus.pdf
vendored
Normal 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
|
12
submodules/TelegramUI/Images.xcassets/Chat List/AddRoundIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat List/AddRoundIcon.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AddPlus.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AvatarBoost.pdf",
|
||||
"filename" : "Reassign.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
|
@ -16,7 +16,7 @@ endobj
|
||||
endobj
|
||||
|
||||
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 ]
|
||||
/ColorSpace /DeviceRGB
|
||||
/Function 1 0 R
|
||||
@ -35,94 +35,78 @@ stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
q
|
||||
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
|
||||
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
|
||||
/Pattern cs
|
||||
/P1 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
|
||||
24.000000 12.000000 m
|
||||
24.000000 5.372583 18.627417 0.000000 12.000000 0.000000 c
|
||||
5.372583 0.000000 0.000000 5.372583 0.000000 12.000000 c
|
||||
0.000000 18.627417 5.372583 24.000000 12.000000 24.000000 c
|
||||
18.627417 24.000000 24.000000 18.627417 24.000000 12.000000 c
|
||||
h
|
||||
f
|
||||
n
|
||||
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
|
||||
6.639333 9.995974 m
|
||||
6.270643 9.995974 5.989172 10.325375 6.046674 10.689552 c
|
||||
6.848394 15.767110 l
|
||||
6.948168 16.399014 6.121798 16.727470 5.760551 16.199497 c
|
||||
6.046674 10.689552 m
|
||||
5.989172 10.325375 6.270643 9.995975 6.639333 9.995975 c
|
||||
9.125995 9.995975 l
|
||||
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.166615 7.536714 0.118531 6.996113 0.601028 6.996113 c
|
||||
3.087691 6.996113 l
|
||||
3.456380 6.996113 3.737850 6.666713 3.680349 6.302535 c
|
||||
2.878629 1.224977 l
|
||||
2.778855 0.593074 3.605225 0.264616 3.966471 0.792590 c
|
||||
9.621180 9.057164 l
|
||||
9.893639 9.455373 9.608493 9.995974 9.125995 9.995974 c
|
||||
6.639333 9.995974 l
|
||||
6.577924 4.609328 l
|
||||
h
|
||||
f*
|
||||
n
|
||||
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
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
2168
|
||||
1598
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 27.319946 27.320000 ]
|
||||
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
|
||||
/Resources 3 0 R
|
||||
/Contents 4 0 R
|
||||
/Parent 7 0 R
|
||||
@ -149,15 +133,15 @@ xref
|
||||
0000000727 00000 n
|
||||
0000000749 00000 n
|
||||
0000001379 00000 n
|
||||
0000003603 00000 n
|
||||
0000003626 00000 n
|
||||
0000003799 00000 n
|
||||
0000003873 00000 n
|
||||
0000003033 00000 n
|
||||
0000003056 00000 n
|
||||
0000003229 00000 n
|
||||
0000003303 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 8 0 R
|
||||
/Size 9
|
||||
>>
|
||||
startxref
|
||||
3932
|
||||
3362
|
||||
%%EOF
|
12
submodules/TelegramUI/Images.xcassets/Premium/Giveaway.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Premium/Giveaway.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "giveaway_30.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
186
submodules/TelegramUI/Images.xcassets/Premium/Giveaway.imageset/giveaway_30.pdf
vendored
Normal file
186
submodules/TelegramUI/Images.xcassets/Premium/Giveaway.imageset/giveaway_30.pdf
vendored
Normal 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
|
@ -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))
|
||||
strongSelf.push(controller)
|
||||
return true
|
||||
case let .giftCode(slug, _, _, _):
|
||||
strongSelf.openResolved(result: .premiumGiftCode(slug: slug), sourceMessageId: message.id)
|
||||
return true
|
||||
case let .suggestedProfilePhoto(image):
|
||||
strongSelf.chatDisplayNode.dismissInput()
|
||||
if let image = image {
|
||||
|
@ -101,12 +101,18 @@ final class ChatMessageAttachedContentButtonNode: HighlightTrackingButtonNode {
|
||||
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)
|
||||
}
|
||||
UIView.transition(with: strongSelf.view, duration: 0.2, options: [.transitionCrossDissolve], animations: {
|
||||
strongSelf.backgroundNode.image = strongSelf.regularImage
|
||||
strongSelf.iconNode.image = strongSelf.regularIconImage
|
||||
strongSelf.textNode.isHidden = false
|
||||
strongSelf.highlightedTextNode.isHidden = true
|
||||
}, completion: nil)
|
||||
if let snapshot = strongSelf.view.snapshotView(afterScreenUpdates: false) {
|
||||
strongSelf.view.addSubview(snapshot)
|
||||
|
||||
snapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
snapshot.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
|
||||
strongSelf.backgroundNode.image = strongSelf.regularImage
|
||||
strongSelf.iconNode.image = strongSelf.regularIconImage
|
||||
strongSelf.textNode.isHidden = false
|
||||
strongSelf.highlightedTextNode.isHidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -192,6 +192,8 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
|
||||
result.append((message, ChatMessageProfilePhotoSuggestionContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||
} else if case .setChatWallpaper = action.action {
|
||||
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 {
|
||||
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 {
|
||||
result.append((message, ChatMessagePollBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||
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 {
|
||||
isUnsupportedMedia = true
|
||||
needReactions = false
|
||||
@ -321,14 +326,16 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
|
||||
result.last?.1 == ChatMessagePollBubbleContentNode.self ||
|
||||
result.last?.1 == ChatMessageContactBubbleContentNode.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)))
|
||||
needReactions = false
|
||||
} else if result.last?.1 == ChatMessageCommentFooterContentNode.self {
|
||||
if result.count >= 2 {
|
||||
if result[result.count - 2].1 == ChatMessageWebpageBubbleContentNode.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)
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
import ChatControllerInteraction
|
||||
import ShimmerEffect
|
||||
import Markdown
|
||||
|
||||
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)
|
||||
@ -173,42 +174,76 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center)
|
||||
|
||||
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 primaryTextColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText
|
||||
|
||||
var duration: String = ""
|
||||
var months: Int32 = 3
|
||||
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 {
|
||||
if let action = media as? TelegramMediaAction {
|
||||
switch action.action {
|
||||
case let .giftPremium(_, _, months, _, _):
|
||||
duration = item.presentationData.strings.Notification_PremiumGift_Subtitle(item.presentationData.strings.Notification_PremiumGift_Months(months)).string
|
||||
switch months {
|
||||
case 12:
|
||||
animationName = "Gift12"
|
||||
case 6:
|
||||
animationName = "Gift6"
|
||||
case 3:
|
||||
animationName = "Gift3"
|
||||
default:
|
||||
animationName = "Gift3"
|
||||
case let .giftPremium(_, _, monthsValue, _, _):
|
||||
months = monthsValue
|
||||
text = item.presentationData.strings.Notification_PremiumGift_Subtitle(item.presentationData.strings.Notification_PremiumGift_Months(months)).string
|
||||
case let .giftCode(_, fromGiveaway, channelId, monthsValue):
|
||||
if fromGiveaway {
|
||||
giftSize.width += 34.0
|
||||
giftSize.height += 84.0
|
||||
textSpacing += 20.0
|
||||
|
||||
title = "Congratulations!"
|
||||
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:
|
||||
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 (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()
|
||||
if labelRects.count > 1 {
|
||||
@ -233,14 +268,23 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
|
||||
let backgroundMaskImage: (CGPoint, UIImage)?
|
||||
var backgroundMaskUpdated = false
|
||||
if let (currentOffset, currentImage, currentRects) = cachedMaskBackgroundImage, currentRects == labelRects {
|
||||
backgroundMaskImage = (currentOffset, currentImage)
|
||||
if hasServiceMessage {
|
||||
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 {
|
||||
backgroundMaskImage = LinkHighlightingNode.generateImage(color: .black, inset: 0.0, innerRadius: 10.0, outerRadius: 10.0, rects: labelRects, useModernPathCalculation: false)
|
||||
backgroundMaskUpdated = true
|
||||
backgroundMaskImage = nil
|
||||
}
|
||||
|
||||
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, { [weak self] animation, synchronousLoads, _ in
|
||||
@ -253,9 +297,11 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
|
||||
strongSelf.updateVisibility()
|
||||
|
||||
strongSelf.labelNode.isHidden = !hasServiceMessage
|
||||
|
||||
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)
|
||||
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)
|
||||
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
|
||||
|
||||
let buttonTitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonTitleLayout.size.width) / 2.0), y: subtitleFrame.maxY + 18.0), size: buttonTitleLayout.size)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -478,7 +478,6 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible {
|
||||
break loop
|
||||
case let .Video(_, _, flags, _):
|
||||
if flags.contains(.instantRoundVideo) {
|
||||
// viewClassName = ChatMessageInstantVideoItemNode.self
|
||||
viewClassName = ChatMessageBubbleItemNode.self
|
||||
break loop
|
||||
}
|
||||
|
@ -1068,6 +1068,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
break
|
||||
case .boost:
|
||||
break
|
||||
case .premiumGiftCode:
|
||||
break
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
@ -148,6 +148,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
|
||||
deinit {
|
||||
self.createActionDisposable.dispose()
|
||||
self.presentationDataDisposable?.dispose()
|
||||
self.confirmationDisposable.dispose()
|
||||
}
|
||||
|
||||
@objc private func beginSearch() {
|
||||
|
@ -881,8 +881,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
|
||||
case .ok:
|
||||
updateImpl?()
|
||||
case let .replace(previousPeer):
|
||||
let text = presentationData.strings.ChannelBoost_ReplaceBoost(previousPeer.compactDisplayTitle, peer.compactDisplayTitle).string
|
||||
let controller = replaceBoostConfirmationController(context: context, fromPeer: previousPeer, toPeer: peer, text: text, commit: {
|
||||
let controller = replaceBoostConfirmationController(context: context, fromPeers: [previousPeer], toPeer: peer, commit: {
|
||||
updateImpl?()
|
||||
})
|
||||
present(controller, nil)
|
||||
@ -953,5 +952,27 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -11,8 +11,82 @@ import AccountContext
|
||||
import AppBundle
|
||||
import AvatarNode
|
||||
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 text: String
|
||||
|
||||
@ -26,13 +100,15 @@ private final class PhotoUpdateConfirmationAlertContentNode: AlertContentNode {
|
||||
private let actionNodes: [TextAlertContentActionNode]
|
||||
private let actionVerticalSeparators: [ASDisplayNode]
|
||||
|
||||
private var boostNodes: [PreviousBoostNode] = []
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
override var dismissOnOutsideTap: Bool {
|
||||
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.text = text
|
||||
|
||||
@ -49,7 +125,7 @@ private final class PhotoUpdateConfirmationAlertContentNode: AlertContentNode {
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
self.iconNode.image = UIImage(bundleImageName: "Premium/AvatarBoost")
|
||||
self.iconNode.image = generateBoostIcon(theme: ptheme)
|
||||
|
||||
self.actionNodesSeparator = ASDisplayNode()
|
||||
self.actionNodesSeparator.isLayerBacked = true
|
||||
@ -68,6 +144,18 @@ private final class PhotoUpdateConfirmationAlertContentNode: AlertContentNode {
|
||||
}
|
||||
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()
|
||||
|
||||
self.addSubnode(self.textNode)
|
||||
@ -86,9 +174,20 @@ private final class PhotoUpdateConfirmationAlertContentNode: AlertContentNode {
|
||||
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.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)
|
||||
}
|
||||
|
||||
@ -145,8 +244,10 @@ private final class PhotoUpdateConfirmationAlertContentNode: AlertContentNode {
|
||||
|
||||
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))
|
||||
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
|
||||
var minActionsWidth: CGFloat = 0.0
|
||||
@ -171,6 +272,17 @@ private final class PhotoUpdateConfirmationAlertContentNode: AlertContentNode {
|
||||
|
||||
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
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
@ -179,8 +291,7 @@ private final class PhotoUpdateConfirmationAlertContentNode: AlertContentNode {
|
||||
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)))
|
||||
|
||||
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 presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
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 contentNode: PhotoUpdateConfirmationAlertContentNode?
|
||||
var contentNode: ReplaceBoostConfirmationAlertContentNode?
|
||||
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
dismissImpl?(true)
|
||||
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.ChannelBoost_Replace, action: {
|
||||
}), TextAlertAction(type: .defaultAction, title: "Reassign", action: {
|
||||
dismissImpl?(true)
|
||||
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!)
|
||||
dismissImpl = { [weak controller] animated in
|
||||
|
@ -5,7 +5,7 @@ import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "TelegramVoip",
|
||||
platforms: [.macOS(.v10_12)],
|
||||
platforms: [.macOS(.v10_13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
|
@ -100,6 +100,7 @@ public enum ParsedInternalUrl {
|
||||
case startAttach(String, String?, String?)
|
||||
case contactToken(String)
|
||||
case chatFolder(slug: String)
|
||||
case premiumGiftCode(slug: String)
|
||||
}
|
||||
|
||||
private enum ParsedUrl {
|
||||
@ -453,6 +454,8 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? {
|
||||
return .chatFolder(slug: pathComponents[1])
|
||||
} else if pathComponents[0] == "boost", pathComponents.count == 2 {
|
||||
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" {
|
||||
if let channelId = Int64(pathComponents[1]), let messageId = Int32(pathComponents[2]) {
|
||||
var threadId: Int32?
|
||||
@ -899,6 +902,8 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl)
|
||||
return .single(.inaccessiblePeer)
|
||||
}
|
||||
}
|
||||
case let .premiumGiftCode(slug):
|
||||
return .single(.premiumGiftCode(slug: slug))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,7 @@ import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "DarwinDirStat",
|
||||
platforms: [.macOS(.v10_12)],
|
||||
platforms: [.macOS(.v10_13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
|
@ -5,7 +5,7 @@ import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "RangeSet",
|
||||
platforms: [.macOS(.v10_12)],
|
||||
platforms: [.macOS(.v10_13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
|
@ -5,7 +5,7 @@ import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "YuvConversion",
|
||||
platforms: [.macOS(.v10_12)],
|
||||
platforms: [.macOS(.v10_13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
|
2
submodules/libphonenumber/Package.swift
vendored
2
submodules/libphonenumber/Package.swift
vendored
@ -5,7 +5,7 @@ import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "libphonenumber",
|
||||
platforms: [.macOS(.v10_12)],
|
||||
platforms: [.macOS(.v10_13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
|
2
submodules/sqlcipher/Package.swift
vendored
2
submodules/sqlcipher/Package.swift
vendored
@ -5,7 +5,7 @@ import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "sqlcipher",
|
||||
platforms: [.macOS(.v10_12)],
|
||||
platforms: [.macOS(.v10_13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
|
2
third-party/rnnoise/Package.swift
vendored
2
third-party/rnnoise/Package.swift
vendored
@ -36,7 +36,7 @@ func replaceSymbols() -> [String] {
|
||||
|
||||
let package = Package(
|
||||
name: "rnoise",
|
||||
platforms: [.macOS(.v10_12)],
|
||||
platforms: [.macOS(.v10_13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
|
Loading…
x
Reference in New Issue
Block a user