Merge commit '525f8cc179fc08fe421e75e7ddc476cfc2cbdcba'

# Conflicts:
#	Telegram/Telegram-iOS/en.lproj/Localizable.strings
This commit is contained in:
Isaac 2024-10-05 20:20:32 +04:00
commit 89d2ad8cf6
39 changed files with 1589 additions and 356 deletions

View File

@ -12950,7 +12950,7 @@ Sorry for the inconvenience.";
"SharedMedia.GiftCount_any" = "%@ gifts";
"Stars.Info.Title" = "What are Stars?";
"Stars.Info.Description" = "Buy packages of Stars on Telegram that let you do following:";
"Stars.Info.Description" = "Telegram Stars allow users to:";
"Stars.Info.Gift.Title" = "Send Gifts to Friends";
"Stars.Info.Gift.Text" = "Give your friends gifts that can be kept on their profiles or converted to Stars.";
"Stars.Info.Miniapp.Title" = "Use Stars in Miniapps";
@ -12976,6 +12976,7 @@ Sorry for the inconvenience.";
"Gift.View.HiddenName" = "Hidden Name";
"Gift.View.Date" = "Date";
"Gift.View.Availability" = "Availability";
"Gift.View.Availability.Of" = "%1$@ of %2$@";
"Gift.View.Hide" = "Hide from My Page";
"Gift.View.Display" = "Display on My Page";
"Gift.View.Convert" = "Convert to %@";
@ -12986,7 +12987,7 @@ Sorry for the inconvenience.";
"Gift.Hidden.Title" = "Gift Removed from Profile";
"Gift.Hidden.Text" = "The gift is no longer displayed in [your profile]().";
"Gift.Convert.Title" = "Convert Gift to Stars";
"Gift.Convert.Text" = "Do you want to convert this gift from **%1$@** to **%2$@**?\n\nThis action cannot be undone.";
"Gift.Convert.Text" = "Do you want to convert this gift from **%1$@** to **%2$@**?\n\nThis will permanently destroy the gift.";
"Gift.Convert.Stars_1" = "%@ Star";
"Gift.Convert.Stars_any" = "%@ Stars";
"Gift.Convert.Convert" = "Convert";
@ -13032,10 +13033,12 @@ Sorry for the inconvenience.";
"Gift.Send.Title" = "Send a Gift";
"Gift.Send.Customize.Title" = "CUSTOMIZE YOUR GIFT";
"Gift.Send.Customize.MessagePlaceholder" = "Enter Message";
"Gift.Send.Customize.MessagePlaceholder" = "Enter Message (Optional)";
"Gift.Send.Customize.Info" = "Only %@ will see your message.";
"Gift.Send.HideMyName" = "Hide My Name";
"Gift.Send.HideMyName.Info" = "Hide my name and message from visitors to %1$@'s profile. %2$@ will still see your name and message.";
"Gift.Send.Send" = "Send a Gift for";
"Gift.Send.Limited" = "Limited";
"Profile.SendGift" = "Send a Gift";
"Settings.SendGift" = "Send a Gift";
@ -13047,9 +13050,22 @@ Sorry for the inconvenience.";
"Report.Title.User" = "Report User";
"Report.Title.Group" = "Report Group";
"Report.Title.Channel" = "Report Channel";
"Report.Title.Bot" = "Report Bot";
"Report.Comment.Placeholder" = "Add Comment";
"Report.Comment.Placeholder.Optional" = "Add Comment (Optional)";
"Report.Comment.Info" = "Please help us by telling what is wrong with the message you have selected.";
"Report.Send" = "Send Report";
f
"Notification.PremiumGift.MonthsTitle_1" = "%@ Month Premium";
"Notification.PremiumGift.MonthsTitle_any" = "%@ Months Premium";
"Notification.PremiumGift.YearsTitle_1" = "%@ Year Premium";
"Notification.PremiumGift.YearsTitle_any" = "%@ Years Premium";
"Notification.PremiumGift.SubscriptionDescription" = "Subscription for exclusive Telegram features.";
"Notification.StarsGift.Stars_1" = "%@ Star";
"Notification.StarsGift.Stars_any" = "%@ Stars";
"WebBrowser.AuthChallenge.Title" = "Sign in to %@";
"WebBrowser.AuthChallenge.Text" = "Your login information will be sent securely.";

View File

@ -1041,7 +1041,7 @@ public protocol SharedAccountContext: AnyObject {
func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal<MiniAppListScreenInitialData, NoError>
func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController
func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool)
func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?)
func makeDebugSettingsController(context: AccountContext?) -> ViewController?

View File

@ -143,6 +143,7 @@ public struct PremiumConfiguration {
showPremiumGiftInTextField: false,
giveawayGiftsPurchaseAvailable: false,
starsGiftsPurchaseAvailable: false,
starGiftsPurchaseBlocked: true,
boostsPerGiftCount: 3,
audioTransciptionTrialMaxDuration: 300,
audioTransciptionTrialCount: 2,
@ -170,6 +171,7 @@ public struct PremiumConfiguration {
public let showPremiumGiftInTextField: Bool
public let giveawayGiftsPurchaseAvailable: Bool
public let starsGiftsPurchaseAvailable: Bool
public let starGiftsPurchaseBlocked: Bool
public let boostsPerGiftCount: Int32
public let audioTransciptionTrialMaxDuration: Int32
public let audioTransciptionTrialCount: Int32
@ -196,6 +198,7 @@ public struct PremiumConfiguration {
showPremiumGiftInTextField: Bool,
giveawayGiftsPurchaseAvailable: Bool,
starsGiftsPurchaseAvailable: Bool,
starGiftsPurchaseBlocked: Bool,
boostsPerGiftCount: Int32,
audioTransciptionTrialMaxDuration: Int32,
audioTransciptionTrialCount: Int32,
@ -221,6 +224,7 @@ public struct PremiumConfiguration {
self.showPremiumGiftInTextField = showPremiumGiftInTextField
self.giveawayGiftsPurchaseAvailable = giveawayGiftsPurchaseAvailable
self.starsGiftsPurchaseAvailable = starsGiftsPurchaseAvailable
self.starGiftsPurchaseBlocked = starGiftsPurchaseBlocked
self.boostsPerGiftCount = boostsPerGiftCount
self.audioTransciptionTrialMaxDuration = audioTransciptionTrialMaxDuration
self.audioTransciptionTrialCount = audioTransciptionTrialCount
@ -254,6 +258,7 @@ public struct PremiumConfiguration {
showPremiumGiftInTextField: data["premium_gift_text_field_icon"] as? Bool ?? defaultValue.showPremiumGiftInTextField,
giveawayGiftsPurchaseAvailable: data["giveaway_gifts_purchase_available"] as? Bool ?? defaultValue.giveawayGiftsPurchaseAvailable,
starsGiftsPurchaseAvailable: data["stars_gifts_enabled"] as? Bool ?? defaultValue.starsGiftsPurchaseAvailable,
starGiftsPurchaseBlocked: data["stargifts_blocked"] as? Bool ?? defaultValue.starGiftsPurchaseBlocked,
boostsPerGiftCount: get(data["boosts_per_sent_gift"]) ?? defaultValue.boostsPerGiftCount,
audioTransciptionTrialMaxDuration: get(data["transcribe_audio_trial_duration_max"]) ?? defaultValue.audioTransciptionTrialMaxDuration,
audioTransciptionTrialCount: get(data["transcribe_audio_trial_weekly_number"]) ?? defaultValue.audioTransciptionTrialCount,

View File

@ -399,7 +399,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
case topic(EnginePeer, ChatListItemContent.ThreadInfo, Int, PresentationTheme, PresentationStrings, ChatListSearchSectionExpandType)
case recentlySearchedPeer(EnginePeer, EnginePeer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, PeerStoryStats?, Bool)
case localPeer(EnginePeer, EnginePeer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType, PeerStoryStats?, Bool)
case globalPeer(FoundPeer, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType, PeerStoryStats?, Bool)
case globalPeer(FoundPeer, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType, PeerStoryStats?, Bool, String?)
case message(EngineMessage, EngineRenderedPeer, EnginePeerReadCounters?, EngineMessageHistoryThread.Info?, ChatListPresentationData, Int32, Bool?, Bool, MessageOrderingKey, (id: String, size: Int64, isFirstInList: Bool)?, MessageSection, Bool, PeerStoryStats?, Bool)
case addContact(String, PresentationTheme, PresentationStrings)
@ -411,7 +411,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
return .localPeerId(peer.id)
case let .localPeer(peer, _, _, _, _, _, _, _, _, _, _):
return .localPeerId(peer.id)
case let .globalPeer(peer, _, _, _, _, _, _, _, _, _):
case let .globalPeer(peer, _, _, _, _, _, _, _, _, _, _):
return .globalPeerId(peer.peer.id)
case let .message(message, _, _, _, _, _, _, _, _, _, section, _, _, _):
return .messageId(message.id, section)
@ -440,8 +440,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
} else {
return false
}
case let .globalPeer(lhsPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder, lhsExpandType, lhsStoryStats, lhsRequiresPremiumForMessaging):
if case let .globalPeer(rhsPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder, rhsExpandType, rhsStoryStats, rhsRequiresPremiumForMessaging) = rhs, lhsPeer == rhsPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 && lhsExpandType == rhsExpandType && lhsStoryStats == rhsStoryStats && lhsRequiresPremiumForMessaging == rhsRequiresPremiumForMessaging {
case let .globalPeer(lhsPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder, lhsExpandType, lhsStoryStats, lhsRequiresPremiumForMessaging, lhsQuery):
if case let .globalPeer(rhsPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder, rhsExpandType, rhsStoryStats, rhsRequiresPremiumForMessaging, rhsQuery) = rhs, lhsPeer == rhsPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 && lhsExpandType == rhsExpandType && lhsStoryStats == rhsStoryStats && lhsRequiresPremiumForMessaging == rhsRequiresPremiumForMessaging, lhsQuery == rhsQuery {
return true
} else {
return false
@ -543,11 +543,11 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
case .globalPeer, .message, .addContact:
return true
}
case let .globalPeer(_, _, lhsIndex, _, _, _, _, _, _, _):
case let .globalPeer(_, _, lhsIndex, _, _, _, _, _, _, _, _):
switch rhs {
case .topic, .recentlySearchedPeer, .localPeer:
return false
case let .globalPeer(_, _, rhsIndex, _, _, _, _, _, _, _):
case let .globalPeer(_, _, rhsIndex, _, _, _, _, _, _, _, _):
return lhsIndex <= rhsIndex
case .message, .addContact:
return true
@ -798,7 +798,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
openStories(peer.id, sourceNode.avatarNode)
}
})
case let .globalPeer(peer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType, storyStats, requiresPremiumForMessaging):
case let .globalPeer(peer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType, storyStats, requiresPremiumForMessaging, query):
var enabled = true
if filter.contains(.onlyWriteable) {
enabled = canSendMessagesToPeer(peer.peer)
@ -822,7 +822,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
var suffixString = ""
if let subscribers = peer.subscribers, subscribers != 0 {
if peer.peer is TelegramUser {
suffixString = ", \(strings.Conversation_StatusSubscribers(subscribers))"
suffixString = ", \(strings.Conversation_StatusBotSubscribers(subscribers))"
} else if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info {
suffixString = ", \(strings.Conversation_StatusSubscribers(subscribers))"
} else {
@ -858,7 +858,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
isSavedMessages = true
}
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch(isSavedMessages: isSavedMessages), peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .addressName(suffixString), badge: badge, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch(isSavedMessages: isSavedMessages), peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .addressName(suffixString), badge: badge, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, searchQuery: query, action: { _ in
interaction.peerSelected(EnginePeer(peer.peer), nil, nil, nil)
}, disabledAction: { _ in
interaction.disabledPeerSelected(EnginePeer(peer.peer), nil, requiresPremiumForMessaging ? .premiumRequired : .generic)
@ -2607,7 +2607,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
if !existingPeerIds.contains(peer.peer.id), filteredPeer(EnginePeer(peer.peer), EnginePeer(accountPeer)) {
existingPeerIds.insert(peer.peer.id)
entries.append(.globalPeer(peer, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalExpandType, nil, false))
entries.append(.globalPeer(peer, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalExpandType, nil, false, finalQuery))
index += 1
numberOfGlobalPeers += 1
}
@ -2953,7 +2953,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
if case let .user(user) = peer, user.flags.contains(.requirePremium) {
requiresPremiumForMessagingPeerIds.append(peer.id)
}
case let .globalPeer(foundPeer, _, _, _, _, _, _, _, _, _):
case let .globalPeer(foundPeer, _, _, _, _, _, _, _, _, _, _):
storyStatsIds.append(foundPeer.peer.id)
if let user = foundPeer.peer as? TelegramUser, user.flags.contains(.requirePremium) {
requiresPremiumForMessagingPeerIds.append(foundPeer.peer.id)
@ -2994,8 +2994,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
mappedItems[i] = .recentlySearchedPeer(peer, associatedPeer, unreadBadge, index, theme, strings, sortOrder, displayOrder, stats[peer.id] ?? nil, requiresPremiumForMessaging[peer.id] ?? false)
case let .localPeer(peer, associatedPeer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, _, _):
mappedItems[i] = .localPeer(peer, associatedPeer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, stats[peer.id] ?? nil, requiresPremiumForMessaging[peer.id] ?? false)
case let .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, _, _):
mappedItems[i] = .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, stats[peer.peer.id] ?? nil, requiresPremiumForMessaging[peer.peer.id] ?? false)
case let .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, _, _, searchQuery):
mappedItems[i] = .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, stats[peer.peer.id] ?? nil, requiresPremiumForMessaging[peer.peer.id] ?? false, searchQuery)
case let .message(message, peer, combinedPeerReadState, threadInfo, presentationData, totalCount, selected, displayCustomHeader, key, resourceId, section, allPaused, _, _):
mappedItems[i] = .message(message, peer, combinedPeerReadState, threadInfo, presentationData, totalCount, selected, displayCustomHeader, key, resourceId, section, allPaused, stats[peer.peerId] ?? nil, requiresPremiumForMessaging[peer.peerId] ?? false)
default:
@ -3672,7 +3672,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
url: "",
simple: true,
source: .generic,
skipTermsOfService: true
skipTermsOfService: true,
payload: nil
)
} else {
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(

View File

@ -1714,9 +1714,20 @@ public final class ChatListNode: ListView {
guard let self else {
return
}
let controller = self.context.sharedContext.makePremiumGiftController(context: self.context, source: .chatList(birthdays), completion: nil)
controller.navigationPresentation = .modal
self.push?(controller)
if let birthdays, birthdays.count == 1, let peerId = birthdays.keys.first {
let _ = (self.context.engine.payments.premiumGiftCodeOptions(peerId: nil, onlyCached: true)
|> filter { !$0.isEmpty }
|> deliverOnMainQueue).start(next: { giftOptions in
let premiumOptions = giftOptions.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) }
let controller = self.context.sharedContext.makeGiftOptionsController(context: self.context, peerId: peerId, premiumOptions: premiumOptions)
controller.navigationPresentation = .modal
self.push?(controller)
})
} else {
let controller = self.context.sharedContext.makePremiumGiftController(context: self.context, source: .chatList(birthdays), completion: nil)
controller.navigationPresentation = .modal
self.push?(controller)
}
}, openPremiumManagement: { [weak self] in
guard let self else {
return

View File

@ -175,6 +175,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
let options: [ItemListPeerItemRevealOption]
let additionalActions: [ContactsPeerItemAction]
let actionIcon: ContactsPeerItemActionIcon
let searchQuery: String?
let action: ((ContactsPeerItemPeer) -> Void)?
let disabledAction: ((ContactsPeerItemPeer) -> Void)?
let setPeerIdWithRevealedOptions: ((EnginePeer.Id?, EnginePeer.Id?) -> Void)?
@ -215,6 +216,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
actionIcon: ContactsPeerItemActionIcon = .none,
index: SortIndex?,
header: ListViewItemHeader?,
searchQuery: String? = nil,
action: ((ContactsPeerItemPeer) -> Void)?,
disabledAction: ((ContactsPeerItemPeer) -> Void)? = nil,
setPeerIdWithRevealedOptions: ((EnginePeer.Id?, EnginePeer.Id?) -> Void)? = nil,
@ -245,6 +247,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
self.options = options
self.additionalActions = additionalActions
self.actionIcon = actionIcon
self.searchQuery = searchQuery
self.action = action
self.disabledAction = disabledAction
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
@ -880,7 +883,16 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: activity ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemSecondaryTextColor)
}
case let .addressName(suffix):
if let addressName = peer.addressName {
var addressName = peer.addressName
if let currentAddressName = addressName, let searchQuery = item.searchQuery?.lowercased(), !peer.usernames.isEmpty && !currentAddressName.lowercased().contains(searchQuery) {
for username in peer.usernames {
if username.username.lowercased().contains(searchQuery) {
addressName = username.username
break
}
}
}
if let addressName {
let addressNameString = NSAttributedString(string: "@" + addressName, font: statusFont, textColor: item.presentationData.theme.list.itemAccentColor)
if !suffix.isEmpty {
let suffixString = NSAttributedString(string: suffix, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)

View File

@ -1,20 +0,0 @@
import Foundation
import UIKit
final class MinimizeKeyboardGestureRecognizer: UISwipeGestureRecognizer, UIGestureRecognizerDelegate {
override init(target: Any?, action: Selector?) {
super.init(target: target, action: action)
self.cancelsTouchesInView = false
self.delaysTouchesBegan = false
self.delaysTouchesEnded = false
self.delegate = self
self.direction = [.left, .right]
self.numberOfTouchesRequired = 2
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}

View File

@ -623,6 +623,8 @@ private final class PendingInAppPurchaseState: Codable {
case untilDate
case stars
case users
case text
case entities
}
enum PurposeType: Int32 {
@ -641,7 +643,7 @@ private final class PendingInAppPurchaseState: Codable {
case upgrade
case restore
case gift(peerId: EnginePeer.Id)
case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?)
case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?, text: String?, entities: [MessageTextEntity]?)
case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32)
case stars(count: Int64)
case starsGift(peerId: EnginePeer.Id, count: Int64)
@ -665,7 +667,9 @@ private final class PendingInAppPurchaseState: Codable {
case .giftCode:
self = .giftCode(
peerIds: try container.decode([Int64].self, forKey: .peers).map { EnginePeer.Id($0) },
boostPeer: try container.decodeIfPresent(Int64.self, forKey: .boostPeer).flatMap({ EnginePeer.Id($0) })
boostPeer: try container.decodeIfPresent(Int64.self, forKey: .boostPeer).flatMap({ EnginePeer.Id($0) }),
text: try container.decodeIfPresent(String.self, forKey: .text),
entities: try container.decodeIfPresent([MessageTextEntity].self, forKey: .entities)
)
case .giveaway:
self = .giveaway(
@ -718,10 +722,12 @@ private final class PendingInAppPurchaseState: Codable {
case let .gift(peerId):
try container.encode(PurposeType.gift.rawValue, forKey: .type)
try container.encode(peerId.toInt64(), forKey: .peer)
case let .giftCode(peerIds, boostPeer):
case let .giftCode(peerIds, boostPeer, text, entities):
try container.encode(PurposeType.giftCode.rawValue, forKey: .type)
try container.encode(peerIds.map { $0.toInt64() }, forKey: .peers)
try container.encodeIfPresent(boostPeer?.toInt64(), forKey: .boostPeer)
try container.encodeIfPresent(text, forKey: .text)
try container.encodeIfPresent(entities, forKey: .entities)
case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate):
try container.encode(PurposeType.giveaway.rawValue, forKey: .type)
try container.encode(boostPeer.toInt64(), forKey: .boostPeer)
@ -764,8 +770,8 @@ private final class PendingInAppPurchaseState: Codable {
self = .restore
case let .gift(peerId, _, _):
self = .gift(peerId: peerId)
case let .giftCode(peerIds, boostPeer, _, _):
self = .giftCode(peerIds: peerIds, boostPeer: boostPeer)
case let .giftCode(peerIds, boostPeer, _, _, text, entities):
self = .giftCode(peerIds: peerIds, boostPeer: boostPeer, text: text, entities: entities)
case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, _, _):
self = .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate)
case let .stars(count, _, _):
@ -788,8 +794,8 @@ private final class PendingInAppPurchaseState: Codable {
return .restore
case let .gift(peerId):
return .gift(peerId: peerId, currency: currency, amount: amount)
case let .giftCode(peerIds, boostPeer):
return .giftCode(peerIds: peerIds, boostPeer: boostPeer, currency: currency, amount: amount)
case let .giftCode(peerIds, boostPeer, text, entities):
return .giftCode(peerIds: peerIds, boostPeer: boostPeer, currency: currency, amount: amount, text: text, entities: entities)
case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate):
return .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount)
case let .stars(count):

View File

@ -1356,7 +1356,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
return
}
let (currency, amount) = selectedProduct.storeProduct.priceCurrencyAndAmount
purpose = .giftCode(peerIds: state.peers, boostPeer: peerId, currency: currency, amount: amount)
purpose = .giftCode(peerIds: state.peers, boostPeer: peerId, currency: currency, amount: amount, text: nil, entities: nil)
quantity = Int32(state.peers.count)
storeProduct = selectedProduct.storeProduct
case .starsGiveaway:

View File

@ -910,7 +910,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
if self.source == .profile || self.source == .attachMenu, let peerId = self.peerIds.first {
purpose = .gift(peerId: peerId, currency: currency, amount: amount)
} else {
purpose = .giftCode(peerIds: self.peerIds, boostPeer: nil, currency: currency, amount: amount)
purpose = .giftCode(peerIds: self.peerIds, boostPeer: nil, currency: currency, amount: amount, text: nil, entities: nil)
quantity = Int32(self.peerIds.count)
}

View File

@ -467,7 +467,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[70813275] = { return Api.InputStickeredMedia.parse_inputStickeredMediaDocument($0) }
dict[1251549527] = { return Api.InputStickeredMedia.parse_inputStickeredMediaPhoto($0) }
dict[1634697192] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentGiftPremium($0) }
dict[-1551868097] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiftCode($0) }
dict[-75955309] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiftCode($0) }
dict[369444042] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiveaway($0) }
dict[-1502273946] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumSubscription($0) }
dict[494149367] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentStarsGift($0) }
@ -551,8 +551,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-1230047312] = { return Api.MessageAction.parse_messageActionEmpty($0) }
dict[-1834538890] = { return Api.MessageAction.parse_messageActionGameScore($0) }
dict[-1730095465] = { return Api.MessageAction.parse_messageActionGeoProximityReached($0) }
dict[1737240073] = { return Api.MessageAction.parse_messageActionGiftCode($0) }
dict[-935499028] = { return Api.MessageAction.parse_messageActionGiftPremium($0) }
dict[1456486804] = { return Api.MessageAction.parse_messageActionGiftCode($0) }
dict[1818391802] = { return Api.MessageAction.parse_messageActionGiftPremium($0) }
dict[1171632161] = { return Api.MessageAction.parse_messageActionGiftStars($0) }
dict[-1475391004] = { return Api.MessageAction.parse_messageActionGiveawayLaunch($0) }
dict[-2015170219] = { return Api.MessageAction.parse_messageActionGiveawayResults($0) }

View File

@ -711,7 +711,7 @@ public extension Api {
public extension Api {
indirect enum InputStorePaymentPurpose: TypeConstructorDescription {
case inputStorePaymentGiftPremium(userId: Api.InputUser, currency: String, amount: Int64)
case inputStorePaymentPremiumGiftCode(flags: Int32, users: [Api.InputUser], boostPeer: Api.InputPeer?, currency: String, amount: Int64)
case inputStorePaymentPremiumGiftCode(flags: Int32, users: [Api.InputUser], boostPeer: Api.InputPeer?, currency: String, amount: Int64, message: Api.TextWithEntities?)
case inputStorePaymentPremiumGiveaway(flags: Int32, boostPeer: Api.InputPeer, additionalPeers: [Api.InputPeer]?, countriesIso2: [String]?, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64)
case inputStorePaymentPremiumSubscription(flags: Int32)
case inputStorePaymentStarsGift(userId: Api.InputUser, stars: Int64, currency: String, amount: Int64)
@ -728,9 +728,9 @@ public extension Api {
serializeString(currency, buffer: buffer, boxed: false)
serializeInt64(amount, buffer: buffer, boxed: false)
break
case .inputStorePaymentPremiumGiftCode(let flags, let users, let boostPeer, let currency, let amount):
case .inputStorePaymentPremiumGiftCode(let flags, let users, let boostPeer, let currency, let amount, let message):
if boxed {
buffer.appendInt32(-1551868097)
buffer.appendInt32(-75955309)
}
serializeInt32(flags, buffer: buffer, boxed: false)
buffer.appendInt32(481674261)
@ -741,6 +741,7 @@ public extension Api {
if Int(flags) & Int(1 << 0) != 0 {boostPeer!.serialize(buffer, true)}
serializeString(currency, buffer: buffer, boxed: false)
serializeInt64(amount, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 1) != 0 {message!.serialize(buffer, true)}
break
case .inputStorePaymentPremiumGiveaway(let flags, let boostPeer, let additionalPeers, let countriesIso2, let prizeDescription, let randomId, let untilDate, let currency, let amount):
if boxed {
@ -818,8 +819,8 @@ public extension Api {
switch self {
case .inputStorePaymentGiftPremium(let userId, let currency, let amount):
return ("inputStorePaymentGiftPremium", [("userId", userId as Any), ("currency", currency as Any), ("amount", amount as Any)])
case .inputStorePaymentPremiumGiftCode(let flags, let users, let boostPeer, let currency, let amount):
return ("inputStorePaymentPremiumGiftCode", [("flags", flags as Any), ("users", users as Any), ("boostPeer", boostPeer as Any), ("currency", currency as Any), ("amount", amount as Any)])
case .inputStorePaymentPremiumGiftCode(let flags, let users, let boostPeer, let currency, let amount, let message):
return ("inputStorePaymentPremiumGiftCode", [("flags", flags as Any), ("users", users as Any), ("boostPeer", boostPeer as Any), ("currency", currency as Any), ("amount", amount as Any), ("message", message as Any)])
case .inputStorePaymentPremiumGiveaway(let flags, let boostPeer, let additionalPeers, let countriesIso2, let prizeDescription, let randomId, let untilDate, let currency, let amount):
return ("inputStorePaymentPremiumGiveaway", [("flags", flags as Any), ("boostPeer", boostPeer as Any), ("additionalPeers", additionalPeers as Any), ("countriesIso2", countriesIso2 as Any), ("prizeDescription", prizeDescription as Any), ("randomId", randomId as Any), ("untilDate", untilDate as Any), ("currency", currency as Any), ("amount", amount as Any)])
case .inputStorePaymentPremiumSubscription(let flags):
@ -867,13 +868,18 @@ public extension Api {
_4 = parseString(reader)
var _5: Int64?
_5 = reader.readInt64()
var _6: Api.TextWithEntities?
if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() {
_6 = Api.parse(reader, signature: signature) as? Api.TextWithEntities
} }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil
let _c4 = _4 != nil
let _c5 = _5 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 {
return Api.InputStorePaymentPurpose.inputStorePaymentPremiumGiftCode(flags: _1!, users: _2!, boostPeer: _3, currency: _4!, amount: _5!)
let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 {
return Api.InputStorePaymentPurpose.inputStorePaymentPremiumGiftCode(flags: _1!, users: _2!, boostPeer: _3, currency: _4!, amount: _5!, message: _6)
}
else {
return nil

View File

@ -986,8 +986,8 @@ public extension Api {
case messageActionEmpty
case messageActionGameScore(gameId: Int64, score: Int32)
case messageActionGeoProximityReached(fromId: Api.Peer, toId: Api.Peer, distance: Int32)
case messageActionGiftCode(flags: Int32, boostPeer: Api.Peer?, months: Int32, slug: String, currency: String?, amount: Int64?, cryptoCurrency: String?, cryptoAmount: Int64?)
case messageActionGiftPremium(flags: Int32, currency: String, amount: Int64, months: Int32, cryptoCurrency: String?, cryptoAmount: Int64?)
case messageActionGiftCode(flags: Int32, boostPeer: Api.Peer?, months: Int32, slug: String, currency: String?, amount: Int64?, cryptoCurrency: String?, cryptoAmount: Int64?, message: Api.TextWithEntities?)
case messageActionGiftPremium(flags: Int32, currency: String, amount: Int64, months: Int32, cryptoCurrency: String?, cryptoAmount: Int64?, message: Api.TextWithEntities?)
case messageActionGiftStars(flags: Int32, currency: String, amount: Int64, stars: Int64, cryptoCurrency: String?, cryptoAmount: Int64?, transactionId: String?)
case messageActionGiveawayLaunch(flags: Int32, stars: Int64?)
case messageActionGiveawayResults(flags: Int32, winnersCount: Int32, unclaimedCount: Int32)
@ -1141,9 +1141,9 @@ public extension Api {
toId.serialize(buffer, true)
serializeInt32(distance, buffer: buffer, boxed: false)
break
case .messageActionGiftCode(let flags, let boostPeer, let months, let slug, let currency, let amount, let cryptoCurrency, let cryptoAmount):
case .messageActionGiftCode(let flags, let boostPeer, let months, let slug, let currency, let amount, let cryptoCurrency, let cryptoAmount, let message):
if boxed {
buffer.appendInt32(1737240073)
buffer.appendInt32(1456486804)
}
serializeInt32(flags, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 1) != 0 {boostPeer!.serialize(buffer, true)}
@ -1153,10 +1153,11 @@ public extension Api {
if Int(flags) & Int(1 << 2) != 0 {serializeInt64(amount!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 3) != 0 {serializeString(cryptoCurrency!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 3) != 0 {serializeInt64(cryptoAmount!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 4) != 0 {message!.serialize(buffer, true)}
break
case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount):
case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount, let message):
if boxed {
buffer.appendInt32(-935499028)
buffer.appendInt32(1818391802)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeString(currency, buffer: buffer, boxed: false)
@ -1164,6 +1165,7 @@ public extension Api {
serializeInt32(months, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {serializeString(cryptoCurrency!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 0) != 0 {serializeInt64(cryptoAmount!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 1) != 0 {message!.serialize(buffer, true)}
break
case .messageActionGiftStars(let flags, let currency, let amount, let stars, let cryptoCurrency, let cryptoAmount, let transactionId):
if boxed {
@ -1439,10 +1441,10 @@ public extension Api {
return ("messageActionGameScore", [("gameId", gameId as Any), ("score", score as Any)])
case .messageActionGeoProximityReached(let fromId, let toId, let distance):
return ("messageActionGeoProximityReached", [("fromId", fromId as Any), ("toId", toId as Any), ("distance", distance as Any)])
case .messageActionGiftCode(let flags, let boostPeer, let months, let slug, let currency, let amount, let cryptoCurrency, let cryptoAmount):
return ("messageActionGiftCode", [("flags", flags as Any), ("boostPeer", boostPeer as Any), ("months", months as Any), ("slug", slug as Any), ("currency", currency as Any), ("amount", amount as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any)])
case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount):
return ("messageActionGiftPremium", [("flags", flags as Any), ("currency", currency as Any), ("amount", amount as Any), ("months", months as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any)])
case .messageActionGiftCode(let flags, let boostPeer, let months, let slug, let currency, let amount, let cryptoCurrency, let cryptoAmount, let message):
return ("messageActionGiftCode", [("flags", flags as Any), ("boostPeer", boostPeer as Any), ("months", months as Any), ("slug", slug as Any), ("currency", currency as Any), ("amount", amount as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any), ("message", message as Any)])
case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount, let message):
return ("messageActionGiftPremium", [("flags", flags as Any), ("currency", currency as Any), ("amount", amount as Any), ("months", months as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any), ("message", message as Any)])
case .messageActionGiftStars(let flags, let currency, let amount, let stars, let cryptoCurrency, let cryptoAmount, let transactionId):
return ("messageActionGiftStars", [("flags", flags as Any), ("currency", currency as Any), ("amount", amount as Any), ("stars", stars as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any), ("transactionId", transactionId as Any)])
case .messageActionGiveawayLaunch(let flags, let stars):
@ -1718,6 +1720,10 @@ public extension Api {
if Int(_1!) & Int(1 << 3) != 0 {_7 = parseString(reader) }
var _8: Int64?
if Int(_1!) & Int(1 << 3) != 0 {_8 = reader.readInt64() }
var _9: Api.TextWithEntities?
if Int(_1!) & Int(1 << 4) != 0 {if let signature = reader.readInt32() {
_9 = Api.parse(reader, signature: signature) as? Api.TextWithEntities
} }
let _c1 = _1 != nil
let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil
let _c3 = _3 != nil
@ -1726,8 +1732,9 @@ public extension Api {
let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil
let _c7 = (Int(_1!) & Int(1 << 3) == 0) || _7 != nil
let _c8 = (Int(_1!) & Int(1 << 3) == 0) || _8 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 {
return Api.MessageAction.messageActionGiftCode(flags: _1!, boostPeer: _2, months: _3!, slug: _4!, currency: _5, amount: _6, cryptoCurrency: _7, cryptoAmount: _8)
let _c9 = (Int(_1!) & Int(1 << 4) == 0) || _9 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 {
return Api.MessageAction.messageActionGiftCode(flags: _1!, boostPeer: _2, months: _3!, slug: _4!, currency: _5, amount: _6, cryptoCurrency: _7, cryptoAmount: _8, message: _9)
}
else {
return nil
@ -1746,14 +1753,19 @@ public extension Api {
if Int(_1!) & Int(1 << 0) != 0 {_5 = parseString(reader) }
var _6: Int64?
if Int(_1!) & Int(1 << 0) != 0 {_6 = reader.readInt64() }
var _7: Api.TextWithEntities?
if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() {
_7 = Api.parse(reader, signature: signature) as? Api.TextWithEntities
} }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = _4 != nil
let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil
let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 {
return Api.MessageAction.messageActionGiftPremium(flags: _1!, currency: _2!, amount: _3!, months: _4!, cryptoCurrency: _5, cryptoAmount: _6)
let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 {
return Api.MessageAction.messageActionGiftPremium(flags: _1!, currency: _2!, amount: _3!, months: _4!, cryptoCurrency: _5, cryptoAmount: _6, message: _7)
}
else {
return nil

View File

@ -7098,7 +7098,10 @@ final class VoiceChatContextReferenceContentSource: ContextReferenceContentSourc
}
public func shouldUseV2VideoChatImpl(context: AccountContext) -> Bool {
var useV2 = true
var useV2 = false
if let data = context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_enable_videochatui_v2"] {
useV2 = true
}
if context.sharedContext.immediateExperimentalUISettings.disableCallV2 {
useV2 = false
}

View File

@ -254,7 +254,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] {
}
case let .messageActionRequestedPeer(_, peers):
result.append(contentsOf: peers.map(\.peerId))
case let .messageActionGiftCode(_, boostPeer, _, _, _, _, _, _):
case let .messageActionGiftCode(_, boostPeer, _, _, _, _, _, _, _):
if let boostPeer = boostPeer {
result.append(boostPeer.peerId)
}

View File

@ -100,8 +100,18 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
return TelegramMediaAction(action: .joinedByRequest)
case let .messageActionWebViewDataSentMe(text, _), let .messageActionWebViewDataSent(text):
return TelegramMediaAction(action: .webViewData(text))
case let .messageActionGiftPremium(_, currency, amount, months, cryptoCurrency, cryptoAmount):
return TelegramMediaAction(action: .giftPremium(currency: currency, amount: amount, months: months, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount))
case let .messageActionGiftPremium(_, currency, amount, months, cryptoCurrency, cryptoAmount, message):
let text: String?
let entities: [MessageTextEntity]?
switch message {
case let .textWithEntities(textValue, entitiesValue):
text = textValue
entities = messageTextEntitiesFromApiEntities(entitiesValue)
default:
text = nil
entities = nil
}
return TelegramMediaAction(action: .giftPremium(currency: currency, amount: amount, months: months, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount, text: text, entities: entities))
case let .messageActionGiftStars(_, currency, amount, stars, cryptoCurrency, cryptoAmount, transactionId):
return TelegramMediaAction(action: .giftStars(currency: currency, amount: amount, count: stars, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount, transactionId: transactionId))
case let .messageActionTopicCreate(_, title, iconColor, iconEmojiId):
@ -133,8 +143,18 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
} else {
return TelegramMediaAction(action: .setChatWallpaper(wallpaper: TelegramWallpaper(apiWallpaper: wallpaper), forBoth: (flags & (1 << 1)) != 0))
}
case let .messageActionGiftCode(flags, boostPeer, months, slug, currency, amount, cryptoCurrency, cryptoAmount):
return TelegramMediaAction(action: .giftCode(slug: slug, fromGiveaway: (flags & (1 << 0)) != 0, isUnclaimed: (flags & (1 << 2)) != 0, boostPeerId: boostPeer?.peerId, months: months, currency: currency, amount: amount, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount))
case let .messageActionGiftCode(flags, boostPeer, months, slug, currency, amount, cryptoCurrency, cryptoAmount, message):
let text: String?
let entities: [MessageTextEntity]?
switch message {
case let .textWithEntities(textValue, entitiesValue):
text = textValue
entities = messageTextEntitiesFromApiEntities(entitiesValue)
default:
text = nil
entities = nil
}
return TelegramMediaAction(action: .giftCode(slug: slug, fromGiveaway: (flags & (1 << 0)) != 0, isUnclaimed: (flags & (1 << 2)) != 0, boostPeerId: boostPeer?.peerId, months: months, currency: currency, amount: amount, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount, text: text, entities: entities))
case let .messageActionGiveawayLaunch(_, stars):
return TelegramMediaAction(action: .giveawayLaunched(stars: stars))
case let .messageActionGiveawayResults(flags, winners, unclaimed):

View File

@ -210,7 +210,7 @@ public class BoxedMessage: NSObject {
public class Serialization: NSObject, MTSerialization {
public func currentLayer() -> UInt {
return 189
return 190
}
public func parseMessage(_ data: Data!) -> Any! {

View File

@ -114,7 +114,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case setChatTheme(emoji: String)
case joinedByRequest
case webViewData(String)
case giftPremium(currency: String, amount: Int64, months: Int32, cryptoCurrency: String?, cryptoAmount: Int64?)
case giftPremium(currency: String, amount: Int64, months: Int32, cryptoCurrency: String?, cryptoAmount: Int64?, text: String?, entities: [MessageTextEntity]?)
case topicCreated(title: String, iconColor: Int32, iconFileId: Int64?)
case topicEdited(components: [ForumTopicEditComponent])
case suggestedProfilePhoto(image: TelegramMediaImage?)
@ -122,7 +122,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case requestedPeer(buttonId: Int32, peerIds: [PeerId])
case setChatWallpaper(wallpaper: TelegramWallpaper, forBoth: Bool)
case setSameChatWallpaper(wallpaper: TelegramWallpaper)
case giftCode(slug: String, fromGiveaway: Bool, isUnclaimed: Bool, boostPeerId: PeerId?, months: Int32, currency: String?, amount: Int64?, cryptoCurrency: String?, cryptoAmount: Int64?)
case giftCode(slug: String, fromGiveaway: Bool, isUnclaimed: Bool, boostPeerId: PeerId?, months: Int32, currency: String?, amount: Int64?, cryptoCurrency: String?, cryptoAmount: Int64?, text: String?, entities: [MessageTextEntity]?)
case giveawayLaunched(stars: Int64?)
case joinedChannel
case giveawayResults(winners: Int32, unclaimed: Int32, stars: Bool)
@ -200,7 +200,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case 26:
self = .webViewData(decoder.decodeStringForKey("t", orElse: ""))
case 27:
self = .giftPremium(currency: decoder.decodeStringForKey("currency", orElse: ""), amount: decoder.decodeInt64ForKey("amount", orElse: 0), months: decoder.decodeInt32ForKey("months", orElse: 0), cryptoCurrency: decoder.decodeOptionalStringForKey("cryptoCurrency"), cryptoAmount: decoder.decodeOptionalInt64ForKey("cryptoAmount"))
self = .giftPremium(currency: decoder.decodeStringForKey("currency", orElse: ""), amount: decoder.decodeInt64ForKey("amount", orElse: 0), months: decoder.decodeInt32ForKey("months", orElse: 0), cryptoCurrency: decoder.decodeOptionalStringForKey("cryptoCurrency"), cryptoAmount: decoder.decodeOptionalInt64ForKey("cryptoAmount"), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities"))
case 28:
self = .topicCreated(title: decoder.decodeStringForKey("title", orElse: ""), iconColor: decoder.decodeInt32ForKey("iconColor", orElse: 0), iconFileId: decoder.decodeOptionalInt64ForKey("iconFileId"))
case 29:
@ -230,7 +230,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case 35:
self = .botAppAccessGranted(appName: decoder.decodeOptionalStringForKey("app"), type: decoder.decodeOptionalInt32ForKey("atp").flatMap { BotSendMessageAccessGrantedType(rawValue: $0) })
case 36:
self = .giftCode(slug: decoder.decodeStringForKey("slug", orElse: ""), fromGiveaway: decoder.decodeBoolForKey("give", orElse: false), isUnclaimed: decoder.decodeBoolForKey("unclaimed", orElse: false), boostPeerId: decoder.decodeOptionalInt64ForKey("pi").flatMap { PeerId($0) }, months: decoder.decodeInt32ForKey("months", orElse: 0), currency: decoder.decodeOptionalStringForKey("currency"), amount: decoder.decodeOptionalInt64ForKey("amount"), cryptoCurrency: decoder.decodeOptionalStringForKey("cryptoCurrency"), cryptoAmount: decoder.decodeOptionalInt64ForKey("cryptoAmount"))
self = .giftCode(slug: decoder.decodeStringForKey("slug", orElse: ""), fromGiveaway: decoder.decodeBoolForKey("give", orElse: false), isUnclaimed: decoder.decodeBoolForKey("unclaimed", orElse: false), boostPeerId: decoder.decodeOptionalInt64ForKey("pi").flatMap { PeerId($0) }, months: decoder.decodeInt32ForKey("months", orElse: 0), currency: decoder.decodeOptionalStringForKey("currency"), amount: decoder.decodeOptionalInt64ForKey("amount"), cryptoCurrency: decoder.decodeOptionalStringForKey("cryptoCurrency"), cryptoAmount: decoder.decodeOptionalInt64ForKey("cryptoAmount"), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities"))
case 37:
self = .giveawayLaunched(stars: decoder.decodeOptionalInt64ForKey("stars"))
case 38:
@ -382,7 +382,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case let .webViewData(text):
encoder.encodeInt32(26, forKey: "_rawValue")
encoder.encodeString(text, forKey: "t")
case let .giftPremium(currency, amount, months, cryptoCurrency, cryptoAmount):
case let .giftPremium(currency, amount, months, cryptoCurrency, cryptoAmount, text, entities):
encoder.encodeInt32(27, forKey: "_rawValue")
encoder.encodeString(currency, forKey: "currency")
encoder.encodeInt64(amount, forKey: "amount")
@ -390,6 +390,16 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
if let cryptoCurrency = cryptoCurrency, let cryptoAmount = cryptoAmount {
encoder.encodeString(cryptoCurrency, forKey: "cryptoCurrency")
encoder.encodeInt64(cryptoAmount, forKey: "cryptoAmount")
} else {
encoder.encodeNil(forKey: "cryptoCurrency")
encoder.encodeNil(forKey: "cryptoAmount")
}
if let text, let entities {
encoder.encodeString(text, forKey: "text")
encoder.encodeObjectArray(entities, forKey: "entities")
} else {
encoder.encodeNil(forKey: "text")
encoder.encodeNil(forKey: "entities")
}
case let .topicCreated(title, iconColor, iconFileId):
encoder.encodeInt32(28, forKey: "_rawValue")
@ -433,7 +443,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
} else {
encoder.encodeNil(forKey: "atp")
}
case let .giftCode(slug, fromGiveaway, unclaimed, boostPeerId, months, currency, amount, cryptoCurrency, cryptoAmount):
case let .giftCode(slug, fromGiveaway, unclaimed, boostPeerId, months, currency, amount, cryptoCurrency, cryptoAmount, text, entities):
encoder.encodeInt32(36, forKey: "_rawValue")
encoder.encodeString(slug, forKey: "slug")
encoder.encodeBool(fromGiveaway, forKey: "give")
@ -464,6 +474,13 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
} else {
encoder.encodeNil(forKey: "cryptoAmount")
}
if let text, let entities {
encoder.encodeString(text, forKey: "text")
encoder.encodeObjectArray(entities, forKey: "entities")
} else {
encoder.encodeNil(forKey: "text")
encoder.encodeNil(forKey: "entities")
}
case let .giveawayLaunched(stars):
encoder.encodeInt32(37, forKey: "_rawValue")
if let stars = stars {
@ -563,7 +580,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
return peerIds
case let .requestedPeer(_, peerIds):
return peerIds
case let .giftCode(_, _, _, boostPeerId, _, _, _, _, _):
case let .giftCode(_, _, _, boostPeerId, _, _, _, _, _, _, _):
return boostPeerId.flatMap { [$0] } ?? []
case let .paymentRefunded(peerId, _, _, _, _):
return [peerId]

View File

@ -15,7 +15,7 @@ public enum AppStoreTransactionPurpose {
case upgrade
case restore
case gift(peerId: EnginePeer.Id, currency: String, amount: Int64)
case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?, currency: String, amount: Int64)
case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?, currency: String, amount: Int64, text: String?, entities: [MessageTextEntity]?)
case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64)
case stars(count: Int64, currency: String, amount: Int64)
case starsGift(peerId: EnginePeer.Id, count: Int64, currency: String, amount: Int64)
@ -43,7 +43,7 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran
}
return .single(.inputStorePaymentGiftPremium(userId: inputUser, currency: currency, amount: amount))
}
case let .giftCode(peerIds, boostPeerId, currency, amount):
case let .giftCode(peerIds, boostPeerId, currency, amount, text, entities):
return account.postbox.transaction { transaction -> Api.InputStorePaymentPurpose in
var flags: Int32 = 0
var apiBoostPeer: Api.InputPeer?
@ -59,8 +59,13 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran
apiBoostPeer = apiPeer
flags |= (1 << 0)
}
return .inputStorePaymentPremiumGiftCode(flags: flags, users: apiInputUsers, boostPeer: apiBoostPeer, currency: currency, amount: amount)
var message: Api.TextWithEntities?
if let text {
flags |= (1 << 1)
message = .textWithEntities(text: text, entities: apiEntitiesFromMessageTextEntities(entities ?? [], associatedPeers: SimpleDictionary()))
}
return .inputStorePaymentPremiumGiftCode(flags: flags, users: apiInputUsers, boostPeer: apiBoostPeer, currency: currency, amount: amount, message: message)
}
case let .giveaway(boostPeerId, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, currency, amount):
return account.postbox.transaction { transaction -> Signal<Api.InputStorePaymentPurpose, NoError> in

View File

@ -293,7 +293,7 @@ func _internal_parseInputInvoice(transaction: Transaction, source: BotPaymentInv
}
}
let inputPurpose: Api.InputStorePaymentPurpose = .inputStorePaymentPremiumGiftCode(flags: 0, users: inputUsers, boostPeer: nil, currency: currency, amount: amount)
let inputPurpose: Api.InputStorePaymentPurpose = .inputStorePaymentPremiumGiftCode(flags: 0, users: inputUsers, boostPeer: nil, currency: currency, amount: amount, message: nil)
var flags: Int32 = 0
if let _ = option.storeProductId {

View File

@ -462,6 +462,10 @@ public extension EnginePeer {
var addressName: String? {
return self._asPeer().addressName
}
var usernames: [TelegramPeerUsername] {
return self._asPeer().usernames
}
var indexName: EnginePeer.IndexName {
return EnginePeer.IndexName(self._asPeer().indexName)

View File

@ -51,7 +51,7 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo
}
}
}
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
var renderedMyPeers: [FoundPeer] = []
@ -61,7 +61,11 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo
if let group = peer as? TelegramGroup, group.migrationReference != nil {
continue
}
renderedMyPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId]))
if let user = peer as? TelegramUser {
renderedMyPeers.append(FoundPeer(peer: peer, subscribers: user.subscriberCount))
} else {
renderedMyPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId]))
}
}
}
@ -72,7 +76,11 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo
if let group = peer as? TelegramGroup, group.migrationReference != nil {
continue
}
renderedPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId]))
if let user = peer as? TelegramUser {
renderedPeers.append(FoundPeer(peer: peer, subscribers: user.subscriberCount))
} else {
renderedPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId]))
}
}
}

View File

@ -735,7 +735,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
}
case let .webViewData(text):
attributedString = NSAttributedString(string: strings.Notification_WebAppSentData(text).string, font: titleFont, textColor: primaryTextColor)
case let .giftPremium(currency, amount, _, _, _):
case let .giftPremium(currency, amount, _, _, _, _, _):
let price = formatCurrencyAmount(amount, currency: currency)
if message.author?.id == accountPeerId {
attributedString = addAttributesToStringWithRanges(strings.Notification_PremiumGift_SentYou(price)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
@ -952,7 +952,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
let resultTitleString = strings.Notification_ChangedToSameWallpaper(compactAuthorName)
attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
}
case let .giftCode(_, _, _, boostPeerId, _, currency, amount, _, _):
case let .giftCode(_, _, _, boostPeerId, _, currency, amount, _, _, _, _):
if boostPeerId == nil, let currency, let amount {
let price = formatCurrencyAmount(amount, currency: currency)
if message.author?.id == accountPeerId {
@ -1058,7 +1058,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
let _ = text
let _ = entities
let starsPrice = "\(gift.price) Stars"
let starsPrice = strings.Notification_StarsGift_Stars(Int32(gift.price))
var authorName = compactAuthorName
var peerIds: [(Int, EnginePeer.Id?)] = [(0, message.author?.id)]
if message.id.peerId.namespace == Namespaces.Peer.CloudUser && message.id.peerId.id._internalGetInt64Value() == 777000 {

View File

@ -37,6 +37,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
private var mediaBackgroundContent: WallpaperBubbleBackgroundNode?
private let titleNode: TextNode
private let subtitleNode: TextNodeWithEntities
private let textClippingNode: ASDisplayNode
private var dustNode: InvisibleInkDustNode?
private let placeholderNode: StickerShimmerEffectNode
private let animationNode: AnimatedStickerNode
@ -49,11 +50,17 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
private let buttonStarsNode: PremiumStarsNode
private let buttonTitleNode: TextNode
private var maskView: UIImageView?
private var maskOverlayView: UIView?
private var cachedMaskBackgroundImage: (CGPoint, UIImage, [CGRect])?
private var absoluteRect: (CGRect, CGSize)?
private var isPlaying: Bool = false
private var isExpanded: Bool = false
private var appliedIsExpanded: Bool = false
private var currentProgressDisposable: Disposable?
override public var visibility: ListViewItemNodeVisibility {
@ -105,6 +112,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
self.subtitleNode.textNode.isUserInteractionEnabled = false
self.subtitleNode.textNode.displaysAsynchronously = false
self.textClippingNode = ASDisplayNode()
self.textClippingNode.clipsToBounds = true
self.buttonNode = HighlightTrackingButtonNode()
self.buttonNode.clipsToBounds = true
self.buttonNode.cornerRadius = 17.0
@ -133,7 +143,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
self.addSubnode(self.labelNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.subtitleNode.textNode)
self.addSubnode(self.textClippingNode)
self.textClippingNode.addSubnode(self.subtitleNode.textNode)
self.addSubnode(self.placeholderNode)
self.addSubnode(self.animationNode)
@ -251,9 +262,12 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
let makeSubtitleLayout = TextNodeWithEntities.asyncLayout(self.subtitleNode)
let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode)
let makeRibbonTextLayout = TextNode.asyncLayout(self.ribbonTextNode)
let makeMeasureTextLayout = TextNode.asyncLayout(nil)
let cachedMaskBackgroundImage = self.cachedMaskBackgroundImage
let currentIsExpanded = self.isExpanded
return { item, layoutConstants, _, _, _, _ in
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center)
@ -279,9 +293,19 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
for media in item.message.media {
if let action = media as? TelegramMediaAction {
switch action.action {
case let .giftPremium(_, _, monthsValue, _, _):
case let .giftPremium(_, _, monthsValue, _, _, giftText, giftEntities):
months = monthsValue
text = item.presentationData.strings.Notification_PremiumGift_Subtitle(item.presentationData.strings.Notification_PremiumGift_Months(months)).string
if months == 12 {
title = item.presentationData.strings.Notification_PremiumGift_YearsTitle(1)
} else {
title = item.presentationData.strings.Notification_PremiumGift_MonthsTitle(months)
}
if let giftText, !giftText.isEmpty {
text = giftText
entities = giftEntities ?? []
} else {
text = item.presentationData.strings.Notification_PremiumGift_SubscriptionDescription
}
case let .giftStars(_, _, count, _, _, _):
if count <= 1000 {
months = 3
@ -310,10 +334,20 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
}
title = item.presentationData.strings.Notification_StarsGiveaway_Title
text = item.presentationData.strings.Notification_StarsGiveaway_Subtitle(peerName, item.presentationData.strings.Notification_StarsGiveaway_Subtitle_Stars(Int32(count))).string
case let .giftCode(_, fromGiveaway, unclaimed, channelId, monthsValue, _, _, _, _):
case let .giftCode(_, fromGiveaway, unclaimed, channelId, monthsValue, _, _, _, _, giftText, giftEntities):
if channelId == nil {
months = monthsValue
text = item.presentationData.strings.Notification_PremiumGift_Subtitle(item.presentationData.strings.Notification_PremiumGift_Months(months)).string
if months == 12 {
title = item.presentationData.strings.Notification_PremiumGift_YearsTitle(1)
} else {
title = item.presentationData.strings.Notification_PremiumGift_MonthsTitle(months)
}
if let giftText, !giftText.isEmpty {
text = giftText
entities = giftEntities ?? []
} else {
text = item.presentationData.strings.Notification_PremiumGift_SubscriptionDescription
}
if item.message.author?.id != item.context.account.peerId {
buttonTitle = item.presentationData.strings.Notification_PremiumGift_UseGift
}
@ -419,13 +453,24 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
), textAlignment: .center)
}
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let textConstrainedSize = CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude)
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
var canExpand = false
var clippedTextHeight: CGFloat = subtitleLayout.size.height
if subtitleLayout.numberOfLines > 4 {
let (measuredTextLayout, _) = makeMeasureTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 4, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
canExpand = true
if !currentIsExpanded {
clippedTextHeight = measuredTextLayout.size.height
}
}
let (buttonTitleLayout, buttonTitleApply) = makeButtonTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: buttonTitle, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (ribbonTextLayout, ribbonTextApply) = makeRibbonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: ribbonTitle, font: Font.semibold(11.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
giftSize.height = titleLayout.size.height + textSpacing + subtitleLayout.size.height + 164.0
giftSize.height = titleLayout.size.height + textSpacing + clippedTextHeight + 164.0
if !buttonTitle.isEmpty {
giftSize.height += 48.0
}
@ -472,8 +517,18 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
}
return (backgroundSize.width, { boundingWidth in
return (backgroundSize, { [weak self] animation, synchronousLoads, _ in
return (backgroundSize, { [weak self] animation, synchronousLoads, info in
if let strongSelf = self {
let isFirstTime = strongSelf.item == nil
var isExpandedUpdated = false
if strongSelf.appliedIsExpanded != currentIsExpanded {
strongSelf.appliedIsExpanded = currentIsExpanded
info?.setInvertOffsetDirection()
isExpandedUpdated = true
}
let _ = isExpandedUpdated
let overlayColor = item.presentationData.theme.theme.overallDarkAppearance ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12)
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - giftSize.width) / 2.0), y: hasServiceMessage ? labelLayout.size.height + 16.0 : 0.0), size: giftSize)
@ -551,8 +606,21 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
let titleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - titleLayout.size.width) / 2.0) , y: mediaBackgroundFrame.minY + 151.0), size: titleLayout.size)
strongSelf.titleNode.frame = titleFrame
let clippingTextFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY + textSpacing), size: CGSize(width: boundingWidth, height: clippedTextHeight))
let subtitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY + textSpacing), size: subtitleLayout.size)
strongSelf.subtitleNode.textNode.frame = subtitleFrame
strongSelf.subtitleNode.textNode.frame = CGRect(origin: .zero, size: subtitleLayout.size)
if isFirstTime {
strongSelf.textClippingNode.frame = clippingTextFrame
} else {
animation.animator.updateFrame(layer: strongSelf.textClippingNode.layer, frame: clippingTextFrame, completion: nil)
}
if let maskView = strongSelf.maskView, let maskOverlayView = strongSelf.maskOverlayView {
animation.animator.updateFrame(layer: maskView.layer, frame: CGRect(origin: .zero, size: CGSize(width: boundingWidth, height: clippingTextFrame.size.height)), completion: nil)
animation.animator.updateFrame(layer: maskOverlayView.layer, frame: CGRect(origin: .zero, size: CGSize(width: boundingWidth, height: clippingTextFrame.size.height)), completion: nil)
}
if !subtitleLayout.spoilers.isEmpty {
let dustColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText
@ -573,11 +641,11 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.dustNode = nil
}
let buttonTitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonTitleLayout.size.width) / 2.0), y: subtitleFrame.maxY + 18.0), size: buttonTitleLayout.size)
let buttonTitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonTitleLayout.size.width) / 2.0), y: clippingTextFrame.maxY + 18.0), size: buttonTitleLayout.size)
strongSelf.buttonTitleNode.frame = buttonTitleFrame
let buttonSize = CGSize(width: buttonTitleLayout.size.width + 38.0, height: 34.0)
strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonSize.width) / 2.0), y: subtitleFrame.maxY + 10.0), size: buttonSize)
strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonSize.width) / 2.0), y: clippingTextFrame.maxY + 10.0), size: buttonSize)
strongSelf.buttonStarsNode.frame = CGRect(origin: .zero, size: buttonSize)
if ribbonTextLayout.size.width > 0.0 {
@ -666,6 +734,26 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.updateAbsoluteRect(rect, within: size)
}
if canExpand {
if strongSelf.maskView?.image == nil {
strongSelf.maskView?.image = generateMaskImage()
}
strongSelf.textClippingNode.view.mask = strongSelf.maskView
// var expandIconFrame: CGRect = .zero
// if let icon = strongSelf.expandIcon.image {
// expandIconFrame = CGRect(origin: CGPoint(x: boundingWidth - icon.size.width - 19.0, y: backgroundFrame.maxY - icon.size.height - 6.0), size: icon.size)
// if wasHidden || isFirstTime {
// strongSelf.expandIcon.position = expandIconFrame.center
// } else {
// animation.animator.updatePosition(layer: strongSelf.expandIcon.layer, position: expandIconFrame.center, completion: nil)
// }
// strongSelf.expandIcon.bounds = CGRect(origin: .zero, size: expandIconFrame.size)
// }
} else {
strongSelf.textClippingNode.view.mask = nil
}
switch strongSelf.visibility {
case .none:
strongSelf.subtitleNode.visibilityRect = nil
@ -853,3 +941,22 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
}
}
}
private func generateMaskImage() -> UIImage? {
return generateImage(CGSize(width: 140, height: 30), rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setFillColor(UIColor.white.cgColor)
context.fill(CGRect(origin: .zero, size: size))
var locations: [CGFloat] = [0.0, 0.5, 1.0]
let colors: [CGColor] = [UIColor.white.cgColor, UIColor.white.withAlphaComponent(0.0).cgColor, UIColor.white.withAlphaComponent(0.0).cgColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.setBlendMode(.copy)
context.clip(to: CGRect(origin: CGPoint(x: 10.0, y: 8.0), size: CGSize(width: 130.0, height: 22.0)))
context.drawLinearGradient(gradient, start: CGPoint(x: 10.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions())
})?.resizableImage(withCapInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 22.0, right: 130.0))
}

View File

@ -468,8 +468,12 @@ private final class SheetContent: CombinedComponent {
switch component.subject {
case .peer:
if let peer = state.peer {
if case .user = peer {
mainTitle = environment.strings.Report_Title_User
if case let .user(user) = peer {
if let _ = user.botInfo {
mainTitle = environment.strings.Report_Title_Bot
} else {
mainTitle = environment.strings.Report_Title_User
}
} else if case let .channel(channel) = peer, case .broadcast = channel.info {
mainTitle = environment.strings.Report_Title_Channel
} else {

View File

@ -306,7 +306,7 @@ final class GiftOptionsScreenComponent: Component {
let giftController = GiftSetupScreen(
context: component.context,
peerId: component.peerId,
gift: gift,
subject: .starGift(gift),
completion: component.completion
)
mainController.push(giftController)
@ -359,94 +359,6 @@ final class GiftOptionsScreenComponent: Component {
}
}
private func buyPremium(_ product: PremiumGiftProduct) {
guard let component = self.component, let inAppPurchaseManager = self.component?.context.inAppPurchaseManager, self.inProgressPremiumGift == nil else {
return
}
self.inProgressPremiumGift = product.id
self.state?.updated()
let (currency, amount) = product.storeProduct.priceCurrencyAndAmount
addAppLogEvent(postbox: component.context.account.postbox, type: "premium_gift.promo_screen_accept")
let purpose: AppStoreTransactionPurpose = .giftCode(peerIds: [component.peerId], boostPeer: nil, currency: currency, amount: amount)
let quantity: Int32 = 1
let completion = component.completion
let _ = (component.context.engine.payments.canPurchasePremium(purpose: purpose)
|> deliverOnMainQueue).start(next: { [weak self] available in
if let strongSelf = self {
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
if available {
strongSelf.purchaseDisposable.set((inAppPurchaseManager.buyProduct(product.storeProduct, quantity: quantity, purpose: purpose)
|> deliverOnMainQueue).start(next: { [weak self] status in
if let completion {
completion()
} else {
guard let self, case .purchased = status, let controller = self.environment?.controller(), let navigationController = controller.navigationController as? NavigationController else {
return
}
var controllers = navigationController.viewControllers
controllers = controllers.filter { !($0 is GiftOptionsScreen) && !($0 is PeerInfoScreen) && !($0 is ContactSelectionController) }
var foundController = false
for controller in controllers.reversed() {
if let chatController = controller as? ChatController, case .peer(id: component.peerId) = chatController.chatLocation {
chatController.hintPlayNextOutgoingGift()
foundController = true
break
}
}
if !foundController {
let chatController = component.context.sharedContext.makeChatController(context: component.context, chatLocation: .peer(id: component.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil)
chatController.hintPlayNextOutgoingGift()
controllers.append(chatController)
}
navigationController.setViewControllers(controllers, animated: true)
}
}, error: { [weak self] error in
guard let self, let controller = self.environment?.controller() else {
return
}
self.inProgressPremiumGift = nil
self.state?.updated(transition: .immediate)
var errorText: String?
switch error {
case .generic:
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
case .network:
errorText = presentationData.strings.Premium_Purchase_ErrorNetwork
case .notAllowed:
errorText = presentationData.strings.Premium_Purchase_ErrorNotAllowed
case .cantMakePayments:
errorText = presentationData.strings.Premium_Purchase_ErrorCantMakePayments
case .assignFailed:
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
case .tryLater:
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
case .cancelled:
break
}
if let errorText {
addAppLogEvent(postbox: component.context.account.postbox, type: "premium_gift.promo_screen_fail")
let alertController = textAlertController(context: component.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
controller.present(alertController, in: .window(.root))
}
}))
} else {
self?.inProgressPremiumGift = nil
self?.state?.updated(transition: .immediate)
}
}
})
}
func update(component: GiftOptionsScreenComponent, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
@ -707,7 +619,7 @@ final class GiftOptionsScreenComponent: Component {
var validIds: [AnyHashable] = []
var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: premiumOptionSize)
for product in premiumProducts {
let itemId = AnyHashable(product.storeProduct.id)
let itemId = AnyHashable(product.id)
validIds.append(itemId)
var itemTransition = transition
@ -756,7 +668,23 @@ final class GiftOptionsScreenComponent: Component {
),
effectAlignment: .center,
action: { [weak self] in
self?.buyPremium(product)
if let self, let component = self.component {
if let controller = controller() as? GiftOptionsScreen {
let mainController: ViewController
if let parentController = controller.parentController() {
mainController = parentController
} else {
mainController = controller
}
let giftController = GiftSetupScreen(
context: component.context,
peerId: component.peerId,
subject: .premium(product),
completion: component.completion
)
mainController.push(giftController)
}
}
},
animateAlpha: false
)
@ -1019,23 +947,43 @@ final class GiftOptionsScreenComponent: Component {
}
self.peer = peer
let shortestOptionPrice: (Int64, NSDecimalNumber)
if let product = availableProducts.first(where: { $0.id.hasSuffix(".monthly") }) {
shortestOptionPrice = (Int64(Float(product.priceCurrencyAndAmount.amount)), product.priceValue)
} else {
shortestOptionPrice = (1, NSDecimalNumber(decimal: 1))
}
var premiumProducts: [PremiumGiftProduct] = []
for option in premiumOptions {
if let product = availableProducts.first(where: { $0.id == option.storeProductId }), !product.isSubscription {
let fraction = Float(product.priceCurrencyAndAmount.amount) / Float(option.months) / Float(shortestOptionPrice.0)
let discountValue = Int(round((1.0 - fraction) * 20.0) * 5.0)
premiumProducts.append(PremiumGiftProduct(giftOption: option, storeProduct: product, discount: discountValue > 0 ? discountValue : nil))
if availableProducts.isEmpty {
var premiumProducts: [PremiumGiftProduct] = []
for option in premiumOptions {
premiumProducts.append(
PremiumGiftProduct(
giftOption: CachedPremiumGiftOption(
months: option.months,
currency: option.currency,
amount: option.amount,
botUrl: "",
storeProductId: option.storeProductId
),
storeProduct: nil,
discount: nil
)
)
}
self.premiumProducts = premiumProducts.sorted(by: { $0.months < $1.months })
} else {
let shortestOptionPrice: (Int64, NSDecimalNumber)
if let product = availableProducts.first(where: { $0.id.hasSuffix(".monthly") }) {
shortestOptionPrice = (Int64(Float(product.priceCurrencyAndAmount.amount)), product.priceValue)
} else {
shortestOptionPrice = (1, NSDecimalNumber(decimal: 1))
}
var premiumProducts: [PremiumGiftProduct] = []
for option in premiumOptions {
if let product = availableProducts.first(where: { $0.id == option.storeProductId }), !product.isSubscription {
let fraction = Float(product.priceCurrencyAndAmount.amount) / Float(option.months) / Float(shortestOptionPrice.0)
let discountValue = Int(round((1.0 - fraction) * 20.0) * 5.0)
premiumProducts.append(PremiumGiftProduct(giftOption: option, storeProduct: product, discount: discountValue > 0 ? discountValue : nil))
}
}
self.premiumProducts = premiumProducts.sorted(by: { $0.months < $1.months })
}
self.premiumProducts = premiumProducts.sorted(by: { $0.months < $1.months })
self.starGifts = starGifts
self.updated()
@ -1105,25 +1053,3 @@ open class GiftOptionsScreen: ViewControllerComponentContainer, GiftOptionsScree
super.containerLayoutUpdated(layout, transition: transition)
}
}
private struct PremiumGiftProduct: Equatable {
let giftOption: CachedPremiumGiftOption
let storeProduct: InAppPurchaseManager.Product
let discount: Int?
var id: String {
return self.storeProduct.id
}
var months: Int32 {
return self.giftOption.months
}
var price: String {
return self.storeProduct.price
}
var pricePerMonth: String {
return self.storeProduct.pricePerMonth(Int(self.months))
}
}

View File

@ -17,6 +17,7 @@ swift_library(
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUIPreferences",
"//submodules/TelegramStringFormatting",
"//submodules/AccountContext",
"//submodules/PresentationDataUtils",
"//submodules/Markdown",
@ -41,6 +42,7 @@ swift_library(
"//submodules/BotPaymentsUI",
"//submodules/TelegramUI/Components/EmojiSuggestionsComponent",
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
"//submodules/InAppPurchaseManager",
],
visibility = [
"//visibility:public",

View File

@ -14,6 +14,10 @@ import WallpaperBackgroundNode
import ListItemComponentAdaptor
final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator {
enum Subject: Equatable {
case premium(months: Int32, amount: Int64, currency: String)
case starGift(gift: StarGift)
}
let context: AccountContext
let theme: PresentationTheme
let componentTheme: PresentationTheme
@ -26,7 +30,7 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd
let nameDisplayOrder: PresentationPersonNameOrder
let accountPeer: EnginePeer?
let gift: StarGift
let subject: ChatGiftPreviewItem.Subject
let text: String
let entities: [MessageTextEntity]
@ -42,7 +46,7 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd
dateTimeFormat: PresentationDateTimeFormat,
nameDisplayOrder: PresentationPersonNameOrder,
accountPeer: EnginePeer?,
gift: StarGift,
subject: ChatGiftPreviewItem.Subject,
text: String,
entities: [MessageTextEntity]
) {
@ -57,7 +61,7 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd
self.dateTimeFormat = dateTimeFormat
self.nameDisplayOrder = nameDisplayOrder
self.accountPeer = accountPeer
self.gift = gift
self.subject = subject
self.text = text
self.entities = entities
}
@ -206,9 +210,22 @@ final class ChatGiftPreviewItemNode: ListViewItemNode {
peers[authorPeerId] = item.accountPeer?._asPeer()
let media: [Media] = [
TelegramMediaAction(action: .starGift(gift: item.gift, convertStars: item.gift.convertStars, text: item.text, entities: item.entities, nameHidden: false, savedToProfile: false, converted: false))
]
let media: [Media]
switch item.subject {
case let .premium(months, amount, currency):
media = [
TelegramMediaAction(
action: .giftPremium(currency: currency, amount: amount, months: months, cryptoCurrency: nil, cryptoAmount: nil, text: item.text, entities: item.entities)
)
]
case let .starGift(gift):
media = [
TelegramMediaAction(
action: .starGift(gift: gift, convertStars: gift.convertStars, text: item.text, entities: item.entities, nameHidden: false, savedToProfile: false, converted: false)
)
]
}
let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[authorPeerId], text: "", attributes: [], media: media, peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false))
}

View File

@ -7,6 +7,7 @@ import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import TelegramStringFormatting
import PresentationDataUtils
import AccountContext
import ComponentFlow
@ -27,24 +28,25 @@ import EmojiSuggestionsComponent
import ChatPresentationInterfaceState
import AudioToolbox
import TextFormat
import InAppPurchaseManager
final class GiftSetupScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let peerId: EnginePeer.Id
let gift: StarGift
let subject: GiftSetupScreen.Subject
let completion: (() -> Void)?
init(
context: AccountContext,
peerId: EnginePeer.Id,
gift: StarGift,
subject: GiftSetupScreen.Subject,
completion: (() -> Void)? = nil
) {
self.context = context
self.peerId = peerId
self.gift = gift
self.subject = subject
self.completion = completion
}
@ -55,7 +57,7 @@ final class GiftSetupScreenComponent: Component {
if lhs.peerId != rhs.peerId {
return false
}
if lhs.gift != rhs.gift {
if lhs.subject != rhs.subject {
return false
}
return true
@ -72,6 +74,7 @@ final class GiftSetupScreenComponent: Component {
private let scrollView: ScrollView
private let navigationTitle = ComponentView<Empty>()
private let remainingCount = ComponentView<Empty>()
private let introContent = ComponentView<Empty>()
private let introSection = ComponentView<Empty>()
private let hideSection = ComponentView<Empty>()
@ -103,6 +106,7 @@ final class GiftSetupScreenComponent: Component {
private var currentEmojiSuggestionView: ComponentHostView<Empty>?
private var hideName = false
private var inProgress = false
private var previousHadInputHeight: Bool = false
private var previousInputHeight: CGFloat?
@ -139,6 +143,8 @@ final class GiftSetupScreenComponent: Component {
self.addSubview(self.scrollView)
self.scrollView.layer.addSublayer(self.topOverscrollLayer)
self.disablesInteractiveKeyboardGestureRecognizer = true
}
required init?(coder: NSCoder) {
@ -189,16 +195,122 @@ final class GiftSetupScreenComponent: Component {
}
func proceed() {
guard let component = self.component, let starsContext = component.context.starsContext, let starsState = starsContext.currentState else {
guard let component = self.component else {
return
}
switch component.subject {
case .premium:
self.proceedWithPremiumGift()
case .starGift:
self.proceedWithStarGift()
}
}
func proceedWithPremiumGift() {
guard let component = self.component, case let .premium(product) = component.subject, let storeProduct = product.storeProduct, let inAppPurchaseManager = component.context.inAppPurchaseManager else {
return
}
self.inProgress = true
self.state?.updated()
let (currency, amount) = storeProduct.priceCurrencyAndAmount
addAppLogEvent(postbox: component.context.account.postbox, type: "premium_gift.promo_screen_accept")
let entities = generateChatInputTextEntities(self.textInputState.text)
let purpose: AppStoreTransactionPurpose = .giftCode(peerIds: [component.peerId], boostPeer: nil, currency: currency, amount: amount, text: self.textInputState.text.string, entities: entities)
let quantity: Int32 = 1
let completion = component.completion
let _ = (component.context.engine.payments.canPurchasePremium(purpose: purpose)
|> deliverOnMainQueue).start(next: { [weak self] available in
guard let self else {
return
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
if available {
let _ = (inAppPurchaseManager.buyProduct(storeProduct, quantity: quantity, purpose: purpose)
|> deliverOnMainQueue).start(next: { [weak self] status in
if let completion {
completion()
} else {
guard let self, case .purchased = status, let controller = self.environment?.controller(), let navigationController = controller.navigationController as? NavigationController else {
return
}
var controllers = navigationController.viewControllers
controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) && !($0 is PeerInfoScreen) && !($0 is ContactSelectionController) }
var foundController = false
for controller in controllers.reversed() {
if let chatController = controller as? ChatController, case .peer(id: component.peerId) = chatController.chatLocation {
chatController.hintPlayNextOutgoingGift()
foundController = true
break
}
}
if !foundController {
let chatController = component.context.sharedContext.makeChatController(context: component.context, chatLocation: .peer(id: component.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil)
chatController.hintPlayNextOutgoingGift()
controllers.append(chatController)
}
navigationController.setViewControllers(controllers, animated: true)
}
}, error: { [weak self] error in
guard let self, let controller = self.environment?.controller() else {
return
}
self.state?.updated(transition: .immediate)
var errorText: String?
switch error {
case .generic:
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
case .network:
errorText = presentationData.strings.Premium_Purchase_ErrorNetwork
case .notAllowed:
errorText = presentationData.strings.Premium_Purchase_ErrorNotAllowed
case .cantMakePayments:
errorText = presentationData.strings.Premium_Purchase_ErrorCantMakePayments
case .assignFailed:
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
case .tryLater:
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
case .cancelled:
break
}
if let errorText {
addAppLogEvent(postbox: component.context.account.postbox, type: "premium_gift.promo_screen_fail")
let alertController = textAlertController(context: component.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
controller.present(alertController, in: .window(.root))
}
})
} else {
self.inProgress = false
self.state?.updated(transition: .immediate)
}
})
}
func proceedWithStarGift() {
guard let component = self.component, case let .starGift(starGift) = component.subject, let starsContext = component.context.starsContext, let starsState = starsContext.currentState else {
return
}
let proceed = { [weak self] in
guard let self else {
return
}
self.inProgress = true
self.state?.updated()
let entities = generateChatInputTextEntities(self.textInputState.text)
let source: BotPaymentInvoiceSource = .starGift(hideName: self.hideName, peerId: component.peerId, giftId: component.gift.id, text: self.textInputState.text.string, entities: entities)
let source: BotPaymentInvoiceSource = .starGift(hideName: self.hideName, peerId: component.peerId, giftId: starGift.id, text: self.textInputState.text.string, entities: entities)
let inputData = BotCheckoutController.InputData.fetch(context: component.context, source: source)
|> map(Optional.init)
|> `catch` { _ -> Signal<BotCheckoutController.InputData?, NoError> in
@ -242,11 +354,13 @@ final class GiftSetupScreenComponent: Component {
}
navigationController.setViewControllers(controllers, animated: true)
}
starsContext.load(force: true)
})
})
}
if starsState.balance < component.gift.price {
if starsState.balance < starGift.price {
let _ = (self.optionsPromise.get()
|> filter { $0 != nil }
|> take(1)
@ -258,7 +372,7 @@ final class GiftSetupScreenComponent: Component {
context: component.context,
starsContext: starsContext,
options: options ?? [],
purpose: .starGift(peerId: component.peerId, requiredStars: component.gift.price),
purpose: .starGift(peerId: component.peerId, requiredStars: starGift.price),
completion: { [weak starsContext] stars in
starsContext?.add(balance: stars)
Queue.mainQueue().after(0.1) {
@ -349,9 +463,7 @@ final class GiftSetupScreenComponent: Component {
return
}
if let textInputView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View {
if self.textInputState.isEditing {
textInputView.insertText(text: text)
}
textInputView.insertText(text: text)
}
},
backwardsDeleteText: { [weak self] in
@ -400,6 +512,16 @@ final class GiftSetupScreenComponent: Component {
}
}
)
if case .starGift = component.subject {
self.optionsDisposable = (component.context.engine.payments.starsTopUpOptions()
|> deliverOnMainQueue).start(next: { [weak self] options in
guard let self else {
return
}
self.options = options
})
}
}
let environment = environment[EnvironmentType.self].value
@ -453,6 +575,42 @@ final class GiftSetupScreenComponent: Component {
contentHeight += environment.navigationHeight
contentHeight += 26.0
if case let .starGift(starGift) = component.subject, let availability = starGift.availability {
let remains: Int32 = availability.remains
let position = CGFloat(remains) / CGFloat(availability.total)
let remainsString = "\(remains)"
let totalString = presentationStringsFormattedNumber(availability.total, environment.dateTimeFormat.groupingSeparator)
let remainingCountSize = self.remainingCount.update(
transition: transition,
component: AnyComponent(RemainingCountComponent(
inactiveColor: environment.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3),
activeColors: [UIColor(rgb: 0x5bc2ff), UIColor(rgb: 0x2d9eff)],
inactiveTitle: environment.strings.Gift_Send_Limited,
inactiveValue: "",
inactiveTitleColor: environment.theme.list.itemSecondaryTextColor,
activeTitle: "",
activeValue: totalString,
activeTitleColor: .white,
badgeText: "\(remainsString)",
badgePosition: position,
badgeGraphPosition: position,
invertProgress: true
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let remainingCountFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight - 36.0), size: remainingCountSize)
if let remainingCountView = self.remainingCount.view {
if remainingCountView.superview == nil {
self.scrollView.addSubview(remainingCountView)
}
transition.setFrame(view: remainingCountView, frame: remainingCountFrame)
}
contentHeight += remainingCountSize.height
contentHeight -= 36.0
contentHeight += sectionSpacing
}
let giftConfiguration = GiftConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
var introSectionItems: [AnyComponentWithIdentity<Empty>] = []
@ -521,20 +679,30 @@ final class GiftSetupScreenComponent: Component {
inputHeight = environment.inputHeight
}
}
let peerName = self.peerMap[component.peerId]?.compactDisplayTitle ?? ""
let introFooter: AnyComponent<Empty>?
switch component.subject {
case .premium:
introFooter = AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Gift_Send_Customize_Info(peerName).string,
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
))
case .starGift:
introFooter = nil
}
let introSectionSize = self.introSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Gift_Send_Customize_Title,
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: nil,
header: nil,
footer: introFooter,
items: introSectionItems
)),
environment: {},
@ -553,6 +721,15 @@ final class GiftSetupScreenComponent: Component {
let listItemParams = ListViewItemLayoutParams(width: availableSize.width - sideInset * 2.0, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true)
if let accountPeer = self.peerMap[component.context.account.peerId] {
let subject: ChatGiftPreviewItem.Subject
switch component.subject {
case let .premium(product):
let (currency, amount) = product.storeProduct?.priceCurrencyAndAmount ?? ("USD", 1)
subject = .premium(months: product.months, amount: amount, currency: currency)
case let .starGift(gift):
subject = .starGift(gift: gift)
}
let introContentSize = self.introContent.update(
transition: transition,
component: AnyComponent(
@ -569,7 +746,7 @@ final class GiftSetupScreenComponent: Component {
dateTimeFormat: environment.dateTimeFormat,
nameDisplayOrder: presentationData.nameDisplayOrder,
accountPeer: accountPeer,
gift: component.gift,
subject: subject,
text: self.textInputState.text.string,
entities: generateChatInputTextEntities(self.textInputState.text)
),
@ -589,55 +766,56 @@ final class GiftSetupScreenComponent: Component {
}
}
let peerName = self.peerMap[component.peerId]?.compactDisplayTitle ?? ""
let hideSectionSize = self.hideSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: nil,
footer: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Gift_Send_HideMyName_Info(peerName, peerName).string,
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
if case .starGift = component.subject {
let hideSectionSize = self.hideSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: nil,
footer: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Gift_Send_HideMyName_Info(peerName, peerName).string,
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
maximumNumberOfLines: 0
items: [
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Gift_Send_HideMyName,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.hideName, action: { [weak self] _ in
guard let self else {
return
}
self.hideName = !self.hideName
self.state?.updated(transition: .spring(duration: 0.4))
})),
action: nil
)))
]
)),
items: [
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Gift_Send_HideMyName,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.hideName, action: { [weak self] _ in
guard let self else {
return
}
self.hideName = !self.hideName
self.state?.updated(transition: .spring(duration: 0.4))
})),
action: nil
)))
]
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let hideSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: hideSectionSize)
if let hideSectionView = self.hideSection.view {
if hideSectionView.superview == nil {
self.scrollView.addSubview(hideSectionView)
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let hideSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: hideSectionSize)
if let hideSectionView = self.hideSection.view {
if hideSectionView.superview == nil {
self.scrollView.addSubview(hideSectionView)
}
transition.setFrame(view: hideSectionView, frame: hideSectionFrame)
}
transition.setFrame(view: hideSectionView, frame: hideSectionFrame)
contentHeight += hideSectionSize.height
}
contentHeight += hideSectionSize.height
contentHeight += bottomContentInset
@ -647,8 +825,18 @@ final class GiftSetupScreenComponent: Component {
if self.starImage == nil || self.starImage?.1 !== environment.theme {
self.starImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: environment.theme.list.itemCheckColors.foregroundColor)!, environment.theme)
}
let amountString = presentationStringsFormattedNumber(Int32(component.gift.price), presentationData.dateTimeFormat.groupingSeparator)
let buttonAttributedString = NSMutableAttributedString(string: "\(environment.strings.Gift_Send_Send) # \(amountString)", font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
let buttonString: String
switch component.subject {
case let .premium(product):
let amountString = product.price
buttonString = "\(environment.strings.Gift_Send_Send) \(amountString)"
case let .starGift(starGift):
let amountString = presentationStringsFormattedNumber(Int32(starGift.price), presentationData.dateTimeFormat.groupingSeparator)
buttonString = "\(environment.strings.Gift_Send_Send) # \(amountString)"
}
let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
if let range = buttonAttributedString.string.range(of: "#"), let starImage = self.starImage?.0 {
buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string))
buttonAttributedString.addAttribute(.foregroundColor, value: environment.theme.list.itemCheckColors.foregroundColor, range: NSRange(range, in: buttonAttributedString.string))
@ -669,7 +857,7 @@ final class GiftSetupScreenComponent: Component {
component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))
),
isEnabled: true,
displaysProgress: false,
displaysProgress: self.inProgress,
action: { [weak self] in
self?.proceed()
}
@ -825,7 +1013,7 @@ final class GiftSetupScreenComponent: Component {
}
}
}
if self.recenterOnTag == nil && self.previousHadInputHeight != (environment.inputHeight > 0.0) {
if self.recenterOnTag == nil && self.previousHadInputHeight != (environment.inputHeight > 0.0), case .keyboard = self.currentInputMode {
if self.textInputState.isEditing {
self.recenterOnTag = self.textInputTag
}
@ -1039,12 +1227,17 @@ final class GiftSetupScreenComponent: Component {
}
public final class GiftSetupScreen: ViewControllerComponentContainer {
public enum Subject: Equatable {
case premium(PremiumGiftProduct)
case starGift(StarGift)
}
private let context: AccountContext
public init(
context: AccountContext,
peerId: EnginePeer.Id,
gift: StarGift,
subject: Subject,
completion: (() -> Void)? = nil
) {
self.context = context
@ -1052,7 +1245,7 @@ public final class GiftSetupScreen: ViewControllerComponentContainer {
super.init(context: context, component: GiftSetupScreenComponent(
context: context,
peerId: peerId,
gift: gift,
subject: subject,
completion: completion
), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil)
@ -1070,9 +1263,6 @@ public final class GiftSetupScreen: ViewControllerComponentContainer {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
@objc private func cancelPressed() {
self.dismiss()
}
@ -1105,3 +1295,31 @@ private struct GiftConfiguration {
}
}
}
public struct PremiumGiftProduct: Equatable {
public let giftOption: CachedPremiumGiftOption
public let storeProduct: InAppPurchaseManager.Product?
public let discount: Int?
public var id: String {
return self.storeProduct?.id ?? (self.giftOption.storeProductId ?? "")
}
public var months: Int32 {
return self.giftOption.months
}
public var price: String {
return self.storeProduct?.price ?? formatCurrencyAmount(self.giftOption.amount, currency: self.giftOption.currency)
}
public var pricePerMonth: String {
return self.storeProduct?.pricePerMonth(Int(self.months)) ?? ""
}
public init(giftOption: CachedPremiumGiftOption, storeProduct: InAppPurchaseManager.Product?, discount: Int?) {
self.giftOption = giftOption
self.storeProduct = storeProduct
self.discount = discount
}
}

View File

@ -0,0 +1,827 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramCore
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import PresentationDataUtils
import ComponentFlow
import MultilineTextComponent
import Markdown
import TextFormat
import RoundedRectWithTailPath
public class RemainingCountComponent: Component {
private let inactiveColor: UIColor
private let activeColors: [UIColor]
private let inactiveTitle: String
private let inactiveValue: String
private let inactiveTitleColor: UIColor
private let activeTitle: String
private let activeValue: String
private let activeTitleColor: UIColor
private let badgeText: String?
private let badgePosition: CGFloat
private let badgeGraphPosition: CGFloat
private let invertProgress: Bool
public init(
inactiveColor: UIColor,
activeColors: [UIColor],
inactiveTitle: String,
inactiveValue: String,
inactiveTitleColor: UIColor,
activeTitle: String,
activeValue: String,
activeTitleColor: UIColor,
badgeText: String?,
badgePosition: CGFloat,
badgeGraphPosition: CGFloat,
invertProgress: Bool = false
) {
self.inactiveColor = inactiveColor
self.activeColors = activeColors
self.inactiveTitle = inactiveTitle
self.inactiveValue = inactiveValue
self.inactiveTitleColor = inactiveTitleColor
self.activeTitle = activeTitle
self.activeValue = activeValue
self.activeTitleColor = activeTitleColor
self.badgeText = badgeText
self.badgePosition = badgePosition
self.badgeGraphPosition = badgeGraphPosition
self.invertProgress = invertProgress
}
public static func ==(lhs: RemainingCountComponent, rhs: RemainingCountComponent) -> Bool {
if lhs.inactiveColor != rhs.inactiveColor {
return false
}
if lhs.activeColors != rhs.activeColors {
return false
}
if lhs.inactiveTitle != rhs.inactiveTitle {
return false
}
if lhs.inactiveValue != rhs.inactiveValue {
return false
}
if lhs.inactiveTitleColor != rhs.inactiveTitleColor {
return false
}
if lhs.activeTitle != rhs.activeTitle {
return false
}
if lhs.activeValue != rhs.activeValue {
return false
}
if lhs.activeTitleColor != rhs.activeTitleColor {
return false
}
if lhs.badgeText != rhs.badgeText {
return false
}
if lhs.badgePosition != rhs.badgePosition {
return false
}
if lhs.badgeGraphPosition != rhs.badgeGraphPosition {
return false
}
if lhs.invertProgress != rhs.invertProgress {
return false
}
return true
}
public final class View: UIView {
private var component: RemainingCountComponent?
private let container: UIView
private let inactiveBackground: SimpleLayer
private let inactiveTitleLabel = ComponentView<Empty>()
private let inactiveValueLabel = ComponentView<Empty>()
private let innerLeftTitleLabel = ComponentView<Empty>()
private let innerRightTitleLabel = ComponentView<Empty>()
private let activeContainer: UIView
private let activeBackground: SimpleLayer
private let activeTitleLabel = ComponentView<Empty>()
private let activeValueLabel = ComponentView<Empty>()
private let badgeView: UIView
private let badgeMaskView: UIView
private let badgeShapeLayer = CAShapeLayer()
private let badgeForeground: SimpleLayer
private let badgeLabel: BadgeLabelView
private let badgeLabelMaskView = UIImageView()
private var badgeTailPosition: CGFloat = 0.0
private var badgeShapeArguments: (Double, Double, CGSize, CGFloat, CGFloat)?
override init(frame: CGRect) {
self.container = UIView()
self.container.clipsToBounds = true
self.container.layer.cornerRadius = 9.0
self.inactiveBackground = SimpleLayer()
self.activeContainer = UIView()
self.activeContainer.clipsToBounds = true
self.activeBackground = SimpleLayer()
self.activeBackground.anchorPoint = CGPoint()
self.badgeView = UIView()
self.badgeView.alpha = 0.0
self.badgeShapeLayer.fillColor = UIColor.white.cgColor
self.badgeShapeLayer.transform = CATransform3DMakeScale(1.0, -1.0, 1.0)
self.badgeMaskView = UIView()
self.badgeMaskView.layer.addSublayer(self.badgeShapeLayer)
self.badgeView.mask = self.badgeMaskView
self.badgeForeground = SimpleLayer()
self.badgeLabel = BadgeLabelView()
let _ = self.badgeLabel.update(value: "0", transition: .immediate)
self.badgeLabel.mask = self.badgeLabelMaskView
super.init(frame: frame)
self.addSubview(self.container)
self.container.layer.addSublayer(self.inactiveBackground)
self.container.addSubview(self.activeContainer)
self.activeContainer.layer.addSublayer(self.activeBackground)
self.addSubview(self.badgeView)
self.badgeView.layer.addSublayer(self.badgeForeground)
self.badgeView.addSubview(self.badgeLabel)
self.badgeLabelMaskView.contentMode = .scaleToFill
self.badgeLabelMaskView.image = generateImage(CGSize(width: 2.0, height: 30.0), rotatedContext: { size, context in
let bounds = CGRect(origin: .zero, size: size)
context.clear(bounds)
let colorsArray: [CGColor] = [
UIColor(rgb: 0xffffff, alpha: 0.0).cgColor,
UIColor(rgb: 0xffffff).cgColor,
UIColor(rgb: 0xffffff).cgColor,
UIColor(rgb: 0xffffff, alpha: 0.0).cgColor,
]
var locations: [CGFloat] = [0.0, 0.24, 0.76, 1.0]
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
})
self.isUserInteractionEnabled = false
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.badgeShapeAnimator?.invalidate()
}
private var didPlayAppearanceAnimation = false
func playAppearanceAnimation(component: RemainingCountComponent, badgeFullSize: CGSize, from: CGFloat? = nil) {
if from == nil {
self.badgeView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.4, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
}
let rotationAngle: CGFloat
if badgeFullSize.width > 100.0 {
rotationAngle = 0.2
} else {
rotationAngle = 0.26
}
let to: CGFloat = self.badgeView.center.x
let positionAnimation = CABasicAnimation(keyPath: "position.x")
positionAnimation.fromValue = NSValue(cgPoint: CGPoint(x: from ?? self.container.frame.width, y: 0.0))
positionAnimation.toValue = NSValue(cgPoint: CGPoint(x: to, y: 0.0))
positionAnimation.duration = 0.5
positionAnimation.fillMode = .forwards
positionAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
self.badgeView.layer.add(positionAnimation, forKey: "appearance1")
if from != to {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotateAnimation.fromValue = 0.0 as NSNumber
rotateAnimation.toValue = rotationAngle as NSNumber
rotateAnimation.duration = 0.15
rotateAnimation.fillMode = .forwards
rotateAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut)
rotateAnimation.isRemovedOnCompletion = false
self.badgeView.layer.add(rotateAnimation, forKey: "appearance2")
Queue.mainQueue().after(0.5, {
let bounceAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
bounceAnimation.fromValue = rotationAngle as NSNumber
bounceAnimation.toValue = -0.04 as NSNumber
bounceAnimation.duration = 0.2
bounceAnimation.fillMode = .forwards
bounceAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut)
bounceAnimation.isRemovedOnCompletion = false
self.badgeView.layer.add(bounceAnimation, forKey: "appearance3")
self.badgeView.layer.removeAnimation(forKey: "appearance2")
Queue.mainQueue().after(0.2) {
let returnAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
returnAnimation.fromValue = -0.04 as NSNumber
returnAnimation.toValue = 0.0 as NSNumber
returnAnimation.duration = 0.15
returnAnimation.fillMode = .forwards
returnAnimation.timingFunction = CAMediaTimingFunction(name: .easeIn)
self.badgeView.layer.add(returnAnimation, forKey: "appearance4")
self.badgeView.layer.removeAnimation(forKey: "appearance3")
}
})
}
if from == nil {
self.badgeView.alpha = 1.0
self.badgeView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
}
if let badgeText = component.badgeText {
let transition: ComponentTransition = .easeInOut(duration: from != nil ? 0.3 : 0.5)
var frameTransition = transition
if from == nil {
frameTransition = frameTransition.withAnimation(.none)
}
let badgeLabelSize = self.badgeLabel.update(value: badgeText, transition: transition)
frameTransition.setFrame(view: self.badgeLabel, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((badgeFullSize.width - badgeLabelSize.width) / 2.0), y: -2.0), size: badgeLabelSize))
}
}
var previousAvailableSize: CGSize?
func update(component: RemainingCountComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
self.component = component
self.inactiveBackground.backgroundColor = component.inactiveColor.cgColor
self.activeBackground.backgroundColor = component.activeColors.last?.cgColor
let size = CGSize(width: availableSize.width, height: 90.0)
self.badgeLabel.color = component.activeTitleColor
let lineHeight: CGFloat = 30.0
let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - lineHeight), size: CGSize(width: size.width, height: lineHeight))
self.container.frame = containerFrame
let activityPosition: CGFloat = floor(containerFrame.width * component.badgeGraphPosition)
let activeWidth: CGFloat = containerFrame.width - activityPosition
let leftTextColor: UIColor
let rightTextColor: UIColor
if component.invertProgress {
leftTextColor = component.inactiveTitleColor
rightTextColor = component.inactiveTitleColor
} else {
leftTextColor = component.inactiveTitleColor
rightTextColor = component.activeTitleColor
}
if "".isEmpty {
if component.invertProgress {
let innerLeftTitleSize = self.innerLeftTitleLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.inactiveTitle,
font: Font.semibold(15.0),
textColor: component.activeTitleColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.innerLeftTitleLabel.view {
if view.superview == nil {
self.activeContainer.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((lineHeight - innerLeftTitleSize.height) / 2.0)), size: innerLeftTitleSize)
}
let innerRightTitleSize = self.innerRightTitleLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.activeValue,
font: Font.semibold(15.0),
textColor: component.activeTitleColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.innerRightTitleLabel.view {
if view.superview == nil {
self.activeContainer.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: containerFrame.width - 12.0 - innerRightTitleSize.width, y: floorToScreenPixels((lineHeight - innerRightTitleSize.height) / 2.0)), size: innerRightTitleSize)
}
}
let inactiveTitleSize = self.inactiveTitleLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.inactiveTitle,
font: Font.semibold(15.0),
textColor: leftTextColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.inactiveTitleLabel.view {
if view.superview == nil {
self.container.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((lineHeight - inactiveTitleSize.height) / 2.0)), size: inactiveTitleSize)
}
let inactiveValueSize = self.inactiveValueLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.inactiveValue,
font: Font.semibold(15.0),
textColor: leftTextColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.inactiveValueLabel.view {
if view.superview == nil {
self.container.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: activityPosition - 12.0 - inactiveValueSize.width, y: floorToScreenPixels((lineHeight - inactiveValueSize.height) / 2.0)), size: inactiveValueSize)
}
let activeTitleSize = self.activeTitleLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.activeTitle,
font: Font.semibold(15.0),
textColor: rightTextColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.activeTitleLabel.view {
if view.superview == nil {
self.container.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: activityPosition + 12.0, y: floorToScreenPixels((lineHeight - activeTitleSize.height) / 2.0)), size: activeTitleSize)
}
let activeValueSize = self.activeValueLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.activeValue,
font: Font.semibold(15.0),
textColor: rightTextColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.activeValueLabel.view {
if view.superview == nil {
self.container.addSubview(view)
if component.invertProgress {
self.container.bringSubviewToFront(self.activeContainer)
}
}
view.frame = CGRect(origin: CGPoint(x: containerFrame.width - 12.0 - activeValueSize.width, y: floorToScreenPixels((lineHeight - activeValueSize.height) / 2.0)), size: activeValueSize)
}
}
var progressTransition: ComponentTransition = .immediate
if !transition.animation.isImmediate {
progressTransition = .easeInOut(duration: 0.5)
}
if "".isEmpty {
if component.invertProgress {
progressTransition.setFrame(layer: self.inactiveBackground, frame: CGRect(origin: CGPoint(x: activityPosition, y: 0.0), size: CGSize(width: size.width - activityPosition, height: lineHeight)))
progressTransition.setFrame(view: self.activeContainer, frame: CGRect(origin: .zero, size: CGSize(width: activityPosition, height: lineHeight)))
progressTransition.setBounds(layer: self.activeBackground, bounds: CGRect(origin: .zero, size: CGSize(width: containerFrame.width * 1.35, height: lineHeight)))
} else {
progressTransition.setFrame(layer: self.inactiveBackground, frame: CGRect(origin: .zero, size: CGSize(width: activityPosition, height: lineHeight)))
progressTransition.setFrame(view: self.activeContainer, frame: CGRect(origin: CGPoint(x: activityPosition, y: 0.0), size: CGSize(width: activeWidth, height: lineHeight)))
progressTransition.setFrame(layer: self.activeBackground, frame: CGRect(origin: CGPoint(x: -activityPosition, y: 0.0), size: CGSize(width: containerFrame.width * 1.35, height: lineHeight)))
}
if self.activeBackground.animation(forKey: "movement") == nil {
self.activeBackground.position = CGPoint(x: -self.activeContainer.frame.width * 0.35, y: lineHeight / 2.0)
}
}
let countWidth: CGFloat
if let badgeText = component.badgeText {
countWidth = CGFloat(badgeText.count) * 10.0
} else {
countWidth = 51.0
}
let badgeWidth: CGFloat = countWidth + 20.0
let badgeSize = CGSize(width: badgeWidth, height: 30.0)
let badgeFullSize = CGSize(width: badgeWidth, height: 30.0 + 8.0)
let tailSize = CGSize(width: 15.0, height: 6.0)
let tailRadius: CGFloat = 3.0
self.badgeMaskView.frame = CGRect(origin: .zero, size: badgeFullSize)
self.badgeShapeLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -4.0), size: badgeFullSize)
self.badgeView.bounds = CGRect(origin: .zero, size: badgeFullSize)
let currentBadgeX = self.badgeView.center.x
let badgePosition = component.badgePosition
if badgePosition > 1.0 - 0.15 {
progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 1.0, y: 1.0))
if progressTransition.animation.isImmediate {
self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailSize: tailSize, tailRadius: tailRadius, tailPosition: 1.0).cgPath
} else {
self.badgeShapeArguments = (CACurrentMediaTime(), 0.5, badgeSize, self.badgeTailPosition, 1.0)
self.animateBadgeTailPositionChange()
}
self.badgeTailPosition = 1.0
if let _ = self.badgeView.layer.animation(forKey: "appearance1") {
} else {
self.badgeView.center = CGPoint(x: 3.0 + (size.width - 6.0) * badgePosition + 3.0, y: 56.0)
}
} else if badgePosition < 0.15 {
progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 0.0, y: 1.0))
if progressTransition.animation.isImmediate {
self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailSize: tailSize, tailRadius: tailRadius, tailPosition: 0.0).cgPath
} else {
self.badgeShapeArguments = (CACurrentMediaTime(), 0.5, badgeSize, self.badgeTailPosition, 0.0)
self.animateBadgeTailPositionChange()
}
self.badgeTailPosition = 0.0
if let _ = self.badgeView.layer.animation(forKey: "appearance1") {
} else {
self.badgeView.center = CGPoint(x: (size.width - 6.0) * badgePosition, y: 56.0)
}
} else {
progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 0.5, y: 1.0))
if progressTransition.animation.isImmediate {
self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailSize: tailSize, tailRadius: tailRadius, tailPosition: 0.5).cgPath
} else {
self.badgeShapeArguments = (CACurrentMediaTime(), 0.5, badgeSize, self.badgeTailPosition, 0.5)
self.animateBadgeTailPositionChange()
}
self.badgeTailPosition = 0.5
if let _ = self.badgeView.layer.animation(forKey: "appearance1") {
} else {
self.badgeView.center = CGPoint(x: size.width * badgePosition, y: 56.0)
}
}
self.badgeForeground.bounds = CGRect(origin: CGPoint(), size: CGSize(width: badgeFullSize.width * 3.0, height: badgeFullSize.height))
if self.badgeForeground.animation(forKey: "movement") == nil {
self.badgeForeground.position = CGPoint(x: badgeSize.width * 3.0 / 2.0 - self.badgeForeground.frame.width * 0.35, y: badgeFullSize.height / 2.0)
}
self.badgeLabelMaskView.frame = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 36.0)
if !self.didPlayAppearanceAnimation || !transition.animation.isImmediate {
self.didPlayAppearanceAnimation = true
if transition.animation.isImmediate {
if component.badgePosition < 0.1 {
self.badgeView.alpha = 1.0
if let badgeText = component.badgeText {
let badgeLabelSize = self.badgeLabel.update(value: badgeText, transition: .immediate)
transition.setFrame(view: self.badgeLabel, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((badgeFullSize.width - badgeLabelSize.width) / 2.0), y: -2.0), size: badgeLabelSize))
}
} else {
self.playAppearanceAnimation(component: component, badgeFullSize: badgeFullSize)
}
} else {
self.playAppearanceAnimation(component: component, badgeFullSize: badgeFullSize, from: currentBadgeX)
}
}
if self.previousAvailableSize != availableSize {
self.previousAvailableSize = availableSize
var locations: [CGFloat] = []
let delta = 1.0 / CGFloat(component.activeColors.count - 1)
for i in 0 ..< component.activeColors.count {
locations.append(delta * CGFloat(i))
}
let gradient = generateGradientImage(size: CGSize(width: 200.0, height: 60.0), colors: component.activeColors, locations: locations, direction: .horizontal)
self.badgeForeground.contentsGravity = .resizeAspectFill
self.badgeForeground.contents = gradient?.cgImage
self.activeBackground.contentsGravity = .resizeAspectFill
self.activeBackground.contents = gradient?.cgImage
self.setupGradientAnimations()
}
return size
}
private var badgeShapeAnimator: ConstantDisplayLinkAnimator?
private func animateBadgeTailPositionChange() {
if self.badgeShapeAnimator == nil {
self.badgeShapeAnimator = ConstantDisplayLinkAnimator(update: { [weak self] in
self?.animateBadgeTailPositionChange()
})
self.badgeShapeAnimator?.isPaused = true
}
if let (startTime, duration, badgeSize, initial, target) = self.badgeShapeArguments {
self.badgeShapeAnimator?.isPaused = false
let t = CGFloat(max(0.0, min(1.0, (CACurrentMediaTime() - startTime) / duration)))
let value = initial + (target - initial) * t
let tailSize = CGSize(width: 15.0, height: 6.0)
let tailRadius: CGFloat = 3.0
self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailSize: tailSize, tailRadius: tailRadius, tailPosition: value).cgPath
if t >= 1.0 {
self.badgeShapeArguments = nil
self.badgeShapeAnimator?.isPaused = true
self.badgeShapeAnimator?.invalidate()
self.badgeShapeAnimator = nil
}
} else {
self.badgeShapeAnimator?.isPaused = true
self.badgeShapeAnimator?.invalidate()
self.badgeShapeAnimator = nil
}
}
private func setupGradientAnimations() {
guard let _ = self.component else {
return
}
if let _ = self.badgeForeground.animation(forKey: "movement") {
} else {
CATransaction.begin()
let badgeOffset = (self.badgeForeground.frame.width - self.badgeView.bounds.width) / 2.0
let badgePreviousValue = self.badgeForeground.position.x
var badgeNewValue: CGFloat = badgeOffset
if badgeOffset - badgePreviousValue < self.badgeForeground.frame.width * 0.25 {
badgeNewValue -= self.badgeForeground.frame.width * 0.35
}
self.badgeForeground.position = CGPoint(x: badgeNewValue, y: self.badgeForeground.bounds.size.height / 2.0)
let lineOffset = 0.0
let linePreviousValue = self.activeBackground.position.x
var lineNewValue: CGFloat = lineOffset
if linePreviousValue < 0.0 {
lineNewValue = 0.0
} else {
lineNewValue = -self.activeContainer.bounds.width * 0.35
}
lineNewValue -= self.activeContainer.frame.minX
self.activeBackground.position = CGPoint(x: lineNewValue, y: 0.0)
let badgeAnimation = CABasicAnimation(keyPath: "position.x")
badgeAnimation.duration = 4.5
badgeAnimation.fromValue = badgePreviousValue
badgeAnimation.toValue = badgeNewValue
badgeAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
CATransaction.setCompletionBlock { [weak self] in
self?.setupGradientAnimations()
}
self.badgeForeground.add(badgeAnimation, forKey: "movement")
let lineAnimation = CABasicAnimation(keyPath: "position.x")
lineAnimation.duration = 4.5
lineAnimation.fromValue = linePreviousValue
lineAnimation.toValue = lineNewValue
lineAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
self.activeBackground.add(lineAnimation, forKey: "movement")
CATransaction.commit()
}
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
private let labelWidth: CGFloat = 10.0
private let labelHeight: CGFloat = 30.0
private let labelSize = CGSize(width: labelWidth, height: labelHeight)
private let font = Font.with(size: 15.0, design: .regular, weight: .semibold, traits: [])
final class BadgeLabelView: UIView {
private class StackView: UIView {
var labels: [UILabel] = []
var currentValue: Int32 = 0
var color: UIColor = .white {
didSet {
for view in self.labels {
view.textColor = self.color
}
}
}
init() {
super.init(frame: CGRect(origin: .zero, size: labelSize))
var height: CGFloat = -labelHeight
for i in -1 ..< 10 {
let label = UILabel()
if i == -1 {
label.text = "9"
} else {
label.text = "\(i)"
}
label.textColor = self.color
label.font = font
label.textAlignment = .center
label.frame = CGRect(x: 0, y: height, width: labelWidth, height: labelHeight)
self.addSubview(label)
self.labels.append(label)
height += labelHeight
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(value: Int32, isFirst: Bool, isLast: Bool, transition: ComponentTransition) {
let previousValue = self.currentValue
self.currentValue = value
self.labels[1].alpha = isFirst && !isLast ? 0.0 : 1.0
if previousValue == 9 && value < 9 {
self.bounds = CGRect(
origin: CGPoint(
x: 0.0,
y: -1.0 * labelSize.height
),
size: labelSize
)
}
let bounds = CGRect(
origin: CGPoint(
x: 0.0,
y: CGFloat(value) * labelSize.height
),
size: labelSize
)
transition.setBounds(view: self, bounds: bounds)
}
}
private var itemViews: [Int: StackView] = [:]
private var staticLabel = UILabel()
init() {
super.init(frame: .zero)
self.clipsToBounds = true
self.isUserInteractionEnabled = false
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var color: UIColor = .white {
didSet {
self.staticLabel.textColor = self.color
for (_, view) in self.itemViews {
view.color = self.color
}
}
}
func update(value: String, transition: ComponentTransition) -> CGSize {
if value.contains(" ") {
for (_, view) in self.itemViews {
view.isHidden = true
}
if self.staticLabel.superview == nil {
self.staticLabel.textColor = self.color
self.staticLabel.font = font
self.addSubview(self.staticLabel)
}
self.staticLabel.text = value
let size = self.staticLabel.sizeThatFits(CGSize(width: 100.0, height: 100.0))
self.staticLabel.frame = CGRect(origin: .zero, size: CGSize(width: size.width, height: labelHeight))
return CGSize(width: ceil(self.staticLabel.bounds.width), height: ceil(self.staticLabel.bounds.height))
}
let string = value
let stringArray = Array(string.map { String($0) }.reversed())
let totalWidth = CGFloat(stringArray.count) * labelWidth
var validIds: [Int] = []
for i in 0 ..< stringArray.count {
validIds.append(i)
let itemView: StackView
var itemTransition = transition
if let current = self.itemViews[i] {
itemView = current
} else {
itemTransition = transition.withAnimation(.none)
itemView = StackView()
itemView.color = self.color
self.itemViews[i] = itemView
self.addSubview(itemView)
}
let digit = Int32(stringArray[i]) ?? 0
itemView.update(value: digit, isFirst: i == stringArray.count - 1, isLast: i == 0, transition: transition)
itemTransition.setFrame(
view: itemView,
frame: CGRect(x: totalWidth - labelWidth * CGFloat(i + 1), y: 0.0, width: labelWidth, height: labelHeight)
)
}
var removeIds: [Int] = []
for (id, itemView) in self.itemViews {
if !validIds.contains(id) {
removeIds.append(id)
transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in
itemView.removeFromSuperview()
})
}
}
for id in removeIds {
self.itemViews.removeValue(forKey: id)
}
return CGSize(width: totalWidth, height: labelHeight)
}
}

View File

@ -181,10 +181,12 @@ private final class GiftViewSheetContent: CombinedComponent {
let text: String?
let entities: [MessageTextEntity]?
let limitTotal: Int32?
var outgoing = false
var incoming = false
var savedToProfile = false
var converted = false
var giftId: Int64 = 0
var date: Int32 = 0
if let arguments = component.subject.arguments {
animationFile = arguments.gift.file
stars = arguments.gift.price
@ -192,10 +194,16 @@ private final class GiftViewSheetContent: CombinedComponent {
entities = arguments.entities
limitTotal = arguments.gift.availability?.total
convertStars = arguments.convertStars
if case .message = component.subject {
outgoing = !arguments.incoming
} else {
outgoing = false
}
incoming = arguments.incoming || arguments.peerId == component.context.account.peerId
savedToProfile = arguments.savedToProfile
converted = arguments.converted
giftId = arguments.gift.id
date = arguments.date
} else {
animationFile = nil
stars = 0
@ -235,11 +243,13 @@ private final class GiftViewSheetContent: CombinedComponent {
descriptionText = modifiedString
}
let formattedAmount = presentationStringsFormattedNumber(abs(Int32(stars)), dateTimeFormat.groupingSeparator)
var formattedAmount = presentationStringsFormattedNumber(abs(Int32(stars)), dateTimeFormat.groupingSeparator)
if outgoing {
formattedAmount = "- \(formattedAmount)"
}
let countFont: UIFont = Font.semibold(17.0)
let amountText = formattedAmount
let countColor = theme.list.itemDisclosureActions.constructive.fillColor
let countColor = outgoing ? theme.list.itemDestructiveColor : theme.list.itemDisclosureActions.constructive.fillColor
let title = title.update(
component: MultilineTextComponent(
@ -331,7 +341,7 @@ private final class GiftViewSheetContent: CombinedComponent {
id: "date",
title: strings.Gift_View_Date,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: Int32(Date().timeIntervalSince1970), strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
)
))
@ -340,11 +350,13 @@ private final class GiftViewSheetContent: CombinedComponent {
if let gift = state.starGiftsMap[giftId], let availability = gift.availability {
remains = availability.remains
}
let remainsString = presentationStringsFormattedNumber(remains, environment.dateTimeFormat.groupingSeparator)
let totalString = presentationStringsFormattedNumber(limitTotal, environment.dateTimeFormat.groupingSeparator)
tableItems.append(.init(
id: "availability",
title: strings.Gift_View_Availability,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: "\(remains) of \(limitTotal)", font: tableFont, textColor: tableTextColor)))
MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_View_Availability_Of("\(remainsString)", "\(totalString)").string, font: tableFont, textColor: tableTextColor)))
)
))
}
@ -361,7 +373,8 @@ private final class GiftViewSheetContent: CombinedComponent {
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
placeholderColor: theme.list.mediaPlaceholderColor,
text: .plain(attributedText)
text: .plain(attributedText),
maximumNumberOfLines: 0
)
)
))
@ -717,14 +730,14 @@ public class GiftViewScreen: ViewControllerComponentContainer {
case message(EngineMessage)
case profileGift(EnginePeer.Id, ProfileGiftsContext.State.StarGift)
var arguments: (peerId: EnginePeer.Id, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, incoming: Bool, gift: StarGift, convertStars: Int64, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool)? {
var arguments: (peerId: EnginePeer.Id, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool)? {
switch self {
case let .message(message):
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted) = action.action {
return (message.id.peerId, message.author?.id, message.author?.compactDisplayTitle, message.id, message.flags.contains(.Incoming), gift, convertStars, text, entities, nameHidden, savedToProfile, converted)
return (message.id.peerId, message.author?.id, message.author?.compactDisplayTitle, message.id, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, converted)
}
case let .profileGift(peerId, gift):
return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, gift.messageId, false, gift.gift, gift.convertStars ?? 0, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, false)
return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, gift.messageId, false, gift.gift, gift.date, gift.convertStars ?? 0, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, false)
}
return nil
}
@ -1065,17 +1078,26 @@ private final class TableComponent: CombinedComponent {
} else {
insets = UIEdgeInsets(top: 0.0, left: horizontalPadding, bottom: 0.0, right: horizontalPadding)
}
let valueChild = valueChildren[item.id].update(
component: item.component,
availableSize: CGSize(width: rightColumnWidth - insets.left - insets.right, height: context.availableSize.height),
transition: context.transition
)
updatedValueChildren.append((valueChild, insets))
var titleHeight: CGFloat = 0.0
if let titleChild = updatedTitleChildren[i] {
titleHeight = titleChild.size.height
}
let availableValueWidth: CGFloat
if titleHeight > 0.0 {
availableValueWidth = rightColumnWidth
} else {
availableValueWidth = context.availableSize.width
}
let valueChild = valueChildren[item.id].update(
component: item.component,
availableSize: CGSize(width: availableValueWidth - insets.left - insets.right, height: context.availableSize.height),
transition: context.transition
)
updatedValueChildren.append((valueChild, insets))
let rowHeight = max(40.0, max(titleHeight, valueChild.size.height) + verticalPadding * 2.0)
rowHeights[i] = rowHeight
totalHeight += rowHeight

View File

@ -5,7 +5,7 @@ import TelegramPresentationData
import AnimationUI
import Display
class DynamicIslandMaskNode: ASDisplayNode {
final class DynamicIslandMaskNode: ASDisplayNode {
var animationNode: AnimationNode?
var isForum = false {
@ -39,7 +39,7 @@ class DynamicIslandMaskNode: ASDisplayNode {
}
}
class DynamicIslandBlurNode: ASDisplayNode {
final class DynamicIslandBlurNode: ASDisplayNode {
private var effectView: UIVisualEffectView?
private let fadeNode = ASDisplayNode()
let gradientNode = ASImageNode()

View File

@ -1382,7 +1382,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
url: "",
simple: true,
source: .generic,
skipTermsOfService: true
skipTermsOfService: true,
payload: nil
)
})
}

View File

@ -138,8 +138,10 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
var validIds: [AnyHashable] = []
var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: 60.0), size: starsOptionSize)
var index: Int32 = 0
for product in starsProducts {
let itemId = AnyHashable(product.date)
let itemId = AnyHashable(index)
validIds.append(itemId)
var itemTransition = transition
@ -221,6 +223,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
itemFrame.origin.x = sideInset
itemFrame.origin.y += starsOptionSize.height + optionSpacing
}
index += 1
}
var removeIds: [AnyHashable] = []
@ -243,7 +246,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
self.starsItems.removeValue(forKey: id)
}
var contentHeight = ceil(CGFloat(starsProducts.count) / 3.0) * starsOptionSize.height + 60.0 + 16.0
var contentHeight = ceil(CGFloat(starsProducts.count) / 3.0) * (starsOptionSize.height + optionSpacing) - optionSpacing + 60.0 + 16.0
if self.peerId == self.context.account.peerId {
let transition = ComponentTransition.immediate

View File

@ -945,8 +945,8 @@ final class AuthorizedApplicationContext {
chatLocation = .peer(peer)
}
if openAppIfAny, let parentController = self.rootController.viewControllers.last as? ViewController {
self.context.sharedContext.openWebApp(context: self.context, parentController: parentController, updatedPresentationData: nil, peer: peer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: true)
if openAppIfAny, case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), let parentController = self.rootController.viewControllers.last as? ViewController {
self.context.sharedContext.openWebApp(context: self.context, parentController: parentController, updatedPresentationData: nil, peer: peer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: true, payload: nil)
} else {
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: chatLocation, subject: isOutgoingMessage ? messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false) } : nil, activateInput: activateInput ? .text : nil))
}

View File

@ -14,7 +14,7 @@ import UndoUI
import UrlHandling
import TelegramPresentationData
func openWebAppImpl(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool) {
func openWebAppImpl(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?) {
let presentationData: PresentationData
if let parentController = parentController as? ChatControllerImpl {
presentationData = parentController.presentationData
@ -195,7 +195,7 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u
} else {
source = url.isEmpty ? .generic : .simple
}
let params = WebAppParameters(source: source, peerId: peer.id, botId: botId, botName: botName, botVerified: botVerified, url: result.url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize))
let params = WebAppParameters(source: source, peerId: peer.id, botId: botId, botName: botName, botVerified: botVerified, url: result.url, queryId: nil, payload: payload, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize))
let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, commit in
ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, present: { c, a in
presentImpl?(c, a)
@ -310,7 +310,7 @@ public extension ChatControllerImpl {
}
self.chatDisplayNode.dismissInput()
self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), threadId: self.chatLocation.threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: false)
self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), threadId: self.chatLocation.threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: false, payload: nil)
}
static func botRequestSwitchInline(context: AccountContext, controller: ChatControllerImpl?, peerId: EnginePeer.Id, botAddress: String, query: String, chatTypes: [ReplyMarkupButtonRequestPeerType]?, completion: @escaping () -> Void) -> Void {
@ -561,7 +561,7 @@ public extension ChatControllerImpl {
}
})
} else {
self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, peer: botPeer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false)
self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, peer: botPeer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false, payload: payload)
}
}
}

View File

@ -1172,7 +1172,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
strongSelf.push(wallpaperPreviewController)
return true
case let .giftPremium(_, _, duration, _, _):
case let .giftPremium(_, _, duration, _, _, _, _):
strongSelf.chatDisplayNode.dismissInput()
let fromPeerId: PeerId = message.author?.id == strongSelf.context.account.peerId ? strongSelf.context.account.peerId : message.id.peerId
let toPeerId: PeerId = message.author?.id == strongSelf.context.account.peerId ? message.id.peerId : strongSelf.context.account.peerId
@ -1187,7 +1187,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let controller = strongSelf.context.sharedContext.makeStarsGiftScreen(context: strongSelf.context, message: EngineMessage(message))
strongSelf.push(controller)
return true
case let .giftCode(slug, _, _, _, _, _, _, _, _):
case let .giftCode(slug, _, _, _, _, _, _, _, _, _, _):
strongSelf.openResolved(result: .premiumGiftCode(slug: slug), sourceMessageId: message.id, progress: params.progress)
return true
case .prizeStars:
@ -9721,7 +9721,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
commit()
})
} else {
strongSelf.context.sharedContext.openWebApp(context: strongSelf.context, parentController: strongSelf, updatedPresentationData: strongSelf.updatedPresentationData, peer: peer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false)
strongSelf.context.sharedContext.openWebApp(context: strongSelf.context, parentController: strongSelf, updatedPresentationData: strongSelf.updatedPresentationData, peer: peer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false, payload: botAppStart.payload)
commit()
}
}

View File

@ -2827,8 +2827,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return MiniAppListScreen(context: context, initialData: initialData as! MiniAppListScreen.InitialData)
}
public func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool) {
openWebAppImpl(context: context, parentController: parentController, updatedPresentationData: updatedPresentationData, peer: peer, threadId: threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: skipTermsOfService)
public func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?) {
openWebAppImpl(context: context, parentController: parentController, updatedPresentationData: updatedPresentationData, peer: peer, threadId: threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: skipTermsOfService, payload: payload)
}
}

View File

@ -1,5 +1,5 @@
{
"app": "11.2",
"app": "11.2.1",
"xcode": "16.0",
"bazel": "7.3.1",
"macos": "15.0"