Various improvements

This commit is contained in:
Ilya Laktyushin 2023-01-21 15:56:57 +04:00
parent 9c99c04e64
commit 58c532b51e
44 changed files with 1270 additions and 358 deletions

View File

@ -8715,9 +8715,17 @@ Sorry for the inconvenience.";
"AvatarEditor.Stickers" = "Stickers"; "AvatarEditor.Stickers" = "Stickers";
"AvatarEditor.SwitchToEmoji" = "SWITCH TO EMOJI"; "AvatarEditor.SwitchToEmoji" = "SWITCH TO EMOJI";
"AvatarEditor.SwitchToStickers" = "SWITCH TO STICKERS"; "AvatarEditor.SwitchToStickers" = "SWITCH TO STICKERS";
"AvatarEditor.SetProfilePhoto" = "Set as Profile Photo"; "AvatarEditor.SetProfilePhoto" = "Set as Profile Photo";
"AvatarEditor.SetGroupPhoto" = "Set as Group Photo"; "AvatarEditor.SetGroupPhoto" = "Set as Group Photo";
"AvatarEditor.SetChannelPhoto" = "Set as Group Photo"; "AvatarEditor.SetChannelPhoto" = "Set as Group Photo";
"AvatarEditor.Set" = "Set"; "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.";

View File

@ -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 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 }, 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() gesture?.cancel()
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}) }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {})
interaction.isInlineMode = isInlineMode interaction.isInlineMode = isInlineMode
let items = (0 ..< 2).map { _ -> ChatListItem in let items = (0 ..< 2).map { _ -> ChatListItem in

View File

@ -2124,6 +2124,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}) })
}, openStorageManagement: { }, openStorageManagement: {
}, openPasswordSetup: { }, openPasswordSetup: {
}, openPremiumIntro: {
}) })
chatListInteraction.isSearchMode = true 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 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 }, 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() gesture?.cancel()
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}) }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {})
var isInlineMode = false var isInlineMode = false
if case .topics = key { if case .topics = key {
isInlineMode = false isInlineMode = false

View File

@ -1846,7 +1846,14 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
} }
let messageString: NSAttributedString let messageString: NSAttributedString
if !message.text.isEmpty && entities.count > 0 { 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 { } else if spoilers != nil || customEmojiRanges != nil {
let mutableString = NSMutableAttributedString(string: messageText, font: textFont, textColor: theme.messageTextColor) let mutableString = NSMutableAttributedString(string: messageText, font: textFont, textColor: theme.messageTextColor)
if let spoilers = spoilers { if let spoilers = spoilers {

View File

@ -94,6 +94,7 @@ public final class ChatListNodeInteraction {
let openForumThread: (EnginePeer.Id, Int64) -> Void let openForumThread: (EnginePeer.Id, Int64) -> Void
let openStorageManagement: () -> Void let openStorageManagement: () -> Void
let openPasswordSetup: () -> Void let openPasswordSetup: () -> Void
let openPremiumIntro: () -> Void
public var searchTextHighightState: String? public var searchTextHighightState: String?
var highlightedChatLocation: ChatListHighlightedLocation? var highlightedChatLocation: ChatListHighlightedLocation?
@ -137,7 +138,8 @@ public final class ChatListNodeInteraction {
present: @escaping (ViewController) -> Void, present: @escaping (ViewController) -> Void,
openForumThread: @escaping (EnginePeer.Id, Int64) -> Void, openForumThread: @escaping (EnginePeer.Id, Int64) -> Void,
openStorageManagement: @escaping () -> Void, openStorageManagement: @escaping () -> Void,
openPasswordSetup: @escaping () -> Void openPasswordSetup: @escaping () -> Void,
openPremiumIntro: @escaping () -> Void
) { ) {
self.activateSearch = activateSearch self.activateSearch = activateSearch
self.peerSelected = peerSelected self.peerSelected = peerSelected
@ -169,6 +171,7 @@ public final class ChatListNodeInteraction {
self.openForumThread = openForumThread self.openForumThread = openForumThread
self.openStorageManagement = openStorageManagement self.openStorageManagement = openStorageManagement
self.openPasswordSetup = openPasswordSetup self.openPasswordSetup = openPasswordSetup
self.openPremiumIntro = openPremiumIntro
} }
} }
@ -615,6 +618,8 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
nodeInteraction?.openStorageManagement() nodeInteraction?.openStorageManagement()
case .setupPassword: case .setupPassword:
nodeInteraction?.openPasswordSetup() nodeInteraction?.openPasswordSetup()
case .premiumUpgrade, .premiumAnnualDiscount:
nodeInteraction?.openPremiumIntro()
} }
}), directionHint: entry.directionHint) }), directionHint: entry.directionHint)
} }
@ -866,6 +871,8 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
nodeInteraction?.openStorageManagement() nodeInteraction?.openStorageManagement()
case .setupPassword: case .setupPassword:
nodeInteraction?.openPasswordSetup() nodeInteraction?.openPasswordSetup()
case .premiumUpgrade, .premiumAnnualDiscount:
nodeInteraction?.openPremiumIntro()
} }
}), directionHint: entry.directionHint) }), directionHint: entry.directionHint)
case .HeaderEntry: case .HeaderEntry:
@ -1363,15 +1370,24 @@ public final class ChatListNode: ListView {
guard let self else { guard let self else {
return return
} }
Queue.mainQueue().after(0.6) { [weak self] in
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.6, execute: { [weak self] in if let self {
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 { guard let self else {
return return
} }
let _ = dismissServerProvidedSuggestion(account: self.context.account, suggestion: .setupPassword).start() Queue.mainQueue().after(0.6) { [weak self] in
}) if let self {
let _ = dismissServerProvidedSuggestion(account: self.context.account, suggestion: .annualPremium).start()
let controller = self.context.sharedContext.makeSetupTwoFactorAuthController(context: self.context) let _ = dismissServerProvidedSuggestion(account: self.context.account, suggestion: .upgradePremium).start()
}
}
let controller = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .ads)
self.push?(controller) self.push?(controller)
}) })
nodeInteraction.isInlineMode = isInlineMode nodeInteraction.isInlineMode = isInlineMode
@ -1438,13 +1454,16 @@ public final class ChatListNode: ListView {
displayArchiveIntro = .single(false) displayArchiveIntro = .single(false)
} }
let suggestPasswordSetup: Signal<Bool, NoError> let suggestedChatListNotice: Signal<ChatListNotice?, NoError>
if case .chatList(groupId: .root) = location, chatListFilter == nil { if case .chatList(groupId: .root) = location, chatListFilter == nil {
suggestPasswordSetup = .single(false) |> then(combineLatest( suggestedChatListNotice = .single(nil)
|> then (
combineLatest(
getServerProvidedSuggestions(account: context.account), getServerProvidedSuggestions(account: context.account),
context.engine.auth.twoStepVerificationConfiguration() context.engine.auth.twoStepVerificationConfiguration()
) )
|> map { suggestions, configuration -> Bool in |> mapToSignal { suggestions, configuration -> Signal<ChatListNotice?, NoError> in
if suggestions.contains(.setupPassword) {
var notSet = false var notSet = false
switch configuration { switch configuration {
case let .notSet(pendingEmail): case let .notSet(pendingEmail):
@ -1454,14 +1473,46 @@ public final class ChatListNode: ListView {
case .set: case .set:
break break
} }
if !notSet { if notSet {
return false return .single(.setupPassword)
} }
return suggestions.contains(.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)
}
}
)
|> distinctUntilChanged |> distinctUntilChanged
} else { } else {
suggestPasswordSetup = .single(false) suggestedChatListNotice = .single(nil)
} }
let storageInfo: Signal<Double?, NoError> let storageInfo: Signal<Double?, NoError>
@ -1553,13 +1604,31 @@ public final class ChatListNode: ListView {
let currentPeerId: EnginePeer.Id = context.account.peerId let currentPeerId: EnginePeer.Id = context.account.peerId
let chatListNodeViewTransition = combineLatest(queue: viewProcessingQueue, hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestPasswordSetup, savedMessagesPeer, chatListViewUpdate, self.statePromise.get()) let chatListNodeViewTransition = combineLatest(
|> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestPasswordSetup, savedMessagesPeer, updateAndFilter, state) -> Signal<ChatListNodeListViewTransition, NoError> in 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 (update, filter) = updateAndFilter
let previousHideArchivedFolderByDefaultValue = previousHideArchivedFolderByDefault.swap(hideArchivedFolderByDefault) 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 isEmpty = true
var entries = rawEntries.filter { entry in var entries = rawEntries.filter { entry in
switch entry { switch entry {

View File

@ -49,6 +49,8 @@ public enum ChatListNodeEntryPromoInfo: Equatable {
enum ChatListNotice: Equatable { enum ChatListNotice: Equatable {
case clearStorage(sizeFraction: Double) case clearStorage(sizeFraction: Double)
case setupPassword case setupPassword
case premiumUpgrade(discount: Int32)
case premiumAnnualDiscount(discount: Int32)
} }
enum ChatListNodeEntry: Comparable, Identifiable { 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 result: [ChatListNodeEntry] = []
var pinnedIndexOffset: UInt16 = 0 var pinnedIndexOffset: UInt16 = 0
@ -666,10 +668,9 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState
if displayArchiveIntro { if displayArchiveIntro {
result.append(.ArchiveIntro(presentationData: state.presentationData)) result.append(.ArchiveIntro(presentationData: state.presentationData))
} }
if suggestPasswordSetup {
result.append(.Notice(presentationData: state.presentationData, notice: .setupPassword)) if let notice {
} else if let storageInfo { result.append(.Notice(presentationData: state.presentationData, notice: notice))
result.append(.Notice(presentationData: state.presentationData, notice: .clearStorage(sizeFraction: storageInfo)))
} }
result.append(.HeaderEntry) result.append(.HeaderEntry)

View File

@ -141,6 +141,26 @@ class ChatListStorageInfoItemNode: ListViewItemNode {
//TODO:localize //TODO:localize
titleString = NSAttributedString(string: "Protect Your Account", font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor) 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) 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))) let titleLayout = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0)))

View File

@ -21,21 +21,12 @@ public func reactionStaticImage(context: AccountContext, animation: TelegramMedi
return Signal { subscriber in return Signal { subscriber in
let fetchDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .image, reference: MediaResourceReference.standalone(resource: animation.resource)).start() 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? var customColor: UIColor?
if animation.isCustomTemplateEmoji { if animation.isCustomTemplateEmoji {
customColor = nil 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 { class AnimationCacheItemWriterImpl: AnimationCacheItemWriter {
let queue: Queue let queue: Queue

View File

@ -281,7 +281,7 @@ class StickerPickerScreen: ViewController {
return return
} }
let context = controller.context 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]) let _ = (context.account.postbox.combinedView(keys: [viewKey])
|> take(1) |> take(1)
|> deliverOnMainQueue).start(next: { views in |> deliverOnMainQueue).start(next: { views in

View File

@ -93,6 +93,7 @@ public final class HashtagSearchController: TelegramBaseController {
}, openForumThread: { _, _ in }, openForumThread: { _, _ in
}, openStorageManagement: { }, openStorageManagement: {
}, openPasswordSetup: { }, openPasswordSetup: {
}, openPremiumIntro: {
}) })
let previousSearchItems = Atomic<[ChatListSearchEntry]?>(value: nil) let previousSearchItems = Atomic<[ChatListSearchEntry]?>(value: nil)

View File

@ -154,10 +154,12 @@ public final class InAppPurchaseManager: NSObject {
private final class PaymentTransactionContext { private final class PaymentTransactionContext {
var state: SKPaymentTransactionState? var state: SKPaymentTransactionState?
var isUpgrade: Bool
var targetPeerId: PeerId? var targetPeerId: PeerId?
let subscriber: (TransactionState) -> Void 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.targetPeerId = targetPeerId
self.subscriber = subscriber 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 { if !self.canMakePayments {
return .fail(.cantMakePayments) return .fail(.cantMakePayments)
} }
@ -258,7 +260,7 @@ public final class InAppPurchaseManager: NSObject {
let disposable = MetaDisposable() let disposable = MetaDisposable()
self.stateQueue.async { self.stateQueue.async {
let paymentContext = PaymentTransactionContext(targetPeerId: targetPeerId, subscriber: { state in let paymentContext = PaymentTransactionContext(isUpgrade: isUpgrade, targetPeerId: targetPeerId, subscriber: { state in
switch state { switch state {
case let .purchased(transactionId), let .restored(transactionId): case let .purchased(transactionId), let .restored(transactionId):
if let transactionId = transactionId { if let transactionId = transactionId {
@ -305,6 +307,13 @@ public final class InAppPurchaseManager: NSObject {
} }
return signal 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 { extension InAppPurchaseManager: SKProductsRequestDelegate {
@ -370,6 +379,7 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver {
productId: transaction.payment.productIdentifier, productId: transaction.payment.productIdentifier,
content: PendingInAppPurchaseState( content: PendingInAppPurchaseState(
productId: transaction.payment.productIdentifier, productId: transaction.payment.productIdentifier,
isUpgrade: paymentContext.isUpgrade,
targetPeerId: paymentContext.targetPeerId targetPeerId: paymentContext.targetPeerId
) )
).start() ).start()
@ -431,7 +441,23 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver {
} }
} }
} else { } 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() let receiptData = getReceiptData() ?? Data()
@ -508,10 +534,12 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver {
private final class PendingInAppPurchaseState: Codable { private final class PendingInAppPurchaseState: Codable {
public let productId: String public let productId: String
public let isUpgrade: Bool
public let targetPeerId: PeerId? public let targetPeerId: PeerId?
public init(productId: String, targetPeerId: PeerId?) { public init(productId: String, isUpgrade: Bool, targetPeerId: PeerId?) {
self.productId = productId self.productId = productId
self.isUpgrade = isUpgrade
self.targetPeerId = targetPeerId self.targetPeerId = targetPeerId
} }
@ -519,6 +547,7 @@ private final class PendingInAppPurchaseState: Codable {
let container = try decoder.container(keyedBy: StringCodingKey.self) let container = try decoder.container(keyedBy: StringCodingKey.self)
self.productId = try container.decode(String.self, forKey: "productId") self.productId = try container.decode(String.self, forKey: "productId")
self.isUpgrade = try container.decodeIfPresent(Bool.self, forKey: "isUpgrade") ?? false
self.targetPeerId = (try container.decodeIfPresent(Int64.self, forKey: "targetPeerId")).flatMap { PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) } 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) var container = encoder.container(keyedBy: StringCodingKey.self)
try container.encode(self.productId, forKey: "productId") try container.encode(self.productId, forKey: "productId")
try container.encode(self.isUpgrade, forKey: "isUpgrade")
if let targetPeerId = self.targetPeerId { if let targetPeerId = self.targetPeerId {
try container.encode(targetPeerId.id._internalGetInt64Value(), forKey: "targetPeerId") try container.encode(targetPeerId.id._internalGetInt64Value(), forKey: "targetPeerId")
} }

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

View File

@ -26,7 +26,7 @@ typedef void (^TGMediaAvatarPresentImpl)(id<LegacyComponentsContext>, void (^)(U
@property (nonatomic, copy) void (^requestSearchController)(TGMediaAssetsController *); @property (nonatomic, copy) void (^requestSearchController)(TGMediaAssetsController *);
@property (nonatomic, copy) CGRect (^sourceRect)(void); @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; @property (nonatomic, strong) id<TGPhotoPaintStickersContext> stickersContext;

View File

@ -205,8 +205,26 @@
return; return;
[strongController dismissAnimated:true]; [strongController dismissAnimated:true];
if (strongSelf != nil) if (strongSelf != nil) {
strongSelf.requestAvatarEditor(^(UIImage *image, NSURL *asset, TGVideoEditAdjustments *adjustments, void (^commit)(void)) { 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; __strong TGMediaAvatarMenuMixin *strongSelf = weakSelf;
if (strongSelf == nil) if (strongSelf == nil)
return; return;
@ -225,6 +243,7 @@
commit(); commit();
} }
}); });
}
}]; }];
[itemViews addObject:viewItem]; [itemViews addObject:viewItem];
} }

View File

@ -49,6 +49,7 @@ public final class ListMessageItem: ListViewItem {
let chatLocation: ChatLocation let chatLocation: ChatLocation
let interaction: ListMessageItemInteraction let interaction: ListMessageItemInteraction
let message: Message? let message: Message?
let translateToLanguage: String?
public let selection: ChatHistoryMessageSelection public let selection: ChatHistoryMessageSelection
let hintIsLink: Bool let hintIsLink: Bool
let isGlobalSearchResult: Bool let isGlobalSearchResult: Bool
@ -61,12 +62,13 @@ public final class ListMessageItem: ListViewItem {
public let selectable: Bool = true 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.presentationData = presentationData
self.context = context self.context = context
self.chatLocation = chatLocation self.chatLocation = chatLocation
self.interaction = interaction self.interaction = interaction
self.message = message self.message = message
self.translateToLanguage = translateToLanguage
if let header = customHeader { if let header = customHeader {
self.header = header self.header = header
} else if displayHeader, let message = message { } else if displayHeader, let message = message {

View File

@ -424,7 +424,12 @@ public final class ListMessageSnippetItemNode: ListMessageNode {
let (messageTextUrl, _) = parseUrl(url: message.text, wasConcealed: false) let (messageTextUrl, _) = parseUrl(url: message.text, wasConcealed: false)
if messageTextUrl != rawUrlString, !item.isGlobalSearchResult { 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() let urlAttributedString = NSMutableAttributedString()
@ -439,8 +444,17 @@ public final class ListMessageSnippetItemNode: ListMessageNode {
} }
break loop break loop
case let .TextUrl(url): 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) 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 { if range.location + range.length > nsString.length {
range.location = max(0, nsString.length - range.length) range.location = max(0, nsString.length - range.length)
range.length = nsString.length - range.location range.length = nsString.length - range.location
@ -470,7 +484,7 @@ public final class ListMessageSnippetItemNode: ListMessageNode {
let (messageTextUrl, _) = parseUrl(url: message.text, wasConcealed: false) let (messageTextUrl, _) = parseUrl(url: message.text, wasConcealed: false)
if messageTextUrl != rawUrlString, !item.isGlobalSearchResult { 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() let urlAttributedString = NSMutableAttributedString()

View File

@ -484,6 +484,14 @@ private struct PremiumProduct: Equatable {
var pricePerMonth: String { var pricePerMonth: String {
return self.storeProduct.pricePerMonth(Int(self.months)) 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 { final class PremiumOptionComponent: CombinedComponent {
@ -815,10 +823,12 @@ private final class CheckComponent: Component {
final class SectionGroupComponent: Component { final class SectionGroupComponent: Component {
public final class Item: Equatable { public final class Item: Equatable {
public let content: AnyComponentWithIdentity<Empty> public let content: AnyComponentWithIdentity<Empty>
public let isEnabled: Bool
public let action: () -> Void 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.content = content
self.isEnabled = isEnabled
self.action = action self.action = action
} }
@ -826,7 +836,9 @@ final class SectionGroupComponent: Component {
if lhs.content != rhs.content { if lhs.content != rhs.content {
return false return false
} }
if lhs.isEnabled != rhs.isEnabled {
return false
}
return true return true
} }
} }
@ -931,6 +943,8 @@ final class SectionGroupComponent: Component {
environment: {}, environment: {},
containerSize: CGSize(width: size.width - sideInset, height: .greatestFiniteMagnitude) 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) 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)) 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 context: AccountContext
let source: PremiumSource let source: PremiumSource
let isPremium: Bool? let isPremium: Bool?
let justBought: Bool
let otherPeerName: String? let otherPeerName: String?
let products: [PremiumProduct]? let products: [PremiumProduct]?
let selectedProductId: String? let selectedProductId: String?
let validTransactionIds: [String]
let promoConfiguration: PremiumPromoConfiguration? let promoConfiguration: PremiumPromoConfiguration?
let present: (ViewController) -> Void let present: (ViewController) -> Void
let selectProduct: (String) -> Void let selectProduct: (String) -> Void
let buy: () -> Void let buy: () -> Void
let updateIsFocused: (Bool) -> 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.context = context
self.source = source self.source = source
self.isPremium = isPremium self.isPremium = isPremium
self.justBought = justBought
self.otherPeerName = otherPeerName self.otherPeerName = otherPeerName
self.products = products self.products = products
self.selectedProductId = selectedProductId self.selectedProductId = selectedProductId
self.validTransactionIds = validTransactionIds
self.promoConfiguration = promoConfiguration self.promoConfiguration = promoConfiguration
self.present = present self.present = present
self.selectProduct = selectProduct self.selectProduct = selectProduct
@ -1192,6 +1210,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
if lhs.isPremium != rhs.isPremium { if lhs.isPremium != rhs.isPremium {
return false return false
} }
if lhs.justBought != rhs.justBought {
return false
}
if lhs.otherPeerName != rhs.otherPeerName { if lhs.otherPeerName != rhs.otherPeerName {
return false return false
} }
@ -1201,6 +1222,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
if lhs.selectedProductId != rhs.selectedProductId { if lhs.selectedProductId != rhs.selectedProductId {
return false return false
} }
if lhs.validTransactionIds != rhs.validTransactionIds {
return false
}
if lhs.promoConfiguration != rhs.promoConfiguration { if lhs.promoConfiguration != rhs.promoConfiguration {
return false return false
} }
@ -1213,6 +1237,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
var products: [PremiumProduct]? var products: [PremiumProduct]?
var selectedProductId: String? var selectedProductId: String?
var validTransactionIds: [String] = []
var isPremium: Bool? 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 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) { init(context: AccountContext, source: PremiumSource) {
self.context = context self.context = context
@ -1317,6 +1354,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
let state = context.state let state = context.state
state.products = context.component.products state.products = context.component.products
state.selectedProductId = context.component.selectedProductId state.selectedProductId = context.component.selectedProductId
state.validTransactionIds = context.component.validTransactionIds
state.isPremium = context.component.isPremium state.isPremium = context.component.isPremium
let theme = environment.theme let theme = environment.theme
@ -1378,7 +1416,11 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
textString = strings.Premium_PersonalDescription textString = strings.Premium_PersonalDescription
} }
} else if context.component.isPremium == true { } else if context.component.isPremium == true {
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 textString = strings.Premium_SubscribedDescription
}
} else { } else {
textString = strings.Premium_Description textString = strings.Premium_Description
} }
@ -1398,11 +1440,22 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
lineSpacing: 0.2 lineSpacing: 0.2
), ),
environment: {}, environment: {},
availableSize: CGSize(width: availableWidth - sideInsets, height: 240.0), availableSize: CGSize(width: availableWidth - sideInsets - 8.0, height: 240.0),
transition: context.transition transition: .immediate
) )
context.add(text context.add(text
.position(CGPoint(x: size.width / 2.0, y: size.height + text.size.height / 2.0)) .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 += text.size.height
size.height += 21.0 size.height += 21.0
@ -1430,9 +1483,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
let updateIsFocused = context.component.updateIsFocused let updateIsFocused = context.component.updateIsFocused
let layoutOptions = { let layoutOptions = {
if state.isPremium == true { if let products = state.products, products.count > 1, state.isPremium == false || (!context.component.justBought && state.canUpgrade) {
} else if let products = state.products, products.count > 1 {
var optionsItems: [SectionGroupComponent.Item] = [] var optionsItems: [SectionGroupComponent.Item] = []
let gradientColors: [UIColor] = [ let gradientColors: [UIColor] = [
UIColor(rgb: 0x8e77ff), UIColor(rgb: 0x8e77ff),
@ -1447,11 +1498,11 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
shortestOptionPrice = (1, NSDecimalNumber(decimal: 1)) shortestOptionPrice = (1, NSDecimalNumber(decimal: 1))
} }
let currentProductMonths = state.products?.first(where: { $0.isCurrent })?.months ?? 0
var i = 0 var i = 0
for product in products { for product in products {
let giftTitle: String let giftTitle: String
let months = product.months
if product.id.hasSuffix(".monthly") { if product.id.hasSuffix(".monthly") {
giftTitle = strings.Premium_Monthly giftTitle = strings.Premium_Monthly
} else if product.id.hasSuffix(".semiannual") { } else if product.id.hasSuffix(".semiannual") {
@ -1460,7 +1511,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
giftTitle = strings.Premium_Annual 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 let discount: String
if discountValue > 0 { if discountValue > 0 {
discount = "-\(discountValue)%" discount = "-\(discountValue)%"
@ -1468,22 +1520,25 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
discount = "" 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 subtitle = ""
var pricePerMonth = product.price var pricePerMonth = product.price
if months > 1 { if product.months > 1 {
pricePerMonth = product.storeProduct.pricePerMonth(Int(months)) pricePerMonth = product.storeProduct.pricePerMonth(Int(product.months))
if discountValue > 0 { if discountValue > 0 {
subtitle = "**\(defaultPrice)** \(product.price)" subtitle = "**\(defaultPrice)** \(product.price)"
if months == 12 { if product.months == 12 {
subtitle = environment.strings.Premium_PricePerYear(subtitle).string subtitle = environment.strings.Premium_PricePerYear(subtitle).string
} }
} else { } else {
subtitle = product.price subtitle = product.price
} }
} }
if product.isCurrent {
subtitle = environment.strings.Premium_CurrentPlan
}
pricePerMonth = environment.strings.Premium_PricePerMonth(pricePerMonth).string pricePerMonth = environment.strings.Premium_PricePerMonth(pricePerMonth).string
optionsItems.append( optionsItems.append(
@ -1496,7 +1551,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
subtitle: subtitle, subtitle: subtitle,
labelPrice: pricePerMonth, labelPrice: pricePerMonth,
discount: discount, discount: discount,
selected: product.id == state.selectedProductId, selected: !product.isCurrent && product.id == state.selectedProductId,
primaryTextColor: textColor, primaryTextColor: textColor,
secondaryTextColor: subtitleColor, secondaryTextColor: subtitleColor,
accentColor: gradientColors[i], accentColor: gradientColors[i],
@ -1505,6 +1560,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
) )
) )
), ),
isEnabled: product.months > currentProductMonths,
action: { action: {
selectProduct(product.id) selectProduct(product.id)
} }
@ -1738,23 +1794,10 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
) )
} }
let termsText = termsText.update( let controller = environment.controller
component: MultilineTextComponent( let termsTapActionImpl: ([NSAttributedString.Key: Any]) -> Void = { attributes in
text: termsString,
horizontalAlignment: .natural,
maximumNumberOfLines: 0,
lineSpacing: 0.0,
highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.3),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { [weak environment] attributes, _ in
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String, if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String,
let controller = environment?.controller() as? PremiumIntroScreen, let navigationController = controller.navigationController as? NavigationController { let controller = controller() as? PremiumIntroScreen, let navigationController = controller.navigationController as? NavigationController {
if url.hasPrefix("https://apps.apple.com/account/subscriptions") { if url.hasPrefix("https://apps.apple.com/account/subscriptions") {
controller.context.sharedContext.applicationBindings.openSubscriptions() controller.context.sharedContext.applicationBindings.openSubscriptions()
} else if url.hasPrefix("https://") || url.hasPrefix("tg://") { } else if url.hasPrefix("https://") || url.hasPrefix("tg://") {
@ -1782,6 +1825,24 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
} }
} }
} }
let termsText = termsText.update(
component: MultilineTextComponent(
text: termsString,
horizontalAlignment: .natural,
maximumNumberOfLines: 0,
lineSpacing: 0.0,
highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.3),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { attributes, _ in
termsTapActionImpl(attributes)
}
), ),
environment: {}, environment: {},
availableSize: CGSize(width: availableWidth - sideInsets - textSideInset * 2.0, height: .greatestFiniteMagnitude), availableSize: CGSize(width: availableWidth - sideInsets - textSideInset * 2.0, height: .greatestFiniteMagnitude),
@ -1903,9 +1964,11 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
private(set) var products: [PremiumProduct]? private(set) var products: [PremiumProduct]?
private(set) var selectedProductId: String? private(set) var selectedProductId: String?
fileprivate var validTransactionIds: [String] = []
var isPremium: Bool? var isPremium: Bool?
var otherPeerName: String? var otherPeerName: String?
var justBought = false
let animationCache: AnimationCache let animationCache: AnimationCache
let animationRenderer: MultiAnimationRenderer 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 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) { init(context: AccountContext, source: PremiumSource, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void) {
self.context = context self.context = context
self.updateInProgress = updateInProgress self.updateInProgress = updateInProgress
@ -1939,6 +2014,8 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
super.init() super.init()
self.validTransactionIds = context.inAppPurchaseManager?.getValidTransactionIds() ?? []
let availableProducts: Signal<[InAppPurchaseManager.Product], NoError> let availableProducts: Signal<[InAppPurchaseManager.Product], NoError>
if let inAppPurchaseManager = context.inAppPurchaseManager { if let inAppPurchaseManager = context.inAppPurchaseManager {
availableProducts = inAppPurchaseManager.availableProducts availableProducts = inAppPurchaseManager.availableProducts
@ -1995,7 +2072,11 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
strongSelf.otherPeerName = otherPeerName strongSelf.otherPeerName = otherPeerName
if !hadProducts { if !hadProducts {
if let _ = products.first(where: { $0.isCurrent }) {
strongSelf.selectedProductId = strongSelf.products?.first?.id
} else {
strongSelf.selectedProductId = strongSelf.products?.last?.id strongSelf.selectedProductId = strongSelf.products?.last?.id
}
for (_, video) in promoConfiguration.videos { 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()) strongSelf.preloadDisposableSet.add(preloadVideoResource(postbox: context.account.postbox, userLocation: .other, userContentType: .video, resourceReference: .standalone(resource: video.resource), duration: 3.0).start())
@ -2037,6 +2118,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
let premiumProduct = self.products?.first(where: { $0.id == self.selectedProductId }), !self.inProgress else { let premiumProduct = self.products?.first(where: { $0.id == self.selectedProductId }), !self.inProgress else {
return return
} }
let isUpgrade = self.products?.first(where: { $0.isCurrent }) != nil
addAppLogEvent(postbox: self.context.account.postbox, type: "premium.promo_screen_accept") addAppLogEvent(postbox: self.context.account.postbox, type: "premium.promo_screen_accept")
@ -2044,11 +2126,11 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
self.updateInProgress(true) self.updateInProgress(true)
self.updated(transition: .immediate) self.updated(transition: .immediate)
let _ = (self.context.engine.payments.canPurchasePremium(purpose: .subscription) let _ = (self.context.engine.payments.canPurchasePremium(purpose: isUpgrade ? .upgrade : .subscription)
|> deliverOnMainQueue).start(next: { [weak self] available in |> deliverOnMainQueue).start(next: { [weak self] available in
if let strongSelf = self { if let strongSelf = self {
if available { if available {
strongSelf.paymentDisposable.set((inAppPurchaseManager.buyProduct(premiumProduct.storeProduct) strongSelf.paymentDisposable.set((inAppPurchaseManager.buyProduct(premiumProduct.storeProduct, isUpgrade: isUpgrade)
|> deliverOnMainQueue).start(next: { [weak self] status in |> deliverOnMainQueue).start(next: { [weak self] status in
if let strongSelf = self, case .purchased = status { if let strongSelf = self, case .purchased = status {
strongSelf.activationDisposable.set((strongSelf.context.account.postbox.peerView(id: strongSelf.context.account.peerId) strongSelf.activationDisposable.set((strongSelf.context.account.postbox.peerView(id: strongSelf.context.account.peerId)
@ -2085,6 +2167,8 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
strongSelf.updateInProgress(false) strongSelf.updateInProgress(false)
strongSelf.isPremium = true strongSelf.isPremium = true
strongSelf.justBought = true
strongSelf.updated(transition: .easeInOut(duration: 0.25)) strongSelf.updated(transition: .easeInOut(duration: 0.25))
strongSelf.completion() strongSelf.completion()
} }
@ -2226,7 +2310,11 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
} else if case .gift = context.component.source { } else if case .gift = context.component.source {
titleString = environment.strings.Premium_GiftedTitle titleString = environment.strings.Premium_GiftedTitle
} else if state.isPremium == true { } else if state.isPremium == true {
if !state.justBought && state.canUpgrade {
titleString = environment.strings.Premium_Title
} else {
titleString = environment.strings.Premium_SubscribedTitle titleString = environment.strings.Premium_SubscribedTitle
}
} else { } else {
titleString = environment.strings.Premium_Title titleString = environment.strings.Premium_Title
} }
@ -2357,7 +2445,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
let bottomPanelPadding: CGFloat = 12.0 let bottomPanelPadding: CGFloat = 12.0
let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding 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( let scrollContent = scrollContent.update(
component: ScrollComponent<EnvironmentType>( component: ScrollComponent<EnvironmentType>(
@ -2365,9 +2453,11 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
context: context.component.context, context: context.component.context,
source: context.component.source, source: context.component.source,
isPremium: state.isPremium, isPremium: state.isPremium,
justBought: state.justBought,
otherPeerName: state.otherPeerName, otherPeerName: state.otherPeerName,
products: state.products, products: state.products,
selectedProductId: state.selectedProductId, selectedProductId: state.selectedProductId,
validTransactionIds: state.validTransactionIds,
promoConfiguration: state.promoConfiguration, promoConfiguration: state.promoConfiguration,
present: context.component.present, present: context.component.present,
selectProduct: { [weak state] productId in 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))) .position(CGPoint(x: context.availableSize.width / 2.0, y: max(topInset + 160.0 - titleOffset, environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0)))
.scale(titleScale) .scale(titleScale)
.opacity(titleAlpha) .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 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 { } 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 sideInset: CGFloat = 16.0
let button = button.update( let button = button.update(
component: SolidRoundedButtonComponent( component: SolidRoundedButtonComponent(
title: state.isAnnual ? environment.strings.Premium_SubscribeForAnnual(state.price ?? "").string : environment.strings.Premium_SubscribeFor(state.price ?? "").string, title: buttonTitle,
theme: SolidRoundedButtonComponent.Theme( theme: SolidRoundedButtonComponent.Theme(
backgroundColor: UIColor(rgb: 0x8878ff), backgroundColor: UIColor(rgb: 0x8878ff),
backgroundColors: [ backgroundColors: [

View File

@ -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 }, 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 }, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel() 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) 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)

View File

@ -843,9 +843,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate
}, activateChatPreview: { _, _, _, gesture, _ in }, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel() gesture?.cancel()
}, present: { _ in }, present: { _ in
}, openForumThread: { _, _ in }, }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {})
openStorageManagement: {}, openPasswordSetup: {
})
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) 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( func makeChatListItem(

View File

@ -367,8 +367,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate {
}, activateChatPreview: { _, _, _, gesture, _ in }, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel() gesture?.cancel()
}, present: { _ in }, present: { _ in
}, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: { }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {} )
})
func makeChatListItem( func makeChatListItem(
peer: EnginePeer, peer: EnginePeer,

View File

@ -56,6 +56,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[2104224014] = { return Api.AttachMenuPeerType.parse_attachMenuPeerTypeSameBotPM($0) } dict[2104224014] = { return Api.AttachMenuPeerType.parse_attachMenuPeerTypeSameBotPM($0) }
dict[-1392388579] = { return Api.Authorization.parse_authorization($0) } dict[-1392388579] = { return Api.Authorization.parse_authorization($0) }
dict[-1896171181] = { return Api.AutoDownloadSettings.parse_autoDownloadSettings($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[-1065882623] = { return Api.AvailableReaction.parse_availableReaction($0) }
dict[-177732982] = { return Api.BankCardOpenUrl.parse_bankCardOpenUrl($0) } dict[-177732982] = { return Api.BankCardOpenUrl.parse_bankCardOpenUrl($0) }
dict[1527845466] = { return Api.BaseTheme.parse_baseThemeArctic($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[1536380829] = { return Api.InputChannel.parse_inputChannelFromMessage($0) }
dict[-1991004873] = { return Api.InputChatPhoto.parse_inputChatPhoto($0) } dict[-1991004873] = { return Api.InputChatPhoto.parse_inputChatPhoto($0) }
dict[480546647] = { return Api.InputChatPhoto.parse_inputChatPhotoEmpty($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[-1736378792] = { return Api.InputCheckPasswordSRP.parse_inputCheckPasswordEmpty($0) }
dict[-763367294] = { return Api.InputCheckPasswordSRP.parse_inputCheckPasswordSRP($0) } dict[-763367294] = { return Api.InputCheckPasswordSRP.parse_inputCheckPasswordSRP($0) }
dict[1968737087] = { return Api.InputClientProxy.parse_inputClientProxy($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[511092620] = { return Api.TopPeerCategory.parse_topPeerCategoryPhoneCalls($0) }
dict[-75283823] = { return Api.TopPeerCategoryPeers.parse_topPeerCategoryPeers($0) } dict[-75283823] = { return Api.TopPeerCategoryPeers.parse_topPeerCategoryPeers($0) }
dict[397910539] = { return Api.Update.parse_updateAttachMenuBots($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[-1177566067] = { return Api.Update.parse_updateBotCallbackQuery($0) }
dict[299870598] = { return Api.Update.parse_updateBotChatInviteRequester($0) } dict[299870598] = { return Api.Update.parse_updateBotChatInviteRequester($0) }
dict[1299263278] = { return Api.Update.parse_updateBotCommands($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[-496024847] = { return Api.UserStatus.parse_userStatusRecently($0) }
dict[-1274595769] = { return Api.Username.parse_username($0) } dict[-1274595769] = { return Api.Username.parse_username($0) }
dict[-567037804] = { return Api.VideoSize.parse_videoSize($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[-1539849235] = { return Api.WallPaper.parse_wallPaper($0) }
dict[-528465642] = { return Api.WallPaper.parse_wallPaperNoFile($0) } dict[-528465642] = { return Api.WallPaper.parse_wallPaperNoFile($0) }
dict[499236004] = { return Api.WallPaperSettings.parse_wallPaperSettings($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[-1389486888] = { return Api.account.AuthorizationForm.parse_authorizationForm($0) }
dict[1275039392] = { return Api.account.Authorizations.parse_authorizations($0) } dict[1275039392] = { return Api.account.Authorizations.parse_authorizations($0) }
dict[1674235686] = { return Api.account.AutoDownloadSettings.parse_autoDownloadSettings($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[1474462241] = { return Api.account.ContentSettings.parse_contentSettings($0) }
dict[731303195] = { return Api.account.EmailVerified.parse_emailVerified($0) } dict[731303195] = { return Api.account.EmailVerified.parse_emailVerified($0) }
dict[-507835039] = { return Api.account.EmailVerified.parse_emailVerifiedLogin($0) } dict[-507835039] = { return Api.account.EmailVerified.parse_emailVerifiedLogin($0) }
@ -1202,6 +1207,10 @@ public extension Api {
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.AutoDownloadSettings: case let _1 as Api.AutoDownloadSettings:
_1.serialize(buffer, boxed) _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: case let _1 as Api.AvailableReaction:
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.BankCardOpenUrl: case let _1 as Api.BankCardOpenUrl:
@ -1696,6 +1705,8 @@ public extension Api {
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.account.AutoDownloadSettings: case let _1 as Api.account.AutoDownloadSettings:
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.account.AutoSaveSettings:
_1.serialize(buffer, boxed)
case let _1 as Api.account.ContentSettings: case let _1 as Api.account.ContentSettings:
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.account.EmailVerified: case let _1 as Api.account.EmailVerified:

View File

@ -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 { public extension Api {
enum AvailableReaction: TypeConstructorDescription { 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?) 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
}
}
}
}

View File

@ -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 { public extension Api {
enum BotInlineMessage: TypeConstructorDescription { enum BotInlineMessage: TypeConstructorDescription {
case botInlineMessageMediaAuto(flags: Int32, message: String, entities: [Api.MessageEntity]?, replyMarkup: Api.ReplyMarkup?) case botInlineMessageMediaAuto(flags: Int32, message: String, entities: [Api.MessageEntity]?, replyMarkup: Api.ReplyMarkup?)

View File

@ -723,6 +723,7 @@ public extension Api {
public extension Api { public extension Api {
indirect enum Update: TypeConstructorDescription { indirect enum Update: TypeConstructorDescription {
case updateAttachMenuBots case updateAttachMenuBots
case updateAutoSaveSettings
case updateBotCallbackQuery(flags: Int32, queryId: Int64, userId: Int64, peer: Api.Peer, msgId: Int32, chatInstance: Int64, data: Buffer?, gameShortName: String?) 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 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]) case updateBotCommands(peer: Api.Peer, botId: Int64, commands: [Api.BotCommand])
@ -839,6 +840,12 @@ public extension Api {
buffer.appendInt32(397910539) buffer.appendInt32(397910539)
} }
break
case .updateAutoSaveSettings:
if boxed {
buffer.appendInt32(-335171433)
}
break break
case .updateBotCallbackQuery(let flags, let queryId, let userId, let peer, let msgId, let chatInstance, let data, let gameShortName): case .updateBotCallbackQuery(let flags, let queryId, let userId, let peer, let msgId, let chatInstance, let data, let gameShortName):
if boxed { if boxed {
@ -1785,6 +1792,8 @@ public extension Api {
switch self { switch self {
case .updateAttachMenuBots: case .updateAttachMenuBots:
return ("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): 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)]) 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): 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? { public static func parse_updateAttachMenuBots(_ reader: BufferReader) -> Update? {
return Api.Update.updateAttachMenuBots return Api.Update.updateAttachMenuBots
} }
public static func parse_updateAutoSaveSettings(_ reader: BufferReader) -> Update? {
return Api.Update.updateAutoSaveSettings
}
public static func parse_updateBotCallbackQuery(_ reader: BufferReader) -> Update? { public static func parse_updateBotCallbackQuery(_ reader: BufferReader) -> Update? {
var _1: Int32? var _1: Int32?
_1 = reader.readInt32() _1 = reader.readInt32()

View File

@ -917,7 +917,8 @@ public extension Api {
public extension Api { public extension Api {
enum VideoSize: TypeConstructorDescription { enum VideoSize: TypeConstructorDescription {
case videoSize(flags: Int32, type: String, w: Int32, h: Int32, size: Int32, videoStartTs: Double?) 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) { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self { switch self {
@ -932,11 +933,10 @@ public extension Api {
serializeInt32(size, buffer: buffer, boxed: false) serializeInt32(size, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {serializeDouble(videoStartTs!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 0) != 0 {serializeDouble(videoStartTs!, buffer: buffer, boxed: false)}
break break
case .videoSizeEmojiMarkup(let type, let emojiId, let backgroundColors): case .videoSizeEmojiMarkup(let emojiId, let backgroundColors):
if boxed { if boxed {
buffer.appendInt32(195933766) buffer.appendInt32(-128171716)
} }
serializeString(type, buffer: buffer, boxed: false)
serializeInt64(emojiId, buffer: buffer, boxed: false) serializeInt64(emojiId, buffer: buffer, boxed: false)
buffer.appendInt32(481674261) buffer.appendInt32(481674261)
buffer.appendInt32(Int32(backgroundColors.count)) buffer.appendInt32(Int32(backgroundColors.count))
@ -944,6 +944,18 @@ public extension Api {
serializeInt32(item, buffer: buffer, boxed: false) serializeInt32(item, buffer: buffer, boxed: false)
} }
break 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 { switch self {
case .videoSize(let flags, let type, let w, let h, let size, let videoStartTs): 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)]) 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): case .videoSizeEmojiMarkup(let emojiId, let backgroundColors):
return ("videoSizeEmojiMarkup", [("type", type as Any), ("emojiId", emojiId as Any), ("backgroundColors", backgroundColors as Any)]) 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? { public static func parse_videoSizeEmojiMarkup(_ reader: BufferReader) -> VideoSize? {
var _1: String? var _1: Int64?
_1 = parseString(reader) _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? var _2: Int64?
_2 = reader.readInt64() _2 = reader.readInt64()
var _3: [Int32]? var _3: [Int32]?
@ -995,7 +1027,7 @@ public extension Api {
let _c2 = _2 != nil let _c2 = _2 != nil
let _c3 = _3 != nil let _c3 = _3 != nil
if _c1 && _c2 && _c3 { 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 { else {
return nil return nil

View File

@ -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 { public extension Api.account {
enum ContentSettings: TypeConstructorDescription { enum ContentSettings: TypeConstructorDescription {
case contentSettings(flags: Int32) case contentSettings(flags: Int32)

View File

@ -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 { public extension Api.functions.account {
static func deleteSecureValue(types: [Api.SecureValueType]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) { static func deleteSecureValue(types: [Api.SecureValueType]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer() 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 { public extension Api.functions.account {
static func getChatThemes(hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.account.Themes>) { static func getChatThemes(hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.account.Themes>) {
let buffer = Buffer() 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 { public extension Api.functions.account {
static func saveRingtone(id: Api.InputDocument, unsave: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.account.SavedRingtone>) { static func saveRingtone(id: Api.InputDocument, unsave: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.account.SavedRingtone>) {
let buffer = Buffer() let buffer = Buffer()

View File

@ -272,7 +272,7 @@ public extension Api {
enum InputChatPhoto: TypeConstructorDescription { enum InputChatPhoto: TypeConstructorDescription {
case inputChatPhoto(id: Api.InputPhoto) case inputChatPhoto(id: Api.InputPhoto)
case inputChatPhotoEmpty 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) { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self { switch self {
@ -288,14 +288,15 @@ public extension Api {
} }
break 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 { if boxed {
buffer.appendInt32(-968723890) buffer.appendInt32(-1110593856)
} }
serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {file!.serialize(buffer, true)} 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 << 1) != 0 {video!.serialize(buffer, true)}
if Int(flags) & Int(1 << 2) != 0 {serializeDouble(videoStartTs!, buffer: buffer, boxed: false)} 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 break
} }
} }
@ -306,8 +307,8 @@ public extension Api {
return ("inputChatPhoto", [("id", id as Any)]) return ("inputChatPhoto", [("id", id as Any)])
case .inputChatPhotoEmpty: case .inputChatPhotoEmpty:
return ("inputChatPhotoEmpty", []) return ("inputChatPhotoEmpty", [])
case .inputChatUploadedPhoto(let flags, let file, let video, let videoStartTs): 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)]) 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? var _4: Double?
if Int(_1!) & Int(1 << 2) != 0 {_4 = reader.readDouble() } 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 _c1 = _1 != nil
let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil
let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil
let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil
if _c1 && _c2 && _c3 && _c4 { let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil
return Api.InputChatPhoto.inputChatUploadedPhoto(flags: _1!, file: _2, video: _3, videoStartTs: _4) if _c1 && _c2 && _c3 && _c4 && _c5 {
return Api.InputChatPhoto.inputChatUploadedPhoto(flags: _1!, file: _2, video: _3, videoStartTs: _4, videoEmojiMarkup: _5)
} }
else { else {
return nil return nil

View File

@ -172,7 +172,7 @@ func telegramMediaFileFromApiDocument(_ document: Api.Document) -> TelegramMedia
videoThumbnails.append(TelegramMediaFile.VideoThumbnail( videoThumbnails.append(TelegramMediaFile.VideoThumbnail(
dimensions: PixelDimensions(width: w, height: h), dimensions: PixelDimensions(width: w, height: h),
resource: resource)) resource: resource))
case .videoSizeEmojiMarkup: case .videoSizeEmojiMarkup, .videoSizeStickerMarkup:
break break
} }
} }

View File

@ -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()) 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)) 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) emojiMarkup = TelegramMediaImage.EmojiMarkup(fileId: emojiId, backgroundColors: backgroundColors)
case .videoSizeStickerMarkup:
break
} }
} }
} }

View File

@ -79,7 +79,6 @@ private extension PremiumPromoConfiguration {
var productOptions: [PremiumProductOption] = [] var productOptions: [PremiumProductOption] = []
for option in options { for option in options {
if case let .premiumSubscriptionOption(flags, transaction, months, currency, amount, botUrl, storeProduct) = option { 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)) 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))
} }

View File

@ -9,6 +9,8 @@ public enum ServerProvidedSuggestion: String {
case validatePhoneNumber = "VALIDATE_PHONE_NUMBER" case validatePhoneNumber = "VALIDATE_PHONE_NUMBER"
case validatePassword = "VALIDATE_PASSWORD" case validatePassword = "VALIDATE_PASSWORD"
case setupPassword = "SETUP_PASSWORD" case setupPassword = "SETUP_PASSWORD"
case upgradePremium = "PREMIUM_UPGRADE"
case annualPremium = "PREMIUM_ANNUAL"
} }
private var dismissedSuggestionsPromise = ValuePromise<[AccountRecordId: Set<ServerProvidedSuggestion>]>([:]) private var dismissedSuggestionsPromise = ValuePromise<[AccountRecordId: Set<ServerProvidedSuggestion>]>([:])

View File

@ -69,8 +69,13 @@ func _internal_canPurchasePremium(account: Account, purpose: AppStoreTransaction
switch purpose { switch purpose {
case .subscription, .restore, .upgrade: case .subscription, .restore, .upgrade:
var flags: Int32 = 0 var flags: Int32 = 0
if case .restore = purpose { switch purpose {
case .upgrade:
flags |= (1 << 1)
case .restore:
flags |= (1 << 0) flags |= (1 << 0)
default:
break
} }
purposeSignal = .single(.inputStorePaymentPremiumSubscription(flags: flags)) purposeSignal = .single(.inputStorePaymentPremiumSubscription(flags: flags))
case let .gift(peerId, currency, amount): case let .gift(peerId, currency, amount):

View File

@ -4,7 +4,6 @@ import SwiftSignalKit
import MtProtoKit import MtProtoKit
import TelegramApi import TelegramApi
public enum UpdatePeerPhotoStatus { public enum UpdatePeerPhotoStatus {
case progress(Float) case progress(Float)
case complete([TelegramMediaImageRepresentation]) case complete([TelegramMediaImageRepresentation])
@ -153,7 +152,7 @@ func _internal_updatePeerPhotoInternal(postbox: Postbox, network: Network, state
var videoEmojiMarkup: Api.VideoSize? var videoEmojiMarkup: Api.VideoSize?
if let fileId, let backgroundColors { if let fileId, let backgroundColors {
videoEmojiMarkup = .videoSizeEmojiMarkup(type: "e", emojiId: fileId, backgroundColors: backgroundColors) videoEmojiMarkup = .videoSizeEmojiMarkup(emojiId: fileId, backgroundColors: backgroundColors)
flags |= (1 << 4) 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()) 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)) videoRepresentations.append(TelegramMediaImage.VideoRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, startTimestamp: videoStartTs))
case .videoSizeEmojiMarkup: case .videoSizeEmojiMarkup, .videoSizeStickerMarkup:
break break
} }
} }
@ -280,9 +279,9 @@ func _internal_updatePeerPhotoInternal(postbox: Postbox, network: Network, state
let request: Signal<Api.Updates, MTRpcError> let request: Signal<Api.Updates, MTRpcError>
if let peer = peer as? TelegramGroup { 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) { } 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 { } else {
assertionFailure() assertionFailure()
request = .complete() request = .complete()

View File

@ -36,6 +36,8 @@ swift_library(
"//submodules/LegacyComponents:LegacyComponents", "//submodules/LegacyComponents:LegacyComponents",
"//submodules/DrawingUI:DrawingUI", "//submodules/DrawingUI:DrawingUI",
"//submodules/StickerResources:StickerResources", "//submodules/StickerResources:StickerResources",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -21,6 +21,8 @@ import GradientBackground
import LegacyComponents import LegacyComponents
import DrawingUI import DrawingUI
import SolidRoundedButtonComponent import SolidRoundedButtonComponent
import AnimationCache
import EmojiTextAttachmentView
enum AvatarBackground: Equatable { enum AvatarBackground: Equatable {
case gradient([UInt32]) case gradient([UInt32])
@ -56,7 +58,7 @@ private let defaultBackgrounds: [AvatarBackground] = [
.gradient([0x82b1ff, 0x665fff]), .gradient([0x82b1ff, 0x665fff]),
] ]
private struct KeyboardInputData: Equatable { public struct AvatarKeyboardInputData: Equatable {
var emoji: EmojiPagerContentComponent var emoji: EmojiPagerContentComponent
var stickers: EmojiPagerContentComponent? var stickers: EmojiPagerContentComponent?
@ -110,6 +112,7 @@ final class AvatarEditorScreenComponent: Component {
final class State: ComponentState { final class State: ComponentState {
let context: AccountContext let context: AccountContext
let ready: Promise<Bool>
var selectedBackground: AvatarBackground var selectedBackground: AvatarBackground
var selectedFile: TelegramMediaFile? var selectedFile: TelegramMediaFile?
@ -124,8 +127,9 @@ final class AvatarEditorScreenComponent: Component {
var isSearchActive: Bool = false 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.context = context
self.ready = ready
self.selectedBackground = defaultBackgrounds.first! self.selectedBackground = defaultBackgrounds.first!
self.previousColor = self.selectedBackground self.previousColor = self.selectedBackground
@ -153,6 +157,7 @@ final class AvatarEditorScreenComponent: Component {
func makeState() -> State { func makeState() -> State {
return State( return State(
context: self.context, context: self.context,
ready: self.ready,
initialFileId: self.initialFileId, initialFileId: self.initialFileId,
initialBackgroundColors: self.initialBackgroundColors initialBackgroundColors: self.initialBackgroundColors
) )
@ -187,7 +192,7 @@ final class AvatarEditorScreenComponent: Component {
private var controller: (() -> AvatarEditorScreen?)? private var controller: (() -> AvatarEditorScreen?)?
private var dataDisposable: Disposable? private var dataDisposable: Disposable?
private var data: KeyboardInputData? private var data: AvatarKeyboardInputData?
private let emojiSearchDisposable = MetaDisposable() private let emojiSearchDisposable = MetaDisposable()
private let emojiSearchResult = Promise<(groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable)?>(nil) private let emojiSearchResult = Promise<(groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable)?>(nil)
@ -232,7 +237,7 @@ final class AvatarEditorScreenComponent: Component {
self.emojiSearchDisposable.dispose() self.emojiSearchDisposable.dispose()
} }
private func updateData(_ data: KeyboardInputData) { private func updateData(_ data: AvatarKeyboardInputData) {
let wasEmpty = self.data == nil let wasEmpty = self.data == nil
self.data = data self.data = data
@ -240,6 +245,7 @@ final class AvatarEditorScreenComponent: Component {
self.state?.selectedFile = data.emoji.panelItemGroups.first?.items.first?.itemFile self.state?.selectedFile = data.emoji.panelItemGroups.first?.items.first?.itemFile
} }
self.state?.updated(transition: .immediate) self.state?.updated(transition: .immediate)
self.state?.ready.set(.single(true))
let updateSearchQuery: (String, String) -> Void = { [weak self] rawQuery, languageCode in let updateSearchQuery: (String, String) -> Void = { [weak self] rawQuery, languageCode in
guard let strongSelf = self, let context = strongSelf.state?.context else { guard let strongSelf = self, let context = strongSelf.state?.context else {
@ -424,7 +430,7 @@ final class AvatarEditorScreenComponent: Component {
return return
} }
let context = controller.context 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]) let _ = (context.account.postbox.combinedView(keys: [viewKey])
|> take(1) |> take(1)
|> deliverOnMainQueue).start(next: { views in |> deliverOnMainQueue).start(next: { views in
@ -738,45 +744,12 @@ final class AvatarEditorScreenComponent: Component {
self.keyboardContainerView.backgroundColor = environment.theme.list.itemBlocksBackgroundColor self.keyboardContainerView.backgroundColor = environment.theme.list.itemBlocksBackgroundColor
self.panelSeparatorView.backgroundColor = environment.theme.list.itemPlainSeparatorColor 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 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(), let signal = combineLatest(queue: .mainQueue(),
emojiItems, controller.inputData |> delay(0.01, queue: .mainQueue()),
stickerItems,
self.emojiSearchResult.get() self.emojiSearchResult.get()
) |> map { emoji, stickers, searchResult -> (KeyboardInputData, (groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable)?) in )
return (KeyboardInputData(emoji: emoji, stickers: stickers), searchResult)
}
self.dataDisposable = (signal self.dataDisposable = (signal
|> deliverOnMainQueue |> deliverOnMainQueue
).start(next: { [weak self, weak state] data, searchResult in ).start(next: { [weak self, weak state] data, searchResult in
@ -819,7 +792,7 @@ final class AvatarEditorScreenComponent: Component {
tapped: { [weak state] in tapped: { [weak state] in
if let state, !state.editingColor { if let state, !state.editingColor {
state.expanded = !state.expanded 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 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 let position = effectiveIsExpanded ? avatarPreviewSize.height / 2.0 : environment.navigationHeight + 10.0
transition.setBounds(view: previewView, bounds: CGRect(origin: .zero, size: avatarPreviewSize)) transition.setBounds(view: previewView, bounds: CGRect(origin: .zero, size: avatarPreviewSize))
@ -1095,6 +1068,9 @@ final class AvatarEditorScreenComponent: Component {
hideTopPanelUpdated: { [weak self] hideTopPanel, transition in hideTopPanelUpdated: { [weak self] hideTopPanel, transition in
if let strongSelf = self { if let strongSelf = self {
strongSelf.state?.isSearchActive = hideTopPanel strongSelf.state?.isSearchActive = hideTopPanel
if hideTopPanel {
strongSelf.state?.expanded = false
}
strongSelf.state?.updated(transition: transition) strongSelf.state?.updated(transition: transition)
} }
}, },
@ -1147,7 +1123,7 @@ final class AvatarEditorScreenComponent: Component {
switch component.peerType { switch component.peerType {
case .user: case .user:
buttonText = strings.AvatarEditor_SetProfilePhoto buttonText = strings.AvatarEditor_SetProfilePhoto
case .group: case .group, .forum:
buttonText = strings.AvatarEditor_SetGroupPhoto buttonText = strings.AvatarEditor_SetGroupPhoto
case .channel: case .channel:
buttonText = strings.AvatarEditor_SetChannelPhoto buttonText = strings.AvatarEditor_SetChannelPhoto
@ -1180,19 +1156,49 @@ final class AvatarEditorScreenComponent: Component {
return availableSize return availableSize
} }
private let queue = Queue()
func complete() { 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 return
} }
let size = CGSize(width: 1920.0, height: 1920.0) let context = controller.context
let image = state.selectedBackground.generateImage(size: size) 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
}
}
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 tempPath = NSTemporaryDirectory() + "/\(UInt64.random(in: 0 ... UInt64.max)).jpg"
let tempUrl = NSURL(fileURLWithPath: tempPath) as URL let tempUrl = NSURL(fileURLWithPath: tempPath) as URL
try? image.jpegData(compressionQuality: 1.0)?.write(to: tempUrl) try? backgroundImage.jpegData(compressionQuality: 0.8)?.write(to: tempUrl)
let entity = DrawingStickerEntity(content: .file(itemFile)) let drawingSize = CGSize(width: 1920.0, height: 1920.0)
entity.referenceDrawingSize = size let entity = DrawingStickerEntity(content: .file(file))
entity.position = CGPoint(x: size.width / 2.0, y: size.height / 2.0) entity.referenceDrawingSize = drawingSize
entity.position = CGPoint(x: drawingSize.width / 2.0, y: drawingSize.height / 2.0)
entity.scale = 3.3 entity.scale = 3.3
var documentId: Int64 = 0 var documentId: Int64 = 0
@ -1226,20 +1232,30 @@ final class AvatarEditorScreenComponent: Component {
) )
let preset: TGMediaVideoConversionPreset = TGMediaVideoConversionPresetProfileHigh let preset: TGMediaVideoConversionPreset = TGMediaVideoConversionPresetProfileHigh
let combinedImage = generateImage(previewView.bounds.size, contextGenerator: { size, context in let combinedImage = generateImage(size, contextGenerator: { size, context in
let bounds = CGRect(origin: .zero, size: size) let bounds = CGRect(origin: .zero, size: size)
if let cgImage = image.cgImage { if let cgImage = backgroundImage.cgImage {
context.draw(cgImage, in: bounds) context.draw(cgImage, in: bounds)
} }
context.translateBy(x: size.width / 2.0, y: size.height / 2.0) context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0) context.scaleBy(x: 0.67, y: 0.67)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
previewView.layer.render(in: context) if let cgImage = image.cgImage {
context.draw(cgImage, in: bounds)
}
}, opaque: false)! }, opaque: false)!
self.controller?()?.completion(combinedImage, tempUrl, TGVideoEditAdjustments(photoEditorValues: adjustments, preset: preset, documentId: documentId, colors: colors), { [weak self] in if entity.isAnimated {
self?.controller?()?.dismiss() 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 user
case group case group
case channel case channel
case forum
} }
fileprivate let context: AccountContext fileprivate let context: AccountContext
fileprivate let inputData: Signal<AvatarKeyboardInputData, NoError>
private let readyValue = Promise<Bool>() private let readyValue = Promise<Bool>()
override public var ready: Promise<Bool> { override public var ready: Promise<Bool> {
return self.readyValue 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.context = context
self.inputData = inputData
let componentReady = Promise<Bool>() let componentReady = Promise<Bool>()
super.init(context: context, component: AvatarEditorScreenComponent(context: context, ready: componentReady, peerType: peerType, initialFileId: initialFileId, initialBackgroundColors: initialBackgroundColors), navigationBarAppearance: .transparent) super.init(context: context, component: AvatarEditorScreenComponent(context: context, ready: componentReady, peerType: peerType, initialFileId: initialFileId, initialBackgroundColors: initialBackgroundColors), navigationBarAppearance: .transparent)

View File

@ -7332,7 +7332,7 @@ public final class EmojiPagerContentComponent: Component {
contentItemGroups: allItemGroups, contentItemGroups: allItemGroups,
itemLayoutType: .compact, itemLayoutType: .compact,
itemContentUniqueId: nil, itemContentUniqueId: nil,
warpContentsOnEdges: isReactionSelection || isStatusSelection, warpContentsOnEdges: isReactionSelection || isStatusSelection || isProfilePhotoEmojiSelection || isGroupPhotoEmojiSelection,
displaySearchWithPlaceholder: displaySearchWithPlaceholder, displaySearchWithPlaceholder: displaySearchWithPlaceholder,
searchInitiallyHidden: searchInitiallyHidden, searchInitiallyHidden: searchInitiallyHidden,
searchIsPlaceholderOnly: false, searchIsPlaceholderOnly: false,
@ -7354,7 +7354,9 @@ public final class EmojiPagerContentComponent: Component {
hasSearch: Bool, hasSearch: Bool,
hasTrending: Bool, hasTrending: Bool,
forceHasPremium: Bool, forceHasPremium: Bool,
searchIsPlaceholderOnly: Bool = true searchIsPlaceholderOnly: Bool = true,
isProfilePhotoEmojiSelection: Bool = false,
isGroupPhotoEmojiSelection: Bool = false
) -> Signal<EmojiPagerContentComponent, NoError> { ) -> Signal<EmojiPagerContentComponent, NoError> {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let isPremiumDisabled = premiumConfiguration.isPremiumDisabled let isPremiumDisabled = premiumConfiguration.isPremiumDisabled
@ -7846,7 +7848,7 @@ public final class EmojiPagerContentComponent: Component {
contentItemGroups: allItemGroups, contentItemGroups: allItemGroups,
itemLayoutType: .detailed, itemLayoutType: .detailed,
itemContentUniqueId: nil, itemContentUniqueId: nil,
warpContentsOnEdges: false, warpContentsOnEdges: isProfilePhotoEmojiSelection || isGroupPhotoEmojiSelection,
displaySearchWithPlaceholder: hasSearch ? strings.StickersSearch_SearchStickersPlaceholder : nil, displaySearchWithPlaceholder: hasSearch ? strings.StickersSearch_SearchStickersPlaceholder : nil,
searchInitiallyHidden: true, searchInitiallyHidden: true,
searchIsPlaceholderOnly: searchIsPlaceholderOnly, searchIsPlaceholderOnly: searchIsPlaceholderOnly,

View File

@ -6699,21 +6699,37 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
if peerId.namespace == Namespaces.Peer.CloudChannel { if peerId.namespace == Namespaces.Peer.CloudChannel {
self.translationStateDisposable = combineLatest( let baseLanguageCode = self.presentationData.strings.baseLanguageCode
queue: .mainQueue(), let isPremium = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
chatTranslationState(context: self.context, peerId: peerId), |> map { peer -> Bool in
return peer?.isPremium ?? false
}
self.translationStateDisposable = (combineLatest(
queue: .concurrentDefaultQueue(),
isPremium,
self.chatDisplayNode.historyNode.cachedPeerDataAndMessages self.chatDisplayNode.historyNode.cachedPeerDataAndMessages
).start(next: { [weak self] translationState, cachedDataAndMessages in ) |> mapToSignal { isPremium, cachedDataAndMessages -> Signal<ChatPresentationTranslationState?, NoError> in
if let strongSelf = self {
let (cachedData, _) = cachedDataAndMessages let (cachedData, _) = cachedDataAndMessages
var isHidden = false var isHidden = false
if let cachedData = cachedData as? CachedChannelData, cachedData.flags.contains(.translationHidden) { if let cachedData = cachedData as? CachedChannelData, cachedData.flags.contains(.translationHidden) {
isHidden = true isHidden = true
} }
var chatTranslationState: ChatPresentationTranslationState?
if let translationState, !isHidden && !translationState.fromLang.isEmpty { if isPremium && !isHidden {
chatTranslationState = ChatPresentationTranslationState(isEnabled: translationState.isEnabled, fromLang: translationState.fromLang, toLang: translationState.toLang ?? strongSelf.presentationData.strings.baseLanguageCode) 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 {
strongSelf.updateChatPresentationInterfaceState(animated: strongSelf.willAppear, interactive: strongSelf.willAppear, { state in strongSelf.updateChatPresentationInterfaceState(animated: strongSelf.willAppear, interactive: strongSelf.willAppear, { state in
return state.updatedTranslationState(chatTranslationState) return state.updatedTranslationState(chatTranslationState)
}) })

View File

@ -1789,8 +1789,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
transition.updateFrame(node: self.inputPanelBackgroundNode, frame: apparentInputBackgroundFrame, beginWithCurrentState: true) transition.updateFrame(node: self.inputPanelBackgroundNode, frame: apparentInputBackgroundFrame, beginWithCurrentState: true)
if let navigationBarBackgroundContent = self.navigationBarBackgroundContent { 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) 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))), within: layout.size, transition: transition) 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 { if let inputPanelBackgroundContent = self.inputPanelBackgroundContent {

View File

@ -234,7 +234,7 @@ private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLoca
case .allButLast: case .allButLast:
displayHeader = listMessageDateHeaderId(timestamp: message.timestamp) != lastHeaderId 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) return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint)
case let .MessageGroupEntry(_, messages, presentationData): case let .MessageGroupEntry(_, messages, presentationData):
@ -279,7 +279,7 @@ private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLoca
case .allButLast: case .allButLast:
displayHeader = listMessageDateHeaderId(timestamp: message.timestamp) != lastHeaderId 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) return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint)
case let .MessageGroupEntry(_, messages, presentationData): case let .MessageGroupEntry(_, messages, presentationData):
@ -1293,7 +1293,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
) )
var translateToLanguage: String? var translateToLanguage: String?
if let translationState, translationState.isEnabled { if let translationState, isPremium && translationState.isEnabled {
translateToLanguage = translationState.toLang ?? presentationData.strings.baseLanguageCode translateToLanguage = translationState.toLang ?? presentationData.strings.baseLanguageCode
} }
@ -3472,7 +3472,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
case .allButLast: case .allButLast:
displayHeader = listMessageDateHeaderId(timestamp: message.timestamp) != historyView.lastHeaderId 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) let updateItem = ListViewUpdateItem(index: index, previousIndex: index, item: item, directionHint: nil)
@ -3528,7 +3528,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
case .allButLast: case .allButLast:
displayHeader = listMessageDateHeaderId(timestamp: message.timestamp) != historyView.lastHeaderId 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) 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 }) self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [updateItem], options: [.AnimateInsertion], scrollToItem: nil, additionalScrollDistance: 0.0, updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })

View File

@ -264,6 +264,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe
}, openForumThread: { _, _ in }, openForumThread: { _, _ in
}, openStorageManagement: { }, openStorageManagement: {
}, openPasswordSetup: { }, openPasswordSetup: {
}, openPremiumIntro: {
}) })
interaction.searchTextHighightState = searchQuery interaction.searchTextHighightState = searchQuery
self.interaction = interaction self.interaction = interaction

View File

@ -18,6 +18,7 @@ import PeerInfoUI
import MapResourceToAvatarSizes import MapResourceToAvatarSizes
import LegacyMediaPickerUI import LegacyMediaPickerUI
import TextFormat import TextFormat
import AvatarEditorScreen
private struct CreateChannelArguments { private struct CreateChannelArguments {
let context: AccountContext 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)! 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) mixin.stickersContext = LegacyPaintStickersContext(context: context)
let _ = currentAvatarMixin.swap(mixin) let _ = currentAvatarMixin.swap(mixin)
@ -633,6 +637,27 @@ public func createChannelController(context: AccountContext, mode: CreateChannel
})) }))
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) 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 mixin.didFinishWithImage = { image in
if let image = image { if let image = image {
completedChannelPhotoImpl(image) completedChannelPhotoImpl(image)

View File

@ -30,6 +30,7 @@ import ContextUI
import ChatTimerScreen import ChatTimerScreen
import AsyncDisplayKit import AsyncDisplayKit
import TextFormat import TextFormat
import AvatarEditorScreen
private struct CreateGroupArguments { private struct CreateGroupArguments {
let context: AccountContext 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)! 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) mixin.stickersContext = LegacyPaintStickersContext(context: context)
let _ = currentAvatarMixin.swap(mixin) let _ = currentAvatarMixin.swap(mixin)
@ -957,6 +961,27 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
})) }))
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) 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 mixin.didFinishWithImage = { image in
if let image = image { if let image = image {
completedGroupPhotoImpl(image) completedGroupPhotoImpl(image)

View File

@ -4859,7 +4859,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
self?.openStats() self?.openStats()
}))) })))
} }
if cachedData.flags.contains(.translationHidden) { if cachedData.flags.contains(.translationHidden) {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ContextMenuTranslate, icon: { theme in items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ContextMenuTranslate, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Translate"), color: theme.contextMenu.primaryColor) generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Translate"), color: theme.contextMenu.primaryColor)
@ -7247,6 +7246,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
confirmationAction = nil 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))! 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) mixin.stickersContext = LegacyPaintStickersContext(context: strongSelf.context)
let _ = strongSelf.currentAvatarMixin.swap(mixin) let _ = strongSelf.currentAvatarMixin.swap(mixin)
@ -7265,8 +7267,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
completion(nil) completion(nil)
} }
} }
mixin.requestAvatarEditor = { [weak self] completion in mixin.requestAvatarEditor = { [weak self] imageCompletion, videoCompletion in
guard let strongSelf = self, let completion else { guard let strongSelf = self, let imageCompletion, let videoCompletion else {
return return
} }
let peerType: AvatarEditorScreen.PeerType let peerType: AvatarEditorScreen.PeerType
@ -7274,15 +7276,16 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
peerType = .group peerType = .group
} else if case let .channel(channel) = peer { } else if case let .channel(channel) = peer {
if case .group = channel.info { if case .group = channel.info {
peerType = .group peerType = channel.flags.contains(.isForum) ? .forum : .group
} else { } else {
peerType = .channel peerType = .channel
} }
} else { } else {
peerType = .user peerType = .user
} }
let controller = AvatarEditorScreen(context: strongSelf.context, peerType: peerType, initialFileId: emojiMarkup?.fileId, initialBackgroundColors: emojiMarkup?.backgroundColors) let controller = AvatarEditorScreen(context: strongSelf.context, inputData: keyboardInputData.get(), peerType: peerType, initialFileId: emojiMarkup?.fileId, initialBackgroundColors: emojiMarkup?.backgroundColors)
controller.completion = completion controller.imageCompletion = imageCompletion
controller.videoCompletion = videoCompletion
(strongSelf.controller?.navigationController?.topViewController as? ViewController)?.push(controller) (strongSelf.controller?.navigationController?.topViewController as? ViewController)?.push(controller)
} }