mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 03:20:48 +00:00
Various improvements
This commit is contained in:
parent
9c99c04e64
commit
58c532b51e
@ -8715,9 +8715,17 @@ Sorry for the inconvenience.";
|
||||
"AvatarEditor.Stickers" = "Stickers";
|
||||
"AvatarEditor.SwitchToEmoji" = "SWITCH TO EMOJI";
|
||||
"AvatarEditor.SwitchToStickers" = "SWITCH TO STICKERS";
|
||||
|
||||
"AvatarEditor.SetProfilePhoto" = "Set as Profile Photo";
|
||||
"AvatarEditor.SetGroupPhoto" = "Set as Group Photo";
|
||||
"AvatarEditor.SetChannelPhoto" = "Set as Group Photo";
|
||||
|
||||
"AvatarEditor.Set" = "Set";
|
||||
|
||||
"Premium.UpgradeDescription" = "Your current **Telegram Premium** plan can be upgraded at a **discount**.";
|
||||
"Premium.CurrentPlan" = "your current plan";
|
||||
"Premium.UpgradeFor" = "Upgrade for %@ / month";
|
||||
"Premium.UpgradeForAnnual" = "Upgrade for %@ / year";
|
||||
|
||||
"ChatList.PremiumAnnualDiscountTitle" = "Telegram Premium with a discount of %@";
|
||||
"ChatList.PremiumAnnualDiscountText" = "Sign up for the annual payment plan for Telegram Premium now to get the discount.";
|
||||
"ChatList.PremiumAnnualUpgradeTitle" = "Save on your subscription up to %@";
|
||||
"ChatList.PremiumAnnualUpgradeText" = "Upgrade to the annual payment plan for Telegram Premium to enjoy the discount.";
|
||||
|
@ -185,7 +185,7 @@ private final class ChatListShimmerNode: ASDisplayNode {
|
||||
let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
|
||||
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in
|
||||
gesture?.cancel()
|
||||
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {})
|
||||
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {})
|
||||
interaction.isInlineMode = isInlineMode
|
||||
|
||||
let items = (0 ..< 2).map { _ -> ChatListItem in
|
||||
|
@ -2124,6 +2124,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
})
|
||||
}, openStorageManagement: {
|
||||
}, openPasswordSetup: {
|
||||
}, openPremiumIntro: {
|
||||
})
|
||||
chatListInteraction.isSearchMode = true
|
||||
|
||||
@ -3325,7 +3326,7 @@ private final class ChatListSearchShimmerNode: ASDisplayNode {
|
||||
let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
|
||||
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in
|
||||
gesture?.cancel()
|
||||
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {})
|
||||
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {})
|
||||
var isInlineMode = false
|
||||
if case .topics = key {
|
||||
isInlineMode = false
|
||||
|
@ -1846,7 +1846,14 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
let messageString: NSAttributedString
|
||||
if !message.text.isEmpty && entities.count > 0 {
|
||||
messageString = stringWithAppliedEntities(trimToLineCount(message.text, lineCount: authorAttributedString == nil ? 2 : 1), entities: entities, baseColor: theme.messageTextColor, linkColor: theme.messageTextColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: italicTextFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message._asMessage())
|
||||
var messageText = message.text
|
||||
var entities = entities
|
||||
if let translation = message.attributes.first(where: { $0 is TranslationMessageAttribute }) as? TranslationMessageAttribute, !translation.text.isEmpty {
|
||||
messageText = translation.text
|
||||
entities = translation.entities
|
||||
}
|
||||
|
||||
messageString = stringWithAppliedEntities(trimToLineCount(messageText, lineCount: authorAttributedString == nil ? 2 : 1), entities: entities, baseColor: theme.messageTextColor, linkColor: theme.messageTextColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: italicTextFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message._asMessage())
|
||||
} else if spoilers != nil || customEmojiRanges != nil {
|
||||
let mutableString = NSMutableAttributedString(string: messageText, font: textFont, textColor: theme.messageTextColor)
|
||||
if let spoilers = spoilers {
|
||||
|
@ -94,6 +94,7 @@ public final class ChatListNodeInteraction {
|
||||
let openForumThread: (EnginePeer.Id, Int64) -> Void
|
||||
let openStorageManagement: () -> Void
|
||||
let openPasswordSetup: () -> Void
|
||||
let openPremiumIntro: () -> Void
|
||||
|
||||
public var searchTextHighightState: String?
|
||||
var highlightedChatLocation: ChatListHighlightedLocation?
|
||||
@ -137,7 +138,8 @@ public final class ChatListNodeInteraction {
|
||||
present: @escaping (ViewController) -> Void,
|
||||
openForumThread: @escaping (EnginePeer.Id, Int64) -> Void,
|
||||
openStorageManagement: @escaping () -> Void,
|
||||
openPasswordSetup: @escaping () -> Void
|
||||
openPasswordSetup: @escaping () -> Void,
|
||||
openPremiumIntro: @escaping () -> Void
|
||||
) {
|
||||
self.activateSearch = activateSearch
|
||||
self.peerSelected = peerSelected
|
||||
@ -169,6 +171,7 @@ public final class ChatListNodeInteraction {
|
||||
self.openForumThread = openForumThread
|
||||
self.openStorageManagement = openStorageManagement
|
||||
self.openPasswordSetup = openPasswordSetup
|
||||
self.openPremiumIntro = openPremiumIntro
|
||||
}
|
||||
}
|
||||
|
||||
@ -615,6 +618,8 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
nodeInteraction?.openStorageManagement()
|
||||
case .setupPassword:
|
||||
nodeInteraction?.openPasswordSetup()
|
||||
case .premiumUpgrade, .premiumAnnualDiscount:
|
||||
nodeInteraction?.openPremiumIntro()
|
||||
}
|
||||
}), directionHint: entry.directionHint)
|
||||
}
|
||||
@ -866,6 +871,8 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
nodeInteraction?.openStorageManagement()
|
||||
case .setupPassword:
|
||||
nodeInteraction?.openPasswordSetup()
|
||||
case .premiumUpgrade, .premiumAnnualDiscount:
|
||||
nodeInteraction?.openPremiumIntro()
|
||||
}
|
||||
}), directionHint: entry.directionHint)
|
||||
case .HeaderEntry:
|
||||
@ -1363,16 +1370,25 @@ public final class ChatListNode: ListView {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.6, execute: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
Queue.mainQueue().after(0.6) { [weak self] in
|
||||
if let self {
|
||||
let _ = dismissServerProvidedSuggestion(account: self.context.account, suggestion: .setupPassword).start()
|
||||
}
|
||||
let _ = dismissServerProvidedSuggestion(account: self.context.account, suggestion: .setupPassword).start()
|
||||
})
|
||||
|
||||
}
|
||||
let controller = self.context.sharedContext.makeSetupTwoFactorAuthController(context: self.context)
|
||||
self.push?(controller)
|
||||
}, openPremiumIntro: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
Queue.mainQueue().after(0.6) { [weak self] in
|
||||
if let self {
|
||||
let _ = dismissServerProvidedSuggestion(account: self.context.account, suggestion: .annualPremium).start()
|
||||
let _ = dismissServerProvidedSuggestion(account: self.context.account, suggestion: .upgradePremium).start()
|
||||
}
|
||||
}
|
||||
let controller = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .ads)
|
||||
self.push?(controller)
|
||||
})
|
||||
nodeInteraction.isInlineMode = isInlineMode
|
||||
|
||||
@ -1437,31 +1453,66 @@ public final class ChatListNode: ListView {
|
||||
} else {
|
||||
displayArchiveIntro = .single(false)
|
||||
}
|
||||
|
||||
let suggestPasswordSetup: Signal<Bool, NoError>
|
||||
|
||||
let suggestedChatListNotice: Signal<ChatListNotice?, NoError>
|
||||
if case .chatList(groupId: .root) = location, chatListFilter == nil {
|
||||
suggestPasswordSetup = .single(false) |> then(combineLatest(
|
||||
getServerProvidedSuggestions(account: context.account),
|
||||
context.engine.auth.twoStepVerificationConfiguration()
|
||||
)
|
||||
|> map { suggestions, configuration -> Bool in
|
||||
var notSet = false
|
||||
switch configuration {
|
||||
case let .notSet(pendingEmail):
|
||||
if pendingEmail == nil {
|
||||
notSet = true
|
||||
suggestedChatListNotice = .single(nil)
|
||||
|> then (
|
||||
combineLatest(
|
||||
getServerProvidedSuggestions(account: context.account),
|
||||
context.engine.auth.twoStepVerificationConfiguration()
|
||||
)
|
||||
|> mapToSignal { suggestions, configuration -> Signal<ChatListNotice?, NoError> in
|
||||
if suggestions.contains(.setupPassword) {
|
||||
var notSet = false
|
||||
switch configuration {
|
||||
case let .notSet(pendingEmail):
|
||||
if pendingEmail == nil {
|
||||
notSet = true
|
||||
}
|
||||
case .set:
|
||||
break
|
||||
}
|
||||
if notSet {
|
||||
return .single(.setupPassword)
|
||||
}
|
||||
}
|
||||
if suggestions.contains(.annualPremium) || suggestions.contains(.upgradePremium), let inAppPurchaseManager = context.inAppPurchaseManager {
|
||||
return inAppPurchaseManager.availableProducts
|
||||
|> map { products -> ChatListNotice? in
|
||||
if products.count > 1 {
|
||||
let shortestOptionPrice: (Int64, NSDecimalNumber)
|
||||
if let product = products.first(where: { $0.id.hasSuffix(".monthly") }) {
|
||||
shortestOptionPrice = (Int64(Float(product.priceCurrencyAndAmount.amount)), product.priceValue)
|
||||
} else {
|
||||
shortestOptionPrice = (1, NSDecimalNumber(decimal: 1))
|
||||
}
|
||||
for product in products {
|
||||
if product.id.hasSuffix(".annual") {
|
||||
let fraction = Float(product.priceCurrencyAndAmount.amount) / Float(12) / Float(shortestOptionPrice.0)
|
||||
let discount = Int32(round((1.0 - fraction) * 20.0) * 5.0)
|
||||
if suggestions.contains(.annualPremium) {
|
||||
return .premiumAnnualDiscount(discount: discount)
|
||||
} else if suggestions.contains(.upgradePremium) {
|
||||
return .premiumUpgrade(discount: discount)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
case .set:
|
||||
break
|
||||
}
|
||||
if !notSet {
|
||||
return false
|
||||
}
|
||||
return suggestions.contains(.setupPassword)
|
||||
})
|
||||
)
|
||||
|> distinctUntilChanged
|
||||
} else {
|
||||
suggestPasswordSetup = .single(false)
|
||||
suggestedChatListNotice = .single(nil)
|
||||
}
|
||||
|
||||
let storageInfo: Signal<Double?, NoError>
|
||||
@ -1553,13 +1604,31 @@ public final class ChatListNode: ListView {
|
||||
|
||||
let currentPeerId: EnginePeer.Id = context.account.peerId
|
||||
|
||||
let chatListNodeViewTransition = combineLatest(queue: viewProcessingQueue, hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestPasswordSetup, savedMessagesPeer, chatListViewUpdate, self.statePromise.get())
|
||||
|> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestPasswordSetup, savedMessagesPeer, updateAndFilter, state) -> Signal<ChatListNodeListViewTransition, NoError> in
|
||||
let chatListNodeViewTransition = combineLatest(
|
||||
queue: viewProcessingQueue,
|
||||
hideArchivedFolderByDefault,
|
||||
displayArchiveIntro,
|
||||
storageInfo,
|
||||
suggestedChatListNotice,
|
||||
savedMessagesPeer,
|
||||
chatListViewUpdate,
|
||||
self.statePromise.get()
|
||||
)
|
||||
|> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestedChatListNotice, savedMessagesPeer, updateAndFilter, state) -> Signal<ChatListNodeListViewTransition, NoError> in
|
||||
let (update, filter) = updateAndFilter
|
||||
|
||||
let previousHideArchivedFolderByDefaultValue = previousHideArchivedFolderByDefault.swap(hideArchivedFolderByDefault)
|
||||
|
||||
let (rawEntries, isLoading) = chatListNodeEntriesForView(update.list, state: state, savedMessagesPeer: savedMessagesPeer, foundPeers: state.foundPeers, hideArchivedFolderByDefault: hideArchivedFolderByDefault, displayArchiveIntro: displayArchiveIntro, storageInfo: storageInfo, suggestPasswordSetup: suggestPasswordSetup, mode: mode, chatListLocation: location)
|
||||
let notice: ChatListNotice?
|
||||
if let suggestedChatListNotice {
|
||||
notice = suggestedChatListNotice
|
||||
} else if let storageInfo {
|
||||
notice = .clearStorage(sizeFraction: storageInfo)
|
||||
} else {
|
||||
notice = nil
|
||||
}
|
||||
|
||||
let (rawEntries, isLoading) = chatListNodeEntriesForView(update.list, state: state, savedMessagesPeer: savedMessagesPeer, foundPeers: state.foundPeers, hideArchivedFolderByDefault: hideArchivedFolderByDefault, displayArchiveIntro: displayArchiveIntro, notice: notice, mode: mode, chatListLocation: location)
|
||||
var isEmpty = true
|
||||
var entries = rawEntries.filter { entry in
|
||||
switch entry {
|
||||
|
@ -49,6 +49,8 @@ public enum ChatListNodeEntryPromoInfo: Equatable {
|
||||
enum ChatListNotice: Equatable {
|
||||
case clearStorage(sizeFraction: Double)
|
||||
case setupPassword
|
||||
case premiumUpgrade(discount: Int32)
|
||||
case premiumAnnualDiscount(discount: Int32)
|
||||
}
|
||||
|
||||
enum ChatListNodeEntry: Comparable, Identifiable {
|
||||
@ -404,7 +406,7 @@ private func offsetPinnedIndex(_ index: EngineChatList.Item.Index, offset: UInt1
|
||||
}
|
||||
}
|
||||
|
||||
func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState, savedMessagesPeer: EnginePeer?, foundPeers: [(EnginePeer, EnginePeer?)], hideArchivedFolderByDefault: Bool, displayArchiveIntro: Bool, storageInfo: Double?, suggestPasswordSetup: Bool, mode: ChatListNodeMode, chatListLocation: ChatListControllerLocation) -> (entries: [ChatListNodeEntry], loading: Bool) {
|
||||
func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState, savedMessagesPeer: EnginePeer?, foundPeers: [(EnginePeer, EnginePeer?)], hideArchivedFolderByDefault: Bool, displayArchiveIntro: Bool, notice: ChatListNotice?, mode: ChatListNodeMode, chatListLocation: ChatListControllerLocation) -> (entries: [ChatListNodeEntry], loading: Bool) {
|
||||
var result: [ChatListNodeEntry] = []
|
||||
|
||||
var pinnedIndexOffset: UInt16 = 0
|
||||
@ -666,10 +668,9 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState
|
||||
if displayArchiveIntro {
|
||||
result.append(.ArchiveIntro(presentationData: state.presentationData))
|
||||
}
|
||||
if suggestPasswordSetup {
|
||||
result.append(.Notice(presentationData: state.presentationData, notice: .setupPassword))
|
||||
} else if let storageInfo {
|
||||
result.append(.Notice(presentationData: state.presentationData, notice: .clearStorage(sizeFraction: storageInfo)))
|
||||
|
||||
if let notice {
|
||||
result.append(.Notice(presentationData: state.presentationData, notice: notice))
|
||||
}
|
||||
|
||||
result.append(.HeaderEntry)
|
||||
|
@ -141,6 +141,26 @@ class ChatListStorageInfoItemNode: ListViewItemNode {
|
||||
//TODO:localize
|
||||
titleString = NSAttributedString(string: "Protect Your Account", font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor)
|
||||
textString = NSAttributedString(string: "Set a password that will be required each time you log in with this phone number.", font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
|
||||
case let .premiumUpgrade(discount):
|
||||
let discountString = "\(discount)%"
|
||||
let rawTitleString = item.strings.ChatList_PremiumAnnualUpgradeTitle(discountString)
|
||||
let titleStringValue = NSMutableAttributedString(attributedString: NSAttributedString(string: rawTitleString.string, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor))
|
||||
if let range = rawTitleString.ranges.first {
|
||||
titleStringValue.addAttribute(.foregroundColor, value: item.theme.rootController.navigationBar.accentTextColor, range: range.range)
|
||||
}
|
||||
titleString = titleStringValue
|
||||
|
||||
textString = NSAttributedString(string: item.strings.ChatList_PremiumAnnualUpgradeText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
|
||||
case let .premiumAnnualDiscount(discount):
|
||||
let discountString = "\(discount)%"
|
||||
let rawTitleString = item.strings.ChatList_PremiumAnnualDiscountTitle(discountString)
|
||||
let titleStringValue = NSMutableAttributedString(attributedString: NSAttributedString(string: rawTitleString.string, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor))
|
||||
if let range = rawTitleString.ranges.first {
|
||||
titleStringValue.addAttribute(.foregroundColor, value: item.theme.rootController.navigationBar.accentTextColor, range: range.range)
|
||||
}
|
||||
titleString = titleStringValue
|
||||
|
||||
textString = NSAttributedString(string: item.strings.ChatList_PremiumAnnualDiscountText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
|
||||
}
|
||||
|
||||
let titleLayout = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0)))
|
||||
|
@ -21,21 +21,12 @@ public func reactionStaticImage(context: AccountContext, animation: TelegramMedi
|
||||
return Signal { subscriber in
|
||||
let fetchDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .image, reference: MediaResourceReference.standalone(resource: animation.resource)).start()
|
||||
|
||||
let type: AnimationCacheAnimationType
|
||||
if animation.isVideoSticker || animation.isVideoEmoji {
|
||||
type = .video
|
||||
} else if animation.isAnimatedSticker {
|
||||
type = .lottie
|
||||
} else {
|
||||
type = .still
|
||||
}
|
||||
|
||||
var customColor: UIColor?
|
||||
if animation.isCustomTemplateEmoji {
|
||||
customColor = nil
|
||||
}
|
||||
|
||||
let fetchFrame = animationCacheFetchFile(context: context, userLocation: .other, userContentType: .sticker, resource: MediaResourceReference.standalone(resource: animation.resource), type: type, keyframeOnly: true, customColor: customColor)
|
||||
let fetchFrame = animationCacheFetchFile(context: context, userLocation: .other, userContentType: .sticker, resource: MediaResourceReference.standalone(resource: animation.resource), type: AnimationCacheAnimationType(file: animation), keyframeOnly: true, customColor: customColor)
|
||||
|
||||
class AnimationCacheItemWriterImpl: AnimationCacheItemWriter {
|
||||
let queue: Queue
|
||||
|
@ -281,7 +281,7 @@ class StickerPickerScreen: ViewController {
|
||||
return
|
||||
}
|
||||
let context = controller.context
|
||||
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks)
|
||||
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks)
|
||||
let _ = (context.account.postbox.combinedView(keys: [viewKey])
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { views in
|
||||
|
@ -93,6 +93,7 @@ public final class HashtagSearchController: TelegramBaseController {
|
||||
}, openForumThread: { _, _ in
|
||||
}, openStorageManagement: {
|
||||
}, openPasswordSetup: {
|
||||
}, openPremiumIntro: {
|
||||
})
|
||||
|
||||
let previousSearchItems = Atomic<[ChatListSearchEntry]?>(value: nil)
|
||||
|
@ -154,10 +154,12 @@ public final class InAppPurchaseManager: NSObject {
|
||||
|
||||
private final class PaymentTransactionContext {
|
||||
var state: SKPaymentTransactionState?
|
||||
var isUpgrade: Bool
|
||||
var targetPeerId: PeerId?
|
||||
let subscriber: (TransactionState) -> Void
|
||||
|
||||
init(targetPeerId: PeerId?, subscriber: @escaping (TransactionState) -> Void) {
|
||||
init(isUpgrade: Bool, targetPeerId: PeerId?, subscriber: @escaping (TransactionState) -> Void) {
|
||||
self.isUpgrade = isUpgrade
|
||||
self.targetPeerId = targetPeerId
|
||||
self.subscriber = subscriber
|
||||
}
|
||||
@ -236,7 +238,7 @@ public final class InAppPurchaseManager: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
public func buyProduct(_ product: Product, targetPeerId: PeerId? = nil) -> Signal<PurchaseState, PurchaseError> {
|
||||
public func buyProduct(_ product: Product, isUpgrade: Bool = false, targetPeerId: PeerId? = nil) -> Signal<PurchaseState, PurchaseError> {
|
||||
if !self.canMakePayments {
|
||||
return .fail(.cantMakePayments)
|
||||
}
|
||||
@ -258,7 +260,7 @@ public final class InAppPurchaseManager: NSObject {
|
||||
let disposable = MetaDisposable()
|
||||
|
||||
self.stateQueue.async {
|
||||
let paymentContext = PaymentTransactionContext(targetPeerId: targetPeerId, subscriber: { state in
|
||||
let paymentContext = PaymentTransactionContext(isUpgrade: isUpgrade, targetPeerId: targetPeerId, subscriber: { state in
|
||||
switch state {
|
||||
case let .purchased(transactionId), let .restored(transactionId):
|
||||
if let transactionId = transactionId {
|
||||
@ -305,6 +307,13 @@ public final class InAppPurchaseManager: NSObject {
|
||||
}
|
||||
return signal
|
||||
}
|
||||
|
||||
public func getValidTransactionIds() -> [String] {
|
||||
guard let data = getReceiptData(), let receipt = parseReceipt(data) else {
|
||||
return []
|
||||
}
|
||||
return receipt.purchases.map { $0.transactionId }
|
||||
}
|
||||
}
|
||||
|
||||
extension InAppPurchaseManager: SKProductsRequestDelegate {
|
||||
@ -370,6 +379,7 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver {
|
||||
productId: transaction.payment.productIdentifier,
|
||||
content: PendingInAppPurchaseState(
|
||||
productId: transaction.payment.productIdentifier,
|
||||
isUpgrade: paymentContext.isUpgrade,
|
||||
targetPeerId: paymentContext.targetPeerId
|
||||
)
|
||||
).start()
|
||||
@ -431,7 +441,23 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
purpose = .single(.subscription)
|
||||
let isUpgrade: Signal<Bool, NoError>
|
||||
if let isUpgradeValue = paymentContexts[productIdentifier]?.isUpgrade {
|
||||
isUpgrade = .single(isUpgradeValue)
|
||||
} else {
|
||||
isUpgrade = pendingInAppPurchaseState(engine: self.engine, productId: productIdentifier)
|
||||
|> mapToSignal { state -> Signal<Bool, NoError> in
|
||||
if let state = state {
|
||||
return .single(state.isUpgrade)
|
||||
} else {
|
||||
return .single(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
purpose = isUpgrade
|
||||
|> map { isUpgrade in
|
||||
return isUpgrade ? .upgrade : .subscription
|
||||
}
|
||||
}
|
||||
|
||||
let receiptData = getReceiptData() ?? Data()
|
||||
@ -508,10 +534,12 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver {
|
||||
|
||||
private final class PendingInAppPurchaseState: Codable {
|
||||
public let productId: String
|
||||
public let isUpgrade: Bool
|
||||
public let targetPeerId: PeerId?
|
||||
|
||||
public init(productId: String, targetPeerId: PeerId?) {
|
||||
public init(productId: String, isUpgrade: Bool, targetPeerId: PeerId?) {
|
||||
self.productId = productId
|
||||
self.isUpgrade = isUpgrade
|
||||
self.targetPeerId = targetPeerId
|
||||
}
|
||||
|
||||
@ -519,6 +547,7 @@ private final class PendingInAppPurchaseState: Codable {
|
||||
let container = try decoder.container(keyedBy: StringCodingKey.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 { PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) }
|
||||
}
|
||||
|
||||
@ -526,6 +555,7 @@ private final class PendingInAppPurchaseState: Codable {
|
||||
var container = encoder.container(keyedBy: StringCodingKey.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")
|
||||
}
|
||||
|
230
submodules/InAppPurchaseManager/Sources/Receipt.swift
Normal file
230
submodules/InAppPurchaseManager/Sources/Receipt.swift
Normal file
@ -0,0 +1,230 @@
|
||||
import Foundation
|
||||
|
||||
private struct Asn1Tag {
|
||||
static let integer: Int32 = 0x02
|
||||
static let octetString: Int32 = 0x04
|
||||
static let objectIdentifier: Int32 = 0x06
|
||||
static let sequence: Int32 = 0x10
|
||||
static let set: Int32 = 0x11
|
||||
static let utf8String: Int32 = 0x0c
|
||||
}
|
||||
|
||||
private struct Asn1Entry {
|
||||
let tag: Int32
|
||||
let data: Data
|
||||
let length: Int
|
||||
}
|
||||
|
||||
private func parse(_ data: Data, startIndex: Int = 0) -> Asn1Entry {
|
||||
var index = startIndex
|
||||
var value = data[index]
|
||||
index += 1
|
||||
var tagValue = Int32(value & 0x1f)
|
||||
if tagValue == 31 {
|
||||
value = data[index]
|
||||
index += 1
|
||||
while (value & 0x80) != 0 {
|
||||
tagValue <<= 8
|
||||
tagValue |= Int32(value & 0x7f)
|
||||
value = data[index]
|
||||
index += 1
|
||||
}
|
||||
tagValue <<= 8
|
||||
tagValue |= Int32(value & 0x7f)
|
||||
}
|
||||
|
||||
var length = 0
|
||||
var nextTag = 0
|
||||
value = data[index]
|
||||
index += 1
|
||||
if value & 0x80 == 0 {
|
||||
length = Int(value)
|
||||
nextTag = index + length
|
||||
} else if value != 0x80 {
|
||||
let octetsCount = Int(value & 0x7f)
|
||||
for _ in 0 ..< octetsCount {
|
||||
length <<= 8
|
||||
value = data[index]
|
||||
index += 1
|
||||
length |= Int(value) & 0xff
|
||||
}
|
||||
nextTag = index + length
|
||||
} else {
|
||||
var scanIndex = index
|
||||
while data[scanIndex] != 0 && data[scanIndex + 1] != 0 {
|
||||
scanIndex += 1
|
||||
}
|
||||
length = scanIndex - index
|
||||
nextTag = scanIndex + 2
|
||||
}
|
||||
return Asn1Entry(tag: tagValue, data: data.subdata(in: index ..< (index + length)), length: nextTag - startIndex)
|
||||
}
|
||||
|
||||
private func parseSequence(_ data: Data) -> [Asn1Entry] {
|
||||
var result : [Asn1Entry] = []
|
||||
var index = 0
|
||||
while index < data.count {
|
||||
let entry = parse(data, startIndex: index)
|
||||
result.append(entry)
|
||||
index += entry.length
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private func parseInteger(_ data: Data) -> Int32 {
|
||||
let length = data.count
|
||||
var value: Int32 = 0
|
||||
for i in 0 ..< length {
|
||||
if i == 0 {
|
||||
value = Int32(data[i] & 0x7f)
|
||||
} else {
|
||||
value <<= 8
|
||||
value |= Int32(data[i])
|
||||
}
|
||||
}
|
||||
if length > 0 && data[0] & 0x80 != 0 {
|
||||
let complement: Int32 = 1 << (length * 8)
|
||||
value -= complement
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
private func parseObjectIdentifier(_ data: Data, startIndex: Int = 0, length: Int? = nil) -> [Int32] {
|
||||
let dataLen = length ?? data.count
|
||||
var index = startIndex
|
||||
var identifier: [Int32] = []
|
||||
while index < startIndex + dataLen {
|
||||
var subidentifier: Int32 = 0
|
||||
var value = data[index]
|
||||
index += 1
|
||||
while (value & 0x80) != 0 {
|
||||
subidentifier <<= 7
|
||||
subidentifier |= Int32(value & 0x7f)
|
||||
value = data[index]
|
||||
index += 1
|
||||
}
|
||||
subidentifier <<= 7
|
||||
subidentifier |= Int32(value & 0x7f)
|
||||
identifier.append(subidentifier)
|
||||
}
|
||||
return identifier
|
||||
}
|
||||
|
||||
private struct ObjectIdentifier {
|
||||
static let pkcs7Data: [Int32] = [42, 840, 113549, 1, 7, 1]
|
||||
static let pkcs7SignedData: [Int32] = [42, 840, 113549, 1, 7, 2]
|
||||
}
|
||||
|
||||
struct Receipt {
|
||||
fileprivate struct Tag {
|
||||
static let purchases: Int32 = 17
|
||||
}
|
||||
|
||||
struct Purchase {
|
||||
fileprivate struct Tag {
|
||||
static let productIdentifier: Int32 = 1702
|
||||
static let transactionIdentifier: Int32 = 1703
|
||||
}
|
||||
|
||||
let productId: String
|
||||
let transactionId: String
|
||||
}
|
||||
|
||||
let purchases: [Purchase]
|
||||
}
|
||||
|
||||
func parseReceipt(_ data: Data) -> Receipt? {
|
||||
let root = parseSequence(data)
|
||||
guard root.count == 1 && root[0].tag == Asn1Tag.sequence else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let rootSeq = parseSequence(root[0].data)
|
||||
guard rootSeq.count == 2 && rootSeq[0].tag == Asn1Tag.objectIdentifier && parseObjectIdentifier(rootSeq[0].data) == ObjectIdentifier.pkcs7SignedData else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let signedData = parseSequence(rootSeq[1].data)
|
||||
guard signedData.count == 1 && signedData[0].tag == Asn1Tag.sequence else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let signedDataSeq = parseSequence(signedData[0].data)
|
||||
guard signedDataSeq.count > 3 && signedDataSeq[2].tag == Asn1Tag.sequence else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let contentData = parseSequence(signedDataSeq[2].data)
|
||||
guard contentData.count == 2 && contentData[0].tag == Asn1Tag.objectIdentifier && parseObjectIdentifier(contentData[0].data) == ObjectIdentifier.pkcs7Data else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let payload = parse(contentData[1].data)
|
||||
guard payload.tag == Asn1Tag.octetString else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let payloadRoot = parse(payload.data)
|
||||
guard payloadRoot.tag == Asn1Tag.set else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var purchases: [Receipt.Purchase] = []
|
||||
|
||||
let receiptAttributes = parseSequence(payloadRoot.data)
|
||||
for attribute in receiptAttributes {
|
||||
if attribute.tag != Asn1Tag.sequence { continue }
|
||||
let attributeEntries = parseSequence(attribute.data)
|
||||
guard attributeEntries.count == 3 && attributeEntries[0].tag == Asn1Tag.integer && attributeEntries[1].tag == Asn1Tag.integer && attributeEntries[2].tag == Asn1Tag.octetString else { return nil
|
||||
}
|
||||
|
||||
let type = parseInteger(attributeEntries[0].data)
|
||||
let value = attributeEntries[2].data
|
||||
switch (type) {
|
||||
case Receipt.Tag.purchases:
|
||||
if let purchase = parsePurchaseAttributes(value) {
|
||||
purchases.append(purchase)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return Receipt(purchases: purchases)
|
||||
}
|
||||
|
||||
private func parsePurchaseAttributes(_ data: Data) -> Receipt.Purchase? {
|
||||
let root = parse(data)
|
||||
guard root.tag == Asn1Tag.set else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var productId: String?
|
||||
var transactionId: String?
|
||||
|
||||
let receiptAttributes = parseSequence(root.data)
|
||||
for attribute in receiptAttributes {
|
||||
if attribute.tag != Asn1Tag.sequence { continue }
|
||||
let attributeEntries = parseSequence(attribute.data)
|
||||
guard attributeEntries.count == 3 && attributeEntries[0].tag == Asn1Tag.integer && attributeEntries[1].tag == Asn1Tag.integer && attributeEntries[2].tag == Asn1Tag.octetString else { return nil
|
||||
}
|
||||
|
||||
let type = parseInteger(attributeEntries[0].data)
|
||||
let value = attributeEntries[2].data
|
||||
switch (type) {
|
||||
case Receipt.Purchase.Tag.productIdentifier:
|
||||
let valEntry = parse(value)
|
||||
guard valEntry.tag == Asn1Tag.utf8String else { return nil }
|
||||
productId = String(bytes: valEntry.data, encoding: .utf8)
|
||||
case Receipt.Purchase.Tag.transactionIdentifier:
|
||||
let valEntry = parse(value)
|
||||
guard valEntry.tag == Asn1Tag.utf8String else { return nil }
|
||||
transactionId = String(bytes: valEntry.data, encoding: .utf8)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
guard let productId, let transactionId else {
|
||||
return nil
|
||||
}
|
||||
return Receipt.Purchase(productId: productId, transactionId: transactionId)
|
||||
}
|
@ -26,7 +26,7 @@ typedef void (^TGMediaAvatarPresentImpl)(id<LegacyComponentsContext>, void (^)(U
|
||||
@property (nonatomic, copy) void (^requestSearchController)(TGMediaAssetsController *);
|
||||
@property (nonatomic, copy) CGRect (^sourceRect)(void);
|
||||
|
||||
@property (nonatomic, copy) void (^requestAvatarEditor)(void (^)(UIImage *image, NSURL *asset, TGVideoEditAdjustments *adjustments, void(^commit)(void)));
|
||||
@property (nonatomic, copy) void (^requestAvatarEditor)(void (^)(UIImage *image, void(^commit)(void)), void (^)(UIImage *image, NSURL *asset, TGVideoEditAdjustments *adjustments, void(^commit)(void)));
|
||||
|
||||
@property (nonatomic, strong) id<TGPhotoPaintStickersContext> stickersContext;
|
||||
|
||||
|
@ -195,7 +195,7 @@
|
||||
|
||||
if (!_signup) {
|
||||
TGMenuSheetButtonItemView *viewItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"ProfilePhoto.SetEmoji") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^
|
||||
{
|
||||
{
|
||||
__strong TGMediaAvatarMenuMixin *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
return;
|
||||
@ -205,8 +205,26 @@
|
||||
return;
|
||||
|
||||
[strongController dismissAnimated:true];
|
||||
if (strongSelf != nil)
|
||||
strongSelf.requestAvatarEditor(^(UIImage *image, NSURL *asset, TGVideoEditAdjustments *adjustments, void (^commit)(void)) {
|
||||
if (strongSelf != nil) {
|
||||
strongSelf.requestAvatarEditor(^(UIImage *image, void (^commit)(void)) {
|
||||
__strong TGMediaAvatarMenuMixin *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
return;
|
||||
|
||||
if (strongSelf.willFinishWithImage != nil) {
|
||||
strongSelf.willFinishWithImage(image, ^{
|
||||
if (strongSelf.didFinishWithImage != nil)
|
||||
strongSelf.didFinishWithImage(image);
|
||||
|
||||
commit();
|
||||
});
|
||||
} else {
|
||||
if (strongSelf.didFinishWithImage != nil)
|
||||
strongSelf.didFinishWithImage(image);
|
||||
|
||||
commit();
|
||||
}
|
||||
}, ^(UIImage *image, NSURL *asset, TGVideoEditAdjustments *adjustments, void (^commit)(void)) {
|
||||
__strong TGMediaAvatarMenuMixin *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
return;
|
||||
@ -225,6 +243,7 @@
|
||||
commit();
|
||||
}
|
||||
});
|
||||
}
|
||||
}];
|
||||
[itemViews addObject:viewItem];
|
||||
}
|
||||
|
@ -49,6 +49,7 @@ public final class ListMessageItem: ListViewItem {
|
||||
let chatLocation: ChatLocation
|
||||
let interaction: ListMessageItemInteraction
|
||||
let message: Message?
|
||||
let translateToLanguage: String?
|
||||
public let selection: ChatHistoryMessageSelection
|
||||
let hintIsLink: Bool
|
||||
let isGlobalSearchResult: Bool
|
||||
@ -61,12 +62,13 @@ public final class ListMessageItem: ListViewItem {
|
||||
|
||||
public let selectable: Bool = true
|
||||
|
||||
public init(presentationData: ChatPresentationData, context: AccountContext, chatLocation: ChatLocation, interaction: ListMessageItemInteraction, message: Message?, selection: ChatHistoryMessageSelection, displayHeader: Bool, customHeader: ListViewItemHeader? = nil, hintIsLink: Bool = false, isGlobalSearchResult: Bool = false, isDownloadList: Bool = false, displayFileInfo: Bool = true, displayBackground: Bool = false, style: ItemListStyle = .plain) {
|
||||
public init(presentationData: ChatPresentationData, context: AccountContext, chatLocation: ChatLocation, interaction: ListMessageItemInteraction, message: Message?, translateToLanguage: String? = nil, selection: ChatHistoryMessageSelection, displayHeader: Bool, customHeader: ListViewItemHeader? = nil, hintIsLink: Bool = false, isGlobalSearchResult: Bool = false, isDownloadList: Bool = false, displayFileInfo: Bool = true, displayBackground: Bool = false, style: ItemListStyle = .plain) {
|
||||
self.presentationData = presentationData
|
||||
self.context = context
|
||||
self.chatLocation = chatLocation
|
||||
self.interaction = interaction
|
||||
self.message = message
|
||||
self.translateToLanguage = translateToLanguage
|
||||
if let header = customHeader {
|
||||
self.header = header
|
||||
} else if displayHeader, let message = message {
|
||||
|
@ -424,7 +424,12 @@ public final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
let (messageTextUrl, _) = parseUrl(url: message.text, wasConcealed: false)
|
||||
|
||||
if messageTextUrl != rawUrlString, !item.isGlobalSearchResult {
|
||||
mutableDescriptionText.append(NSAttributedString(string: message.text + "\n", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor))
|
||||
var messageText = message.text
|
||||
if let translation = message.attributes.first(where: { $0 is TranslationMessageAttribute }) as? TranslationMessageAttribute, !translation.text.isEmpty, item.translateToLanguage == translation.toLang {
|
||||
messageText = translation.text
|
||||
}
|
||||
|
||||
mutableDescriptionText.append(NSAttributedString(string: messageText + "\n", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor))
|
||||
}
|
||||
|
||||
let urlAttributedString = NSMutableAttributedString()
|
||||
@ -439,8 +444,17 @@ public final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
}
|
||||
break loop
|
||||
case let .TextUrl(url):
|
||||
var messageText = message.text
|
||||
var entity = entity
|
||||
if let translation = message.attributes.first(where: { $0 is TranslationMessageAttribute }) as? TranslationMessageAttribute, !translation.text.isEmpty, item.translateToLanguage == translation.toLang {
|
||||
messageText = translation.text
|
||||
if entities.count == translation.entities.count, let index = entities.firstIndex(of: entity), index < translation.entities.count {
|
||||
entity = translation.entities[index]
|
||||
}
|
||||
}
|
||||
|
||||
var range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
|
||||
let nsString = message.text as NSString
|
||||
let nsString = messageText as NSString
|
||||
if range.location + range.length > nsString.length {
|
||||
range.location = max(0, nsString.length - range.length)
|
||||
range.length = nsString.length - range.location
|
||||
@ -470,7 +484,7 @@ public final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
let (messageTextUrl, _) = parseUrl(url: message.text, wasConcealed: false)
|
||||
|
||||
if messageTextUrl != rawUrlString, !item.isGlobalSearchResult {
|
||||
mutableDescriptionText.append(NSAttributedString(string: message.text + "\n", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor))
|
||||
mutableDescriptionText.append(NSAttributedString(string: messageText + "\n", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor))
|
||||
}
|
||||
|
||||
let urlAttributedString = NSMutableAttributedString()
|
||||
|
@ -484,6 +484,14 @@ private struct PremiumProduct: Equatable {
|
||||
var pricePerMonth: String {
|
||||
return self.storeProduct.pricePerMonth(Int(self.months))
|
||||
}
|
||||
|
||||
var isCurrent: Bool {
|
||||
return self.option.isCurrent
|
||||
}
|
||||
|
||||
var transactionId: String? {
|
||||
return self.option.transactionId
|
||||
}
|
||||
}
|
||||
|
||||
final class PremiumOptionComponent: CombinedComponent {
|
||||
@ -815,10 +823,12 @@ private final class CheckComponent: Component {
|
||||
final class SectionGroupComponent: Component {
|
||||
public final class Item: Equatable {
|
||||
public let content: AnyComponentWithIdentity<Empty>
|
||||
public let isEnabled: Bool
|
||||
public let action: () -> Void
|
||||
|
||||
public init(_ content: AnyComponentWithIdentity<Empty>, action: @escaping () -> Void) {
|
||||
public init(_ content: AnyComponentWithIdentity<Empty>, isEnabled: Bool = true, action: @escaping () -> Void) {
|
||||
self.content = content
|
||||
self.isEnabled = isEnabled
|
||||
self.action = action
|
||||
}
|
||||
|
||||
@ -826,7 +836,9 @@ final class SectionGroupComponent: Component {
|
||||
if lhs.content != rhs.content {
|
||||
return false
|
||||
}
|
||||
|
||||
if lhs.isEnabled != rhs.isEnabled {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -931,6 +943,8 @@ final class SectionGroupComponent: Component {
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width - sideInset, height: .greatestFiniteMagnitude)
|
||||
)
|
||||
buttonView.isEnabled = item.isEnabled
|
||||
itemView.alpha = item.isEnabled ? 1.0 : 0.3
|
||||
|
||||
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height), size: itemSize)
|
||||
buttonView.frame = CGRect(origin: itemFrame.origin, size: CGSize(width: availableSize.width, height: itemSize.height + UIScreenPixel))
|
||||
@ -1159,22 +1173,26 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
let context: AccountContext
|
||||
let source: PremiumSource
|
||||
let isPremium: Bool?
|
||||
let justBought: Bool
|
||||
let otherPeerName: String?
|
||||
let products: [PremiumProduct]?
|
||||
let selectedProductId: String?
|
||||
let validTransactionIds: [String]
|
||||
let promoConfiguration: PremiumPromoConfiguration?
|
||||
let present: (ViewController) -> Void
|
||||
let selectProduct: (String) -> Void
|
||||
let buy: () -> Void
|
||||
let updateIsFocused: (Bool) -> Void
|
||||
|
||||
init(context: AccountContext, source: PremiumSource, isPremium: Bool?, otherPeerName: String?, products: [PremiumProduct]?, selectedProductId: String?, promoConfiguration: PremiumPromoConfiguration?, present: @escaping (ViewController) -> Void, selectProduct: @escaping (String) -> Void, buy: @escaping () -> Void, updateIsFocused: @escaping (Bool) -> Void) {
|
||||
init(context: AccountContext, source: PremiumSource, isPremium: Bool?, justBought: Bool, otherPeerName: String?, products: [PremiumProduct]?, selectedProductId: String?, validTransactionIds: [String], promoConfiguration: PremiumPromoConfiguration?, present: @escaping (ViewController) -> Void, selectProduct: @escaping (String) -> Void, buy: @escaping () -> Void, updateIsFocused: @escaping (Bool) -> Void) {
|
||||
self.context = context
|
||||
self.source = source
|
||||
self.isPremium = isPremium
|
||||
self.justBought = justBought
|
||||
self.otherPeerName = otherPeerName
|
||||
self.products = products
|
||||
self.selectedProductId = selectedProductId
|
||||
self.validTransactionIds = validTransactionIds
|
||||
self.promoConfiguration = promoConfiguration
|
||||
self.present = present
|
||||
self.selectProduct = selectProduct
|
||||
@ -1192,6 +1210,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
if lhs.isPremium != rhs.isPremium {
|
||||
return false
|
||||
}
|
||||
if lhs.justBought != rhs.justBought {
|
||||
return false
|
||||
}
|
||||
if lhs.otherPeerName != rhs.otherPeerName {
|
||||
return false
|
||||
}
|
||||
@ -1201,6 +1222,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
if lhs.selectedProductId != rhs.selectedProductId {
|
||||
return false
|
||||
}
|
||||
if lhs.validTransactionIds != rhs.validTransactionIds {
|
||||
return false
|
||||
}
|
||||
if lhs.promoConfiguration != rhs.promoConfiguration {
|
||||
return false
|
||||
}
|
||||
@ -1213,6 +1237,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
|
||||
var products: [PremiumProduct]?
|
||||
var selectedProductId: String?
|
||||
var validTransactionIds: [String] = []
|
||||
|
||||
var isPremium: Bool?
|
||||
|
||||
@ -1230,6 +1255,18 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
return self.products?.first(where: { $0.id == self.selectedProductId })?.id.hasSuffix(".annual") ?? false
|
||||
}
|
||||
|
||||
var canUpgrade: Bool {
|
||||
if let products = self.products, let current = products.first(where: { $0.isCurrent }), let transactionId = current.transactionId {
|
||||
if self.validTransactionIds.contains(transactionId) {
|
||||
return products.first(where: { $0.months > current.months }) != nil
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
init(context: AccountContext, source: PremiumSource) {
|
||||
self.context = context
|
||||
|
||||
@ -1317,6 +1354,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
let state = context.state
|
||||
state.products = context.component.products
|
||||
state.selectedProductId = context.component.selectedProductId
|
||||
state.validTransactionIds = context.component.validTransactionIds
|
||||
state.isPremium = context.component.isPremium
|
||||
|
||||
let theme = environment.theme
|
||||
@ -1378,7 +1416,11 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
textString = strings.Premium_PersonalDescription
|
||||
}
|
||||
} else if context.component.isPremium == true {
|
||||
textString = strings.Premium_SubscribedDescription
|
||||
if !context.component.justBought, let products = state.products, let current = products.first(where: { $0.isCurrent }), current.months == 1 {
|
||||
textString = strings.Premium_UpgradeDescription
|
||||
} else {
|
||||
textString = strings.Premium_SubscribedDescription
|
||||
}
|
||||
} else {
|
||||
textString = strings.Premium_Description
|
||||
}
|
||||
@ -1398,11 +1440,22 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
lineSpacing: 0.2
|
||||
),
|
||||
environment: {},
|
||||
availableSize: CGSize(width: availableWidth - sideInsets, height: 240.0),
|
||||
transition: context.transition
|
||||
availableSize: CGSize(width: availableWidth - sideInsets - 8.0, height: 240.0),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(text
|
||||
.position(CGPoint(x: size.width / 2.0, y: size.height + text.size.height / 2.0))
|
||||
// .update(Transition.Update { _, view, _ in
|
||||
// if let snapshot = view.snapshotView(afterScreenUpdates: false) {
|
||||
// let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
|
||||
// view.superview?.addSubview(snapshot)
|
||||
// transition.setAlpha(view: snapshot, alpha: 0.0, completion: { [weak snapshot] _ in
|
||||
// snapshot?.removeFromSuperview()
|
||||
// })
|
||||
// snapshot.frame = view.frame
|
||||
// transition.animateAlpha(view: view, from: 0.0, to: 1.0)
|
||||
// }
|
||||
// })
|
||||
)
|
||||
size.height += text.size.height
|
||||
size.height += 21.0
|
||||
@ -1430,9 +1483,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
let updateIsFocused = context.component.updateIsFocused
|
||||
|
||||
let layoutOptions = {
|
||||
if state.isPremium == true {
|
||||
|
||||
} else if let products = state.products, products.count > 1 {
|
||||
if let products = state.products, products.count > 1, state.isPremium == false || (!context.component.justBought && state.canUpgrade) {
|
||||
var optionsItems: [SectionGroupComponent.Item] = []
|
||||
let gradientColors: [UIColor] = [
|
||||
UIColor(rgb: 0x8e77ff),
|
||||
@ -1447,11 +1498,11 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
shortestOptionPrice = (1, NSDecimalNumber(decimal: 1))
|
||||
}
|
||||
|
||||
let currentProductMonths = state.products?.first(where: { $0.isCurrent })?.months ?? 0
|
||||
|
||||
var i = 0
|
||||
for product in products {
|
||||
let giftTitle: String
|
||||
let months = product.months
|
||||
|
||||
if product.id.hasSuffix(".monthly") {
|
||||
giftTitle = strings.Premium_Monthly
|
||||
} else if product.id.hasSuffix(".semiannual") {
|
||||
@ -1459,8 +1510,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
} else {
|
||||
giftTitle = strings.Premium_Annual
|
||||
}
|
||||
|
||||
let discountValue = Int((1.0 - Float(product.storeProduct.priceCurrencyAndAmount.amount) / Float(months) / Float(shortestOptionPrice.0)) * 100.0)
|
||||
|
||||
let fraction = Float(product.storeProduct.priceCurrencyAndAmount.amount) / Float(product.months) / Float(shortestOptionPrice.0)
|
||||
let discountValue = Int(round((1.0 - fraction) * 20.0) * 5.0)
|
||||
let discount: String
|
||||
if discountValue > 0 {
|
||||
discount = "-\(discountValue)%"
|
||||
@ -1468,22 +1520,25 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
discount = ""
|
||||
}
|
||||
|
||||
let defaultPrice = product.storeProduct.defaultPrice(shortestOptionPrice.1, monthsCount: Int(months))
|
||||
let defaultPrice = product.storeProduct.defaultPrice(shortestOptionPrice.1, monthsCount: Int(product.months))
|
||||
|
||||
var subtitle = ""
|
||||
var pricePerMonth = product.price
|
||||
if months > 1 {
|
||||
pricePerMonth = product.storeProduct.pricePerMonth(Int(months))
|
||||
if product.months > 1 {
|
||||
pricePerMonth = product.storeProduct.pricePerMonth(Int(product.months))
|
||||
|
||||
if discountValue > 0 {
|
||||
subtitle = "**\(defaultPrice)** \(product.price)"
|
||||
if months == 12 {
|
||||
if product.months == 12 {
|
||||
subtitle = environment.strings.Premium_PricePerYear(subtitle).string
|
||||
}
|
||||
} else {
|
||||
subtitle = product.price
|
||||
}
|
||||
}
|
||||
if product.isCurrent {
|
||||
subtitle = environment.strings.Premium_CurrentPlan
|
||||
}
|
||||
pricePerMonth = environment.strings.Premium_PricePerMonth(pricePerMonth).string
|
||||
|
||||
optionsItems.append(
|
||||
@ -1496,7 +1551,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
subtitle: subtitle,
|
||||
labelPrice: pricePerMonth,
|
||||
discount: discount,
|
||||
selected: product.id == state.selectedProductId,
|
||||
selected: !product.isCurrent && product.id == state.selectedProductId,
|
||||
primaryTextColor: textColor,
|
||||
secondaryTextColor: subtitleColor,
|
||||
accentColor: gradientColors[i],
|
||||
@ -1505,6 +1560,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
)
|
||||
)
|
||||
),
|
||||
isEnabled: product.months > currentProductMonths,
|
||||
action: {
|
||||
selectProduct(product.id)
|
||||
}
|
||||
@ -1738,6 +1794,38 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
)
|
||||
}
|
||||
|
||||
let controller = environment.controller
|
||||
let termsTapActionImpl: ([NSAttributedString.Key: Any]) -> Void = { attributes in
|
||||
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String,
|
||||
let controller = controller() as? PremiumIntroScreen, let navigationController = controller.navigationController as? NavigationController {
|
||||
if url.hasPrefix("https://apps.apple.com/account/subscriptions") {
|
||||
controller.context.sharedContext.applicationBindings.openSubscriptions()
|
||||
} else if url.hasPrefix("https://") || url.hasPrefix("tg://") {
|
||||
controller.context.sharedContext.openExternalUrl(context: controller.context, urlContext: .generic, url: url, forceExternal: !url.hasPrefix("tg://"), presentationData: controller.context.sharedContext.currentPresentationData.with({$0}), navigationController: nil, dismissInput: {})
|
||||
} else {
|
||||
let context = controller.context
|
||||
let signal: Signal<ResolvedUrl, NoError>?
|
||||
switch url {
|
||||
case "terms":
|
||||
signal = cachedTermsPage(context: context)
|
||||
case "privacy":
|
||||
signal = cachedPrivacyPage(context: context)
|
||||
default:
|
||||
signal = nil
|
||||
}
|
||||
if let signal = signal {
|
||||
let _ = (signal
|
||||
|> deliverOnMainQueue).start(next: { resolvedUrl in
|
||||
context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in
|
||||
}, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { [weak controller] c, arguments in
|
||||
controller?.push(c)
|
||||
}, dismissInput: {}, contentContext: nil)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let termsText = termsText.update(
|
||||
component: MultilineTextComponent(
|
||||
text: termsString,
|
||||
@ -1752,35 +1840,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
return nil
|
||||
}
|
||||
},
|
||||
tapAction: { [weak environment] attributes, _ in
|
||||
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String,
|
||||
let controller = environment?.controller() as? PremiumIntroScreen, let navigationController = controller.navigationController as? NavigationController {
|
||||
if url.hasPrefix("https://apps.apple.com/account/subscriptions") {
|
||||
controller.context.sharedContext.applicationBindings.openSubscriptions()
|
||||
} else if url.hasPrefix("https://") || url.hasPrefix("tg://") {
|
||||
controller.context.sharedContext.openExternalUrl(context: controller.context, urlContext: .generic, url: url, forceExternal: !url.hasPrefix("tg://"), presentationData: controller.context.sharedContext.currentPresentationData.with({$0}), navigationController: nil, dismissInput: {})
|
||||
} else {
|
||||
let context = controller.context
|
||||
let signal: Signal<ResolvedUrl, NoError>?
|
||||
switch url {
|
||||
case "terms":
|
||||
signal = cachedTermsPage(context: context)
|
||||
case "privacy":
|
||||
signal = cachedPrivacyPage(context: context)
|
||||
default:
|
||||
signal = nil
|
||||
}
|
||||
if let signal = signal {
|
||||
let _ = (signal
|
||||
|> deliverOnMainQueue).start(next: { resolvedUrl in
|
||||
context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in
|
||||
}, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { [weak controller] c, arguments in
|
||||
controller?.push(c)
|
||||
}, dismissInput: {}, contentContext: nil)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
tapAction: { attributes, _ in
|
||||
termsTapActionImpl(attributes)
|
||||
}
|
||||
),
|
||||
environment: {},
|
||||
@ -1903,9 +1964,11 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
|
||||
private(set) var products: [PremiumProduct]?
|
||||
private(set) var selectedProductId: String?
|
||||
fileprivate var validTransactionIds: [String] = []
|
||||
|
||||
var isPremium: Bool?
|
||||
var otherPeerName: String?
|
||||
var justBought = false
|
||||
|
||||
let animationCache: AnimationCache
|
||||
let animationRenderer: MultiAnimationRenderer
|
||||
@ -1928,6 +1991,18 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
return self.products?.first(where: { $0.id == self.selectedProductId })?.id.hasSuffix(".annual") ?? false
|
||||
}
|
||||
|
||||
var canUpgrade: Bool {
|
||||
if let products = self.products, let current = products.first(where: { $0.isCurrent }), let transactionId = current.transactionId {
|
||||
if self.validTransactionIds.contains(transactionId) {
|
||||
return products.first(where: { $0.months > current.months }) != nil
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
init(context: AccountContext, source: PremiumSource, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.updateInProgress = updateInProgress
|
||||
@ -1939,6 +2014,8 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
|
||||
super.init()
|
||||
|
||||
self.validTransactionIds = context.inAppPurchaseManager?.getValidTransactionIds() ?? []
|
||||
|
||||
let availableProducts: Signal<[InAppPurchaseManager.Product], NoError>
|
||||
if let inAppPurchaseManager = context.inAppPurchaseManager {
|
||||
availableProducts = inAppPurchaseManager.availableProducts
|
||||
@ -1995,7 +2072,11 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
strongSelf.otherPeerName = otherPeerName
|
||||
|
||||
if !hadProducts {
|
||||
strongSelf.selectedProductId = strongSelf.products?.last?.id
|
||||
if let _ = products.first(where: { $0.isCurrent }) {
|
||||
strongSelf.selectedProductId = strongSelf.products?.first?.id
|
||||
} else {
|
||||
strongSelf.selectedProductId = strongSelf.products?.last?.id
|
||||
}
|
||||
|
||||
for (_, video) in promoConfiguration.videos {
|
||||
strongSelf.preloadDisposableSet.add(preloadVideoResource(postbox: context.account.postbox, userLocation: .other, userContentType: .video, resourceReference: .standalone(resource: video.resource), duration: 3.0).start())
|
||||
@ -2037,18 +2118,19 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
let premiumProduct = self.products?.first(where: { $0.id == self.selectedProductId }), !self.inProgress else {
|
||||
return
|
||||
}
|
||||
let isUpgrade = self.products?.first(where: { $0.isCurrent }) != nil
|
||||
|
||||
addAppLogEvent(postbox: self.context.account.postbox, type: "premium.promo_screen_accept")
|
||||
|
||||
self.inProgress = true
|
||||
self.updateInProgress(true)
|
||||
self.updated(transition: .immediate)
|
||||
|
||||
let _ = (self.context.engine.payments.canPurchasePremium(purpose: .subscription)
|
||||
|
||||
let _ = (self.context.engine.payments.canPurchasePremium(purpose: isUpgrade ? .upgrade : .subscription)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] available in
|
||||
if let strongSelf = self {
|
||||
if available {
|
||||
strongSelf.paymentDisposable.set((inAppPurchaseManager.buyProduct(premiumProduct.storeProduct)
|
||||
strongSelf.paymentDisposable.set((inAppPurchaseManager.buyProduct(premiumProduct.storeProduct, isUpgrade: isUpgrade)
|
||||
|> 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)
|
||||
@ -2085,6 +2167,8 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
strongSelf.updateInProgress(false)
|
||||
|
||||
strongSelf.isPremium = true
|
||||
strongSelf.justBought = true
|
||||
|
||||
strongSelf.updated(transition: .easeInOut(duration: 0.25))
|
||||
strongSelf.completion()
|
||||
}
|
||||
@ -2226,7 +2310,11 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
} else if case .gift = context.component.source {
|
||||
titleString = environment.strings.Premium_GiftedTitle
|
||||
} else if state.isPremium == true {
|
||||
titleString = environment.strings.Premium_SubscribedTitle
|
||||
if !state.justBought && state.canUpgrade {
|
||||
titleString = environment.strings.Premium_Title
|
||||
} else {
|
||||
titleString = environment.strings.Premium_SubscribedTitle
|
||||
}
|
||||
} else {
|
||||
titleString = environment.strings.Premium_Title
|
||||
}
|
||||
@ -2357,7 +2445,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
|
||||
let bottomPanelPadding: CGFloat = 12.0
|
||||
let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding
|
||||
let bottomPanelHeight: CGFloat = state.isPremium == true ? bottomInset : bottomPanelPadding + 50.0 + bottomInset
|
||||
let bottomPanelHeight: CGFloat = state.isPremium == true && !state.canUpgrade ? bottomInset : bottomPanelPadding + 50.0 + bottomInset
|
||||
|
||||
let scrollContent = scrollContent.update(
|
||||
component: ScrollComponent<EnvironmentType>(
|
||||
@ -2365,9 +2453,11 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
context: context.component.context,
|
||||
source: context.component.source,
|
||||
isPremium: state.isPremium,
|
||||
justBought: state.justBought,
|
||||
otherPeerName: state.otherPeerName,
|
||||
products: state.products,
|
||||
selectedProductId: state.selectedProductId,
|
||||
validTransactionIds: state.validTransactionIds,
|
||||
promoConfiguration: state.promoConfiguration,
|
||||
present: context.component.present,
|
||||
selectProduct: { [weak state] productId in
|
||||
@ -2454,6 +2544,17 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: max(topInset + 160.0 - titleOffset, environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0)))
|
||||
.scale(titleScale)
|
||||
.opacity(titleAlpha)
|
||||
// .update(Transition.Update { _, view, _ in
|
||||
// if let snapshot = view.snapshotView(afterScreenUpdates: false) {
|
||||
// let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
|
||||
// view.superview?.addSubview(snapshot)
|
||||
// transition.setAlpha(view: snapshot, alpha: 0.0, completion: { [weak snapshot] _ in
|
||||
// snapshot?.removeFromSuperview()
|
||||
// })
|
||||
// snapshot.frame = view.frame
|
||||
// transition.animateAlpha(view: view, from: 0.0, to: titleAlpha)
|
||||
// }
|
||||
// })
|
||||
)
|
||||
|
||||
context.add(secondaryTitle
|
||||
@ -2469,14 +2570,20 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
if state.isPremium == true || isGiftView {
|
||||
if (state.isPremium == true && (!state.canUpgrade || state.justBought)) || isGiftView {
|
||||
|
||||
} else {
|
||||
let buttonTitle: String
|
||||
if state.isPremium == true && state.canUpgrade {
|
||||
buttonTitle = state.isAnnual ? environment.strings.Premium_UpgradeForAnnual(state.price ?? "—").string : environment.strings.Premium_UpgradeFor(state.price ?? "—").string
|
||||
} else {
|
||||
buttonTitle = state.isAnnual ? environment.strings.Premium_SubscribeForAnnual(state.price ?? "—").string : environment.strings.Premium_SubscribeFor(state.price ?? "—").string
|
||||
}
|
||||
|
||||
let sideInset: CGFloat = 16.0
|
||||
let button = button.update(
|
||||
component: SolidRoundedButtonComponent(
|
||||
title: state.isAnnual ? environment.strings.Premium_SubscribeForAnnual(state.price ?? "—").string : environment.strings.Premium_SubscribeFor(state.price ?? "—").string,
|
||||
title: buttonTitle,
|
||||
theme: SolidRoundedButtonComponent.Theme(
|
||||
backgroundColor: UIColor(rgb: 0x8878ff),
|
||||
backgroundColors: [
|
||||
|
@ -222,8 +222,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView
|
||||
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in
|
||||
}, activateChatPreview: { _, _, _, gesture, _ in
|
||||
gesture?.cancel()
|
||||
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {
|
||||
})
|
||||
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {})
|
||||
|
||||
let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true)
|
||||
|
||||
|
@ -843,9 +843,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate
|
||||
}, activateChatPreview: { _, _, _, gesture, _ in
|
||||
gesture?.cancel()
|
||||
}, present: { _ in
|
||||
}, openForumThread: { _, _ in },
|
||||
openStorageManagement: {}, openPasswordSetup: {
|
||||
})
|
||||
}, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {})
|
||||
let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true)
|
||||
|
||||
func makeChatListItem(
|
||||
|
@ -367,8 +367,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}, activateChatPreview: { _, _, _, gesture, _ in
|
||||
gesture?.cancel()
|
||||
}, present: { _ in
|
||||
}, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {
|
||||
})
|
||||
}, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {} )
|
||||
|
||||
func makeChatListItem(
|
||||
peer: EnginePeer,
|
||||
|
@ -56,6 +56,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[2104224014] = { return Api.AttachMenuPeerType.parse_attachMenuPeerTypeSameBotPM($0) }
|
||||
dict[-1392388579] = { return Api.Authorization.parse_authorization($0) }
|
||||
dict[-1896171181] = { return Api.AutoDownloadSettings.parse_autoDownloadSettings($0) }
|
||||
dict[-2124403385] = { return Api.AutoSaveException.parse_autoSaveException($0) }
|
||||
dict[-934791986] = { return Api.AutoSaveSettings.parse_autoSaveSettings($0) }
|
||||
dict[-1065882623] = { return Api.AvailableReaction.parse_availableReaction($0) }
|
||||
dict[-177732982] = { return Api.BankCardOpenUrl.parse_bankCardOpenUrl($0) }
|
||||
dict[1527845466] = { return Api.BaseTheme.parse_baseThemeArctic($0) }
|
||||
@ -273,7 +275,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[1536380829] = { return Api.InputChannel.parse_inputChannelFromMessage($0) }
|
||||
dict[-1991004873] = { return Api.InputChatPhoto.parse_inputChatPhoto($0) }
|
||||
dict[480546647] = { return Api.InputChatPhoto.parse_inputChatPhotoEmpty($0) }
|
||||
dict[-968723890] = { return Api.InputChatPhoto.parse_inputChatUploadedPhoto($0) }
|
||||
dict[-1110593856] = { return Api.InputChatPhoto.parse_inputChatUploadedPhoto($0) }
|
||||
dict[-1736378792] = { return Api.InputCheckPasswordSRP.parse_inputCheckPasswordEmpty($0) }
|
||||
dict[-763367294] = { return Api.InputCheckPasswordSRP.parse_inputCheckPasswordSRP($0) }
|
||||
dict[1968737087] = { return Api.InputClientProxy.parse_inputClientProxy($0) }
|
||||
@ -777,6 +779,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[511092620] = { return Api.TopPeerCategory.parse_topPeerCategoryPhoneCalls($0) }
|
||||
dict[-75283823] = { return Api.TopPeerCategoryPeers.parse_topPeerCategoryPeers($0) }
|
||||
dict[397910539] = { return Api.Update.parse_updateAttachMenuBots($0) }
|
||||
dict[-335171433] = { return Api.Update.parse_updateAutoSaveSettings($0) }
|
||||
dict[-1177566067] = { return Api.Update.parse_updateBotCallbackQuery($0) }
|
||||
dict[299870598] = { return Api.Update.parse_updateBotChatInviteRequester($0) }
|
||||
dict[1299263278] = { return Api.Update.parse_updateBotCommands($0) }
|
||||
@ -908,7 +911,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[-496024847] = { return Api.UserStatus.parse_userStatusRecently($0) }
|
||||
dict[-1274595769] = { return Api.Username.parse_username($0) }
|
||||
dict[-567037804] = { return Api.VideoSize.parse_videoSize($0) }
|
||||
dict[195933766] = { return Api.VideoSize.parse_videoSizeEmojiMarkup($0) }
|
||||
dict[-128171716] = { return Api.VideoSize.parse_videoSizeEmojiMarkup($0) }
|
||||
dict[228623102] = { return Api.VideoSize.parse_videoSizeStickerMarkup($0) }
|
||||
dict[-1539849235] = { return Api.WallPaper.parse_wallPaper($0) }
|
||||
dict[-528465642] = { return Api.WallPaper.parse_wallPaperNoFile($0) }
|
||||
dict[499236004] = { return Api.WallPaperSettings.parse_wallPaperSettings($0) }
|
||||
@ -925,6 +929,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[-1389486888] = { return Api.account.AuthorizationForm.parse_authorizationForm($0) }
|
||||
dict[1275039392] = { return Api.account.Authorizations.parse_authorizations($0) }
|
||||
dict[1674235686] = { return Api.account.AutoDownloadSettings.parse_autoDownloadSettings($0) }
|
||||
dict[1279133341] = { return Api.account.AutoSaveSettings.parse_autoSaveSettings($0) }
|
||||
dict[1474462241] = { return Api.account.ContentSettings.parse_contentSettings($0) }
|
||||
dict[731303195] = { return Api.account.EmailVerified.parse_emailVerified($0) }
|
||||
dict[-507835039] = { return Api.account.EmailVerified.parse_emailVerifiedLogin($0) }
|
||||
@ -1202,6 +1207,10 @@ public extension Api {
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.AutoDownloadSettings:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.AutoSaveException:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.AutoSaveSettings:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.AvailableReaction:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.BankCardOpenUrl:
|
||||
@ -1696,6 +1705,8 @@ public extension Api {
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.account.AutoDownloadSettings:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.account.AutoSaveSettings:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.account.ContentSettings:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.account.EmailVerified:
|
||||
|
@ -522,6 +522,90 @@ public extension Api {
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api {
|
||||
enum AutoSaveException: TypeConstructorDescription {
|
||||
case autoSaveException(peer: Api.Peer, settings: Api.AutoSaveSettings)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .autoSaveException(let peer, let settings):
|
||||
if boxed {
|
||||
buffer.appendInt32(-2124403385)
|
||||
}
|
||||
peer.serialize(buffer, true)
|
||||
settings.serialize(buffer, true)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .autoSaveException(let peer, let settings):
|
||||
return ("autoSaveException", [("peer", peer as Any), ("settings", settings as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_autoSaveException(_ reader: BufferReader) -> AutoSaveException? {
|
||||
var _1: Api.Peer?
|
||||
if let signature = reader.readInt32() {
|
||||
_1 = Api.parse(reader, signature: signature) as? Api.Peer
|
||||
}
|
||||
var _2: Api.AutoSaveSettings?
|
||||
if let signature = reader.readInt32() {
|
||||
_2 = Api.parse(reader, signature: signature) as? Api.AutoSaveSettings
|
||||
}
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
if _c1 && _c2 {
|
||||
return Api.AutoSaveException.autoSaveException(peer: _1!, settings: _2!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api {
|
||||
enum AutoSaveSettings: TypeConstructorDescription {
|
||||
case autoSaveSettings(flags: Int32, videoMaxSize: Int64?)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .autoSaveSettings(let flags, let videoMaxSize):
|
||||
if boxed {
|
||||
buffer.appendInt32(-934791986)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 2) != 0 {serializeInt64(videoMaxSize!, buffer: buffer, boxed: false)}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .autoSaveSettings(let flags, let videoMaxSize):
|
||||
return ("autoSaveSettings", [("flags", flags as Any), ("videoMaxSize", videoMaxSize as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_autoSaveSettings(_ reader: BufferReader) -> AutoSaveSettings? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
var _2: Int64?
|
||||
if Int(_1!) & Int(1 << 2) != 0 {_2 = reader.readInt64() }
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = (Int(_1!) & Int(1 << 2) == 0) || _2 != nil
|
||||
if _c1 && _c2 {
|
||||
return Api.AutoSaveSettings.autoSaveSettings(flags: _1!, videoMaxSize: _2)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api {
|
||||
enum AvailableReaction: TypeConstructorDescription {
|
||||
case availableReaction(flags: Int32, reaction: String, title: String, staticIcon: Api.Document, appearAnimation: Api.Document, selectAnimation: Api.Document, activateAnimation: Api.Document, effectAnimation: Api.Document, aroundAnimation: Api.Document?, centerIcon: Api.Document?)
|
||||
@ -940,75 +1024,3 @@ public extension Api {
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api {
|
||||
enum BotInfo: TypeConstructorDescription {
|
||||
case botInfo(flags: Int32, userId: Int64?, description: String?, descriptionPhoto: Api.Photo?, descriptionDocument: Api.Document?, commands: [Api.BotCommand]?, menuButton: Api.BotMenuButton?)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .botInfo(let flags, let userId, let description, let descriptionPhoto, let descriptionDocument, let commands, let menuButton):
|
||||
if boxed {
|
||||
buffer.appendInt32(-1892676777)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 0) != 0 {serializeInt64(userId!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 1) != 0 {serializeString(description!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 4) != 0 {descriptionPhoto!.serialize(buffer, true)}
|
||||
if Int(flags) & Int(1 << 5) != 0 {descriptionDocument!.serialize(buffer, true)}
|
||||
if Int(flags) & Int(1 << 2) != 0 {buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(commands!.count))
|
||||
for item in commands! {
|
||||
item.serialize(buffer, true)
|
||||
}}
|
||||
if Int(flags) & Int(1 << 3) != 0 {menuButton!.serialize(buffer, true)}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .botInfo(let flags, let userId, let description, let descriptionPhoto, let descriptionDocument, let commands, let menuButton):
|
||||
return ("botInfo", [("flags", flags as Any), ("userId", userId as Any), ("description", description as Any), ("descriptionPhoto", descriptionPhoto as Any), ("descriptionDocument", descriptionDocument as Any), ("commands", commands as Any), ("menuButton", menuButton as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_botInfo(_ reader: BufferReader) -> BotInfo? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
var _2: Int64?
|
||||
if Int(_1!) & Int(1 << 0) != 0 {_2 = reader.readInt64() }
|
||||
var _3: String?
|
||||
if Int(_1!) & Int(1 << 1) != 0 {_3 = parseString(reader) }
|
||||
var _4: Api.Photo?
|
||||
if Int(_1!) & Int(1 << 4) != 0 {if let signature = reader.readInt32() {
|
||||
_4 = Api.parse(reader, signature: signature) as? Api.Photo
|
||||
} }
|
||||
var _5: Api.Document?
|
||||
if Int(_1!) & Int(1 << 5) != 0 {if let signature = reader.readInt32() {
|
||||
_5 = Api.parse(reader, signature: signature) as? Api.Document
|
||||
} }
|
||||
var _6: [Api.BotCommand]?
|
||||
if Int(_1!) & Int(1 << 2) != 0 {if let _ = reader.readInt32() {
|
||||
_6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.BotCommand.self)
|
||||
} }
|
||||
var _7: Api.BotMenuButton?
|
||||
if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() {
|
||||
_7 = Api.parse(reader, signature: signature) as? Api.BotMenuButton
|
||||
} }
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil
|
||||
let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil
|
||||
let _c4 = (Int(_1!) & Int(1 << 4) == 0) || _4 != nil
|
||||
let _c5 = (Int(_1!) & Int(1 << 5) == 0) || _5 != nil
|
||||
let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil
|
||||
let _c7 = (Int(_1!) & Int(1 << 3) == 0) || _7 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 {
|
||||
return Api.BotInfo.botInfo(flags: _1!, userId: _2, description: _3, descriptionPhoto: _4, descriptionDocument: _5, commands: _6, menuButton: _7)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,75 @@
|
||||
public extension Api {
|
||||
enum BotInfo: TypeConstructorDescription {
|
||||
case botInfo(flags: Int32, userId: Int64?, description: String?, descriptionPhoto: Api.Photo?, descriptionDocument: Api.Document?, commands: [Api.BotCommand]?, menuButton: Api.BotMenuButton?)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .botInfo(let flags, let userId, let description, let descriptionPhoto, let descriptionDocument, let commands, let menuButton):
|
||||
if boxed {
|
||||
buffer.appendInt32(-1892676777)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 0) != 0 {serializeInt64(userId!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 1) != 0 {serializeString(description!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 4) != 0 {descriptionPhoto!.serialize(buffer, true)}
|
||||
if Int(flags) & Int(1 << 5) != 0 {descriptionDocument!.serialize(buffer, true)}
|
||||
if Int(flags) & Int(1 << 2) != 0 {buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(commands!.count))
|
||||
for item in commands! {
|
||||
item.serialize(buffer, true)
|
||||
}}
|
||||
if Int(flags) & Int(1 << 3) != 0 {menuButton!.serialize(buffer, true)}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .botInfo(let flags, let userId, let description, let descriptionPhoto, let descriptionDocument, let commands, let menuButton):
|
||||
return ("botInfo", [("flags", flags as Any), ("userId", userId as Any), ("description", description as Any), ("descriptionPhoto", descriptionPhoto as Any), ("descriptionDocument", descriptionDocument as Any), ("commands", commands as Any), ("menuButton", menuButton as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_botInfo(_ reader: BufferReader) -> BotInfo? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
var _2: Int64?
|
||||
if Int(_1!) & Int(1 << 0) != 0 {_2 = reader.readInt64() }
|
||||
var _3: String?
|
||||
if Int(_1!) & Int(1 << 1) != 0 {_3 = parseString(reader) }
|
||||
var _4: Api.Photo?
|
||||
if Int(_1!) & Int(1 << 4) != 0 {if let signature = reader.readInt32() {
|
||||
_4 = Api.parse(reader, signature: signature) as? Api.Photo
|
||||
} }
|
||||
var _5: Api.Document?
|
||||
if Int(_1!) & Int(1 << 5) != 0 {if let signature = reader.readInt32() {
|
||||
_5 = Api.parse(reader, signature: signature) as? Api.Document
|
||||
} }
|
||||
var _6: [Api.BotCommand]?
|
||||
if Int(_1!) & Int(1 << 2) != 0 {if let _ = reader.readInt32() {
|
||||
_6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.BotCommand.self)
|
||||
} }
|
||||
var _7: Api.BotMenuButton?
|
||||
if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() {
|
||||
_7 = Api.parse(reader, signature: signature) as? Api.BotMenuButton
|
||||
} }
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil
|
||||
let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil
|
||||
let _c4 = (Int(_1!) & Int(1 << 4) == 0) || _4 != nil
|
||||
let _c5 = (Int(_1!) & Int(1 << 5) == 0) || _5 != nil
|
||||
let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil
|
||||
let _c7 = (Int(_1!) & Int(1 << 3) == 0) || _7 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 {
|
||||
return Api.BotInfo.botInfo(flags: _1!, userId: _2, description: _3, descriptionPhoto: _4, descriptionDocument: _5, commands: _6, menuButton: _7)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api {
|
||||
enum BotInlineMessage: TypeConstructorDescription {
|
||||
case botInlineMessageMediaAuto(flags: Int32, message: String, entities: [Api.MessageEntity]?, replyMarkup: Api.ReplyMarkup?)
|
||||
|
@ -723,6 +723,7 @@ public extension Api {
|
||||
public extension Api {
|
||||
indirect enum Update: TypeConstructorDescription {
|
||||
case updateAttachMenuBots
|
||||
case updateAutoSaveSettings
|
||||
case updateBotCallbackQuery(flags: Int32, queryId: Int64, userId: Int64, peer: Api.Peer, msgId: Int32, chatInstance: Int64, data: Buffer?, gameShortName: String?)
|
||||
case updateBotChatInviteRequester(peer: Api.Peer, date: Int32, userId: Int64, about: String, invite: Api.ExportedChatInvite, qts: Int32)
|
||||
case updateBotCommands(peer: Api.Peer, botId: Int64, commands: [Api.BotCommand])
|
||||
@ -839,6 +840,12 @@ public extension Api {
|
||||
buffer.appendInt32(397910539)
|
||||
}
|
||||
|
||||
break
|
||||
case .updateAutoSaveSettings:
|
||||
if boxed {
|
||||
buffer.appendInt32(-335171433)
|
||||
}
|
||||
|
||||
break
|
||||
case .updateBotCallbackQuery(let flags, let queryId, let userId, let peer, let msgId, let chatInstance, let data, let gameShortName):
|
||||
if boxed {
|
||||
@ -1785,6 +1792,8 @@ public extension Api {
|
||||
switch self {
|
||||
case .updateAttachMenuBots:
|
||||
return ("updateAttachMenuBots", [])
|
||||
case .updateAutoSaveSettings:
|
||||
return ("updateAutoSaveSettings", [])
|
||||
case .updateBotCallbackQuery(let flags, let queryId, let userId, let peer, let msgId, let chatInstance, let data, let gameShortName):
|
||||
return ("updateBotCallbackQuery", [("flags", flags as Any), ("queryId", queryId as Any), ("userId", userId as Any), ("peer", peer as Any), ("msgId", msgId as Any), ("chatInstance", chatInstance as Any), ("data", data as Any), ("gameShortName", gameShortName as Any)])
|
||||
case .updateBotChatInviteRequester(let peer, let date, let userId, let about, let invite, let qts):
|
||||
@ -2007,6 +2016,9 @@ public extension Api {
|
||||
public static func parse_updateAttachMenuBots(_ reader: BufferReader) -> Update? {
|
||||
return Api.Update.updateAttachMenuBots
|
||||
}
|
||||
public static func parse_updateAutoSaveSettings(_ reader: BufferReader) -> Update? {
|
||||
return Api.Update.updateAutoSaveSettings
|
||||
}
|
||||
public static func parse_updateBotCallbackQuery(_ reader: BufferReader) -> Update? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
|
@ -917,7 +917,8 @@ public extension Api {
|
||||
public extension Api {
|
||||
enum VideoSize: TypeConstructorDescription {
|
||||
case videoSize(flags: Int32, type: String, w: Int32, h: Int32, size: Int32, videoStartTs: Double?)
|
||||
case videoSizeEmojiMarkup(type: String, emojiId: Int64, backgroundColors: [Int32])
|
||||
case videoSizeEmojiMarkup(emojiId: Int64, backgroundColors: [Int32])
|
||||
case videoSizeStickerMarkup(stickerset: Api.InputStickerSet, stickerId: Int64, backgroundColors: [Int32])
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
@ -932,11 +933,10 @@ public extension Api {
|
||||
serializeInt32(size, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 0) != 0 {serializeDouble(videoStartTs!, buffer: buffer, boxed: false)}
|
||||
break
|
||||
case .videoSizeEmojiMarkup(let type, let emojiId, let backgroundColors):
|
||||
case .videoSizeEmojiMarkup(let emojiId, let backgroundColors):
|
||||
if boxed {
|
||||
buffer.appendInt32(195933766)
|
||||
buffer.appendInt32(-128171716)
|
||||
}
|
||||
serializeString(type, buffer: buffer, boxed: false)
|
||||
serializeInt64(emojiId, buffer: buffer, boxed: false)
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(backgroundColors.count))
|
||||
@ -944,6 +944,18 @@ public extension Api {
|
||||
serializeInt32(item, buffer: buffer, boxed: false)
|
||||
}
|
||||
break
|
||||
case .videoSizeStickerMarkup(let stickerset, let stickerId, let backgroundColors):
|
||||
if boxed {
|
||||
buffer.appendInt32(228623102)
|
||||
}
|
||||
stickerset.serialize(buffer, true)
|
||||
serializeInt64(stickerId, buffer: buffer, boxed: false)
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(backgroundColors.count))
|
||||
for item in backgroundColors {
|
||||
serializeInt32(item, buffer: buffer, boxed: false)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@ -951,8 +963,10 @@ public extension Api {
|
||||
switch self {
|
||||
case .videoSize(let flags, let type, let w, let h, let size, let videoStartTs):
|
||||
return ("videoSize", [("flags", flags as Any), ("type", type as Any), ("w", w as Any), ("h", h as Any), ("size", size as Any), ("videoStartTs", videoStartTs as Any)])
|
||||
case .videoSizeEmojiMarkup(let type, let emojiId, let backgroundColors):
|
||||
return ("videoSizeEmojiMarkup", [("type", type as Any), ("emojiId", emojiId as Any), ("backgroundColors", backgroundColors as Any)])
|
||||
case .videoSizeEmojiMarkup(let emojiId, let backgroundColors):
|
||||
return ("videoSizeEmojiMarkup", [("emojiId", emojiId as Any), ("backgroundColors", backgroundColors as Any)])
|
||||
case .videoSizeStickerMarkup(let stickerset, let stickerId, let backgroundColors):
|
||||
return ("videoSizeStickerMarkup", [("stickerset", stickerset as Any), ("stickerId", stickerId as Any), ("backgroundColors", backgroundColors as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
@ -983,8 +997,26 @@ public extension Api {
|
||||
}
|
||||
}
|
||||
public static func parse_videoSizeEmojiMarkup(_ reader: BufferReader) -> VideoSize? {
|
||||
var _1: String?
|
||||
_1 = parseString(reader)
|
||||
var _1: Int64?
|
||||
_1 = reader.readInt64()
|
||||
var _2: [Int32]?
|
||||
if let _ = reader.readInt32() {
|
||||
_2 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self)
|
||||
}
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
if _c1 && _c2 {
|
||||
return Api.VideoSize.videoSizeEmojiMarkup(emojiId: _1!, backgroundColors: _2!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_videoSizeStickerMarkup(_ reader: BufferReader) -> VideoSize? {
|
||||
var _1: Api.InputStickerSet?
|
||||
if let signature = reader.readInt32() {
|
||||
_1 = Api.parse(reader, signature: signature) as? Api.InputStickerSet
|
||||
}
|
||||
var _2: Int64?
|
||||
_2 = reader.readInt64()
|
||||
var _3: [Int32]?
|
||||
@ -995,7 +1027,7 @@ public extension Api {
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
if _c1 && _c2 && _c3 {
|
||||
return Api.VideoSize.videoSizeEmojiMarkup(type: _1!, emojiId: _2!, backgroundColors: _3!)
|
||||
return Api.VideoSize.videoSizeStickerMarkup(stickerset: _1!, stickerId: _2!, backgroundColors: _3!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
|
@ -308,6 +308,86 @@ public extension Api.account {
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api.account {
|
||||
enum AutoSaveSettings: TypeConstructorDescription {
|
||||
case autoSaveSettings(usersSettings: Api.AutoSaveSettings, chatsSettings: Api.AutoSaveSettings, broadcastsSettings: Api.AutoSaveSettings, exceptions: [Api.AutoSaveException], chats: [Api.Chat], users: [Api.User])
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .autoSaveSettings(let usersSettings, let chatsSettings, let broadcastsSettings, let exceptions, let chats, let users):
|
||||
if boxed {
|
||||
buffer.appendInt32(1279133341)
|
||||
}
|
||||
usersSettings.serialize(buffer, true)
|
||||
chatsSettings.serialize(buffer, true)
|
||||
broadcastsSettings.serialize(buffer, true)
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(exceptions.count))
|
||||
for item in exceptions {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
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 .autoSaveSettings(let usersSettings, let chatsSettings, let broadcastsSettings, let exceptions, let chats, let users):
|
||||
return ("autoSaveSettings", [("usersSettings", usersSettings as Any), ("chatsSettings", chatsSettings as Any), ("broadcastsSettings", broadcastsSettings as Any), ("exceptions", exceptions as Any), ("chats", chats as Any), ("users", users as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_autoSaveSettings(_ reader: BufferReader) -> AutoSaveSettings? {
|
||||
var _1: Api.AutoSaveSettings?
|
||||
if let signature = reader.readInt32() {
|
||||
_1 = Api.parse(reader, signature: signature) as? Api.AutoSaveSettings
|
||||
}
|
||||
var _2: Api.AutoSaveSettings?
|
||||
if let signature = reader.readInt32() {
|
||||
_2 = Api.parse(reader, signature: signature) as? Api.AutoSaveSettings
|
||||
}
|
||||
var _3: Api.AutoSaveSettings?
|
||||
if let signature = reader.readInt32() {
|
||||
_3 = Api.parse(reader, signature: signature) as? Api.AutoSaveSettings
|
||||
}
|
||||
var _4: [Api.AutoSaveException]?
|
||||
if let _ = reader.readInt32() {
|
||||
_4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.AutoSaveException.self)
|
||||
}
|
||||
var _5: [Api.Chat]?
|
||||
if let _ = reader.readInt32() {
|
||||
_5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
|
||||
}
|
||||
var _6: [Api.User]?
|
||||
if let _ = reader.readInt32() {
|
||||
_6 = 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
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 {
|
||||
return Api.account.AutoSaveSettings.autoSaveSettings(usersSettings: _1!, chatsSettings: _2!, broadcastsSettings: _3!, exceptions: _4!, chats: _5!, users: _6!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api.account {
|
||||
enum ContentSettings: TypeConstructorDescription {
|
||||
case contentSettings(flags: Int32)
|
||||
|
@ -187,6 +187,21 @@ public extension Api.functions.account {
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.account {
|
||||
static func deleteAutoSaveExceptions() -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(1404829728)
|
||||
|
||||
return (FunctionDescription(name: "account.deleteAutoSaveExceptions", parameters: []), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.Bool?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.Bool
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.account {
|
||||
static func deleteSecureValue(types: [Api.SecureValueType]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||
let buffer = Buffer()
|
||||
@ -298,6 +313,21 @@ public extension Api.functions.account {
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.account {
|
||||
static func getAutoSaveSettings() -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.account.AutoSaveSettings>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(-1379156774)
|
||||
|
||||
return (FunctionDescription(name: "account.getAutoSaveSettings", parameters: []), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.account.AutoSaveSettings? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.account.AutoSaveSettings?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.account.AutoSaveSettings
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.account {
|
||||
static func getChatThemes(hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.account.Themes>) {
|
||||
let buffer = Buffer()
|
||||
@ -889,6 +919,23 @@ public extension Api.functions.account {
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.account {
|
||||
static func saveAutoSaveSettings(flags: Int32, peer: Api.InputPeer?, settings: Api.AutoSaveSettings) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(-694451359)
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 3) != 0 {peer!.serialize(buffer, true)}
|
||||
settings.serialize(buffer, true)
|
||||
return (FunctionDescription(name: "account.saveAutoSaveSettings", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("settings", String(describing: settings))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.Bool?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.Bool
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.account {
|
||||
static func saveRingtone(id: Api.InputDocument, unsave: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.account.SavedRingtone>) {
|
||||
let buffer = Buffer()
|
||||
|
@ -272,7 +272,7 @@ public extension Api {
|
||||
enum InputChatPhoto: TypeConstructorDescription {
|
||||
case inputChatPhoto(id: Api.InputPhoto)
|
||||
case inputChatPhotoEmpty
|
||||
case inputChatUploadedPhoto(flags: Int32, file: Api.InputFile?, video: Api.InputFile?, videoStartTs: Double?)
|
||||
case inputChatUploadedPhoto(flags: Int32, file: Api.InputFile?, video: Api.InputFile?, videoStartTs: Double?, videoEmojiMarkup: Api.VideoSize?)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
@ -288,14 +288,15 @@ public extension Api {
|
||||
}
|
||||
|
||||
break
|
||||
case .inputChatUploadedPhoto(let flags, let file, let video, let videoStartTs):
|
||||
case .inputChatUploadedPhoto(let flags, let file, let video, let videoStartTs, let videoEmojiMarkup):
|
||||
if boxed {
|
||||
buffer.appendInt32(-968723890)
|
||||
buffer.appendInt32(-1110593856)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 0) != 0 {file!.serialize(buffer, true)}
|
||||
if Int(flags) & Int(1 << 1) != 0 {video!.serialize(buffer, true)}
|
||||
if Int(flags) & Int(1 << 2) != 0 {serializeDouble(videoStartTs!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 3) != 0 {videoEmojiMarkup!.serialize(buffer, true)}
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -306,8 +307,8 @@ public extension Api {
|
||||
return ("inputChatPhoto", [("id", id as Any)])
|
||||
case .inputChatPhotoEmpty:
|
||||
return ("inputChatPhotoEmpty", [])
|
||||
case .inputChatUploadedPhoto(let flags, let file, let video, let videoStartTs):
|
||||
return ("inputChatUploadedPhoto", [("flags", flags as Any), ("file", file as Any), ("video", video as Any), ("videoStartTs", videoStartTs as Any)])
|
||||
case .inputChatUploadedPhoto(let flags, let file, let video, let videoStartTs, let videoEmojiMarkup):
|
||||
return ("inputChatUploadedPhoto", [("flags", flags as Any), ("file", file as Any), ("video", video as Any), ("videoStartTs", videoStartTs as Any), ("videoEmojiMarkup", videoEmojiMarkup as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
@ -340,12 +341,17 @@ public extension Api {
|
||||
} }
|
||||
var _4: Double?
|
||||
if Int(_1!) & Int(1 << 2) != 0 {_4 = reader.readDouble() }
|
||||
var _5: Api.VideoSize?
|
||||
if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() {
|
||||
_5 = Api.parse(reader, signature: signature) as? Api.VideoSize
|
||||
} }
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil
|
||||
let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil
|
||||
let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 {
|
||||
return Api.InputChatPhoto.inputChatUploadedPhoto(flags: _1!, file: _2, video: _3, videoStartTs: _4)
|
||||
let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 {
|
||||
return Api.InputChatPhoto.inputChatUploadedPhoto(flags: _1!, file: _2, video: _3, videoStartTs: _4, videoEmojiMarkup: _5)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
|
@ -172,7 +172,7 @@ func telegramMediaFileFromApiDocument(_ document: Api.Document) -> TelegramMedia
|
||||
videoThumbnails.append(TelegramMediaFile.VideoThumbnail(
|
||||
dimensions: PixelDimensions(width: w, height: h),
|
||||
resource: resource))
|
||||
case .videoSizeEmojiMarkup:
|
||||
case .videoSizeEmojiMarkup, .videoSizeStickerMarkup:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -50,8 +50,10 @@ func telegramMediaImageFromApiPhoto(_ photo: Api.Photo) -> TelegramMediaImage? {
|
||||
resource = CloudPhotoSizeMediaResource(datacenterId: dcId, photoId: id, accessHash: accessHash, sizeSpec: type, size: Int64(size), fileReference: fileReference.makeData())
|
||||
|
||||
videoRepresentations.append(TelegramMediaImage.VideoRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, startTimestamp: videoStartTs))
|
||||
case let .videoSizeEmojiMarkup(_, emojiId, backgroundColors):
|
||||
case let .videoSizeEmojiMarkup(emojiId, backgroundColors):
|
||||
emojiMarkup = TelegramMediaImage.EmojiMarkup(fileId: emojiId, backgroundColors: backgroundColors)
|
||||
case .videoSizeStickerMarkup:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -79,7 +79,6 @@ private extension PremiumPromoConfiguration {
|
||||
|
||||
var productOptions: [PremiumProductOption] = []
|
||||
for option in options {
|
||||
|
||||
if case let .premiumSubscriptionOption(flags, transaction, months, currency, amount, botUrl, storeProduct) = option {
|
||||
productOptions.append(PremiumProductOption(isCurrent: (flags & (1 << 1)) != 0, months: months, currency: currency, amount: amount, botUrl: botUrl, transactionId: transaction, availableForUpgrade: (flags & (1 << 2)) != 0, storeProductId: storeProduct))
|
||||
}
|
||||
|
@ -9,6 +9,8 @@ public enum ServerProvidedSuggestion: String {
|
||||
case validatePhoneNumber = "VALIDATE_PHONE_NUMBER"
|
||||
case validatePassword = "VALIDATE_PASSWORD"
|
||||
case setupPassword = "SETUP_PASSWORD"
|
||||
case upgradePremium = "PREMIUM_UPGRADE"
|
||||
case annualPremium = "PREMIUM_ANNUAL"
|
||||
}
|
||||
|
||||
private var dismissedSuggestionsPromise = ValuePromise<[AccountRecordId: Set<ServerProvidedSuggestion>]>([:])
|
||||
|
@ -69,8 +69,13 @@ func _internal_canPurchasePremium(account: Account, purpose: AppStoreTransaction
|
||||
switch purpose {
|
||||
case .subscription, .restore, .upgrade:
|
||||
var flags: Int32 = 0
|
||||
if case .restore = purpose {
|
||||
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):
|
||||
|
@ -4,7 +4,6 @@ import SwiftSignalKit
|
||||
import MtProtoKit
|
||||
import TelegramApi
|
||||
|
||||
|
||||
public enum UpdatePeerPhotoStatus {
|
||||
case progress(Float)
|
||||
case complete([TelegramMediaImageRepresentation])
|
||||
@ -153,7 +152,7 @@ func _internal_updatePeerPhotoInternal(postbox: Postbox, network: Network, state
|
||||
|
||||
var videoEmojiMarkup: Api.VideoSize?
|
||||
if let fileId, let backgroundColors {
|
||||
videoEmojiMarkup = .videoSizeEmojiMarkup(type: "e", emojiId: fileId, backgroundColors: backgroundColors)
|
||||
videoEmojiMarkup = .videoSizeEmojiMarkup(emojiId: fileId, backgroundColors: backgroundColors)
|
||||
flags |= (1 << 4)
|
||||
}
|
||||
|
||||
@ -217,7 +216,7 @@ func _internal_updatePeerPhotoInternal(postbox: Postbox, network: Network, state
|
||||
resource = CloudPhotoSizeMediaResource(datacenterId: dcId, photoId: id, accessHash: accessHash, sizeSpec: type, size: Int64(size), fileReference: fileReference.makeData())
|
||||
|
||||
videoRepresentations.append(TelegramMediaImage.VideoRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, startTimestamp: videoStartTs))
|
||||
case .videoSizeEmojiMarkup:
|
||||
case .videoSizeEmojiMarkup, .videoSizeStickerMarkup:
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -280,9 +279,9 @@ func _internal_updatePeerPhotoInternal(postbox: Postbox, network: Network, state
|
||||
|
||||
let request: Signal<Api.Updates, MTRpcError>
|
||||
if let peer = peer as? TelegramGroup {
|
||||
request = network.request(Api.functions.messages.editChatPhoto(chatId: peer.id.id._internalGetInt64Value(), photo: .inputChatUploadedPhoto(flags: flags, file: file, video: videoFile, videoStartTs: videoStartTimestamp)))
|
||||
request = network.request(Api.functions.messages.editChatPhoto(chatId: peer.id.id._internalGetInt64Value(), photo: .inputChatUploadedPhoto(flags: flags, file: file, video: videoFile, videoStartTs: videoStartTimestamp, videoEmojiMarkup: nil)))
|
||||
} else if let peer = peer as? TelegramChannel, let inputChannel = apiInputChannel(peer) {
|
||||
request = network.request(Api.functions.channels.editPhoto(channel: inputChannel, photo: .inputChatUploadedPhoto(flags: flags, file: file, video: videoFile, videoStartTs: videoStartTimestamp)))
|
||||
request = network.request(Api.functions.channels.editPhoto(channel: inputChannel, photo: .inputChatUploadedPhoto(flags: flags, file: file, video: videoFile, videoStartTs: videoStartTimestamp, videoEmojiMarkup: nil)))
|
||||
} else {
|
||||
assertionFailure()
|
||||
request = .complete()
|
||||
|
@ -36,6 +36,8 @@ swift_library(
|
||||
"//submodules/LegacyComponents:LegacyComponents",
|
||||
"//submodules/DrawingUI:DrawingUI",
|
||||
"//submodules/StickerResources:StickerResources",
|
||||
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
|
||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -21,6 +21,8 @@ import GradientBackground
|
||||
import LegacyComponents
|
||||
import DrawingUI
|
||||
import SolidRoundedButtonComponent
|
||||
import AnimationCache
|
||||
import EmojiTextAttachmentView
|
||||
|
||||
enum AvatarBackground: Equatable {
|
||||
case gradient([UInt32])
|
||||
@ -56,7 +58,7 @@ private let defaultBackgrounds: [AvatarBackground] = [
|
||||
.gradient([0x82b1ff, 0x665fff]),
|
||||
]
|
||||
|
||||
private struct KeyboardInputData: Equatable {
|
||||
public struct AvatarKeyboardInputData: Equatable {
|
||||
var emoji: EmojiPagerContentComponent
|
||||
var stickers: EmojiPagerContentComponent?
|
||||
|
||||
@ -110,7 +112,8 @@ final class AvatarEditorScreenComponent: Component {
|
||||
|
||||
final class State: ComponentState {
|
||||
let context: AccountContext
|
||||
|
||||
let ready: Promise<Bool>
|
||||
|
||||
var selectedBackground: AvatarBackground
|
||||
var selectedFile: TelegramMediaFile?
|
||||
|
||||
@ -124,8 +127,9 @@ final class AvatarEditorScreenComponent: Component {
|
||||
|
||||
var isSearchActive: Bool = false
|
||||
|
||||
init(context: AccountContext, initialFileId: Int64?, initialBackgroundColors: [Int32]?) {
|
||||
init(context: AccountContext, ready: Promise<Bool>, initialFileId: Int64?, initialBackgroundColors: [Int32]?) {
|
||||
self.context = context
|
||||
self.ready = ready
|
||||
|
||||
self.selectedBackground = defaultBackgrounds.first!
|
||||
self.previousColor = self.selectedBackground
|
||||
@ -153,6 +157,7 @@ final class AvatarEditorScreenComponent: Component {
|
||||
func makeState() -> State {
|
||||
return State(
|
||||
context: self.context,
|
||||
ready: self.ready,
|
||||
initialFileId: self.initialFileId,
|
||||
initialBackgroundColors: self.initialBackgroundColors
|
||||
)
|
||||
@ -187,7 +192,7 @@ final class AvatarEditorScreenComponent: Component {
|
||||
private var controller: (() -> AvatarEditorScreen?)?
|
||||
|
||||
private var dataDisposable: Disposable?
|
||||
private var data: KeyboardInputData?
|
||||
private var data: AvatarKeyboardInputData?
|
||||
|
||||
private let emojiSearchDisposable = MetaDisposable()
|
||||
private let emojiSearchResult = Promise<(groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable)?>(nil)
|
||||
@ -232,7 +237,7 @@ final class AvatarEditorScreenComponent: Component {
|
||||
self.emojiSearchDisposable.dispose()
|
||||
}
|
||||
|
||||
private func updateData(_ data: KeyboardInputData) {
|
||||
private func updateData(_ data: AvatarKeyboardInputData) {
|
||||
let wasEmpty = self.data == nil
|
||||
self.data = data
|
||||
|
||||
@ -240,6 +245,7 @@ final class AvatarEditorScreenComponent: Component {
|
||||
self.state?.selectedFile = data.emoji.panelItemGroups.first?.items.first?.itemFile
|
||||
}
|
||||
self.state?.updated(transition: .immediate)
|
||||
self.state?.ready.set(.single(true))
|
||||
|
||||
let updateSearchQuery: (String, String) -> Void = { [weak self] rawQuery, languageCode in
|
||||
guard let strongSelf = self, let context = strongSelf.state?.context else {
|
||||
@ -424,7 +430,7 @@ final class AvatarEditorScreenComponent: Component {
|
||||
return
|
||||
}
|
||||
let context = controller.context
|
||||
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks)
|
||||
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks)
|
||||
let _ = (context.account.postbox.combinedView(keys: [viewKey])
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { views in
|
||||
@ -664,7 +670,7 @@ final class AvatarEditorScreenComponent: Component {
|
||||
func update(component: AvatarEditorScreenComponent, availableSize: CGSize, state: State, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
|
||||
let environment = environment[ViewControllerComponentContainer.Environment.self].value
|
||||
let strings = environment.strings
|
||||
|
||||
@ -738,45 +744,12 @@ final class AvatarEditorScreenComponent: Component {
|
||||
self.keyboardContainerView.backgroundColor = environment.theme.list.itemBlocksBackgroundColor
|
||||
self.panelSeparatorView.backgroundColor = environment.theme.list.itemPlainSeparatorColor
|
||||
|
||||
if self.dataDisposable == nil {
|
||||
if self.dataDisposable == nil, let controller = controller() as? AvatarEditorScreen {
|
||||
let context = component.context
|
||||
let emojiItems = EmojiPagerContentComponent.emojiInputData(
|
||||
context: context,
|
||||
animationCache: context.animationCache,
|
||||
animationRenderer: context.animationRenderer,
|
||||
isStandalone: false,
|
||||
isStatusSelection: false,
|
||||
isReactionSelection: false,
|
||||
isEmojiSelection: false,
|
||||
isProfilePhotoEmojiSelection: true,
|
||||
topReactionItems: [],
|
||||
areUnicodeEmojiEnabled: false,
|
||||
areCustomEmojiEnabled: true,
|
||||
chatPeerId: context.account.peerId,
|
||||
hasSearch: true,
|
||||
forceHasPremium: true
|
||||
)
|
||||
|
||||
let stickerItems = EmojiPagerContentComponent.stickerInputData(
|
||||
context: context,
|
||||
animationCache: context.animationCache,
|
||||
animationRenderer: context.animationRenderer,
|
||||
stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks],
|
||||
stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers],
|
||||
chatPeerId: context.account.peerId,
|
||||
hasSearch: true,
|
||||
hasTrending: false,
|
||||
forceHasPremium: true,
|
||||
searchIsPlaceholderOnly: false
|
||||
)
|
||||
|
||||
let signal = combineLatest(queue: .mainQueue(),
|
||||
emojiItems,
|
||||
stickerItems,
|
||||
controller.inputData |> delay(0.01, queue: .mainQueue()),
|
||||
self.emojiSearchResult.get()
|
||||
) |> map { emoji, stickers, searchResult -> (KeyboardInputData, (groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable)?) in
|
||||
return (KeyboardInputData(emoji: emoji, stickers: stickers), searchResult)
|
||||
}
|
||||
)
|
||||
self.dataDisposable = (signal
|
||||
|> deliverOnMainQueue
|
||||
).start(next: { [weak self, weak state] data, searchResult in
|
||||
@ -819,7 +792,7 @@ final class AvatarEditorScreenComponent: Component {
|
||||
tapped: { [weak state] in
|
||||
if let state, !state.editingColor {
|
||||
state.expanded = !state.expanded
|
||||
state.updated(transition: .easeInOut(duration: 0.3))
|
||||
state.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring)))
|
||||
}
|
||||
}
|
||||
)
|
||||
@ -833,7 +806,7 @@ final class AvatarEditorScreenComponent: Component {
|
||||
}
|
||||
|
||||
let previewScale = effectiveIsExpanded ? 1.0 : collapsedAvatarSize.width / avatarPreviewSize.width
|
||||
let cornerRadius = effectiveIsExpanded ? 0.0 : availableSize.width / 2.0
|
||||
let cornerRadius = effectiveIsExpanded ? 0.0 : availableSize.width / (component.peerType == .forum ? 4.0 : 2.0)
|
||||
let position = effectiveIsExpanded ? avatarPreviewSize.height / 2.0 : environment.navigationHeight + 10.0
|
||||
|
||||
transition.setBounds(view: previewView, bounds: CGRect(origin: .zero, size: avatarPreviewSize))
|
||||
@ -1095,6 +1068,9 @@ final class AvatarEditorScreenComponent: Component {
|
||||
hideTopPanelUpdated: { [weak self] hideTopPanel, transition in
|
||||
if let strongSelf = self {
|
||||
strongSelf.state?.isSearchActive = hideTopPanel
|
||||
if hideTopPanel {
|
||||
strongSelf.state?.expanded = false
|
||||
}
|
||||
strongSelf.state?.updated(transition: transition)
|
||||
}
|
||||
},
|
||||
@ -1147,7 +1123,7 @@ final class AvatarEditorScreenComponent: Component {
|
||||
switch component.peerType {
|
||||
case .user:
|
||||
buttonText = strings.AvatarEditor_SetProfilePhoto
|
||||
case .group:
|
||||
case .group, .forum:
|
||||
buttonText = strings.AvatarEditor_SetGroupPhoto
|
||||
case .channel:
|
||||
buttonText = strings.AvatarEditor_SetChannelPhoto
|
||||
@ -1180,66 +1156,106 @@ final class AvatarEditorScreenComponent: Component {
|
||||
return availableSize
|
||||
}
|
||||
|
||||
private let queue = Queue()
|
||||
func complete() {
|
||||
guard let state = self.state, let itemFile = state.selectedFile, let previewView = self.previewView.view else {
|
||||
guard let state = self.state, let file = state.selectedFile, let controller = self.controller?() else {
|
||||
return
|
||||
}
|
||||
let size = CGSize(width: 1920.0, height: 1920.0)
|
||||
let image = state.selectedBackground.generateImage(size: size)
|
||||
let tempPath = NSTemporaryDirectory() + "/\(UInt64.random(in: 0 ... UInt64.max)).jpg"
|
||||
let tempUrl = NSURL(fileURLWithPath: tempPath) as URL
|
||||
try? image.jpegData(compressionQuality: 1.0)?.write(to: tempUrl)
|
||||
|
||||
let entity = DrawingStickerEntity(content: .file(itemFile))
|
||||
entity.referenceDrawingSize = size
|
||||
entity.position = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
|
||||
entity.scale = 3.3
|
||||
|
||||
var documentId: Int64 = 0
|
||||
if case let .file(file) = entity.content, file.isCustomEmoji {
|
||||
documentId = file.fileId.id
|
||||
}
|
||||
|
||||
let colors: [NSNumber] = state.selectedBackground.colors.map { Int32(bitPattern: $0) as NSNumber }
|
||||
|
||||
let entitiesData = DrawingEntitiesView.encodeEntities([entity])
|
||||
|
||||
let paintingData = TGPaintingData(
|
||||
drawing: nil,
|
||||
entitiesData: entitiesData,
|
||||
image: nil,
|
||||
stillImage: nil,
|
||||
hasAnimation: entity.isAnimated,
|
||||
stickers: []
|
||||
)
|
||||
|
||||
let adjustments = PGPhotoEditorValues(
|
||||
originalSize: size,
|
||||
cropRect: CGRect(origin: .zero, size: size),
|
||||
cropRotation: 0.0,
|
||||
cropOrientation: .up,
|
||||
cropLockedAspectRatio: 1.0,
|
||||
cropMirrored: false,
|
||||
toolValues: [:],
|
||||
paintingData: paintingData,
|
||||
sendAsGif: true
|
||||
)
|
||||
let preset: TGMediaVideoConversionPreset = TGMediaVideoConversionPresetProfileHigh
|
||||
|
||||
let combinedImage = generateImage(previewView.bounds.size, contextGenerator: { size, context in
|
||||
let bounds = CGRect(origin: .zero, size: size)
|
||||
if let cgImage = image.cgImage {
|
||||
context.draw(cgImage, in: bounds)
|
||||
let context = controller.context
|
||||
let _ = context.animationCache.getFirstFrame(queue: self.queue, sourceId: file.resource.id.stringRepresentation, size: CGSize(width: 640.0, height: 640.0), fetch: animationCacheFetchFile(context: context, userLocation: .other, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: true, customColor: nil), completion: { result in
|
||||
guard let item = result.item else {
|
||||
return
|
||||
}
|
||||
var image: UIImage?
|
||||
if let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) {
|
||||
switch frame.frame.format {
|
||||
case let .rgba(data, width, height, bytesPerRow):
|
||||
guard let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow) else {
|
||||
return
|
||||
}
|
||||
|
||||
data.withUnsafeBytes { bytes -> Void in
|
||||
memcpy(context.bytes, bytes.baseAddress!, height * bytesPerRow)
|
||||
}
|
||||
|
||||
image = context.generateImage()
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
||||
context.scaleBy(x: 1.0, y: -1.0)
|
||||
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
||||
|
||||
previewView.layer.render(in: context)
|
||||
}, opaque: false)!
|
||||
|
||||
self.controller?()?.completion(combinedImage, tempUrl, TGVideoEditAdjustments(photoEditorValues: adjustments, preset: preset, documentId: documentId, colors: colors), { [weak self] in
|
||||
self?.controller?()?.dismiss()
|
||||
Queue.mainQueue().async {
|
||||
guard let image else {
|
||||
return
|
||||
}
|
||||
|
||||
let size = CGSize(width: 800.0, height: 800.0)
|
||||
let backgroundImage = state.selectedBackground.generateImage(size: size)
|
||||
let tempPath = NSTemporaryDirectory() + "/\(UInt64.random(in: 0 ... UInt64.max)).jpg"
|
||||
let tempUrl = NSURL(fileURLWithPath: tempPath) as URL
|
||||
try? backgroundImage.jpegData(compressionQuality: 0.8)?.write(to: tempUrl)
|
||||
|
||||
let drawingSize = CGSize(width: 1920.0, height: 1920.0)
|
||||
let entity = DrawingStickerEntity(content: .file(file))
|
||||
entity.referenceDrawingSize = drawingSize
|
||||
entity.position = CGPoint(x: drawingSize.width / 2.0, y: drawingSize.height / 2.0)
|
||||
entity.scale = 3.3
|
||||
|
||||
var documentId: Int64 = 0
|
||||
if case let .file(file) = entity.content, file.isCustomEmoji {
|
||||
documentId = file.fileId.id
|
||||
}
|
||||
|
||||
let colors: [NSNumber] = state.selectedBackground.colors.map { Int32(bitPattern: $0) as NSNumber }
|
||||
|
||||
let entitiesData = DrawingEntitiesView.encodeEntities([entity])
|
||||
|
||||
let paintingData = TGPaintingData(
|
||||
drawing: nil,
|
||||
entitiesData: entitiesData,
|
||||
image: nil,
|
||||
stillImage: nil,
|
||||
hasAnimation: entity.isAnimated,
|
||||
stickers: []
|
||||
)
|
||||
|
||||
let adjustments = PGPhotoEditorValues(
|
||||
originalSize: size,
|
||||
cropRect: CGRect(origin: .zero, size: size),
|
||||
cropRotation: 0.0,
|
||||
cropOrientation: .up,
|
||||
cropLockedAspectRatio: 1.0,
|
||||
cropMirrored: false,
|
||||
toolValues: [:],
|
||||
paintingData: paintingData,
|
||||
sendAsGif: true
|
||||
)
|
||||
let preset: TGMediaVideoConversionPreset = TGMediaVideoConversionPresetProfileHigh
|
||||
|
||||
let combinedImage = generateImage(size, contextGenerator: { size, context in
|
||||
let bounds = CGRect(origin: .zero, size: size)
|
||||
if let cgImage = backgroundImage.cgImage {
|
||||
context.draw(cgImage, in: bounds)
|
||||
}
|
||||
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
||||
context.scaleBy(x: 0.67, y: 0.67)
|
||||
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
||||
|
||||
if let cgImage = image.cgImage {
|
||||
context.draw(cgImage, in: bounds)
|
||||
}
|
||||
}, opaque: false)!
|
||||
|
||||
if entity.isAnimated {
|
||||
controller.videoCompletion(combinedImage, tempUrl, TGVideoEditAdjustments(photoEditorValues: adjustments, preset: preset, documentId: documentId, colors: colors), { [weak controller] in
|
||||
controller?.dismiss()
|
||||
})
|
||||
} else {
|
||||
controller.imageCompletion(combinedImage, { [weak controller] in
|
||||
controller?.dismiss()
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1259,18 +1275,65 @@ public final class AvatarEditorScreen: ViewControllerComponentContainer {
|
||||
case user
|
||||
case group
|
||||
case channel
|
||||
case forum
|
||||
}
|
||||
fileprivate let context: AccountContext
|
||||
fileprivate let inputData: Signal<AvatarKeyboardInputData, NoError>
|
||||
|
||||
private let readyValue = Promise<Bool>()
|
||||
override public var ready: Promise<Bool> {
|
||||
return self.readyValue
|
||||
}
|
||||
|
||||
public var completion: (UIImage, URL, TGVideoEditAdjustments, @escaping () -> Void) -> Void = { _, _, _, _ in }
|
||||
public var imageCompletion: (UIImage, @escaping () -> Void) -> Void = { _, _ in }
|
||||
public var videoCompletion: (UIImage, URL, TGVideoEditAdjustments, @escaping () -> Void) -> Void = { _, _, _, _ in }
|
||||
|
||||
public init(context: AccountContext, peerType: PeerType, initialFileId: Int64?, initialBackgroundColors: [Int32]?) {
|
||||
public static func inputData(context: AccountContext, isGroup: Bool) -> Signal<AvatarKeyboardInputData, NoError> {
|
||||
let emojiItems = EmojiPagerContentComponent.emojiInputData(
|
||||
context: context,
|
||||
animationCache: context.animationCache,
|
||||
animationRenderer: context.animationRenderer,
|
||||
isStandalone: false,
|
||||
isStatusSelection: false,
|
||||
isReactionSelection: false,
|
||||
isEmojiSelection: false,
|
||||
isProfilePhotoEmojiSelection: !isGroup,
|
||||
isGroupPhotoEmojiSelection: isGroup,
|
||||
topReactionItems: [],
|
||||
areUnicodeEmojiEnabled: false,
|
||||
areCustomEmojiEnabled: true,
|
||||
chatPeerId: context.account.peerId,
|
||||
hasSearch: true,
|
||||
forceHasPremium: true
|
||||
)
|
||||
|
||||
let stickerItems = EmojiPagerContentComponent.stickerInputData(
|
||||
context: context,
|
||||
animationCache: context.animationCache,
|
||||
animationRenderer: context.animationRenderer,
|
||||
stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks],
|
||||
stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers],
|
||||
chatPeerId: context.account.peerId,
|
||||
hasSearch: true,
|
||||
hasTrending: false,
|
||||
forceHasPremium: true,
|
||||
searchIsPlaceholderOnly: false,
|
||||
isProfilePhotoEmojiSelection: !isGroup,
|
||||
isGroupPhotoEmojiSelection: isGroup
|
||||
)
|
||||
|
||||
let signal = combineLatest(queue: .mainQueue(),
|
||||
emojiItems,
|
||||
stickerItems
|
||||
) |> map { emoji, stickers -> AvatarKeyboardInputData in
|
||||
return AvatarKeyboardInputData(emoji: emoji, stickers: stickers)
|
||||
}
|
||||
return signal
|
||||
}
|
||||
|
||||
public init(context: AccountContext, inputData: Signal<AvatarKeyboardInputData, NoError>, peerType: PeerType, initialFileId: Int64?, initialBackgroundColors: [Int32]?) {
|
||||
self.context = context
|
||||
self.inputData = inputData
|
||||
|
||||
let componentReady = Promise<Bool>()
|
||||
super.init(context: context, component: AvatarEditorScreenComponent(context: context, ready: componentReady, peerType: peerType, initialFileId: initialFileId, initialBackgroundColors: initialBackgroundColors), navigationBarAppearance: .transparent)
|
||||
|
@ -7332,7 +7332,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
contentItemGroups: allItemGroups,
|
||||
itemLayoutType: .compact,
|
||||
itemContentUniqueId: nil,
|
||||
warpContentsOnEdges: isReactionSelection || isStatusSelection,
|
||||
warpContentsOnEdges: isReactionSelection || isStatusSelection || isProfilePhotoEmojiSelection || isGroupPhotoEmojiSelection,
|
||||
displaySearchWithPlaceholder: displaySearchWithPlaceholder,
|
||||
searchInitiallyHidden: searchInitiallyHidden,
|
||||
searchIsPlaceholderOnly: false,
|
||||
@ -7354,7 +7354,9 @@ public final class EmojiPagerContentComponent: Component {
|
||||
hasSearch: Bool,
|
||||
hasTrending: Bool,
|
||||
forceHasPremium: Bool,
|
||||
searchIsPlaceholderOnly: Bool = true
|
||||
searchIsPlaceholderOnly: Bool = true,
|
||||
isProfilePhotoEmojiSelection: Bool = false,
|
||||
isGroupPhotoEmojiSelection: Bool = false
|
||||
) -> Signal<EmojiPagerContentComponent, NoError> {
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
|
||||
let isPremiumDisabled = premiumConfiguration.isPremiumDisabled
|
||||
@ -7846,7 +7848,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
contentItemGroups: allItemGroups,
|
||||
itemLayoutType: .detailed,
|
||||
itemContentUniqueId: nil,
|
||||
warpContentsOnEdges: false,
|
||||
warpContentsOnEdges: isProfilePhotoEmojiSelection || isGroupPhotoEmojiSelection,
|
||||
displaySearchWithPlaceholder: hasSearch ? strings.StickersSearch_SearchStickersPlaceholder : nil,
|
||||
searchInitiallyHidden: true,
|
||||
searchIsPlaceholderOnly: searchIsPlaceholderOnly,
|
||||
|
@ -6699,21 +6699,37 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
|
||||
if peerId.namespace == Namespaces.Peer.CloudChannel {
|
||||
self.translationStateDisposable = combineLatest(
|
||||
queue: .mainQueue(),
|
||||
chatTranslationState(context: self.context, peerId: peerId),
|
||||
let baseLanguageCode = self.presentationData.strings.baseLanguageCode
|
||||
let isPremium = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|
||||
|> map { peer -> Bool in
|
||||
return peer?.isPremium ?? false
|
||||
}
|
||||
self.translationStateDisposable = (combineLatest(
|
||||
queue: .concurrentDefaultQueue(),
|
||||
isPremium,
|
||||
self.chatDisplayNode.historyNode.cachedPeerDataAndMessages
|
||||
).start(next: { [weak self] translationState, cachedDataAndMessages in
|
||||
) |> mapToSignal { isPremium, cachedDataAndMessages -> Signal<ChatPresentationTranslationState?, NoError> in
|
||||
let (cachedData, _) = cachedDataAndMessages
|
||||
var isHidden = false
|
||||
if let cachedData = cachedData as? CachedChannelData, cachedData.flags.contains(.translationHidden) {
|
||||
isHidden = true
|
||||
}
|
||||
|
||||
if isPremium && !isHidden {
|
||||
return chatTranslationState(context: context, peerId: peerId)
|
||||
|> map { translationState -> ChatPresentationTranslationState? in
|
||||
if let translationState, !translationState.fromLang.isEmpty {
|
||||
return ChatPresentationTranslationState(isEnabled: translationState.isEnabled, fromLang: translationState.fromLang, toLang: translationState.toLang ?? baseLanguageCode)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { [weak self] chatTranslationState in
|
||||
if let strongSelf = self {
|
||||
let (cachedData, _) = cachedDataAndMessages
|
||||
var isHidden = false
|
||||
if let cachedData = cachedData as? CachedChannelData, cachedData.flags.contains(.translationHidden) {
|
||||
isHidden = true
|
||||
}
|
||||
var chatTranslationState: ChatPresentationTranslationState?
|
||||
if let translationState, !isHidden && !translationState.fromLang.isEmpty {
|
||||
chatTranslationState = ChatPresentationTranslationState(isEnabled: translationState.isEnabled, fromLang: translationState.fromLang, toLang: translationState.toLang ?? strongSelf.presentationData.strings.baseLanguageCode)
|
||||
}
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: strongSelf.willAppear, interactive: strongSelf.willAppear, { state in
|
||||
return state.updatedTranslationState(chatTranslationState)
|
||||
})
|
||||
|
@ -1789,8 +1789,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
transition.updateFrame(node: self.inputPanelBackgroundNode, frame: apparentInputBackgroundFrame, beginWithCurrentState: true)
|
||||
|
||||
if let navigationBarBackgroundContent = self.navigationBarBackgroundContent {
|
||||
transition.updateFrame(node: navigationBarBackgroundContent, frame: CGRect(origin: .zero, size: CGSize(width: layout.size.width, height: navigationBarHeight + (titleAccessoryPanelBackgroundHeight ?? 0.0))), beginWithCurrentState: true)
|
||||
navigationBarBackgroundContent.update(rect: CGRect(origin: .zero, size: CGSize(width: layout.size.width, height: navigationBarHeight + (titleAccessoryPanelBackgroundHeight ?? 0.0))), within: layout.size, transition: transition)
|
||||
transition.updateFrame(node: navigationBarBackgroundContent, frame: CGRect(origin: .zero, size: CGSize(width: layout.size.width, height: navigationBarHeight + (titleAccessoryPanelBackgroundHeight ?? 0.0) + (translationPanelHeight ?? 0.0))), beginWithCurrentState: true)
|
||||
navigationBarBackgroundContent.update(rect: CGRect(origin: .zero, size: CGSize(width: layout.size.width, height: navigationBarHeight + (titleAccessoryPanelBackgroundHeight ?? 0.0) + (translationPanelHeight ?? 0.0))), within: layout.size, transition: transition)
|
||||
}
|
||||
|
||||
if let inputPanelBackgroundContent = self.inputPanelBackgroundContent {
|
||||
|
@ -234,7 +234,7 @@ private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLoca
|
||||
case .allButLast:
|
||||
displayHeader = listMessageDateHeaderId(timestamp: message.timestamp) != lastHeaderId
|
||||
}
|
||||
item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: message, selection: selection, displayHeader: displayHeader, hintIsLink: hintLinks, isGlobalSearchResult: isGlobalSearch)
|
||||
item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: message, translateToLanguage: associatedData.translateToLanguage, selection: selection, displayHeader: displayHeader, hintIsLink: hintLinks, isGlobalSearchResult: isGlobalSearch)
|
||||
}
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint)
|
||||
case let .MessageGroupEntry(_, messages, presentationData):
|
||||
@ -279,7 +279,7 @@ private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLoca
|
||||
case .allButLast:
|
||||
displayHeader = listMessageDateHeaderId(timestamp: message.timestamp) != lastHeaderId
|
||||
}
|
||||
item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: message, selection: selection, displayHeader: displayHeader, hintIsLink: hintLinks, isGlobalSearchResult: isGlobalSearch)
|
||||
item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: message, translateToLanguage: associatedData.translateToLanguage, selection: selection, displayHeader: displayHeader, hintIsLink: hintLinks, isGlobalSearchResult: isGlobalSearch)
|
||||
}
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint)
|
||||
case let .MessageGroupEntry(_, messages, presentationData):
|
||||
@ -1293,7 +1293,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
)
|
||||
|
||||
var translateToLanguage: String?
|
||||
if let translationState, translationState.isEnabled {
|
||||
if let translationState, isPremium && translationState.isEnabled {
|
||||
translateToLanguage = translationState.toLang ?? presentationData.strings.baseLanguageCode
|
||||
}
|
||||
|
||||
@ -3472,7 +3472,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
case .allButLast:
|
||||
displayHeader = listMessageDateHeaderId(timestamp: message.timestamp) != historyView.lastHeaderId
|
||||
}
|
||||
item = ListMessageItem(presentationData: presentationData, context: self.context, chatLocation: self.chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: self.controllerInteraction), message: message, selection: selection, displayHeader: displayHeader, hintIsLink: hintLinks, isGlobalSearchResult: isGlobalSearch)
|
||||
item = ListMessageItem(presentationData: presentationData, context: self.context, chatLocation: self.chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: self.controllerInteraction), message: message, translateToLanguage: associatedData.translateToLanguage, selection: selection, displayHeader: displayHeader, hintIsLink: hintLinks, isGlobalSearchResult: isGlobalSearch)
|
||||
}
|
||||
let updateItem = ListViewUpdateItem(index: index, previousIndex: index, item: item, directionHint: nil)
|
||||
|
||||
@ -3528,7 +3528,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
case .allButLast:
|
||||
displayHeader = listMessageDateHeaderId(timestamp: message.timestamp) != historyView.lastHeaderId
|
||||
}
|
||||
item = ListMessageItem(presentationData: presentationData, context: self.context, chatLocation: self.chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: self.controllerInteraction), message: message, selection: selection, displayHeader: displayHeader, hintIsLink: hintLinks, isGlobalSearchResult: isGlobalSearch)
|
||||
item = ListMessageItem(presentationData: presentationData, context: self.context, chatLocation: self.chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: self.controllerInteraction), message: message, translateToLanguage: associatedData.translateToLanguage, selection: selection, displayHeader: displayHeader, hintIsLink: hintLinks, isGlobalSearchResult: isGlobalSearch)
|
||||
}
|
||||
let updateItem = ListViewUpdateItem(index: index, previousIndex: index, item: item, directionHint: nil)
|
||||
self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [updateItem], options: [.AnimateInsertion], scrollToItem: nil, additionalScrollDistance: 0.0, updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
|
@ -264,6 +264,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe
|
||||
}, openForumThread: { _, _ in
|
||||
}, openStorageManagement: {
|
||||
}, openPasswordSetup: {
|
||||
}, openPremiumIntro: {
|
||||
})
|
||||
interaction.searchTextHighightState = searchQuery
|
||||
self.interaction = interaction
|
||||
|
@ -18,6 +18,7 @@ import PeerInfoUI
|
||||
import MapResourceToAvatarSizes
|
||||
import LegacyMediaPickerUI
|
||||
import TextFormat
|
||||
import AvatarEditorScreen
|
||||
|
||||
private struct CreateChannelArguments {
|
||||
let context: AccountContext
|
||||
@ -623,6 +624,9 @@ public func createChannelController(context: AccountContext, mode: CreateChannel
|
||||
}
|
||||
}
|
||||
|
||||
let keyboardInputData = Promise<AvatarKeyboardInputData>()
|
||||
keyboardInputData.set(AvatarEditorScreen.inputData(context: context, isGroup: true))
|
||||
|
||||
let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: stateValue.with({ $0.avatar }) != nil, hasViewButton: false, personalPhoto: false, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: false, title: nil, isSuggesting: false)!
|
||||
mixin.stickersContext = LegacyPaintStickersContext(context: context)
|
||||
let _ = currentAvatarMixin.swap(mixin)
|
||||
@ -633,6 +637,27 @@ public func createChannelController(context: AccountContext, mode: CreateChannel
|
||||
}))
|
||||
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
}
|
||||
mixin.requestAvatarEditor = { imageCompletion, videoCompletion in
|
||||
guard let imageCompletion, let videoCompletion else {
|
||||
return
|
||||
}
|
||||
let peerType: AvatarEditorScreen.PeerType
|
||||
if case .legacyGroup = peer {
|
||||
peerType = .group
|
||||
} else if case let .channel(channel) = peer {
|
||||
if case .group = channel.info {
|
||||
peerType = channel.flags.contains(.isForum) ? .forum : .group
|
||||
} else {
|
||||
peerType = .channel
|
||||
}
|
||||
} else {
|
||||
peerType = .user
|
||||
}
|
||||
let controller = AvatarEditorScreen(context: context, inputData: keyboardInputData.get(), peerType: peerType, initialFileId: nil, initialBackgroundColors: nil)
|
||||
controller.imageCompletion = imageCompletion
|
||||
controller.videoCompletion = videoCompletion
|
||||
pushControllerImpl?(controller)
|
||||
}
|
||||
mixin.didFinishWithImage = { image in
|
||||
if let image = image {
|
||||
completedChannelPhotoImpl(image)
|
||||
|
@ -30,6 +30,7 @@ import ContextUI
|
||||
import ChatTimerScreen
|
||||
import AsyncDisplayKit
|
||||
import TextFormat
|
||||
import AvatarEditorScreen
|
||||
|
||||
private struct CreateGroupArguments {
|
||||
let context: AccountContext
|
||||
@ -947,6 +948,9 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
|
||||
}
|
||||
}
|
||||
|
||||
let keyboardInputData = Promise<AvatarKeyboardInputData>()
|
||||
keyboardInputData.set(AvatarEditorScreen.inputData(context: context, isGroup: true))
|
||||
|
||||
let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: stateValue.with({ $0.avatar }) != nil, hasViewButton: false, personalPhoto: false, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: false, title: nil, isSuggesting: false)!
|
||||
mixin.stickersContext = LegacyPaintStickersContext(context: context)
|
||||
let _ = currentAvatarMixin.swap(mixin)
|
||||
@ -957,6 +961,27 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
|
||||
}))
|
||||
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
}
|
||||
mixin.requestAvatarEditor = { imageCompletion, videoCompletion in
|
||||
guard let imageCompletion, let videoCompletion else {
|
||||
return
|
||||
}
|
||||
let peerType: AvatarEditorScreen.PeerType
|
||||
if case .legacyGroup = peer {
|
||||
peerType = .group
|
||||
} else if case let .channel(channel) = peer {
|
||||
if case .group = channel.info {
|
||||
peerType = channel.flags.contains(.isForum) ? .forum : .group
|
||||
} else {
|
||||
peerType = .channel
|
||||
}
|
||||
} else {
|
||||
peerType = .user
|
||||
}
|
||||
let controller = AvatarEditorScreen(context: context, inputData: keyboardInputData.get(), peerType: peerType, initialFileId: nil, initialBackgroundColors: nil)
|
||||
controller.imageCompletion = imageCompletion
|
||||
controller.videoCompletion = videoCompletion
|
||||
pushImpl?(controller)
|
||||
}
|
||||
mixin.didFinishWithImage = { image in
|
||||
if let image = image {
|
||||
completedGroupPhotoImpl(image)
|
||||
|
@ -4859,7 +4859,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
self?.openStats()
|
||||
})))
|
||||
}
|
||||
|
||||
if cachedData.flags.contains(.translationHidden) {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ContextMenuTranslate, icon: { theme in
|
||||
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Translate"), color: theme.contextMenu.primaryColor)
|
||||
@ -7247,6 +7246,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
confirmationAction = nil
|
||||
}
|
||||
|
||||
let keyboardInputData = Promise<AvatarKeyboardInputData>()
|
||||
keyboardInputData.set(AvatarEditorScreen.inputData(context: strongSelf.context, isGroup: peer.id.namespace != Namespaces.Peer.CloudUser))
|
||||
|
||||
let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasDeleteButton, hasViewButton: false, personalPhoto: strongSelf.isSettings, isVideo: currentIsVideo, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: isForum, title: title, isSuggesting: [.custom, .suggest].contains(mode))!
|
||||
mixin.stickersContext = LegacyPaintStickersContext(context: strongSelf.context)
|
||||
let _ = strongSelf.currentAvatarMixin.swap(mixin)
|
||||
@ -7265,8 +7267,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
completion(nil)
|
||||
}
|
||||
}
|
||||
mixin.requestAvatarEditor = { [weak self] completion in
|
||||
guard let strongSelf = self, let completion else {
|
||||
mixin.requestAvatarEditor = { [weak self] imageCompletion, videoCompletion in
|
||||
guard let strongSelf = self, let imageCompletion, let videoCompletion else {
|
||||
return
|
||||
}
|
||||
let peerType: AvatarEditorScreen.PeerType
|
||||
@ -7274,15 +7276,16 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
peerType = .group
|
||||
} else if case let .channel(channel) = peer {
|
||||
if case .group = channel.info {
|
||||
peerType = .group
|
||||
peerType = channel.flags.contains(.isForum) ? .forum : .group
|
||||
} else {
|
||||
peerType = .channel
|
||||
}
|
||||
} else {
|
||||
peerType = .user
|
||||
}
|
||||
let controller = AvatarEditorScreen(context: strongSelf.context, peerType: peerType, initialFileId: emojiMarkup?.fileId, initialBackgroundColors: emojiMarkup?.backgroundColors)
|
||||
controller.completion = completion
|
||||
let controller = AvatarEditorScreen(context: strongSelf.context, inputData: keyboardInputData.get(), peerType: peerType, initialFileId: emojiMarkup?.fileId, initialBackgroundColors: emojiMarkup?.backgroundColors)
|
||||
controller.imageCompletion = imageCompletion
|
||||
controller.videoCompletion = videoCompletion
|
||||
(strongSelf.controller?.navigationController?.topViewController as? ViewController)?.push(controller)
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user