Merge branch 'master' into glass

# Conflicts:
#	Telegram/Telegram-iOS/en.lproj/Localizable.strings
#	submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift
#	submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift
#	submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift
#	submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift
This commit is contained in:
Isaac 2025-09-16 10:14:07 +04:00
commit 0678d0ded0
58 changed files with 1821 additions and 244 deletions

View File

@ -15031,3 +15031,30 @@ Error: %8$@";
"Login.InfoLastNamePlaceholder" = "Last Name";
"Login.InfoDeletePhoto" = "Remove Photo";
"Login.InfoHelp" = "Enter your name and add a profile photo.";
"Gift.Upgrade.PriceWillDecrease" = "Price will decrease in {m}:{s}";
"Stars.Purchase.RemoveOriginalDetailsStarGiftInfo" = "Buy Stars to remove original details of your gift.";
"Notification.StarGift.TitleTo" = "Gift for %@";
"Gift.Upgrade.Gift.Sent.Text" = "Upgrade sent!";
"Gift.Upgrade.Gift.Sent.GiftMore" = "Gift More";
"Gift.Options.Gift.Limited.Left_1" = "%@ left";
"Gift.Options.Gift.Limited.Left_any" = "%@ left";
"Gift.Options.Gift.Premium.Left_1" = "%@ left";
"Gift.Options.Gift.Premium.Left_any" = "%@ left";
"Gift.Send.Limited.Success.Title" = "Gift Sent!";
"Gift.Send.Limited.Success.Text_1" = "You can send **%@** more.";
"Gift.Send.Limited.Success.Text_any" = "You can send **%@** more.";
"Gift.Send.Limited.Success.Text.None" = "You've reached your limit on this gift.";
"Gift.UpgradeCost.Title" = "Upgrade Cost";
"Gift.UpgradeCost.Stars_1" = "%@ Star";
"Gift.UpgradeCost.Stars_any" = "%@ Stars";
"Gift.UpgradeCost.Description" = "Users who upgrade their gifts first get collectibles with shorter numbers.";
"Gift.UpgradeCost.AdditionalDescription" = "Users who upgrade their gifts first get collectibles with shorter numbers.";
"Gift.UpgradeCost.Done" = "Understood";

View File

@ -703,6 +703,7 @@ public enum PeerInfoControllerMode {
case recommendedChannels
case myProfile
case gifts
case upgradableGifts
case myProfileGifts
case groupsInCommon
case monoforum(EnginePeer.Id)
@ -1310,7 +1311,7 @@ public protocol SharedAccountContext: AnyObject {
func makeStoryStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peerId: EnginePeer.Id, storyId: Int32, storyItem: EngineStoryItem, fromStory: Bool) -> ViewController
func makeStarsTransactionsScreen(context: AccountContext, starsContext: StarsContext) -> ViewController
func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [Any], purpose: StarsPurchasePurpose, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [Any], purpose: StarsPurchasePurpose, targetPeerId: EnginePeer.Id?, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController
func makeStarsSubscriptionTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, link: String, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)?, NoError>, navigateToPeer: @escaping (EnginePeer) -> Void) -> ViewController
func makeStarsTransactionScreen(context: AccountContext, transaction: StarsContext.State.Transaction, peer: EnginePeer) -> ViewController

View File

@ -143,6 +143,7 @@ public enum StarsPurchasePurpose: Equatable {
case transferStarGift(requiredStars: Int64)
case sendMessage(peerId: EnginePeer.Id, requiredStars: Int64)
case buyStarGift(requiredStars: Int64)
case removeOriginalDetailsStarGift(requiredStars: Int64)
}
public struct PremiumConfiguration {

View File

@ -6231,7 +6231,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let starsContext = self.context.starsContext else {
return
}
let controller = self.context.sharedContext.makeStarsPurchaseScreen(context: self.context, starsContext: starsContext, options: [], purpose: amount.flatMap({ .topUp(requiredStars: $0, purpose: "subs") }) ?? .generic, completion: { _ in })
let controller = self.context.sharedContext.makeStarsPurchaseScreen(context: self.context, starsContext: starsContext, options: [], purpose: amount.flatMap({ .topUp(requiredStars: $0, purpose: "subs") }) ?? .generic, targetPeerId: nil, completion: { _ in })
self.push(controller)
}

View File

@ -35,6 +35,7 @@ public final class MultilineTextWithEntitiesComponent: Component {
public let manualVisibilityControl: Bool
public let resetAnimationsOnVisibilityChange: Bool
public let displaysAsynchronously: Bool
public let maxWidth: CGFloat?
public let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)?
public let tapAction: (([NSAttributedString.Key: Any], Int) -> Void)?
public let longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)?
@ -60,6 +61,7 @@ public final class MultilineTextWithEntitiesComponent: Component {
manualVisibilityControl: Bool = false,
resetAnimationsOnVisibilityChange: Bool = false,
displaysAsynchronously: Bool = true,
maxWidth: CGFloat? = nil,
highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = nil,
tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil,
longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil
@ -85,6 +87,7 @@ public final class MultilineTextWithEntitiesComponent: Component {
self.manualVisibilityControl = manualVisibilityControl
self.resetAnimationsOnVisibilityChange = resetAnimationsOnVisibilityChange
self.displaysAsynchronously = displaysAsynchronously
self.maxWidth = maxWidth
self.tapAction = tapAction
self.longTapAction = longTapAction
}
@ -126,6 +129,9 @@ public final class MultilineTextWithEntitiesComponent: Component {
if lhs.displaysAsynchronously != rhs.displaysAsynchronously {
return false
}
if lhs.maxWidth != rhs.maxWidth {
return false
}
if let lhsTextShadowColor = lhs.textShadowColor, let rhsTextShadowColor = rhs.textShadowColor {
if !lhsTextShadowColor.isEqual(rhsTextShadowColor) {
return false
@ -237,7 +243,12 @@ public final class MultilineTextWithEntitiesComponent: Component {
)
}
let size = self.textNode.updateLayout(availableSize)
var constrainedSize = availableSize
if let maxWidth = component.maxWidth {
constrainedSize.width = maxWidth
}
let size = self.textNode.updateLayout(constrainedSize)
self.textNode.frame = CGRect(origin: .zero, size: size)
if component.handleSpoilers {
@ -266,7 +277,7 @@ public final class MultilineTextWithEntitiesComponent: Component {
spoilerTextNode.textStroke = component.textStroke
spoilerTextNode.isUserInteractionEnabled = false
let size = spoilerTextNode.updateLayout(availableSize)
let size = spoilerTextNode.updateLayout(constrainedSize)
spoilerTextNode.frame = CGRect(origin: .zero, size: size)
if spoilerTextNode.view.superview == nil {

View File

@ -60,6 +60,7 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
}
public let content: AnyComponent<ChildEnvironmentType>
public let headerContent: AnyComponent<Empty>?
public let backgroundColor: BackgroundColor
public let followContentSizeChanges: Bool
public let clipsContent: Bool
@ -73,6 +74,7 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
public init(
content: AnyComponent<ChildEnvironmentType>,
headerContent: AnyComponent<Empty>? = nil,
backgroundColor: BackgroundColor,
followContentSizeChanges: Bool = false,
clipsContent: Bool = false,
@ -85,6 +87,7 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
willDismiss: @escaping () -> Void = {}
) {
self.content = content
self.headerContent = headerContent
self.backgroundColor = backgroundColor
self.followContentSizeChanges = followContentSizeChanges
self.clipsContent = clipsContent
@ -101,6 +104,9 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
if lhs.content != rhs.content {
return false
}
if lhs.headerContent != rhs.headerContent {
return false
}
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
@ -159,6 +165,7 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
private let backgroundView: UIView
private var effectView: UIVisualEffectView?
private let contentView: ComponentView<ChildEnvironmentType>
private var headerView: ComponentView<Empty>?
private var isAnimatingOut: Bool = false
private var previousIsDisplaying: Bool = false
@ -272,10 +279,12 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let headerView = self.headerView?.view, headerView.bounds.contains(self.convert(point, to: headerView)) {
return super.hitTest(point, with: event)
}
if !self.backgroundView.bounds.contains(self.convert(point, to: self.backgroundView)) {
return self.dimView
}
return super.hitTest(point, with: event)
}
@ -288,6 +297,10 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
transition.animateView(allowUserInteraction: true, {
self.scrollView.center = targetPosition
})
if let headerContent = self.headerView {
headerContent.view?.layer.animateAlpha(from: 0.1, to: 0.0, duration: 0.15)
}
}
private func animateOut(initialVelocity: CGFloat? = nil, completion: @escaping () -> Void) {
@ -300,6 +313,10 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
self.isUserInteractionEnabled = false
self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
if let headerContent = self.headerView {
headerContent.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
guard let contentView = self.contentView.view else {
return
}
@ -404,6 +421,33 @@ public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: C
}
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize), completion: nil)
if let headerContent = component.headerContent {
let headerView: ComponentView<Empty>
if let current = self.headerView {
headerView = current
} else {
headerView = ComponentView()
self.headerView = headerView
}
let headerSize = headerView.update(
transition: transition,
component: headerContent,
environment: {},
containerSize: CGSize(width: contentSize.width, height: 44.0)
)
let headerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - headerSize.width) / 2.0), y: self.backgroundView.frame.minY - headerSize.height - 10.0), size: headerSize)
if let headerView = headerView.view {
if headerView.superview == nil {
self.scrollView.addSubview(headerView)
}
transition.setFrame(view: headerView, frame: headerFrame)
}
} else if let headerView = self.headerView {
self.headerView = nil
headerView.view?.removeFromSuperview()
}
let previousContentSize = self.scrollView.contentSize
let updateContentSize = {
self.scrollView.contentSize = contentSize

View File

@ -140,36 +140,55 @@ public extension ContainerViewLayout {
return false
}
var deviceOrientationSize: CGSize {
let screenSize = self.deviceMetrics.screenSize
return self.actualOrientation == .landscape ? CGSize(width: screenSize.height, height: screenSize.width) : screenSize
}
var inSplitView: Bool {
var maybeSplitView = false
if case .tablet = self.deviceMetrics.type {
if case .compact = self.metrics.widthClass {
maybeSplitView = true
}
if case .compact = self.metrics.heightClass {
maybeSplitView = true
}
guard case .tablet = self.deviceMetrics.type else {
return false
}
if maybeSplitView && abs(max(self.size.width, self.size.height) - self.deviceMetrics.screenSize.height) < 1.0 {
guard self.metrics.widthClass == .compact || self.metrics.heightClass == .compact else {
return false
}
let orient = self.deviceOrientationSize
guard abs(self.size.height - orient.height) < 1.0 else {
return false
}
let ratio = self.size.width / max(orient.width, 1.0)
let tol: CGFloat = 0.04
let isSplitFraction = abs(ratio - 0.5) < tol || abs(ratio - (1.0/3.0)) < tol || abs(ratio - (2.0/3.0)) < tol
return isSplitFraction
}
var inSlideOver: Bool {
guard case .tablet = self.deviceMetrics.type else {
return false
}
guard self.metrics.widthClass == .compact || self.metrics.heightClass == .compact else {
return false
}
let currentLong = max(self.size.width, self.size.height)
let screenLong = max(self.deviceMetrics.screenSize.width, self.deviceMetrics.screenSize.height)
if abs(currentLong - screenLong) > 10.0 {
return true
}
return false
}
var inSlideOver: Bool {
var maybeSlideOver = false
if case .tablet = self.deviceMetrics.type {
if case .compact = self.metrics.widthClass {
maybeSlideOver = true
}
if case .compact = self.metrics.heightClass {
maybeSlideOver = true
}
}
if maybeSlideOver && abs(max(self.size.width, self.size.height) - self.deviceMetrics.screenSize.height) > 10.0 {
return true
}
return false
var actualOrientation: LayoutOrientation {
let screenPortraitHeight = max(self.deviceMetrics.screenSize.width, self.deviceMetrics.screenSize.height)
let screenPortraitWidth = min(self.deviceMetrics.screenSize.width, self.deviceMetrics.screenSize.height)
let deltaPortrait = abs(self.size.height - screenPortraitHeight)
let deltaLandscape = abs(self.size.height - screenPortraitWidth)
return deltaLandscape < deltaPortrait ? .landscape : .portrait
}
var orientation: LayoutOrientation {

View File

@ -257,7 +257,7 @@ public final class MediaStreamComponent: CombinedComponent {
let local = Local()
return { context in
_body(context, local) // { context in
_body(context, local)
}
}
@ -1052,10 +1052,10 @@ public final class MediaStreamComponentController: ViewControllerComponentContai
view.expandFromPictureInPicture()
}
self.view.clipsToBounds = true
self.view.layer.animatePosition(from: CGPoint(x: self.view.frame.center.x, y: self.view.bounds.maxY + self.view.bounds.height / 2), to: self.view.center, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
})
self.view.clipsToBounds = true
self.view.layer.animatePosition(from: CGPoint(x: self.view.frame.center.x, y: self.view.bounds.maxY + self.view.bounds.height / 2), to: self.view.center, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
})
self.view.layer.allowsGroupOpacity = true
@ -1081,8 +1081,7 @@ public final class MediaStreamComponentController: ViewControllerComponentContai
override public func viewDidLoad() {
super.viewDidLoad()
// TODO: replace with actual color
backgroundDimView.backgroundColor = .black.withAlphaComponent(0.3)
self.backgroundDimView.backgroundColor = .black.withAlphaComponent(0.3)
self.view.clipsToBounds = false
}
@ -1094,7 +1093,7 @@ public final class MediaStreamComponentController: ViewControllerComponentContai
override public func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let dimViewSide: CGFloat = max(view.bounds.width, view.bounds.height)
backgroundDimView.frame = .init(x: view.bounds.midX - dimViewSide / 2, y: -view.bounds.height * 3, width: dimViewSide, height: view.bounds.height * 4)
self.backgroundDimView.frame = .init(x: view.bounds.midX - dimViewSide / 2, y: -view.bounds.height * 3, width: dimViewSide, height: view.bounds.height * 4)
}
public func dismiss(closing: Bool, manual: Bool) {

View File

@ -580,7 +580,9 @@ final class MediaStreamVideoComponent: Component {
videoView.isHidden = true
}
} else {
videoSize = CGSize(width: 16 / 9 * 100.0, height: 100.0).aspectFitted(.init(width: availableSize.width - videoInset * 2, height: availableSize.height))
let availableVideoWidth = min(availableSize.width, availableSize.height) - videoInset * 2
let availableVideoHeight = availableVideoWidth * 9.0 / 16
videoSize = CGSize(width: 16 / 9 * 100.0, height: 100.0).aspectFitted(.init(width: availableVideoWidth, height: availableVideoHeight))
}
let loadingBlurViewFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - videoSize.width) / 2.0), y: floor((availableSize.height - videoSize.height) / 2.0)), size: videoSize)

View File

@ -196,11 +196,11 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
return nil
}
return TelegramMediaAction(action: .starGift(gift: gift, convertStars: convertStars, text: text, entities: entities, nameHidden: (flags & (1 << 0)) != 0, savedToProfile: (flags & (1 << 2)) != 0, converted: (flags & (1 << 3)) != 0, upgraded: (flags & (1 << 5)) != 0, canUpgrade: (flags & (1 << 10)) != 0, upgradeStars: upgradeStars, isRefunded: (flags & (1 << 9)) != 0, isPrepaidUpgrade: (flags & (1 << 13)) != 0, upgradeMessageId: upgradeMessageId, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId, prepaidUpgradeHash: prepaidUpgradeHash, giftMessageId: giftMessageId, upgradeSeparate: (flags & (1 << 16)) != 0))
case let .messageActionStarGiftUnique(flags, apiGift, canExportAt, transferStars, fromId, peer, savedId, resaleAmount, canTransferDate, canResaleDate, _):
case let .messageActionStarGiftUnique(flags, apiGift, canExportAt, transferStars, fromId, peer, savedId, resaleAmount, canTransferDate, canResaleDate, dropOriginalDetailsStars):
guard let gift = StarGift(apiStarGift: apiGift) else {
return nil
}
return TelegramMediaAction(action: .starGiftUnique(gift: gift, isUpgrade: (flags & (1 << 0)) != 0, isTransferred: (flags & (1 << 1)) != 0, savedToProfile: (flags & (1 << 2)) != 0, canExportDate: canExportAt, transferStars: transferStars, isRefunded: (flags & (1 << 5)) != 0, isPrepaidUpgrade: (flags & (1 << 11)) != 0, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId, resaleAmount: resaleAmount.flatMap { CurrencyAmount(apiAmount: $0) }, canTransferDate: canTransferDate, canResaleDate: canResaleDate))
return TelegramMediaAction(action: .starGiftUnique(gift: gift, isUpgrade: (flags & (1 << 0)) != 0, isTransferred: (flags & (1 << 1)) != 0, savedToProfile: (flags & (1 << 2)) != 0, canExportDate: canExportAt, transferStars: transferStars, isRefunded: (flags & (1 << 5)) != 0, isPrepaidUpgrade: (flags & (1 << 11)) != 0, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId, resaleAmount: resaleAmount.flatMap { CurrencyAmount(apiAmount: $0) }, canTransferDate: canTransferDate, canResaleDate: canResaleDate, dropOriginalDetailsStars: dropOriginalDetailsStars))
case let .messageActionPaidMessagesRefunded(count, stars):
return TelegramMediaAction(action: .paidMessagesRefunded(count: count, stars: stars))
case let .messageActionPaidMessagesPrice(flags, stars):

View File

@ -244,7 +244,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case giftStars(currency: String, amount: Int64, count: Int64, cryptoCurrency: String?, cryptoAmount: Int64?, transactionId: String?)
case prizeStars(amount: Int64, isUnclaimed: Bool, boostPeerId: PeerId?, transactionId: String?, giveawayMessageId: MessageId?)
case starGift(gift: StarGift, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, isRefunded: Bool, isPrepaidUpgrade: Bool, upgradeMessageId: Int32?, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, prepaidUpgradeHash: String?, giftMessageId: Int32?, upgradeSeparate: Bool)
case starGiftUnique(gift: StarGift, isUpgrade: Bool, isTransferred: Bool, savedToProfile: Bool, canExportDate: Int32?, transferStars: Int64?, isRefunded: Bool, isPrepaidUpgrade: Bool, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, resaleAmount: CurrencyAmount?, canTransferDate: Int32?, canResaleDate: Int32?)
case starGiftUnique(gift: StarGift, isUpgrade: Bool, isTransferred: Bool, savedToProfile: Bool, canExportDate: Int32?, transferStars: Int64?, isRefunded: Bool, isPrepaidUpgrade: Bool, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, resaleAmount: CurrencyAmount?, canTransferDate: Int32?, canResaleDate: Int32?, dropOriginalDetailsStars: Int64?)
case paidMessagesRefunded(count: Int32, stars: Int64)
case paidMessagesPriceEdited(stars: Int64, broadcastMessagesAllowed: Bool)
case conferenceCall(ConferenceCall)
@ -389,7 +389,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
} else if let stars = decoder.decodeOptionalInt64ForKey("resaleStars") {
resaleAmount = CurrencyAmount(amount: StarsAmount(value: stars, nanos: 0), currency: .stars)
}
self = .starGiftUnique(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, isUpgrade: decoder.decodeBoolForKey("isUpgrade", orElse: false), isTransferred: decoder.decodeBoolForKey("isTransferred", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), canExportDate: decoder.decodeOptionalInt32ForKey("canExportDate"), transferStars: decoder.decodeOptionalInt64ForKey("transferStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), isPrepaidUpgrade: decoder.decodeBoolForKey("isPrepaidUpgrade", orElse: false), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId"), resaleAmount: resaleAmount, canTransferDate: decoder.decodeOptionalInt32ForKey("canTransferDate"), canResaleDate: decoder.decodeOptionalInt32ForKey("canResaleDate"))
self = .starGiftUnique(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, isUpgrade: decoder.decodeBoolForKey("isUpgrade", orElse: false), isTransferred: decoder.decodeBoolForKey("isTransferred", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), canExportDate: decoder.decodeOptionalInt32ForKey("canExportDate"), transferStars: decoder.decodeOptionalInt64ForKey("transferStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), isPrepaidUpgrade: decoder.decodeBoolForKey("isPrepaidUpgrade", orElse: false), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId"), resaleAmount: resaleAmount, canTransferDate: decoder.decodeOptionalInt32ForKey("canTransferDate"), canResaleDate: decoder.decodeOptionalInt32ForKey("canResaleDate"), dropOriginalDetailsStars: decoder.decodeOptionalInt64ForKey("dropOriginalDetailsStars"))
case 46:
self = .paidMessagesRefunded(count: decoder.decodeInt32ForKey("count", orElse: 0), stars: decoder.decodeInt64ForKey("stars", orElse: 0))
case 47:
@ -771,7 +771,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
encoder.encodeNil(forKey: "giftMessageId")
}
encoder.encodeBool(upgradeSeparate, forKey: "upgradeSeparate")
case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate):
case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, dropOriginalDetailsStars):
encoder.encodeInt32(45, forKey: "_rawValue")
encoder.encodeObject(gift, forKey: "gift")
encoder.encodeBool(isUpgrade, forKey: "isUpgrade")
@ -819,6 +819,11 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
} else {
encoder.encodeNil(forKey: "canResaleDate")
}
if let dropOriginalDetailsStars {
encoder.encodeInt64(dropOriginalDetailsStars, forKey: "dropOriginalDetailsStars")
} else {
encoder.encodeNil(forKey: "dropOriginalDetailsStars")
}
case let .paidMessagesRefunded(count, stars):
encoder.encodeInt32(46, forKey: "_rawValue")
encoder.encodeInt32(count, forKey: "count")
@ -908,7 +913,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
peerIds.append(releasedBy)
}
return peerIds
case let .starGiftUnique(gift, _, _, _, _, _, _, _, peerId, senderId, _, _, _, _):
case let .starGiftUnique(gift, _, _, _, _, _, _, _, peerId, senderId, _, _, _, _, _):
var peerIds: [PeerId] = []
if let peerId {
peerIds.append(peerId)

View File

@ -19,6 +19,7 @@ public enum BotPaymentInvoiceSource {
case premiumGift(peerId: EnginePeer.Id, option: CachedPremiumGiftOption, text: String?, entities: [MessageTextEntity]?)
case starGiftResale(slug: String, toPeerId: EnginePeer.Id, ton: Bool)
case starGiftPrepaidUpgrade(peerId: EnginePeer.Id, hash: String)
case starGiftDropOriginalDetails(reference: StarGiftReference)
}
public struct BotPaymentInvoiceFields: OptionSet {
@ -420,6 +421,8 @@ func _internal_parseInputInvoice(transaction: Transaction, source: BotPaymentInv
return nil
}
return .inputInvoiceStarGiftPrepaidUpgrade(peer: inputPeer, hash: hash)
case let .starGiftDropOriginalDetails(reference):
return reference.apiStarGiftReference(transaction: transaction).flatMap { .inputInvoiceStarGiftDropOriginalDetails(stargift: $0) }
}
}
@ -761,7 +764,7 @@ func _internal_sendBotPaymentForm(account: Account, formId: Int64, source: BotPa
receiptMessageId = id
}
}
case .giftCode, .stars, .starsGift, .starsChatSubscription, .starGift, .starGiftUpgrade, .starGiftTransfer, .premiumGift, .starGiftResale, .starGiftPrepaidUpgrade:
case .giftCode, .stars, .starsGift, .starsChatSubscription, .starGift, .starGiftUpgrade, .starGiftTransfer, .premiumGift, .starGiftResale, .starGiftPrepaidUpgrade, .starGiftDropOriginalDetails:
receiptMessageId = nil
}
}

View File

@ -830,6 +830,27 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
themePeerId: themePeerId
)
}
public func withAttributes(_ attributes: [Attribute]) -> UniqueGift {
return UniqueGift(
id: self.id,
giftId: self.giftId,
title: self.title,
number: self.number,
slug: self.slug,
owner: self.owner,
attributes: attributes,
availability: self.availability,
giftAddress: self.giftAddress,
resellAmounts: self.resellAmounts,
resellForTonOnly: self.resellForTonOnly,
releasedBy: self.releasedBy,
valueAmount: self.valueAmount,
valueCurrency: self.valueCurrency,
flags: self.flags,
themePeerId: self.themePeerId
)
}
}
public enum DecodingError: Error {
@ -1135,6 +1156,25 @@ func _internal_buyStarGift(account: Account, slug: String, peerId: EnginePeer.Id
}
}
public enum DropStarGiftOriginalDetailsError {
case generic
}
func _internal_dropStarGiftOriginalDetails(account: Account, reference: StarGiftReference) -> Signal<Never, DropStarGiftOriginalDetailsError> {
let source: BotPaymentInvoiceSource = .starGiftDropOriginalDetails(reference: reference)
return _internal_fetchBotPaymentForm(accountPeerId: account.peerId, postbox: account.postbox, network: account.network, source: source, themeParams: nil)
|> `catch` { error -> Signal<BotPaymentForm, DropStarGiftOriginalDetailsError> in
return .fail(.generic)
}
|> mapToSignal { paymentForm in
return _internal_sendStarsPaymentForm(account: account, formId: paymentForm.id, source: source)
|> mapError { _ -> DropStarGiftOriginalDetailsError in
return .generic
}
|> ignoreValues
}
}
func _internal_transferStarGift(account: Account, prepaid: Bool, reference: StarGiftReference, peerId: EnginePeer.Id) -> Signal<Never, TransferStarGiftError> {
return account.postbox.transaction { transaction -> (Api.InputPeer, Api.InputSavedStarGift)? in
guard let inputPeer = transaction.getPeer(peerId).flatMap(apiInputPeer), let starGift = reference.apiStarGiftReference(transaction: transaction) else {
@ -1225,7 +1265,7 @@ func _internal_upgradeStarGift(account: Account, formId: Int64?, reference: Star
case let .updateNewMessage(message, _, _):
if let message = StoreMessage(apiMessage: message, accountPeerId: account.peerId, peerIsForum: false) {
for media in message.media {
if let action = media as? TelegramMediaAction, case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, _, peerId, _, savedId, _, canTransferDate, canResaleDate) = action.action, case let .Id(messageId) = message.id {
if let action = media as? TelegramMediaAction, case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, _, peerId, _, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars) = action.action, case let .Id(messageId) = message.id {
let reference: StarGiftReference
if let peerId, let savedId {
reference = .peer(peerId: peerId, id: savedId)
@ -1251,7 +1291,8 @@ func _internal_upgradeStarGift(account: Account, formId: Int64?, reference: Star
canResaleDate: canResaleDate,
collectionIds: nil,
prepaidUpgradeHash: nil,
upgradeSeparate: false
upgradeSeparate: false,
dropOriginalDetailsStars: dropOriginalDetailsStars
))
}
}
@ -1790,6 +1831,21 @@ private final class ProfileGiftsContextImpl {
})
)
}
public func dropOriginalDetails(reference: StarGiftReference) -> Signal<Never, DropStarGiftOriginalDetailsError> {
if let index = self.gifts.firstIndex(where: { $0.reference == reference }), case let .unique(uniqueGift) = self.gifts[index].gift {
let updatedUniqueGift = uniqueGift.withAttributes(uniqueGift.attributes.filter { $0.attributeType != .originalInfo })
self.gifts[index] = self.gifts[index].withGift(.unique(updatedUniqueGift))
}
if let index = self.filteredGifts.firstIndex(where: { $0.reference == reference }), case let .unique(uniqueGift) = self.filteredGifts[index].gift {
let updatedUniqueGift = uniqueGift.withAttributes(uniqueGift.attributes.filter { $0.attributeType != .originalInfo })
self.filteredGifts[index] = self.filteredGifts[index].withGift(.unique(updatedUniqueGift))
}
self.pushState()
return _internal_dropStarGiftOriginalDetails(account: self.account, reference: reference)
}
func convertStarGift(reference: StarGiftReference) {
self.actionDisposable.set(
@ -2164,6 +2220,7 @@ public final class ProfileGiftsContext {
case collectionIds
case prepaidUpgradeHash
case upgradeSeparate
case dropOriginalDetailsStars
}
public let gift: TelegramCore.StarGift
@ -2185,6 +2242,7 @@ public final class ProfileGiftsContext {
public let collectionIds: [Int32]?
public let prepaidUpgradeHash: String?
public let upgradeSeparate: Bool
public let dropOriginalDetailsStars: Int64?
fileprivate let _fromPeerId: EnginePeer.Id?
@ -2211,7 +2269,8 @@ public final class ProfileGiftsContext {
canResaleDate: Int32?,
collectionIds: [Int32]?,
prepaidUpgradeHash: String?,
upgradeSeparate: Bool
upgradeSeparate: Bool,
dropOriginalDetailsStars: Int64?
) {
self.gift = gift
self.reference = reference
@ -2233,6 +2292,7 @@ public final class ProfileGiftsContext {
self.collectionIds = collectionIds
self.prepaidUpgradeHash = prepaidUpgradeHash
self.upgradeSeparate = upgradeSeparate
self.dropOriginalDetailsStars = dropOriginalDetailsStars
}
public init(from decoder: Decoder) throws {
@ -2264,6 +2324,7 @@ public final class ProfileGiftsContext {
self.collectionIds = try container.decodeIfPresent([Int32].self, forKey: .collectionIds)
self.prepaidUpgradeHash = try container.decodeIfPresent(String.self, forKey: .prepaidUpgradeHash)
self.upgradeSeparate = try container.decodeIfPresent(Bool.self, forKey: .upgradeSeparate) ?? false
self.dropOriginalDetailsStars = try container.decodeIfPresent(Int64.self, forKey: .dropOriginalDetailsStars)
}
public func encode(to encoder: Encoder) throws {
@ -2288,6 +2349,7 @@ public final class ProfileGiftsContext {
try container.encodeIfPresent(self.collectionIds, forKey: .collectionIds)
try container.encodeIfPresent(self.prepaidUpgradeHash, forKey: .prepaidUpgradeHash)
try container.encode(self.upgradeSeparate, forKey: .upgradeSeparate)
try container.encodeIfPresent(self.dropOriginalDetailsStars, forKey: .dropOriginalDetailsStars)
}
public func withGift(_ gift: TelegramCore.StarGift) -> StarGift {
@ -2310,7 +2372,8 @@ public final class ProfileGiftsContext {
canResaleDate: self.canResaleDate,
collectionIds: self.collectionIds,
prepaidUpgradeHash: self.prepaidUpgradeHash,
upgradeSeparate: self.upgradeSeparate
upgradeSeparate: self.upgradeSeparate,
dropOriginalDetailsStars: self.dropOriginalDetailsStars
)
}
@ -2334,7 +2397,8 @@ public final class ProfileGiftsContext {
canResaleDate: self.canResaleDate,
collectionIds: self.collectionIds,
prepaidUpgradeHash: self.prepaidUpgradeHash,
upgradeSeparate: self.upgradeSeparate
upgradeSeparate: self.upgradeSeparate,
dropOriginalDetailsStars: self.dropOriginalDetailsStars
)
}
@ -2358,7 +2422,8 @@ public final class ProfileGiftsContext {
canResaleDate: self.canResaleDate,
collectionIds: self.collectionIds,
prepaidUpgradeHash: self.prepaidUpgradeHash,
upgradeSeparate: self.upgradeSeparate
upgradeSeparate: self.upgradeSeparate,
dropOriginalDetailsStars: self.dropOriginalDetailsStars
)
}
fileprivate func withFromPeer(_ fromPeer: EnginePeer?) -> StarGift {
@ -2381,7 +2446,8 @@ public final class ProfileGiftsContext {
canResaleDate: self.canResaleDate,
collectionIds: self.collectionIds,
prepaidUpgradeHash: self.prepaidUpgradeHash,
upgradeSeparate: self.upgradeSeparate
upgradeSeparate: self.upgradeSeparate,
dropOriginalDetailsStars: self.dropOriginalDetailsStars
)
}
@ -2405,7 +2471,8 @@ public final class ProfileGiftsContext {
canResaleDate: self.canResaleDate,
collectionIds: collectionIds,
prepaidUpgradeHash: self.prepaidUpgradeHash,
upgradeSeparate: self.upgradeSeparate
upgradeSeparate: self.upgradeSeparate,
dropOriginalDetailsStars: self.dropOriginalDetailsStars
)
}
}
@ -2491,6 +2558,20 @@ public final class ProfileGiftsContext {
}
}
public func dropOriginalDetails(reference: StarGiftReference) -> Signal<Never, DropStarGiftOriginalDetailsError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.dropOriginalDetails(reference: reference).start(error: { error in
subscriber.putError(error)
}, completed: {
subscriber.putCompletion()
}))
}
return disposable
}
}
public func convertStarGift(reference: StarGiftReference) {
self.impl.with { impl in
impl.convertStarGift(reference: reference)
@ -2609,7 +2690,7 @@ public final class ProfileGiftsContext {
extension ProfileGiftsContext.State.StarGift {
init?(apiSavedStarGift: Api.SavedStarGift, peerId: EnginePeer.Id, transaction: Transaction) {
switch apiSavedStarGift {
case let .savedStarGift(flags, fromId, date, apiGift, message, msgId, savedId, convertStars, upgradeStars, canExportDate, transferStars, canTransferAt, canResaleAt, collectionIds, prepaidUpgradeHash, _):
case let .savedStarGift(flags, fromId, date, apiGift, message, msgId, savedId, convertStars, upgradeStars, canExportDate, transferStars, canTransferAt, canResaleAt, collectionIds, prepaidUpgradeHash, dropOriginalDetailsStars):
guard let gift = StarGift(apiStarGift: apiGift) else {
return nil
}
@ -2658,6 +2739,7 @@ extension ProfileGiftsContext.State.StarGift {
self.collectionIds = collectionIds
self.prepaidUpgradeHash = prepaidUpgradeHash
self.upgradeSeparate = (flags & (1 << 17)) != 0
self.dropOriginalDetailsStars = dropOriginalDetailsStars
}
}
}

View File

@ -1626,10 +1626,10 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot
receiptMessageId = id
}
}
case .giftCode, .stars, .starsGift, .starsChatSubscription, .starGift, .starGiftUpgrade, .starGiftTransfer, .premiumGift, .starGiftResale, .starGiftPrepaidUpgrade:
case .giftCode, .stars, .starsGift, .starsChatSubscription, .starGift, .starGiftUpgrade, .starGiftTransfer, .premiumGift, .starGiftResale, .starGiftPrepaidUpgrade, .starGiftDropOriginalDetails:
receiptMessageId = nil
}
} else if case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, _, peerId, _, savedId, _, canTransferDate, canResaleDate) = action.action, case let .Id(messageId) = message.id {
} else if case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, _, peerId, _, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars) = action.action, case let .Id(messageId) = message.id {
let reference: StarGiftReference
if let peerId, let savedId {
reference = .peer(peerId: peerId, id: savedId)
@ -1655,7 +1655,8 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot
canResaleDate: canResaleDate,
collectionIds: nil,
prepaidUpgradeHash: nil,
upgradeSeparate: false
upgradeSeparate: false,
dropOriginalDetailsStars: dropOriginalDetailsStars
)
}
}

View File

@ -125,6 +125,10 @@ public extension TelegramEngine {
return _internal_updateStarGiftAddedToProfile(account: self.account, reference: reference, added: added)
}
public func dropStarGiftOriginalDetails(reference: StarGiftReference) -> Signal<Never, DropStarGiftOriginalDetailsError> {
return _internal_dropStarGiftOriginalDetails(account: self.account, reference: reference)
}
public func transferStarGift(prepaid: Bool, reference: StarGiftReference, peerId: EnginePeer.Id) -> Signal<Never, TransferStarGiftError> {
return _internal_transferStarGift(account: self.account, prepaid: prepaid, reference: reference, peerId: peerId)
}

View File

@ -671,3 +671,14 @@ public func stringForIntervalSinceUpdateAction(strings: PresentationStrings, val
return strings.Chat_NonContactUser_UpdatedDays(Int32(round(Float(value) / (24 * 60 * 60))))
}
}
public func stringForGiftUpgradeTimestamp(strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, timestamp: Int32) -> String {
var t: time_t = time_t(timestamp)
var timeinfo: tm = tm()
localtime_r(&t, &timeinfo)
let time = stringForShortTimestamp(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, dateTimeFormat: dateTimeFormat)
let date = strings.Date_ChatDateHeader(monthAtIndex(Int(timeinfo.tm_mon), strings: strings), "\(timeinfo.tm_mday)").string
return "\(time), \(date)"
}

View File

@ -1257,7 +1257,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Sent(authorName, starsPrice)._tuple, body: bodyAttributes, argumentAttributes: attributes)
}
}
case let .starGiftUnique(gift, isUpgrade, _, _, _, _, _, isPrepaidUpgrade, peerId, senderId, _, resaleStars, _, _):
case let .starGiftUnique(gift, isUpgrade, _, _, _, _, _, isPrepaidUpgrade, peerId, senderId, _, resaleStars, _, _, _):
if case let .unique(gift) = gift {
if !forAdditionalServiceMessage && !"".isEmpty {
attributedString = NSAttributedString(string: "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))", font: titleFont, textColor: primaryTextColor)

View File

@ -278,7 +278,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
switch action.action {
case let .starGift(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
releasedBy = gift.releasedBy
case let .starGiftUnique(gift, _, _, _, _, _, _, _, _, _, _, _, _, _):
case let .starGiftUnique(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
releasedBy = gift.releasedBy
default:
break
@ -558,7 +558,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
isStarGift = true
var authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
let isSelfGift = item.message.id.peerId == item.context.account.peerId
let isChannelGift = item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel || channelPeerId != nil
if isSelfGift {
@ -569,8 +569,12 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
} else {
if let senderPeerId, let name = item.message.peers[senderPeerId].flatMap(EnginePeer.init)?.compactDisplayTitle {
authorName = name
title = item.presentationData.strings.Notification_StarGift_Title(authorName).string
} else if !incoming, let name = item.message.peers[item.message.id.peerId].flatMap(EnginePeer.init)?.compactDisplayTitle {
title = item.presentationData.strings.Notification_StarGift_TitleTo(name).string
} else {
title = item.presentationData.strings.Gift_View_Unknown_Title
}
title = item.presentationData.strings.Notification_StarGift_Title(authorName).string
}
}
if let giftText, !giftText.isEmpty {
@ -647,10 +651,14 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
buttonTitle = item.presentationData.strings.Notification_StarGift_Unpack
buttonIcon = "GiftUnpack"
} else {
buttonTitle = item.presentationData.strings.Notification_StarGift_View
if isPrepaidUpgrade && !incoming {
buttonTitle = ""
} else {
buttonTitle = item.presentationData.strings.Notification_StarGift_View
}
}
}
case let .starGiftUnique(gift, isUpgrade, _, _, _, _, isRefunded, _, _, _, _, _, _, _):
case let .starGiftUnique(gift, isUpgrade, _, _, _, _, isRefunded, _, _, _, _, _, _, _, _):
if case let .unique(uniqueGift) = gift {
isStarGift = true

View File

@ -439,6 +439,7 @@ public class ChatMessagePaymentAlertController: AlertController {
starsContext: starsContext,
options: options,
purpose: .generic,
targetPeerId: nil,
completion: { _ in }
)
navigationController.pushViewController(controller)

View File

@ -1420,11 +1420,17 @@ private final class ChatSendStarsScreenComponent: Component {
theme: environment.theme,
currency: .stars,
action: { [weak self] in
guard let self, let starsContext = context.starsContext, let navigationController = self.environment?.controller()?.navigationController as? NavigationController else {
guard let self, let component = self.component, let starsContext = context.starsContext, let navigationController = self.environment?.controller()?.navigationController as? NavigationController else {
return
}
self.environment?.controller()?.dismiss()
let targetPeerId: EnginePeer.Id
switch component.initialData.subjectInitialData {
case let .react(reactData):
targetPeerId = reactData.peer.id
}
let _ = (context.engine.payments.starsTopUpOptions()
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { options in
@ -1433,6 +1439,7 @@ private final class ChatSendStarsScreenComponent: Component {
starsContext: starsContext,
options: options,
purpose: .generic,
targetPeerId: targetPeerId,
completion: { _ in }
)
navigationController.pushViewController(controller)
@ -2295,7 +2302,7 @@ private final class ChatSendStarsScreenComponent: Component {
purchasePurpose = .reactions(peerId: reactData.peer.id, requiredStars: Int64(self.amount.realValue))
}
let purchaseScreen = component.context.sharedContext.makeStarsPurchaseScreen(context: component.context, starsContext: starsContext, options: options, purpose: purchasePurpose, completion: { result in
let purchaseScreen = component.context.sharedContext.makeStarsPurchaseScreen(context: component.context, starsContext: starsContext, options: options, purpose: purchasePurpose, targetPeerId: nil, completion: { result in
let _ = result
//TODO:release
})

View File

@ -41,6 +41,7 @@ public final class EmojiStatusComponent: Component {
public enum SizeType {
case compact
case large
case smaller
}
public enum Content: Equatable {
@ -357,7 +358,7 @@ public final class EmojiStatusComponent: Component {
case let .verified(fillColor, foregroundColor, sizeType):
let imageNamePrefix: String
switch sizeType {
case .compact:
case .compact, .smaller:
imageNamePrefix = "Chat List/PeerVerifiedIcon"
case .large:
imageNamePrefix = "Peer Info/VerifiedIcon"

View File

@ -506,14 +506,26 @@ final class GiftOptionsScreenComponent: Component {
}
isSoldOut = true
} else if let _ = gift.availability {
let text: String
if let perUserLimit = gift.perUserLimit {
text = environment.strings.Gift_Options_Gift_Limited_Left(perUserLimit.remains)
} else {
text = environment.strings.Gift_Options_Gift_Limited
}
ribbon = GiftItemComponent.Ribbon(
text: environment.strings.Gift_Options_Gift_Limited,
text: text,
color: .blue
)
}
if !isSoldOut && gift.flags.contains(.requiresPremium) {
let text: String
if component.context.isPremium, let perUserLimit = gift.perUserLimit {
text = environment.strings.Gift_Options_Gift_Premium_Left(perUserLimit.remains)
} else {
text = environment.strings.Gift_Options_Gift_Premium
}
ribbon = GiftItemComponent.Ribbon(
text: environment.strings.Gift_Options_Gift_Premium,
text: text,
color: .orange
)
outline = .orange
@ -837,6 +849,7 @@ final class GiftOptionsScreenComponent: Component {
starsContext: starsContext,
options: options ?? [],
purpose: .transferStarGift(requiredStars: transferStars),
targetPeerId: nil,
completion: { stars in
starsContext.add(balance: StarsAmount(value: stars, nanos: 0))
proceed(true)

View File

@ -457,6 +457,34 @@ final class GiftSetupScreenComponent: Component {
controllers.append(chatController)
}
navigationController.setViewControllers(controllers, animated: true)
if case let .starGift(starGift, _) = component.subject, let perUserLimit = starGift.perUserLimit {
Queue.mainQueue().after(0.5) {
let remains = max(0, perUserLimit.remains - 1)
let text: String
if remains == 0 {
text = presentationData.strings.Gift_Send_Limited_Success_Text_None
} else {
text = presentationData.strings.Gift_Send_Limited_Success_Text(remains)
}
let tooltipController = UndoOverlayController(
presentationData: presentationData,
content: .sticker(
context: context,
file: starGift.file,
loop: true,
title: presentationData.strings.Gift_Send_Limited_Success_Title,
text: text,
undoText: nil,
customAction: nil
),
position: .top,
action: { _ in return true }
)
(navigationController.viewControllers.last as? ViewController)?.present(tooltipController, in: .current)
}
}
}
if let completion {
@ -519,6 +547,7 @@ final class GiftSetupScreenComponent: Component {
starsContext: starsContext,
options: options ?? [],
purpose: .starGift(peerId: component.peerId, requiredStars: finalPrice),
targetPeerId: nil,
completion: { [weak self, weak starsContext] stars in
guard let self, let starsContext else {
return
@ -1130,7 +1159,7 @@ final class GiftSetupScreenComponent: Component {
|> filter { $0 != nil }
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { options in
let purchaseController = component.context.sharedContext.makeStarsPurchaseScreen(context: component.context, starsContext: starsContext, options: options ?? [], purpose: .generic, completion: { stars in
let purchaseController = component.context.sharedContext.makeStarsPurchaseScreen(context: component.context, starsContext: starsContext, options: options ?? [], purpose: .generic, targetPeerId: nil, completion: { stars in
starsContext.add(balance: StarsAmount(value: stars, nanos: 0))
})
controller.push(purchaseController)

View File

@ -55,6 +55,7 @@ swift_library(
"//submodules/TelegramUI/Components/Stars/BalanceNeededScreen",
"//submodules/TelegramUI/Components/ChatThemeScreen",
"//submodules/ImageBlur",
"//submodules/TelegramUI/Components/PeerInfo/ProfileLevelRatingBarComponent",
],
visibility = [
"//visibility:public",

View File

@ -241,7 +241,7 @@ private final class GiftRemoveInfoAlertContentNode: AlertContentNode {
)
),
environment: {},
containerSize: CGSize(width: contentWidth - 32.0, height: size.height)
containerSize: CGSize(width: contentWidth - 64.0, height: size.height)
)
let infoFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - infoSize.width) / 2.0), y: titleSize.height + textSize.height + 54.0), size: infoSize)
if let view = self.infoView.view {
@ -326,8 +326,8 @@ public func giftRemoveInfoAlertController(
var contentNode: GiftRemoveInfoAlertContentNode?
var dismissImpl: ((Bool) -> Void)?
let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: buttonText, action: { [weak contentNode] in
contentNode?.inProgress = true
let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: buttonText, action: {
dismissImpl?(true)
commit()
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?(true)

View File

@ -138,7 +138,7 @@ private final class GiftTransferAlertContentNode: AlertContentNode {
return ("URL", url)
}
), textAlignment: .center)
self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/AlertArrow"), color: theme.secondaryColor)
self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/AlertArrow"), color: theme.secondaryColor.withAlphaComponent(0.9))
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {

View File

@ -0,0 +1,682 @@
import Foundation
import UIKit
import SwiftSignalKit
import Display
import TelegramCore
import TelegramPresentationData
import ComponentFlow
import ComponentDisplayAdapters
import AccountContext
import ViewControllerComponent
import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import BalancedTextComponent
import ButtonComponent
import PresentationDataUtils
import LottieComponent
import ProfileLevelRatingBarComponent
import TextFormat
import TelegramStringFormatting
private final class GiftUpgradeCostScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let upgradePreview: StarGiftUpgradePreview
init(
context: AccountContext,
upgradePreview: StarGiftUpgradePreview
) {
self.context = context
self.upgradePreview = upgradePreview
}
static func ==(lhs: GiftUpgradeCostScreenComponent, rhs: GiftUpgradeCostScreenComponent) -> Bool {
return true
}
private final class ScrollView: UIScrollView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return super.hitTest(point, with: event)
}
}
private struct ItemLayout: Equatable {
var containerSize: CGSize
var containerInset: CGFloat
var bottomInset: CGFloat
var topInset: CGFloat
init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat) {
self.containerSize = containerSize
self.containerInset = containerInset
self.bottomInset = bottomInset
self.topInset = topInset
}
}
final class View: UIView, UIScrollViewDelegate {
private let dimView: UIView
private let backgroundLayer: SimpleLayer
private let navigationBarContainer: SparseContainerView
private let navigationBackgroundView: BlurredBackgroundView
private let navigationBarSeparator: SimpleLayer
private let scrollView: ScrollView
private let scrollContentClippingView: SparseContainerView
private let scrollContentView: UIView
private let closeButton = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private let descriptionText = ComponentView<Empty>()
private let bar = ComponentView<Empty>()
private let table = ComponentView<Empty>()
private let additionalDescription = ComponentView<Empty>()
private let bottomPanelContainer: UIView
private let bottomPanelSeparator: SimpleLayer
private let actionButton = ComponentView<Empty>()
private var isFirstTimeApplyingModalFactor: Bool = true
private var ignoreScrolling: Bool = false
private var component: GiftUpgradeCostScreenComponent?
private weak var state: EmptyComponentState?
private var environment: ViewControllerComponentContainer.Environment?
private var isUpdating: Bool = false
private var itemLayout: ItemLayout?
private var topOffsetDistance: CGFloat?
private var cachedCloseImage: UIImage?
private var upgradePreviewTimer: SwiftSignalKit.Timer?
private var effectiveUpgradePrice: StarGiftUpgradePreview.Price?
override init(frame: CGRect) {
self.dimView = UIView()
self.backgroundLayer = SimpleLayer()
self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
self.backgroundLayer.cornerRadius = 10.0
self.navigationBarContainer = SparseContainerView()
self.navigationBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
self.navigationBarSeparator = SimpleLayer()
self.scrollView = ScrollView()
self.scrollContentClippingView = SparseContainerView()
self.scrollContentClippingView.clipsToBounds = true
self.scrollContentView = UIView()
self.bottomPanelContainer = UIView()
self.bottomPanelSeparator = SimpleLayer()
super.init(frame: frame)
self.addSubview(self.dimView)
self.layer.addSublayer(self.backgroundLayer)
self.scrollView.delaysContentTouches = false
self.scrollView.canCancelContentTouches = true
self.scrollView.clipsToBounds = false
self.scrollView.contentInsetAdjustmentBehavior = .never
if #available(iOS 13.0, *) {
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
}
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.alwaysBounceHorizontal = false
self.scrollView.alwaysBounceVertical = true
self.scrollView.scrollsToTop = false
self.scrollView.delegate = self
self.scrollView.clipsToBounds = true
self.addSubview(self.scrollContentClippingView)
self.scrollContentClippingView.addSubview(self.scrollView)
self.scrollView.addSubview(self.scrollContentView)
self.addSubview(self.navigationBarContainer)
self.addSubview(self.bottomPanelContainer)
self.navigationBarContainer.addSubview(self.navigationBackgroundView)
self.navigationBarContainer.layer.addSublayer(self.navigationBarSeparator)
self.layer.addSublayer(self.bottomPanelSeparator)
self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
self.updateScrolling(transition: .immediate)
}
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.bounds.contains(point) {
return nil
}
if !self.backgroundLayer.frame.contains(point) {
return self.dimView
}
if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) {
return result
}
let result = super.hitTest(point, with: event)
return result
}
@objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
guard let environment = self.environment, let controller = environment.controller() else {
return
}
controller.dismiss()
}
}
func upgradePreviewTimerTick() {
guard let upgradePreview = self.component?.upgradePreview else {
return
}
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
if let currentPrice = self.effectiveUpgradePrice {
if let price = upgradePreview.nextPrices.reversed().first(where: { currentTime >= $0.date }) {
if price.stars != currentPrice.stars {
self.effectiveUpgradePrice = price
if !self.isUpdating {
self.state?.updated(transition: .immediate.withUserData(ProfileLevelRatingBarComponent.TransitionHint(animate: true)))
}
}
} else {
self.upgradePreviewTimer?.invalidate()
self.upgradePreviewTimer = nil
}
} else if let price = upgradePreview.nextPrices.reversed().first(where: { currentTime >= $0.date}) {
self.effectiveUpgradePrice = price
if !self.isUpdating {
self.state?.updated()
}
}
}
private func updateScrolling(transition: ComponentTransition) {
guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else {
return
}
var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset
let titleTransformFraction: CGFloat = max(0.0, min(1.0, -topOffset / 20.0))
let navigationAlpha: CGFloat = titleTransformFraction
transition.setAlpha(view: self.navigationBackgroundView, alpha: navigationAlpha)
transition.setAlpha(layer: self.navigationBarSeparator, alpha: navigationAlpha)
let bottomPanelAlphaDistance: CGFloat = 20.0
let bottomPanelDistance: CGFloat = self.scrollView.contentSize.height - self.scrollView.bounds.maxY
let bottomPanelAlphaFraction: CGFloat = max(0.0, min(1.0, bottomPanelDistance / bottomPanelAlphaDistance))
let bottomPanelAlpha: CGFloat = bottomPanelAlphaFraction
if self.bottomPanelSeparator.opacity != Float(bottomPanelAlpha) {
let alphaTransition = transition
alphaTransition.setAlpha(layer: self.bottomPanelSeparator, alpha: bottomPanelAlpha)
}
topOffset = max(0.0, topOffset)
transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0))
transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset))
let topOffsetDistance: CGFloat = 80.0
self.topOffsetDistance = topOffsetDistance
var topOffsetFraction = topOffset / topOffsetDistance
topOffsetFraction = max(0.0, min(1.0, topOffsetFraction))
let transitionFactor: CGFloat = 1.0 - topOffsetFraction
var modalOverlayTransition = transition
if self.isFirstTimeApplyingModalFactor {
self.isFirstTimeApplyingModalFactor = false
modalOverlayTransition = .spring(duration: 0.5)
}
if self.isUpdating {
DispatchQueue.main.async { [weak controller] in
guard let controller else {
return
}
controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: modalOverlayTransition.containedViewLayoutTransition)
}
} else {
controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: modalOverlayTransition.containedViewLayoutTransition)
}
}
func animateIn() {
self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY
self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.bottomPanelContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.bottomPanelSeparator.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
func animateOut(completion: @escaping () -> Void) {
let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY
self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in
completion()
})
self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
self.bottomPanelContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
self.bottomPanelSeparator.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
if let environment = self.environment, let controller = environment.controller() {
controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .easeInOut))
}
}
func update(component: GiftUpgradeCostScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let environment = environment[ViewControllerComponentContainer.Environment.self].value
let themeUpdated = self.environment?.theme !== environment.theme
let resetScrolling = self.scrollView.bounds.width != availableSize.width
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let isFirstTime = self.component == nil
self.component = component
self.state = state
self.environment = environment
if isFirstTime {
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
if let _ = component.upgradePreview.nextPrices.first(where: { currentTime < $0.date }) {
self.upgradePreviewTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
self?.upgradePreviewTimerTick()
}, queue: Queue.mainQueue())
self.upgradePreviewTimer?.start()
self.upgradePreviewTimerTick()
}
}
if themeUpdated {
self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
self.backgroundLayer.backgroundColor = environment.theme.actionSheet.opaqueItemBackgroundColor.cgColor
self.navigationBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate)
self.navigationBarSeparator.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor
self.bottomPanelSeparator.backgroundColor = environment.theme.rootController.tabBar.separatorColor.cgColor
}
transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize))
var contentHeight: CGFloat = 0.0
let closeImage: UIImage
if let image = self.cachedCloseImage, !themeUpdated {
closeImage = image
} else {
closeImage = generateCloseButtonImage(backgroundColor: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.05), foregroundColor: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.4))!
self.cachedCloseImage = closeImage
}
let closeButtonSize = self.closeButton.update(
transition: transition,
component: AnyComponent(Button(
content: AnyComponent(Image(image: closeImage, size: closeImage.size)),
action: { [weak self] in
guard let self, let controller = self.environment?.controller() else {
return
}
controller.dismiss()
}
).minSize(CGSize(width: 62.0, height: 56.0))),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
let closeButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - environment.safeInsets.right - closeButtonSize.width, y: 0.0), size: closeButtonSize)
if let closeButtonView = self.closeButton.view {
if closeButtonView.superview == nil {
self.navigationBarContainer.addSubview(closeButtonView)
}
transition.setFrame(view: closeButtonView, frame: closeButtonFrame)
}
let containerInset: CGFloat = environment.statusBarHeight + 10.0
let clippingY: CGFloat
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_UpgradeCost_Title, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
)
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: floor((56.0 - titleSize.height) * 0.5)), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
self.navigationBarContainer.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: titleFrame)
}
contentHeight += 56.0
let navigationBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: 54.0))
transition.setFrame(view: self.navigationBackgroundView, frame: navigationBackgroundFrame)
self.navigationBackgroundView.update(size: navigationBackgroundFrame.size, cornerRadius: 10.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition.containedViewLayoutTransition)
transition.setFrame(layer: self.navigationBarSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 54.0), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
var value: CGFloat = 0.0
if let startStars = component.upgradePreview.prices.first?.stars, let endStars = component.upgradePreview.prices.last?.stars {
let effectiveValue = self.effectiveUpgradePrice?.stars ?? endStars
value = CGFloat(effectiveValue - endStars) / CGFloat(startStars - endStars)
}
let barSize = self.bar.update(
transition: transition,
component: AnyComponent(ProfileLevelRatingBarComponent(
theme: environment.theme,
value: 1.0 - value,
leftLabel: environment.strings.Gift_UpgradeCost_Stars(Int32(clamping: component.upgradePreview.prices.first?.stars ?? 0)),
rightLabel: environment.strings.Gift_UpgradeCost_Stars(Int32(clamping: component.upgradePreview.prices.last?.stars ?? 0)),
badgeValue: "\(self.effectiveUpgradePrice?.stars ?? 0)",
badgeTotal: "",
level: 0,
icon: .stars,
inversed: true
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 110.0)
)
let barFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - barSize.width) * 0.5), y: contentHeight), size: barSize)
if let barView = self.bar.view {
if barView.superview == nil {
self.scrollContentView.addSubview(barView)
}
transition.setFrame(view: barView, frame: barFrame)
}
contentHeight += barSize.height + 25.0
let descriptionSize = self.descriptionText.update(
transition: transition,
component: AnyComponent(BalancedTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Gift_UpgradeCost_Description,
font: Font.regular(15.0),
textColor: environment.theme.list.itemPrimaryTextColor,
paragraphAlignment: .center
)),
horizontalAlignment: .center,
maximumNumberOfLines: 3,
lineSpacing: 0.2
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 50.0, height: .greatestFiniteMagnitude)
)
let descriptionFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - descriptionSize.width) * 0.5), y: contentHeight), size: descriptionSize)
if let descriptionView = self.descriptionText.view {
if descriptionView.superview == nil {
self.scrollContentView.addSubview(descriptionView)
}
transition.setFrame(view: descriptionView, frame: descriptionFrame)
}
contentHeight += descriptionSize.height + 23.0
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
var tableItems: [TableComponent.Item] = []
for price in component.upgradePreview.prices {
if price.date < currentTime {
continue
}
let valueString = "⭐️\(presentationStringsFormattedNumber(abs(Int32(clamping: price.stars)), environment.dateTimeFormat.groupingSeparator))"
let valueAttributedString = NSMutableAttributedString(string: valueString, font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)
let range = (valueAttributedString.string as NSString).range(of: "⭐️")
if range.location != NSNotFound {
valueAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
valueAttributedString.addAttribute(.baselineOffset, value: 1.0, range: range)
}
tableItems.append(TableComponent.Item(
id: price.stars,
title: stringForGiftUpgradeTimestamp(strings: environment.strings, dateTimeFormat: environment.dateTimeFormat, timestamp: price.date),
titleFont: .bold,
component: AnyComponent(MultilineTextWithEntitiesComponent(context: component.context, animationCache: component.context.animationCache, animationRenderer: component.context.animationRenderer, placeholderColor: .white, text: .plain(valueAttributedString)))
))
}
let tableSize = self.table.update(
transition: transition,
component: AnyComponent(TableComponent(
theme: environment.theme,
items: tableItems
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude)
)
let tableFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - tableSize.width) * 0.5), y: contentHeight), size: tableSize)
if let tableView = self.table.view {
if tableView.superview == nil {
self.scrollContentView.addSubview(tableView)
}
transition.setFrame(view: tableView, frame: tableFrame)
}
contentHeight += tableSize.height + 15.0
let additionalDescriptionSize = self.additionalDescription.update(
transition: transition,
component: AnyComponent(BalancedTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Gift_UpgradeCost_AdditionalDescription,
font: Font.regular(13.0),
textColor: environment.theme.list.itemSecondaryTextColor,
paragraphAlignment: .center
)),
horizontalAlignment: .center,
maximumNumberOfLines: 5,
lineSpacing: 0.2
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 50.0, height: .greatestFiniteMagnitude)
)
let additionalDescriptionFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - additionalDescriptionSize.width) * 0.5), y: contentHeight), size: additionalDescriptionSize)
if let additionalDescriptionView = self.additionalDescription.view {
if additionalDescriptionView.superview == nil {
self.scrollContentView.addSubview(additionalDescriptionView)
}
transition.setFrame(view: additionalDescriptionView, frame: additionalDescriptionFrame)
}
contentHeight += additionalDescriptionSize.height + 15.0
let actionButtonTitle: String = environment.strings.Gift_UpgradeCost_Done
var buttonTitle: [AnyComponentWithIdentity<Empty>] = []
let playButtonAnimation = ActionSlot<Void>()
buttonTitle.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "anim_ok"),
color: environment.theme.list.itemCheckColors.foregroundColor,
startingPosition: .begin,
size: CGSize(width: 28.0, height: 28.0),
playOnce: playButtonAnimation
))))
buttonTitle.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(ButtonTextContentComponent(
text: actionButtonTitle,
badge: 0,
textColor: environment.theme.list.itemCheckColors.foregroundColor,
badgeBackground: environment.theme.list.itemCheckColors.foregroundColor,
badgeForeground: environment.theme.list.itemCheckColors.fillColor
))))
let actionButtonSize = self.actionButton.update(
transition: transition,
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
color: environment.theme.list.itemCheckColors.fillColor,
foreground: environment.theme.list.itemCheckColors.foregroundColor,
pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(HStack(buttonTitle, spacing: 2.0))
),
isEnabled: true,
displaysProgress: false,
action: { [weak self] in
guard let self else {
return
}
self.environment?.controller()?.dismiss()
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
)
let bottomPanelHeight = 10.0 + environment.safeInsets.bottom + actionButtonSize.height
let bottomPanelSeparatorFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0), size: CGSize(width: availableSize.width, height: UIScreenPixel))
transition.setFrame(layer: self.bottomPanelSeparator, frame: bottomPanelSeparatorFrame)
let bottomPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight), size: CGSize(width: availableSize.width, height: bottomPanelHeight))
transition.setFrame(view: self.bottomPanelContainer, frame: bottomPanelFrame)
let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: actionButtonSize)
if let actionButtonView = self.actionButton.view {
if actionButtonView.superview == nil {
self.bottomPanelContainer.addSubview(actionButtonView)
playButtonAnimation.invoke(Void())
}
transition.setFrame(view: actionButtonView, frame: actionButtonFrame)
}
contentHeight += bottomPanelHeight
clippingY = bottomPanelFrame.minY - 8.0
let topInset: CGFloat = max(0.0, availableSize.height - containerInset - contentHeight)
let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset)
self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset)
transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight)))
transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0))
transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: availableSize))
let scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset), size: CGSize(width: availableSize.width - sideInset * 2.0, height: clippingY - containerInset))
transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center)
transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size))
self.ignoreScrolling = true
let previousBounds = self.scrollView.bounds
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)))
let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight)
if contentSize != self.scrollView.contentSize {
self.scrollView.contentSize = contentSize
}
if resetScrolling {
self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize)
} else {
if !previousBounds.isEmpty, !transition.animation.isImmediate {
let bounds = self.scrollView.bounds
if bounds.maxY != previousBounds.maxY {
let offsetY = previousBounds.maxY - bounds.maxY
transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true)
}
}
}
self.ignoreScrolling = false
self.updateScrolling(transition: transition)
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public class GiftUpgradeCostScreen: ViewControllerComponentContainer {
private let context: AccountContext
private var isDismissed: Bool = false
public init(
context: AccountContext,
upgradePreview: StarGiftUpgradePreview
) {
self.context = context
super.init(context: context, component: GiftUpgradeCostScreenComponent(
context: context,
upgradePreview: upgradePreview
), navigationBarAppearance: .none, theme: .default)
self.statusBar.statusBarStyle = .Ignore
self.navigationPresentation = .flatModal
self.blocksBackgroundWhenInOverlay = true
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.view.disablesInteractiveModalDismiss = true
if let componentView = self.node.hostView.componentView as? GiftUpgradeCostScreenComponent.View {
componentView.animateIn()
}
}
override public func dismiss(completion: (() -> Void)? = nil) {
if !self.isDismissed {
self.isDismissed = true
if let componentView = self.node.hostView.componentView as? GiftUpgradeCostScreenComponent.View {
componentView.animateOut(completion: { [weak self] in
completion?()
self?.dismiss(animated: false)
})
} else {
self.dismiss(animated: false)
}
}
}
}

View File

@ -37,6 +37,8 @@ import BalanceNeededScreen
import GiftItemComponent
import GiftAnimationComponent
import ChatThemeScreen
import ProfileLevelRatingBarComponent
import AnimatedTextComponent
private final class GiftViewSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -108,13 +110,14 @@ private final class GiftViewSheetContent: CombinedComponent {
var canSkip = false
var testUpgradeAnimation = !"".isEmpty
var nextGiftToUpgrade: ProfileGiftsContext.State.StarGift?
var inUpgradePreview = false
var upgradeForm: BotPaymentForm?
var upgradeFormDisposable: Disposable?
var upgradeDisposable: Disposable?
var scheduledUpgradeCommit = false
let levelsDisposable = MetaDisposable()
var nextGiftToUpgrade: ProfileGiftsContext.State.StarGift?
var buyForm: BotPaymentForm?
var buyFormDisposable: Disposable?
@ -125,8 +128,11 @@ private final class GiftViewSheetContent: CombinedComponent {
var pendingWear = false
var pendingTakeOff = false
var inUpgradePreview = false
var scheduledUpgradePreview = false
var upgradePreview: StarGiftUpgradePreview?
let upgradePreviewDisposable = DisposableSet()
var upgradePreviewTimer: SwiftSignalKit.Timer?
var keepOriginalInfo = false
@ -264,17 +270,27 @@ private final class GiftViewSheetContent: CombinedComponent {
}
self.updated()
if arguments.upgradeStars == nil {
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
if let _ = upgradePreview.nextPrices.first(where: { currentTime < $0.date }) {
self.upgradePreviewTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
self?.upgradePreviewTimerTick()
}, queue: Queue.mainQueue())
self.upgradePreviewTimer?.start()
self.upgradePreviewTimerTick()
}
}
if self.scheduledUpgradePreview {
self.inProgress = false
self.scheduledUpgradePreview = false
self.requestUpgradePreview()
}
}))
if arguments.upgradeStars == nil, let reference = arguments.reference {
self.upgradeFormDisposable = (context.engine.payments.fetchBotPaymentForm(source: .starGiftUpgrade(keepOriginalInfo: false, reference: reference), themeParams: nil)
|> deliverOnMainQueue).start(next: { [weak self] paymentForm in
guard let self else {
return
}
self.upgradeForm = paymentForm
self.updated()
})
if arguments.upgradeStars == nil {
self.fetchUpgradeForm()
}
}
}
@ -521,7 +537,7 @@ private final class GiftViewSheetContent: CombinedComponent {
action: { [weak navigationController] action in
if case .undo = action, let navigationController, let giftsPeerId {
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: giftsPeerId))
|> deliverOnMainQueue).start(next: { [weak navigationController] peer in
|> deliverOnMainQueue).start(next: { [weak navigationController] peer in
guard let peer, let navigationController else {
return
}
@ -548,7 +564,7 @@ private final class GiftViewSheetContent: CombinedComponent {
}
func convertToStars() {
guard let controller = self.getController() as? GiftViewScreen, let starsContext = context.starsContext, let arguments = self.subject.arguments, let reference = arguments.reference, let fromPeerName = arguments.fromPeerName, let convertStars = arguments.convertStars, let navigationController = controller.navigationController as? NavigationController else {
guard let controller = self.getController() as? GiftViewScreen, let starsContext = context.starsContext, let arguments = self.subject.arguments, let reference = arguments.reference, let fromPeerName = arguments.fromPeerCompactName, let convertStars = arguments.convertStars, let navigationController = controller.navigationController as? NavigationController else {
return
}
@ -664,22 +680,98 @@ private final class GiftViewSheetContent: CombinedComponent {
controller.push(introController)
}
func openRemoveInfo() {
guard let controller = self.getController(), let gift = self.subject.arguments?.gift, case let .unique(uniqueGift) = gift else {
func openDropOriginalDetails() {
guard let controller = self.getController(), let gift = self.subject.arguments?.gift, case let .unique(uniqueGift) = gift, let price = self.subject.arguments?.dropOriginalDetailsStars else {
return
}
//TODO:release
let removeInfoController = giftRemoveInfoAlertController(
context: self.context,
gift: uniqueGift,
peers: self.peerMap,
removeInfoStars: 1000,
removeInfoStars: price,
navigationController: controller.navigationController as? NavigationController,
commit: {}
commit: { [weak self] in
guard let self else {
return
}
self.commitDropOriginalDetails()
}
)
controller.present(removeInfoController, in: .window(.root))
}
func commitDropOriginalDetails() {
guard let arguments = self.subject.arguments, let controller = self.getController() as? GiftViewScreen, let gift = self.subject.arguments?.gift, case let .unique(uniqueGift) = gift, let starsContext = self.context.starsContext, let starsState = starsContext.currentState, let reference = arguments.reference, let price = self.subject.arguments?.dropOriginalDetailsStars else {
return
}
let context = self.context
let proceed = {
let dropOriginalDetailsImpl = controller.dropOriginalDetails
let signal: Signal<Never, DropStarGiftOriginalDetailsError>
if let dropOriginalDetailsImpl {
signal = dropOriginalDetailsImpl(reference)
} else {
signal = (context.engine.payments.dropStarGiftOriginalDetails(reference: reference)
|> deliverOnMainQueue)
}
self.upgradeDisposable = (signal
|> deliverOnMainQueue).start(error: { _ in
}, completed: { [weak self, weak starsContext] in
guard let self else {
return
}
Queue.mainQueue().after(0.5) {
starsContext?.load(force: true)
}
switch self.subject {
case let .profileGift(peerId, gift):
let updatedAttributes = uniqueGift.attributes.filter { $0.attributeType != .originalInfo }
self.subject = .profileGift(peerId, gift.withGift(.unique(uniqueGift.withAttributes(updatedAttributes))))
default:
break
}
self.updated(transition: .spring(duration: 0.3))
})
}
if starsState.balance < StarsAmount(value: price, nanos: 0) {
let _ = (self.optionsPromise.get()
|> filter { $0 != nil }
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak self] options in
guard let self, let controller = self.getController() else {
return
}
let purchaseController = self.context.sharedContext.makeStarsPurchaseScreen(
context: self.context,
starsContext: starsContext,
options: options ?? [],
purpose: .removeOriginalDetailsStarGift(requiredStars: price),
targetPeerId: nil,
completion: { [weak self, weak starsContext] stars in
guard let self, let starsContext else {
return
}
self.inProgress = true
self.updated()
starsContext.add(balance: StarsAmount(value: stars, nanos: 0))
let _ = (starsContext.onUpdate
|> deliverOnMainQueue).start(next: {
proceed()
})
}
)
controller.push(purchaseController)
})
} else {
proceed()
}
}
private var isOpeningValue = false
func openValue() {
guard let controller = self.getController(), let gift = self.subject.arguments?.gift, case let .unique(uniqueGift) = gift, !self.isOpeningValue else {
@ -1328,14 +1420,72 @@ private final class GiftViewSheetContent: CombinedComponent {
}
}
private func fetchUpgradeForm() {
guard let reference = self.subject.arguments?.reference else {
return
}
self.upgradeForm = nil
self.upgradeFormDisposable = (self.context.engine.payments.fetchBotPaymentForm(source: .starGiftUpgrade(keepOriginalInfo: false, reference: reference), themeParams: nil)
|> deliverOnMainQueue).start(next: { [weak self] paymentForm in
guard let self else {
return
}
self.upgradeForm = paymentForm
self.updated()
if self.scheduledUpgradeCommit {
self.scheduledUpgradeCommit = false
self.commitUpgrade()
}
})
}
private(set) var effectiveUpgradePrice: StarGiftUpgradePreview.Price?
private(set) var nextUpgradePrice: StarGiftUpgradePreview.Price?
func upgradePreviewTimerTick() {
guard let upgradePreview = self.upgradePreview else {
return
}
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
if let currentPrice = self.effectiveUpgradePrice {
if let price = upgradePreview.nextPrices.reversed().first(where: { currentTime >= $0.date }) {
if price.stars != currentPrice.stars {
self.effectiveUpgradePrice = price
if let nextPrice = upgradePreview.nextPrices.first(where: { $0.stars < price.stars }) {
self.nextUpgradePrice = nextPrice
}
self.fetchUpgradeForm()
}
} else {
self.upgradePreviewTimer?.invalidate()
self.upgradePreviewTimer = nil
}
} else if let price = upgradePreview.nextPrices.reversed().first(where: { currentTime >= $0.date}) {
self.effectiveUpgradePrice = price
if let nextPrice = upgradePreview.nextPrices.first(where: { $0.stars < price.stars }) {
self.nextUpgradePrice = nextPrice
}
}
self.updated()
}
func requestUpgradePreview() {
self.context.starsContext?.load(force: false)
self.inUpgradePreview = true
self.updated(transition: .spring(duration: 0.4))
if let controller = self.getController() as? GiftViewScreen, self.upgradeForm != nil {
controller.showBalance = true
if let _ = self.upgradePreview {
self.context.starsContext?.load(force: false)
self.inUpgradePreview = true
self.updated(transition: .spring(duration: 0.4))
if let controller = self.getController() as? GiftViewScreen, self.upgradeForm != nil {
controller.showBalance = true
}
} else {
self.scheduledUpgradePreview = true
self.inProgress = true
self.updated()
}
}
@ -1399,19 +1549,14 @@ private final class GiftViewSheetContent: CombinedComponent {
if let buyGift = controller.buyGift {
buyGiftImpl = { slug, peerId, price in
return buyGift(slug, peerId, price)
|> afterCompleted {
context.starsContext?.load(force: true)
}
}
} else {
buyGiftImpl = { slug, peerId, price in
return self.context.engine.payments.buyStarGift(slug: slug, peerId: peerId, price: price)
|> afterCompleted {
context.starsContext?.load(force: true)
}
}
}
let finalPrice = acceptedPrice ?? resellAmount
self.buyDisposable = (buyGiftImpl(uniqueGift.slug, recipientPeerId, acceptedPrice ?? resellAmount)
|> deliverOnMainQueue).start(
error: { [weak self] error in
@ -1533,7 +1678,12 @@ private final class GiftViewSheetContent: CombinedComponent {
self.updated(transition: .spring(duration: 0.4))
Queue.mainQueue().after(0.5) {
context.starsContext?.load(force: true)
switch finalPrice.currency {
case .stars:
context.starsContext?.load(force: true)
case .ton:
context.tonContext?.load(force: true)
}
}
}
)
@ -1558,6 +1708,7 @@ private final class GiftViewSheetContent: CombinedComponent {
starsContext: starsContext,
options: options ?? [],
purpose: .buyStarGift(requiredStars: resellAmount.amount.value),
targetPeerId: nil,
completion: { [weak self, weak starsContext] stars in
guard let self, let starsContext else {
return
@ -1744,7 +1895,6 @@ private final class GiftViewSheetContent: CombinedComponent {
controller.showBalance = false
let context = self.context
let upgradeGiftImpl: ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)
if let upgradeGift = controller.upgradeGift {
guard let reference = arguments.reference else {
@ -1752,11 +1902,6 @@ private final class GiftViewSheetContent: CombinedComponent {
}
upgradeGiftImpl = { formId, keepOriginalInfo in
return upgradeGift(formId, reference, keepOriginalInfo)
|> afterCompleted {
if formId != nil {
context.starsContext?.load(force: true)
}
}
}
} else {
guard let reference = arguments.reference else {
@ -1764,11 +1909,6 @@ private final class GiftViewSheetContent: CombinedComponent {
}
upgradeGiftImpl = { formId, keepOriginalInfo in
return self.context.engine.payments.upgradeStarGift(formId: formId, reference: reference, keepOriginalInfo: keepOriginalInfo)
|> afterCompleted {
if formId != nil {
context.starsContext?.load(force: true)
}
}
}
}
@ -1851,6 +1991,7 @@ private final class GiftViewSheetContent: CombinedComponent {
starsContext: starsContext,
options: options ?? [],
purpose: .upgradeStarGift(requiredStars: price),
targetPeerId: nil,
completion: { [weak self, weak starsContext] stars in
guard let self, let starsContext else {
return
@ -1870,15 +2011,21 @@ private final class GiftViewSheetContent: CombinedComponent {
} else {
proceed(upgradeForm.id)
}
} else {
self.scheduledUpgradeCommit = true
}
// if let controller = self.getController() as? GiftViewScreen {
// controller.showBalance = true
// }
}
func openUpgradePricePreview() {
guard let controller = self.getController(), let upgradePreview = self.upgradePreview else {
return
}
let costController = GiftUpgradeCostScreen(context: self.context, upgradePreview: upgradePreview)
controller.push(costController)
}
func commitPrepaidUpgrade() {
guard let arguments = self.subject.arguments, let peerId = arguments.peerId, let prepaidUpgradeHash = arguments.prepaidUpgradeHash, let starsContext = self.context.starsContext, let starsState = starsContext.currentState else {
guard let arguments = self.subject.arguments, let controller = self.getController() as? GiftViewScreen, let peerId = arguments.peerId, let prepaidUpgradeHash = arguments.prepaidUpgradeHash, let starsContext = self.context.starsContext, let starsState = starsContext.currentState else {
return
}
guard case let .generic(gift) = arguments.gift else {
@ -1888,6 +2035,7 @@ private final class GiftViewSheetContent: CombinedComponent {
return
}
let context = self.context
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let proceed: () -> Void = { [weak self, weak starsContext] in
guard let self else {
return
@ -1910,7 +2058,7 @@ private final class GiftViewSheetContent: CombinedComponent {
}
self.upgradeDisposable = (signal
|> deliverOnMainQueue).start(next: { [weak self, weak starsContext] result in
|> deliverOnMainQueue).start(next: { [weak self, weak controller, weak starsContext] result in
guard let self else {
return
}
@ -1918,12 +2066,54 @@ private final class GiftViewSheetContent: CombinedComponent {
starsContext?.load(force: true)
}
let navigationController = controller?.navigationController as? NavigationController
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
|> deliverOnMainQueue).start(next: { [weak self, weak navigationController] peer in
guard let self, let peer else {
return
}
self.openPeer(peer, gifts: false, dismiss: true)
Queue.mainQueue().after(0.5) {
if let lastController = navigationController?.viewControllers.last as? ViewController {
let resultController = UndoOverlayController(
presentationData: presentationData,
content: .sticker(
context: context,
file: gift.file,
loop: false,
title: nil,
text: presentationData.strings.Gift_Upgrade_Gift_Sent_Text,
undoText: presentationData.strings.Gift_Upgrade_Gift_Sent_GiftMore,
customAction: nil
),
elevatedLayout: !(lastController is ChatController),
action: { [weak navigationController] action in
if case .undo = action, let navigationController {
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).start(next: { [weak navigationController] peer in
guard let peer, let navigationController else {
return
}
if let controller = context.sharedContext.makePeerInfoController(
context: context,
updatedPresentationData: nil,
peer: peer._asPeer(),
mode: .upgradableGifts,
avatarInitiallyExpanded: false,
fromChat: false,
requestsContext: nil
) {
navigationController.pushViewController(controller, animated: true)
}
})
}
return true
}
)
lastController.present(resultController, in: .current)
}
}
})
}, error: { _ in
@ -1934,8 +2124,8 @@ private final class GiftViewSheetContent: CombinedComponent {
let _ = (self.optionsPromise.get()
|> filter { $0 != nil }
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak self] options in
guard let self, let controller = self.getController() else {
|> deliverOnMainQueue).startStandalone(next: { [weak self, weak controller] options in
guard let self, let controller else {
return
}
let purchaseController = self.context.sharedContext.makeStarsPurchaseScreen(
@ -1943,6 +2133,7 @@ private final class GiftViewSheetContent: CombinedComponent {
starsContext: starsContext,
options: options ?? [],
purpose: .upgradeStarGift(requiredStars: price),
targetPeerId: nil,
completion: { [weak self, weak starsContext] stars in
guard let self, let starsContext else {
return
@ -2008,7 +2199,8 @@ private final class GiftViewSheetContent: CombinedComponent {
let upgradeDescription = Child(BalancedTextComponent.self)
let upgradePerks = Child(List<Empty>.self)
let upgradeKeepName = Child(PlainButtonComponent.self)
let upgradePriceButton = Child(PlainButtonComponent.self)
let spaceRegex = try? NSRegularExpression(pattern: "\\[(.*?)\\]", options: [])
let giftCompositionExternalState = GiftCompositionComponent.ExternalState()
@ -2294,6 +2486,16 @@ private final class GiftViewSheetContent: CombinedComponent {
var headerComponents: [() -> Void] = []
let tableFont = Font.regular(15.0)
let tableBoldFont = Font.semibold(15.0)
let tableItalicFont = Font.italic(15.0)
let tableBoldItalicFont = Font.semiboldItalic(15.0)
let tableMonospaceFont = Font.monospace(15.0)
let tableLargeMonospaceFont = Font.monospace(16.0)
let tableTextColor = theme.list.itemPrimaryTextColor
let tableLinkColor = theme.list.itemAccentColor
if let headerSubject {
let animation = animation.update(
component: GiftCompositionComponent(
@ -2983,15 +3185,6 @@ private final class GiftViewSheetContent: CombinedComponent {
}
}
let tableFont = Font.regular(15.0)
let tableBoldFont = Font.semibold(15.0)
let tableItalicFont = Font.italic(15.0)
let tableBoldItalicFont = Font.semiboldItalic(15.0)
let tableMonospaceFont = Font.monospace(15.0)
let tableLargeMonospaceFont = Font.monospace(16.0)
let tableTextColor = theme.list.itemPrimaryTextColor
let tableLinkColor = theme.list.itemAccentColor
var tableItems: [TableComponent.Item] = []
var isWearing = state.pendingWear
@ -3490,6 +3683,7 @@ private final class GiftViewSheetContent: CombinedComponent {
insets: id == "originalInfo" ? UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0) : .zero,
highlightColor: tableLinkColor.withAlphaComponent(0.1),
handleSpoilers: true,
maxWidth: id == "originalInfo" ? context.availableSize.width - sideInset * 2.0 - 68.0 : nil,
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)
@ -3527,18 +3721,18 @@ private final class GiftViewSheetContent: CombinedComponent {
var itemAlignment: HStackAlignment = .left
var itemSpacing: CGFloat = 4.0
if id == "originalInfo" {
if id == "originalInfo", let _ = subject.arguments?.dropOriginalDetailsStars {
items.append(AnyComponentWithIdentity(
id: AnyHashable(1),
component: AnyComponent(Button(
content: AnyComponent(BundleIconComponent(name: "Chat/Context Menu/Delete", tintColor: tableLinkColor)),
action: { [weak state] in
state?.openRemoveInfo()
state?.openDropOriginalDetails()
}
))
))
itemAlignment = .alternatingLeftRight
itemSpacing = 16.0
itemSpacing = 8.0
}
var itemComponent = AnyComponent(
@ -3654,7 +3848,7 @@ private final class GiftViewSheetContent: CombinedComponent {
if let upgradeStars, upgradeStars > 0 {
finalStars += upgradeStars
}
let valueString = "\(presentationStringsFormattedNumber(abs(Int32(finalStars)), dateTimeFormat.groupingSeparator))⭐️"
let valueString = "\(presentationStringsFormattedNumber(abs(Int32(clamping: finalStars)), dateTimeFormat.groupingSeparator))⭐️"
let valueAttributedString = NSMutableAttributedString(string: valueString, font: tableFont, textColor: tableTextColor)
let range = (valueAttributedString.string as NSString).range(of: "⭐️")
if range.location != NSNotFound {
@ -3780,13 +3974,14 @@ private final class GiftViewSheetContent: CombinedComponent {
let table = table.update(
component: TableComponent(
theme: environment.theme,
theme: theme,
items: tableItems
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude),
transition: context.transition
)
context.add(table
.clipsToBounds(true)
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + table.size.height / 2.0))
.appear(.default(alpha: true))
.disappear(ComponentTransition.Disappear({ view, transition, completion in
@ -4096,14 +4291,18 @@ private final class GiftViewSheetContent: CombinedComponent {
if state.cachedStarImage == nil || state.cachedStarImage?.1 !== theme {
state.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: theme.list.itemCheckColors.foregroundColor)!, theme)
}
var buttonTitleItems: [AnyComponentWithIdentity<Empty>] = []
var upgradeString = strings.Gift_Upgrade_Upgrade
if !incoming {
if let gift = state.starGiftsMap[giftId], let upgradeStars = gift.upgradeStars {
let priceString = presentationStringsFormattedNumber(Int32(clamping: upgradeStars), environment.dateTimeFormat.groupingSeparator)
upgradeString = strings.Gift_Upgrade_GiftUpgrade(" # \(priceString)").string
}
} else if let upgradeForm = state.upgradeForm, let price = upgradeForm.invoice.prices.first?.amount {
let priceString = presentationStringsFormattedNumber(Int32(clamping: price), environment.dateTimeFormat.groupingSeparator)
} else if let upgradeStars = state.effectiveUpgradePrice?.stars {
let priceString = presentationStringsFormattedNumber(Int32(clamping: upgradeStars), environment.dateTimeFormat.groupingSeparator)
upgradeString = strings.Gift_Upgrade_GiftUpgrade(" # \(priceString)").string
} else if let upgradeForm = state.upgradeForm, let upgradeStars = upgradeForm.invoice.prices.first?.amount {
let priceString = presentationStringsFormattedNumber(Int32(clamping: upgradeStars), environment.dateTimeFormat.groupingSeparator)
upgradeString = strings.Gift_Upgrade_UpgradeFor(" # \(priceString)").string
}
let buttonTitle = subject.arguments?.upgradeStars != nil ? strings.Gift_Upgrade_Confirm : upgradeString
@ -4114,12 +4313,60 @@ private final class GiftViewSheetContent: CombinedComponent {
buttonAttributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: buttonAttributedString.string))
buttonAttributedString.addAttribute(.kern, value: 2.0, range: NSRange(range, in: buttonAttributedString.string))
}
if let nextUpgradePrice = state.nextUpgradePrice {
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
let upgradeTimeout = nextUpgradePrice.date - currentTime
buttonTitleItems.append(AnyComponentWithIdentity(id: "static_label", component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))))
let minutes = Int(upgradeTimeout / 60)
let seconds = Int(upgradeTimeout % 60)
let rawString = strings.Gift_Upgrade_PriceWillDecrease
var buttonAnimatedTitleItems: [AnimatedTextComponent.Item] = []
var startIndex = rawString.startIndex
while true {
if let range = rawString.range(of: "{", range: startIndex ..< rawString.endIndex) {
if range.lowerBound != startIndex {
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: AnyHashable(buttonAnimatedTitleItems.count), content: .text(String(rawString[startIndex ..< range.lowerBound]))))
}
startIndex = range.upperBound
if let endRange = rawString.range(of: "}", range: startIndex ..< rawString.endIndex) {
let controlString = rawString[range.upperBound ..< endRange.lowerBound]
if controlString == "m" {
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: AnyHashable(buttonAnimatedTitleItems.count), content: .number(minutes, minDigits: 2)))
} else if controlString == "s" {
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: AnyHashable(buttonAnimatedTitleItems.count), content: .number(seconds, minDigits: 2)))
}
startIndex = endRange.upperBound
}
} else {
break
}
}
if startIndex != rawString.endIndex {
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: AnyHashable(buttonAnimatedTitleItems.count), content: .text(String(rawString[startIndex ..< rawString.endIndex]))))
}
buttonTitleItems.append(AnyComponentWithIdentity(id: "timer", component: AnyComponent(AnimatedTextComponent(
font: Font.with(size: 11.0, weight: .medium, traits: .monospacedNumbers),
color: environment.theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7),
items: buttonAnimatedTitleItems,
noDelay: true
))))
} else {
buttonTitleItems.append(AnyComponentWithIdentity(id: "static_label", component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))))
}
buttonChild = button.update(
component: ButtonComponent(
background: buttonBackground,
content: AnyComponentWithIdentity(
id: AnyHashable("upgrade"),
component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))
component: AnyComponent(VStack(buttonTitleItems, spacing: 1.0))
),
isEnabled: true,
displaysProgress: state.inProgress,
@ -4131,9 +4378,9 @@ private final class GiftViewSheetContent: CombinedComponent {
}
}),
availableSize: buttonSize,
transition: context.transition
transition: .spring(duration: 0.2)
)
} else if upgraded, let upgradeMessageIdId = subject.arguments?.upgradeMessageId, let originalMessageId = subject.arguments?.messageId {
} else if upgraded, let arguments = subject.arguments, let upgradeMessageIdId = arguments.upgradeMessageId, let originalMessageId = arguments.messageId, !arguments.upgradeSeparate {
let upgradeMessageId = MessageId(peerId: originalMessageId.peerId, namespace: originalMessageId.namespace, id: upgradeMessageIdId)
let buttonTitle = strings.Gift_View_ViewUpgraded
buttonChild = button.update(
@ -4350,6 +4597,40 @@ private final class GiftViewSheetContent: CombinedComponent {
originY += buttonChild.size.height
originY += 7.0
if showUpgradePreview, let _ = state.nextUpgradePrice {
originY += 20.0
if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme {
state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: theme.actionSheet.controlAccentColor)!, theme)
}
//TODO:localize
let attributedString = NSMutableAttributedString(string: "See how price will decrease >", font: Font.regular(13.0), textColor: theme.actionSheet.controlAccentColor)
if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedSmallChevronImage?.0 {
attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string))
}
let upgradePriceButton = upgradePriceButton.update(
component: PlainButtonComponent(
content: AnyComponent(
MultilineTextComponent(text: .plain(attributedString))
),
action: { [weak state] in
state?.openUpgradePricePreview()
},
animateScale: false
),
environment: {},
availableSize: buttonChild.size,
transition: .immediate
)
context.add(upgradePriceButton
.position(CGPoint(x: buttonFrame.midX, y: originY))
.appear(.default(scale: true, alpha: true))
.disappear(.default(scale: true, alpha: true))
)
originY += upgradePriceButton.size.height
}
context.add(buttons
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - 16.0 - buttons.size.width / 2.0, y: 28.0))
)
@ -4394,6 +4675,43 @@ final class GiftViewSheetComponent: CombinedComponent {
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
var headerContent: AnyComponent<Empty>?
if let arguments = context.component.subject.arguments, case .unique = arguments.gift, let fromPeerId = arguments.fromPeerId, let fromPeerName = arguments.fromPeerName, arguments.fromPeerId != context.component.context.account.peerId {
let dateString = stringForMediumDate(timestamp: arguments.date, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat, withTime: false)
let rawString = "**\(fromPeerName)** sent you this gift on **\(dateString)**."
let attributedString = parseMarkdownIntoAttributedString(rawString, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: .white), bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: .white), link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: .white), linkAttribute: { _ in return nil }))
let context = context.component.context
headerContent = AnyComponent(
PlainButtonComponent(content: AnyComponent(HeaderContentComponent(attributedText: attributedString)), action: {
if let controller = controller(), let navigationController = controller.navigationController as? NavigationController {
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: fromPeerId))
|> deliverOnMainQueue).start(next: { [weak navigationController] peer in
guard let peer, let navigationController else {
return
}
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(
navigationController: navigationController,
chatController: nil,
context: context,
chatLocation: .peer(peer),
subject: nil,
botStart: nil,
updateTextInputState: nil,
keepStack: .always,
useExisting: true,
purposefulAction: nil,
scrollToEndIfExists: false,
activateMessageSearch: nil,
animated: true
))
})
}
})
)
}
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(GiftViewSheetContent(
@ -4402,10 +4720,12 @@ final class GiftViewSheetComponent: CombinedComponent {
animateOut: animateOut,
getController: controller
)),
headerContent: headerContent,
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
followContentSizeChanges: true,
clipsContent: true,
hasDimView: false,
autoAnimateOut: false,
externalState: sheetExternalState,
animateOut: animateOut,
onPan: {
@ -4492,7 +4812,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
case upgradePreview([StarGift.UniqueGift.Attribute], String)
case wearPreview(StarGift.UniqueGift)
var arguments: (peerId: EnginePeer.Id?, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, reference: StarGiftReference?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, pinnedToTop: Bool?, converted: Bool, upgraded: Bool, refunded: Bool, canUpgrade: Bool, upgradeStars: Int64?, transferStars: Int64?, resellAmounts: [CurrencyAmount]?, canExportDate: Int32?, upgradeMessageId: Int32?, canTransferDate: Int32?, canResaleDate: Int32?, prepaidUpgradeHash: String?, upgradeSeparate: Bool)? {
var arguments: (peerId: EnginePeer.Id?, fromPeerId: EnginePeer.Id?, fromPeerName: String?, fromPeerCompactName: String?, messageId: EngineMessage.Id?, reference: StarGiftReference?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, pinnedToTop: Bool?, converted: Bool, upgraded: Bool, refunded: Bool, canUpgrade: Bool, upgradeStars: Int64?, transferStars: Int64?, resellAmounts: [CurrencyAmount]?, canExportDate: Int32?, upgradeMessageId: Int32?, canTransferDate: Int32?, canResaleDate: Int32?, prepaidUpgradeHash: String?, upgradeSeparate: Bool, dropOriginalDetailsStars: Int64?)? {
switch self {
case let .message(message):
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction {
@ -4506,8 +4826,8 @@ public class GiftViewScreen: ViewControllerComponentContainer {
} else {
reference = .message(messageId: message.id)
}
return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId, nil, nil, prepaidUpgradeHash, upgradeSeparate)
case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, _, peerId, senderId, savedId, _, canTransferDate, canResaleDate):
return (message.id.peerId, senderId ?? message.author?.id, message.author?.debugDisplayTitle, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId, nil, nil, prepaidUpgradeHash, upgradeSeparate, nil)
case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, _, peerId, senderId, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars):
var reference: StarGiftReference
if let peerId, let savedId {
reference = .peer(peerId: peerId, id: savedId)
@ -4531,13 +4851,13 @@ public class GiftViewScreen: ViewControllerComponentContainer {
if case let .unique(uniqueGift) = gift {
resellAmounts = uniqueGift.resellAmounts
}
return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, nil, false, false, false, false, nil, transferStars, resellAmounts, canExportDate, nil, canTransferDate, canResaleDate, nil, false)
return (message.id.peerId, senderId ?? message.author?.id, message.author?.debugDisplayTitle, message.author?.compactDisplayTitle, message.id, reference, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, nil, false, false, false, false, nil, transferStars, resellAmounts, canExportDate, nil, canTransferDate, canResaleDate, nil, false, dropOriginalDetailsStars)
default:
return nil
}
}
case let .uniqueGift(gift, _), let .wearPreview(gift):
return (nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, gift.resellAmounts, nil, nil, nil, nil, nil, false)
return (nil, nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, gift.resellAmounts, nil, nil, nil, nil, nil, false, nil)
case let .profileGift(peerId, gift):
var messageId: EngineMessage.Id?
if case let .message(messageIdValue) = gift.reference {
@ -4547,7 +4867,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
if case let .unique(uniqueGift) = gift.gift {
resellAmounts = uniqueGift.resellAmounts
}
return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, messageId, gift.reference, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, gift.pinnedToTop, false, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, resellAmounts, gift.canExportDate, nil, gift.canTransferDate, gift.canResaleDate, gift.prepaidUpgradeHash, gift.upgradeSeparate)
return (peerId, gift.fromPeer?.id, gift.fromPeer?.debugDisplayTitle, gift.fromPeer?.compactDisplayTitle, messageId, gift.reference, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, gift.pinnedToTop, false, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, resellAmounts, gift.canExportDate, nil, gift.canTransferDate, gift.canResaleDate, gift.prepaidUpgradeHash, gift.upgradeSeparate, gift.dropOriginalDetailsStars)
case .soldOutGift:
return nil
case .upgradePreview:
@ -4589,6 +4909,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
fileprivate let updateSavedToProfile: ((StarGiftReference, Bool) -> Void)?
fileprivate let convertToStars: ((StarGiftReference) -> Void)?
fileprivate let dropOriginalDetails: ((StarGiftReference) -> Signal<Never, DropStarGiftOriginalDetailsError>)?
fileprivate let transferGift: ((Bool, StarGiftReference, EnginePeer.Id) -> Signal<Never, TransferStarGiftError>)?
fileprivate let upgradeGift: ((Int64?, StarGiftReference, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)?
fileprivate let buyGift: ((String, EnginePeer.Id, CurrencyAmount?) -> Signal<Never, BuyStarGiftError>)?
@ -4607,6 +4928,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
forceDark: Bool = false,
updateSavedToProfile: ((StarGiftReference, Bool) -> Void)? = nil,
convertToStars: ((StarGiftReference) -> Void)? = nil,
dropOriginalDetails: ((StarGiftReference) -> Signal<Never, DropStarGiftOriginalDetailsError>)? = nil,
transferGift: ((Bool, StarGiftReference, EnginePeer.Id) -> Signal<Never, TransferStarGiftError>)? = nil,
upgradeGift: ((Int64?, StarGiftReference, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)? = nil,
buyGift: ((String, EnginePeer.Id, CurrencyAmount?) -> Signal<Never, BuyStarGiftError>)? = nil,
@ -4620,6 +4942,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
self.updateSavedToProfile = updateSavedToProfile
self.convertToStars = convertToStars
self.dropOriginalDetails = dropOriginalDetails
self.transferGift = transferGift
self.upgradeGift = upgradeGift
self.buyGift = buyGift
@ -4839,6 +5162,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
starsContext: starsContext,
options: options,
purpose: .generic,
targetPeerId: nil,
completion: { _ in }
)
navigationController.pushViewController(controller)
@ -4988,6 +5312,79 @@ private final class PeerCellComponent: Component {
}
}
final class HeaderContentComponent: Component {
let attributedText: NSAttributedString
init(
attributedText: NSAttributedString
) {
self.attributedText = attributedText
}
static func ==(lhs: HeaderContentComponent, rhs: HeaderContentComponent) -> Bool {
if lhs.attributedText != rhs.attributedText {
return false
}
return true
}
final class View: UIView {
private var component: HeaderContentComponent?
private let backgroundView: BlurredBackgroundView
private let title = ComponentView<Empty>()
override init(frame: CGRect) {
self.backgroundView = BlurredBackgroundView(color: UIColor.black.withAlphaComponent(0.2))
super.init(frame: frame)
self.addSubview(self.backgroundView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: HeaderContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(
MultilineTextComponent(text: .plain(component.attributedText), horizontalAlignment: .center)
),
environment: {},
containerSize: availableSize
)
let padding: CGFloat = 10.0
let size = CGSize(width: titleSize.width + padding * 2.0, height: 19.0)
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - titleSize.height) / 2.0) - UIScreenPixel), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: titleFrame)
}
self.backgroundView.update(size: size, cornerRadius: size.height / 2.0, transition: transition.containedViewLayoutTransition)
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: size))
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class ButtonContentComponent: Component {
let context: AccountContext
let text: String

View File

@ -7,15 +7,22 @@ import MultilineTextComponent
final class TableComponent: CombinedComponent {
class Item: Equatable {
enum TitleFont {
case regular
case bold
}
public let id: AnyHashable
public let title: String?
public let titleFont: TitleFont
public let hasBackground: Bool
public let component: AnyComponent<Empty>
public let insets: UIEdgeInsets?
public init<IdType: Hashable>(id: IdType, title: String?, hasBackground: Bool = false, component: AnyComponent<Empty>, insets: UIEdgeInsets? = nil) {
public init<IdType: Hashable>(id: IdType, title: String?, titleFont: TitleFont = .regular, hasBackground: Bool = false, component: AnyComponent<Empty>, insets: UIEdgeInsets? = nil) {
self.id = AnyHashable(id)
self.title = title
self.titleFont = titleFont
self.hasBackground = hasBackground
self.component = component
self.insets = insets
@ -28,6 +35,9 @@ final class TableComponent: CombinedComponent {
if lhs.title != rhs.title {
return false
}
if lhs.titleFont != rhs.titleFont {
return false
}
if lhs.hasBackground != rhs.hasBackground {
return false
}
@ -60,6 +70,7 @@ final class TableComponent: CombinedComponent {
}
final class State: ComponentState {
var cachedLeftColumnImage: (UIImage, PresentationTheme)?
var cachedBorderImage: (UIImage, PresentationTheme)?
}
@ -68,7 +79,7 @@ final class TableComponent: CombinedComponent {
}
public static var body: Body {
let leftColumnBackground = Child(Rectangle.self)
let leftColumnBackground = Child(Image.self)
let lastBackground = Child(Rectangle.self)
let verticalBorder = Child(Rectangle.self)
let titleChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self)
@ -99,7 +110,7 @@ final class TableComponent: CombinedComponent {
}
let titleChild = titleChildren[item.id].update(
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: title, font: Font.regular(15.0), textColor: context.component.theme.list.itemPrimaryTextColor))
text: .plain(NSAttributedString(string: title, font: item.titleFont == .bold ? Font.semibold(15.0) : Font.regular(15.0), textColor: context.component.theme.list.itemPrimaryTextColor))
)),
availableSize: context.availableSize,
transition: context.transition
@ -184,25 +195,39 @@ final class TableComponent: CombinedComponent {
)
}
let borderRadius: CGFloat = 10.0
let leftColumnImage: UIImage
if let (currentImage, theme) = context.state.cachedLeftColumnImage, theme === context.component.theme {
leftColumnImage = currentImage
} else {
leftColumnImage = generateImage(CGSize(width: 24.0, height: 24.0), rotatedContext: { size, context in
let bounds = CGRect(origin: .zero, size: CGSize(width: size.width + borderRadius, height: size.height))
context.clear(bounds)
let path = CGPath(roundedRect: bounds.insetBy(dx: borderWidth / 2.0, dy: borderWidth / 2.0), cornerWidth: borderRadius, cornerHeight: borderRadius, transform: nil)
context.setFillColor(secondaryBackgroundColor.cgColor)
context.addPath(path)
context.fillPath()
})!.stretchableImage(withLeftCapWidth: 10, topCapHeight: 10)
context.state.cachedLeftColumnImage = (leftColumnImage, context.component.theme)
}
let leftColumnBackground = leftColumnBackground.update(
component: Rectangle(color: secondaryBackgroundColor),
component: Image(image: leftColumnImage),
availableSize: CGSize(width: leftColumnWidth, height: innerTotalHeight),
transition: context.transition
)
context.add(
leftColumnBackground
.position(CGPoint(x: leftColumnWidth / 2.0, y: innerTotalHeight / 2.0))
context.add(leftColumnBackground
.position(CGPoint(x: leftColumnWidth / 2.0, y: innerTotalHeight / 2.0))
)
let borderImage: UIImage
if let (currentImage, theme) = context.state.cachedBorderImage, theme === context.component.theme {
borderImage = currentImage
} else {
let borderRadius: CGFloat = 10.0
borderImage = generateImage(CGSize(width: 24.0, height: 24.0), rotatedContext: { size, context in
let bounds = CGRect(origin: .zero, size: size)
context.setFillColor(backgroundColor.cgColor)
context.fill(bounds)
context.clear(bounds)
let path = CGPath(roundedRect: bounds.insetBy(dx: borderWidth / 2.0, dy: borderWidth / 2.0), cornerWidth: borderRadius, cornerHeight: borderRadius, transform: nil)
context.setBlendMode(.clear)
@ -259,12 +284,16 @@ final class TableComponent: CombinedComponent {
context.add(valueChild
.position(valueFrame.center)
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
if i < updatedBorderChildren.count {
let borderChild = updatedBorderChildren[i]
context.add(borderChild
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + rowHeight - borderWidth / 2.0))
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
}

View File

@ -3731,7 +3731,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}
} else if case let .gift(gift) = subject {
isGift = true
let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, isPrepaidUpgrade: false, peerId: nil, senderId: nil, savedId: nil, resaleAmount: nil, canTransferDate: nil, canResaleDate: nil))]
let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, isPrepaidUpgrade: false, peerId: nil, senderId: nil, savedId: nil, resaleAmount: nil, canTransferDate: nil, canResaleDate: nil, dropOriginalDetailsStars: nil))]
let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: self.context.account.peerId, namespace: Namespaces.Message.Cloud, id: -1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: media, peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
messages = .single([message])
} else {

View File

@ -431,7 +431,7 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll
if let _ = self.item(at: location.y) {
if self.isExpanded {
return abs(velocity.x) > abs(velocity.y)
return abs(velocity.x) > abs(velocity.y) && !self.isApplyingTransition
} else {
return abs(velocity.y) > abs(velocity.x)
}

View File

@ -178,6 +178,10 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode {
}
self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
if isReduceTransparencyEnabled() {
self.backgroundNode.isHidden = true
}
}
@objc private func pressed() {

View File

@ -2339,6 +2339,9 @@ final class PeerInfoHeaderNode: ASDisplayNode {
transition.updateFrameAdditive(node: self.buttonsBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: buttonRightOrigin.y), size: CGSize(width: width, height: buttonSize.height + 40.0)))
self.buttonsBackgroundNode.update(size: self.buttonsBackgroundNode.bounds.size, transition: transition)
self.buttonsBackgroundNode.updateColor(color: contentButtonBackgroundColor, enableBlur: true, transition: transition)
if isReduceTransparencyEnabled() {
self.buttonsBackgroundNode.alpha = 0.1
}
for buttonKey in buttonKeys.reversed() {
let buttonNode: PeerInfoHeaderButtonNode

View File

@ -5029,6 +5029,12 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
profileGifts.convertStarGift(reference: reference)
},
dropOriginalDetails: { [weak profileGifts] reference in
guard let profileGifts else {
return .complete()
}
return profileGifts.dropOriginalDetails(reference: reference)
},
transferGift: { [weak profileGifts] prepaid, reference, peerId in
guard let profileGifts else {
return .complete()

View File

@ -617,6 +617,12 @@ final class GiftsListView: UIView {
}
self.profileGifts.convertStarGift(reference: reference)
},
dropOriginalDetails: { [weak self] reference in
guard let self else {
return .complete()
}
return self.profileGifts.dropOriginalDetails(reference: reference)
},
transferGift: { [weak self] prepaid, reference, peerId in
guard let self else {
return .complete()

View File

@ -30,6 +30,7 @@ swift_library(
"//submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath",
"//submodules/Components/HierarchyTrackingLayer",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/TelegramUI/Components/PeerInfo/ProfileLevelRatingBarComponent",
],
visibility = [
"//visibility:public",

View File

@ -18,6 +18,7 @@ import Markdown
import PremiumUI
import LottieComponent
import AnimatedTextComponent
import ProfileLevelRatingBarComponent
private final class ProfileLevelInfoScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -84,10 +85,7 @@ private final class ProfileLevelInfoScreenComponent: Component {
private let closeButton = ComponentView<Empty>()
private let peerAvatar = ComponentView<Empty>()
private let callIconBackground = ComponentView<Empty>()
private let callIcon = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private let levelInfo = ComponentView<Empty>()
private var secondaryDescriptionText: ComponentView<Empty>?
@ -97,9 +95,7 @@ private final class ProfileLevelInfoScreenComponent: Component {
private let bottomPanelContainer: UIView
private let actionButton = ComponentView<Empty>()
private let bottomOverscrollLimit: CGFloat
private var isFirstTimeApplyingModalFactor: Bool = true
private var ignoreScrolling: Bool = false
@ -115,9 +111,7 @@ private final class ProfileLevelInfoScreenComponent: Component {
private var cachedChevronImage: UIImage?
private var cachedCloseImage: UIImage?
override init(frame: CGRect) {
self.bottomOverscrollLimit = 200.0
override init(frame: CGRect) {
self.dimView = UIView()
self.backgroundLayer = SimpleLayer()
@ -496,7 +490,8 @@ private final class ProfileLevelInfoScreenComponent: Component {
rightLabel: currentLevel < 0 ? "Negative rating" : nextLevel.flatMap { environment.strings.ProfileLevelInfo_LevelIndex(Int32($0)) } ?? "",
badgeValue: badgeText,
badgeTotal: badgeTextSuffix,
level: Int(currentLevel)
level: Int(currentLevel),
icon: .rating
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 110.0)

View File

@ -0,0 +1,32 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ProfileLevelRatingBarComponent",
module_name = "ProfileLevelRatingBarComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Display",
"//submodules/TelegramCore",
"//submodules/Postbox",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/MultiAnimationRenderer",
"//submodules/TelegramUI/Components/AnimationCache",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/HierarchyTrackingLayer",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/TelegramUI/Components/LottieComponent",
],
visibility = [
"//visibility:public",
],
)

View File

@ -3,7 +3,6 @@ import UIKit
import Display
import TelegramPresentationData
import ComponentFlow
import RoundedRectWithTailPath
import AnimatedTextComponent
import MultilineTextComponent
import LottieComponent
@ -17,18 +16,26 @@ final class ProfileLevelRatingBarBadge: Component {
}
}
enum Icon {
case rating
case stars
}
let theme: PresentationTheme
let title: String
let suffix: String?
let icon: Icon
init(
theme: PresentationTheme,
title: String,
suffix: String?
suffix: String?,
icon: Icon
) {
self.theme = theme
self.title = title
self.suffix = suffix
self.icon = icon
}
static func ==(lhs: ProfileLevelRatingBarBadge, rhs: ProfileLevelRatingBarBadge) -> Bool {
@ -41,6 +48,9 @@ final class ProfileLevelRatingBarBadge: Component {
if lhs.suffix != rhs.suffix {
return false
}
if lhs.icon != rhs.icon {
return false
}
return true
}
@ -141,10 +151,15 @@ final class ProfileLevelRatingBarBadge: Component {
labelsTransition.animateAlpha(view: self.badgeIcon, from: 0.0, to: 1.0)
}
if component.title.isEmpty {
self.badgeIcon.image = UIImage(bundleImageName: "Peer Info/ProfileLevelWarningIcon")?.withRenderingMode(.alwaysTemplate)
} else {
self.badgeIcon.image = UIImage(bundleImageName: "Peer Info/ProfileLevelProgressIcon")?.withRenderingMode(.alwaysTemplate)
switch component.icon {
case .rating:
if component.title.isEmpty {
self.badgeIcon.image = UIImage(bundleImageName: "Peer Info/ProfileLevelWarningIcon")?.withRenderingMode(.alwaysTemplate)
} else {
self.badgeIcon.image = UIImage(bundleImageName: "Peer Info/ProfileLevelProgressIcon")?.withRenderingMode(.alwaysTemplate)
}
case .stars:
self.badgeIcon.image = UIImage(bundleImageName: "Premium/SendStarsStarSliderIcon")?.withRenderingMode(.alwaysTemplate)
}
}

View File

@ -7,15 +7,20 @@ import MultilineTextComponent
import BundleIconComponent
import HierarchyTrackingLayer
final class ProfileLevelRatingBarComponent: Component {
final class TransitionHint {
let animate: Bool
public final class ProfileLevelRatingBarComponent: Component {
public final class TransitionHint {
public let animate: Bool
init(animate: Bool) {
public init(animate: Bool) {
self.animate = animate
}
}
public enum Icon {
case rating
case stars
}
let theme: PresentationTheme
let value: CGFloat
let leftLabel: String
@ -23,15 +28,19 @@ final class ProfileLevelRatingBarComponent: Component {
let badgeValue: String
let badgeTotal: String?
let level: Int
let icon: Icon
let inversed: Bool
init(
public init(
theme: PresentationTheme,
value: CGFloat,
leftLabel: String,
rightLabel: String,
badgeValue: String,
badgeTotal: String?,
level: Int
level: Int,
icon: Icon,
inversed: Bool = false
) {
self.theme = theme
self.value = value
@ -40,9 +49,11 @@ final class ProfileLevelRatingBarComponent: Component {
self.badgeValue = badgeValue
self.badgeTotal = badgeTotal
self.level = level
self.icon = icon
self.inversed = inversed
}
static func ==(lhs: ProfileLevelRatingBarComponent, rhs: ProfileLevelRatingBarComponent) -> Bool {
public static func ==(lhs: ProfileLevelRatingBarComponent, rhs: ProfileLevelRatingBarComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
@ -64,6 +75,12 @@ final class ProfileLevelRatingBarComponent: Component {
if lhs.level != rhs.level {
return false
}
if lhs.icon != rhs.icon {
return false
}
if lhs.inversed != rhs.inversed {
return false
}
return true
}
@ -152,7 +169,7 @@ final class ProfileLevelRatingBarComponent: Component {
}
}
final class View: UIView {
public final class View: UIView {
private let barBackground: UIImageView
private let backgroundClippingContainer: UIView
private let foregroundBarClippingContainer: UIView
@ -490,7 +507,9 @@ final class ProfileLevelRatingBarComponent: Component {
let barBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - barHeight), size: CGSize(width: availableSize.width, height: barHeight))
transition.setFrame(view: self.barBackground, frame: barBackgroundFrame)
var barForegroundFrame = CGRect(origin: barBackgroundFrame.origin, size: CGSize(width: floorToScreenPixels(progressValue * barBackgroundFrame.width), height: barBackgroundFrame.height))
let barForegroundOriginX = barBackgroundFrame.minX
let barForegroundWidth = floorToScreenPixels(progressValue * barBackgroundFrame.width)
var barForegroundFrame = CGRect(origin: CGPoint(x: barForegroundOriginX, y: barBackgroundFrame.minY), size: CGSize(width: barForegroundWidth, height: barBackgroundFrame.height))
var foregroundAlpha: CGFloat = 1.0
var foregroundContentsAlpha: CGFloat = 1.0
@ -552,7 +571,7 @@ final class ProfileLevelRatingBarComponent: Component {
self.barForeground.tintColor = badgeColor
var effectiveBarForegroundFrame = barForegroundFrame
if currentIsNegativeRating {
if currentIsNegativeRating || component.inversed {
effectiveBarForegroundFrame.size.width = barBackgroundFrame.maxX - barForegroundFrame.maxX
effectiveBarForegroundFrame.origin.x = barBackgroundFrame.maxX - effectiveBarForegroundFrame.width
}
@ -564,7 +583,23 @@ final class ProfileLevelRatingBarComponent: Component {
transition.setAlpha(view: self.foregroundBarClippingContainer, alpha: foregroundAlpha)
transition.setAlpha(view: self.foregroundClippingContainer, alpha: foregroundContentsAlpha)
let backgroundClippingFrame = CGRect(origin: CGPoint(x: barBackgroundFrame.minX + barForegroundFrame.width, y: barBackgroundFrame.minY), size: CGSize(width: barBackgroundFrame.width - barForegroundFrame.width, height: barBackgroundFrame.height))
let backgroundClippingFrame: CGRect
if currentIsNegativeRating || component.inversed {
backgroundClippingFrame = CGRect(
x: barBackgroundFrame.minX,
y: barBackgroundFrame.minY,
width: max(0.0, effectiveBarForegroundFrame.minX - barBackgroundFrame.minX),
height: barBackgroundFrame.height
)
} else {
backgroundClippingFrame = CGRect(
x: effectiveBarForegroundFrame.maxX,
y: barBackgroundFrame.minY,
width: max(0, barBackgroundFrame.maxX - effectiveBarForegroundFrame.maxX),
height: barBackgroundFrame.height
)
}
transition.setPosition(view: self.backgroundClippingContainer, position: backgroundClippingFrame.center)
transition.setBounds(view: self.backgroundClippingContainer, bounds: CGRect(origin: CGPoint(x: backgroundClippingFrame.minX - barBackgroundFrame.minX, y: 0.0), size: backgroundClippingFrame.size))
transition.setAlpha(view: self.backgroundClippingContainer, alpha: foregroundContentsAlpha)
@ -642,12 +677,21 @@ final class ProfileLevelRatingBarComponent: Component {
foregroundRightLabelView.bounds = CGRect(origin: CGPoint(), size: rightLabelFrame.size)
}
let icon: ProfileLevelRatingBarBadge.Icon
switch component.icon {
case .rating:
icon = .rating
case .stars:
icon = .stars
}
let badgeSize = self.badge.update(
transition: transition.withUserData(ProfileLevelRatingBarBadge.TransitionHint(animateText: !labelsTransition.animation.isImmediate)),
component: AnyComponent(ProfileLevelRatingBarBadge(
theme: component.theme,
title: component.level < 0 ? "" : "\(component.badgeValue)",
suffix: component.level < 0 ? nil : component.badgeTotal
suffix: component.level < 0 ? nil : component.badgeTotal,
icon: icon
)),
environment: {},
containerSize: CGSize(width: 200.0, height: 200.0)
@ -665,7 +709,8 @@ final class ProfileLevelRatingBarComponent: Component {
apparentBadgeSize = badgeSize
}
var badgeFrame = CGRect(origin: CGPoint(x: barBackgroundFrame.minX + barForegroundFrame.width - apparentBadgeSize.width * 0.5, y: barBackgroundFrame.minY - 18.0 - badgeSize.height), size: apparentBadgeSize)
let badgeOriginX = barBackgroundFrame.minX + barForegroundFrame.width
var badgeFrame = CGRect(origin: CGPoint(x: badgeOriginX - apparentBadgeSize.width * 0.5, y: barBackgroundFrame.minY - 18.0 - badgeSize.height), size: apparentBadgeSize)
let badgeSideInset: CGFloat = 0.0
@ -703,11 +748,11 @@ final class ProfileLevelRatingBarComponent: Component {
}
}
func makeView() -> View {
public func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -251,6 +251,8 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
}
case .buyStarGift:
textString = strings.Stars_Purchase_BuyStarGiftInfo
case .removeOriginalDetailsStarGift:
textString = strings.Stars_Purchase_RemoveOriginalDetailsStarGiftInfo
}
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: accentColor), linkAttribute: { contents in
@ -834,7 +836,7 @@ private final class StarsPurchaseScreenComponent: CombinedComponent {
titleText = strings.Stars_Purchase_GetStars
case .gift:
titleText = strings.Stars_Purchase_GiftStars
case let .topUp(requiredStars, _), let .transfer(_, requiredStars), let .reactions(_, requiredStars), let .subscription(_, requiredStars, _), let .unlockMedia(requiredStars), let .starGift(_, requiredStars), let .upgradeStarGift(requiredStars), let .transferStarGift(requiredStars), let .sendMessage(_, requiredStars), let .buyStarGift(requiredStars):
case let .topUp(requiredStars, _), let .transfer(_, requiredStars), let .reactions(_, requiredStars), let .subscription(_, requiredStars, _), let .unlockMedia(requiredStars), let .starGift(_, requiredStars), let .upgradeStarGift(requiredStars), let .transferStarGift(requiredStars), let .sendMessage(_, requiredStars), let .buyStarGift(requiredStars), let .removeOriginalDetailsStarGift(requiredStars):
titleText = strings.Stars_Purchase_StarsNeeded(Int32(requiredStars))
}
@ -1010,6 +1012,7 @@ public final class StarsPurchaseScreen: ViewControllerComponentContainer {
starsContext: StarsContext,
options: [Any] = [],
purpose: StarsPurchasePurpose,
targetPeerId: EnginePeer.Id?,
completion: @escaping (Int64) -> Void = { _ in }
) {
self.context = context

View File

@ -180,7 +180,7 @@ final class StarsBalanceComponent: Component {
let formattedLabel: String
switch component.currency {
case .ton:
formattedLabel = formatTonAmountText(component.count.value, dateTimeFormat: component.dateTimeFormat)
formattedLabel = formatTonAmountText(component.count.value, dateTimeFormat: component.dateTimeFormat, maxDecimalPositions: 3)
case .stars:
formattedLabel = formatStarsAmountText(component.count, dateTimeFormat: component.dateTimeFormat)
}

View File

@ -910,7 +910,7 @@ public final class StarsStatisticsScreen: ViewControllerComponentContainer {
guard let self, let starsContext = context.starsContext else {
return
}
let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: options, purpose: .generic, completion: { [weak self] stars in
let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: options, purpose: .generic, targetPeerId: nil, completion: { [weak self] stars in
guard let self else {
return
}

View File

@ -1334,7 +1334,7 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer {
guard let self else {
return
}
let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: options, purpose: .generic, completion: { [weak self] stars in
let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: options, purpose: .generic, targetPeerId: nil, completion: { [weak self] stars in
guard let self else {
return
}
@ -1458,6 +1458,7 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer {
starsContext: starsContext,
options: options,
purpose: .gift(peerId: peerId),
targetPeerId: nil,
completion: { [weak self] stars in
guard let self else {
return

View File

@ -588,6 +588,7 @@ private final class SheetContent: CombinedComponent {
starsContext: starsContext,
options: state?.options ?? [],
purpose: purpose,
targetPeerId: nil,
completion: { [weak starsContext] stars in
guard let starsContext else {
return

View File

@ -466,7 +466,7 @@ private final class SheetContent: CombinedComponent {
case .ton:
if let value = state.amount?.value, value > 0 {
let tonValue = Int64(Float(value) * Float(resaleConfiguration.starGiftCommissionTonPermille) / 1000.0)
let tonString = formatTonAmountText(tonValue, dateTimeFormat: environment.dateTimeFormat, maxDecimalPositions: nil) + " TON"
let tonString = formatTonAmountText(tonValue, dateTimeFormat: environment.dateTimeFormat, maxDecimalPositions: 3) + " TON"
amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.Stars_SellGift_AmountInfo(tonString).string, attributes: amountMarkdownAttributes, textAlignment: .natural))
if let tonUsdRate = withdrawConfiguration.tonUsdRate {
@ -894,7 +894,7 @@ private final class SheetContent: CombinedComponent {
guard let controller, let state else {
return
}
let purchaseController = state.context.sharedContext.makeStarsPurchaseScreen(context: state.context, starsContext: starsContext, options: options, purpose: .generic, completion: { _ in
let purchaseController = state.context.sharedContext.makeStarsPurchaseScreen(context: state.context, starsContext: starsContext, options: options, purpose: .generic, targetPeerId: nil, completion: { _ in
})
controller.push(purchaseController)
})
@ -1230,7 +1230,13 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
var text = presentationData.strings.Stars_Withdraw_Withdraw_ErrorMinimum(presentationData.strings.Stars_Withdraw_Withdraw_ErrorMinimum_Stars(Int32(clamping: minAmount))).string
if case .starGiftResell = self.mode {
text = presentationData.strings.Stars_SellGiftMinAmountToast_Text("\(presentationData.strings.Stars_Withdraw_Withdraw_ErrorMinimum_Stars(Int32(clamping: minAmount)))").string
switch currency {
case .stars:
text = presentationData.strings.Stars_SellGiftMinAmountToast_Text("\(presentationData.strings.Stars_Withdraw_Withdraw_ErrorMinimum_Stars(Int32(clamping: minAmount)))").string
case .ton:
let amountString = formatTonAmountText(minAmount, dateTimeFormat: presentationData.dateTimeFormat) + " TON"
text = presentationData.strings.Stars_SellGiftMinAmountToast_Text(amountString).string
}
} else if case let .suggestedPost(mode, _, _, _) = self.mode {
let resaleConfiguration = StarsSubscriptionConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 })
switch currency {
@ -1253,7 +1259,7 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer {
let resultController = UndoOverlayController(
presentationData: presentationData,
content: .image(
image: UIImage(bundleImageName: "Premium/Stars/StarLarge")!,
image: currency == .ton ? generateTintedImage(image: UIImage(bundleImageName: "Premium/TonGift"), color: .white)! : UIImage(bundleImageName: "Premium/Stars/StarLarge")!,
title: nil,
text: text,
round: false,
@ -1314,9 +1320,9 @@ private final class AmountFieldStarsFormatter: NSObject, UITextFieldDelegate {
}
case .ton:
let scale: Int64 = 1_000_000_000 // 10 (one nano)
if let dot = text.firstIndex(of: ".") {
if let decimalSeparator = self.dateTimeFormat.decimalSeparator.first, let dot = text.firstIndex(of: decimalSeparator) {
// Slices for the parts on each side of the dot
var wholeSlice = String(text[..<dot])
var wholeSlice = String(text[..<dot])
if wholeSlice.isEmpty {
wholeSlice = "0"
}
@ -1356,7 +1362,7 @@ private final class AmountFieldStarsFormatter: NSObject, UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
var acceptZero = false
if self.minValue <= 0 {
if case .ton = self.currency, self.minValue < 1_000_000_000 {
acceptZero = true
}
@ -1367,7 +1373,7 @@ private final class AmountFieldStarsFormatter: NSObject, UITextFieldDelegate {
return false
default:
if case .ton = self.currency {
if c == "." {
if let decimalSeparator = self.dateTimeFormat.decimalSeparator.first, c == decimalSeparator {
return false
}
}
@ -1376,7 +1382,7 @@ private final class AmountFieldStarsFormatter: NSObject, UITextFieldDelegate {
}) {
return false
}
if newText.count(where: { $0 == "." }) > 1 {
if let decimalSeparator = self.dateTimeFormat.decimalSeparator.first, newText.count(where: { $0 == decimalSeparator }) > 1 {
return false
}
@ -1390,7 +1396,7 @@ private final class AmountFieldStarsFormatter: NSObject, UITextFieldDelegate {
}
case .ton:
var fixedText = false
if let index = newText.firstIndex(of: ".") {
if let decimalSeparator = self.dateTimeFormat.decimalSeparator.first, let index = newText.firstIndex(of: decimalSeparator) {
let fractionalString = newText[newText.index(after: index)...]
if fractionalString.count > 2 {
newText = String(newText[newText.startIndex ..< newText.index(index, offsetBy: 3)])
@ -1398,7 +1404,16 @@ private final class AmountFieldStarsFormatter: NSObject, UITextFieldDelegate {
}
}
if (newText == "0" && !acceptZero) || (newText.count > 1 && newText.hasPrefix("0") && !newText.hasPrefix("0.")) {
if newText == self.dateTimeFormat.decimalSeparator {
if !acceptZero {
newText.removeFirst()
} else {
newText = "0\(newText)"
}
fixedText = true
}
if (newText == "0" && !acceptZero) || (newText.count > 1 && newText.hasPrefix("0") && !newText.hasPrefix("0\(self.dateTimeFormat.decimalSeparator)")) {
newText.removeFirst()
fixedText = true
}
@ -1416,7 +1431,7 @@ private final class AmountFieldStarsFormatter: NSObject, UITextFieldDelegate {
case .stars:
textField.text = "\(self.maxValue)"
case .ton:
textField.text = "\(formatTonAmountText(self.maxValue, dateTimeFormat: PresentationDateTimeFormat(timeFormat: self.dateTimeFormat.timeFormat, dateFormat: self.dateTimeFormat.dateFormat, dateSeparator: "", dateSuffix: "", requiresFullYear: false, decimalSeparator: ".", groupingSeparator: ""), maxDecimalPositions: nil))"
textField.text = "\(formatTonAmountText(self.maxValue, dateTimeFormat: PresentationDateTimeFormat(timeFormat: self.dateTimeFormat.timeFormat, dateFormat: self.dateTimeFormat.dateFormat, dateSeparator: "", dateSuffix: "", requiresFullYear: false, decimalSeparator: self.dateTimeFormat.decimalSeparator, groupingSeparator: ""), maxDecimalPositions: nil))"
}
self.onTextChanged(text: self.textField.text ?? "")
self.animateError()
@ -1639,7 +1654,7 @@ private final class AmountFieldComponent: Component {
self.tonFormatter = nil
self.textField.delegate = self.starsFormatter
case .ton:
self.textField.keyboardType = .numbersAndPunctuation
self.textField.keyboardType = .decimalPad
if self.tonFormatter == nil {
self.tonFormatter = AmountFieldStarsFormatter(
textField: self.textField,

View File

@ -14,6 +14,7 @@ import AsyncDisplayKit
import StoryContainerScreen
import MultilineTextComponent
import HierarchyTrackingLayer
import EmojiStatusComponent
private func calculateCircleIntersection(center: CGPoint, otherCenter: CGPoint, radius: CGFloat) -> (point1Angle: CGFloat, point2Angle: CGFloat)? {
let distanceVector = CGPoint(x: otherCenter.x - center.x, y: otherCenter.y - center.y)
@ -484,6 +485,7 @@ public final class StoryPeerListItemComponent: Component {
private let extractedBackgroundView: UIImageView
private let button: HighlightTrackingButton
private let titleContainer: UIView
fileprivate var composeLayer: StoryComposeLayer?
fileprivate let avatarContent: PortalSourceView
@ -501,6 +503,7 @@ public final class StoryPeerListItemComponent: Component {
private let indicatorShapeSeenLayer: SimpleShapeLayer
private let indicatorShapeUnseenLayer: SimpleShapeLayer
private let title = ComponentView<Empty>()
private var verifiedIconView: ComponentHostView<Empty>?
private let composeTitle = ComponentView<Empty>()
private var component: StoryPeerListItemComponent?
@ -512,6 +515,9 @@ public final class StoryPeerListItemComponent: Component {
self.button = HighlightTrackingButton()
self.titleContainer = UIView()
self.titleContainer.isUserInteractionEnabled = false
self.extractedContainerNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
self.extractedBackgroundView = UIImageView()
@ -558,6 +564,8 @@ public final class StoryPeerListItemComponent: Component {
self.avatarContent.addSubview(self.avatarContainer)
self.button.addSubview(self.avatarContent)
self.button.addSubview(self.titleContainer)
self.avatarContent.layer.addSublayer(self.indicatorColorSeenLayer)
self.avatarContent.layer.addSublayer(self.indicatorColorUnseenLayer)
self.indicatorMaskSeenLayer.addSublayer(self.indicatorShapeSeenLayer)
@ -876,6 +884,18 @@ public final class StoryPeerListItemComponent: Component {
}
}
let iconContainerSize = CGSize(width: 12.0, height: 12.0)
let iconSpacing: CGFloat = 1.0
var currentVerifiedIconContent: EmojiStatusComponent.Content?
if component.peer.isVerified {
currentVerifiedIconContent = .verified(fillColor: component.theme.list.itemCheckColors.fillColor, foregroundColor: component.theme.list.itemCheckColors.foregroundColor, sizeType: .smaller)
}
var titleConstrainedSize = CGSize(width: availableSize.width + 12.0, height: 100.0)
if let _ = currentVerifiedIconContent {
titleConstrainedSize.width -= iconContainerSize.width + iconSpacing
}
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
@ -883,21 +903,64 @@ public final class StoryPeerListItemComponent: Component {
maximumNumberOfLines: 1
)),
environment: {},
containerSize: CGSize(width: availableSize.width + 12.0, height: 100.0)
containerSize: titleConstrainedSize
)
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5) + (effectiveWidth - availableSize.width) * 0.5, y: indicatorFrame.midY + (indicatorFrame.height * 0.5 + 2.0) * effectiveScale), size: titleSize)
var totalTitleWidth = titleSize.width
if let currentVerifiedIconContent {
let verifiedIconView: ComponentHostView<Empty>
if let current = self.verifiedIconView {
verifiedIconView = current
} else {
verifiedIconView = ComponentHostView<Empty>()
verifiedIconView.isUserInteractionEnabled = false
self.verifiedIconView = verifiedIconView
self.titleContainer.addSubview(verifiedIconView)
}
let verifiedIconComponent = EmojiStatusComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
content: currentVerifiedIconContent,
size: iconContainerSize,
isVisibleForAnimations: component.context.sharedContext.energyUsageSettings.loopEmoji,
action: nil
)
let iconSize = verifiedIconView.update(
transition: .immediate,
component: AnyComponent(verifiedIconComponent),
environment: {},
containerSize: iconContainerSize
)
totalTitleWidth += iconSize.width + iconSpacing
let verifiedIconFrame = CGRect(origin: CGPoint(x: totalTitleWidth - iconSize.width, y: UIScreenPixel), size: iconSize)
titleTransition.setFrame(view: verifiedIconView, frame: verifiedIconFrame)
} else if let verifiedIconView = self.verifiedIconView {
self.verifiedIconView = nil
verifiedIconView.removeFromSuperview()
}
let titleFrame = CGRect(origin: .zero, size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
titleView.isUserInteractionEnabled = false
self.button.addSubview(titleView)
self.titleContainer.addSubview(titleView)
}
titleTransition.setPosition(view: titleView, position: titleFrame.center)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
titleTransition.setScale(view: titleView, scale: effectiveScale)
titleTransition.setAlpha(view: titleView, alpha: component.expandedAlphaFraction)
}
let titleContainerSize = CGSize(width: totalTitleWidth, height: titleSize.height)
let titleContainerFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleContainerSize.width) * 0.5) + (effectiveWidth - availableSize.width) * 0.5, y: indicatorFrame.midY + (indicatorFrame.height * 0.5 + 2.0) * effectiveScale), size: titleContainerSize)
titleTransition.setPosition(view: self.titleContainer, position: titleContainerFrame.center)
self.titleContainer.bounds = CGRect(origin: CGPoint(), size: titleContainerFrame.size)
titleTransition.setScale(view: self.titleContainer, scale: effectiveScale)
titleTransition.setAlpha(view: self.titleContainer, alpha: component.expandedAlphaFraction)
if let ringAnimation = component.ringAnimation {
var progressTransition = transition
let progressLayer: StoryProgressLayer

View File

@ -4273,7 +4273,7 @@ extension ChatControllerImpl {
guard let self else {
return
}
let controller = self.context.sharedContext.makeStarsPurchaseScreen(context: self.context, starsContext: starsContext, options: options, purpose: .generic, completion: { _ in
let controller = self.context.sharedContext.makeStarsPurchaseScreen(context: self.context, starsContext: starsContext, options: options, purpose: .generic, targetPeerId: nil, completion: { _ in
})
self.push(controller)
})

View File

@ -462,7 +462,7 @@ extension ChatControllerImpl {
return
}
let purchaseScreen = strongSelf.context.sharedContext.makeStarsPurchaseScreen(context: strongSelf.context, starsContext: starsContext, options: options, purpose: .reactions(peerId: message.id.peerId, requiredStars: 1), completion: { result in
let purchaseScreen = strongSelf.context.sharedContext.makeStarsPurchaseScreen(context: strongSelf.context, starsContext: starsContext, options: options, purpose: .reactions(peerId: message.id.peerId, requiredStars: 1), targetPeerId: nil, completion: { result in
let _ = result
})
strongSelf.push(purchaseScreen)

View File

@ -76,7 +76,7 @@ extension ChatControllerImpl {
guard let self else {
return
}
let controller = self.context.sharedContext.makeStarsPurchaseScreen(context: self.context, starsContext: starsContext, options: options, purpose: .sendMessage(peerId: peer.id, requiredStars: totalAmount), completion: { stars in
let controller = self.context.sharedContext.makeStarsPurchaseScreen(context: self.context, starsContext: starsContext, options: options, purpose: .sendMessage(peerId: peer.id, requiredStars: totalAmount), targetPeerId: nil, completion: { stars in
starsContext.add(balance: StarsAmount(value: stars, nanos: 0))
let _ = (starsContext.onUpdate
|> deliverOnMainQueue).start(next: {

View File

@ -1806,7 +1806,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return
}
let purchaseScreen = strongSelf.context.sharedContext.makeStarsPurchaseScreen(context: strongSelf.context, starsContext: starsContext, options: options, purpose: .reactions(peerId: peerId, requiredStars: 1), completion: { result in
let purchaseScreen = strongSelf.context.sharedContext.makeStarsPurchaseScreen(context: strongSelf.context, starsContext: starsContext, options: options, purpose: .reactions(peerId: peerId, requiredStars: 1), targetPeerId: nil, completion: { result in
let _ = result
})
strongSelf.push(purchaseScreen)
@ -2436,7 +2436,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let strongSelf else {
return
}
let purchaseController = strongSelf.context.sharedContext.makeStarsPurchaseScreen(context: strongSelf.context, starsContext: starsContext, options: options, purpose: .generic, completion: { _ in
let purchaseController = strongSelf.context.sharedContext.makeStarsPurchaseScreen(context: strongSelf.context, starsContext: starsContext, options: options, purpose: .generic, targetPeerId: nil, completion: { _ in
})
strongSelf.push(purchaseController)
})

View File

@ -1918,10 +1918,15 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
if case .regular = layout.metrics.widthClass, layout.size.height == layout.deviceMetrics.screenSize.width {
displayMode = .aspectFit
} else if case .compact = layout.metrics.widthClass {
if layout.size.width < layout.size.height && layout.size.height < layout.deviceMetrics.screenSize.height {
wallpaperBounds.size = layout.deviceMetrics.screenSize
} else if layout.size.width > layout.size.height && layout.size.height < layout.deviceMetrics.screenSize.width {
wallpaperBounds.size = layout.deviceMetrics.screenSize
if layout.inSplitView {
displayMode = .aspectFit
} else if layout.inSlideOver {
switch layout.actualOrientation {
case .portrait:
wallpaperBounds.size = CGSize(width: layout.size.width, height: layout.deviceMetrics.screenSize.height)
case .landscape:
wallpaperBounds.size = CGSize(width: layout.size.width, height: layout.deviceMetrics.screenSize.width)
}
}
}
self.backgroundNode.updateLayout(size: wallpaperBounds.size, displayMode: displayMode, transition: transition)

View File

@ -1131,7 +1131,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
let sendGiftTitle: String
var isIncoming = message.effectivelyIncoming(context.account.peerId)
for media in message.media {
if let action = media as? TelegramMediaAction, case let .starGiftUnique(_, isUpgrade, _, _, _, _, _, _, _, _, _, _, _, _) = action.action {
if let action = media as? TelegramMediaAction, case let .starGiftUnique(_, isUpgrade, _, _, _, _, _, _, _, _, _, _, _, _, _) = action.action {
if isUpgrade && message.author?.id == context.account.peerId {
isIncoming = true
}

View File

@ -844,7 +844,7 @@ func openResolvedUrlImpl(
dismissInput()
if let starsContext = context.starsContext {
let proceed = {
let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: [], purpose: .topUp(requiredStars: amount, purpose: purpose), completion: { _ in })
let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: [], purpose: .topUp(requiredStars: amount, purpose: purpose), targetPeerId: nil, completion: { _ in })
if let navigationController = navigationController {
navigationController.pushViewController(controller, animated: true)
}

View File

@ -1137,7 +1137,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
}
if isInternetUrl {
if parsedUrl.host == "t.me" || parsedUrl.host == "telegram.me" {
if parsedUrl.host == "t.me" || parsedUrl.host == "telegram.me" || parsedUrl.host == "telegram.dog" {
handleInternalUrl(parsedUrl.absoluteString)
} else {
let settings = combineLatest(context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.webBrowserSettings, ApplicationSpecificSharedDataKeys.presentationPasscodeSettings]), context.sharedContext.accountManager.accessChallengeData())
@ -1159,11 +1159,6 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
return settings
}
// var isCompact = false
// if let metrics = navigationController?.validLayout?.metrics, case .compact = metrics.widthClass {
// isCompact = true
// }
let _ = (settings
|> deliverOnMainQueue).startStandalone(next: { settings in
var isTonSite = false
@ -1216,7 +1211,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
}
if parsedUrl.scheme == "http" || parsedUrl.scheme == "https" {
let nativeHosts = ["t.me", "telegram.me"]
let nativeHosts = ["t.me", "telegram.me", "telegram.dog"]
if let host = parsedUrl.host, nativeHosts.contains(host) {
continueHandling()
} else {

View File

@ -3287,6 +3287,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
starsContext: starsContext,
options: options ?? [],
purpose: .transferStarGift(requiredStars: transferStars),
targetPeerId: nil,
completion: { stars in
starsContext.add(balance: StarsAmount(value: stars, nanos: 0))
proceed(true)
@ -3693,8 +3694,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return StarsTransactionsScreen(context: context, starsContext: starsContext)
}
public func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [Any], purpose: StarsPurchasePurpose, completion: @escaping (Int64) -> Void) -> ViewController {
return StarsPurchaseScreen(context: context, starsContext: starsContext, options: options, purpose: purpose, completion: completion)
public func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [Any], purpose: StarsPurchasePurpose, targetPeerId: EnginePeer.Id?, completion: @escaping (Int64) -> Void) -> ViewController {
return StarsPurchaseScreen(context: context, starsContext: starsContext, options: options, purpose: purpose, targetPeerId: targetPeerId, completion: completion)
}
public func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController {
@ -3947,7 +3948,7 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation
forumTopicThread = thread
case .recommendedChannels:
switchToRecommendedChannels = true
case .gifts:
case .gifts, .upgradableGifts:
switchToGifts = true
case .groupsInCommon:
switchToGroupsInCommon = true
@ -3985,7 +3986,7 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation
forumTopicThread = thread
case .myProfile:
isMyProfile = true
case .gifts:
case .gifts, .upgradableGifts:
switchToGifts = true
case .myProfileGifts:
isMyProfile = true

View File

@ -1503,16 +1503,24 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou
let targetSize: CGSize = self.bounds.size
let containerSize: CGSize = rect.containerSize
let useAspectFit: Bool = false
let renderScale: CGFloat = useAspectFit
let isAspectFit: Bool = (displayMode == .aspectFit || displayMode == .halfAspectFill)
let renderScale: CGFloat = isAspectFit
? min(targetSize.width / containerSize.width, targetSize.height / containerSize.height)
: max(targetSize.width / containerSize.width, targetSize.height / containerSize.height)
let drawingSize = CGSize(width: containerSize.width * renderScale, height: containerSize.height * renderScale)
let offsetX = (targetSize.width - drawingSize.width) * 0.5
let offsetY = (targetSize.height - drawingSize.height) * 0.5
let offsetX: CGFloat
let offsetY: CGFloat
if isAspectFit {
offsetX = 0.0
offsetY = (targetSize.height - drawingSize.height) * 0.5
} else {
offsetX = (targetSize.width - drawingSize.width) * 0.5
offsetY = (targetSize.height - drawingSize.height) * 0.5
}
let onScreenCenter = CGPoint(x: offsetX + rect.center.x * renderScale, y: offsetY + rect.center.y * renderScale)

View File

@ -227,7 +227,7 @@ final class WebAppWebView: WKWebView {
}
func sendEvent(name: String, data: String?) {
let script = "window.TelegramGameProxy.receiveEvent(\"\(name)\", \(data ?? "null"))"
let script = "window.TelegramGameProxy && window.TelegramGameProxy.receiveEvent && window.TelegramGameProxy.receiveEvent(\"\(name)\", \(data ?? "null"))"
self.evaluateJavaScript(script, completionHandler: { _, _ in
})
}