This commit is contained in:
Ilya Laktyushin 2024-05-17 23:40:10 +04:00
parent 8f6767c010
commit c099ec3641
66 changed files with 4487 additions and 162 deletions

View File

@ -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()

View File

@ -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,

View File

@ -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))
}
}
}
}

View File

@ -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())

View File

@ -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",

Binary file not shown.

View File

@ -3,6 +3,7 @@ import UIKit
import SceneKit
import Display
import AppBundle
import PremiumStarComponent
private let sceneVersion: Int = 1

View File

@ -3,6 +3,7 @@ import UIKit
import SceneKit
import Display
import AppBundle
import PremiumStarComponent
final class BadgeStarsView: UIView, PhoneDemoDecorationView {
private let sceneView: SCNView

View File

@ -11,6 +11,7 @@ import AvatarNode
import TelegramCore
import MultilineTextComponent
import TelegramPresentationData
import PremiumStarComponent
private let sceneVersion: Int = 1

View File

@ -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

View File

@ -8,6 +8,7 @@ import ItemListUI
import PresentationDataUtils
import Markdown
import ComponentFlow
import PremiumStarComponent
final class CreateGiveawayHeaderItem: ItemListControllerHeaderItem {
let theme: PresentationTheme

View File

@ -4,6 +4,7 @@ import SceneKit
import Display
import AppBundle
import LegacyComponents
import PremiumStarComponent
private let sceneVersion: Int = 1

View File

@ -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

View File

@ -7,6 +7,7 @@ import SceneKit
import GZip
import AppBundle
import LegacyComponents
import PremiumStarComponent
private let sceneVersion: Int = 3

View File

@ -21,6 +21,7 @@ import TextFormat
import TelegramStringFormatting
import UndoUI
import InvisibleInkDustNode
import PremiumStarComponent
private final class PremiumGiftCodeSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment

View File

@ -21,6 +21,7 @@ import TextFormat
import UniversalMediaPlayer
import InstantPageCache
import ScrollComponent
import PremiumStarComponent
extension PremiumGiftSource {
var identifier: String? {

View File

@ -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 {

View File

@ -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

View File

@ -4,6 +4,7 @@ import SceneKit
import Display
import AppBundle
import SwiftSignalKit
import PremiumStarComponent
private let sceneVersion: Int = 1

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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()

View File

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

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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 {

View File

@ -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)
}

View File

@ -375,6 +375,10 @@ public extension Message {
return false
}
}
func isAgeRestricted() -> Bool {
return false
}
}
public extension Message {

View File

@ -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")

View File

@ -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 {

View File

@ -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": [],

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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 {

View File

@ -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
))
}
}

View File

@ -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())

View File

@ -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",
],
)

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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",
],
)

View File

@ -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",
],
)

View File

@ -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)
}
}

View File

@ -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
}
}
}

View File

@ -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)
}
}

View File

@ -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()
}
}

View File

@ -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",
],
)

View File

@ -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

View File

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "transactionstar_20 (2).pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "Mock2.png",
"filename" : "balancestar_48.pdf",
"idiom" : "universal"
}
],

View File

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "mock.png",
"filename" : "topbalance.pdf",
"idiom" : "universal"
}
],

View File

@ -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))
}
}
}
}

View File

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

View File

@ -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? {

View File

@ -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