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.SwitchToEmoji" = "SWITCH TO EMOJI";
"AvatarEditor.SwitchToStickers" = "SWITCH TO STICKERS";
"AvatarEditor.SetProfilePhoto" = "Set as Profile Photo";
"AvatarEditor.SetGroupPhoto" = "Set as Group Photo";
"AvatarEditor.SetChannelPhoto" = "Set as Group Photo";
"AvatarEditor.Set" = "Set";
"Premium.UpgradeDescription" = "Your current **Telegram Premium** plan can be upgraded at a **discount**.";
"Premium.CurrentPlan" = "your current plan";
"Premium.UpgradeFor" = "Upgrade for %@ / month";
"Premium.UpgradeForAnnual" = "Upgrade for %@ / year";
"ChatList.PremiumAnnualDiscountTitle" = "Telegram Premium with a discount of %@";
"ChatList.PremiumAnnualDiscountText" = "Sign up for the annual payment plan for Telegram Premium now to get the discount.";
"ChatList.PremiumAnnualUpgradeTitle" = "Save on your subscription up to %@";
"ChatList.PremiumAnnualUpgradeText" = "Upgrade to the annual payment plan for Telegram Premium to enjoy the discount.";

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
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel()
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {})
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {})
interaction.isInlineMode = isInlineMode
let items = (0 ..< 2).map { _ -> ChatListItem in

View File

@ -2124,6 +2124,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
})
}, openStorageManagement: {
}, openPasswordSetup: {
}, openPremiumIntro: {
})
chatListInteraction.isSearchMode = true
@ -3325,7 +3326,7 @@ private final class ChatListSearchShimmerNode: ASDisplayNode {
let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel()
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {})
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {})
var isInlineMode = false
if case .topics = key {
isInlineMode = false

View File

@ -1846,7 +1846,14 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
}
let messageString: NSAttributedString
if !message.text.isEmpty && entities.count > 0 {
messageString = stringWithAppliedEntities(trimToLineCount(message.text, lineCount: authorAttributedString == nil ? 2 : 1), entities: entities, baseColor: theme.messageTextColor, linkColor: theme.messageTextColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: italicTextFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message._asMessage())
var messageText = message.text
var entities = entities
if let translation = message.attributes.first(where: { $0 is TranslationMessageAttribute }) as? TranslationMessageAttribute, !translation.text.isEmpty {
messageText = translation.text
entities = translation.entities
}
messageString = stringWithAppliedEntities(trimToLineCount(messageText, lineCount: authorAttributedString == nil ? 2 : 1), entities: entities, baseColor: theme.messageTextColor, linkColor: theme.messageTextColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: italicTextFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message._asMessage())
} else if spoilers != nil || customEmojiRanges != nil {
let mutableString = NSMutableAttributedString(string: messageText, font: textFont, textColor: theme.messageTextColor)
if let spoilers = spoilers {

View File

@ -94,6 +94,7 @@ public final class ChatListNodeInteraction {
let openForumThread: (EnginePeer.Id, Int64) -> Void
let openStorageManagement: () -> Void
let openPasswordSetup: () -> Void
let openPremiumIntro: () -> Void
public var searchTextHighightState: String?
var highlightedChatLocation: ChatListHighlightedLocation?
@ -137,7 +138,8 @@ public final class ChatListNodeInteraction {
present: @escaping (ViewController) -> Void,
openForumThread: @escaping (EnginePeer.Id, Int64) -> Void,
openStorageManagement: @escaping () -> Void,
openPasswordSetup: @escaping () -> Void
openPasswordSetup: @escaping () -> Void,
openPremiumIntro: @escaping () -> Void
) {
self.activateSearch = activateSearch
self.peerSelected = peerSelected
@ -169,6 +171,7 @@ public final class ChatListNodeInteraction {
self.openForumThread = openForumThread
self.openStorageManagement = openStorageManagement
self.openPasswordSetup = openPasswordSetup
self.openPremiumIntro = openPremiumIntro
}
}
@ -615,6 +618,8 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
nodeInteraction?.openStorageManagement()
case .setupPassword:
nodeInteraction?.openPasswordSetup()
case .premiumUpgrade, .premiumAnnualDiscount:
nodeInteraction?.openPremiumIntro()
}
}), directionHint: entry.directionHint)
}
@ -866,6 +871,8 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
nodeInteraction?.openStorageManagement()
case .setupPassword:
nodeInteraction?.openPasswordSetup()
case .premiumUpgrade, .premiumAnnualDiscount:
nodeInteraction?.openPremiumIntro()
}
}), directionHint: entry.directionHint)
case .HeaderEntry:
@ -1363,16 +1370,25 @@ public final class ChatListNode: ListView {
guard let self else {
return
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.6, execute: { [weak self] in
guard let self else {
return
Queue.mainQueue().after(0.6) { [weak self] in
if let self {
let _ = dismissServerProvidedSuggestion(account: self.context.account, suggestion: .setupPassword).start()
}
let _ = dismissServerProvidedSuggestion(account: self.context.account, suggestion: .setupPassword).start()
})
}
let controller = self.context.sharedContext.makeSetupTwoFactorAuthController(context: self.context)
self.push?(controller)
}, openPremiumIntro: { [weak self] in
guard let self else {
return
}
Queue.mainQueue().after(0.6) { [weak self] in
if let self {
let _ = dismissServerProvidedSuggestion(account: self.context.account, suggestion: .annualPremium).start()
let _ = dismissServerProvidedSuggestion(account: self.context.account, suggestion: .upgradePremium).start()
}
}
let controller = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .ads)
self.push?(controller)
})
nodeInteraction.isInlineMode = isInlineMode
@ -1437,31 +1453,66 @@ public final class ChatListNode: ListView {
} else {
displayArchiveIntro = .single(false)
}
let suggestPasswordSetup: Signal<Bool, NoError>
let suggestedChatListNotice: Signal<ChatListNotice?, NoError>
if case .chatList(groupId: .root) = location, chatListFilter == nil {
suggestPasswordSetup = .single(false) |> then(combineLatest(
getServerProvidedSuggestions(account: context.account),
context.engine.auth.twoStepVerificationConfiguration()
)
|> map { suggestions, configuration -> Bool in
var notSet = false
switch configuration {
case let .notSet(pendingEmail):
if pendingEmail == nil {
notSet = true
suggestedChatListNotice = .single(nil)
|> then (
combineLatest(
getServerProvidedSuggestions(account: context.account),
context.engine.auth.twoStepVerificationConfiguration()
)
|> mapToSignal { suggestions, configuration -> Signal<ChatListNotice?, NoError> in
if suggestions.contains(.setupPassword) {
var notSet = false
switch configuration {
case let .notSet(pendingEmail):
if pendingEmail == nil {
notSet = true
}
case .set:
break
}
if notSet {
return .single(.setupPassword)
}
}
if suggestions.contains(.annualPremium) || suggestions.contains(.upgradePremium), let inAppPurchaseManager = context.inAppPurchaseManager {
return inAppPurchaseManager.availableProducts
|> map { products -> ChatListNotice? in
if products.count > 1 {
let shortestOptionPrice: (Int64, NSDecimalNumber)
if let product = products.first(where: { $0.id.hasSuffix(".monthly") }) {
shortestOptionPrice = (Int64(Float(product.priceCurrencyAndAmount.amount)), product.priceValue)
} else {
shortestOptionPrice = (1, NSDecimalNumber(decimal: 1))
}
for product in products {
if product.id.hasSuffix(".annual") {
let fraction = Float(product.priceCurrencyAndAmount.amount) / Float(12) / Float(shortestOptionPrice.0)
let discount = Int32(round((1.0 - fraction) * 20.0) * 5.0)
if suggestions.contains(.annualPremium) {
return .premiumAnnualDiscount(discount: discount)
} else if suggestions.contains(.upgradePremium) {
return .premiumUpgrade(discount: discount)
}
break
}
}
return nil
} else {
return nil
}
}
} else {
return .single(nil)
}
case .set:
break
}
if !notSet {
return false
}
return suggestions.contains(.setupPassword)
})
)
|> distinctUntilChanged
} else {
suggestPasswordSetup = .single(false)
suggestedChatListNotice = .single(nil)
}
let storageInfo: Signal<Double?, NoError>
@ -1553,13 +1604,31 @@ public final class ChatListNode: ListView {
let currentPeerId: EnginePeer.Id = context.account.peerId
let chatListNodeViewTransition = combineLatest(queue: viewProcessingQueue, hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestPasswordSetup, savedMessagesPeer, chatListViewUpdate, self.statePromise.get())
|> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestPasswordSetup, savedMessagesPeer, updateAndFilter, state) -> Signal<ChatListNodeListViewTransition, NoError> in
let chatListNodeViewTransition = combineLatest(
queue: viewProcessingQueue,
hideArchivedFolderByDefault,
displayArchiveIntro,
storageInfo,
suggestedChatListNotice,
savedMessagesPeer,
chatListViewUpdate,
self.statePromise.get()
)
|> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestedChatListNotice, savedMessagesPeer, updateAndFilter, state) -> Signal<ChatListNodeListViewTransition, NoError> in
let (update, filter) = updateAndFilter
let previousHideArchivedFolderByDefaultValue = previousHideArchivedFolderByDefault.swap(hideArchivedFolderByDefault)
let (rawEntries, isLoading) = chatListNodeEntriesForView(update.list, state: state, savedMessagesPeer: savedMessagesPeer, foundPeers: state.foundPeers, hideArchivedFolderByDefault: hideArchivedFolderByDefault, displayArchiveIntro: displayArchiveIntro, storageInfo: storageInfo, suggestPasswordSetup: suggestPasswordSetup, mode: mode, chatListLocation: location)
let notice: ChatListNotice?
if let suggestedChatListNotice {
notice = suggestedChatListNotice
} else if let storageInfo {
notice = .clearStorage(sizeFraction: storageInfo)
} else {
notice = nil
}
let (rawEntries, isLoading) = chatListNodeEntriesForView(update.list, state: state, savedMessagesPeer: savedMessagesPeer, foundPeers: state.foundPeers, hideArchivedFolderByDefault: hideArchivedFolderByDefault, displayArchiveIntro: displayArchiveIntro, notice: notice, mode: mode, chatListLocation: location)
var isEmpty = true
var entries = rawEntries.filter { entry in
switch entry {

View File

@ -49,6 +49,8 @@ public enum ChatListNodeEntryPromoInfo: Equatable {
enum ChatListNotice: Equatable {
case clearStorage(sizeFraction: Double)
case setupPassword
case premiumUpgrade(discount: Int32)
case premiumAnnualDiscount(discount: Int32)
}
enum ChatListNodeEntry: Comparable, Identifiable {
@ -404,7 +406,7 @@ private func offsetPinnedIndex(_ index: EngineChatList.Item.Index, offset: UInt1
}
}
func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState, savedMessagesPeer: EnginePeer?, foundPeers: [(EnginePeer, EnginePeer?)], hideArchivedFolderByDefault: Bool, displayArchiveIntro: Bool, storageInfo: Double?, suggestPasswordSetup: Bool, mode: ChatListNodeMode, chatListLocation: ChatListControllerLocation) -> (entries: [ChatListNodeEntry], loading: Bool) {
func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState, savedMessagesPeer: EnginePeer?, foundPeers: [(EnginePeer, EnginePeer?)], hideArchivedFolderByDefault: Bool, displayArchiveIntro: Bool, notice: ChatListNotice?, mode: ChatListNodeMode, chatListLocation: ChatListControllerLocation) -> (entries: [ChatListNodeEntry], loading: Bool) {
var result: [ChatListNodeEntry] = []
var pinnedIndexOffset: UInt16 = 0
@ -666,10 +668,9 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState
if displayArchiveIntro {
result.append(.ArchiveIntro(presentationData: state.presentationData))
}
if suggestPasswordSetup {
result.append(.Notice(presentationData: state.presentationData, notice: .setupPassword))
} else if let storageInfo {
result.append(.Notice(presentationData: state.presentationData, notice: .clearStorage(sizeFraction: storageInfo)))
if let notice {
result.append(.Notice(presentationData: state.presentationData, notice: notice))
}
result.append(.HeaderEntry)

View File

@ -141,6 +141,26 @@ class ChatListStorageInfoItemNode: ListViewItemNode {
//TODO:localize
titleString = NSAttributedString(string: "Protect Your Account", font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor)
textString = NSAttributedString(string: "Set a password that will be required each time you log in with this phone number.", font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
case let .premiumUpgrade(discount):
let discountString = "\(discount)%"
let rawTitleString = item.strings.ChatList_PremiumAnnualUpgradeTitle(discountString)
let titleStringValue = NSMutableAttributedString(attributedString: NSAttributedString(string: rawTitleString.string, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor))
if let range = rawTitleString.ranges.first {
titleStringValue.addAttribute(.foregroundColor, value: item.theme.rootController.navigationBar.accentTextColor, range: range.range)
}
titleString = titleStringValue
textString = NSAttributedString(string: item.strings.ChatList_PremiumAnnualUpgradeText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
case let .premiumAnnualDiscount(discount):
let discountString = "\(discount)%"
let rawTitleString = item.strings.ChatList_PremiumAnnualDiscountTitle(discountString)
let titleStringValue = NSMutableAttributedString(attributedString: NSAttributedString(string: rawTitleString.string, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor))
if let range = rawTitleString.ranges.first {
titleStringValue.addAttribute(.foregroundColor, value: item.theme.rootController.navigationBar.accentTextColor, range: range.range)
}
titleString = titleStringValue
textString = NSAttributedString(string: item.strings.ChatList_PremiumAnnualDiscountText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
}
let titleLayout = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0)))

View File

@ -21,21 +21,12 @@ public func reactionStaticImage(context: AccountContext, animation: TelegramMedi
return Signal { subscriber in
let fetchDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .image, reference: MediaResourceReference.standalone(resource: animation.resource)).start()
let type: AnimationCacheAnimationType
if animation.isVideoSticker || animation.isVideoEmoji {
type = .video
} else if animation.isAnimatedSticker {
type = .lottie
} else {
type = .still
}
var customColor: UIColor?
if animation.isCustomTemplateEmoji {
customColor = nil
}
let fetchFrame = animationCacheFetchFile(context: context, userLocation: .other, userContentType: .sticker, resource: MediaResourceReference.standalone(resource: animation.resource), type: type, keyframeOnly: true, customColor: customColor)
let fetchFrame = animationCacheFetchFile(context: context, userLocation: .other, userContentType: .sticker, resource: MediaResourceReference.standalone(resource: animation.resource), type: AnimationCacheAnimationType(file: animation), keyframeOnly: true, customColor: customColor)
class AnimationCacheItemWriterImpl: AnimationCacheItemWriter {
let queue: Queue

View File

@ -281,7 +281,7 @@ class StickerPickerScreen: ViewController {
return
}
let context = controller.context
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks)
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks)
let _ = (context.account.postbox.combinedView(keys: [viewKey])
|> take(1)
|> deliverOnMainQueue).start(next: { views in

View File

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

View File

@ -154,10 +154,12 @@ public final class InAppPurchaseManager: NSObject {
private final class PaymentTransactionContext {
var state: SKPaymentTransactionState?
var isUpgrade: Bool
var targetPeerId: PeerId?
let subscriber: (TransactionState) -> Void
init(targetPeerId: PeerId?, subscriber: @escaping (TransactionState) -> Void) {
init(isUpgrade: Bool, targetPeerId: PeerId?, subscriber: @escaping (TransactionState) -> Void) {
self.isUpgrade = isUpgrade
self.targetPeerId = targetPeerId
self.subscriber = subscriber
}
@ -236,7 +238,7 @@ public final class InAppPurchaseManager: NSObject {
}
}
public func buyProduct(_ product: Product, targetPeerId: PeerId? = nil) -> Signal<PurchaseState, PurchaseError> {
public func buyProduct(_ product: Product, isUpgrade: Bool = false, targetPeerId: PeerId? = nil) -> Signal<PurchaseState, PurchaseError> {
if !self.canMakePayments {
return .fail(.cantMakePayments)
}
@ -258,7 +260,7 @@ public final class InAppPurchaseManager: NSObject {
let disposable = MetaDisposable()
self.stateQueue.async {
let paymentContext = PaymentTransactionContext(targetPeerId: targetPeerId, subscriber: { state in
let paymentContext = PaymentTransactionContext(isUpgrade: isUpgrade, targetPeerId: targetPeerId, subscriber: { state in
switch state {
case let .purchased(transactionId), let .restored(transactionId):
if let transactionId = transactionId {
@ -305,6 +307,13 @@ public final class InAppPurchaseManager: NSObject {
}
return signal
}
public func getValidTransactionIds() -> [String] {
guard let data = getReceiptData(), let receipt = parseReceipt(data) else {
return []
}
return receipt.purchases.map { $0.transactionId }
}
}
extension InAppPurchaseManager: SKProductsRequestDelegate {
@ -370,6 +379,7 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver {
productId: transaction.payment.productIdentifier,
content: PendingInAppPurchaseState(
productId: transaction.payment.productIdentifier,
isUpgrade: paymentContext.isUpgrade,
targetPeerId: paymentContext.targetPeerId
)
).start()
@ -431,7 +441,23 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver {
}
}
} else {
purpose = .single(.subscription)
let isUpgrade: Signal<Bool, NoError>
if let isUpgradeValue = paymentContexts[productIdentifier]?.isUpgrade {
isUpgrade = .single(isUpgradeValue)
} else {
isUpgrade = pendingInAppPurchaseState(engine: self.engine, productId: productIdentifier)
|> mapToSignal { state -> Signal<Bool, NoError> in
if let state = state {
return .single(state.isUpgrade)
} else {
return .single(false)
}
}
}
purpose = isUpgrade
|> map { isUpgrade in
return isUpgrade ? .upgrade : .subscription
}
}
let receiptData = getReceiptData() ?? Data()
@ -508,10 +534,12 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver {
private final class PendingInAppPurchaseState: Codable {
public let productId: String
public let isUpgrade: Bool
public let targetPeerId: PeerId?
public init(productId: String, targetPeerId: PeerId?) {
public init(productId: String, isUpgrade: Bool, targetPeerId: PeerId?) {
self.productId = productId
self.isUpgrade = isUpgrade
self.targetPeerId = targetPeerId
}
@ -519,6 +547,7 @@ private final class PendingInAppPurchaseState: Codable {
let container = try decoder.container(keyedBy: StringCodingKey.self)
self.productId = try container.decode(String.self, forKey: "productId")
self.isUpgrade = try container.decodeIfPresent(Bool.self, forKey: "isUpgrade") ?? false
self.targetPeerId = (try container.decodeIfPresent(Int64.self, forKey: "targetPeerId")).flatMap { PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) }
}
@ -526,6 +555,7 @@ private final class PendingInAppPurchaseState: Codable {
var container = encoder.container(keyedBy: StringCodingKey.self)
try container.encode(self.productId, forKey: "productId")
try container.encode(self.isUpgrade, forKey: "isUpgrade")
if let targetPeerId = self.targetPeerId {
try container.encode(targetPeerId.id._internalGetInt64Value(), forKey: "targetPeerId")
}

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

View File

@ -195,7 +195,7 @@
if (!_signup) {
TGMenuSheetButtonItemView *viewItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"ProfilePhoto.SetEmoji") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^
{
{
__strong TGMediaAvatarMenuMixin *strongSelf = weakSelf;
if (strongSelf == nil)
return;
@ -205,8 +205,26 @@
return;
[strongController dismissAnimated:true];
if (strongSelf != nil)
strongSelf.requestAvatarEditor(^(UIImage *image, NSURL *asset, TGVideoEditAdjustments *adjustments, void (^commit)(void)) {
if (strongSelf != nil) {
strongSelf.requestAvatarEditor(^(UIImage *image, void (^commit)(void)) {
__strong TGMediaAvatarMenuMixin *strongSelf = weakSelf;
if (strongSelf == nil)
return;
if (strongSelf.willFinishWithImage != nil) {
strongSelf.willFinishWithImage(image, ^{
if (strongSelf.didFinishWithImage != nil)
strongSelf.didFinishWithImage(image);
commit();
});
} else {
if (strongSelf.didFinishWithImage != nil)
strongSelf.didFinishWithImage(image);
commit();
}
}, ^(UIImage *image, NSURL *asset, TGVideoEditAdjustments *adjustments, void (^commit)(void)) {
__strong TGMediaAvatarMenuMixin *strongSelf = weakSelf;
if (strongSelf == nil)
return;
@ -225,6 +243,7 @@
commit();
}
});
}
}];
[itemViews addObject:viewItem];
}

View File

@ -49,6 +49,7 @@ public final class ListMessageItem: ListViewItem {
let chatLocation: ChatLocation
let interaction: ListMessageItemInteraction
let message: Message?
let translateToLanguage: String?
public let selection: ChatHistoryMessageSelection
let hintIsLink: Bool
let isGlobalSearchResult: Bool
@ -61,12 +62,13 @@ public final class ListMessageItem: ListViewItem {
public let selectable: Bool = true
public init(presentationData: ChatPresentationData, context: AccountContext, chatLocation: ChatLocation, interaction: ListMessageItemInteraction, message: Message?, selection: ChatHistoryMessageSelection, displayHeader: Bool, customHeader: ListViewItemHeader? = nil, hintIsLink: Bool = false, isGlobalSearchResult: Bool = false, isDownloadList: Bool = false, displayFileInfo: Bool = true, displayBackground: Bool = false, style: ItemListStyle = .plain) {
public init(presentationData: ChatPresentationData, context: AccountContext, chatLocation: ChatLocation, interaction: ListMessageItemInteraction, message: Message?, translateToLanguage: String? = nil, selection: ChatHistoryMessageSelection, displayHeader: Bool, customHeader: ListViewItemHeader? = nil, hintIsLink: Bool = false, isGlobalSearchResult: Bool = false, isDownloadList: Bool = false, displayFileInfo: Bool = true, displayBackground: Bool = false, style: ItemListStyle = .plain) {
self.presentationData = presentationData
self.context = context
self.chatLocation = chatLocation
self.interaction = interaction
self.message = message
self.translateToLanguage = translateToLanguage
if let header = customHeader {
self.header = header
} else if displayHeader, let message = message {

View File

@ -424,7 +424,12 @@ public final class ListMessageSnippetItemNode: ListMessageNode {
let (messageTextUrl, _) = parseUrl(url: message.text, wasConcealed: false)
if messageTextUrl != rawUrlString, !item.isGlobalSearchResult {
mutableDescriptionText.append(NSAttributedString(string: message.text + "\n", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor))
var messageText = message.text
if let translation = message.attributes.first(where: { $0 is TranslationMessageAttribute }) as? TranslationMessageAttribute, !translation.text.isEmpty, item.translateToLanguage == translation.toLang {
messageText = translation.text
}
mutableDescriptionText.append(NSAttributedString(string: messageText + "\n", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor))
}
let urlAttributedString = NSMutableAttributedString()
@ -439,8 +444,17 @@ public final class ListMessageSnippetItemNode: ListMessageNode {
}
break loop
case let .TextUrl(url):
var messageText = message.text
var entity = entity
if let translation = message.attributes.first(where: { $0 is TranslationMessageAttribute }) as? TranslationMessageAttribute, !translation.text.isEmpty, item.translateToLanguage == translation.toLang {
messageText = translation.text
if entities.count == translation.entities.count, let index = entities.firstIndex(of: entity), index < translation.entities.count {
entity = translation.entities[index]
}
}
var range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
let nsString = message.text as NSString
let nsString = messageText as NSString
if range.location + range.length > nsString.length {
range.location = max(0, nsString.length - range.length)
range.length = nsString.length - range.location
@ -470,7 +484,7 @@ public final class ListMessageSnippetItemNode: ListMessageNode {
let (messageTextUrl, _) = parseUrl(url: message.text, wasConcealed: false)
if messageTextUrl != rawUrlString, !item.isGlobalSearchResult {
mutableDescriptionText.append(NSAttributedString(string: message.text + "\n", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor))
mutableDescriptionText.append(NSAttributedString(string: messageText + "\n", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor))
}
let urlAttributedString = NSMutableAttributedString()

View File

@ -484,6 +484,14 @@ private struct PremiumProduct: Equatable {
var pricePerMonth: String {
return self.storeProduct.pricePerMonth(Int(self.months))
}
var isCurrent: Bool {
return self.option.isCurrent
}
var transactionId: String? {
return self.option.transactionId
}
}
final class PremiumOptionComponent: CombinedComponent {
@ -815,10 +823,12 @@ private final class CheckComponent: Component {
final class SectionGroupComponent: Component {
public final class Item: Equatable {
public let content: AnyComponentWithIdentity<Empty>
public let isEnabled: Bool
public let action: () -> Void
public init(_ content: AnyComponentWithIdentity<Empty>, action: @escaping () -> Void) {
public init(_ content: AnyComponentWithIdentity<Empty>, isEnabled: Bool = true, action: @escaping () -> Void) {
self.content = content
self.isEnabled = isEnabled
self.action = action
}
@ -826,7 +836,9 @@ final class SectionGroupComponent: Component {
if lhs.content != rhs.content {
return false
}
if lhs.isEnabled != rhs.isEnabled {
return false
}
return true
}
}
@ -931,6 +943,8 @@ final class SectionGroupComponent: Component {
environment: {},
containerSize: CGSize(width: size.width - sideInset, height: .greatestFiniteMagnitude)
)
buttonView.isEnabled = item.isEnabled
itemView.alpha = item.isEnabled ? 1.0 : 0.3
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height), size: itemSize)
buttonView.frame = CGRect(origin: itemFrame.origin, size: CGSize(width: availableSize.width, height: itemSize.height + UIScreenPixel))
@ -1159,22 +1173,26 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
let context: AccountContext
let source: PremiumSource
let isPremium: Bool?
let justBought: Bool
let otherPeerName: String?
let products: [PremiumProduct]?
let selectedProductId: String?
let validTransactionIds: [String]
let promoConfiguration: PremiumPromoConfiguration?
let present: (ViewController) -> Void
let selectProduct: (String) -> Void
let buy: () -> Void
let updateIsFocused: (Bool) -> Void
init(context: AccountContext, source: PremiumSource, isPremium: Bool?, otherPeerName: String?, products: [PremiumProduct]?, selectedProductId: String?, promoConfiguration: PremiumPromoConfiguration?, present: @escaping (ViewController) -> Void, selectProduct: @escaping (String) -> Void, buy: @escaping () -> Void, updateIsFocused: @escaping (Bool) -> Void) {
init(context: AccountContext, source: PremiumSource, isPremium: Bool?, justBought: Bool, otherPeerName: String?, products: [PremiumProduct]?, selectedProductId: String?, validTransactionIds: [String], promoConfiguration: PremiumPromoConfiguration?, present: @escaping (ViewController) -> Void, selectProduct: @escaping (String) -> Void, buy: @escaping () -> Void, updateIsFocused: @escaping (Bool) -> Void) {
self.context = context
self.source = source
self.isPremium = isPremium
self.justBought = justBought
self.otherPeerName = otherPeerName
self.products = products
self.selectedProductId = selectedProductId
self.validTransactionIds = validTransactionIds
self.promoConfiguration = promoConfiguration
self.present = present
self.selectProduct = selectProduct
@ -1192,6 +1210,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
if lhs.isPremium != rhs.isPremium {
return false
}
if lhs.justBought != rhs.justBought {
return false
}
if lhs.otherPeerName != rhs.otherPeerName {
return false
}
@ -1201,6 +1222,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
if lhs.selectedProductId != rhs.selectedProductId {
return false
}
if lhs.validTransactionIds != rhs.validTransactionIds {
return false
}
if lhs.promoConfiguration != rhs.promoConfiguration {
return false
}
@ -1213,6 +1237,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
var products: [PremiumProduct]?
var selectedProductId: String?
var validTransactionIds: [String] = []
var isPremium: Bool?
@ -1230,6 +1255,18 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
return self.products?.first(where: { $0.id == self.selectedProductId })?.id.hasSuffix(".annual") ?? false
}
var canUpgrade: Bool {
if let products = self.products, let current = products.first(where: { $0.isCurrent }), let transactionId = current.transactionId {
if self.validTransactionIds.contains(transactionId) {
return products.first(where: { $0.months > current.months }) != nil
} else {
return false
}
} else {
return false
}
}
init(context: AccountContext, source: PremiumSource) {
self.context = context
@ -1317,6 +1354,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
let state = context.state
state.products = context.component.products
state.selectedProductId = context.component.selectedProductId
state.validTransactionIds = context.component.validTransactionIds
state.isPremium = context.component.isPremium
let theme = environment.theme
@ -1378,7 +1416,11 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
textString = strings.Premium_PersonalDescription
}
} else if context.component.isPremium == true {
textString = strings.Premium_SubscribedDescription
if !context.component.justBought, let products = state.products, let current = products.first(where: { $0.isCurrent }), current.months == 1 {
textString = strings.Premium_UpgradeDescription
} else {
textString = strings.Premium_SubscribedDescription
}
} else {
textString = strings.Premium_Description
}
@ -1398,11 +1440,22 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
lineSpacing: 0.2
),
environment: {},
availableSize: CGSize(width: availableWidth - sideInsets, height: 240.0),
transition: context.transition
availableSize: CGSize(width: availableWidth - sideInsets - 8.0, height: 240.0),
transition: .immediate
)
context.add(text
.position(CGPoint(x: size.width / 2.0, y: size.height + text.size.height / 2.0))
// .update(Transition.Update { _, view, _ in
// if let snapshot = view.snapshotView(afterScreenUpdates: false) {
// let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
// view.superview?.addSubview(snapshot)
// transition.setAlpha(view: snapshot, alpha: 0.0, completion: { [weak snapshot] _ in
// snapshot?.removeFromSuperview()
// })
// snapshot.frame = view.frame
// transition.animateAlpha(view: view, from: 0.0, to: 1.0)
// }
// })
)
size.height += text.size.height
size.height += 21.0
@ -1430,9 +1483,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
let updateIsFocused = context.component.updateIsFocused
let layoutOptions = {
if state.isPremium == true {
} else if let products = state.products, products.count > 1 {
if let products = state.products, products.count > 1, state.isPremium == false || (!context.component.justBought && state.canUpgrade) {
var optionsItems: [SectionGroupComponent.Item] = []
let gradientColors: [UIColor] = [
UIColor(rgb: 0x8e77ff),
@ -1447,11 +1498,11 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
shortestOptionPrice = (1, NSDecimalNumber(decimal: 1))
}
let currentProductMonths = state.products?.first(where: { $0.isCurrent })?.months ?? 0
var i = 0
for product in products {
let giftTitle: String
let months = product.months
if product.id.hasSuffix(".monthly") {
giftTitle = strings.Premium_Monthly
} else if product.id.hasSuffix(".semiannual") {
@ -1459,8 +1510,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
} else {
giftTitle = strings.Premium_Annual
}
let discountValue = Int((1.0 - Float(product.storeProduct.priceCurrencyAndAmount.amount) / Float(months) / Float(shortestOptionPrice.0)) * 100.0)
let fraction = Float(product.storeProduct.priceCurrencyAndAmount.amount) / Float(product.months) / Float(shortestOptionPrice.0)
let discountValue = Int(round((1.0 - fraction) * 20.0) * 5.0)
let discount: String
if discountValue > 0 {
discount = "-\(discountValue)%"
@ -1468,22 +1520,25 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
discount = ""
}
let defaultPrice = product.storeProduct.defaultPrice(shortestOptionPrice.1, monthsCount: Int(months))
let defaultPrice = product.storeProduct.defaultPrice(shortestOptionPrice.1, monthsCount: Int(product.months))
var subtitle = ""
var pricePerMonth = product.price
if months > 1 {
pricePerMonth = product.storeProduct.pricePerMonth(Int(months))
if product.months > 1 {
pricePerMonth = product.storeProduct.pricePerMonth(Int(product.months))
if discountValue > 0 {
subtitle = "**\(defaultPrice)** \(product.price)"
if months == 12 {
if product.months == 12 {
subtitle = environment.strings.Premium_PricePerYear(subtitle).string
}
} else {
subtitle = product.price
}
}
if product.isCurrent {
subtitle = environment.strings.Premium_CurrentPlan
}
pricePerMonth = environment.strings.Premium_PricePerMonth(pricePerMonth).string
optionsItems.append(
@ -1496,7 +1551,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
subtitle: subtitle,
labelPrice: pricePerMonth,
discount: discount,
selected: product.id == state.selectedProductId,
selected: !product.isCurrent && product.id == state.selectedProductId,
primaryTextColor: textColor,
secondaryTextColor: subtitleColor,
accentColor: gradientColors[i],
@ -1505,6 +1560,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
)
)
),
isEnabled: product.months > currentProductMonths,
action: {
selectProduct(product.id)
}
@ -1738,6 +1794,38 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
)
}
let controller = environment.controller
let termsTapActionImpl: ([NSAttributedString.Key: Any]) -> Void = { attributes in
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String,
let controller = controller() as? PremiumIntroScreen, let navigationController = controller.navigationController as? NavigationController {
if url.hasPrefix("https://apps.apple.com/account/subscriptions") {
controller.context.sharedContext.applicationBindings.openSubscriptions()
} else if url.hasPrefix("https://") || url.hasPrefix("tg://") {
controller.context.sharedContext.openExternalUrl(context: controller.context, urlContext: .generic, url: url, forceExternal: !url.hasPrefix("tg://"), presentationData: controller.context.sharedContext.currentPresentationData.with({$0}), navigationController: nil, dismissInput: {})
} else {
let context = controller.context
let signal: Signal<ResolvedUrl, NoError>?
switch url {
case "terms":
signal = cachedTermsPage(context: context)
case "privacy":
signal = cachedPrivacyPage(context: context)
default:
signal = nil
}
if let signal = signal {
let _ = (signal
|> deliverOnMainQueue).start(next: { resolvedUrl in
context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in
}, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { [weak controller] c, arguments in
controller?.push(c)
}, dismissInput: {}, contentContext: nil)
})
}
}
}
}
let termsText = termsText.update(
component: MultilineTextComponent(
text: termsString,
@ -1752,35 +1840,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
return nil
}
},
tapAction: { [weak environment] attributes, _ in
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String,
let controller = environment?.controller() as? PremiumIntroScreen, let navigationController = controller.navigationController as? NavigationController {
if url.hasPrefix("https://apps.apple.com/account/subscriptions") {
controller.context.sharedContext.applicationBindings.openSubscriptions()
} else if url.hasPrefix("https://") || url.hasPrefix("tg://") {
controller.context.sharedContext.openExternalUrl(context: controller.context, urlContext: .generic, url: url, forceExternal: !url.hasPrefix("tg://"), presentationData: controller.context.sharedContext.currentPresentationData.with({$0}), navigationController: nil, dismissInput: {})
} else {
let context = controller.context
let signal: Signal<ResolvedUrl, NoError>?
switch url {
case "terms":
signal = cachedTermsPage(context: context)
case "privacy":
signal = cachedPrivacyPage(context: context)
default:
signal = nil
}
if let signal = signal {
let _ = (signal
|> deliverOnMainQueue).start(next: { resolvedUrl in
context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in
}, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { [weak controller] c, arguments in
controller?.push(c)
}, dismissInput: {}, contentContext: nil)
})
}
}
}
tapAction: { attributes, _ in
termsTapActionImpl(attributes)
}
),
environment: {},
@ -1903,9 +1964,11 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
private(set) var products: [PremiumProduct]?
private(set) var selectedProductId: String?
fileprivate var validTransactionIds: [String] = []
var isPremium: Bool?
var otherPeerName: String?
var justBought = false
let animationCache: AnimationCache
let animationRenderer: MultiAnimationRenderer
@ -1928,6 +1991,18 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
return self.products?.first(where: { $0.id == self.selectedProductId })?.id.hasSuffix(".annual") ?? false
}
var canUpgrade: Bool {
if let products = self.products, let current = products.first(where: { $0.isCurrent }), let transactionId = current.transactionId {
if self.validTransactionIds.contains(transactionId) {
return products.first(where: { $0.months > current.months }) != nil
} else {
return false
}
} else {
return false
}
}
init(context: AccountContext, source: PremiumSource, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void) {
self.context = context
self.updateInProgress = updateInProgress
@ -1939,6 +2014,8 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
super.init()
self.validTransactionIds = context.inAppPurchaseManager?.getValidTransactionIds() ?? []
let availableProducts: Signal<[InAppPurchaseManager.Product], NoError>
if let inAppPurchaseManager = context.inAppPurchaseManager {
availableProducts = inAppPurchaseManager.availableProducts
@ -1995,7 +2072,11 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
strongSelf.otherPeerName = otherPeerName
if !hadProducts {
strongSelf.selectedProductId = strongSelf.products?.last?.id
if let _ = products.first(where: { $0.isCurrent }) {
strongSelf.selectedProductId = strongSelf.products?.first?.id
} else {
strongSelf.selectedProductId = strongSelf.products?.last?.id
}
for (_, video) in promoConfiguration.videos {
strongSelf.preloadDisposableSet.add(preloadVideoResource(postbox: context.account.postbox, userLocation: .other, userContentType: .video, resourceReference: .standalone(resource: video.resource), duration: 3.0).start())
@ -2037,18 +2118,19 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
let premiumProduct = self.products?.first(where: { $0.id == self.selectedProductId }), !self.inProgress else {
return
}
let isUpgrade = self.products?.first(where: { $0.isCurrent }) != nil
addAppLogEvent(postbox: self.context.account.postbox, type: "premium.promo_screen_accept")
self.inProgress = true
self.updateInProgress(true)
self.updated(transition: .immediate)
let _ = (self.context.engine.payments.canPurchasePremium(purpose: .subscription)
let _ = (self.context.engine.payments.canPurchasePremium(purpose: isUpgrade ? .upgrade : .subscription)
|> deliverOnMainQueue).start(next: { [weak self] available in
if let strongSelf = self {
if available {
strongSelf.paymentDisposable.set((inAppPurchaseManager.buyProduct(premiumProduct.storeProduct)
strongSelf.paymentDisposable.set((inAppPurchaseManager.buyProduct(premiumProduct.storeProduct, isUpgrade: isUpgrade)
|> deliverOnMainQueue).start(next: { [weak self] status in
if let strongSelf = self, case .purchased = status {
strongSelf.activationDisposable.set((strongSelf.context.account.postbox.peerView(id: strongSelf.context.account.peerId)
@ -2085,6 +2167,8 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
strongSelf.updateInProgress(false)
strongSelf.isPremium = true
strongSelf.justBought = true
strongSelf.updated(transition: .easeInOut(duration: 0.25))
strongSelf.completion()
}
@ -2226,7 +2310,11 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
} else if case .gift = context.component.source {
titleString = environment.strings.Premium_GiftedTitle
} else if state.isPremium == true {
titleString = environment.strings.Premium_SubscribedTitle
if !state.justBought && state.canUpgrade {
titleString = environment.strings.Premium_Title
} else {
titleString = environment.strings.Premium_SubscribedTitle
}
} else {
titleString = environment.strings.Premium_Title
}
@ -2357,7 +2445,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
let bottomPanelPadding: CGFloat = 12.0
let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding
let bottomPanelHeight: CGFloat = state.isPremium == true ? bottomInset : bottomPanelPadding + 50.0 + bottomInset
let bottomPanelHeight: CGFloat = state.isPremium == true && !state.canUpgrade ? bottomInset : bottomPanelPadding + 50.0 + bottomInset
let scrollContent = scrollContent.update(
component: ScrollComponent<EnvironmentType>(
@ -2365,9 +2453,11 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
context: context.component.context,
source: context.component.source,
isPremium: state.isPremium,
justBought: state.justBought,
otherPeerName: state.otherPeerName,
products: state.products,
selectedProductId: state.selectedProductId,
validTransactionIds: state.validTransactionIds,
promoConfiguration: state.promoConfiguration,
present: context.component.present,
selectProduct: { [weak state] productId in
@ -2454,6 +2544,17 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
.position(CGPoint(x: context.availableSize.width / 2.0, y: max(topInset + 160.0 - titleOffset, environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0)))
.scale(titleScale)
.opacity(titleAlpha)
// .update(Transition.Update { _, view, _ in
// if let snapshot = view.snapshotView(afterScreenUpdates: false) {
// let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
// view.superview?.addSubview(snapshot)
// transition.setAlpha(view: snapshot, alpha: 0.0, completion: { [weak snapshot] _ in
// snapshot?.removeFromSuperview()
// })
// snapshot.frame = view.frame
// transition.animateAlpha(view: view, from: 0.0, to: titleAlpha)
// }
// })
)
context.add(secondaryTitle
@ -2469,14 +2570,20 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
}
}
if state.isPremium == true || isGiftView {
if (state.isPremium == true && (!state.canUpgrade || state.justBought)) || isGiftView {
} else {
let buttonTitle: String
if state.isPremium == true && state.canUpgrade {
buttonTitle = state.isAnnual ? environment.strings.Premium_UpgradeForAnnual(state.price ?? "").string : environment.strings.Premium_UpgradeFor(state.price ?? "").string
} else {
buttonTitle = state.isAnnual ? environment.strings.Premium_SubscribeForAnnual(state.price ?? "").string : environment.strings.Premium_SubscribeFor(state.price ?? "").string
}
let sideInset: CGFloat = 16.0
let button = button.update(
component: SolidRoundedButtonComponent(
title: state.isAnnual ? environment.strings.Premium_SubscribeForAnnual(state.price ?? "").string : environment.strings.Premium_SubscribeFor(state.price ?? "").string,
title: buttonTitle,
theme: SolidRoundedButtonComponent.Theme(
backgroundColor: UIColor(rgb: 0x8878ff),
backgroundColors: [

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
}, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel()
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {
})
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {})
let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true)

View File

@ -843,9 +843,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate
}, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel()
}, present: { _ in
}, openForumThread: { _, _ in },
openStorageManagement: {}, openPasswordSetup: {
})
}, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {})
let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true)
func makeChatListItem(

View File

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

View File

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

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 {
enum AvailableReaction: TypeConstructorDescription {
case availableReaction(flags: Int32, reaction: String, title: String, staticIcon: Api.Document, appearAnimation: Api.Document, selectAnimation: Api.Document, activateAnimation: Api.Document, effectAnimation: Api.Document, aroundAnimation: Api.Document?, centerIcon: Api.Document?)
@ -940,75 +1024,3 @@ public extension Api {
}
}
public extension Api {
enum BotInfo: TypeConstructorDescription {
case botInfo(flags: Int32, userId: Int64?, description: String?, descriptionPhoto: Api.Photo?, descriptionDocument: Api.Document?, commands: [Api.BotCommand]?, menuButton: Api.BotMenuButton?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .botInfo(let flags, let userId, let description, let descriptionPhoto, let descriptionDocument, let commands, let menuButton):
if boxed {
buffer.appendInt32(-1892676777)
}
serializeInt32(flags, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {serializeInt64(userId!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 1) != 0 {serializeString(description!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 4) != 0 {descriptionPhoto!.serialize(buffer, true)}
if Int(flags) & Int(1 << 5) != 0 {descriptionDocument!.serialize(buffer, true)}
if Int(flags) & Int(1 << 2) != 0 {buffer.appendInt32(481674261)
buffer.appendInt32(Int32(commands!.count))
for item in commands! {
item.serialize(buffer, true)
}}
if Int(flags) & Int(1 << 3) != 0 {menuButton!.serialize(buffer, true)}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .botInfo(let flags, let userId, let description, let descriptionPhoto, let descriptionDocument, let commands, let menuButton):
return ("botInfo", [("flags", flags as Any), ("userId", userId as Any), ("description", description as Any), ("descriptionPhoto", descriptionPhoto as Any), ("descriptionDocument", descriptionDocument as Any), ("commands", commands as Any), ("menuButton", menuButton as Any)])
}
}
public static func parse_botInfo(_ reader: BufferReader) -> BotInfo? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Int64?
if Int(_1!) & Int(1 << 0) != 0 {_2 = reader.readInt64() }
var _3: String?
if Int(_1!) & Int(1 << 1) != 0 {_3 = parseString(reader) }
var _4: Api.Photo?
if Int(_1!) & Int(1 << 4) != 0 {if let signature = reader.readInt32() {
_4 = Api.parse(reader, signature: signature) as? Api.Photo
} }
var _5: Api.Document?
if Int(_1!) & Int(1 << 5) != 0 {if let signature = reader.readInt32() {
_5 = Api.parse(reader, signature: signature) as? Api.Document
} }
var _6: [Api.BotCommand]?
if Int(_1!) & Int(1 << 2) != 0 {if let _ = reader.readInt32() {
_6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.BotCommand.self)
} }
var _7: Api.BotMenuButton?
if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() {
_7 = Api.parse(reader, signature: signature) as? Api.BotMenuButton
} }
let _c1 = _1 != nil
let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil
let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil
let _c4 = (Int(_1!) & Int(1 << 4) == 0) || _4 != nil
let _c5 = (Int(_1!) & Int(1 << 5) == 0) || _5 != nil
let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil
let _c7 = (Int(_1!) & Int(1 << 3) == 0) || _7 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 {
return Api.BotInfo.botInfo(flags: _1!, userId: _2, description: _3, descriptionPhoto: _4, descriptionDocument: _5, commands: _6, menuButton: _7)
}
else {
return nil
}
}
}
}

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

View File

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

View File

@ -917,7 +917,8 @@ public extension Api {
public extension Api {
enum VideoSize: TypeConstructorDescription {
case videoSize(flags: Int32, type: String, w: Int32, h: Int32, size: Int32, videoStartTs: Double?)
case videoSizeEmojiMarkup(type: String, emojiId: Int64, backgroundColors: [Int32])
case videoSizeEmojiMarkup(emojiId: Int64, backgroundColors: [Int32])
case videoSizeStickerMarkup(stickerset: Api.InputStickerSet, stickerId: Int64, backgroundColors: [Int32])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
@ -932,11 +933,10 @@ public extension Api {
serializeInt32(size, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {serializeDouble(videoStartTs!, buffer: buffer, boxed: false)}
break
case .videoSizeEmojiMarkup(let type, let emojiId, let backgroundColors):
case .videoSizeEmojiMarkup(let emojiId, let backgroundColors):
if boxed {
buffer.appendInt32(195933766)
buffer.appendInt32(-128171716)
}
serializeString(type, buffer: buffer, boxed: false)
serializeInt64(emojiId, buffer: buffer, boxed: false)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(backgroundColors.count))
@ -944,6 +944,18 @@ public extension Api {
serializeInt32(item, buffer: buffer, boxed: false)
}
break
case .videoSizeStickerMarkup(let stickerset, let stickerId, let backgroundColors):
if boxed {
buffer.appendInt32(228623102)
}
stickerset.serialize(buffer, true)
serializeInt64(stickerId, buffer: buffer, boxed: false)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(backgroundColors.count))
for item in backgroundColors {
serializeInt32(item, buffer: buffer, boxed: false)
}
break
}
}
@ -951,8 +963,10 @@ public extension Api {
switch self {
case .videoSize(let flags, let type, let w, let h, let size, let videoStartTs):
return ("videoSize", [("flags", flags as Any), ("type", type as Any), ("w", w as Any), ("h", h as Any), ("size", size as Any), ("videoStartTs", videoStartTs as Any)])
case .videoSizeEmojiMarkup(let type, let emojiId, let backgroundColors):
return ("videoSizeEmojiMarkup", [("type", type as Any), ("emojiId", emojiId as Any), ("backgroundColors", backgroundColors as Any)])
case .videoSizeEmojiMarkup(let emojiId, let backgroundColors):
return ("videoSizeEmojiMarkup", [("emojiId", emojiId as Any), ("backgroundColors", backgroundColors as Any)])
case .videoSizeStickerMarkup(let stickerset, let stickerId, let backgroundColors):
return ("videoSizeStickerMarkup", [("stickerset", stickerset as Any), ("stickerId", stickerId as Any), ("backgroundColors", backgroundColors as Any)])
}
}
@ -983,8 +997,26 @@ public extension Api {
}
}
public static func parse_videoSizeEmojiMarkup(_ reader: BufferReader) -> VideoSize? {
var _1: String?
_1 = parseString(reader)
var _1: Int64?
_1 = reader.readInt64()
var _2: [Int32]?
if let _ = reader.readInt32() {
_2 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
if _c1 && _c2 {
return Api.VideoSize.videoSizeEmojiMarkup(emojiId: _1!, backgroundColors: _2!)
}
else {
return nil
}
}
public static func parse_videoSizeStickerMarkup(_ reader: BufferReader) -> VideoSize? {
var _1: Api.InputStickerSet?
if let signature = reader.readInt32() {
_1 = Api.parse(reader, signature: signature) as? Api.InputStickerSet
}
var _2: Int64?
_2 = reader.readInt64()
var _3: [Int32]?
@ -995,7 +1027,7 @@ public extension Api {
let _c2 = _2 != nil
let _c3 = _3 != nil
if _c1 && _c2 && _c3 {
return Api.VideoSize.videoSizeEmojiMarkup(type: _1!, emojiId: _2!, backgroundColors: _3!)
return Api.VideoSize.videoSizeStickerMarkup(stickerset: _1!, stickerId: _2!, backgroundColors: _3!)
}
else {
return nil

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 {
enum ContentSettings: TypeConstructorDescription {
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 {
static func deleteSecureValue(types: [Api.SecureValueType]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer()
@ -298,6 +313,21 @@ public extension Api.functions.account {
})
}
}
public extension Api.functions.account {
static func getAutoSaveSettings() -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.account.AutoSaveSettings>) {
let buffer = Buffer()
buffer.appendInt32(-1379156774)
return (FunctionDescription(name: "account.getAutoSaveSettings", parameters: []), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.account.AutoSaveSettings? in
let reader = BufferReader(buffer)
var result: Api.account.AutoSaveSettings?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.account.AutoSaveSettings
}
return result
})
}
}
public extension Api.functions.account {
static func getChatThemes(hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.account.Themes>) {
let buffer = Buffer()
@ -889,6 +919,23 @@ public extension Api.functions.account {
})
}
}
public extension Api.functions.account {
static func saveAutoSaveSettings(flags: Int32, peer: Api.InputPeer?, settings: Api.AutoSaveSettings) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer()
buffer.appendInt32(-694451359)
serializeInt32(flags, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 3) != 0 {peer!.serialize(buffer, true)}
settings.serialize(buffer, true)
return (FunctionDescription(name: "account.saveAutoSaveSettings", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("settings", String(describing: settings))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
let reader = BufferReader(buffer)
var result: Api.Bool?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.Bool
}
return result
})
}
}
public extension Api.functions.account {
static func saveRingtone(id: Api.InputDocument, unsave: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.account.SavedRingtone>) {
let buffer = Buffer()

View File

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

View File

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

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())
videoRepresentations.append(TelegramMediaImage.VideoRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, startTimestamp: videoStartTs))
case let .videoSizeEmojiMarkup(_, emojiId, backgroundColors):
case let .videoSizeEmojiMarkup(emojiId, backgroundColors):
emojiMarkup = TelegramMediaImage.EmojiMarkup(fileId: emojiId, backgroundColors: backgroundColors)
case .videoSizeStickerMarkup:
break
}
}
}

View File

@ -79,7 +79,6 @@ private extension PremiumPromoConfiguration {
var productOptions: [PremiumProductOption] = []
for option in options {
if case let .premiumSubscriptionOption(flags, transaction, months, currency, amount, botUrl, storeProduct) = option {
productOptions.append(PremiumProductOption(isCurrent: (flags & (1 << 1)) != 0, months: months, currency: currency, amount: amount, botUrl: botUrl, transactionId: transaction, availableForUpgrade: (flags & (1 << 2)) != 0, storeProductId: storeProduct))
}

View File

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

View File

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

View File

@ -4,7 +4,6 @@ import SwiftSignalKit
import MtProtoKit
import TelegramApi
public enum UpdatePeerPhotoStatus {
case progress(Float)
case complete([TelegramMediaImageRepresentation])
@ -153,7 +152,7 @@ func _internal_updatePeerPhotoInternal(postbox: Postbox, network: Network, state
var videoEmojiMarkup: Api.VideoSize?
if let fileId, let backgroundColors {
videoEmojiMarkup = .videoSizeEmojiMarkup(type: "e", emojiId: fileId, backgroundColors: backgroundColors)
videoEmojiMarkup = .videoSizeEmojiMarkup(emojiId: fileId, backgroundColors: backgroundColors)
flags |= (1 << 4)
}
@ -217,7 +216,7 @@ func _internal_updatePeerPhotoInternal(postbox: Postbox, network: Network, state
resource = CloudPhotoSizeMediaResource(datacenterId: dcId, photoId: id, accessHash: accessHash, sizeSpec: type, size: Int64(size), fileReference: fileReference.makeData())
videoRepresentations.append(TelegramMediaImage.VideoRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, startTimestamp: videoStartTs))
case .videoSizeEmojiMarkup:
case .videoSizeEmojiMarkup, .videoSizeStickerMarkup:
break
}
}
@ -280,9 +279,9 @@ func _internal_updatePeerPhotoInternal(postbox: Postbox, network: Network, state
let request: Signal<Api.Updates, MTRpcError>
if let peer = peer as? TelegramGroup {
request = network.request(Api.functions.messages.editChatPhoto(chatId: peer.id.id._internalGetInt64Value(), photo: .inputChatUploadedPhoto(flags: flags, file: file, video: videoFile, videoStartTs: videoStartTimestamp)))
request = network.request(Api.functions.messages.editChatPhoto(chatId: peer.id.id._internalGetInt64Value(), photo: .inputChatUploadedPhoto(flags: flags, file: file, video: videoFile, videoStartTs: videoStartTimestamp, videoEmojiMarkup: nil)))
} else if let peer = peer as? TelegramChannel, let inputChannel = apiInputChannel(peer) {
request = network.request(Api.functions.channels.editPhoto(channel: inputChannel, photo: .inputChatUploadedPhoto(flags: flags, file: file, video: videoFile, videoStartTs: videoStartTimestamp)))
request = network.request(Api.functions.channels.editPhoto(channel: inputChannel, photo: .inputChatUploadedPhoto(flags: flags, file: file, video: videoFile, videoStartTs: videoStartTimestamp, videoEmojiMarkup: nil)))
} else {
assertionFailure()
request = .complete()

View File

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

View File

@ -21,6 +21,8 @@ import GradientBackground
import LegacyComponents
import DrawingUI
import SolidRoundedButtonComponent
import AnimationCache
import EmojiTextAttachmentView
enum AvatarBackground: Equatable {
case gradient([UInt32])
@ -56,7 +58,7 @@ private let defaultBackgrounds: [AvatarBackground] = [
.gradient([0x82b1ff, 0x665fff]),
]
private struct KeyboardInputData: Equatable {
public struct AvatarKeyboardInputData: Equatable {
var emoji: EmojiPagerContentComponent
var stickers: EmojiPagerContentComponent?
@ -110,7 +112,8 @@ final class AvatarEditorScreenComponent: Component {
final class State: ComponentState {
let context: AccountContext
let ready: Promise<Bool>
var selectedBackground: AvatarBackground
var selectedFile: TelegramMediaFile?
@ -124,8 +127,9 @@ final class AvatarEditorScreenComponent: Component {
var isSearchActive: Bool = false
init(context: AccountContext, initialFileId: Int64?, initialBackgroundColors: [Int32]?) {
init(context: AccountContext, ready: Promise<Bool>, initialFileId: Int64?, initialBackgroundColors: [Int32]?) {
self.context = context
self.ready = ready
self.selectedBackground = defaultBackgrounds.first!
self.previousColor = self.selectedBackground
@ -153,6 +157,7 @@ final class AvatarEditorScreenComponent: Component {
func makeState() -> State {
return State(
context: self.context,
ready: self.ready,
initialFileId: self.initialFileId,
initialBackgroundColors: self.initialBackgroundColors
)
@ -187,7 +192,7 @@ final class AvatarEditorScreenComponent: Component {
private var controller: (() -> AvatarEditorScreen?)?
private var dataDisposable: Disposable?
private var data: KeyboardInputData?
private var data: AvatarKeyboardInputData?
private let emojiSearchDisposable = MetaDisposable()
private let emojiSearchResult = Promise<(groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable)?>(nil)
@ -232,7 +237,7 @@ final class AvatarEditorScreenComponent: Component {
self.emojiSearchDisposable.dispose()
}
private func updateData(_ data: KeyboardInputData) {
private func updateData(_ data: AvatarKeyboardInputData) {
let wasEmpty = self.data == nil
self.data = data
@ -240,6 +245,7 @@ final class AvatarEditorScreenComponent: Component {
self.state?.selectedFile = data.emoji.panelItemGroups.first?.items.first?.itemFile
}
self.state?.updated(transition: .immediate)
self.state?.ready.set(.single(true))
let updateSearchQuery: (String, String) -> Void = { [weak self] rawQuery, languageCode in
guard let strongSelf = self, let context = strongSelf.state?.context else {
@ -424,7 +430,7 @@ final class AvatarEditorScreenComponent: Component {
return
}
let context = controller.context
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks)
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks)
let _ = (context.account.postbox.combinedView(keys: [viewKey])
|> take(1)
|> deliverOnMainQueue).start(next: { views in
@ -664,7 +670,7 @@ final class AvatarEditorScreenComponent: Component {
func update(component: AvatarEditorScreenComponent, availableSize: CGSize, state: State, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
self.component = component
self.state = state
let environment = environment[ViewControllerComponentContainer.Environment.self].value
let strings = environment.strings
@ -738,45 +744,12 @@ final class AvatarEditorScreenComponent: Component {
self.keyboardContainerView.backgroundColor = environment.theme.list.itemBlocksBackgroundColor
self.panelSeparatorView.backgroundColor = environment.theme.list.itemPlainSeparatorColor
if self.dataDisposable == nil {
if self.dataDisposable == nil, let controller = controller() as? AvatarEditorScreen {
let context = component.context
let emojiItems = EmojiPagerContentComponent.emojiInputData(
context: context,
animationCache: context.animationCache,
animationRenderer: context.animationRenderer,
isStandalone: false,
isStatusSelection: false,
isReactionSelection: false,
isEmojiSelection: false,
isProfilePhotoEmojiSelection: true,
topReactionItems: [],
areUnicodeEmojiEnabled: false,
areCustomEmojiEnabled: true,
chatPeerId: context.account.peerId,
hasSearch: true,
forceHasPremium: true
)
let stickerItems = EmojiPagerContentComponent.stickerInputData(
context: context,
animationCache: context.animationCache,
animationRenderer: context.animationRenderer,
stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks],
stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers],
chatPeerId: context.account.peerId,
hasSearch: true,
hasTrending: false,
forceHasPremium: true,
searchIsPlaceholderOnly: false
)
let signal = combineLatest(queue: .mainQueue(),
emojiItems,
stickerItems,
controller.inputData |> delay(0.01, queue: .mainQueue()),
self.emojiSearchResult.get()
) |> map { emoji, stickers, searchResult -> (KeyboardInputData, (groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable)?) in
return (KeyboardInputData(emoji: emoji, stickers: stickers), searchResult)
}
)
self.dataDisposable = (signal
|> deliverOnMainQueue
).start(next: { [weak self, weak state] data, searchResult in
@ -819,7 +792,7 @@ final class AvatarEditorScreenComponent: Component {
tapped: { [weak state] in
if let state, !state.editingColor {
state.expanded = !state.expanded
state.updated(transition: .easeInOut(duration: 0.3))
state.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring)))
}
}
)
@ -833,7 +806,7 @@ final class AvatarEditorScreenComponent: Component {
}
let previewScale = effectiveIsExpanded ? 1.0 : collapsedAvatarSize.width / avatarPreviewSize.width
let cornerRadius = effectiveIsExpanded ? 0.0 : availableSize.width / 2.0
let cornerRadius = effectiveIsExpanded ? 0.0 : availableSize.width / (component.peerType == .forum ? 4.0 : 2.0)
let position = effectiveIsExpanded ? avatarPreviewSize.height / 2.0 : environment.navigationHeight + 10.0
transition.setBounds(view: previewView, bounds: CGRect(origin: .zero, size: avatarPreviewSize))
@ -1095,6 +1068,9 @@ final class AvatarEditorScreenComponent: Component {
hideTopPanelUpdated: { [weak self] hideTopPanel, transition in
if let strongSelf = self {
strongSelf.state?.isSearchActive = hideTopPanel
if hideTopPanel {
strongSelf.state?.expanded = false
}
strongSelf.state?.updated(transition: transition)
}
},
@ -1147,7 +1123,7 @@ final class AvatarEditorScreenComponent: Component {
switch component.peerType {
case .user:
buttonText = strings.AvatarEditor_SetProfilePhoto
case .group:
case .group, .forum:
buttonText = strings.AvatarEditor_SetGroupPhoto
case .channel:
buttonText = strings.AvatarEditor_SetChannelPhoto
@ -1180,66 +1156,106 @@ final class AvatarEditorScreenComponent: Component {
return availableSize
}
private let queue = Queue()
func complete() {
guard let state = self.state, let itemFile = state.selectedFile, let previewView = self.previewView.view else {
guard let state = self.state, let file = state.selectedFile, let controller = self.controller?() else {
return
}
let size = CGSize(width: 1920.0, height: 1920.0)
let image = state.selectedBackground.generateImage(size: size)
let tempPath = NSTemporaryDirectory() + "/\(UInt64.random(in: 0 ... UInt64.max)).jpg"
let tempUrl = NSURL(fileURLWithPath: tempPath) as URL
try? image.jpegData(compressionQuality: 1.0)?.write(to: tempUrl)
let entity = DrawingStickerEntity(content: .file(itemFile))
entity.referenceDrawingSize = size
entity.position = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
entity.scale = 3.3
var documentId: Int64 = 0
if case let .file(file) = entity.content, file.isCustomEmoji {
documentId = file.fileId.id
}
let colors: [NSNumber] = state.selectedBackground.colors.map { Int32(bitPattern: $0) as NSNumber }
let entitiesData = DrawingEntitiesView.encodeEntities([entity])
let paintingData = TGPaintingData(
drawing: nil,
entitiesData: entitiesData,
image: nil,
stillImage: nil,
hasAnimation: entity.isAnimated,
stickers: []
)
let adjustments = PGPhotoEditorValues(
originalSize: size,
cropRect: CGRect(origin: .zero, size: size),
cropRotation: 0.0,
cropOrientation: .up,
cropLockedAspectRatio: 1.0,
cropMirrored: false,
toolValues: [:],
paintingData: paintingData,
sendAsGif: true
)
let preset: TGMediaVideoConversionPreset = TGMediaVideoConversionPresetProfileHigh
let combinedImage = generateImage(previewView.bounds.size, contextGenerator: { size, context in
let bounds = CGRect(origin: .zero, size: size)
if let cgImage = image.cgImage {
context.draw(cgImage, in: bounds)
let context = controller.context
let _ = context.animationCache.getFirstFrame(queue: self.queue, sourceId: file.resource.id.stringRepresentation, size: CGSize(width: 640.0, height: 640.0), fetch: animationCacheFetchFile(context: context, userLocation: .other, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: true, customColor: nil), completion: { result in
guard let item = result.item else {
return
}
var image: UIImage?
if let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) {
switch frame.frame.format {
case let .rgba(data, width, height, bytesPerRow):
guard let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow) else {
return
}
data.withUnsafeBytes { bytes -> Void in
memcpy(context.bytes, bytes.baseAddress!, height * bytesPerRow)
}
image = context.generateImage()
default:
return
}
}
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
previewView.layer.render(in: context)
}, opaque: false)!
self.controller?()?.completion(combinedImage, tempUrl, TGVideoEditAdjustments(photoEditorValues: adjustments, preset: preset, documentId: documentId, colors: colors), { [weak self] in
self?.controller?()?.dismiss()
Queue.mainQueue().async {
guard let image else {
return
}
let size = CGSize(width: 800.0, height: 800.0)
let backgroundImage = state.selectedBackground.generateImage(size: size)
let tempPath = NSTemporaryDirectory() + "/\(UInt64.random(in: 0 ... UInt64.max)).jpg"
let tempUrl = NSURL(fileURLWithPath: tempPath) as URL
try? backgroundImage.jpegData(compressionQuality: 0.8)?.write(to: tempUrl)
let drawingSize = CGSize(width: 1920.0, height: 1920.0)
let entity = DrawingStickerEntity(content: .file(file))
entity.referenceDrawingSize = drawingSize
entity.position = CGPoint(x: drawingSize.width / 2.0, y: drawingSize.height / 2.0)
entity.scale = 3.3
var documentId: Int64 = 0
if case let .file(file) = entity.content, file.isCustomEmoji {
documentId = file.fileId.id
}
let colors: [NSNumber] = state.selectedBackground.colors.map { Int32(bitPattern: $0) as NSNumber }
let entitiesData = DrawingEntitiesView.encodeEntities([entity])
let paintingData = TGPaintingData(
drawing: nil,
entitiesData: entitiesData,
image: nil,
stillImage: nil,
hasAnimation: entity.isAnimated,
stickers: []
)
let adjustments = PGPhotoEditorValues(
originalSize: size,
cropRect: CGRect(origin: .zero, size: size),
cropRotation: 0.0,
cropOrientation: .up,
cropLockedAspectRatio: 1.0,
cropMirrored: false,
toolValues: [:],
paintingData: paintingData,
sendAsGif: true
)
let preset: TGMediaVideoConversionPreset = TGMediaVideoConversionPresetProfileHigh
let combinedImage = generateImage(size, contextGenerator: { size, context in
let bounds = CGRect(origin: .zero, size: size)
if let cgImage = backgroundImage.cgImage {
context.draw(cgImage, in: bounds)
}
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 0.67, y: 0.67)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
if let cgImage = image.cgImage {
context.draw(cgImage, in: bounds)
}
}, opaque: false)!
if entity.isAnimated {
controller.videoCompletion(combinedImage, tempUrl, TGVideoEditAdjustments(photoEditorValues: adjustments, preset: preset, documentId: documentId, colors: colors), { [weak controller] in
controller?.dismiss()
})
} else {
controller.imageCompletion(combinedImage, { [weak controller] in
controller?.dismiss()
})
}
}
})
}
}
@ -1259,18 +1275,65 @@ public final class AvatarEditorScreen: ViewControllerComponentContainer {
case user
case group
case channel
case forum
}
fileprivate let context: AccountContext
fileprivate let inputData: Signal<AvatarKeyboardInputData, NoError>
private let readyValue = Promise<Bool>()
override public var ready: Promise<Bool> {
return self.readyValue
}
public var completion: (UIImage, URL, TGVideoEditAdjustments, @escaping () -> Void) -> Void = { _, _, _, _ in }
public var imageCompletion: (UIImage, @escaping () -> Void) -> Void = { _, _ in }
public var videoCompletion: (UIImage, URL, TGVideoEditAdjustments, @escaping () -> Void) -> Void = { _, _, _, _ in }
public init(context: AccountContext, peerType: PeerType, initialFileId: Int64?, initialBackgroundColors: [Int32]?) {
public static func inputData(context: AccountContext, isGroup: Bool) -> Signal<AvatarKeyboardInputData, NoError> {
let emojiItems = EmojiPagerContentComponent.emojiInputData(
context: context,
animationCache: context.animationCache,
animationRenderer: context.animationRenderer,
isStandalone: false,
isStatusSelection: false,
isReactionSelection: false,
isEmojiSelection: false,
isProfilePhotoEmojiSelection: !isGroup,
isGroupPhotoEmojiSelection: isGroup,
topReactionItems: [],
areUnicodeEmojiEnabled: false,
areCustomEmojiEnabled: true,
chatPeerId: context.account.peerId,
hasSearch: true,
forceHasPremium: true
)
let stickerItems = EmojiPagerContentComponent.stickerInputData(
context: context,
animationCache: context.animationCache,
animationRenderer: context.animationRenderer,
stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks],
stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers],
chatPeerId: context.account.peerId,
hasSearch: true,
hasTrending: false,
forceHasPremium: true,
searchIsPlaceholderOnly: false,
isProfilePhotoEmojiSelection: !isGroup,
isGroupPhotoEmojiSelection: isGroup
)
let signal = combineLatest(queue: .mainQueue(),
emojiItems,
stickerItems
) |> map { emoji, stickers -> AvatarKeyboardInputData in
return AvatarKeyboardInputData(emoji: emoji, stickers: stickers)
}
return signal
}
public init(context: AccountContext, inputData: Signal<AvatarKeyboardInputData, NoError>, peerType: PeerType, initialFileId: Int64?, initialBackgroundColors: [Int32]?) {
self.context = context
self.inputData = inputData
let componentReady = Promise<Bool>()
super.init(context: context, component: AvatarEditorScreenComponent(context: context, ready: componentReady, peerType: peerType, initialFileId: initialFileId, initialBackgroundColors: initialBackgroundColors), navigationBarAppearance: .transparent)

View File

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

View File

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

View File

@ -1789,8 +1789,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
transition.updateFrame(node: self.inputPanelBackgroundNode, frame: apparentInputBackgroundFrame, beginWithCurrentState: true)
if let navigationBarBackgroundContent = self.navigationBarBackgroundContent {
transition.updateFrame(node: navigationBarBackgroundContent, frame: CGRect(origin: .zero, size: CGSize(width: layout.size.width, height: navigationBarHeight + (titleAccessoryPanelBackgroundHeight ?? 0.0))), beginWithCurrentState: true)
navigationBarBackgroundContent.update(rect: CGRect(origin: .zero, size: CGSize(width: layout.size.width, height: navigationBarHeight + (titleAccessoryPanelBackgroundHeight ?? 0.0))), within: layout.size, transition: transition)
transition.updateFrame(node: navigationBarBackgroundContent, frame: CGRect(origin: .zero, size: CGSize(width: layout.size.width, height: navigationBarHeight + (titleAccessoryPanelBackgroundHeight ?? 0.0) + (translationPanelHeight ?? 0.0))), beginWithCurrentState: true)
navigationBarBackgroundContent.update(rect: CGRect(origin: .zero, size: CGSize(width: layout.size.width, height: navigationBarHeight + (titleAccessoryPanelBackgroundHeight ?? 0.0) + (translationPanelHeight ?? 0.0))), within: layout.size, transition: transition)
}
if let inputPanelBackgroundContent = self.inputPanelBackgroundContent {

View File

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

View File

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

View File

@ -18,6 +18,7 @@ import PeerInfoUI
import MapResourceToAvatarSizes
import LegacyMediaPickerUI
import TextFormat
import AvatarEditorScreen
private struct CreateChannelArguments {
let context: AccountContext
@ -623,6 +624,9 @@ public func createChannelController(context: AccountContext, mode: CreateChannel
}
}
let keyboardInputData = Promise<AvatarKeyboardInputData>()
keyboardInputData.set(AvatarEditorScreen.inputData(context: context, isGroup: true))
let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: stateValue.with({ $0.avatar }) != nil, hasViewButton: false, personalPhoto: false, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: false, title: nil, isSuggesting: false)!
mixin.stickersContext = LegacyPaintStickersContext(context: context)
let _ = currentAvatarMixin.swap(mixin)
@ -633,6 +637,27 @@ public func createChannelController(context: AccountContext, mode: CreateChannel
}))
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}
mixin.requestAvatarEditor = { imageCompletion, videoCompletion in
guard let imageCompletion, let videoCompletion else {
return
}
let peerType: AvatarEditorScreen.PeerType
if case .legacyGroup = peer {
peerType = .group
} else if case let .channel(channel) = peer {
if case .group = channel.info {
peerType = channel.flags.contains(.isForum) ? .forum : .group
} else {
peerType = .channel
}
} else {
peerType = .user
}
let controller = AvatarEditorScreen(context: context, inputData: keyboardInputData.get(), peerType: peerType, initialFileId: nil, initialBackgroundColors: nil)
controller.imageCompletion = imageCompletion
controller.videoCompletion = videoCompletion
pushControllerImpl?(controller)
}
mixin.didFinishWithImage = { image in
if let image = image {
completedChannelPhotoImpl(image)

View File

@ -30,6 +30,7 @@ import ContextUI
import ChatTimerScreen
import AsyncDisplayKit
import TextFormat
import AvatarEditorScreen
private struct CreateGroupArguments {
let context: AccountContext
@ -947,6 +948,9 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
}
}
let keyboardInputData = Promise<AvatarKeyboardInputData>()
keyboardInputData.set(AvatarEditorScreen.inputData(context: context, isGroup: true))
let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: stateValue.with({ $0.avatar }) != nil, hasViewButton: false, personalPhoto: false, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: false, title: nil, isSuggesting: false)!
mixin.stickersContext = LegacyPaintStickersContext(context: context)
let _ = currentAvatarMixin.swap(mixin)
@ -957,6 +961,27 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
}))
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}
mixin.requestAvatarEditor = { imageCompletion, videoCompletion in
guard let imageCompletion, let videoCompletion else {
return
}
let peerType: AvatarEditorScreen.PeerType
if case .legacyGroup = peer {
peerType = .group
} else if case let .channel(channel) = peer {
if case .group = channel.info {
peerType = channel.flags.contains(.isForum) ? .forum : .group
} else {
peerType = .channel
}
} else {
peerType = .user
}
let controller = AvatarEditorScreen(context: context, inputData: keyboardInputData.get(), peerType: peerType, initialFileId: nil, initialBackgroundColors: nil)
controller.imageCompletion = imageCompletion
controller.videoCompletion = videoCompletion
pushImpl?(controller)
}
mixin.didFinishWithImage = { image in
if let image = image {
completedGroupPhotoImpl(image)

View File

@ -4859,7 +4859,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
self?.openStats()
})))
}
if cachedData.flags.contains(.translationHidden) {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ContextMenuTranslate, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Translate"), color: theme.contextMenu.primaryColor)
@ -7247,6 +7246,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
confirmationAction = nil
}
let keyboardInputData = Promise<AvatarKeyboardInputData>()
keyboardInputData.set(AvatarEditorScreen.inputData(context: strongSelf.context, isGroup: peer.id.namespace != Namespaces.Peer.CloudUser))
let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasDeleteButton, hasViewButton: false, personalPhoto: strongSelf.isSettings, isVideo: currentIsVideo, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: isForum, title: title, isSuggesting: [.custom, .suggest].contains(mode))!
mixin.stickersContext = LegacyPaintStickersContext(context: strongSelf.context)
let _ = strongSelf.currentAvatarMixin.swap(mixin)
@ -7265,8 +7267,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
completion(nil)
}
}
mixin.requestAvatarEditor = { [weak self] completion in
guard let strongSelf = self, let completion else {
mixin.requestAvatarEditor = { [weak self] imageCompletion, videoCompletion in
guard let strongSelf = self, let imageCompletion, let videoCompletion else {
return
}
let peerType: AvatarEditorScreen.PeerType
@ -7274,15 +7276,16 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
peerType = .group
} else if case let .channel(channel) = peer {
if case .group = channel.info {
peerType = .group
peerType = channel.flags.contains(.isForum) ? .forum : .group
} else {
peerType = .channel
}
} else {
peerType = .user
}
let controller = AvatarEditorScreen(context: strongSelf.context, peerType: peerType, initialFileId: emojiMarkup?.fileId, initialBackgroundColors: emojiMarkup?.backgroundColors)
controller.completion = completion
let controller = AvatarEditorScreen(context: strongSelf.context, inputData: keyboardInputData.get(), peerType: peerType, initialFileId: emojiMarkup?.fileId, initialBackgroundColors: emojiMarkup?.backgroundColors)
controller.imageCompletion = imageCompletion
controller.videoCompletion = videoCompletion
(strongSelf.controller?.navigationController?.topViewController as? ViewController)?.push(controller)
}