diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 50bde0fc90..b6d52aa300 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -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"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 4d3431911f..ccfa02210b 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -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 { diff --git a/submodules/AccountContext/Sources/PeerSelectionController.swift b/submodules/AccountContext/Sources/PeerSelectionController.swift index 947dbe23b3..d806ea0bf1 100644 --- a/submodules/AccountContext/Sources/PeerSelectionController.swift +++ b/submodules/AccountContext/Sources/PeerSelectionController.swift @@ -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)? = 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)? = nil, diff --git a/submodules/Crc32/Package.swift b/submodules/Crc32/Package.swift index 9a73c17566..aec41a7bf5 100644 --- a/submodules/Crc32/Package.swift +++ b/submodules/Crc32/Package.swift @@ -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( diff --git a/submodules/CryptoUtils/Package.swift b/submodules/CryptoUtils/Package.swift index 42be8a3256..40e68bc9bb 100644 --- a/submodules/CryptoUtils/Package.swift +++ b/submodules/CryptoUtils/Package.swift @@ -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( diff --git a/submodules/DatePickerNode/Sources/DatePickerNode.swift b/submodules/DatePickerNode/Sources/DatePickerNode.swift index 54d9a85b3f..1395eb7b2b 100644 --- a/submodules/DatePickerNode/Sources/DatePickerNode.swift +++ b/submodules/DatePickerNode/Sources/DatePickerNode.swift @@ -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) { diff --git a/submodules/Emoji/Package.swift b/submodules/Emoji/Package.swift index 98ba6f1eb0..0c4617ccda 100644 --- a/submodules/Emoji/Package.swift +++ b/submodules/Emoji/Package.swift @@ -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( diff --git a/submodules/EncryptionProvider/Package.swift b/submodules/EncryptionProvider/Package.swift index 2bbc806847..7571dda761 100644 --- a/submodules/EncryptionProvider/Package.swift +++ b/submodules/EncryptionProvider/Package.swift @@ -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( diff --git a/submodules/FFMpegBinding/Package.swift b/submodules/FFMpegBinding/Package.swift index a7ddae8d59..da926bdafc 100644 --- a/submodules/FFMpegBinding/Package.swift +++ b/submodules/FFMpegBinding/Package.swift @@ -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( diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index d611d9d3b3..e48e10fea8 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -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 { diff --git a/submodules/GraphCore/Package.swift b/submodules/GraphCore/Package.swift index f0ee70d8de..a82d5a1123 100644 --- a/submodules/GraphCore/Package.swift +++ b/submodules/GraphCore/Package.swift @@ -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( diff --git a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift index 2a27d2825b..19d12db08a 100644 --- a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift +++ b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift @@ -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 { + public func buyProduct(_ product: Product, purpose: AppStoreTransactionPurpose) -> Signal { 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() - let purpose: Signal - if !isSubscriptionProductId(productIdentifier) { - let peerId: Signal - if let targetPeerId = paymentContexts[productIdentifier]?.targetPeerId { - peerId = .single(targetPeerId) + let products = self.availableProducts + |> filter { products in + return !products.isEmpty + } + |> take(1) + + + let product: Signal = 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 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 - if let isUpgradeValue = paymentContexts[productIdentifier]?.isUpgrade { - isUpgrade = .single(isUpgradeValue) - } else { - isUpgrade = pendingInAppPurchaseState(engine: self.engine, productId: productIdentifier) - |> mapToSignal { state -> Signal 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 + 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 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 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) } } diff --git a/submodules/InviteLinksUI/BUILD b/submodules/InviteLinksUI/BUILD index bc125a6266..1e3b637e4d 100644 --- a/submodules/InviteLinksUI/BUILD +++ b/submodules/InviteLinksUI/BUILD @@ -58,6 +58,7 @@ swift_library( "//submodules/PeerInfoAvatarListNode:PeerInfoAvatarListNode", "//submodules/QrCodeUI:QrCodeUI", "//submodules/PromptUI", + "//submodules/TelegramUI/Components/ItemListDatePickerItem:ItemListDatePickerItem", ], visibility = [ "//visibility:public", diff --git a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift index 5179d173f9..0ae1175a9f 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift @@ -16,6 +16,7 @@ import AppBundle import ContextUI import TelegramStringFormatting import UndoUI +import ItemListDatePickerItem private final class InviteLinkEditControllerArguments { let context: AccountContext diff --git a/submodules/ManagedFile/Package.swift b/submodules/ManagedFile/Package.swift index 67e49adcbd..48bde4e5b6 100644 --- a/submodules/ManagedFile/Package.swift +++ b/submodules/ManagedFile/Package.swift @@ -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( diff --git a/submodules/MtProtoKit/Package.swift b/submodules/MtProtoKit/Package.swift index d464433335..7adf5f2bef 100644 --- a/submodules/MtProtoKit/Package.swift +++ b/submodules/MtProtoKit/Package.swift @@ -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( diff --git a/submodules/MurMurHash32/Package.swift b/submodules/MurMurHash32/Package.swift index a6841471df..3ae1b30397 100644 --- a/submodules/MurMurHash32/Package.swift +++ b/submodules/MurMurHash32/Package.swift @@ -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( diff --git a/submodules/NetworkLogging/Package.swift b/submodules/NetworkLogging/Package.swift index a3e2220488..793c8dc7cb 100644 --- a/submodules/NetworkLogging/Package.swift +++ b/submodules/NetworkLogging/Package.swift @@ -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( diff --git a/submodules/OpenSSLEncryptionProvider/Package.swift b/submodules/OpenSSLEncryptionProvider/Package.swift index df2556d5d5..1fd1668319 100644 --- a/submodules/OpenSSLEncryptionProvider/Package.swift +++ b/submodules/OpenSSLEncryptionProvider/Package.swift @@ -7,7 +7,7 @@ import PackageDescription let package = Package( name: "OpenSSLEncryption", platforms: [ - .macOS(.v10_12) + .macOS(.v10_13) ], products: [ .library( diff --git a/submodules/OpusBinding/Package.swift b/submodules/OpusBinding/Package.swift index 7ab91a7e99..12028015a3 100644 --- a/submodules/OpusBinding/Package.swift +++ b/submodules/OpusBinding/Package.swift @@ -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( diff --git a/submodules/Postbox/Package.swift b/submodules/Postbox/Package.swift index dd499b6a67..36fef05100 100644 --- a/submodules/Postbox/Package.swift +++ b/submodules/Postbox/Package.swift @@ -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( diff --git a/submodules/PremiumUI/BUILD b/submodules/PremiumUI/BUILD index 287f60e007..0b623ba638 100644 --- a/submodules/PremiumUI/BUILD +++ b/submodules/PremiumUI/BUILD @@ -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", diff --git a/submodules/PremiumUI/Sources/CreateGiveawayController.swift b/submodules/PremiumUI/Sources/CreateGiveawayController.swift new file mode 100644 index 0000000000..8eda3a3fd3 --- /dev/null +++ b/submodules/PremiumUI/Sources/CreateGiveawayController.swift @@ -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)? = 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(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 +} diff --git a/submodules/PremiumUI/Sources/CreateGiveawayFooterItem.swift b/submodules/PremiumUI/Sources/CreateGiveawayFooterItem.swift new file mode 100644 index 0000000000..b2641d8201 --- /dev/null +++ b/submodules/PremiumUI/Sources/CreateGiveawayFooterItem.swift @@ -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 + } + } +} diff --git a/submodules/PremiumUI/Sources/CreateGiveawayHeaderItem.swift b/submodules/PremiumUI/Sources/CreateGiveawayHeaderItem.swift new file mode 100644 index 0000000000..439aca60dd --- /dev/null +++ b/submodules/PremiumUI/Sources/CreateGiveawayHeaderItem.swift @@ -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?, (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? + + private var params: (AnyComponent, 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() + 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) + } +} diff --git a/submodules/PremiumUI/Sources/GiftModeItem.swift b/submodules/PremiumUI/Sources/GiftModeItem.swift new file mode 100644 index 0000000000..53e3cfebc8 --- /dev/null +++ b/submodules/PremiumUI/Sources/GiftModeItem.swift @@ -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?, (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) + } +} diff --git a/submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift new file mode 100644 index 0000000000..d03f457ff4 --- /dev/null +++ b/submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift @@ -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.self) + let animateOut = StoredActionSlot(Action.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(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 + + public init(id: IdType, title: String, component: AnyComponent) { + 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() + + 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, 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, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift index b83c4eb5a8..abdd935698 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift @@ -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) { diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 9857052b8c..8336a4d824 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -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) diff --git a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift index 6879b3aaca..b7290eca8e 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift @@ -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() + private let badgeBackground = UIView() + private let badgeText = ComponentView() + 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 } } diff --git a/submodules/PremiumUI/Sources/SubscriptionsCountItem.swift b/submodules/PremiumUI/Sources/SubscriptionsCountItem.swift new file mode 100644 index 0000000000..ec5554a272 --- /dev/null +++ b/submodules/PremiumUI/Sources/SubscriptionsCountItem.swift @@ -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? + let sectionId: ItemListSectionId + let updated: (Int32) -> Void + + init(theme: PresentationTheme, strings: PresentationStrings, text: String, value: Int32, range: Range?, 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?, (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)) + } +} diff --git a/submodules/Reachability/LegacyReachability/Package.swift b/submodules/Reachability/LegacyReachability/Package.swift index d5d9a0aa99..d0ddfe915b 100644 --- a/submodules/Reachability/LegacyReachability/Package.swift +++ b/submodules/Reachability/LegacyReachability/Package.swift @@ -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( diff --git a/submodules/Reachability/Package.swift b/submodules/Reachability/Package.swift index ce0c93317f..6becc10d3c 100644 --- a/submodules/Reachability/Package.swift +++ b/submodules/Reachability/Package.swift @@ -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( diff --git a/submodules/SSignalKit/Package.swift b/submodules/SSignalKit/Package.swift index b6cf5c3a19..fe090562c6 100644 --- a/submodules/SSignalKit/Package.swift +++ b/submodules/SSignalKit/Package.swift @@ -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( diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index 3880b212a6..c639730a54 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -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, 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, 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 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) diff --git a/submodules/StringTransliteration/Package.swift b/submodules/StringTransliteration/Package.swift index 1066784488..4b06d92f78 100644 --- a/submodules/StringTransliteration/Package.swift +++ b/submodules/StringTransliteration/Package.swift @@ -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( diff --git a/submodules/TelegramApi/Package.swift b/submodules/TelegramApi/Package.swift index ca9c6e3171..dc1ffdc671 100644 --- a/submodules/TelegramApi/Package.swift +++ b/submodules/TelegramApi/Package.swift @@ -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( diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 24587ec7b1..b5200e3a00 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -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: diff --git a/submodules/TelegramApi/Sources/Api10.swift b/submodules/TelegramApi/Sources/Api10.swift index c916dec5ee..594cf9600e 100644 --- a/submodules/TelegramApi/Sources/Api10.swift +++ b/submodules/TelegramApi/Sources/Api10.swift @@ -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 - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api11.swift b/submodules/TelegramApi/Sources/Api11.swift index 714fc85780..daac038500 100644 --- a/submodules/TelegramApi/Sources/Api11.swift +++ b/submodules/TelegramApi/Sources/Api11.swift @@ -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 - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api12.swift b/submodules/TelegramApi/Sources/Api12.swift index 07992a5908..fb0a25191d 100644 --- a/submodules/TelegramApi/Sources/Api12.swift +++ b/submodules/TelegramApi/Sources/Api12.swift @@ -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() diff --git a/submodules/TelegramApi/Sources/Api13.swift b/submodules/TelegramApi/Sources/Api13.swift index 609644708d..c67dfc86dc 100644 --- a/submodules/TelegramApi/Sources/Api13.swift +++ b/submodules/TelegramApi/Sources/Api13.swift @@ -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() diff --git a/submodules/TelegramApi/Sources/Api17.swift b/submodules/TelegramApi/Sources/Api17.swift index c50124ce4c..a9d789fe45 100644 --- a/submodules/TelegramApi/Sources/Api17.swift +++ b/submodules/TelegramApi/Sources/Api17.swift @@ -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?) diff --git a/submodules/TelegramApi/Sources/Api28.swift b/submodules/TelegramApi/Sources/Api28.swift index 2f7261907a..688c2ba5a6 100644 --- a/submodules/TelegramApi/Sources/Api28.swift +++ b/submodules/TelegramApi/Sources/Api28.swift @@ -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 - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api29.swift b/submodules/TelegramApi/Sources/Api29.swift index 79b798b05d..72d9e2ec78 100644 --- a/submodules/TelegramApi/Sources/Api29.swift +++ b/submodules/TelegramApi/Sources/Api29.swift @@ -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) diff --git a/submodules/TelegramApi/Sources/Api31.swift b/submodules/TelegramApi/Sources/Api31.swift index 62235a359c..bc7ed761f8 100644 --- a/submodules/TelegramApi/Sources/Api31.swift +++ b/submodules/TelegramApi/Sources/Api31.swift @@ -7440,6 +7440,21 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.payments { + static func applyGiftCode(slug: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + 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) { 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) { + 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) { 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) { let buffer = Buffer() diff --git a/submodules/TelegramCore/Package.swift b/submodules/TelegramCore/Package.swift index c98e938f00..bd86af416d 100644 --- a/submodules/TelegramCore/Package.swift +++ b/submodules/TelegramCore/Package.swift @@ -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( diff --git a/submodules/TelegramCore/Sources/Account/AccountManager.swift b/submodules/TelegramCore/Sources/Account/AccountManager.swift index 89807cef1d..e8a5358bc6 100644 --- a/submodules/TelegramCore/Sources/Account/AccountManager.swift +++ b/submodules/TelegramCore/Sources/Account/AccountManager.swift @@ -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 }() diff --git a/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift b/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift index 2a4baa3a60..46a78ee071 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift @@ -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) } } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 2982656c33..1ca7f692f3 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -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) } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index 997719f0cd..f7ae9582bc 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -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)) } } diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index fe151528ce..ec47cea9fe 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -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! { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index 9e1cf940a1..90ad9f1efc 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -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 [] } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaGiveaway.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaGiveaway.swift new file mode 100644 index 0000000000..bc6cf567d0 --- /dev/null +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaGiveaway.swift @@ -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) + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift index cfb26483ab..08035cbc02 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift @@ -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 { - var purposeSignal: Signal +private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTransactionPurpose) -> Signal { 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 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 { + return apiInputStorePaymentPurpose(account: account, purpose: purpose) |> castError(AssignAppStoreTransactionError.self) |> mapToSignal { purpose -> Signal 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 { - var purposeSignal: Signal - 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 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 in return account.network.request(Api.functions.payments.canPurchasePremium(purpose: purpose)) |> map { result -> Bool in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/GiftCodes.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/GiftCodes.swift new file mode 100644 index 0000000000..a029bb2b5f --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/GiftCodes.swift @@ -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 { + return account.network.request(Api.functions.payments.checkGiftCode(slug: slug)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal 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 { + return account.network.request(Api.functions.payments.applyGiftCode(slug: slug)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { updates -> Signal 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 + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index c9773af950..9eadddbf77 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -45,5 +45,17 @@ public extension TelegramEngine { public func canPurchasePremium(purpose: AppStoreTransactionPurpose) -> Signal { return _internal_canPurchasePremium(account: self.account, purpose: purpose) } + + public func checkPremiumGiftCode(slug: String) -> Signal { + return _internal_checkPremiumGiftCode(account: self.account, slug: slug) + } + + public func applyPremiumGiftCode(slug: String) -> Signal { + 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) + } } } diff --git a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift index c9a2e45a2b..bd674e312b 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift @@ -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), diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index 8b783db238..42845c0d51 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -41,6 +41,7 @@ public enum PresentationResourceKey: Int32 { case itemListCheckIcon case itemListSecondaryCheckIcon case itemListPlusIcon + case itemListRoundPlusIcon case itemListDeleteIcon case itemListDeleteIndicatorIcon case itemListReorderIndicatorIcon diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift index 9d009cdc17..18a2b3d525 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift @@ -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) diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 0998610d15..78287c6337 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -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 } diff --git a/submodules/TelegramUI/Components/ItemListDatePickerItem/BUILD b/submodules/TelegramUI/Components/ItemListDatePickerItem/BUILD new file mode 100644 index 0000000000..4fd87b12e8 --- /dev/null +++ b/submodules/TelegramUI/Components/ItemListDatePickerItem/BUILD @@ -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", + ], +) diff --git a/submodules/InviteLinksUI/Sources/ItemListDatePickerItem.swift b/submodules/TelegramUI/Components/ItemListDatePickerItem/Sources/ItemListDatePickerItem.swift similarity index 100% rename from submodules/InviteLinksUI/Sources/ItemListDatePickerItem.swift rename to submodules/TelegramUI/Components/ItemListDatePickerItem/Sources/ItemListDatePickerItem.swift diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index c4dbc9366c..32ecd4de3c 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -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 } diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/BUILD b/submodules/TelegramUI/Components/ShareWithPeersScreen/BUILD index f53e1721b9..6660ba5098 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/BUILD +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/BUILD @@ -40,6 +40,7 @@ swift_library( "//submodules/TooltipUI", "//submodules/OverlayStatusController", "//submodules/UndoUI", + "//submodules/TemporaryCachedPeerDataManager", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index 44b7c6c9ad..2c8c20bcb0 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -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 = Set() - fileprivate let blockedPeersContext: BlockedPeersContext? - - private var stateDisposable: Disposable? - private let stateSubject = Promise() - public var state: Signal { - return self.stateSubject.get() - } - private let readySubject = ValuePromise(false, ignoreRepeated: true) - public var ready: Signal { - return self.readySubject.get() - } - - public init( - context: AccountContext, - subject: Subject = .chats(blocked: false), - editing: Bool, - initialSelectedPeers: [EngineStoryPrivacy.Base: [EnginePeer.Id]] = [:], - initialPeerIds: Set = 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]), NoError> in - return context.engine.data.subscribe( - EngineDataMap(peers.map(\.id).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init)) - ) - |> map { participantCountMap -> ([EnginePeer], [EnginePeer.Id: Optional]) 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] = [] - 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] = [] - 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] = [] - 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]), NoError> in - return context.engine.data.subscribe( - EngineDataMap(peers.map(\.id).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init)) - ) - |> map { participantCountMap -> ([EnginePeer], [EnginePeer.Id: Optional]) 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.Id: Optional], [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.Id: Optional], [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() - for peer in grayListPeers { - grayListPeersIds.insert(peer.id) - } - - var existingIds = Set() - 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.Id: Optional]), 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.Id: Optional]) 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 { - 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) -} diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift new file mode 100644 index 0000000000..61c18565fb --- /dev/null +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift @@ -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 = Set() + let blockedPeersContext: BlockedPeersContext? + + private var stateDisposable: Disposable? + private let stateSubject = Promise() + public var state: Signal { + return self.stateSubject.get() + } + private var listControl: PeerChannelMemberCategoryControl? + + private let readySubject = ValuePromise(false, ignoreRepeated: true) + public var ready: Signal { + return self.readySubject.get() + } + + public init( + context: AccountContext, + subject: Subject = .chats(blocked: false), + editing: Bool = false, + initialSelectedPeers: [EngineStoryPrivacy.Base: [EnginePeer.Id]] = [:], + initialPeerIds: Set = 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]), NoError> in + return context.engine.data.subscribe( + EngineDataMap(peers.map(\.id).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init)) + ) + |> map { participantCountMap -> ([EnginePeer], [EnginePeer.Id: Optional]) 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] = [] + 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] = [] + 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] = [] + 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]), NoError> in + return context.engine.data.subscribe( + EngineDataMap(peers.map(\.id).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init)) + ) + |> map { participantCountMap -> ([EnginePeer], [EnginePeer.Id: Optional]) 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.Id: Optional], [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.Id: Optional], [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() + for peer in grayListPeers { + grayListPeersIds.insert(peer.id) + } + + var existingIds = Set() + 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.Id: Optional]), 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.Id: Optional]) 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() + let contactsState = Promise() + + + + 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() + 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 { + 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) +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/AddRoundIcon.imageset/AddPlus.pdf b/submodules/TelegramUI/Images.xcassets/Chat List/AddRoundIcon.imageset/AddPlus.pdf new file mode 100644 index 0000000000..a6d720b959 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat List/AddRoundIcon.imageset/AddPlus.pdf @@ -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 \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/AddRoundIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat List/AddRoundIcon.imageset/Contents.json new file mode 100644 index 0000000000..6f73ecc7a7 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat List/AddRoundIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "AddPlus.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/Contents.json index 17f972537b..168869f39d 100644 --- a/submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "AvatarBoost.pdf", + "filename" : "Reassign.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/AvatarBoost.pdf b/submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/Reassign.pdf similarity index 52% rename from submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/AvatarBoost.pdf rename to submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/Reassign.pdf index ea8526a585..a1e5a5f0b4 100644 --- a/submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/AvatarBoost.pdf +++ b/submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/Reassign.pdf @@ -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 \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Giveaway.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Giveaway.imageset/Contents.json new file mode 100644 index 0000000000..68543cd872 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Giveaway.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "giveaway_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Giveaway.imageset/giveaway_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Giveaway.imageset/giveaway_30.pdf new file mode 100644 index 0000000000..a4930da9e8 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Giveaway.imageset/giveaway_30.pdf @@ -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 \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 210866b501..3c53d82926 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -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 { diff --git a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift index cfbd1db7e2..d2338430ee 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift @@ -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 } } } diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index dc42dcce44..4ec3409468 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -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) } } diff --git a/submodules/TelegramUI/Sources/ChatMessageGiftItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageGiftItemNode.swift index 9eb69af77c..e7242e7b08 100644 --- a/submodules/TelegramUI/Sources/ChatMessageGiftItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageGiftItemNode.swift @@ -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) diff --git a/submodules/TelegramUI/Sources/ChatMessageGiveawayBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageGiveawayBubbleContentNode.swift new file mode 100644 index 0000000000..666d752f30 --- /dev/null +++ b/submodules/TelegramUI/Sources/ChatMessageGiveawayBubbleContentNode.swift @@ -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) + } +} diff --git a/submodules/TelegramUI/Sources/ChatMessageItem.swift b/submodules/TelegramUI/Sources/ChatMessageItem.swift index 3290e4bebc..2ca5c503f9 100644 --- a/submodules/TelegramUI/Sources/ChatMessageItem.swift +++ b/submodules/TelegramUI/Sources/ChatMessageItem.swift @@ -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 } diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index ae11529790..1f85e9a22d 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -1068,6 +1068,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { break case .boost: break + case .premiumGiftCode: + break } } })) diff --git a/submodules/TelegramUI/Sources/ContactSelectionController.swift b/submodules/TelegramUI/Sources/ContactSelectionController.swift index d52f0cfc65..6acddae341 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionController.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionController.swift @@ -148,6 +148,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController deinit { self.createActionDisposable.dispose() self.presentationDataDisposable?.dispose() + self.confirmationDisposable.dispose() } @objc private func beginSearch() { diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 6401288d2b..63caf12882 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -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) + } + }) } } diff --git a/submodules/TelegramUI/Sources/ReplaceBoostConfirmationController.swift b/submodules/TelegramUI/Sources/ReplaceBoostConfirmationController.swift index a27ae1d227..660b0b90be 100644 --- a/submodules/TelegramUI/Sources/ReplaceBoostConfirmationController.swift +++ b/submodules/TelegramUI/Sources/ReplaceBoostConfirmationController.swift @@ -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 diff --git a/submodules/TelegramVoip/Package.swift b/submodules/TelegramVoip/Package.swift index 1eeb60ca4b..4799372938 100644 --- a/submodules/TelegramVoip/Package.swift +++ b/submodules/TelegramVoip/Package.swift @@ -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( diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index 50491a9ecb..606a5dc44f 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -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)) } } diff --git a/submodules/Utils/DarwinDirStat/Package.swift b/submodules/Utils/DarwinDirStat/Package.swift index 4011c1c65b..7a11c46465 100644 --- a/submodules/Utils/DarwinDirStat/Package.swift +++ b/submodules/Utils/DarwinDirStat/Package.swift @@ -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( diff --git a/submodules/Utils/RangeSet/Package.swift b/submodules/Utils/RangeSet/Package.swift index f8494e3608..6a9b31a7c7 100644 --- a/submodules/Utils/RangeSet/Package.swift +++ b/submodules/Utils/RangeSet/Package.swift @@ -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( diff --git a/submodules/YuvConversion/Package.swift b/submodules/YuvConversion/Package.swift index d3a43056cf..f6bb29070b 100644 --- a/submodules/YuvConversion/Package.swift +++ b/submodules/YuvConversion/Package.swift @@ -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( diff --git a/submodules/libphonenumber/Package.swift b/submodules/libphonenumber/Package.swift index 0d64d78ced..2d7b64648f 100644 --- a/submodules/libphonenumber/Package.swift +++ b/submodules/libphonenumber/Package.swift @@ -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( diff --git a/submodules/sqlcipher/Package.swift b/submodules/sqlcipher/Package.swift index b6c02557fb..0b760023aa 100644 --- a/submodules/sqlcipher/Package.swift +++ b/submodules/sqlcipher/Package.swift @@ -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( diff --git a/third-party/rnnoise/Package.swift b/third-party/rnnoise/Package.swift index 7d0fb525f0..6cd01cbfe8 100644 --- a/third-party/rnnoise/Package.swift +++ b/third-party/rnnoise/Package.swift @@ -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(