Merge commit '943d22c54e0d4c97a0d842d5804a7cc2e58f952c'

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

View File

@ -10020,6 +10020,9 @@ Sorry for the inconvenience.";
"Stats.Boosts.BoostersNone" = "BOOSTERS";
"Stats.Boosts.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";

View File

@ -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 {

View File

@ -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,

View File

@ -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(

View File

@ -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(

View File

@ -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) {

View File

@ -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(

View File

@ -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(

View File

@ -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(

View File

@ -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 {

View File

@ -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(

View File

@ -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)
}
}

View File

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

View File

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

View File

@ -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(

View File

@ -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(

View File

@ -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(

View File

@ -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(

View File

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

View File

@ -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(

View File

@ -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(

View File

@ -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",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -670,11 +670,12 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
self.updateInProgress(true)
self.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) {

View File

@ -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)

View File

@ -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
}
}

View File

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

View File

@ -5,7 +5,7 @@ import PackageDescription
let package = Package(
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(

View File

@ -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(

View File

@ -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(

View File

@ -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)

View File

@ -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(

View File

@ -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(

View File

@ -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:

View File

@ -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
}
}
}
}

View File

@ -1,3 +1,57 @@
public extension Api {
enum InputWebDocument: TypeConstructorDescription {
case inputWebDocument(url: String, size: Int32, mimeType: String, attributes: [Api.DocumentAttribute])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .inputWebDocument(let url, let size, let mimeType, let attributes):
if boxed {
buffer.appendInt32(-1678949555)
}
serializeString(url, buffer: buffer, boxed: false)
serializeInt32(size, buffer: buffer, boxed: false)
serializeString(mimeType, buffer: buffer, boxed: false)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(attributes.count))
for item in attributes {
item.serialize(buffer, true)
}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .inputWebDocument(let url, let size, let mimeType, let attributes):
return ("inputWebDocument", [("url", url as Any), ("size", size as Any), ("mimeType", mimeType as Any), ("attributes", attributes as Any)])
}
}
public static func parse_inputWebDocument(_ reader: BufferReader) -> InputWebDocument? {
var _1: String?
_1 = parseString(reader)
var _2: Int32?
_2 = reader.readInt32()
var _3: String?
_3 = parseString(reader)
var _4: [Api.DocumentAttribute]?
if let _ = reader.readInt32() {
_4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.DocumentAttribute.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = _4 != nil
if _c1 && _c2 && _c3 && _c4 {
return Api.InputWebDocument.inputWebDocument(url: _1!, size: _2!, mimeType: _3!, attributes: _4!)
}
else {
return nil
}
}
}
}
public extension Api {
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
}
}
}
}

View File

@ -1,3 +1,111 @@
public extension Api {
enum LangPackString: TypeConstructorDescription {
case langPackString(key: String, value: String)
case langPackStringDeleted(key: String)
case langPackStringPluralized(flags: Int32, key: String, zeroValue: String?, oneValue: String?, twoValue: String?, fewValue: String?, manyValue: String?, otherValue: String)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .langPackString(let key, let value):
if boxed {
buffer.appendInt32(-892239370)
}
serializeString(key, buffer: buffer, boxed: false)
serializeString(value, buffer: buffer, boxed: false)
break
case .langPackStringDeleted(let key):
if boxed {
buffer.appendInt32(695856818)
}
serializeString(key, buffer: buffer, boxed: false)
break
case .langPackStringPluralized(let flags, let key, let zeroValue, let oneValue, let twoValue, let fewValue, let manyValue, let otherValue):
if boxed {
buffer.appendInt32(1816636575)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeString(key, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {serializeString(zeroValue!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 1) != 0 {serializeString(oneValue!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 2) != 0 {serializeString(twoValue!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 3) != 0 {serializeString(fewValue!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 4) != 0 {serializeString(manyValue!, buffer: buffer, boxed: false)}
serializeString(otherValue, buffer: buffer, boxed: false)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .langPackString(let key, let value):
return ("langPackString", [("key", key as Any), ("value", value as Any)])
case .langPackStringDeleted(let key):
return ("langPackStringDeleted", [("key", key as Any)])
case .langPackStringPluralized(let flags, let key, let zeroValue, let oneValue, let twoValue, let fewValue, let manyValue, let otherValue):
return ("langPackStringPluralized", [("flags", flags as Any), ("key", key as Any), ("zeroValue", zeroValue as Any), ("oneValue", oneValue as Any), ("twoValue", twoValue as Any), ("fewValue", fewValue as Any), ("manyValue", manyValue as Any), ("otherValue", otherValue as Any)])
}
}
public static func parse_langPackString(_ reader: BufferReader) -> LangPackString? {
var _1: String?
_1 = parseString(reader)
var _2: String?
_2 = parseString(reader)
let _c1 = _1 != nil
let _c2 = _2 != nil
if _c1 && _c2 {
return Api.LangPackString.langPackString(key: _1!, value: _2!)
}
else {
return nil
}
}
public static func parse_langPackStringDeleted(_ reader: BufferReader) -> LangPackString? {
var _1: String?
_1 = parseString(reader)
let _c1 = _1 != nil
if _c1 {
return Api.LangPackString.langPackStringDeleted(key: _1!)
}
else {
return nil
}
}
public static func parse_langPackStringPluralized(_ reader: BufferReader) -> LangPackString? {
var _1: Int32?
_1 = reader.readInt32()
var _2: String?
_2 = parseString(reader)
var _3: String?
if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) }
var _4: String?
if Int(_1!) & Int(1 << 1) != 0 {_4 = parseString(reader) }
var _5: String?
if Int(_1!) & Int(1 << 2) != 0 {_5 = parseString(reader) }
var _6: String?
if Int(_1!) & Int(1 << 3) != 0 {_6 = parseString(reader) }
var _7: String?
if Int(_1!) & Int(1 << 4) != 0 {_7 = parseString(reader) }
var _8: String?
_8 = parseString(reader)
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil
let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil
let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil
let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil
let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil
let _c8 = _8 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 {
return Api.LangPackString.langPackStringPluralized(flags: _1!, key: _2!, zeroValue: _3, oneValue: _4, twoValue: _5, fewValue: _6, manyValue: _7, otherValue: _8!)
}
else {
return nil
}
}
}
}
public extension Api {
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()

View File

@ -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()

View File

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

View File

@ -536,6 +536,84 @@ public extension Api.payments {
}
}
public extension Api.payments {
enum CheckedGiftCode: TypeConstructorDescription {
case checkedGiftCode(flags: Int32, fromId: Api.Peer, toId: Int64?, date: Int32, months: Int32, usedDate: Int32?, chats: [Api.Chat], users: [Api.User])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .checkedGiftCode(let flags, let fromId, let toId, let date, let months, let usedDate, let chats, let users):
if boxed {
buffer.appendInt32(-9426548)
}
serializeInt32(flags, buffer: buffer, boxed: false)
fromId.serialize(buffer, true)
if Int(flags) & Int(1 << 0) != 0 {serializeInt64(toId!, buffer: buffer, boxed: false)}
serializeInt32(date, buffer: buffer, boxed: false)
serializeInt32(months, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 1) != 0 {serializeInt32(usedDate!, buffer: buffer, boxed: false)}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(chats.count))
for item in chats {
item.serialize(buffer, true)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(users.count))
for item in users {
item.serialize(buffer, true)
}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .checkedGiftCode(let flags, let fromId, let toId, let date, let months, let usedDate, let chats, let users):
return ("checkedGiftCode", [("flags", flags as Any), ("fromId", fromId as Any), ("toId", toId as Any), ("date", date as Any), ("months", months as Any), ("usedDate", usedDate as Any), ("chats", chats as Any), ("users", users as Any)])
}
}
public static func parse_checkedGiftCode(_ reader: BufferReader) -> CheckedGiftCode? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Api.Peer?
if let signature = reader.readInt32() {
_2 = Api.parse(reader, signature: signature) as? Api.Peer
}
var _3: Int64?
if Int(_1!) & Int(1 << 0) != 0 {_3 = reader.readInt64() }
var _4: Int32?
_4 = reader.readInt32()
var _5: Int32?
_5 = reader.readInt32()
var _6: Int32?
if Int(_1!) & Int(1 << 1) != 0 {_6 = reader.readInt32() }
var _7: [Api.Chat]?
if let _ = reader.readInt32() {
_7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
}
var _8: [Api.User]?
if let _ = reader.readInt32() {
_8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil
let _c4 = _4 != nil
let _c5 = _5 != nil
let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil
let _c7 = _7 != nil
let _c8 = _8 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 {
return Api.payments.CheckedGiftCode.checkedGiftCode(flags: _1!, fromId: _2!, toId: _3, date: _4!, months: _5!, usedDate: _6, chats: _7!, users: _8!)
}
else {
return nil
}
}
}
}
public extension Api.payments {
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
}
}
}
}

View File

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

View File

@ -7440,6 +7440,21 @@ public extension Api.functions.messages {
})
}
}
public extension Api.functions.payments {
static func applyGiftCode(slug: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
let buffer = Buffer()
buffer.appendInt32(-152934316)
serializeString(slug, buffer: buffer, boxed: false)
return (FunctionDescription(name: "payments.applyGiftCode", parameters: [("slug", String(describing: slug))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
let reader = BufferReader(buffer)
var result: Api.Updates?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.Updates
}
return result
})
}
}
public extension Api.functions.payments {
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()

View File

@ -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(

View File

@ -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
}()

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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))
}
}

View File

@ -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! {

View File

@ -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 []
}

View File

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

View File

@ -13,12 +13,13 @@ public enum AssignAppStoreTransactionError {
public enum AppStoreTransactionPurpose {
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

View File

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

View File

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

View File

@ -444,7 +444,7 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati
),
itemCheckColors: PresentationThemeFillStrokeForeground(
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),

View File

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

View File

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

View File

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

View File

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

View File

@ -3999,7 +3999,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
adminedChannels: self.adminedChannels.get(),
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
}

View File

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

View File

@ -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)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

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

View File

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

View File

@ -930,6 +930,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let controller = PremiumIntroScreen(context: strongSelf.context, source: .gift(from: fromPeerId, to: toPeerId, duration: duration))
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 {

View File

@ -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
}
}
}

View File

@ -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)
}
}

View File

@ -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)

View File

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

View File

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

View File

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

View File

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

View File

@ -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)
}
})
}
}

View File

@ -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

View File

@ -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(

View File

@ -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))
}
}

View File

@ -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(

View File

@ -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(

View File

@ -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(

View File

@ -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(

View File

@ -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(

View File

@ -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(