mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-03 21:16:35 +00:00
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:
commit
0678d0ded0
@ -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";
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)"
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -439,6 +439,7 @@ public class ChatMessagePaymentAlertController: AlertController {
|
||||
starsContext: starsContext,
|
||||
options: options,
|
||||
purpose: .generic,
|
||||
targetPeerId: nil,
|
||||
completion: { _ in }
|
||||
)
|
||||
navigationController.pushViewController(controller)
|
||||
|
||||
@ -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
|
||||
})
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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",
|
||||
],
|
||||
)
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
})
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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)
|
||||
})
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user