mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 13:35:19 +00:00
Stars
This commit is contained in:
parent
8f6767c010
commit
c099ec3641
@ -1039,6 +1039,10 @@ public protocol SharedAccountContext: AnyObject {
|
||||
func makeMessagesStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, messageId: EngineMessage.Id) -> ViewController
|
||||
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: [StarsTopUpOption], peerId: EnginePeer.Id?, requiredStars: Int32?) -> ViewController
|
||||
func makeStarsTransferScreen(context: AccountContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>) -> ViewController
|
||||
|
||||
func makeDebugSettingsController(context: AccountContext?) -> ViewController?
|
||||
|
||||
func navigateToCurrentCall()
|
||||
|
@ -13,9 +13,9 @@ public final class BotCheckoutController: ViewController {
|
||||
case generic
|
||||
}
|
||||
|
||||
let form: BotPaymentForm
|
||||
let validatedFormInfo: BotPaymentValidatedFormInfo?
|
||||
let botPeer: EnginePeer?
|
||||
public let form: BotPaymentForm
|
||||
public let validatedFormInfo: BotPaymentValidatedFormInfo?
|
||||
public let botPeer: EnginePeer?
|
||||
|
||||
private init(
|
||||
form: BotPaymentForm,
|
||||
|
@ -43,7 +43,7 @@ public final class ConfettiView: UIView {
|
||||
|
||||
private var localTime: Float = 0.0
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
public init(frame: CGRect, customImage: UIImage? = nil) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.isUserInteractionEnabled = false
|
||||
@ -56,19 +56,25 @@ public final class ConfettiView: UIView {
|
||||
] as [UInt32]).map(UIColor.init(rgb:))
|
||||
let imageSize = CGSize(width: 8.0, height: 8.0)
|
||||
var images: [(CGImage, CGSize)] = []
|
||||
for imageType in 0 ..< 2 {
|
||||
if let customImage {
|
||||
for color in colors {
|
||||
if imageType == 0 {
|
||||
images.append((generateFilledCircleImage(diameter: imageSize.width, color: color)!.cgImage!, imageSize))
|
||||
} else {
|
||||
let spriteSize = CGSize(width: 2.0, height: 6.0)
|
||||
images.append((generateImage(spriteSize, opaque: false, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(color.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.width)))
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: size.height - size.width), size: CGSize(width: size.width, height: size.width)))
|
||||
context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.width / 2.0), size: CGSize(width: size.width, height: size.height - size.width)))
|
||||
})!.cgImage!, spriteSize))
|
||||
images.append((generateTintedImage(image: customImage, color: color)!.cgImage!, customImage.size))
|
||||
}
|
||||
} else {
|
||||
for imageType in 0 ..< 2 {
|
||||
for color in colors {
|
||||
if imageType == 0 {
|
||||
images.append((generateFilledCircleImage(diameter: imageSize.width, color: color)!.cgImage!, imageSize))
|
||||
} else {
|
||||
let spriteSize = CGSize(width: 2.0, height: 6.0)
|
||||
images.append((generateImage(spriteSize, opaque: false, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(color.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.width)))
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: size.height - size.width), size: CGSize(width: size.width, height: size.width)))
|
||||
context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.width / 2.0), size: CGSize(width: size.width, height: size.height - size.width)))
|
||||
})!.cgImage!, spriteSize))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -369,6 +369,9 @@ public func generateGradientTintedImage(image: UIImage?, colors: [UIColor], dire
|
||||
case .diagonal:
|
||||
start = CGPoint(x: 0.0, y: 0.0)
|
||||
end = CGPoint(x: imageRect.width, y: imageRect.height)
|
||||
case .mirroredDiagonal:
|
||||
start = CGPoint(x: imageRect.width, y: 0.0)
|
||||
end = CGPoint(x: 0.0, y: imageRect.height)
|
||||
}
|
||||
|
||||
context.drawLinearGradient(gradient, start: start, end: end, options: CGGradientDrawingOptions())
|
||||
@ -390,6 +393,7 @@ public enum GradientImageDirection {
|
||||
case vertical
|
||||
case horizontal
|
||||
case diagonal
|
||||
case mirroredDiagonal
|
||||
}
|
||||
|
||||
public func generateGradientImage(size: CGSize, scale: CGFloat = 0.0, colors: [UIColor], locations: [CGFloat], direction: GradientImageDirection = .vertical) -> UIImage? {
|
||||
@ -440,6 +444,9 @@ public func generateGradientFilledCircleImage(diameter: CGFloat, colors: NSArray
|
||||
case .diagonal:
|
||||
start = CGPoint(x: 0.0, y: 0.0)
|
||||
end = CGPoint(x: size.width, y: size.height)
|
||||
case .mirroredDiagonal:
|
||||
start = CGPoint(x: size.width, y: 0.0)
|
||||
end = CGPoint(x: 0.0, y: size.height)
|
||||
}
|
||||
|
||||
context.drawLinearGradient(gradient, start: start, end:end, options: CGGradientDrawingOptions())
|
||||
|
@ -119,6 +119,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/PremiumPeerShortcutComponent",
|
||||
"//submodules/TelegramUI/Components/EmojiActionIconComponent",
|
||||
"//submodules/TelegramUI/Components/ScrollComponent",
|
||||
"//submodules/TelegramUI/Components/Premium/PremiumStarComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
BIN
submodules/PremiumUI/Resources/star.scn
Normal file
BIN
submodules/PremiumUI/Resources/star.scn
Normal file
Binary file not shown.
@ -3,6 +3,7 @@ import UIKit
|
||||
import SceneKit
|
||||
import Display
|
||||
import AppBundle
|
||||
import PremiumStarComponent
|
||||
|
||||
private let sceneVersion: Int = 1
|
||||
|
||||
|
@ -3,6 +3,7 @@ import UIKit
|
||||
import SceneKit
|
||||
import Display
|
||||
import AppBundle
|
||||
import PremiumStarComponent
|
||||
|
||||
final class BadgeStarsView: UIView, PhoneDemoDecorationView {
|
||||
private let sceneView: SCNView
|
||||
|
@ -11,6 +11,7 @@ import AvatarNode
|
||||
import TelegramCore
|
||||
import MultilineTextComponent
|
||||
import TelegramPresentationData
|
||||
import PremiumStarComponent
|
||||
|
||||
private let sceneVersion: Int = 1
|
||||
|
||||
|
@ -498,7 +498,7 @@ final class BusinessPageComponent: CombinedComponent {
|
||||
let updateDismissOffset: (CGFloat) -> Void
|
||||
let updatedIsDisplaying: (Bool) -> Void
|
||||
|
||||
var resetScroll: ActionSlot<Void>?
|
||||
var resetScroll: ActionSlot<CGPoint?>?
|
||||
|
||||
var topContentOffset: CGFloat = 0.0
|
||||
var bottomContentOffset: CGFloat = 100.0 {
|
||||
@ -519,7 +519,7 @@ final class BusinessPageComponent: CombinedComponent {
|
||||
self.updatedIsDisplaying(self.isDisplaying)
|
||||
|
||||
if !self.isDisplaying {
|
||||
self.resetScroll?.invoke(Void())
|
||||
self.resetScroll?.invoke(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -566,7 +566,7 @@ final class BusinessPageComponent: CombinedComponent {
|
||||
let topSeparator = Child(Rectangle.self)
|
||||
let title = Child(MultilineTextComponent.self)
|
||||
|
||||
let resetScroll = ActionSlot<Void>()
|
||||
let resetScroll = ActionSlot<CGPoint?>()
|
||||
|
||||
return { context in
|
||||
let state = context.state
|
||||
|
@ -8,6 +8,7 @@ import ItemListUI
|
||||
import PresentationDataUtils
|
||||
import Markdown
|
||||
import ComponentFlow
|
||||
import PremiumStarComponent
|
||||
|
||||
final class CreateGiveawayHeaderItem: ItemListControllerHeaderItem {
|
||||
let theme: PresentationTheme
|
||||
|
@ -4,6 +4,7 @@ import SceneKit
|
||||
import Display
|
||||
import AppBundle
|
||||
import LegacyComponents
|
||||
import PremiumStarComponent
|
||||
|
||||
private let sceneVersion: Int = 1
|
||||
|
||||
|
@ -453,7 +453,7 @@ final class LimitsPageComponent: CombinedComponent {
|
||||
let updateDismissOffset: (CGFloat) -> Void
|
||||
let updatedIsDisplaying: (Bool) -> Void
|
||||
|
||||
var resetScroll: ActionSlot<Void>?
|
||||
var resetScroll: ActionSlot<CGPoint?>?
|
||||
|
||||
var topContentOffset: CGFloat = 0.0
|
||||
var bottomContentOffset: CGFloat = 100.0 {
|
||||
@ -474,7 +474,7 @@ final class LimitsPageComponent: CombinedComponent {
|
||||
self.updatedIsDisplaying(self.isDisplaying)
|
||||
|
||||
if !self.isDisplaying {
|
||||
self.resetScroll?.invoke(Void())
|
||||
self.resetScroll?.invoke(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -521,7 +521,7 @@ final class LimitsPageComponent: CombinedComponent {
|
||||
let topSeparator = Child(Rectangle.self)
|
||||
let title = Child(MultilineTextComponent.self)
|
||||
|
||||
let resetScroll = ActionSlot<Void>()
|
||||
let resetScroll = ActionSlot<CGPoint?>()
|
||||
|
||||
return { context in
|
||||
let state = context.state
|
||||
|
@ -7,6 +7,7 @@ import SceneKit
|
||||
import GZip
|
||||
import AppBundle
|
||||
import LegacyComponents
|
||||
import PremiumStarComponent
|
||||
|
||||
private let sceneVersion: Int = 3
|
||||
|
||||
|
@ -21,6 +21,7 @@ import TextFormat
|
||||
import TelegramStringFormatting
|
||||
import UndoUI
|
||||
import InvisibleInkDustNode
|
||||
import PremiumStarComponent
|
||||
|
||||
private final class PremiumGiftCodeSheetContent: CombinedComponent {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
@ -21,6 +21,7 @@ import TextFormat
|
||||
import UniversalMediaPlayer
|
||||
import InstantPageCache
|
||||
import ScrollComponent
|
||||
import PremiumStarComponent
|
||||
|
||||
extension PremiumGiftSource {
|
||||
var identifier: String? {
|
||||
|
@ -13,7 +13,6 @@ import SolidRoundedButtonComponent
|
||||
import MultilineTextComponent
|
||||
import MultilineTextWithEntitiesComponent
|
||||
import BundleIconComponent
|
||||
import SolidRoundedButtonComponent
|
||||
import BlurredBackgroundComponent
|
||||
import Markdown
|
||||
import InAppPurchaseManager
|
||||
@ -34,6 +33,7 @@ import EmojiStatusComponent
|
||||
import EntityKeyboard
|
||||
import EmojiActionIconComponent
|
||||
import ScrollComponent
|
||||
import PremiumStarComponent
|
||||
|
||||
public enum PremiumSource: Equatable {
|
||||
public static func == (lhs: PremiumSource, rhs: PremiumSource) -> Bool {
|
||||
|
@ -516,7 +516,7 @@ final class StoriesPageComponent: CombinedComponent {
|
||||
let updateDismissOffset: (CGFloat) -> Void
|
||||
let updatedIsDisplaying: (Bool) -> Void
|
||||
|
||||
var resetScroll: ActionSlot<Void>?
|
||||
var resetScroll: ActionSlot<CGPoint?>?
|
||||
|
||||
var topContentOffset: CGFloat = 0.0
|
||||
var bottomContentOffset: CGFloat = 100.0 {
|
||||
@ -537,7 +537,7 @@ final class StoriesPageComponent: CombinedComponent {
|
||||
self.updatedIsDisplaying(self.isDisplaying)
|
||||
|
||||
if !self.isDisplaying {
|
||||
self.resetScroll?.invoke(Void())
|
||||
self.resetScroll?.invoke(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -584,7 +584,7 @@ final class StoriesPageComponent: CombinedComponent {
|
||||
let topSeparator = Child(Rectangle.self)
|
||||
let title = Child(MultilineTextComponent.self)
|
||||
|
||||
let resetScroll = ActionSlot<Void>()
|
||||
let resetScroll = ActionSlot<CGPoint?>()
|
||||
|
||||
return { context in
|
||||
let state = context.state
|
||||
|
@ -4,6 +4,7 @@ import SceneKit
|
||||
import Display
|
||||
import AppBundle
|
||||
import SwiftSignalKit
|
||||
import PremiumStarComponent
|
||||
|
||||
private let sceneVersion: Int = 1
|
||||
|
||||
|
@ -16,6 +16,10 @@ private func generateLoupeIcon(color: UIColor) -> UIImage? {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: color)
|
||||
}
|
||||
|
||||
private func generateHashtagIcon(color: UIColor) -> UIImage? {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Hashtag"), color: color)
|
||||
}
|
||||
|
||||
private func generateClearIcon(color: UIColor) -> UIImage? {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: color)
|
||||
}
|
||||
@ -844,6 +848,12 @@ public enum SearchBarStyle {
|
||||
}
|
||||
|
||||
public class SearchBarNode: ASDisplayNode, UITextFieldDelegate {
|
||||
public enum Icon {
|
||||
case loupe
|
||||
case hashtag
|
||||
}
|
||||
public let icon: Icon
|
||||
|
||||
public var cancel: (() -> Void)?
|
||||
public var textUpdated: ((String, String?) -> Void)?
|
||||
public var textReturned: ((String) -> Void)?
|
||||
@ -947,10 +957,11 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate {
|
||||
private var strings: PresentationStrings?
|
||||
private let cancelText: String?
|
||||
|
||||
public init(theme: SearchBarNodeTheme, strings: PresentationStrings, fieldStyle: SearchBarStyle = .legacy, forceSeparator: Bool = false, displayBackground: Bool = true, cancelText: String? = nil) {
|
||||
public init(theme: SearchBarNodeTheme, strings: PresentationStrings, fieldStyle: SearchBarStyle = .legacy, icon: Icon = .loupe, forceSeparator: Bool = false, displayBackground: Bool = true, cancelText: String? = nil) {
|
||||
self.fieldStyle = fieldStyle
|
||||
self.forceSeparator = forceSeparator
|
||||
self.cancelText = cancelText
|
||||
self.icon = icon
|
||||
|
||||
self.backgroundNode = NavigationBackgroundNode(color: theme.background)
|
||||
self.backgroundNode.isUserInteractionEnabled = false
|
||||
@ -1036,7 +1047,14 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate {
|
||||
self.textBackgroundNode.backgroundColor = theme.inputFill
|
||||
self.textField.textColor = theme.primaryText
|
||||
self.clearButton.setImage(generateClearIcon(color: theme.inputClear), for: [])
|
||||
self.iconNode.image = generateLoupeIcon(color: theme.inputIcon)
|
||||
let icon: UIImage?
|
||||
switch self.icon {
|
||||
case .loupe:
|
||||
icon = generateLoupeIcon(color: theme.inputIcon)
|
||||
case .hashtag:
|
||||
icon = generateHashtagIcon(color: theme.inputIcon)
|
||||
}
|
||||
self.iconNode.image = icon
|
||||
self.textField.keyboardAppearance = theme.keyboard.keyboardAppearance
|
||||
self.textField.tintColor = theme.accent
|
||||
|
||||
|
@ -280,6 +280,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[1103040667] = { return Api.ExportedContactToken.parse_exportedContactToken($0) }
|
||||
dict[1571494644] = { return Api.ExportedMessageLink.parse_exportedMessageLink($0) }
|
||||
dict[1070138683] = { return Api.ExportedStoryLink.parse_exportedStoryLink($0) }
|
||||
dict[-1197736753] = { return Api.FactCheck.parse_factCheck($0) }
|
||||
dict[-207944868] = { return Api.FileHash.parse_fileHash($0) }
|
||||
dict[-11252123] = { return Api.Folder.parse_folder($0) }
|
||||
dict[-373643672] = { return Api.FolderPeer.parse_folderPeer($0) }
|
||||
@ -515,7 +516,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[340088945] = { return Api.MediaArea.parse_mediaAreaSuggestedReaction($0) }
|
||||
dict[-1098720356] = { return Api.MediaArea.parse_mediaAreaVenue($0) }
|
||||
dict[64088654] = { return Api.MediaAreaCoordinates.parse_mediaAreaCoordinates($0) }
|
||||
dict[-1109353426] = { return Api.Message.parse_message($0) }
|
||||
dict[-1808510398] = { return Api.Message.parse_message($0) }
|
||||
dict[-1868117372] = { return Api.Message.parse_messageEmpty($0) }
|
||||
dict[721967202] = { return Api.Message.parse_messageService($0) }
|
||||
dict[-872240531] = { return Api.MessageAction.parse_messageActionBoostApply($0) }
|
||||
@ -1604,6 +1605,8 @@ public extension Api {
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.ExportedStoryLink:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.FactCheck:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.FileHash:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.Folder:
|
||||
|
@ -316,15 +316,15 @@ public extension Api {
|
||||
}
|
||||
public extension Api {
|
||||
indirect enum Message: TypeConstructorDescription {
|
||||
case message(flags: Int32, flags2: Int32, id: Int32, fromId: Api.Peer?, fromBoostsApplied: Int32?, peerId: Api.Peer, savedPeerId: Api.Peer?, fwdFrom: Api.MessageFwdHeader?, viaBotId: Int64?, viaBusinessBotId: Int64?, replyTo: Api.MessageReplyHeader?, date: Int32, message: String, media: Api.MessageMedia?, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?, views: Int32?, forwards: Int32?, replies: Api.MessageReplies?, editDate: Int32?, postAuthor: String?, groupedId: Int64?, reactions: Api.MessageReactions?, restrictionReason: [Api.RestrictionReason]?, ttlPeriod: Int32?, quickReplyShortcutId: Int32?, effect: Int64?)
|
||||
case message(flags: Int32, flags2: Int32, id: Int32, fromId: Api.Peer?, fromBoostsApplied: Int32?, peerId: Api.Peer, savedPeerId: Api.Peer?, fwdFrom: Api.MessageFwdHeader?, viaBotId: Int64?, viaBusinessBotId: Int64?, replyTo: Api.MessageReplyHeader?, date: Int32, message: String, media: Api.MessageMedia?, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?, views: Int32?, forwards: Int32?, replies: Api.MessageReplies?, editDate: Int32?, postAuthor: String?, groupedId: Int64?, reactions: Api.MessageReactions?, restrictionReason: [Api.RestrictionReason]?, ttlPeriod: Int32?, quickReplyShortcutId: Int32?, effect: Int64?, factcheck: Api.FactCheck?)
|
||||
case messageEmpty(flags: Int32, id: Int32, peerId: Api.Peer?)
|
||||
case messageService(flags: Int32, id: Int32, fromId: Api.Peer?, peerId: Api.Peer, replyTo: Api.MessageReplyHeader?, date: Int32, action: Api.MessageAction, ttlPeriod: Int32?)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .message(let flags, let flags2, let id, let fromId, let fromBoostsApplied, let peerId, let savedPeerId, let fwdFrom, let viaBotId, let viaBusinessBotId, let replyTo, let date, let message, let media, let replyMarkup, let entities, let views, let forwards, let replies, let editDate, let postAuthor, let groupedId, let reactions, let restrictionReason, let ttlPeriod, let quickReplyShortcutId, let effect):
|
||||
case .message(let flags, let flags2, let id, let fromId, let fromBoostsApplied, let peerId, let savedPeerId, let fwdFrom, let viaBotId, let viaBusinessBotId, let replyTo, let date, let message, let media, let replyMarkup, let entities, let views, let forwards, let replies, let editDate, let postAuthor, let groupedId, let reactions, let restrictionReason, let ttlPeriod, let quickReplyShortcutId, let effect, let factcheck):
|
||||
if boxed {
|
||||
buffer.appendInt32(-1109353426)
|
||||
buffer.appendInt32(-1808510398)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
serializeInt32(flags2, buffer: buffer, boxed: false)
|
||||
@ -361,6 +361,7 @@ public extension Api {
|
||||
if Int(flags) & Int(1 << 25) != 0 {serializeInt32(ttlPeriod!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 30) != 0 {serializeInt32(quickReplyShortcutId!, buffer: buffer, boxed: false)}
|
||||
if Int(flags2) & Int(1 << 2) != 0 {serializeInt64(effect!, buffer: buffer, boxed: false)}
|
||||
if Int(flags2) & Int(1 << 3) != 0 {factcheck!.serialize(buffer, true)}
|
||||
break
|
||||
case .messageEmpty(let flags, let id, let peerId):
|
||||
if boxed {
|
||||
@ -388,8 +389,8 @@ public extension Api {
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .message(let flags, let flags2, let id, let fromId, let fromBoostsApplied, let peerId, let savedPeerId, let fwdFrom, let viaBotId, let viaBusinessBotId, let replyTo, let date, let message, let media, let replyMarkup, let entities, let views, let forwards, let replies, let editDate, let postAuthor, let groupedId, let reactions, let restrictionReason, let ttlPeriod, let quickReplyShortcutId, let effect):
|
||||
return ("message", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("fromId", fromId as Any), ("fromBoostsApplied", fromBoostsApplied as Any), ("peerId", peerId as Any), ("savedPeerId", savedPeerId as Any), ("fwdFrom", fwdFrom as Any), ("viaBotId", viaBotId as Any), ("viaBusinessBotId", viaBusinessBotId as Any), ("replyTo", replyTo as Any), ("date", date as Any), ("message", message as Any), ("media", media as Any), ("replyMarkup", replyMarkup as Any), ("entities", entities as Any), ("views", views as Any), ("forwards", forwards as Any), ("replies", replies as Any), ("editDate", editDate as Any), ("postAuthor", postAuthor as Any), ("groupedId", groupedId as Any), ("reactions", reactions as Any), ("restrictionReason", restrictionReason as Any), ("ttlPeriod", ttlPeriod as Any), ("quickReplyShortcutId", quickReplyShortcutId as Any), ("effect", effect as Any)])
|
||||
case .message(let flags, let flags2, let id, let fromId, let fromBoostsApplied, let peerId, let savedPeerId, let fwdFrom, let viaBotId, let viaBusinessBotId, let replyTo, let date, let message, let media, let replyMarkup, let entities, let views, let forwards, let replies, let editDate, let postAuthor, let groupedId, let reactions, let restrictionReason, let ttlPeriod, let quickReplyShortcutId, let effect, let factcheck):
|
||||
return ("message", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("fromId", fromId as Any), ("fromBoostsApplied", fromBoostsApplied as Any), ("peerId", peerId as Any), ("savedPeerId", savedPeerId as Any), ("fwdFrom", fwdFrom as Any), ("viaBotId", viaBotId as Any), ("viaBusinessBotId", viaBusinessBotId as Any), ("replyTo", replyTo as Any), ("date", date as Any), ("message", message as Any), ("media", media as Any), ("replyMarkup", replyMarkup as Any), ("entities", entities as Any), ("views", views as Any), ("forwards", forwards as Any), ("replies", replies as Any), ("editDate", editDate as Any), ("postAuthor", postAuthor as Any), ("groupedId", groupedId as Any), ("reactions", reactions as Any), ("restrictionReason", restrictionReason as Any), ("ttlPeriod", ttlPeriod as Any), ("quickReplyShortcutId", quickReplyShortcutId as Any), ("effect", effect as Any), ("factcheck", factcheck as Any)])
|
||||
case .messageEmpty(let flags, let id, let peerId):
|
||||
return ("messageEmpty", [("flags", flags as Any), ("id", id as Any), ("peerId", peerId as Any)])
|
||||
case .messageService(let flags, let id, let fromId, let peerId, let replyTo, let date, let action, let ttlPeriod):
|
||||
@ -474,6 +475,10 @@ public extension Api {
|
||||
if Int(_1!) & Int(1 << 30) != 0 {_26 = reader.readInt32() }
|
||||
var _27: Int64?
|
||||
if Int(_2!) & Int(1 << 2) != 0 {_27 = reader.readInt64() }
|
||||
var _28: Api.FactCheck?
|
||||
if Int(_2!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() {
|
||||
_28 = Api.parse(reader, signature: signature) as? Api.FactCheck
|
||||
} }
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
@ -501,8 +506,9 @@ public extension Api {
|
||||
let _c25 = (Int(_1!) & Int(1 << 25) == 0) || _25 != nil
|
||||
let _c26 = (Int(_1!) & Int(1 << 30) == 0) || _26 != nil
|
||||
let _c27 = (Int(_2!) & Int(1 << 2) == 0) || _27 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 && _c23 && _c24 && _c25 && _c26 && _c27 {
|
||||
return Api.Message.message(flags: _1!, flags2: _2!, id: _3!, fromId: _4, fromBoostsApplied: _5, peerId: _6!, savedPeerId: _7, fwdFrom: _8, viaBotId: _9, viaBusinessBotId: _10, replyTo: _11, date: _12!, message: _13!, media: _14, replyMarkup: _15, entities: _16, views: _17, forwards: _18, replies: _19, editDate: _20, postAuthor: _21, groupedId: _22, reactions: _23, restrictionReason: _24, ttlPeriod: _25, quickReplyShortcutId: _26, effect: _27)
|
||||
let _c28 = (Int(_2!) & Int(1 << 3) == 0) || _28 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 && _c23 && _c24 && _c25 && _c26 && _c27 && _c28 {
|
||||
return Api.Message.message(flags: _1!, flags2: _2!, id: _3!, fromId: _4, fromBoostsApplied: _5, peerId: _6!, savedPeerId: _7, fwdFrom: _8, viaBotId: _9, viaBusinessBotId: _10, replyTo: _11, date: _12!, message: _13!, media: _14, replyMarkup: _15, entities: _16, views: _17, forwards: _18, replies: _19, editDate: _20, postAuthor: _21, groupedId: _22, reactions: _23, restrictionReason: _24, ttlPeriod: _25, quickReplyShortcutId: _26, effect: _27, factcheck: _28)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
|
@ -4952,6 +4952,22 @@ public extension Api.functions.messages {
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.messages {
|
||||
static func deleteFactCheck(peer: Api.InputPeer, msgId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(-774204404)
|
||||
peer.serialize(buffer, true)
|
||||
serializeInt32(msgId, buffer: buffer, boxed: false)
|
||||
return (FunctionDescription(name: "messages.deleteFactCheck", parameters: [("peer", String(describing: peer)), ("msgId", String(describing: msgId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.Updates?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.Updates
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.messages {
|
||||
static func deleteHistory(flags: Int32, peer: Api.InputPeer, maxId: Int32, minDate: Int32?, maxDate: Int32?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.AffectedHistory>) {
|
||||
let buffer = Buffer()
|
||||
@ -5214,6 +5230,23 @@ public extension Api.functions.messages {
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.messages {
|
||||
static func editFactCheck(peer: Api.InputPeer, msgId: Int32, text: Api.TextWithEntities) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(92925557)
|
||||
peer.serialize(buffer, true)
|
||||
serializeInt32(msgId, buffer: buffer, boxed: false)
|
||||
text.serialize(buffer, true)
|
||||
return (FunctionDescription(name: "messages.editFactCheck", parameters: [("peer", String(describing: peer)), ("msgId", String(describing: msgId)), ("text", String(describing: text))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.Updates?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.Updates
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.messages {
|
||||
static func editInlineBotMessage(flags: Int32, id: Api.InputBotInlineMessageID, message: String?, media: Api.InputMedia?, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||
let buffer = Buffer()
|
||||
@ -5921,6 +5954,26 @@ public extension Api.functions.messages {
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.messages {
|
||||
static func getFactCheck(peer: Api.InputPeer, msgId: [Int32]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Api.FactCheck]>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(-1177696786)
|
||||
peer.serialize(buffer, true)
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(msgId.count))
|
||||
for item in msgId {
|
||||
serializeInt32(item, buffer: buffer, boxed: false)
|
||||
}
|
||||
return (FunctionDescription(name: "messages.getFactCheck", parameters: [("peer", String(describing: peer)), ("msgId", String(describing: msgId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> [Api.FactCheck]? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: [Api.FactCheck]?
|
||||
if let _ = reader.readInt32() {
|
||||
result = Api.parseVector(reader, elementSignature: 0, elementType: Api.FactCheck.self)
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.messages {
|
||||
static func getFavedStickers(hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.FavedStickers>) {
|
||||
let buffer = Buffer()
|
||||
|
@ -168,6 +168,56 @@ public extension Api {
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api {
|
||||
enum FactCheck: TypeConstructorDescription {
|
||||
case factCheck(flags: Int32, country: String?, text: Api.TextWithEntities?, hash: Int64)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .factCheck(let flags, let country, let text, let hash):
|
||||
if boxed {
|
||||
buffer.appendInt32(-1197736753)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 1) != 0 {serializeString(country!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 1) != 0 {text!.serialize(buffer, true)}
|
||||
serializeInt64(hash, buffer: buffer, boxed: false)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .factCheck(let flags, let country, let text, let hash):
|
||||
return ("factCheck", [("flags", flags as Any), ("country", country as Any), ("text", text as Any), ("hash", hash as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_factCheck(_ reader: BufferReader) -> FactCheck? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
var _2: String?
|
||||
if Int(_1!) & Int(1 << 1) != 0 {_2 = parseString(reader) }
|
||||
var _3: Api.TextWithEntities?
|
||||
if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() {
|
||||
_3 = Api.parse(reader, signature: signature) as? Api.TextWithEntities
|
||||
} }
|
||||
var _4: Int64?
|
||||
_4 = reader.readInt64()
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil
|
||||
let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil
|
||||
let _c4 = _4 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 {
|
||||
return Api.FactCheck.factCheck(flags: _1!, country: _2, text: _3, hash: _4!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api {
|
||||
enum FileHash: TypeConstructorDescription {
|
||||
case fileHash(offset: Int64, limit: Int32, hash: Buffer)
|
||||
|
@ -126,7 +126,7 @@ public func tagsForStoreMessage(incoming: Bool, attributes: [MessageAttribute],
|
||||
|
||||
func apiMessagePeerId(_ messsage: Api.Message) -> PeerId? {
|
||||
switch messsage {
|
||||
case let .message(_, _, _, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
|
||||
case let .message(_, _, _, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
|
||||
let chatPeerId = messagePeerId
|
||||
return chatPeerId.peerId
|
||||
case let .messageEmpty(_, _, peerId):
|
||||
@ -142,7 +142,7 @@ func apiMessagePeerId(_ messsage: Api.Message) -> PeerId? {
|
||||
|
||||
func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] {
|
||||
switch message {
|
||||
case let .message(_, _, _, fromId, _, chatPeerId, savedPeerId, fwdHeader, viaBotId, viaBusinessBotId, replyTo, _, _, media, _, entities, _, _, _, _, _, _, _, _, _, _, _):
|
||||
case let .message(_, _, _, fromId, _, chatPeerId, savedPeerId, fwdHeader, viaBotId, viaBusinessBotId, replyTo, _, _, media, _, entities, _, _, _, _, _, _, _, _, _, _, _, _):
|
||||
let peerId: PeerId = chatPeerId.peerId
|
||||
|
||||
var result = [peerId]
|
||||
@ -266,7 +266,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] {
|
||||
|
||||
func apiMessageAssociatedMessageIds(_ message: Api.Message) -> (replyIds: ReferencedReplyMessageIds, generalIds: [MessageId])? {
|
||||
switch message {
|
||||
case let .message(_, _, id, _, _, chatPeerId, _, _, _, _, replyTo, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
|
||||
case let .message(_, _, id, _, _, chatPeerId, _, _, _, _, replyTo, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
|
||||
if let replyTo = replyTo {
|
||||
let peerId: PeerId = chatPeerId.peerId
|
||||
|
||||
@ -609,7 +609,7 @@ func messageTextEntitiesFromApiEntities(_ entities: [Api.MessageEntity]) -> [Mes
|
||||
extension StoreMessage {
|
||||
convenience init?(apiMessage: Api.Message, accountPeerId: PeerId, peerIsForum: Bool, namespace: MessageId.Namespace = Namespaces.Message.Cloud) {
|
||||
switch apiMessage {
|
||||
case let .message(flags, flags2, id, fromId, boosts, chatPeerId, savedPeerId, fwdFrom, viaBotId, viaBusinessBotId, replyTo, date, message, media, replyMarkup, entities, views, forwards, replies, editDate, postAuthor, groupingId, reactions, restrictionReason, ttlPeriod, quickReplyShortcutId, messageEffectId):
|
||||
case let .message(flags, flags2, id, fromId, boosts, chatPeerId, savedPeerId, fwdFrom, viaBotId, viaBusinessBotId, replyTo, date, message, media, replyMarkup, entities, views, forwards, replies, editDate, postAuthor, groupingId, reactions, restrictionReason, ttlPeriod, quickReplyShortcutId, messageEffectId, _):
|
||||
let resolvedFromId = fromId?.peerId ?? chatPeerId.peerId
|
||||
|
||||
var namespace = namespace
|
||||
|
@ -96,7 +96,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes
|
||||
var updatedTimestamp: Int32?
|
||||
if let apiMessage = apiMessage {
|
||||
switch apiMessage {
|
||||
case let .message(_, _, _, _, _, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
|
||||
case let .message(_, _, _, _, _, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
|
||||
updatedTimestamp = date
|
||||
case .messageEmpty:
|
||||
break
|
||||
|
@ -58,7 +58,7 @@ class UpdateMessageService: NSObject, MTMessageService {
|
||||
self.putNext(groups)
|
||||
}
|
||||
case let .updateShortChatMessage(flags, id, fromId, chatId, message, pts, ptsCount, date, fwdFrom, viaBotId, replyHeader, entities, ttlPeriod):
|
||||
let generatedMessage = Api.Message.message(flags: flags, flags2: 0, id: id, fromId: .peerUser(userId: fromId), fromBoostsApplied: nil, peerId: Api.Peer.peerChat(chatId: chatId), savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, viaBusinessBotId: nil, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod, quickReplyShortcutId: nil, effect: nil)
|
||||
let generatedMessage = Api.Message.message(flags: flags, flags2: 0, id: id, fromId: .peerUser(userId: fromId), fromBoostsApplied: nil, peerId: Api.Peer.peerChat(chatId: chatId), savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, viaBusinessBotId: nil, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod, quickReplyShortcutId: nil, effect: nil, factcheck: nil)
|
||||
let update = Api.Update.updateNewMessage(message: generatedMessage, pts: pts, ptsCount: ptsCount)
|
||||
let groups = groupUpdates([update], users: [], chats: [], date: date, seqRange: nil)
|
||||
if groups.count != 0 {
|
||||
@ -74,7 +74,7 @@ class UpdateMessageService: NSObject, MTMessageService {
|
||||
|
||||
let generatedPeerId = Api.Peer.peerUser(userId: userId)
|
||||
|
||||
let generatedMessage = Api.Message.message(flags: flags, flags2: 0, id: id, fromId: generatedFromId, fromBoostsApplied: nil, peerId: generatedPeerId, savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, viaBusinessBotId: nil, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod, quickReplyShortcutId: nil, effect: nil)
|
||||
let generatedMessage = Api.Message.message(flags: flags, flags2: 0, id: id, fromId: generatedFromId, fromBoostsApplied: nil, peerId: generatedPeerId, savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, viaBusinessBotId: nil, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod, quickReplyShortcutId: nil, effect: nil, factcheck: nil)
|
||||
let update = Api.Update.updateNewMessage(message: generatedMessage, pts: pts, ptsCount: ptsCount)
|
||||
let groups = groupUpdates([update], users: [], chats: [], date: date, seqRange: nil)
|
||||
if groups.count != 0 {
|
||||
|
@ -104,7 +104,7 @@ extension Api.MessageMedia {
|
||||
extension Api.Message {
|
||||
var rawId: Int32 {
|
||||
switch self {
|
||||
case let .message(_, _, id, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
|
||||
case let .message(_, _, id, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
|
||||
return id
|
||||
case let .messageEmpty(_, id, _):
|
||||
return id
|
||||
@ -115,7 +115,7 @@ extension Api.Message {
|
||||
|
||||
func id(namespace: MessageId.Namespace = Namespaces.Message.Cloud) -> MessageId? {
|
||||
switch self {
|
||||
case let .message(_, _, id, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
|
||||
case let .message(_, _, id, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
|
||||
let peerId: PeerId = messagePeerId.peerId
|
||||
return MessageId(peerId: peerId, namespace: namespace, id: id)
|
||||
case let .messageEmpty(_, id, peerId):
|
||||
@ -132,7 +132,7 @@ extension Api.Message {
|
||||
|
||||
var peerId: PeerId? {
|
||||
switch self {
|
||||
case let .message(_, _, _, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
|
||||
case let .message(_, _, _, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
|
||||
let peerId: PeerId = messagePeerId.peerId
|
||||
return peerId
|
||||
case let .messageEmpty(_, _, peerId):
|
||||
@ -145,7 +145,7 @@ extension Api.Message {
|
||||
|
||||
var timestamp: Int32? {
|
||||
switch self {
|
||||
case let .message(_, _, _, _, _, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
|
||||
case let .message(_, _, _, _, _, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
|
||||
return date
|
||||
case let .messageService(_, _, _, _, _, date, _, _):
|
||||
return date
|
||||
@ -156,7 +156,7 @@ extension Api.Message {
|
||||
|
||||
var preCachedResources: [(MediaResource, Data)]? {
|
||||
switch self {
|
||||
case let .message(_, _, _, _, _, _, _, _, _, _, _, _, _, media, _, _, _, _, _, _, _, _, _, _, _, _, _):
|
||||
case let .message(_, _, _, _, _, _, _, _, _, _, _, _, _, media, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
|
||||
return media?.preCachedResources
|
||||
default:
|
||||
return nil
|
||||
@ -165,7 +165,7 @@ extension Api.Message {
|
||||
|
||||
var preCachedStories: [StoryId: Api.StoryItem]? {
|
||||
switch self {
|
||||
case let .message(_, _, _, _, _, _, _, _, _, _, _, _, _, media, _, _, _, _, _, _, _, _, _, _, _, _, _):
|
||||
case let .message(_, _, _, _, _, _, _, _, _, _, _, _, _, media, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
|
||||
return media?.preCachedStories
|
||||
default:
|
||||
return nil
|
||||
|
@ -66,13 +66,13 @@ func _internal_starsTopUpOptions(account: Account) -> Signal<[StarsTopUpOption],
|
||||
}
|
||||
}
|
||||
|
||||
private struct InternalStarsStatus {
|
||||
struct InternalStarsStatus {
|
||||
let balance: Int64
|
||||
let transactions: [StarsContext.State.Transaction]
|
||||
let nextOffset: String?
|
||||
}
|
||||
|
||||
private func requestStarsState(account: Account, peerId: EnginePeer.Id, offset: String?) -> Signal<InternalStarsStatus?, NoError> {
|
||||
func _internal_requestStarsState(account: Account, peerId: EnginePeer.Id, offset: String?) -> Signal<InternalStarsStatus?, NoError> {
|
||||
return account.postbox.transaction { transaction -> Peer? in
|
||||
return transaction.getPeer(peerId)
|
||||
} |> mapToSignal { peer -> Signal<InternalStarsStatus?, NoError> in
|
||||
@ -152,6 +152,7 @@ private final class StarsContextImpl {
|
||||
return
|
||||
}
|
||||
self._state = StarsContext.State(balance: balance, transactions: state.transactions)
|
||||
self.load()
|
||||
})
|
||||
}
|
||||
|
||||
@ -164,12 +165,14 @@ private final class StarsContextImpl {
|
||||
func load() {
|
||||
assert(Queue.mainQueue().isCurrent())
|
||||
|
||||
self.disposable.set((requestStarsState(account: self.account, peerId: self.peerId, offset: nil)
|
||||
self.disposable.set((_internal_requestStarsState(account: self.account, peerId: self.peerId, offset: nil)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] status in
|
||||
if let self {
|
||||
if let status {
|
||||
self._state = StarsContext.State(balance: status.balance, transactions: status.transactions)
|
||||
self.nextOffset = status.nextOffset
|
||||
|
||||
self.loadMore()
|
||||
} else {
|
||||
self._state = nil
|
||||
}
|
||||
@ -183,7 +186,7 @@ private final class StarsContextImpl {
|
||||
guard let currentState = self._state, let nextOffset = self.nextOffset else {
|
||||
return
|
||||
}
|
||||
self.disposable.set((requestStarsState(account: self.account, peerId: self.peerId, offset: nextOffset)
|
||||
self.disposable.set((_internal_requestStarsState(account: self.account, peerId: self.peerId, offset: nextOffset)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] status in
|
||||
if let self {
|
||||
if let status {
|
||||
|
@ -74,6 +74,16 @@ public extension TelegramEngine {
|
||||
return StarsContext(account: self.account, peerId: peerId)
|
||||
}
|
||||
|
||||
public func peerStarsState(peerId: EnginePeer.Id) -> Signal<StarsContext.State?, NoError> {
|
||||
return _internal_requestStarsState(account: self.account, peerId: peerId, offset: nil)
|
||||
|> map { state in
|
||||
guard let state else {
|
||||
return nil
|
||||
}
|
||||
return StarsContext.State(balance: state.balance, transactions: state.transactions)
|
||||
}
|
||||
}
|
||||
|
||||
public func sendStarsPaymentForm(formId: Int64, source: BotPaymentInvoiceSource) -> Signal<SendBotPaymentResult, SendBotPaymentFormError> {
|
||||
return _internal_sendStarsPaymentForm(account: self.account, formId: formId, source: source)
|
||||
}
|
||||
|
@ -375,6 +375,10 @@ public extension Message {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isAgeRestricted() -> Bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public extension Message {
|
||||
|
@ -109,6 +109,30 @@ public struct PresentationResourcesSettings {
|
||||
|
||||
drawBorder(context: context, rect: bounds)
|
||||
})
|
||||
|
||||
public static let stars = generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
context.clear(bounds)
|
||||
|
||||
let path = UIBezierPath(roundedRect: bounds, cornerRadius: 7.0)
|
||||
context.addPath(path.cgPath)
|
||||
context.clip()
|
||||
|
||||
let colorsArray: [CGColor] = [
|
||||
UIColor(rgb: 0xfec80f).cgColor,
|
||||
UIColor(rgb: 0xdd6f12).cgColor
|
||||
]
|
||||
var locations: [CGFloat] = [0.0, 1.0]
|
||||
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)!
|
||||
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: size.height), options: CGGradientDrawingOptions())
|
||||
|
||||
if let image = generateTintedImage(image: UIImage(bundleImageName: "Premium/ButtonIcon"), color: UIColor(rgb: 0xffffff)), let cgImage = image.cgImage {
|
||||
context.draw(cgImage, in: CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - image.size.width) / 2.0), y: floorToScreenPixels((bounds.height - image.size.height) / 2.0)), size: image.size))
|
||||
}
|
||||
|
||||
drawBorder(context: context, rect: bounds)
|
||||
})
|
||||
|
||||
public static let passport = renderIcon(name: "Settings/Menu/Passport")
|
||||
public static let watch = renderIcon(name: "Settings/Menu/Watch")
|
||||
|
@ -547,7 +547,18 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
||||
|
||||
range = (mutableString.string as NSString).range(of: "{amount}")
|
||||
if range.location != NSNotFound {
|
||||
mutableString.replaceCharacters(in: range, with: NSAttributedString(string: formatCurrencyAmount(totalAmount, currency: currency), font: titleBoldFont, textColor: primaryTextColor))
|
||||
if currency == "XTR" {
|
||||
let amountAttributedString = NSMutableAttributedString(string: " > \(totalAmount)", font: titleBoldFont, textColor: primaryTextColor)
|
||||
if let range = amountAttributedString.string.range(of: ">"), let starImage = generateScaledImage(image: UIImage(bundleImageName: "Premium/Stars/Star"), size: CGSize(width: 16.0, height: 16.0), opaque: false)?.withRenderingMode(.alwaysTemplate) {
|
||||
amountAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: amountAttributedString.string))
|
||||
amountAttributedString.addAttribute(.foregroundColor, value: primaryTextColor, range: NSRange(range, in: amountAttributedString.string))
|
||||
amountAttributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: amountAttributedString.string))
|
||||
}
|
||||
|
||||
mutableString.replaceCharacters(in: range, with: amountAttributedString)
|
||||
} else {
|
||||
mutableString.replaceCharacters(in: range, with: NSAttributedString(string: formatCurrencyAmount(totalAmount, currency: currency), font: titleBoldFont, textColor: primaryTextColor))
|
||||
}
|
||||
}
|
||||
range = (mutableString.string as NSString).range(of: "{name}")
|
||||
if range.location != NSNotFound {
|
||||
|
@ -448,6 +448,9 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/Settings/BotSettingsScreen",
|
||||
"//submodules/TelegramUI/Components/AdminUserActionsSheet",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview",
|
||||
"//submodules/TelegramUI/Components/Stars/StarsTransactionsScreen",
|
||||
"//submodules/TelegramUI/Components/Stars/StarsPurchaseScreen",
|
||||
"//submodules/TelegramUI/Components/Stars/StarsTransferScreen",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||
"//build-system:ios_sim_arm64": [],
|
||||
|
@ -191,7 +191,11 @@ private final class ChatMessageActionButtonNode: ASDisplayNode {
|
||||
case .switchInline:
|
||||
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingShareIconImage : graphics.chatBubbleActionButtonOutgoingShareIconImage
|
||||
case .payment:
|
||||
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingPaymentIconImage : graphics.chatBubbleActionButtonOutgoingPaymentIconImage
|
||||
if button.title.contains("Pay XTR") {
|
||||
iconImage = nil
|
||||
} else {
|
||||
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingPaymentIconImage : graphics.chatBubbleActionButtonOutgoingPaymentIconImage
|
||||
}
|
||||
case .openUserProfile:
|
||||
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingProfileIconImage : graphics.chatBubbleActionButtonOutgoingProfileIconImage
|
||||
case .openWebView:
|
||||
@ -215,7 +219,23 @@ private final class ChatMessageActionButtonNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
let messageTheme = incoming ? theme.theme.chat.message.incoming : theme.theme.chat.message.outgoing
|
||||
let (titleSize, titleApply) = titleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: bubbleVariableColor(variableColor: messageTheme.actionButtonsTextColor, wallpaper: theme.wallpaper)), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(44.0, constrainedWidth - minimumSideInset - minimumSideInset), height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets(top: 1.0, left: 0.0, bottom: 1.0, right: 0.0)))
|
||||
|
||||
let titleColor = bubbleVariableColor(variableColor: messageTheme.actionButtonsTextColor, wallpaper: theme.wallpaper)
|
||||
let attributedTitle: NSAttributedString
|
||||
if title.contains("Pay XTR") {
|
||||
let stars = title.replacingOccurrences(of: "Pay XTR", with: "")
|
||||
let buttonAttributedString = NSMutableAttributedString(string: "Pay > \(stars)", font: titleFont, textColor: titleColor, paragraphAlignment: .center)
|
||||
if let range = buttonAttributedString.string.range(of: ">"), let starImage = UIImage(bundleImageName: "Item List/PremiumIcon") {
|
||||
buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string))
|
||||
buttonAttributedString.addAttribute(.foregroundColor, value: titleColor, range: NSRange(range, in: buttonAttributedString.string))
|
||||
buttonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: buttonAttributedString.string))
|
||||
}
|
||||
attributedTitle = buttonAttributedString
|
||||
} else {
|
||||
attributedTitle = NSAttributedString(string: title, font: titleFont, textColor: titleColor)
|
||||
}
|
||||
|
||||
let (titleSize, titleApply) = titleLayout(TextNodeLayoutArguments(attributedString: attributedTitle, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(44.0, constrainedWidth - minimumSideInset - minimumSideInset), height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets(top: 1.0, left: 0.0, bottom: 1.0, right: 0.0)))
|
||||
|
||||
return (titleSize.size.width + sideInset + sideInset, { width in
|
||||
return (CGSize(width: width, height: 42.0), { animation in
|
||||
|
@ -208,8 +208,12 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
|
||||
if let _ = invoice.extendedMedia {
|
||||
result.append((message, ChatMessageMediaBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .media, neighborSpacing: .default)))
|
||||
} else {
|
||||
skipText = true
|
||||
result.append((message, ChatMessageInvoiceBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
|
||||
if invoice.currency == "XTR" {
|
||||
result.append((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
|
||||
} else {
|
||||
skipText = true
|
||||
result.append((message, ChatMessageInvoiceBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
|
||||
}
|
||||
}
|
||||
needReactions = false
|
||||
break inner
|
||||
|
@ -290,11 +290,14 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
var isSeekableWebMedia = false
|
||||
var isUnsupportedMedia = false
|
||||
var story: Stories.Item?
|
||||
var invoice: TelegramMediaInvoice?
|
||||
for media in item.message.media {
|
||||
if let file = media as? TelegramMediaFile, let duration = file.duration {
|
||||
mediaDuration = Double(duration)
|
||||
}
|
||||
if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, webEmbedType(content: content).supportsSeeking {
|
||||
if let media = media as? TelegramMediaInvoice, media.currency == "XTR" {
|
||||
invoice = media
|
||||
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, webEmbedType(content: content).supportsSeeking {
|
||||
isSeekableWebMedia = true
|
||||
} else if media is TelegramMediaUnsupported {
|
||||
isUnsupportedMedia = true
|
||||
@ -308,7 +311,9 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
|
||||
var isTranslating = false
|
||||
if let story {
|
||||
if let invoice {
|
||||
rawText = invoice.description
|
||||
} else if let story {
|
||||
rawText = story.text
|
||||
messageEntities = story.entities
|
||||
} else if isUnsupportedMedia {
|
||||
|
@ -113,8 +113,14 @@ public final class ListActionItemComponent: Component {
|
||||
case disabled
|
||||
}
|
||||
|
||||
public enum Alignment {
|
||||
case `default`
|
||||
case center
|
||||
}
|
||||
|
||||
public let theme: PresentationTheme
|
||||
public let title: AnyComponent<Empty>
|
||||
public let titleAlignment: Alignment
|
||||
public let contentInsets: UIEdgeInsets
|
||||
public let leftIcon: LeftIcon?
|
||||
public let icon: Icon?
|
||||
@ -125,6 +131,7 @@ public final class ListActionItemComponent: Component {
|
||||
public init(
|
||||
theme: PresentationTheme,
|
||||
title: AnyComponent<Empty>,
|
||||
titleAlignment: Alignment = .default,
|
||||
contentInsets: UIEdgeInsets = UIEdgeInsets(top: 12.0, left: 0.0, bottom: 12.0, right: 0.0),
|
||||
leftIcon: LeftIcon? = nil,
|
||||
icon: Icon? = nil,
|
||||
@ -134,6 +141,7 @@ public final class ListActionItemComponent: Component {
|
||||
) {
|
||||
self.theme = theme
|
||||
self.title = title
|
||||
self.titleAlignment = titleAlignment
|
||||
self.contentInsets = contentInsets
|
||||
self.leftIcon = leftIcon
|
||||
self.icon = icon
|
||||
@ -149,6 +157,9 @@ public final class ListActionItemComponent: Component {
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.titleAlignment != rhs.titleAlignment {
|
||||
return false
|
||||
}
|
||||
if lhs.contentInsets != rhs.contentInsets {
|
||||
return false
|
||||
}
|
||||
@ -373,6 +384,11 @@ public final class ListActionItemComponent: Component {
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - contentLeftInset - contentRightInset, height: availableSize.height)
|
||||
)
|
||||
|
||||
if case .center = component.titleAlignment {
|
||||
contentLeftInset = floor((availableSize.width - titleSize.width) / 2.0)
|
||||
}
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: contentHeight), size: titleSize)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
|
@ -348,6 +348,7 @@ final class PeerInfoScreenData {
|
||||
let hasSavedMessageTags: Bool
|
||||
let isPremiumRequiredForStoryPosting: Bool
|
||||
let personalChannel: PeerInfoPersonalChannelData?
|
||||
let starsState: StarsContext.State?
|
||||
|
||||
let _isContact: Bool
|
||||
var forceIsContact: Bool = false
|
||||
@ -387,7 +388,8 @@ final class PeerInfoScreenData {
|
||||
accountIsPremium: Bool,
|
||||
hasSavedMessageTags: Bool,
|
||||
isPremiumRequiredForStoryPosting: Bool,
|
||||
personalChannel: PeerInfoPersonalChannelData?
|
||||
personalChannel: PeerInfoPersonalChannelData?,
|
||||
starsState: StarsContext.State?
|
||||
) {
|
||||
self.peer = peer
|
||||
self.chatPeer = chatPeer
|
||||
@ -416,6 +418,7 @@ final class PeerInfoScreenData {
|
||||
self.hasSavedMessageTags = hasSavedMessageTags
|
||||
self.isPremiumRequiredForStoryPosting = isPremiumRequiredForStoryPosting
|
||||
self.personalChannel = personalChannel
|
||||
self.starsState = starsState
|
||||
}
|
||||
}
|
||||
|
||||
@ -675,7 +678,7 @@ private func peerInfoPersonalChannel(context: AccountContext, peerId: EnginePeer
|
||||
|> distinctUntilChanged
|
||||
}
|
||||
|
||||
func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, accountsAndPeers: Signal<[(AccountContext, EnginePeer, Int32)], NoError>, activeSessionsContextAndCount: Signal<(ActiveSessionsContext, Int, WebSessionsContext)?, NoError>, notificationExceptions: Signal<NotificationExceptionsList?, NoError>, privacySettings: Signal<AccountPrivacySettings?, NoError>, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, hasPassport: Signal<Bool, NoError>) -> Signal<PeerInfoScreenData, NoError> {
|
||||
func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, accountsAndPeers: Signal<[(AccountContext, EnginePeer, Int32)], NoError>, activeSessionsContextAndCount: Signal<(ActiveSessionsContext, Int, WebSessionsContext)?, NoError>, notificationExceptions: Signal<NotificationExceptionsList?, NoError>, privacySettings: Signal<AccountPrivacySettings?, NoError>, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, hasPassport: Signal<Bool, NoError>, starsContext: StarsContext?) -> Signal<PeerInfoScreenData, NoError> {
|
||||
let preferences = context.sharedContext.accountManager.sharedData(keys: [
|
||||
SharedDataKeys.proxySettings,
|
||||
ApplicationSpecificSharedDataKeys.inAppNotificationSettings,
|
||||
@ -794,6 +797,13 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
|
||||
}
|
||||
}
|
||||
|
||||
let starsState: Signal<StarsContext.State?, NoError>
|
||||
if let starsContext {
|
||||
starsState = starsContext.state
|
||||
} else {
|
||||
starsState = .single(nil)
|
||||
}
|
||||
|
||||
return combineLatest(
|
||||
context.account.viewTracker.peerView(peerId, updateData: true),
|
||||
accountsAndPeers,
|
||||
@ -818,9 +828,10 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
|
||||
|> distinctUntilChanged,
|
||||
hasStories,
|
||||
bots,
|
||||
peerInfoPersonalChannel(context: context, peerId: peerId, isSettings: true)
|
||||
peerInfoPersonalChannel(context: context, peerId: peerId, isSettings: true),
|
||||
starsState
|
||||
)
|
||||
|> map { peerView, accountsAndPeers, accountSessions, privacySettings, sharedPreferences, notifications, stickerPacks, hasPassport, hasWatchApp, accountPreferences, suggestions, limits, hasPassword, isPowerSavingEnabled, hasStories, bots, personalChannel -> PeerInfoScreenData in
|
||||
|> map { peerView, accountsAndPeers, accountSessions, privacySettings, sharedPreferences, notifications, stickerPacks, hasPassport, hasWatchApp, accountPreferences, suggestions, limits, hasPassword, isPowerSavingEnabled, hasStories, bots, personalChannel, starsState -> PeerInfoScreenData in
|
||||
let (notificationExceptions, notificationsAuthorizationStatus, notificationsWarningSuppressed) = notifications
|
||||
let (featuredStickerPacks, archivedStickerPacks) = stickerPacks
|
||||
|
||||
@ -893,7 +904,8 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
|
||||
accountIsPremium: peer?.isPremium ?? false,
|
||||
hasSavedMessageTags: false,
|
||||
isPremiumRequiredForStoryPosting: true,
|
||||
personalChannel: personalChannel
|
||||
personalChannel: personalChannel,
|
||||
starsState: starsState
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -932,7 +944,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
accountIsPremium: false,
|
||||
hasSavedMessageTags: false,
|
||||
isPremiumRequiredForStoryPosting: true,
|
||||
personalChannel: nil
|
||||
personalChannel: nil,
|
||||
starsState: nil
|
||||
))
|
||||
case let .user(userPeerId, secretChatId, kind):
|
||||
let groupsInCommon: GroupsInCommonContext?
|
||||
@ -1259,7 +1272,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
accountIsPremium: accountIsPremium,
|
||||
hasSavedMessageTags: hasSavedMessageTags,
|
||||
isPremiumRequiredForStoryPosting: false,
|
||||
personalChannel: personalChannel
|
||||
personalChannel: personalChannel,
|
||||
starsState: nil
|
||||
)
|
||||
}
|
||||
case .channel:
|
||||
@ -1430,7 +1444,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
accountIsPremium: accountIsPremium,
|
||||
hasSavedMessageTags: hasSavedMessageTags,
|
||||
isPremiumRequiredForStoryPosting: isPremiumRequiredForStoryPosting,
|
||||
personalChannel: nil
|
||||
personalChannel: nil,
|
||||
starsState: nil
|
||||
)
|
||||
}
|
||||
case let .group(groupId):
|
||||
@ -1724,7 +1739,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
accountIsPremium: accountIsPremium,
|
||||
hasSavedMessageTags: hasSavedMessageTags,
|
||||
isPremiumRequiredForStoryPosting: isPremiumRequiredForStoryPosting,
|
||||
personalChannel: nil
|
||||
personalChannel: nil,
|
||||
starsState: nil
|
||||
))
|
||||
}
|
||||
}
|
||||
|
@ -521,6 +521,7 @@ private enum PeerInfoSettingsSection {
|
||||
case businessSetup
|
||||
case profile
|
||||
case premiumManagement
|
||||
case stars
|
||||
}
|
||||
|
||||
private enum PeerInfoReportType {
|
||||
@ -978,10 +979,20 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p
|
||||
items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 100, label: .text(""), text: presentationData.strings.Settings_Premium, icon: PresentationResourcesSettings.premium, action: {
|
||||
interaction.openSettings(.premium)
|
||||
}))
|
||||
items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 101, label: .text(""), additionalBadgeLabel: presentationData.strings.Settings_New, text: presentationData.strings.Settings_Business, icon: PresentationResourcesSettings.business, action: {
|
||||
//TODO:localize
|
||||
let balanceText: String
|
||||
if let balance = data.starsState?.balance, balance > 0 {
|
||||
balanceText = "\(balance)"
|
||||
} else {
|
||||
balanceText = ""
|
||||
}
|
||||
items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 102, label: .text(balanceText), text: "Your Stars", icon: PresentationResourcesSettings.stars, action: {
|
||||
interaction.openSettings(.stars)
|
||||
}))
|
||||
items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 103, label: .text(""), additionalBadgeLabel: presentationData.strings.Settings_New, text: presentationData.strings.Settings_Business, icon: PresentationResourcesSettings.business, action: {
|
||||
interaction.openSettings(.businessSetup)
|
||||
}))
|
||||
items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 102, label: .text(""), text: presentationData.strings.Settings_PremiumGift, icon: PresentationResourcesSettings.premiumGift, action: {
|
||||
items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 104, label: .text(""), text: presentationData.strings.Settings_PremiumGift, icon: PresentationResourcesSettings.premiumGift, action: {
|
||||
interaction.openSettings(.premiumGift)
|
||||
}))
|
||||
}
|
||||
@ -2521,7 +2532,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
}
|
||||
private var didSetReady = false
|
||||
|
||||
init(controller: PeerInfoScreenImpl, context: AccountContext, peerId: PeerId, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], isSettings: Bool, isMyProfile: Bool, hintGroupInCommon: PeerId?, requestsContext: PeerInvitationImportersContext?, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, initialPaneKey: PeerInfoPaneKey?) {
|
||||
init(controller: PeerInfoScreenImpl, context: AccountContext, peerId: PeerId, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], isSettings: Bool, isMyProfile: Bool, hintGroupInCommon: PeerId?, requestsContext: PeerInvitationImportersContext?, starsContext: StarsContext?, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, initialPaneKey: PeerInfoPaneKey?) {
|
||||
self.controller = controller
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
@ -4158,7 +4169,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
|
||||
self.cachedFaq.set(.single(nil) |> then(cachedFaqInstantPage(context: self.context) |> map(Optional.init)))
|
||||
|
||||
screenData = peerInfoScreenSettingsData(context: context, peerId: peerId, accountsAndPeers: self.accountsAndPeers.get(), activeSessionsContextAndCount: self.activeSessionsContextAndCount.get(), notificationExceptions: self.notificationExceptions.get(), privacySettings: self.privacySettings.get(), archivedStickerPacks: self.archivedPacks.get(), hasPassport: hasPassport)
|
||||
screenData = peerInfoScreenSettingsData(context: context, peerId: peerId, accountsAndPeers: self.accountsAndPeers.get(), activeSessionsContextAndCount: self.activeSessionsContextAndCount.get(), notificationExceptions: self.notificationExceptions.get(), privacySettings: self.privacySettings.get(), archivedStickerPacks: self.archivedPacks.get(), hasPassport: hasPassport, starsContext: starsContext)
|
||||
|
||||
|
||||
self.headerNode.displayCopyContextMenu = { [weak self] node, copyPhone, copyUsername in
|
||||
@ -9998,6 +10009,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
return
|
||||
}
|
||||
self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: !url.hasPrefix("tg://") && !url.contains("?start="), presentationData: self.context.sharedContext.currentPresentationData.with({$0}), navigationController: controller.navigationController as? NavigationController, dismissInput: {})
|
||||
case .stars:
|
||||
if let starsContext = self.controller?.starsContext {
|
||||
push(self.context.sharedContext.makeStarsTransactionsScreen(context: self.context, starsContext: starsContext))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -11834,6 +11849,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
|
||||
private let isMyProfile: Bool
|
||||
private let hintGroupInCommon: PeerId?
|
||||
private weak var requestsContext: PeerInvitationImportersContext?
|
||||
fileprivate let starsContext: StarsContext?
|
||||
private let switchToRecommendedChannels: Bool
|
||||
private let chatLocation: ChatLocation
|
||||
private let chatLocationContextHolder = Atomic<ChatLocationContextHolder?>(value: nil)
|
||||
@ -11912,6 +11928,12 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
|
||||
self.chatLocation = .peer(id: peerId)
|
||||
}
|
||||
|
||||
if isSettings {
|
||||
self.starsContext = context.engine.payments.peerStarsContext(peerId: context.account.peerId)
|
||||
} else {
|
||||
self.starsContext = nil
|
||||
}
|
||||
|
||||
self.presentationData = updatedPresentationData?.0 ?? context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let baseNavigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData)
|
||||
@ -12238,7 +12260,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = PeerInfoScreenNode(controller: self, context: self.context, peerId: self.peerId, avatarInitiallyExpanded: self.avatarInitiallyExpanded, isOpenedFromChat: self.isOpenedFromChat, nearbyPeerDistance: self.nearbyPeerDistance, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages, isSettings: self.isSettings, isMyProfile: self.isMyProfile, hintGroupInCommon: self.hintGroupInCommon, requestsContext: self.requestsContext, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, initialPaneKey: self.switchToRecommendedChannels ? .recommended : nil)
|
||||
self.displayNode = PeerInfoScreenNode(controller: self, context: self.context, peerId: self.peerId, avatarInitiallyExpanded: self.avatarInitiallyExpanded, isOpenedFromChat: self.isOpenedFromChat, nearbyPeerDistance: self.nearbyPeerDistance, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages, isSettings: self.isSettings, isMyProfile: self.isMyProfile, hintGroupInCommon: self.hintGroupInCommon, requestsContext: self.requestsContext, starsContext: self.starsContext, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, initialPaneKey: self.switchToRecommendedChannels ? .recommended : nil)
|
||||
self.controllerNode.accountsAndPeers.set(self.accountsAndPeers.get() |> map { $0.1 })
|
||||
self.controllerNode.activeSessionsContextAndCount.set(self.activeSessionsContextAndCount.get())
|
||||
self.cachedDataPromise.set(self.controllerNode.cachedDataPromise.get())
|
||||
|
@ -0,0 +1,28 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "PremiumStarComponent",
|
||||
module_name = "PremiumStarComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/GZip",
|
||||
"//submodules/LegacyComponents",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/TelegramUI/Components/Chat/MergedAvatarsNode",
|
||||
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -16,38 +16,49 @@ import TelegramPresentationData
|
||||
|
||||
private let sceneVersion: Int = 1
|
||||
|
||||
final class GiftAvatarComponent: Component {
|
||||
public final class GiftAvatarComponent: Component {
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let peers: [EnginePeer]
|
||||
let isVisible: Bool
|
||||
let hasIdleAnimations: Bool
|
||||
let hasScaleAnimation: Bool
|
||||
let color: UIColor?
|
||||
let offset: CGFloat?
|
||||
|
||||
init(context: AccountContext, theme: PresentationTheme, peers: [EnginePeer], isVisible: Bool, hasIdleAnimations: Bool) {
|
||||
public init(context: AccountContext, theme: PresentationTheme, peers: [EnginePeer], isVisible: Bool, hasIdleAnimations: Bool, hasScaleAnimation: Bool = true, color: UIColor? = nil, offset: CGFloat? = nil) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.peers = peers
|
||||
self.isVisible = isVisible
|
||||
self.hasIdleAnimations = hasIdleAnimations
|
||||
self.hasScaleAnimation = hasScaleAnimation
|
||||
self.color = color
|
||||
self.offset = offset
|
||||
}
|
||||
|
||||
static func ==(lhs: GiftAvatarComponent, rhs: GiftAvatarComponent) -> Bool {
|
||||
return lhs.peers == rhs.peers && lhs.theme === rhs.theme && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations
|
||||
public static func ==(lhs: GiftAvatarComponent, rhs: GiftAvatarComponent) -> Bool {
|
||||
return lhs.peers == rhs.peers && lhs.theme === rhs.theme && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations && lhs.hasScaleAnimation == rhs.hasScaleAnimation && lhs.offset == rhs.offset
|
||||
}
|
||||
|
||||
final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView {
|
||||
final class Tag {
|
||||
public final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView {
|
||||
public final class Tag {
|
||||
public init() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func matches(tag: Any) -> Bool {
|
||||
public func matches(tag: Any) -> Bool {
|
||||
if let _ = tag as? Tag {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private var component: GiftAvatarComponent?
|
||||
|
||||
private var _ready = Promise<Bool>()
|
||||
var ready: Signal<Bool, NoError> {
|
||||
public var ready: Signal<Bool, NoError> {
|
||||
return self._ready.get()
|
||||
}
|
||||
|
||||
@ -66,7 +77,7 @@ final class GiftAvatarComponent: Component {
|
||||
private var timer: SwiftSignalKit.Timer?
|
||||
private var hasIdleAnimations = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
public override init(frame: CGRect) {
|
||||
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: CGSize(width: 64.0, height: 64.0)))
|
||||
self.sceneView.backgroundColor = .clear
|
||||
self.sceneView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
|
||||
@ -80,9 +91,7 @@ final class GiftAvatarComponent: Component {
|
||||
|
||||
self.addSubview(self.sceneView)
|
||||
self.addSubview(self.avatarNode.view)
|
||||
|
||||
self.setup()
|
||||
|
||||
|
||||
let tapGestureRecoginzer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
|
||||
self.addGestureRecognizer(tapGestureRecoginzer)
|
||||
|
||||
@ -105,19 +114,43 @@ final class GiftAvatarComponent: Component {
|
||||
self.playAppearanceAnimation(velocity: nil, mirror: false, explode: true)
|
||||
}
|
||||
|
||||
private var didSetup = false
|
||||
private func setup() {
|
||||
guard let scene = loadCompressedScene(name: "gift", version: sceneVersion) else {
|
||||
guard let scene = loadCompressedScene(name: "gift", version: sceneVersion), !self.didSetup else {
|
||||
return
|
||||
}
|
||||
|
||||
self.didSetup = true
|
||||
|
||||
self.sceneView.scene = scene
|
||||
self.sceneView.delegate = self
|
||||
|
||||
let _ = self.sceneView.snapshot()
|
||||
if let color = self.component?.color {
|
||||
let names: [String] = [
|
||||
"particles_left",
|
||||
"particles_right",
|
||||
"particles_left_bottom",
|
||||
"particles_right_bottom",
|
||||
"particles_center"
|
||||
]
|
||||
|
||||
for name in names {
|
||||
if let node = scene.rootNode.childNode(withName: name, recursively: false), let particleSystem = node.particleSystems?.first {
|
||||
particleSystem.particleColor = color
|
||||
particleSystem.particleColorVariation = SCNVector4Make(0, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
self.didSetReady = true
|
||||
self._ready.set(.single(true))
|
||||
self.onReady()
|
||||
} else {
|
||||
let _ = self.sceneView.snapshot()
|
||||
}
|
||||
}
|
||||
|
||||
private var didSetReady = false
|
||||
func renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval) {
|
||||
public func renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval) {
|
||||
if !self.didSetReady {
|
||||
self.didSetReady = true
|
||||
|
||||
@ -146,6 +179,10 @@ final class GiftAvatarComponent: Component {
|
||||
}
|
||||
|
||||
private func setupScaleAnimation() {
|
||||
guard self.component?.hasScaleAnimation == true else {
|
||||
return
|
||||
}
|
||||
|
||||
let animation = CABasicAnimation(keyPath: "transform.scale")
|
||||
animation.duration = 2.0
|
||||
animation.fromValue = 1.0
|
||||
@ -245,9 +282,13 @@ final class GiftAvatarComponent: Component {
|
||||
}
|
||||
|
||||
func update(component: GiftAvatarComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
|
||||
self.setup()
|
||||
|
||||
self.sceneView.bounds = CGRect(origin: .zero, size: CGSize(width: availableSize.width * 2.0, height: availableSize.height * 2.0))
|
||||
if self.sceneView.superview == self {
|
||||
self.sceneView.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)
|
||||
self.sceneView.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0 + (component.offset ?? 0.0))
|
||||
}
|
||||
|
||||
self.hasIdleAnimations = component.hasIdleAnimations
|
||||
@ -325,11 +366,11 @@ final class GiftAvatarComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
}
|
@ -30,22 +30,17 @@ private func generateShineTexture() -> UIImage {
|
||||
return UIImage()
|
||||
}
|
||||
|
||||
private func generateDiffuseTexture() -> UIImage {
|
||||
private func generateDiffuseTexture(colors: [UIColor]) -> UIImage {
|
||||
return generateImage(CGSize(width: 256, height: 256), rotatedContext: { size, context in
|
||||
let colorsArray: [CGColor] = [
|
||||
UIColor(rgb: 0x0079ff).cgColor,
|
||||
UIColor(rgb: 0x6a93ff).cgColor,
|
||||
UIColor(rgb: 0x9172fe).cgColor,
|
||||
UIColor(rgb: 0xe46acd).cgColor,
|
||||
]
|
||||
let colorsArray: [CGColor] = colors.map { $0.cgColor }
|
||||
var locations: [CGFloat] = [0.0, 0.25, 0.5, 0.75, 1.0]
|
||||
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)!
|
||||
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: size.height), options: CGGradientDrawingOptions())
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: size.height), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions())
|
||||
})!
|
||||
}
|
||||
|
||||
func loadCompressedScene(name: String, version: Int) -> SCNScene? {
|
||||
public func loadCompressedScene(name: String, version: Int) -> SCNScene? {
|
||||
let resourceUrl: URL
|
||||
if let url = getAppBundle().url(forResource: name, withExtension: "scn") {
|
||||
resourceUrl = url
|
||||
@ -69,40 +64,52 @@ func loadCompressedScene(name: String, version: Int) -> SCNScene? {
|
||||
return scene
|
||||
}
|
||||
|
||||
final class PremiumStarComponent: Component {
|
||||
public final class PremiumStarComponent: Component {
|
||||
let isIntro: Bool
|
||||
let isVisible: Bool
|
||||
let hasIdleAnimations: Bool
|
||||
|
||||
init(isIntro: Bool, isVisible: Bool, hasIdleAnimations: Bool) {
|
||||
let colors: [UIColor]?
|
||||
|
||||
public init(
|
||||
isIntro: Bool,
|
||||
isVisible: Bool,
|
||||
hasIdleAnimations: Bool,
|
||||
colors: [UIColor]? = nil
|
||||
) {
|
||||
self.isIntro = isIntro
|
||||
self.isVisible = isVisible
|
||||
self.hasIdleAnimations = hasIdleAnimations
|
||||
self.colors = colors
|
||||
}
|
||||
|
||||
static func ==(lhs: PremiumStarComponent, rhs: PremiumStarComponent) -> Bool {
|
||||
return lhs.isIntro == rhs.isIntro && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations
|
||||
public static func ==(lhs: PremiumStarComponent, rhs: PremiumStarComponent) -> Bool {
|
||||
return lhs.isIntro == rhs.isIntro && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations && lhs.colors == rhs.colors
|
||||
}
|
||||
|
||||
final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView {
|
||||
final class Tag {
|
||||
public final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView {
|
||||
public final class Tag {
|
||||
public init() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func matches(tag: Any) -> Bool {
|
||||
public func matches(tag: Any) -> Bool {
|
||||
if let _ = tag as? Tag {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private var component: PremiumStarComponent?
|
||||
|
||||
private var _ready = Promise<Bool>()
|
||||
var ready: Signal<Bool, NoError> {
|
||||
public var ready: Signal<Bool, NoError> {
|
||||
return self._ready.get()
|
||||
}
|
||||
|
||||
weak var animateFrom: UIView?
|
||||
weak var containerView: UIView?
|
||||
var animationColor: UIColor?
|
||||
public weak var animateFrom: UIView?
|
||||
public weak var containerView: UIView?
|
||||
public var animationColor: UIColor?
|
||||
|
||||
private let sceneView: SCNView
|
||||
|
||||
@ -126,8 +133,6 @@ final class PremiumStarComponent: Component {
|
||||
|
||||
self.addSubview(self.sceneView)
|
||||
|
||||
self.setup()
|
||||
|
||||
let panGestureRecoginzer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
|
||||
self.addGestureRecognizer(panGestureRecoginzer)
|
||||
|
||||
@ -274,21 +279,46 @@ final class PremiumStarComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
private var didSetup = false
|
||||
private func setup() {
|
||||
guard let scene = loadCompressedScene(name: "star", version: sceneVersion) else {
|
||||
guard !self.didSetup, let scene = loadCompressedScene(name: "star", version: sceneVersion) else {
|
||||
return
|
||||
}
|
||||
|
||||
self.didSetup = true
|
||||
self.sceneView.scene = scene
|
||||
self.sceneView.delegate = self
|
||||
|
||||
self.didSetReady = true
|
||||
self._ready.set(.single(true))
|
||||
self.onReady()
|
||||
if let node = scene.rootNode.childNode(withName: "star", recursively: false), let colors = self.component?.colors, let color = colors.first {
|
||||
node.geometry?.materials.first?.diffuse.contents = generateDiffuseTexture(colors: colors)
|
||||
|
||||
let names: [String] = [
|
||||
"particles_left",
|
||||
"particles_right",
|
||||
"particles_left_bottom",
|
||||
"particles_right_bottom",
|
||||
"particles_center"
|
||||
]
|
||||
|
||||
for name in names {
|
||||
if let node = scene.rootNode.childNode(withName: name, recursively: false), let particleSystem = node.particleSystems?.first {
|
||||
particleSystem.particleColor = color
|
||||
particleSystem.particleColorVariation = SCNVector4Make(0, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.animateFrom != nil {
|
||||
let _ = self.sceneView.snapshot()
|
||||
} else {
|
||||
self.didSetReady = true
|
||||
self._ready.set(.single(true))
|
||||
self.onReady()
|
||||
}
|
||||
}
|
||||
|
||||
private var didSetReady = false
|
||||
func renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval) {
|
||||
public func renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval) {
|
||||
if !self.didSetReady {
|
||||
self.didSetReady = true
|
||||
|
||||
@ -305,7 +335,7 @@ final class PremiumStarComponent: Component {
|
||||
}
|
||||
|
||||
containerView = containerView.subviews[2].subviews[1]
|
||||
|
||||
|
||||
if let animationColor = self.animationColor {
|
||||
let newNode = node.clone()
|
||||
newNode.geometry = node.geometry?.copy() as? SCNGeometry
|
||||
@ -562,6 +592,10 @@ final class PremiumStarComponent: Component {
|
||||
}
|
||||
|
||||
func update(component: PremiumStarComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
|
||||
self.setup()
|
||||
|
||||
self.sceneView.bounds = CGRect(origin: .zero, size: CGSize(width: availableSize.width * 2.0, height: availableSize.height * 2.0))
|
||||
if self.sceneView.superview == self {
|
||||
self.sceneView.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)
|
||||
@ -573,11 +607,11 @@ final class PremiumStarComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect(), isIntro: self.isIntro)
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
}
|
@ -35,7 +35,7 @@ public final class ScrollComponent<ChildEnvironment: Equatable>: Component {
|
||||
let contentInsets: UIEdgeInsets
|
||||
let contentOffsetUpdated: (_ top: CGFloat, _ bottom: CGFloat) -> Void
|
||||
let contentOffsetWillCommit: (UnsafeMutablePointer<CGPoint>) -> Void
|
||||
let resetScroll: ActionSlot<Void>
|
||||
let resetScroll: ActionSlot<CGPoint?>
|
||||
|
||||
public init(
|
||||
content: AnyComponent<(ChildEnvironment, ScrollChildEnvironment)>,
|
||||
@ -43,7 +43,7 @@ public final class ScrollComponent<ChildEnvironment: Equatable>: Component {
|
||||
contentInsets: UIEdgeInsets,
|
||||
contentOffsetUpdated: @escaping (_ top: CGFloat, _ bottom: CGFloat) -> Void,
|
||||
contentOffsetWillCommit: @escaping (UnsafeMutablePointer<CGPoint>) -> Void,
|
||||
resetScroll: ActionSlot<Void> = ActionSlot()
|
||||
resetScroll: ActionSlot<CGPoint?> = ActionSlot()
|
||||
) {
|
||||
self.content = content
|
||||
self.externalState = externalState
|
||||
@ -120,8 +120,8 @@ public final class ScrollComponent<ChildEnvironment: Equatable>: Component {
|
||||
)
|
||||
transition.setFrame(view: self.contentView, frame: CGRect(origin: .zero, size: contentSize), completion: nil)
|
||||
|
||||
component.resetScroll.connect { [weak self] _ in
|
||||
self?.setContentOffset(.zero, animated: false)
|
||||
component.resetScroll.connect { [weak self] point in
|
||||
self?.setContentOffset(point ?? .zero, animated: point != nil)
|
||||
}
|
||||
|
||||
if self.contentSize != contentSize {
|
||||
|
@ -0,0 +1,43 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "StarsPurchaseScreen",
|
||||
module_name = "StarsPurchaseScreen",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/ViewControllerComponent",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/BalancedTextComponent",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/ItemListUI",
|
||||
"//submodules/TelegramStringFormatting",
|
||||
"//submodules/PresentationDataUtils",
|
||||
"//submodules/Components/SheetComponent",
|
||||
"//submodules/UndoUI",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/TelegramUI/Components/ListSectionComponent",
|
||||
"//submodules/TelegramUI/Components/ListActionItemComponent",
|
||||
"//submodules/TelegramUI/Components/ScrollComponent",
|
||||
"//submodules/TelegramUI/Components/Premium/PremiumStarComponent",
|
||||
"//submodules/Components/BlurredBackgroundComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/ConfettiEffect",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,45 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "StarsTransactionsScreen",
|
||||
module_name = "StarsTransactionsScreen",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/ViewControllerComponent",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/BalancedTextComponent",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/ItemListUI",
|
||||
"//submodules/TelegramStringFormatting",
|
||||
"//submodules/PresentationDataUtils",
|
||||
"//submodules/Components/SheetComponent",
|
||||
"//submodules/UndoUI",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/TelegramUI/Components/ListSectionComponent",
|
||||
"//submodules/TelegramUI/Components/ListActionItemComponent",
|
||||
"//submodules/TelegramUI/Components/ScrollComponent",
|
||||
"//submodules/TelegramUI/Components/Premium/PremiumStarComponent",
|
||||
"//submodules/Components/BlurredBackgroundComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/Components/SolidRoundedButtonComponent",
|
||||
"//submodules/TelegramUI/Components/AnimatedTextComponent",
|
||||
"//submodules/AvatarNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,163 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import ComponentFlow
|
||||
import AccountContext
|
||||
import MultilineTextComponent
|
||||
import TelegramPresentationData
|
||||
import PresentationDataUtils
|
||||
import SolidRoundedButtonComponent
|
||||
import AnimatedTextComponent
|
||||
|
||||
final class StarsBalanceComponent: Component {
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let count: Int64
|
||||
let buy: () -> Void
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
count: Int64,
|
||||
buy: @escaping () -> Void
|
||||
) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.count = count
|
||||
self.buy = buy
|
||||
}
|
||||
|
||||
static func ==(lhs: StarsBalanceComponent, rhs: StarsBalanceComponent) -> Bool {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.count != rhs.count {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let icon = UIImageView()
|
||||
private let title = ComponentView<Empty>()
|
||||
private let subtitle = ComponentView<Empty>()
|
||||
private var button = ComponentView<Empty>()
|
||||
|
||||
private var component: StarsBalanceComponent?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.icon.image = UIImage(bundleImageName: "Premium/Stars/StarLarge")
|
||||
|
||||
self.addSubview(self.icon)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: StarsBalanceComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
|
||||
let sideInset: CGFloat = 16.0
|
||||
|
||||
let size = CGSize(width: availableSize.width, height: 172.0)
|
||||
|
||||
var animatedTextItems: [AnimatedTextComponent.Item] = []
|
||||
animatedTextItems.append(AnimatedTextComponent.Item(
|
||||
id: 1,
|
||||
isUnbreakable: true,
|
||||
content: .number(Int(component.count), minDigits: 1)
|
||||
))
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: .easeInOut(duration: 0.2),
|
||||
component: AnyComponent(
|
||||
AnimatedTextComponent(
|
||||
font: Font.with(size: 48.0, design: .round, weight: .semibold),
|
||||
color: component.theme.list.itemPrimaryTextColor,
|
||||
items: animatedTextItems
|
||||
)
|
||||
// MultilineTextComponent(
|
||||
// text: .plain(NSAttributedString(string: "\(component.count)", font: Font.with(size: 48.0, design: .round, weight: .semibold), textColor: component.theme.list.itemPrimaryTextColor)),
|
||||
// horizontalAlignment: .center
|
||||
// )
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
|
||||
)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
if let icon = self.icon.image {
|
||||
let spacing: CGFloat = 3.0
|
||||
let totalWidth = titleSize.width + icon.size.width + spacing
|
||||
let origin = floorToScreenPixels((availableSize.width - totalWidth) / 2.0)
|
||||
let titleFrame = CGRect(origin: CGPoint(x: origin + icon.size.width + spacing, y: 13.0), size: titleSize)
|
||||
titleView.frame = titleFrame
|
||||
|
||||
self.icon.frame = CGRect(origin: CGPoint(x: origin, y: 18.0), size: icon.size)
|
||||
}
|
||||
}
|
||||
|
||||
let subtitleSize = self.subtitle.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "your balance", font: Font.regular(17.0), textColor: component.theme.list.itemSecondaryTextColor)),
|
||||
horizontalAlignment: .center
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
|
||||
)
|
||||
if let subtitleView = self.subtitle.view {
|
||||
if subtitleView.superview == nil {
|
||||
self.addSubview(subtitleView)
|
||||
}
|
||||
let subtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - subtitleSize.width) / 2.0), y: 70.0), size: subtitleSize)
|
||||
subtitleView.frame = subtitleFrame
|
||||
}
|
||||
|
||||
let buttonSize = self.button.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
SolidRoundedButtonComponent(
|
||||
title: "Buy More Stars",
|
||||
theme: SolidRoundedButtonComponent.Theme(theme: component.theme),
|
||||
height: 50.0,
|
||||
cornerRadius: 11.0,
|
||||
action: { [weak self] in
|
||||
self?.component?.buy()
|
||||
}
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
|
||||
)
|
||||
if let buttonView = self.button.view {
|
||||
if buttonView.superview == nil {
|
||||
self.addSubview(buttonView)
|
||||
}
|
||||
let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: size.height - buttonSize.height - sideInset), size: buttonSize)
|
||||
buttonView.frame = buttonFrame
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
@ -0,0 +1,553 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import ViewControllerComponent
|
||||
import ComponentDisplayAdapters
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import TelegramCore
|
||||
import MultilineTextComponent
|
||||
import ListActionItemComponent
|
||||
import TelegramStringFormatting
|
||||
import AvatarNode
|
||||
import BundleIconComponent
|
||||
|
||||
final class StarsTransactionsListPanelComponent: Component {
|
||||
typealias EnvironmentType = StarsTransactionsPanelEnvironment
|
||||
|
||||
final class Item: Equatable {
|
||||
let transaction: StarsContext.State.Transaction
|
||||
|
||||
init(
|
||||
transaction: StarsContext.State.Transaction
|
||||
) {
|
||||
self.transaction = transaction
|
||||
}
|
||||
|
||||
static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||
if lhs.transaction != rhs.transaction {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
final class Items: Equatable {
|
||||
let items: [Item]
|
||||
|
||||
init(items: [Item]) {
|
||||
self.items = items
|
||||
}
|
||||
|
||||
static func ==(lhs: Items, rhs: Items) -> Bool {
|
||||
if lhs === rhs {
|
||||
return true
|
||||
}
|
||||
return lhs.items == rhs.items
|
||||
}
|
||||
}
|
||||
|
||||
let context: AccountContext
|
||||
let items: Items?
|
||||
let action: (StarsContext.State.Transaction) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
items: Items?,
|
||||
action: @escaping (StarsContext.State.Transaction) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.items = items
|
||||
self.action = action
|
||||
}
|
||||
|
||||
static func ==(lhs: StarsTransactionsListPanelComponent, rhs: StarsTransactionsListPanelComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.items != rhs.items {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private struct ItemLayout: Equatable {
|
||||
let containerInsets: UIEdgeInsets
|
||||
let containerWidth: CGFloat
|
||||
let itemHeight: CGFloat
|
||||
let itemCount: Int
|
||||
|
||||
let contentHeight: CGFloat
|
||||
|
||||
init(
|
||||
containerInsets: UIEdgeInsets,
|
||||
containerWidth: CGFloat,
|
||||
itemHeight: CGFloat,
|
||||
itemCount: Int
|
||||
) {
|
||||
self.containerInsets = containerInsets
|
||||
self.containerWidth = containerWidth
|
||||
self.itemHeight = itemHeight
|
||||
self.itemCount = itemCount
|
||||
|
||||
self.contentHeight = containerInsets.top + containerInsets.bottom + CGFloat(itemCount) * itemHeight
|
||||
}
|
||||
|
||||
func visibleItems(for rect: CGRect) -> Range<Int>? {
|
||||
let offsetRect = rect.offsetBy(dx: -self.containerInsets.left, dy: -self.containerInsets.top)
|
||||
var minVisibleRow = Int(floor((offsetRect.minY) / (self.itemHeight)))
|
||||
minVisibleRow = max(0, minVisibleRow)
|
||||
let maxVisibleRow = Int(ceil((offsetRect.maxY) / (self.itemHeight)))
|
||||
|
||||
let minVisibleIndex = minVisibleRow
|
||||
let maxVisibleIndex = maxVisibleRow
|
||||
|
||||
if maxVisibleIndex >= minVisibleIndex {
|
||||
return minVisibleIndex ..< (maxVisibleIndex + 1)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func itemFrame(for index: Int) -> CGRect {
|
||||
return CGRect(origin: CGPoint(x: 0.0, y: self.containerInsets.top + CGFloat(index) * self.itemHeight), size: CGSize(width: self.containerWidth, height: self.itemHeight))
|
||||
}
|
||||
}
|
||||
|
||||
private final class ScrollViewImpl: UIScrollView {
|
||||
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
class View: UIView, UIScrollViewDelegate {
|
||||
private let scrollView: ScrollViewImpl
|
||||
|
||||
private let measureItem = ComponentView<Empty>()
|
||||
private var visibleItems: [String: ComponentView<Empty>] = [:]
|
||||
private var separatorViews: [String: UIView] = [:]
|
||||
|
||||
private var ignoreScrolling: Bool = false
|
||||
|
||||
private var component: StarsTransactionsListPanelComponent?
|
||||
private var environment: StarsTransactionsPanelEnvironment?
|
||||
private var itemLayout: ItemLayout?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.scrollView = ScrollViewImpl()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.scrollView.delaysContentTouches = true
|
||||
self.scrollView.canCancelContentTouches = true
|
||||
self.scrollView.clipsToBounds = false
|
||||
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
if #available(iOS 13.0, *) {
|
||||
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
|
||||
}
|
||||
self.scrollView.showsVerticalScrollIndicator = true
|
||||
self.scrollView.showsHorizontalScrollIndicator = false
|
||||
self.scrollView.alwaysBounceHorizontal = false
|
||||
self.scrollView.scrollsToTop = false
|
||||
self.scrollView.delegate = self
|
||||
self.scrollView.clipsToBounds = true
|
||||
self.addSubview(self.scrollView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if !self.ignoreScrolling {
|
||||
self.updateScrolling(transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
cancelContextGestures(view: scrollView)
|
||||
}
|
||||
|
||||
private func updateScrolling(transition: Transition) {
|
||||
guard let component = self.component, let environment = self.environment, let items = component.items, let itemLayout = self.itemLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -100.0)
|
||||
|
||||
var validIds = Set<String>()
|
||||
if let visibleItems = itemLayout.visibleItems(for: visibleBounds) {
|
||||
for index in visibleItems.lowerBound ..< visibleItems.upperBound {
|
||||
if index >= items.items.count {
|
||||
continue
|
||||
}
|
||||
let item = items.items[index]
|
||||
let id = item.transaction.id
|
||||
validIds.insert(id)
|
||||
|
||||
var itemTransition = transition
|
||||
let itemView: ComponentView<Empty>
|
||||
let separatorView: UIView
|
||||
if let current = self.visibleItems[id], let currentSeparator = self.separatorViews[id] {
|
||||
itemView = current
|
||||
separatorView = currentSeparator
|
||||
} else {
|
||||
itemTransition = .immediate
|
||||
itemView = ComponentView()
|
||||
self.visibleItems[id] = itemView
|
||||
|
||||
separatorView = UIView()
|
||||
self.separatorViews[id] = separatorView
|
||||
self.addSubview(separatorView)
|
||||
}
|
||||
|
||||
separatorView.backgroundColor = environment.theme.list.itemBlocksSeparatorColor
|
||||
|
||||
let fontBaseDisplaySize = 17.0
|
||||
|
||||
let itemTitle: String
|
||||
let itemSubtitle: String
|
||||
let itemLabel: NSAttributedString
|
||||
switch item.transaction.peer {
|
||||
case let .peer(peer):
|
||||
itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)
|
||||
itemLabel = NSAttributedString(string: "- \(item.transaction.count * -1)", font: Font.medium(fontBaseDisplaySize), textColor: environment.theme.list.itemDestructiveColor)
|
||||
case .appStore:
|
||||
itemTitle = "In-App Purchase"
|
||||
itemLabel = NSAttributedString(string: "+ \(item.transaction.count)", font: Font.medium(fontBaseDisplaySize), textColor: environment.theme.list.itemDisclosureActions.constructive.fillColor)
|
||||
case .playMarket:
|
||||
itemTitle = "Play Market"
|
||||
itemLabel = NSAttributedString(string: "+ \(item.transaction.count)", font: Font.medium(fontBaseDisplaySize), textColor: environment.theme.list.itemDisclosureActions.constructive.fillColor)
|
||||
case .fragment:
|
||||
itemTitle = "Fragment"
|
||||
itemLabel = NSAttributedString(string: "+ \(item.transaction.count)", font: Font.medium(fontBaseDisplaySize), textColor: environment.theme.list.itemDisclosureActions.constructive.fillColor)
|
||||
}
|
||||
itemSubtitle = stringForMediumCompactDate(timestamp: item.transaction.date, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat)
|
||||
|
||||
let _ = itemView.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(ListActionItemComponent(
|
||||
theme: environment.theme,
|
||||
title: AnyComponent(VStack([
|
||||
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: itemTitle,
|
||||
font: Font.semibold(fontBaseDisplaySize),
|
||||
textColor: environment.theme.list.itemPrimaryTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 0
|
||||
))),
|
||||
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: itemSubtitle,
|
||||
font: Font.regular(floor(fontBaseDisplaySize * 14.0 / 17.0)),
|
||||
textColor: environment.theme.list.itemSecondaryTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.18
|
||||
)))
|
||||
], alignment: .left, spacing: 2.0)),
|
||||
contentInsets: UIEdgeInsets(top: 11.0, left: 0.0, bottom: 11.0, right: 0.0),
|
||||
leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(AvatarComponent(context: component.context, theme: environment.theme, peer: item.transaction.peer)))),
|
||||
icon: nil,
|
||||
accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(LabelComponent(text: itemLabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))),
|
||||
action: { [weak self] _ in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.action(item.transaction)
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: itemLayout.containerWidth, height: itemLayout.itemHeight)
|
||||
)
|
||||
let itemFrame = itemLayout.itemFrame(for: index)
|
||||
if let itemComponentView = itemView.view {
|
||||
if itemComponentView.superview == nil {
|
||||
self.scrollView.addSubview(itemComponentView)
|
||||
}
|
||||
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
|
||||
}
|
||||
let sideInset: CGFloat = 60.0
|
||||
itemTransition.setFrame(view: separatorView, frame: CGRect(x: sideInset, y: itemFrame.maxY, width: itemFrame.width - sideInset, height: UIScreenPixel))
|
||||
}
|
||||
}
|
||||
|
||||
var removeIds: [String] = []
|
||||
for (id, itemView) in self.visibleItems {
|
||||
if !validIds.contains(id) {
|
||||
removeIds.append(id)
|
||||
if let itemComponentView = itemView.view {
|
||||
transition.setAlpha(view: itemComponentView, alpha: 0.0, completion: { [weak itemComponentView] _ in
|
||||
itemComponentView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
for (id, separatorView) in self.separatorViews {
|
||||
if !validIds.contains(id) {
|
||||
transition.setAlpha(view: separatorView, alpha: 0.0, completion: { [weak separatorView] _ in
|
||||
separatorView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
for id in removeIds {
|
||||
self.visibleItems.removeValue(forKey: id)
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: StarsTransactionsListPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<StarsTransactionsPanelEnvironment>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
|
||||
let environment = environment[StarsTransactionsPanelEnvironment.self].value
|
||||
self.environment = environment
|
||||
|
||||
let fontBaseDisplaySize = 17.0
|
||||
let measureItemSize = self.measureItem.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(ListActionItemComponent(
|
||||
theme: environment.theme,
|
||||
title: AnyComponent(VStack([
|
||||
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: "ABC",
|
||||
font: Font.regular(fontBaseDisplaySize),
|
||||
textColor: environment.theme.list.itemPrimaryTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 0
|
||||
))),
|
||||
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: "abc",
|
||||
font: Font.regular(floor(fontBaseDisplaySize * 13.0 / 17.0)),
|
||||
textColor: environment.theme.list.itemSecondaryTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.18
|
||||
)))
|
||||
], alignment: .left, spacing: 2.0)),
|
||||
leftIcon: nil,
|
||||
icon: nil,
|
||||
accessory: nil,
|
||||
action: { _ in }
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 1000.0)
|
||||
)
|
||||
|
||||
let itemLayout = ItemLayout(
|
||||
containerInsets: environment.containerInsets,
|
||||
containerWidth: availableSize.width,
|
||||
itemHeight: measureItemSize.height,
|
||||
itemCount: component.items?.items.count ?? 0
|
||||
)
|
||||
self.itemLayout = itemLayout
|
||||
|
||||
self.ignoreScrolling = true
|
||||
let contentOffset = self.scrollView.bounds.minY
|
||||
transition.setPosition(view: self.scrollView, position: CGRect(origin: CGPoint(), size: availableSize).center)
|
||||
var scrollBounds = self.scrollView.bounds
|
||||
scrollBounds.size = availableSize
|
||||
if !environment.isScrollable {
|
||||
scrollBounds.origin = CGPoint()
|
||||
}
|
||||
transition.setBounds(view: self.scrollView, bounds: scrollBounds)
|
||||
self.scrollView.isScrollEnabled = environment.isScrollable
|
||||
let contentSize = CGSize(width: availableSize.width, height: itemLayout.contentHeight)
|
||||
if self.scrollView.contentSize != contentSize {
|
||||
self.scrollView.contentSize = contentSize
|
||||
}
|
||||
self.scrollView.scrollIndicatorInsets = environment.containerInsets
|
||||
if !transition.animation.isImmediate && self.scrollView.bounds.minY != contentOffset {
|
||||
let deltaOffset = self.scrollView.bounds.minY - contentOffset
|
||||
transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: -deltaOffset), 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<StarsTransactionsPanelEnvironment>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
func cancelContextGestures(view: UIView) {
|
||||
if let gestureRecognizers = view.gestureRecognizers {
|
||||
for gesture in gestureRecognizers {
|
||||
if let gesture = gesture as? ContextGesture {
|
||||
gesture.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
for subview in view.subviews {
|
||||
cancelContextGestures(view: subview)
|
||||
}
|
||||
}
|
||||
|
||||
private final class AvatarComponent: Component {
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let peer: StarsContext.State.Transaction.Peer
|
||||
|
||||
init(context: AccountContext, theme: PresentationTheme, peer: StarsContext.State.Transaction.Peer) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.peer = peer
|
||||
}
|
||||
|
||||
static func ==(lhs: AvatarComponent, rhs: AvatarComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let avatarNode: AvatarNode
|
||||
private let backgroundView = UIImageView()
|
||||
private let iconView = UIImageView()
|
||||
|
||||
private var component: AvatarComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 16.0))
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.iconView.contentMode = .center
|
||||
self.iconView.image = UIImage(bundleImageName: "Premium/Stars/TopUp")
|
||||
|
||||
self.addSubnode(self.avatarNode)
|
||||
self.addSubview(self.backgroundView)
|
||||
self.addSubview(self.iconView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: AvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let size = CGSize(width: 40.0, height: 40.0)
|
||||
|
||||
let gradientImage = generateGradientFilledCircleImage(diameter: size.width, colors: [UIColor(rgb: 0xf67447).cgColor, UIColor(rgb: 0xfdbe1c).cgColor], direction: .mirroredDiagonal)
|
||||
|
||||
switch component.peer {
|
||||
case let .peer(peer):
|
||||
self.avatarNode.setPeer(
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
peer: peer,
|
||||
synchronousLoad: true
|
||||
)
|
||||
self.backgroundView.isHidden = true
|
||||
self.iconView.isHidden = true
|
||||
self.avatarNode.isHidden = false
|
||||
case .appStore:
|
||||
self.backgroundView.image = gradientImage
|
||||
self.backgroundView.isHidden = false
|
||||
self.iconView.isHidden = false
|
||||
self.avatarNode.isHidden = true
|
||||
case .playMarket:
|
||||
self.backgroundView.image = gradientImage
|
||||
self.backgroundView.isHidden = false
|
||||
self.iconView.isHidden = false
|
||||
self.avatarNode.isHidden = true
|
||||
case .fragment:
|
||||
self.backgroundView.image = gradientImage
|
||||
self.backgroundView.isHidden = false
|
||||
self.iconView.isHidden = false
|
||||
self.avatarNode.isHidden = true
|
||||
}
|
||||
|
||||
self.avatarNode.frame = CGRect(origin: .zero, size: size)
|
||||
self.iconView.frame = CGRect(origin: .zero, size: size)
|
||||
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: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private final class LabelComponent: CombinedComponent {
|
||||
let text: NSAttributedString
|
||||
|
||||
init(
|
||||
text: NSAttributedString
|
||||
) {
|
||||
self.text = text
|
||||
}
|
||||
|
||||
static func ==(lhs: LabelComponent, rhs: LabelComponent) -> Bool {
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let text = Child(MultilineTextComponent.self)
|
||||
let icon = Child(BundleIconComponent.self)
|
||||
|
||||
return { context in
|
||||
let component = context.component
|
||||
|
||||
let text = text.update(
|
||||
component: MultilineTextComponent(text: .plain(component.text)),
|
||||
availableSize: CGSize(width: 100.0, height: 40.0),
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let iconSize = CGSize(width: 20.0, height: 20.0)
|
||||
let icon = icon.update(
|
||||
component: BundleIconComponent(
|
||||
name: "Premium/Stars/Star",
|
||||
tintColor: nil
|
||||
),
|
||||
availableSize: iconSize,
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let spacing: CGFloat = 3.0
|
||||
let totalWidth = text.size.width + spacing + iconSize.width
|
||||
let size = CGSize(width: totalWidth, height: iconSize.height)
|
||||
|
||||
context.add(text
|
||||
.position(CGPoint(x: text.size.width / 2.0, y: size.height / 2.0))
|
||||
)
|
||||
context.add(icon
|
||||
.position(CGPoint(x: totalWidth - iconSize.width / 2.0, y: size.height / 2.0))
|
||||
)
|
||||
return size
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,795 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import ComponentDisplayAdapters
|
||||
import TelegramPresentationData
|
||||
|
||||
final class StarsTransactionsPanelContainerEnvironment: Equatable {
|
||||
let isScrollable: Bool
|
||||
|
||||
init(
|
||||
isScrollable: Bool
|
||||
) {
|
||||
self.isScrollable = isScrollable
|
||||
}
|
||||
|
||||
static func ==(lhs: StarsTransactionsPanelContainerEnvironment, rhs: StarsTransactionsPanelContainerEnvironment) -> Bool {
|
||||
if lhs.isScrollable != rhs.isScrollable {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
final class StarsTransactionsPanelEnvironment: Equatable {
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let dateTimeFormat: PresentationDateTimeFormat
|
||||
let containerInsets: UIEdgeInsets
|
||||
let isScrollable: Bool
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
dateTimeFormat: PresentationDateTimeFormat,
|
||||
containerInsets: UIEdgeInsets,
|
||||
isScrollable: Bool
|
||||
) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.dateTimeFormat = dateTimeFormat
|
||||
self.containerInsets = containerInsets
|
||||
self.isScrollable = isScrollable
|
||||
}
|
||||
|
||||
static func ==(lhs: StarsTransactionsPanelEnvironment, rhs: StarsTransactionsPanelEnvironment) -> Bool {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.dateTimeFormat != rhs.dateTimeFormat {
|
||||
return false
|
||||
}
|
||||
if lhs.containerInsets != rhs.containerInsets {
|
||||
return false
|
||||
}
|
||||
if lhs.isScrollable != rhs.isScrollable {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private final class StarsTransactionsHeaderItemComponent: CombinedComponent {
|
||||
let theme: PresentationTheme
|
||||
let title: String
|
||||
let activityFraction: CGFloat
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
title: String,
|
||||
activityFraction: CGFloat
|
||||
) {
|
||||
self.theme = theme
|
||||
self.title = title
|
||||
self.activityFraction = activityFraction
|
||||
}
|
||||
|
||||
static func ==(lhs: StarsTransactionsHeaderItemComponent, rhs: StarsTransactionsHeaderItemComponent) -> Bool {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.activityFraction != rhs.activityFraction {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let activeText = Child(Text.self)
|
||||
let inactiveText = Child(Text.self)
|
||||
|
||||
return { context in
|
||||
let activeText = activeText.update(
|
||||
component: Text(text: context.component.title, font: Font.medium(14.0), color: context.component.theme.list.itemAccentColor),
|
||||
availableSize: context.availableSize,
|
||||
transition: .immediate
|
||||
)
|
||||
let inactiveText = inactiveText.update(
|
||||
component: Text(text: context.component.title, font: Font.medium(14.0), color: context.component.theme.list.itemSecondaryTextColor),
|
||||
availableSize: context.availableSize,
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
context.add(activeText
|
||||
.position(CGPoint(x: activeText.size.width * 0.5, y: activeText.size.height * 0.5))
|
||||
.opacity(context.component.activityFraction)
|
||||
)
|
||||
context.add(inactiveText
|
||||
.position(CGPoint(x: inactiveText.size.width * 0.5, y: inactiveText.size.height * 0.5))
|
||||
.opacity(1.0 - context.component.activityFraction)
|
||||
)
|
||||
|
||||
return activeText.size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension CGFloat {
|
||||
func interpolate(with other: CGFloat, fraction: CGFloat) -> CGFloat {
|
||||
let invT = 1.0 - fraction
|
||||
let result = other * fraction + self * invT
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private extension CGPoint {
|
||||
func interpolate(with other: CGPoint, fraction: CGFloat) -> CGPoint {
|
||||
return CGPoint(x: self.x.interpolate(with: other.x, fraction: fraction), y: self.y.interpolate(with: other.y, fraction: fraction))
|
||||
}
|
||||
}
|
||||
|
||||
private extension CGSize {
|
||||
func interpolate(with other: CGSize, fraction: CGFloat) -> CGSize {
|
||||
return CGSize(width: self.width.interpolate(with: other.width, fraction: fraction), height: self.height.interpolate(with: other.height, fraction: fraction))
|
||||
}
|
||||
}
|
||||
|
||||
private extension CGRect {
|
||||
func interpolate(with other: CGRect, fraction: CGFloat) -> CGRect {
|
||||
return CGRect(origin: self.origin.interpolate(with: other.origin, fraction: fraction), size: self.size.interpolate(with: other.size, fraction: fraction))
|
||||
}
|
||||
}
|
||||
|
||||
private final class StarsTransactionsHeaderComponent: Component {
|
||||
struct Item: Equatable {
|
||||
let id: AnyHashable
|
||||
let title: String
|
||||
|
||||
init(
|
||||
id: AnyHashable,
|
||||
title: String
|
||||
) {
|
||||
self.id = id
|
||||
self.title = title
|
||||
}
|
||||
}
|
||||
|
||||
let theme: PresentationTheme
|
||||
let items: [Item]
|
||||
let activeIndex: Int
|
||||
let transitionFraction: CGFloat
|
||||
let switchToPanel: (AnyHashable) -> Void
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
items: [Item],
|
||||
activeIndex: Int,
|
||||
transitionFraction: CGFloat,
|
||||
switchToPanel: @escaping (AnyHashable) -> Void
|
||||
) {
|
||||
self.theme = theme
|
||||
self.items = items
|
||||
self.activeIndex = activeIndex
|
||||
self.transitionFraction = transitionFraction
|
||||
self.switchToPanel = switchToPanel
|
||||
}
|
||||
|
||||
static func ==(lhs: StarsTransactionsHeaderComponent, rhs: StarsTransactionsHeaderComponent) -> Bool {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.items != rhs.items {
|
||||
return false
|
||||
}
|
||||
if lhs.activeIndex != rhs.activeIndex {
|
||||
return false
|
||||
}
|
||||
if lhs.transitionFraction != rhs.transitionFraction {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
class View: UIView {
|
||||
private var component: StarsTransactionsHeaderComponent?
|
||||
|
||||
private var visibleItems: [AnyHashable: ComponentView<Empty>] = [:]
|
||||
private let activeItemLayer: SimpleLayer
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.activeItemLayer = SimpleLayer()
|
||||
self.activeItemLayer.cornerRadius = 2.0
|
||||
self.activeItemLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.layer.addSublayer(self.activeItemLayer)
|
||||
|
||||
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
let point = recognizer.location(in: self)
|
||||
var closestId: (CGFloat, AnyHashable)?
|
||||
if self.bounds.contains(point) {
|
||||
for (id, item) in self.visibleItems {
|
||||
if let itemView = item.view {
|
||||
let distance: CGFloat = min(abs(point.x - itemView.frame.minX), abs(point.x - itemView.frame.maxX))
|
||||
if let closestIdValue = closestId {
|
||||
if distance < closestIdValue.0 {
|
||||
closestId = (distance, id)
|
||||
}
|
||||
} else {
|
||||
closestId = (distance, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let closestId = closestId, let component = self.component {
|
||||
component.switchToPanel(closestId.1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: StarsTransactionsHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let themeUpdated = self.component?.theme !== component.theme
|
||||
|
||||
self.component = component
|
||||
|
||||
var validIds = Set<AnyHashable>()
|
||||
for i in 0 ..< component.items.count {
|
||||
let item = component.items[i]
|
||||
validIds.insert(item.id)
|
||||
|
||||
let itemView: ComponentView<Empty>
|
||||
var itemTransition = transition
|
||||
if let current = self.visibleItems[item.id] {
|
||||
itemView = current
|
||||
} else {
|
||||
itemTransition = .immediate
|
||||
itemView = ComponentView()
|
||||
self.visibleItems[item.id] = itemView
|
||||
}
|
||||
|
||||
let activeIndex: CGFloat = CGFloat(component.activeIndex) - component.transitionFraction
|
||||
let activityDistance: CGFloat = abs(activeIndex - CGFloat(i))
|
||||
|
||||
let activityFraction: CGFloat
|
||||
if activityDistance < 1.0 {
|
||||
activityFraction = 1.0 - activityDistance
|
||||
} else {
|
||||
activityFraction = 0.0
|
||||
}
|
||||
|
||||
let itemSize = itemView.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(StarsTransactionsHeaderItemComponent(
|
||||
theme: component.theme,
|
||||
title: item.title,
|
||||
activityFraction: activityFraction
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
|
||||
let itemHorizontalSpace = availableSize.width / CGFloat(component.items.count)
|
||||
let itemX: CGFloat
|
||||
if component.items.count == 1 {
|
||||
itemX = 37.0
|
||||
} else {
|
||||
itemX = itemHorizontalSpace * CGFloat(i) + floor((itemHorizontalSpace - itemSize.width) / 2.0)
|
||||
}
|
||||
|
||||
let itemFrame = CGRect(origin: CGPoint(x: itemX, y: floor((availableSize.height - itemSize.height) / 2.0)), size: itemSize)
|
||||
if let itemComponentView = itemView.view {
|
||||
if itemComponentView.superview == nil {
|
||||
self.addSubview(itemComponentView)
|
||||
itemComponentView.isUserInteractionEnabled = false
|
||||
}
|
||||
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
|
||||
}
|
||||
}
|
||||
|
||||
if component.activeIndex < component.items.count {
|
||||
let activeView = self.visibleItems[component.items[component.activeIndex].id]?.view
|
||||
let nextIndex: Int
|
||||
if component.transitionFraction > 0.0 {
|
||||
nextIndex = max(0, component.activeIndex - 1)
|
||||
} else {
|
||||
nextIndex = min(component.items.count - 1, component.activeIndex + 1)
|
||||
}
|
||||
let nextView = self.visibleItems[component.items[nextIndex].id]?.view
|
||||
if let activeView = activeView, let nextView = nextView {
|
||||
let mergedFrame = activeView.frame.interpolate(with: nextView.frame, fraction: abs(component.transitionFraction))
|
||||
transition.setFrame(layer: self.activeItemLayer, frame: CGRect(origin: CGPoint(x: mergedFrame.minX, y: availableSize.height - 3.0), size: CGSize(width: mergedFrame.width, height: 3.0)))
|
||||
}
|
||||
}
|
||||
|
||||
if themeUpdated {
|
||||
self.activeItemLayer.backgroundColor = component.theme.list.itemAccentColor.cgColor
|
||||
}
|
||||
|
||||
var removeIds: [AnyHashable] = []
|
||||
for (id, itemView) in self.visibleItems {
|
||||
if !validIds.contains(id) {
|
||||
removeIds.append(id)
|
||||
if let itemComponentView = itemView.view {
|
||||
itemComponentView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
for id in removeIds {
|
||||
self.visibleItems.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
final class StarsTransactionsPanelContainerComponent: Component {
|
||||
typealias EnvironmentType = StarsTransactionsPanelContainerEnvironment
|
||||
|
||||
struct Item: Equatable {
|
||||
let id: AnyHashable
|
||||
let title: String
|
||||
let panel: AnyComponent<StarsTransactionsPanelEnvironment>
|
||||
|
||||
init(
|
||||
id: AnyHashable,
|
||||
title: String,
|
||||
panel: AnyComponent<StarsTransactionsPanelEnvironment>
|
||||
) {
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.panel = panel
|
||||
}
|
||||
}
|
||||
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let dateTimeFormat: PresentationDateTimeFormat
|
||||
let insets: UIEdgeInsets
|
||||
let items: [Item]
|
||||
let currentPanelUpdated: (AnyHashable, Transition) -> Void
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
dateTimeFormat: PresentationDateTimeFormat,
|
||||
insets: UIEdgeInsets,
|
||||
items: [Item],
|
||||
currentPanelUpdated: @escaping (AnyHashable, Transition) -> Void
|
||||
) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.dateTimeFormat = dateTimeFormat
|
||||
self.insets = insets
|
||||
self.items = items
|
||||
self.currentPanelUpdated = currentPanelUpdated
|
||||
}
|
||||
|
||||
static func ==(lhs: StarsTransactionsPanelContainerComponent, rhs: StarsTransactionsPanelContainerComponent) -> Bool {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.dateTimeFormat != rhs.dateTimeFormat {
|
||||
return false
|
||||
}
|
||||
if lhs.insets != rhs.insets {
|
||||
return false
|
||||
}
|
||||
if lhs.items != rhs.items {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
class View: UIView, UIGestureRecognizerDelegate {
|
||||
private let topPanelBackgroundView: UIView
|
||||
private let topPanelMergedBackgroundView: UIView
|
||||
private let topPanelSeparatorLayer: SimpleLayer
|
||||
private let header = ComponentView<Empty>()
|
||||
|
||||
private var component: StarsTransactionsPanelContainerComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
private let panelsBackgroundLayer: SimpleLayer
|
||||
private var visiblePanels: [AnyHashable: ComponentView<StarsTransactionsPanelEnvironment>] = [:]
|
||||
private var actualVisibleIds = Set<AnyHashable>()
|
||||
private var currentId: AnyHashable?
|
||||
private var transitionFraction: CGFloat = 0.0
|
||||
private var animatingTransition: Bool = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.topPanelBackgroundView = UIView()
|
||||
|
||||
self.topPanelMergedBackgroundView = UIView()
|
||||
self.topPanelMergedBackgroundView.alpha = 0.0
|
||||
|
||||
self.topPanelSeparatorLayer = SimpleLayer()
|
||||
|
||||
self.panelsBackgroundLayer = SimpleLayer()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.layer.addSublayer(self.panelsBackgroundLayer)
|
||||
self.addSubview(self.topPanelBackgroundView)
|
||||
self.addSubview(self.topPanelMergedBackgroundView)
|
||||
self.layer.addSublayer(self.topPanelSeparatorLayer)
|
||||
|
||||
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] point in
|
||||
guard let self, let component = self.component, let currentId = self.currentId else {
|
||||
return []
|
||||
}
|
||||
guard let index = component.items.firstIndex(where: { $0.id == currentId }) else {
|
||||
return []
|
||||
}
|
||||
|
||||
/*if strongSelf.tabsContainerNode.bounds.contains(strongSelf.view.convert(point, to: strongSelf.tabsContainerNode.view)) {
|
||||
return []
|
||||
}*/
|
||||
|
||||
if index == 0 {
|
||||
return .left
|
||||
}
|
||||
return [.left, .right]
|
||||
})
|
||||
panRecognizer.delegate = self
|
||||
panRecognizer.delaysTouchesBegan = false
|
||||
panRecognizer.cancelsTouchesInView = true
|
||||
self.addGestureRecognizer(panRecognizer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
var currentPanelView: UIView? {
|
||||
guard let currentId = self.currentId, let panel = self.visiblePanels[currentId] else {
|
||||
return nil
|
||||
}
|
||||
return panel.view
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer {
|
||||
return false
|
||||
}
|
||||
if let _ = otherGestureRecognizer as? UIPanGestureRecognizer {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
func cancelContextGestures(view: UIView) {
|
||||
if let gestureRecognizers = view.gestureRecognizers {
|
||||
for gesture in gestureRecognizers {
|
||||
if let gesture = gesture as? ContextGesture {
|
||||
gesture.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
for subview in view.subviews {
|
||||
cancelContextGestures(view: subview)
|
||||
}
|
||||
}
|
||||
|
||||
cancelContextGestures(view: self)
|
||||
|
||||
//self.animatingTransition = true
|
||||
case .changed:
|
||||
guard let component = self.component, let currentId = self.currentId else {
|
||||
return
|
||||
}
|
||||
guard let index = component.items.firstIndex(where: { $0.id == currentId }) else {
|
||||
return
|
||||
}
|
||||
|
||||
let translation = recognizer.translation(in: self)
|
||||
var transitionFraction = translation.x / self.bounds.width
|
||||
if index <= 0 {
|
||||
transitionFraction = min(0.0, transitionFraction)
|
||||
}
|
||||
if index >= component.items.count - 1 {
|
||||
transitionFraction = max(0.0, transitionFraction)
|
||||
}
|
||||
self.transitionFraction = transitionFraction
|
||||
self.state?.updated(transition: .immediate)
|
||||
case .cancelled, .ended:
|
||||
guard let component = self.component, let currentId = self.currentId else {
|
||||
return
|
||||
}
|
||||
guard let index = component.items.firstIndex(where: { $0.id == currentId }) else {
|
||||
return
|
||||
}
|
||||
|
||||
let translation = recognizer.translation(in: self)
|
||||
let velocity = recognizer.velocity(in: self)
|
||||
var directionIsToRight: Bool?
|
||||
if abs(velocity.x) > 10.0 {
|
||||
directionIsToRight = velocity.x < 0.0
|
||||
} else {
|
||||
if abs(translation.x) > self.bounds.width / 2.0 {
|
||||
directionIsToRight = translation.x > self.bounds.width / 2.0
|
||||
}
|
||||
}
|
||||
if let directionIsToRight = directionIsToRight {
|
||||
var updatedIndex = index
|
||||
if directionIsToRight {
|
||||
updatedIndex = min(updatedIndex + 1, component.items.count - 1)
|
||||
} else {
|
||||
updatedIndex = max(updatedIndex - 1, 0)
|
||||
}
|
||||
self.currentId = component.items[updatedIndex].id
|
||||
}
|
||||
self.transitionFraction = 0.0
|
||||
|
||||
let transition = Transition(animation: .curve(duration: 0.35, curve: .spring))
|
||||
if let currentId = self.currentId {
|
||||
self.state?.updated(transition: transition)
|
||||
component.currentPanelUpdated(currentId, transition)
|
||||
}
|
||||
|
||||
self.animatingTransition = false
|
||||
//self.currentPaneUpdated?(false)
|
||||
|
||||
//self.currentPaneStatusPromise.set(self.currentPane?.node.status ?? .single(nil))
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func updateNavigationMergeFactor(value: CGFloat, transition: Transition) {
|
||||
transition.setAlpha(view: self.topPanelMergedBackgroundView, alpha: value)
|
||||
transition.setAlpha(view: self.topPanelBackgroundView, alpha: 1.0 - value)
|
||||
}
|
||||
|
||||
func update(component: StarsTransactionsPanelContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<StarsTransactionsPanelContainerEnvironment>, transition: Transition) -> CGSize {
|
||||
let environment = environment[StarsTransactionsPanelContainerEnvironment.self].value
|
||||
|
||||
let themeUpdated = self.component?.theme !== component.theme
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
if themeUpdated {
|
||||
self.panelsBackgroundLayer.backgroundColor = component.theme.list.itemBlocksBackgroundColor.cgColor
|
||||
self.topPanelSeparatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor
|
||||
self.topPanelBackgroundView.backgroundColor = component.theme.list.itemBlocksBackgroundColor
|
||||
self.topPanelMergedBackgroundView.backgroundColor = component.theme.rootController.navigationBar.blurredBackgroundColor
|
||||
}
|
||||
|
||||
let topPanelCoverHeight: CGFloat = 10.0
|
||||
|
||||
let topPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: -topPanelCoverHeight), size: CGSize(width: availableSize.width, height: 44.0))
|
||||
transition.setFrame(view: self.topPanelBackgroundView, frame: topPanelFrame)
|
||||
transition.setFrame(view: self.topPanelMergedBackgroundView, frame: topPanelFrame)
|
||||
|
||||
transition.setFrame(layer: self.panelsBackgroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelFrame.maxY)))
|
||||
|
||||
transition.setFrame(layer: self.topPanelSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
|
||||
|
||||
if let currentIdValue = self.currentId, !component.items.contains(where: { $0.id == currentIdValue }) {
|
||||
self.currentId = nil
|
||||
}
|
||||
if self.currentId == nil {
|
||||
self.currentId = component.items.first?.id
|
||||
}
|
||||
|
||||
var visibleIds = Set<AnyHashable>()
|
||||
var currentIndex: Int?
|
||||
if let currentId = self.currentId {
|
||||
visibleIds.insert(currentId)
|
||||
|
||||
if let index = component.items.firstIndex(where: { $0.id == currentId }) {
|
||||
currentIndex = index
|
||||
if index != 0 {
|
||||
visibleIds.insert(component.items[index - 1].id)
|
||||
}
|
||||
if index != component.items.count - 1 {
|
||||
visibleIds.insert(component.items[index + 1].id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = self.header.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(StarsTransactionsHeaderComponent(
|
||||
theme: component.theme,
|
||||
items: component.items.map { item -> StarsTransactionsHeaderComponent.Item in
|
||||
return StarsTransactionsHeaderComponent.Item(
|
||||
id: item.id,
|
||||
title: item.title
|
||||
)
|
||||
},
|
||||
activeIndex: currentIndex ?? 0,
|
||||
transitionFraction: self.transitionFraction,
|
||||
switchToPanel: { [weak self] id in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
if component.items.contains(where: { $0.id == id }) {
|
||||
self.currentId = id
|
||||
let transition = Transition(animation: .curve(duration: 0.35, curve: .spring))
|
||||
self.state?.updated(transition: transition)
|
||||
component.currentPanelUpdated(id, transition)
|
||||
}
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: topPanelFrame.size
|
||||
)
|
||||
if let headerView = self.header.view {
|
||||
if headerView.superview == nil {
|
||||
self.addSubview(headerView)
|
||||
}
|
||||
transition.setFrame(view: headerView, frame: topPanelFrame)
|
||||
}
|
||||
|
||||
let childEnvironment = StarsTransactionsPanelEnvironment(
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
dateTimeFormat: component.dateTimeFormat,
|
||||
containerInsets: UIEdgeInsets(top: 0.0, left: component.insets.left, bottom: component.insets.bottom, right: component.insets.right),
|
||||
isScrollable: environment.isScrollable
|
||||
)
|
||||
|
||||
let centralPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelFrame.maxY))
|
||||
|
||||
if self.animatingTransition {
|
||||
visibleIds = visibleIds.filter({ self.visiblePanels[$0] != nil })
|
||||
}
|
||||
|
||||
self.actualVisibleIds = visibleIds
|
||||
|
||||
for (id, _) in self.visiblePanels {
|
||||
visibleIds.insert(id)
|
||||
}
|
||||
|
||||
var validIds = Set<AnyHashable>()
|
||||
if let currentIndex {
|
||||
var anyAnchorOffset: CGFloat = 0.0
|
||||
for (id, panel) in self.visiblePanels {
|
||||
guard let itemIndex = component.items.firstIndex(where: { $0.id == id }), let panelView = panel.view else {
|
||||
continue
|
||||
}
|
||||
var itemFrame = centralPanelFrame.offsetBy(dx: self.transitionFraction * availableSize.width, dy: 0.0)
|
||||
if itemIndex < currentIndex {
|
||||
itemFrame.origin.x -= itemFrame.width
|
||||
} else if itemIndex > currentIndex {
|
||||
itemFrame.origin.x += itemFrame.width
|
||||
}
|
||||
|
||||
anyAnchorOffset = itemFrame.minX - panelView.frame.minX
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
for id in visibleIds {
|
||||
guard let itemIndex = component.items.firstIndex(where: { $0.id == id }) else {
|
||||
continue
|
||||
}
|
||||
let panelItem = component.items[itemIndex]
|
||||
|
||||
var itemFrame = centralPanelFrame.offsetBy(dx: self.transitionFraction * availableSize.width, dy: 0.0)
|
||||
if itemIndex < currentIndex {
|
||||
itemFrame.origin.x -= itemFrame.width
|
||||
} else if itemIndex > currentIndex {
|
||||
itemFrame.origin.x += itemFrame.width
|
||||
}
|
||||
|
||||
validIds.insert(panelItem.id)
|
||||
|
||||
let panel: ComponentView<StarsTransactionsPanelEnvironment>
|
||||
var panelTransition = transition
|
||||
var animateInIfNeeded = false
|
||||
if let current = self.visiblePanels[panelItem.id] {
|
||||
panel = current
|
||||
|
||||
if let panelView = panel.view, !panelView.bounds.isEmpty {
|
||||
var wasHidden = false
|
||||
if abs(panelView.frame.minX - availableSize.width) < .ulpOfOne || abs(panelView.frame.maxX - 0.0) < .ulpOfOne {
|
||||
wasHidden = true
|
||||
}
|
||||
var isHidden = false
|
||||
if abs(itemFrame.minX - availableSize.width) < .ulpOfOne || abs(itemFrame.maxX - 0.0) < .ulpOfOne {
|
||||
isHidden = true
|
||||
}
|
||||
if wasHidden && isHidden {
|
||||
panelTransition = .immediate
|
||||
}
|
||||
}
|
||||
} else {
|
||||
panelTransition = .immediate
|
||||
animateInIfNeeded = true
|
||||
|
||||
panel = ComponentView()
|
||||
self.visiblePanels[panelItem.id] = panel
|
||||
}
|
||||
let _ = panel.update(
|
||||
transition: panelTransition,
|
||||
component: panelItem.panel,
|
||||
environment: {
|
||||
childEnvironment
|
||||
},
|
||||
containerSize: centralPanelFrame.size
|
||||
)
|
||||
if let panelView = panel.view {
|
||||
if panelView.superview == nil {
|
||||
self.insertSubview(panelView, belowSubview: self.topPanelBackgroundView)
|
||||
}
|
||||
|
||||
panelTransition.setFrame(view: panelView, frame: itemFrame, completion: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if !self.actualVisibleIds.contains(id) {
|
||||
if let panel = self.visiblePanels[id] {
|
||||
self.visiblePanels.removeValue(forKey: id)
|
||||
panel.view?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
})
|
||||
if animateInIfNeeded && anyAnchorOffset != 0.0 {
|
||||
transition.animatePosition(view: panelView, from: CGPoint(x: -anyAnchorOffset, y: 0.0), to: CGPoint(), additive: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var removeIds: [AnyHashable] = []
|
||||
for (id, panel) in self.visiblePanels {
|
||||
if !validIds.contains(id) {
|
||||
removeIds.append(id)
|
||||
if let panelView = panel.view {
|
||||
panelView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
for id in removeIds {
|
||||
self.visiblePanels.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<StarsTransactionsPanelContainerEnvironment>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
@ -0,0 +1,694 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import ViewControllerComponent
|
||||
import ComponentDisplayAdapters
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import MultilineTextComponent
|
||||
import BalancedTextComponent
|
||||
import Markdown
|
||||
import PremiumStarComponent
|
||||
import ListSectionComponent
|
||||
import TextFormat
|
||||
|
||||
final class StarsTransactionsScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let starsContext: StarsContext
|
||||
let buy: () -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
starsContext: StarsContext,
|
||||
buy: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.starsContext = starsContext
|
||||
self.buy = buy
|
||||
}
|
||||
|
||||
static func ==(lhs: StarsTransactionsScreenComponent, rhs: StarsTransactionsScreenComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.starsContext !== rhs.starsContext {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private final class ScrollViewImpl: UIScrollView {
|
||||
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override var contentOffset: CGPoint {
|
||||
set(value) {
|
||||
var value = value
|
||||
if value.y > self.contentSize.height - self.bounds.height {
|
||||
value.y = max(0.0, self.contentSize.height - self.bounds.height)
|
||||
self.bounces = false
|
||||
} else {
|
||||
self.bounces = true
|
||||
}
|
||||
super.contentOffset = value
|
||||
} get {
|
||||
return super.contentOffset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class View: UIView, UIScrollViewDelegate {
|
||||
private let scrollView: ScrollViewImpl
|
||||
|
||||
private var currentSelectedPanelId: AnyHashable?
|
||||
|
||||
private let navigationBackgroundView: BlurredBackgroundView
|
||||
private let navigationSeparatorLayer: SimpleLayer
|
||||
private let navigationSeparatorLayerContainer: SimpleLayer
|
||||
|
||||
private let headerView = ComponentView<Empty>()
|
||||
private let headerOffsetContainer: UIView
|
||||
|
||||
private let scrollContainerView: UIView
|
||||
|
||||
private let overscroll = ComponentView<Empty>()
|
||||
private let fade = ComponentView<Empty>()
|
||||
private let starView = ComponentView<Empty>()
|
||||
private let titleView = ComponentView<Empty>()
|
||||
private let descriptionView = ComponentView<Empty>()
|
||||
|
||||
private let balanceView = ComponentView<Empty>()
|
||||
|
||||
private let topBalanceView = ComponentView<Empty>()
|
||||
|
||||
private let panelContainer = ComponentView<StarsTransactionsPanelContainerEnvironment>()
|
||||
|
||||
private var component: StarsTransactionsScreenComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
private var navigationMetrics: (navigationHeight: CGFloat, statusBarHeight: CGFloat)?
|
||||
private var controller: (() -> ViewController?)?
|
||||
|
||||
private var enableVelocityTracking: Bool = false
|
||||
private var previousVelocityM1: CGFloat = 0.0
|
||||
private var previousVelocity: CGFloat = 0.0
|
||||
|
||||
private var ignoreScrolling: Bool = false
|
||||
|
||||
private var stateDisposable: Disposable?
|
||||
private var starsState: StarsContext.State?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.headerOffsetContainer = UIView()
|
||||
self.headerOffsetContainer.isUserInteractionEnabled = false
|
||||
|
||||
self.navigationBackgroundView = BlurredBackgroundView(color: nil, enableBlur: true)
|
||||
self.navigationBackgroundView.alpha = 0.0
|
||||
|
||||
self.navigationSeparatorLayer = SimpleLayer()
|
||||
self.navigationSeparatorLayer.opacity = 0.0
|
||||
self.navigationSeparatorLayerContainer = SimpleLayer()
|
||||
self.navigationSeparatorLayerContainer.opacity = 0.0
|
||||
|
||||
self.scrollContainerView = UIView()
|
||||
self.scrollView = ScrollViewImpl()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.scrollView.delaysContentTouches = true
|
||||
self.scrollView.canCancelContentTouches = true
|
||||
self.scrollView.clipsToBounds = false
|
||||
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
||||
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.scrollsToTop = false
|
||||
self.scrollView.delegate = self
|
||||
self.scrollView.clipsToBounds = true
|
||||
self.addSubview(self.scrollView)
|
||||
|
||||
self.scrollView.addSubview(self.scrollContainerView)
|
||||
|
||||
self.addSubview(self.navigationBackgroundView)
|
||||
|
||||
self.navigationSeparatorLayerContainer.addSublayer(self.navigationSeparatorLayer)
|
||||
self.layer.addSublayer(self.navigationSeparatorLayerContainer)
|
||||
|
||||
self.addSubview(self.headerOffsetContainer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.stateDisposable?.dispose()
|
||||
}
|
||||
|
||||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
self.enableVelocityTracking = true
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if !self.ignoreScrolling {
|
||||
if self.enableVelocityTracking {
|
||||
self.previousVelocityM1 = self.previousVelocity
|
||||
if let value = (scrollView.value(forKey: (["_", "verticalVelocity"] as [String]).joined()) as? NSNumber)?.doubleValue {
|
||||
self.previousVelocity = CGFloat(value)
|
||||
}
|
||||
}
|
||||
|
||||
self.updateScrolling(transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
||||
guard let _ = self.navigationMetrics else {
|
||||
return
|
||||
}
|
||||
|
||||
let paneAreaExpansionDistance: CGFloat = 32.0
|
||||
let paneAreaExpansionFinalPoint: CGFloat = scrollView.contentSize.height - scrollView.bounds.height
|
||||
if targetContentOffset.pointee.y > paneAreaExpansionFinalPoint - paneAreaExpansionDistance && targetContentOffset.pointee.y < paneAreaExpansionFinalPoint {
|
||||
targetContentOffset.pointee.y = paneAreaExpansionFinalPoint
|
||||
self.enableVelocityTracking = false
|
||||
self.previousVelocity = 0.0
|
||||
self.previousVelocityM1 = 0.0
|
||||
}
|
||||
}
|
||||
|
||||
private func updateScrolling(transition: Transition) {
|
||||
let scrollBounds = self.scrollView.bounds
|
||||
|
||||
let isLockedAtPanels = scrollBounds.maxY == self.scrollView.contentSize.height
|
||||
|
||||
if let navigationMetrics = self.navigationMetrics {
|
||||
let topInset: CGFloat = navigationMetrics.navigationHeight - 56.0
|
||||
|
||||
let titleOffset: CGFloat
|
||||
let titleScale: CGFloat
|
||||
let titleOffsetDelta = (topInset + 160.0) - (navigationMetrics.statusBarHeight + (navigationMetrics.navigationHeight - navigationMetrics.statusBarHeight) / 2.0)
|
||||
|
||||
var topContentOffset = self.scrollView.contentOffset.y
|
||||
|
||||
let navigationBackgroundAlpha = min(20.0, max(0.0, topContentOffset - 95.0)) / 20.0
|
||||
topContentOffset = topContentOffset + max(0.0, min(1.0, topContentOffset / titleOffsetDelta)) * 10.0
|
||||
titleOffset = topContentOffset
|
||||
let fraction = max(0.0, min(1.0, titleOffset / titleOffsetDelta))
|
||||
titleScale = 1.0 - fraction * 0.36
|
||||
|
||||
let headerTransition: Transition = .immediate
|
||||
|
||||
if let starView = self.starView.view {
|
||||
let starPosition = CGPoint(x: self.scrollView.frame.width / 2.0, y: topInset + starView.bounds.height / 2.0 - 30.0 - titleOffset * titleScale)
|
||||
|
||||
headerTransition.setPosition(view: starView, position: starPosition)
|
||||
headerTransition.setScale(view: starView, scale: titleScale)
|
||||
}
|
||||
|
||||
if let titleView = self.titleView.view {
|
||||
let titlePosition = CGPoint(x: scrollBounds.width / 2.0, y: max(topInset + 160.0 - titleOffset, navigationMetrics.statusBarHeight + (navigationMetrics.navigationHeight - navigationMetrics.statusBarHeight) / 2.0))
|
||||
|
||||
headerTransition.setPosition(view: titleView, position: titlePosition)
|
||||
headerTransition.setScale(view: titleView, scale: titleScale)
|
||||
}
|
||||
|
||||
let animatedTransition = Transition(animation: .curve(duration: 0.18, curve: .easeInOut))
|
||||
animatedTransition.setAlpha(view: self.navigationBackgroundView, alpha: navigationBackgroundAlpha)
|
||||
animatedTransition.setAlpha(layer: self.navigationSeparatorLayerContainer, alpha: navigationBackgroundAlpha)
|
||||
|
||||
let expansionDistance: CGFloat = 32.0
|
||||
var expansionDistanceFactor: CGFloat = abs(scrollBounds.maxY - self.scrollView.contentSize.height) / expansionDistance
|
||||
expansionDistanceFactor = max(0.0, min(1.0, expansionDistanceFactor))
|
||||
|
||||
transition.setAlpha(layer: self.navigationSeparatorLayer, alpha: expansionDistanceFactor)
|
||||
if let panelContainerView = self.panelContainer.view as? StarsTransactionsPanelContainerComponent.View {
|
||||
panelContainerView.updateNavigationMergeFactor(value: 1.0 - expansionDistanceFactor, transition: transition)
|
||||
}
|
||||
|
||||
if let topBalanceView = self.topBalanceView.view {
|
||||
topBalanceView.alpha = 1.0 - expansionDistanceFactor
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// if let headerView = self.headerView.view, let navigationMetrics = self.navigationMetrics {
|
||||
// var headerOffset: CGFloat = scrollBounds.minY
|
||||
//
|
||||
// let minY = navigationMetrics.statusBarHeight + floor((navigationMetrics.navigationHeight - navigationMetrics.statusBarHeight) / 2.0)
|
||||
//
|
||||
// let minOffset = headerView.center.y - minY
|
||||
//
|
||||
// headerOffset = min(headerOffset, minOffset)
|
||||
//
|
||||
// let animatedTransition = Transition(animation: .curve(duration: 0.18, curve: .easeInOut))
|
||||
// let navigationBackgroundAlpha: CGFloat = abs(headerOffset - minOffset) < 4.0 ? 1.0 : 0.0
|
||||
//
|
||||
// animatedTransition.setAlpha(view: self.navigationBackgroundView, alpha: navigationBackgroundAlpha)
|
||||
// animatedTransition.setAlpha(layer: self.navigationSeparatorLayerContainer, alpha: navigationBackgroundAlpha)
|
||||
//
|
||||
// let expansionDistance: CGFloat = 32.0
|
||||
// var expansionDistanceFactor: CGFloat = abs(scrollBounds.maxY - self.scrollView.contentSize.height) / expansionDistance
|
||||
// expansionDistanceFactor = max(0.0, min(1.0, expansionDistanceFactor))
|
||||
//
|
||||
// transition.setAlpha(layer: self.navigationSeparatorLayer, alpha: expansionDistanceFactor)
|
||||
// if let panelContainerView = self.panelContainer.view as? StarsTransactionsPanelContainerComponent.View {
|
||||
// panelContainerView.updateNavigationMergeFactor(value: 1.0 - expansionDistanceFactor, transition: transition)
|
||||
// }
|
||||
//
|
||||
// var offsetFraction: CGFloat = abs(headerOffset - minOffset) / 60.0
|
||||
// offsetFraction = min(1.0, max(0.0, offsetFraction))
|
||||
// transition.setScale(view: headerView, scale: 1.0 * offsetFraction + 0.8 * (1.0 - offsetFraction))
|
||||
//
|
||||
// transition.setBounds(view: self.headerOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: headerOffset), size: self.headerOffsetContainer.bounds.size))
|
||||
// }
|
||||
|
||||
let _ = self.panelContainer.updateEnvironment(
|
||||
transition: transition,
|
||||
environment: {
|
||||
StarsTransactionsPanelContainerEnvironment(isScrollable: isLockedAtPanels)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private var previousBalance: Int64?
|
||||
|
||||
private var isUpdating = false
|
||||
func update(component: StarsTransactionsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
self.isUpdating = false
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
var balanceUpdated = false
|
||||
if let starsState = self.starsState {
|
||||
if let previousBalance, starsState.balance != previousBalance {
|
||||
balanceUpdated = true
|
||||
}
|
||||
self.previousBalance = starsState.balance
|
||||
}
|
||||
|
||||
let environment = environment[ViewControllerComponentContainer.Environment.self].value
|
||||
|
||||
if self.stateDisposable == nil {
|
||||
self.stateDisposable = (component.starsContext.state
|
||||
|> deliverOnMainQueue).start(next: { [weak self] state in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.starsState = state
|
||||
if !self.isUpdating {
|
||||
self.state?.updated()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var wasLockedAtPanels = false
|
||||
if let panelContainerView = self.panelContainer.view, let navigationMetrics = self.navigationMetrics {
|
||||
if self.scrollView.bounds.minY > 0.0 && abs(self.scrollView.bounds.minY - (panelContainerView.frame.minY - navigationMetrics.navigationHeight)) <= UIScreenPixel {
|
||||
wasLockedAtPanels = true
|
||||
}
|
||||
}
|
||||
|
||||
self.controller = environment.controller
|
||||
|
||||
self.navigationMetrics = (environment.navigationHeight, environment.statusBarHeight)
|
||||
|
||||
self.navigationSeparatorLayer.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor
|
||||
|
||||
let navigationFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: environment.navigationHeight))
|
||||
self.navigationBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate)
|
||||
self.navigationBackgroundView.update(size: navigationFrame.size, transition: transition.containedViewLayoutTransition)
|
||||
transition.setFrame(view: self.navigationBackgroundView, frame: navigationFrame)
|
||||
|
||||
let navigationSeparatorFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationFrame.maxY), size: CGSize(width: availableSize.width, height: UIScreenPixel))
|
||||
|
||||
transition.setFrame(layer: self.navigationSeparatorLayerContainer, frame: navigationSeparatorFrame)
|
||||
transition.setFrame(layer: self.navigationSeparatorLayer, frame: CGRect(origin: CGPoint(), size: navigationSeparatorFrame.size))
|
||||
|
||||
self.backgroundColor = environment.theme.list.blocksBackgroundColor
|
||||
|
||||
var contentHeight: CGFloat = 0.0
|
||||
|
||||
let sideInsets: CGFloat = environment.safeInsets.left + environment.safeInsets.right + 16 * 2.0
|
||||
let bottomInset: CGFloat = environment.safeInsets.bottom
|
||||
|
||||
contentHeight += environment.statusBarHeight
|
||||
|
||||
let starTransition: Transition = .immediate
|
||||
|
||||
var topBackgroundColor = environment.theme.list.plainBackgroundColor
|
||||
let bottomBackgroundColor = environment.theme.list.blocksBackgroundColor
|
||||
if environment.theme.overallDarkAppearance {
|
||||
topBackgroundColor = bottomBackgroundColor
|
||||
}
|
||||
|
||||
let overscrollSize = self.overscroll.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Rectangle(color: topBackgroundColor)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 1000.0)
|
||||
)
|
||||
let overscrollFrame = CGRect(origin: CGPoint(x: 0.0, y: -overscrollSize.height), size: overscrollSize)
|
||||
if let overscrollView = self.overscroll.view {
|
||||
if overscrollView.superview == nil {
|
||||
self.scrollView.addSubview(overscrollView)
|
||||
}
|
||||
starTransition.setFrame(view: overscrollView, frame: overscrollFrame)
|
||||
}
|
||||
|
||||
let fadeSize = self.fade.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(RoundedRectangle(
|
||||
colors: [
|
||||
topBackgroundColor,
|
||||
bottomBackgroundColor
|
||||
],
|
||||
cornerRadius: 0.0,
|
||||
gradientDirection: .vertical
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 1000.0)
|
||||
)
|
||||
let fadeFrame = CGRect(origin: CGPoint(x: 0.0, y: -fadeSize.height), size: fadeSize)
|
||||
if let fadeView = self.fade.view {
|
||||
if fadeView.superview == nil {
|
||||
self.scrollView.addSubview(fadeView)
|
||||
}
|
||||
starTransition.setFrame(view: fadeView, frame: fadeFrame)
|
||||
}
|
||||
|
||||
let starSize = self.starView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(PremiumStarComponent(
|
||||
isIntro: true,
|
||||
isVisible: true,
|
||||
hasIdleAnimations: true,
|
||||
colors: [
|
||||
UIColor(rgb: 0xea8904),
|
||||
UIColor(rgb: 0xf09903),
|
||||
UIColor(rgb: 0xfec209),
|
||||
UIColor(rgb: 0xfed31a)
|
||||
]
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: min(414.0, availableSize.width), height: 220.0)
|
||||
)
|
||||
let starFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: starSize)
|
||||
if let starView = self.starView.view {
|
||||
if starView.superview == nil {
|
||||
self.insertSubview(starView, aboveSubview: self.scrollView)
|
||||
}
|
||||
starTransition.setFrame(view: starView, frame: starFrame)
|
||||
}
|
||||
|
||||
let titleSize = self.titleView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "Telegram Stars", font: Font.bold(28.0), textColor: environment.theme.list.itemPrimaryTextColor)),
|
||||
horizontalAlignment: .center,
|
||||
truncationType: .end,
|
||||
maximumNumberOfLines: 1
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
if let titleView = self.titleView.view {
|
||||
if titleView.superview == nil {
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
starTransition.setBounds(view: titleView, bounds: CGRect(origin: .zero, size: titleSize))
|
||||
}
|
||||
|
||||
let textFont = Font.regular(14.0)
|
||||
let boldTextFont = Font.semibold(14.0)
|
||||
let textColor = environment.theme.actionSheet.primaryTextColor
|
||||
let linkColor = environment.theme.actionSheet.controlAccentColor
|
||||
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in
|
||||
return (TelegramTextAttributes.URL, contents)
|
||||
})
|
||||
let balanceAttributedString = parseMarkdownIntoAttributedString(" Balance\n > **\(starsState?.balance ?? 0)**", attributes: markdownAttributes, textAlignment: .right).mutableCopy() as! NSMutableAttributedString
|
||||
if let range = balanceAttributedString.string.range(of: ">"), let chevronImage = generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: UIColor(rgb: 0xf09903)) {
|
||||
balanceAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: balanceAttributedString.string))
|
||||
balanceAttributedString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xf09903), range: NSRange(range, in: balanceAttributedString.string))
|
||||
balanceAttributedString.addAttribute(.baselineOffset, value: 2.0, range: NSRange(range, in: balanceAttributedString.string))
|
||||
}
|
||||
let topBalanceSize = self.topBalanceView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(balanceAttributedString),
|
||||
horizontalAlignment: .right,
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.1
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 120.0, height: 100.0)
|
||||
)
|
||||
if let topBalanceView = self.topBalanceView.view {
|
||||
if topBalanceView.superview == nil {
|
||||
topBalanceView.alpha = 0.0
|
||||
self.addSubview(topBalanceView)
|
||||
}
|
||||
starTransition.setFrame(view: topBalanceView, frame: CGRect(origin: CGPoint(x: availableSize.width - topBalanceSize.width - 16.0, y: 56.0), size: topBalanceSize))
|
||||
}
|
||||
|
||||
contentHeight += 181.0
|
||||
|
||||
let descriptionSize = self.descriptionView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
BalancedTextComponent(
|
||||
text: .plain(NSAttributedString(string: "Buy Stars to unlock content and services in miniapps on Telegram.", font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.2
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInsets - 8.0, height: 240.0)
|
||||
)
|
||||
let descriptionFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - descriptionSize.width) / 2.0), y: contentHeight), size: descriptionSize)
|
||||
if let descriptionView = self.descriptionView.view {
|
||||
if descriptionView.superview == nil {
|
||||
self.scrollView.addSubview(descriptionView)
|
||||
}
|
||||
|
||||
starTransition.setFrame(view: descriptionView, frame: descriptionFrame)
|
||||
}
|
||||
|
||||
contentHeight += descriptionSize.height
|
||||
contentHeight += 29.0
|
||||
|
||||
let balanceSize = self.balanceView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(ListSectionComponent(
|
||||
theme: environment.theme,
|
||||
header: nil,
|
||||
footer: nil,
|
||||
items: [AnyComponentWithIdentity(id: 0, component: AnyComponent(
|
||||
StarsBalanceComponent(
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
count: self.starsState?.balance ?? 0,
|
||||
buy: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.buy()
|
||||
}
|
||||
)
|
||||
))]
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInsets, height: availableSize.height)
|
||||
)
|
||||
let balanceFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - balanceSize.width) / 2.0), y: contentHeight), size: balanceSize)
|
||||
if let balanceView = self.balanceView.view {
|
||||
if balanceView.superview == nil {
|
||||
self.scrollView.addSubview(balanceView)
|
||||
}
|
||||
starTransition.setFrame(view: balanceView, frame: balanceFrame)
|
||||
}
|
||||
|
||||
contentHeight += balanceSize.height
|
||||
contentHeight += 44.0
|
||||
|
||||
//TODO: localize
|
||||
let transactions = self.starsState?.transactions ?? []
|
||||
let allItems = StarsTransactionsListPanelComponent.Items(
|
||||
items: transactions.map { StarsTransactionsListPanelComponent.Item(transaction: $0) }
|
||||
)
|
||||
let incomingItems = StarsTransactionsListPanelComponent.Items(
|
||||
items: transactions.filter { $0.count > 0 }.map { StarsTransactionsListPanelComponent.Item(transaction: $0) }
|
||||
)
|
||||
let outgoingItems = StarsTransactionsListPanelComponent.Items(
|
||||
items: transactions.filter { $0.count < 0 }.map { StarsTransactionsListPanelComponent.Item(transaction: $0) }
|
||||
)
|
||||
|
||||
var panelItems: [StarsTransactionsPanelContainerComponent.Item] = []
|
||||
panelItems.append(StarsTransactionsPanelContainerComponent.Item(
|
||||
id: "all",
|
||||
title: "All Transactions",
|
||||
panel: AnyComponent(StarsTransactionsListPanelComponent(
|
||||
context: component.context,
|
||||
items: allItems,
|
||||
action: { _ in
|
||||
}
|
||||
))
|
||||
))
|
||||
|
||||
panelItems.append(StarsTransactionsPanelContainerComponent.Item(
|
||||
id: "incoming",
|
||||
title: "Incoming",
|
||||
panel: AnyComponent(StarsTransactionsListPanelComponent(
|
||||
context: component.context,
|
||||
items: incomingItems,
|
||||
action: { _ in
|
||||
}
|
||||
))
|
||||
))
|
||||
|
||||
panelItems.append(StarsTransactionsPanelContainerComponent.Item(
|
||||
id: "outgoing",
|
||||
title: "Outgoing",
|
||||
panel: AnyComponent(StarsTransactionsListPanelComponent(
|
||||
context: component.context,
|
||||
items: outgoingItems,
|
||||
action: { _ in
|
||||
}
|
||||
))
|
||||
))
|
||||
|
||||
var panelTransition = transition
|
||||
if balanceUpdated {
|
||||
panelTransition = .easeInOut(duration: 0.25)
|
||||
}
|
||||
|
||||
if !panelItems.isEmpty {
|
||||
let panelContainerSize = self.panelContainer.update(
|
||||
transition: panelTransition,
|
||||
component: AnyComponent(StarsTransactionsPanelContainerComponent(
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
dateTimeFormat: environment.dateTimeFormat,
|
||||
insets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: bottomInset, right: environment.safeInsets.right),
|
||||
items: panelItems,
|
||||
currentPanelUpdated: { [weak self] id, transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.currentSelectedPanelId = id
|
||||
self.state?.updated(transition: transition)
|
||||
}
|
||||
)),
|
||||
environment: {
|
||||
StarsTransactionsPanelContainerEnvironment(isScrollable: wasLockedAtPanels)
|
||||
},
|
||||
containerSize: CGSize(width: availableSize.width, height: availableSize.height - environment.navigationHeight)
|
||||
)
|
||||
if let panelContainerView = self.panelContainer.view {
|
||||
if panelContainerView.superview == nil {
|
||||
self.scrollContainerView.addSubview(panelContainerView)
|
||||
}
|
||||
transition.setFrame(view: panelContainerView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: panelContainerSize))
|
||||
}
|
||||
contentHeight += panelContainerSize.height
|
||||
} else {
|
||||
self.panelContainer.view?.removeFromSuperview()
|
||||
}
|
||||
|
||||
self.ignoreScrolling = true
|
||||
|
||||
let contentOffset = self.scrollView.bounds.minY
|
||||
transition.setPosition(view: self.scrollView, position: CGRect(origin: CGPoint(), size: availableSize).center)
|
||||
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
|
||||
if self.scrollView.contentSize != contentSize {
|
||||
self.scrollView.contentSize = contentSize
|
||||
}
|
||||
transition.setFrame(view: self.scrollContainerView, frame: CGRect(origin: CGPoint(), size: contentSize))
|
||||
|
||||
var scrollViewBounds = self.scrollView.bounds
|
||||
scrollViewBounds.size = availableSize
|
||||
if wasLockedAtPanels, let panelContainerView = self.panelContainer.view {
|
||||
scrollViewBounds.origin.y = panelContainerView.frame.minY - environment.navigationHeight
|
||||
}
|
||||
transition.setBounds(view: self.scrollView, bounds: scrollViewBounds)
|
||||
|
||||
if !wasLockedAtPanels && !transition.animation.isImmediate && self.scrollView.bounds.minY != contentOffset {
|
||||
let deltaOffset = self.scrollView.bounds.minY - contentOffset
|
||||
transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: -deltaOffset), 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: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public final class StarsTransactionsScreen: ViewControllerComponentContainer {
|
||||
private let context: AccountContext
|
||||
|
||||
private let options = Promise<[StarsTopUpOption]>()
|
||||
|
||||
public init(context: AccountContext, starsContext: StarsContext, forceDark: Bool = false) {
|
||||
self.context = context
|
||||
|
||||
var buyImpl: (() -> Void)?
|
||||
super.init(context: context, component: StarsTransactionsScreenComponent(context: context, starsContext: starsContext, buy: {
|
||||
buyImpl?()
|
||||
}), navigationBarAppearance: .transparent)
|
||||
|
||||
self.options.set(.single([]) |> then(context.engine.payments.starsTopUpOptions()))
|
||||
|
||||
buyImpl = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = (self.options.get()
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] options in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: options, peerId: nil, requiredStars: nil)
|
||||
self.push(controller)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override public func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "StarsTransferScreen",
|
||||
module_name = "StarsTransferScreen",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/ViewControllerComponent",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/BalancedTextComponent",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/ItemListUI",
|
||||
"//submodules/TelegramStringFormatting",
|
||||
"//submodules/PresentationDataUtils",
|
||||
"//submodules/Components/SheetComponent",
|
||||
"//submodules/UndoUI",
|
||||
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||
"//submodules/TelegramUI/Components/ListSectionComponent",
|
||||
"//submodules/TelegramUI/Components/ListActionItemComponent",
|
||||
"//submodules/TelegramUI/Components/Premium/PremiumStarComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,450 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import Markdown
|
||||
import TextFormat
|
||||
import TelegramPresentationData
|
||||
import ViewControllerComponent
|
||||
import SheetComponent
|
||||
import BalancedTextComponent
|
||||
import MultilineTextComponent
|
||||
import ItemListUI
|
||||
import UndoUI
|
||||
import AccountContext
|
||||
import PremiumStarComponent
|
||||
import ButtonComponent
|
||||
|
||||
private final class SheetContent: CombinedComponent {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let invoice: TelegramMediaInvoice
|
||||
let source: BotPaymentInvoiceSource
|
||||
let inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
invoice: TelegramMediaInvoice,
|
||||
source: BotPaymentInvoiceSource,
|
||||
inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>
|
||||
) {
|
||||
self.context = context
|
||||
self.invoice = invoice
|
||||
self.source = source
|
||||
self.inputData = inputData
|
||||
}
|
||||
|
||||
static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.invoice != rhs.invoice {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class State: ComponentState {
|
||||
var cachedCloseImage: (UIImage, PresentationTheme)?
|
||||
var cachedChevronImage: (UIImage, PresentationTheme)?
|
||||
var cachedStarImage: (UIImage, PresentationTheme)?
|
||||
|
||||
private let context: AccountContext
|
||||
private let source: BotPaymentInvoiceSource
|
||||
|
||||
var peer: EnginePeer?
|
||||
var peerDisposable: Disposable?
|
||||
var balance: Int64?
|
||||
var form: BotPaymentForm?
|
||||
|
||||
var inProgress = false
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
source: BotPaymentInvoiceSource,
|
||||
inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>
|
||||
) {
|
||||
self.context = context
|
||||
self.source = source
|
||||
|
||||
super.init()
|
||||
|
||||
self.peerDisposable = (inputData
|
||||
|> deliverOnMainQueue).start(next: { [weak self] inputData in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.balance = inputData?.0.balance ?? 0
|
||||
self.form = inputData?.1
|
||||
self.peer = inputData?.2
|
||||
self.updated(transition: .immediate)
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.peerDisposable?.dispose()
|
||||
}
|
||||
|
||||
func buy(completion: @escaping () -> Void) {
|
||||
guard let form else {
|
||||
return
|
||||
}
|
||||
self.inProgress = true
|
||||
self.updated()
|
||||
|
||||
let _ = (self.context.engine.payments.sendStarsPaymentForm(formId: form.id, source: self.source)
|
||||
|> deliverOnMainQueue).start(next: { _ in
|
||||
completion()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func makeState() -> State {
|
||||
return State(context: self.context, source: self.source, inputData: self.inputData)
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let background = Child(RoundedRectangle.self)
|
||||
let star = Child(GiftAvatarComponent.self)
|
||||
let closeButton = Child(Button.self)
|
||||
let title = Child(Text.self)
|
||||
let text = Child(BalancedTextComponent.self)
|
||||
let balanceText = Child(MultilineTextComponent.self)
|
||||
let button = Child(ButtonComponent.self)
|
||||
|
||||
return { context in
|
||||
let environment = context.environment[EnvironmentType.self]
|
||||
let component = context.component
|
||||
let state = context.state
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let theme = presentationData.theme
|
||||
// let strings = presentationData.strings
|
||||
|
||||
// let sideInset: CGFloat = 16.0 + environment.safeInsets.left
|
||||
|
||||
var contentSize = CGSize(width: context.availableSize.width, height: 18.0)
|
||||
|
||||
let background = background.update(
|
||||
component: RoundedRectangle(color: theme.list.blocksBackgroundColor, cornerRadius: 8.0),
|
||||
availableSize: CGSize(width: context.availableSize.width, height: 1000.0),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(background
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0))
|
||||
)
|
||||
|
||||
if let peer = state.peer {
|
||||
let star = star.update(
|
||||
component: GiftAvatarComponent(
|
||||
context: context.component.context,
|
||||
theme: environment.theme,
|
||||
peers: [peer],
|
||||
isVisible: true,
|
||||
hasIdleAnimations: true,
|
||||
hasScaleAnimation: false,
|
||||
color: UIColor(rgb: 0xf7ab04),
|
||||
offset: 40.0
|
||||
),
|
||||
availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0),
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
context.add(star
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: 0.0 + star.size.height / 2.0 - 30.0))
|
||||
)
|
||||
}
|
||||
|
||||
let closeImage: UIImage
|
||||
if let (image, cacheTheme) = state.cachedCloseImage, theme === cacheTheme {
|
||||
closeImage = image
|
||||
} else {
|
||||
closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: theme.actionSheet.inputClearButtonColor)!
|
||||
state.cachedCloseImage = (closeImage, theme)
|
||||
}
|
||||
let closeButton = closeButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(Image(image: closeImage)),
|
||||
action: {
|
||||
// component.dismiss()
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: 30.0, height: 30.0),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(closeButton
|
||||
.position(CGPoint(x: context.availableSize.width - closeButton.size.width, y: 28.0))
|
||||
)
|
||||
|
||||
let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0
|
||||
|
||||
|
||||
contentSize.height += 130.0
|
||||
let title = title.update(
|
||||
component: Text(text: "Confirm Your Purchase", font: Font.bold(24.0), color: theme.list.itemPrimaryTextColor),
|
||||
availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(title
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0))
|
||||
)
|
||||
contentSize.height += title.size.height
|
||||
contentSize.height += 13.0
|
||||
|
||||
let textFont = Font.regular(15.0)
|
||||
let boldTextFont = Font.semibold(15.0)
|
||||
let textColor = theme.actionSheet.primaryTextColor
|
||||
let linkColor = theme.actionSheet.controlAccentColor
|
||||
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in
|
||||
return (TelegramTextAttributes.URL, contents)
|
||||
})
|
||||
|
||||
let amount = component.invoice.totalAmount
|
||||
let text = text.update(
|
||||
component: BalancedTextComponent(
|
||||
text: .markdown(text: "Do you want to buy **\(component.invoice.title)** in **\(state.peer?.compactDisplayTitle ?? "levlam_bot")** for **\(amount) Stars**?", attributes: markdownAttributes),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.2
|
||||
),
|
||||
availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(text
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + text.size.height / 2.0))
|
||||
)
|
||||
contentSize.height += text.size.height
|
||||
contentSize.height += 24.0
|
||||
|
||||
if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== theme {
|
||||
state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: UIColor(rgb: 0xf09903))!, theme)
|
||||
}
|
||||
|
||||
let balanceAttributedString = parseMarkdownIntoAttributedString("Balance\n > **\(state.balance ?? 0)**", attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString
|
||||
if let range = balanceAttributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 {
|
||||
balanceAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: balanceAttributedString.string))
|
||||
balanceAttributedString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xf09903), range: NSRange(range, in: balanceAttributedString.string))
|
||||
balanceAttributedString.addAttribute(.baselineOffset, value: 2.0, range: NSRange(range, in: balanceAttributedString.string))
|
||||
}
|
||||
let balanceText = balanceText.update(
|
||||
component: MultilineTextComponent(
|
||||
text: .plain(balanceAttributedString),
|
||||
horizontalAlignment: .left,
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.2
|
||||
),
|
||||
availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(balanceText
|
||||
.position(CGPoint(x: 16.0 + balanceText.size.width / 2.0, y: 29.0))
|
||||
)
|
||||
|
||||
if state.cachedStarImage == nil || state.cachedStarImage?.1 !== theme {
|
||||
state.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: .white)!, theme)
|
||||
}
|
||||
|
||||
let buttonAttributedString = NSMutableAttributedString(string: "Confirm and Pay > \(amount)", font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center)
|
||||
if let range = buttonAttributedString.string.range(of: ">"), let starImage = state.cachedStarImage?.0 {
|
||||
buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string))
|
||||
buttonAttributedString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xffffff), range: NSRange(range, in: buttonAttributedString.string))
|
||||
buttonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: buttonAttributedString.string))
|
||||
}
|
||||
|
||||
let controller = environment.controller() as? StarsTransferScreen
|
||||
|
||||
let accountContext = component.context
|
||||
let botTitle = state.peer?.compactDisplayTitle ?? ""
|
||||
|
||||
let invoice = component.invoice
|
||||
let button = button.update(
|
||||
component: ButtonComponent(
|
||||
background: ButtonComponent.Background(
|
||||
color: theme.list.itemCheckColors.fillColor,
|
||||
foreground: theme.list.itemCheckColors.foregroundColor,
|
||||
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
|
||||
cornerRadius: 10.0
|
||||
),
|
||||
content: AnyComponentWithIdentity(
|
||||
id: AnyHashable(0),
|
||||
component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))
|
||||
),
|
||||
isEnabled: true,
|
||||
displaysProgress: state.inProgress,
|
||||
action: { [weak state, weak controller] in
|
||||
state?.buy(completion: { [weak controller] in
|
||||
let presentationData = accountContext.sharedContext.currentPresentationData.with { $0 }
|
||||
let resultController = UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .image(image: UIImage(bundleImageName: "Premium/Stars/Star")!, title: "Purchase Completed", text: "You acquired **\(invoice.title)** in **\(botTitle)** for **\(invoice.totalAmount) Stars**.", round: false, undoText: nil),
|
||||
elevatedLayout: true,
|
||||
action: { _ in return true})
|
||||
controller?.present(resultController, in: .window(.root))
|
||||
|
||||
controller?.dismissAnimated()
|
||||
})
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: 361.0, height: 50),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(button
|
||||
.clipsToBounds(true)
|
||||
.cornerRadius(10.0)
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0))
|
||||
)
|
||||
contentSize.height += button.size.height
|
||||
contentSize.height += 48.0
|
||||
|
||||
return contentSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class StarsTransferSheetComponent: CombinedComponent {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
private let context: AccountContext
|
||||
private let invoice: TelegramMediaInvoice
|
||||
private let source: BotPaymentInvoiceSource
|
||||
private let inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
invoice: TelegramMediaInvoice,
|
||||
source: BotPaymentInvoiceSource,
|
||||
inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>
|
||||
) {
|
||||
self.context = context
|
||||
self.invoice = invoice
|
||||
self.source = source
|
||||
self.inputData = inputData
|
||||
}
|
||||
|
||||
static func ==(lhs: StarsTransferSheetComponent, rhs: StarsTransferSheetComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.invoice != rhs.invoice {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let sheet = Child(SheetComponent<(EnvironmentType)>.self)
|
||||
let animateOut = StoredActionSlot(Action<Void>.self)
|
||||
|
||||
return { context in
|
||||
let environment = context.environment[EnvironmentType.self]
|
||||
|
||||
let controller = environment.controller
|
||||
|
||||
let sheet = sheet.update(
|
||||
component: SheetComponent<EnvironmentType>(
|
||||
content: AnyComponent<EnvironmentType>(SheetContent(
|
||||
context: context.component.context,
|
||||
invoice: context.component.invoice,
|
||||
source: context.component.source,
|
||||
inputData: context.component.inputData
|
||||
)),
|
||||
backgroundColor: .blur(.light),
|
||||
followContentSizeChanges: true,
|
||||
animateOut: animateOut
|
||||
),
|
||||
environment: {
|
||||
environment
|
||||
SheetComponentEnvironment(
|
||||
isDisplaying: environment.value.isVisible,
|
||||
isCentered: environment.metrics.widthClass == .regular,
|
||||
hasInputHeight: !environment.inputHeight.isZero,
|
||||
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
|
||||
dismiss: { animated in
|
||||
if animated {
|
||||
animateOut.invoke(Action { _ in
|
||||
if let controller = controller() {
|
||||
controller.dismiss(completion: nil)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
if let controller = controller() {
|
||||
controller.dismiss(completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
availableSize: context.availableSize,
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
context.add(sheet
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
|
||||
)
|
||||
|
||||
return context.availableSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class StarsTransferScreen: ViewControllerComponentContainer {
|
||||
private let context: AccountContext
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
invoice: TelegramMediaInvoice,
|
||||
source: BotPaymentInvoiceSource,
|
||||
inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>
|
||||
) {
|
||||
self.context = context
|
||||
|
||||
super.init(
|
||||
context: context,
|
||||
component: StarsTransferSheetComponent(
|
||||
context: context,
|
||||
invoice: invoice,
|
||||
source: source,
|
||||
inputData: inputData
|
||||
),
|
||||
navigationBarAppearance: .none,
|
||||
statusBarStyle: .ignore,
|
||||
theme: .default
|
||||
)
|
||||
|
||||
self.navigationPresentation = .flatModal
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public func dismissAnimated() {
|
||||
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
|
||||
view.dismissAnimated()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? {
|
||||
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setFillColor(backgroundColor.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setLineWidth(2.0)
|
||||
context.setLineCap(.round)
|
||||
context.setStrokeColor(foregroundColor.cgColor)
|
||||
|
||||
context.move(to: CGPoint(x: 10.0, y: 10.0))
|
||||
context.addLine(to: CGPoint(x: 20.0, y: 20.0))
|
||||
context.strokePath()
|
||||
|
||||
context.move(to: CGPoint(x: 20.0, y: 10.0))
|
||||
context.addLine(to: CGPoint(x: 10.0, y: 20.0))
|
||||
context.strokePath()
|
||||
})
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 8.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 110 KiB |
@ -0,0 +1,9 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
12
submodules/TelegramUI/Images.xcassets/Premium/Stars/Star.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Premium/Stars/Star.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "transactionstar_20 (2).pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Premium/Stars/Star.imageset/transactionstar_20 (2).pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Premium/Stars/Star.imageset/transactionstar_20 (2).pdf
vendored
Normal file
Binary file not shown.
@ -1,7 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Mock2.png",
|
||||
"filename" : "balancestar_48.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
BIN
submodules/TelegramUI/Images.xcassets/Premium/Stars/StarLarge.imageset/balancestar_48.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Premium/Stars/StarLarge.imageset/balancestar_48.pdf
vendored
Normal file
Binary file not shown.
@ -1,7 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "mock.png",
|
||||
"filename" : "topbalance.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
BIN
submodules/TelegramUI/Images.xcassets/Premium/Stars/TopUp.imageset/topbalance.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Premium/Stars/TopUp.imageset/topbalance.pdf
vendored
Normal file
Binary file not shown.
@ -2913,22 +2913,45 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
|> `catch` { _ -> Signal<BotCheckoutController.InputData?, NoError> in
|
||||
return .single(nil)
|
||||
})
|
||||
strongSelf.present(BotCheckoutController(context: strongSelf.context, invoice: invoice, source: .message(messageId), inputData: inputData, completed: { currencyValue, receiptMessageId in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
if invoice.currency == "XTR" {
|
||||
let statePromise = Promise<StarsContext.State?>()
|
||||
statePromise.set(strongSelf.context.engine.payments.peerStarsState(peerId: strongSelf.context.account.peerId))
|
||||
let starsInputData = combineLatest(
|
||||
inputData.get(),
|
||||
statePromise.get()
|
||||
)
|
||||
|> map { data, state -> (StarsContext.State, BotPaymentForm, EnginePeer?)? in
|
||||
if let data, let state {
|
||||
return (state, data.form, data.botPeer)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .paymentSent(currencyValue: currencyValue, itemTitle: invoice.title), elevatedLayout: false, action: { action in
|
||||
guard let strongSelf = self, let receiptMessageId = receiptMessageId else {
|
||||
let _ = (starsInputData |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let controller = strongSelf.context.sharedContext.makeStarsTransferScreen(context: strongSelf.context, invoice: invoice, source: .message(messageId), inputData: starsInputData)
|
||||
strongSelf.push(controller)
|
||||
})
|
||||
} else {
|
||||
strongSelf.present(BotCheckoutController(context: strongSelf.context, invoice: invoice, source: .message(messageId), inputData: inputData, completed: { currencyValue, receiptMessageId in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .paymentSent(currencyValue: currencyValue, itemTitle: invoice.title), elevatedLayout: false, action: { action in
|
||||
guard let strongSelf = self, let receiptMessageId = receiptMessageId else {
|
||||
return false
|
||||
}
|
||||
|
||||
if case .info = action {
|
||||
strongSelf.present(BotReceiptController(context: strongSelf.context, messageId: receiptMessageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if case .info = action {
|
||||
strongSelf.present(BotReceiptController(context: strongSelf.context, messageId: receiptMessageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}), in: .current)
|
||||
}), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
}), in: .current)
|
||||
}), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -820,25 +820,45 @@ func openResolvedUrlImpl(
|
||||
if let navigationController = navigationController {
|
||||
let inputData = Promise<BotCheckoutController.InputData?>()
|
||||
inputData.set(BotCheckoutController.InputData.fetch(context: context, source: .slug(slug))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<BotCheckoutController.InputData?, NoError> in
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<BotCheckoutController.InputData?, NoError> in
|
||||
return .single(nil)
|
||||
})
|
||||
let checkoutController = BotCheckoutController(context: context, invoice: invoice, source: .slug(slug), inputData: inputData, completed: { currencyValue, receiptMessageId in
|
||||
/*strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .paymentSent(currencyValue: currencyValue, itemTitle: invoice.title), elevatedLayout: false, action: { action in
|
||||
guard let strongSelf = self, let receiptMessageId = receiptMessageId else {
|
||||
return false
|
||||
}
|
||||
|
||||
if case .info = action {
|
||||
strongSelf.present(BotReceiptController(context: strongSelf.context, messageId: receiptMessageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}), in: .current)*/
|
||||
})
|
||||
checkoutController.navigationPresentation = .modal
|
||||
navigationController.pushViewController(checkoutController)
|
||||
if invoice.currency == "XTR" {
|
||||
let statePromise = Promise<StarsContext.State?>()
|
||||
statePromise.set(context.engine.payments.peerStarsState(peerId: context.account.peerId))
|
||||
let starsInputData = combineLatest(
|
||||
inputData.get(),
|
||||
statePromise.get()
|
||||
)
|
||||
|> map { data, state -> (StarsContext.State, BotPaymentForm, EnginePeer?)? in
|
||||
if let data, let state {
|
||||
return (state, data.form, data.botPeer)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
let _ = (starsInputData |> take(1) |> deliverOnMainQueue).start(next: { _ in
|
||||
let controller = context.sharedContext.makeStarsTransferScreen(context: context, invoice: invoice, source: .slug(slug), inputData: starsInputData)
|
||||
navigationController.pushViewController(controller)
|
||||
})
|
||||
} else {
|
||||
let checkoutController = BotCheckoutController(context: context, invoice: invoice, source: .slug(slug), inputData: inputData, completed: { currencyValue, receiptMessageId in
|
||||
/*strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .paymentSent(currencyValue: currencyValue, itemTitle: invoice.title), elevatedLayout: false, action: { action in
|
||||
guard let strongSelf = self, let receiptMessageId = receiptMessageId else {
|
||||
return false
|
||||
}
|
||||
|
||||
if case .info = action {
|
||||
strongSelf.present(BotReceiptController(context: strongSelf.context, messageId: receiptMessageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}), in: .current)*/
|
||||
})
|
||||
checkoutController.navigationPresentation = .modal
|
||||
navigationController.pushViewController(checkoutController)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Chat_ErrorInvoiceNotFound, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
||||
|
@ -64,6 +64,9 @@ import TelegramNotices
|
||||
import BotSettingsScreen
|
||||
import CameraScreen
|
||||
import BirthdayPickerScreen
|
||||
import StarsTransactionsScreen
|
||||
import StarsPurchaseScreen
|
||||
import StarsTransferScreen
|
||||
|
||||
private final class AccountUserInterfaceInUseContext {
|
||||
let subscribers = Bag<(Bool) -> Void>()
|
||||
@ -2609,6 +2612,18 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
public func makeStoryStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peerId: EnginePeer.Id, storyId: Int32, storyItem: EngineStoryItem, fromStory: Bool) -> ViewController {
|
||||
return messageStatsController(context: context, updatedPresentationData: updatedPresentationData, subject: .story(peerId: peerId, id: storyId, item: storyItem, fromStory: fromStory))
|
||||
}
|
||||
|
||||
public func makeStarsTransactionsScreen(context: AccountContext, starsContext: StarsContext) -> ViewController {
|
||||
return StarsTransactionsScreen(context: context, starsContext: starsContext)
|
||||
}
|
||||
|
||||
public func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [StarsTopUpOption], peerId: EnginePeer.Id?, requiredStars: Int32?) -> ViewController {
|
||||
return StarsPurchaseScreen(context: context, starsContext: starsContext, options: options, peerId: peerId, requiredStars: requiredStars, modal: true)
|
||||
}
|
||||
|
||||
public func makeStarsTransferScreen(context: AccountContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>) -> ViewController {
|
||||
return StarsTransferScreen(context: context, invoice: invoice, source: source, inputData: inputData)
|
||||
}
|
||||
}
|
||||
|
||||
private func peerInfoControllerImpl(context: AccountContext, updatedPresentationData: (PresentationData, Signal<PresentationData, NoError>)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, requestsContext: PeerInvitationImportersContext? = nil) -> ViewController? {
|
||||
|
@ -857,14 +857,28 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
return .single(nil)
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { [weak self] invoice in
|
||||
if let strongSelf = self, let invoice = invoice {
|
||||
if let strongSelf = self, let invoice, let navigationController = strongSelf.controller?.getNavigationController() {
|
||||
let inputData = Promise<BotCheckoutController.InputData?>()
|
||||
inputData.set(BotCheckoutController.InputData.fetch(context: strongSelf.context, source: .slug(slug))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<BotCheckoutController.InputData?, NoError> in
|
||||
return .single(nil)
|
||||
})
|
||||
if let navigationController = strongSelf.controller?.getNavigationController() {
|
||||
if invoice.currency == "XTR" {
|
||||
let starsInputData = combineLatest(
|
||||
inputData.get(),
|
||||
strongSelf.context.engine.payments.peerStarsState(peerId: strongSelf.context.account.peerId)
|
||||
)
|
||||
|> map { data, state -> (StarsContext.State, BotPaymentForm, EnginePeer?)? in
|
||||
if let data, let state {
|
||||
return (state, data.form, data.botPeer)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
let controller = strongSelf.context.sharedContext.makeStarsTransferScreen(context: strongSelf.context, invoice: invoice, source: .slug(slug), inputData: starsInputData)
|
||||
navigationController.pushViewController(controller)
|
||||
} else {
|
||||
let checkoutController = BotCheckoutController(context: strongSelf.context, invoice: invoice, source: .slug(slug), inputData: inputData, completed: { currencyValue, receiptMessageId in
|
||||
self?.sendInvoiceClosedEvent(slug: slug, result: .paid)
|
||||
}, cancelled: { [weak self] in
|
||||
|
Loading…
x
Reference in New Issue
Block a user