Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,36 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "GiftAnimationComponent",
module_name = "GiftAnimationComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/AppBundle",
"//submodules/TelegramStringFormatting",
"//submodules/PresentationDataUtils",
"//submodules/TextFormat",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
"//submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent",
"//submodules/TelegramUI/Components/EmojiStatusComponent",
"//submodules/AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode",
"//submodules/UIKitRuntimeUtils",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,110 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramCore
import TelegramPresentationData
import AppBundle
import AccountContext
import EmojiTextAttachmentView
import TextFormat
public final class GiftAnimationComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let file: TelegramMediaFile?
let still: Bool
let size: CGSize?
public init(
context: AccountContext,
theme: PresentationTheme,
file: TelegramMediaFile?,
still: Bool = false,
size: CGSize? = nil
) {
self.context = context
self.theme = theme
self.file = file
self.still = still
self.size = size
}
public static func ==(lhs: GiftAnimationComponent, rhs: GiftAnimationComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.file != rhs.file {
return false
}
if lhs.still != rhs.still {
return false
}
if lhs.size != rhs.size {
return false
}
return true
}
public final class View: UIView {
private var component: GiftAnimationComponent?
private weak var componentState: EmptyComponentState?
private var animationLayer: InlineStickerItemLayer?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: GiftAnimationComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.componentState = state
let emoji = ChatTextInputTextCustomEmojiAttribute(
interactivelySelectedFromPackId: nil,
fileId: component.file?.fileId.id ?? 0,
file: component.file
)
let iconSize = component.size ?? availableSize
if self.animationLayer == nil {
let animationLayer = InlineStickerItemLayer(
context: .account(component.context),
userLocation: .other,
attemptSynchronousLoad: false,
emoji: emoji,
file: component.file,
cache: component.context.animationCache,
renderer: component.context.animationRenderer,
unique: !component.still,
placeholderColor: component.theme.list.mediaPlaceholderColor,
pointSize: CGSize(width: iconSize.width * 1.2, height: iconSize.height * 1.2),
loopCount: component.still ? 0 : 1
)
animationLayer.isVisibleForAnimations = !component.still
self.animationLayer = animationLayer
self.layer.addSublayer(animationLayer)
}
if let animationLayer = self.animationLayer {
transition.setFrame(layer: animationLayer, frame: CGRect(origin: .zero, size: iconSize))
}
return iconSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,42 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "GiftDemoScreen",
module_name = "GiftDemoScreen",
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/TelegramStringFormatting",
"//submodules/PresentationDataUtils",
"//submodules/Components/SheetComponent",
"//submodules/TextFormat",
"//submodules/TelegramUI/Components/ScrollComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/PremiumUI",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/EdgeEffect",
"//submodules/TelegramUI/Components/Gifts/InfoParagraphComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,584 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import PresentationDataUtils
import ComponentFlow
import ViewControllerComponent
import SheetComponent
import MultilineTextComponent
import BalancedTextComponent
import BundleIconComponent
import ButtonComponent
import Markdown
import GlassBarButtonComponent
import PremiumUI
import ScrollComponent
import LottieComponent
import EdgeEffect
import InfoParagraphComponent
final class PageComponent<ChildEnvironment: Equatable>: CombinedComponent {
typealias EnvironmentType = ChildEnvironment
private let content: AnyComponent<ChildEnvironment>
private let title: String
private let text: String
private let textColor: UIColor
init(
content: AnyComponent<ChildEnvironment>,
title: String,
text: String,
textColor: UIColor
) {
self.content = content
self.title = title
self.text = text
self.textColor = textColor
}
static func ==(lhs: PageComponent<ChildEnvironment>, rhs: PageComponent<ChildEnvironment>) -> Bool {
if lhs.content != rhs.content {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.textColor != rhs.textColor {
return false
}
return true
}
static var body: Body {
let children = ChildMap(environment: ChildEnvironment.self, keyedBy: AnyHashable.self)
let title = Child(MultilineTextComponent.self)
let text = Child(MultilineTextComponent.self)
return { context in
let availableSize = context.availableSize
let component = context.component
let sideInset: CGFloat = 16.0
let textSideInset: CGFloat = 24.0
let textColor = component.textColor
let textFont = Font.regular(17.0)
let boldTextFont = Font.semibold(17.0)
let content = children["main"].update(
component: component.content,
environment: {
context.environment[ChildEnvironment.self]
},
availableSize: CGSize(width: availableSize.width, height: availableSize.width),
transition: context.transition
)
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.title,
font: boldTextFont,
textColor: component.textColor,
paragraphAlignment: .center
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let markdownAttributes = MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
link: MarkdownAttributeSet(font: textFont, textColor: textColor),
linkAttribute: { _ in
return nil
}
)
let text = text.update(
component: MultilineTextComponent(
text: .markdown(text: component.text, attributes: markdownAttributes),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.0
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: content.size.height + 40.0))
)
context.add(text
.position(CGPoint(x: context.availableSize.width / 2.0, y: content.size.height + 60.0 + text.size.height / 2.0))
)
context.add(content
.position(CGPoint(x: content.size.width / 2.0, y: content.size.height / 2.0))
)
return availableSize
}
}
}
private final class DemoSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let action: () -> Void
let dismiss: () -> Void
init(
context: AccountContext,
action: @escaping () -> Void,
dismiss: @escaping () -> Void
) {
self.context = context
self.action = action
self.dismiss = dismiss
}
static func ==(lhs: DemoSheetContent, rhs: DemoSheetContent) -> Bool {
if lhs.context !== rhs.context {
return false
}
return true
}
final class State: ComponentState {
private let context: AccountContext
private var disposable: Disposable?
private(set) var promoConfiguration: PremiumPromoConfiguration?
fileprivate let playButtonAnimation = ActionSlot<Void>()
private var didPlayAnimation = false
init(context: AccountContext) {
self.context = context
super.init()
self.disposable = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.PremiumPromo())
|> deliverOnMainQueue).start(next: { [weak self] promoConfiguration in
guard let self else {
return
}
self.promoConfiguration = promoConfiguration
self.updated(transition: .immediate)
})
}
deinit {
self.disposable?.dispose()
}
func playAnimationIfNeeded() {
if !self.didPlayAnimation {
self.didPlayAnimation = true
self.playButtonAnimation.invoke(Void())
}
}
}
func makeState() -> State {
return State(context: self.context)
}
static var body: Body {
let closeButton = Child(GlassBarButtonComponent.self)
let background = Child(PremiumGradientBackgroundComponent.self)
let demo = Child(PhoneDemoComponent.self)
let scroll = Child(ScrollComponent<Empty>.self)
let bottomEdgeEffect = Child(EdgeEffectComponent.self)
let button = Child(ButtonComponent.self)
return { context in
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let component = context.component
let theme = environment.theme
let strings = environment.strings
let state = context.state
var contentSize = CGSize(width: context.availableSize.width, height: context.availableSize.width)
let remainingHeight = 365.0
let scroll = scroll.update(
component: ScrollComponent<Empty>(
content: AnyComponent(
GiftDemoListComponent(
context: context.component.context,
theme: environment.theme
)
),
contentInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 80.0, right: 0.0),
contentOffsetUpdated: { _, _ in },
contentOffsetWillCommit: { _ in }
),
availableSize: CGSize(width: context.availableSize.width, height: remainingHeight),
transition: context.transition
)
context.add(scroll
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + scroll.size.height / 2.0))
)
let background = background.update(
component: PremiumGradientBackgroundComponent(colors: [
UIColor(rgb: 0x0077ff),
UIColor(rgb: 0x6b93ff),
UIColor(rgb: 0x8878ff),
UIColor(rgb: 0xe46ace)
]),
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.width),
transition: .immediate
)
context.add(background
.position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0))
)
let demo = demo.update(
component: PhoneDemoComponent(
context: component.context,
position: .top,
model: .island,
videoFile: state.promoConfiguration?.videos["gifts"],
decoration: .badgeStars
),
environment: { DemoPageEnvironment(isDisplaying: true, isCentral: true, position: 0.0) },
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.width),
transition: context.transition
)
context.add(demo
.position(CGPoint(x: context.availableSize.width / 2.0, y: demo.size.height / 2.0))
)
let closeButton = closeButton.update(
component: GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0),
backgroundColor: UIColor(rgb: 0x7f76f4),
isDark: false,
state: .tintedGlass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: .white
)
)),
action: { _ in
component.dismiss()
}
),
availableSize: CGSize(width: 40.0, height: 40.0),
transition: .immediate
)
context.add(closeButton
.position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0))
)
contentSize.height += remainingHeight
let bottomEdgeEffectHeight = 108.0
let bottomEdgeEffect = bottomEdgeEffect.update(
component: EdgeEffectComponent(
color: environment.theme.actionSheet.opaqueItemBackgroundColor,
blur: true,
alpha: 1.0,
size: CGSize(width: context.availableSize.width, height: bottomEdgeEffectHeight),
edge: .bottom,
edgeSize: bottomEdgeEffectHeight
),
availableSize: CGSize(width: context.availableSize.width, height: bottomEdgeEffectHeight),
transition: context.transition
)
context.add(bottomEdgeEffect
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height - bottomEdgeEffect.size.height / 2.0))
)
var buttonTitle: [AnyComponentWithIdentity<Empty>] = []
buttonTitle.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "anim_ok"),
color: theme.list.itemCheckColors.foregroundColor,
startingPosition: .begin,
size: CGSize(width: 28.0, height: 28.0),
playOnce: state.playButtonAnimation
))))
buttonTitle.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(ButtonTextContentComponent(
text: strings.Gift_Demo_Understood,
badge: 0,
textColor: theme.list.itemCheckColors.foregroundColor,
badgeBackground: theme.list.itemCheckColors.foregroundColor,
badgeForeground: theme.list.itemCheckColors.fillColor
))))
let button = button.update(
component: ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
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(HStack(buttonTitle, spacing: 2.0))
),
action: {
component.dismiss()
}
),
availableSize: CGSize(width: context.availableSize.width - 30.0 * 2.0, height: 52.0),
transition: .immediate
)
context.add(button
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height - button.size.height / 2.0 - 30.0))
)
state.playAnimationIfNeeded()
return contentSize
}
}
}
private final class DemoSheetComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let action: () -> Void
init(context: AccountContext, action: @escaping () -> Void) {
self.context = context
self.action = action
}
static func ==(lhs: DemoSheetComponent, rhs: DemoSheetComponent) -> Bool {
if lhs.context !== rhs.context {
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>(DemoSheetContent(
context: context.component.context,
action: context.component.action,
dismiss: {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
}
)),
style: .glass,
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
followContentSizeChanges: true,
animateOut: animateOut
),
environment: {
environment
SheetComponentEnvironment(
isDisplaying: environment.value.isVisible,
isCentered: environment.metrics.widthClass == .regular,
hasInputHeight: !environment.inputHeight.isZero,
regularMetricsSize: nil,
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 class GiftDemoScreen: ViewControllerComponentContainer {
public init(context: AccountContext, action: @escaping () -> Void = {}) {
super.init(context: context, component: DemoSheetComponent(context: context, action: action), navigationBarAppearance: .none, theme: .default)
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.navigationPresentation = .flatModal
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func viewDidLoad() {
super.viewDidLoad()
self.view.disablesInteractiveModalDismiss = true
}
public func dismissAnimated() {
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
}
private final class GiftDemoListComponent: CombinedComponent {
typealias EnvironmentType = (Empty, ScrollChildEnvironment)
let context: AccountContext
let theme: PresentationTheme
init(context: AccountContext, theme: PresentationTheme) {
self.context = context
self.theme = theme
}
static func ==(lhs: GiftDemoListComponent, rhs: GiftDemoListComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
return true
}
static var body: Body {
let title = Child(MultilineTextComponent.self)
let description = Child(BalancedTextComponent.self)
let list = Child(List<Empty>.self)
return { context in
let theme = context.component.theme
let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings
let titleColor = theme.list.itemPrimaryTextColor
let textColor = theme.list.itemSecondaryTextColor
let iconColor = theme.list.itemAccentColor
var contentSize = CGSize(width: context.availableSize.width, height: 32.0)
let title = title.update(
component: MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Demo_Title, font: Font.bold(25.0), textColor: titleColor))),
availableSize: context.availableSize,
transition: context.transition
)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height * 0.5))
)
contentSize.height += title.size.height
contentSize.height += 9.0
let description = description.update(
component: BalancedTextComponent(
text: .plain(NSAttributedString(string: strings.Gift_Demo_Description, font: Font.regular(15.0), textColor: textColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2
),
availableSize: context.availableSize,
transition: context.transition
)
context.add(description
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + description.size.height * 0.5))
)
contentSize.height += description.size.height
contentSize.height += 26.0
var items: [AnyComponentWithIdentity<Empty>] = []
items.append(
AnyComponentWithIdentity(
id: "unique",
component: AnyComponent(InfoParagraphComponent(
title: strings.Gift_Demo_Unique_Title,
titleColor: titleColor,
text: strings.Gift_Demo_Unique_Text,
textColor: textColor,
accentColor: iconColor,
iconName: "Premium/Collectible/Unique",
iconColor: iconColor
))
)
)
items.append(
AnyComponentWithIdentity(
id: "tradable",
component: AnyComponent(InfoParagraphComponent(
title: strings.Gift_Demo_Tradable_Title,
titleColor: titleColor,
text: strings.Gift_Demo_Tradable_Text,
textColor: textColor,
accentColor: iconColor,
iconName: "Premium/Collectible/Transferable",
iconColor: iconColor
))
)
)
items.append(
AnyComponentWithIdentity(
id: "wearable",
component: AnyComponent(InfoParagraphComponent(
title: strings.Gift_Demo_Wearable_Title,
titleColor: titleColor,
text: strings.Gift_Demo_Wearable_Text,
textColor: textColor,
accentColor: iconColor,
iconName: "Premium/Collectible/Tradable",
iconColor: iconColor
))
)
)
let list = list.update(
component: List(items),
availableSize: CGSize(width: context.availableSize.width - 32.0 * 2.0, height: 10000.0),
transition: context.transition
)
context.add(list
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + list.size.height / 2.0))
)
contentSize.height += list.size.height
contentSize.height += 88.0
return CGSize(width: context.availableSize.width, height: contentSize.height)
}
}
}
@@ -0,0 +1,41 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "GiftItemComponent",
module_name = "GiftItemComponent",
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/MultilineTextWithEntitiesComponent",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/AppBundle",
"//submodules/TelegramStringFormatting",
"//submodules/PresentationDataUtils",
"//submodules/TextFormat",
"//submodules/Markdown",
"//submodules/AvatarNode",
"//submodules/CheckNode",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
"//submodules/TelegramUI/Components/Stars/ItemShimmeringLoadingComponent",
"//submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/AnimatedTextComponent"
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,36 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "GiftLoadingShimmerView",
module_name = "GiftLoadingShimmerView",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/MultilineTextWithEntitiesComponent",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/AppBundle",
"//submodules/TelegramStringFormatting",
"//submodules/PresentationDataUtils",
"//submodules/TextFormat",
"//submodules/Markdown",
"//submodules/AvatarNode",
"//submodules/CheckNode",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
"//submodules/TelegramUI/Components/Stars/ItemShimmeringLoadingComponent",
"//submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent",
"//submodules/Components/BundleIconComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,201 @@
import UIKit
import Display
import ComponentFlow
import TelegramPresentationData
private final class ShimmerEffectView: UIView {
private var currentBackgroundColor: UIColor?
private var currentForegroundColor: UIColor?
private let imageContainerView: UIView
private let imageView: UIImageView
private var absoluteLocation: (CGRect, CGSize)?
private var shouldBeAnimating = false
override init(frame: CGRect = .zero) {
self.imageContainerView = UIView()
self.imageContainerView.isUserInteractionEnabled = false
self.imageView = UIImageView()
self.imageView.isUserInteractionEnabled = false
self.imageView.contentMode = .scaleToFill
super.init(frame: frame)
self.isUserInteractionEnabled = false
self.clipsToBounds = true
self.imageContainerView.addSubview(self.imageView)
self.addSubview(self.imageContainerView)
}
required init?(coder: NSCoder) {
preconditionFailure()
}
override func didMoveToWindow() {
super.didMoveToWindow()
updateAnimation()
}
func update(backgroundColor: UIColor, foregroundColor: UIColor) {
if let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.argb == backgroundColor.argb,
let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.argb == foregroundColor.argb {
return
}
self.currentBackgroundColor = backgroundColor
self.currentForegroundColor = foregroundColor
self.imageView.image = generateImage(CGSize(width: 4.0, height: 320.0), opaque: true, scale: 1.0, rotatedContext: { size, context in
context.setFillColor(backgroundColor.cgColor)
context.fill(CGRect(origin: .zero, size: size))
context.clip(to: CGRect(origin: .zero, size: size))
let transparentColor = foregroundColor.withAlphaComponent(0.0).cgColor
let peakColor = foregroundColor.cgColor
var locations: [CGFloat] = [0.0, 0.5, 1.0]
let colors: [CGColor] = [transparentColor, peakColor, transparentColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: [])
})
}
func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
if let absoluteLocation, absoluteLocation.0 == rect && absoluteLocation.1 == containerSize {
return
}
let sizeUpdated = self.absoluteLocation?.1 != containerSize
let frameUpdated = self.absoluteLocation?.0 != rect
self.absoluteLocation = (rect, containerSize)
if sizeUpdated, shouldBeAnimating {
self.imageView.layer.removeAnimation(forKey: "shimmer")
self.addImageAnimation()
}
if frameUpdated {
self.imageContainerView.frame = CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: containerSize)
}
self.updateAnimation()
}
private func updateAnimation() {
let inHierarchy = (self.window != nil)
let shouldAnimate = inHierarchy && (self.absoluteLocation != nil)
if shouldAnimate != self.shouldBeAnimating {
self.shouldBeAnimating = shouldAnimate
if shouldAnimate {
self.addImageAnimation()
} else {
self.imageView.layer.removeAnimation(forKey: "shimmer")
}
}
}
private func addImageAnimation() {
guard let containerSize = self.absoluteLocation?.1 else { return }
let gradientHeight: CGFloat = 250.0
self.imageView.frame = CGRect(origin: CGPoint(x: 0.0, y: -gradientHeight),
size: CGSize(width: containerSize.width, height: gradientHeight))
let anim = CABasicAnimation(keyPath: "position.y")
anim.fromValue = 0.0
anim.toValue = (containerSize.height + gradientHeight)
anim.duration = 1.3
anim.timingFunction = CAMediaTimingFunction(name: .easeOut)
anim.fillMode = .removed
anim.isRemovedOnCompletion = true
anim.repeatCount = .infinity
anim.beginTime = CACurrentMediaTime() + 1.0
anim.isAdditive = true
self.imageView.layer.add(anim, forKey: "shimmer")
}
}
public final class GiftLoadingShimmerView: UIView {
private let backgroundView = UIView()
private let effectView = ShimmerEffectView()
private let maskImageView = UIImageView()
private var currentParams: (size: CGSize, theme: PresentationTheme, showFilters: Bool)?
public override init(frame: CGRect = .zero) {
super.init(frame: frame)
self.isUserInteractionEnabled = false
self.backgroundColor = .clear
self.layer.allowsGroupOpacity = true
self.addSubview(self.backgroundView)
self.addSubview(self.effectView)
self.addSubview(self.maskImageView)
}
required init?(coder: NSCoder) { fatalError() }
public func update(size: CGSize, theme: PresentationTheme, showFilters: Bool = false, isPlain: Bool = false, transition: ContainedViewLayoutTransition) {
let backgroundColor = isPlain ? theme.list.itemBlocksBackgroundColor : theme.list.blocksBackgroundColor
let color = theme.list.itemSecondaryTextColor.mixedWith(theme.list.blocksBackgroundColor, alpha: 0.85)
if self.currentParams?.size != size || self.currentParams?.theme !== theme || self.currentParams?.showFilters != showFilters {
self.currentParams = (size, theme, showFilters)
self.backgroundView.backgroundColor = color
self.maskImageView.image = generateImage(size, rotatedContext: { size, context in
context.setFillColor(backgroundColor.cgColor)
context.fill(CGRect(origin: .zero, size: size))
let sideInset: CGFloat = 16.0
if showFilters {
let filterSpacing: CGFloat = 6.0
let filterWidth = (size.width - sideInset * 2.0 - filterSpacing * 3.0) / 4.0
for i in 0 ..< 4 {
let rect = CGRect(origin: CGPoint(x: sideInset + (filterWidth + filterSpacing) * CGFloat(i), y: 0.0),
size: CGSize(width: filterWidth, height: 28.0))
context.addPath(CGPath(roundedRect: rect, cornerWidth: 14.0, cornerHeight: 14.0, transform: nil))
}
}
var currentY: CGFloat = 39.0 + 7.0
var rowIndex: Int = 0
let optionSpacing: CGFloat = 10.0
let optionWidth = (size.width - sideInset * 2.0 - optionSpacing * 2.0) / 3.0
let itemSize = CGSize(width: optionWidth, height: 154.0)
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
while currentY < size.height {
for i in 0 ..< 3 {
let itemOrigin = CGPoint(
x: sideInset + CGFloat(i) * (itemSize.width + optionSpacing),
y: 39.0 + 9.0 + CGFloat(rowIndex) * (itemSize.height + optionSpacing)
)
context.addPath(CGPath(roundedRect: CGRect(origin: itemOrigin, size: itemSize),
cornerWidth: 10.0, cornerHeight: 10.0, transform: nil))
}
currentY += itemSize.height
rowIndex += 1
}
context.fillPath()
})
self.effectView.update(backgroundColor: color, foregroundColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4))
self.effectView.updateAbsoluteRect(CGRect(origin: .zero, size: size), within: size)
}
transition.updateFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: size))
transition.updateFrame(view: self.maskImageView, frame: CGRect(origin: .zero, size: size))
transition.updateFrame(view: self.effectView, frame: CGRect(origin: .zero, size: size))
}
}
@@ -0,0 +1,53 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "GiftOptionsScreen",
module_name = "GiftOptionsScreen",
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/TelegramNotices",
"//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/PlainButtonComponent",
"//submodules/TelegramUI/Components/Premium/PremiumStarComponent",
"//submodules/Components/BlurredBackgroundComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/Gifts/GiftItemComponent",
"//submodules/ConfettiEffect",
"//submodules/InAppPurchaseManager",
"//submodules/TelegramUI/Components/TabSelectorComponent",
"//submodules/TelegramUI/Components/Gifts/GiftSetupScreen",
"//submodules/TelegramUI/Components/Gifts/GiftViewScreen",
"//submodules/TelegramUI/Components/EdgeEffect",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,267 @@
import Foundation
import UIKit
import SwiftSignalKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import AppBundle
import AvatarNode
import Markdown
private final class GiftAuctionTransferAlertContentNode: AlertContentNode {
private let strings: PresentationStrings
private let title: String
private let text: String
private let titleNode: ASTextNode
private let textNode: ASTextNode
private let avatarNode: AvatarNode
private let arrowNode: ASImageNode
private let secondAvatarNode: AvatarNode
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private var validLayout: CGSize?
override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, fromPeer: EnginePeer, toPeer: EnginePeer, title: String, text: String, actions: [TextAlertAction]) {
self.strings = strings
self.title = title
self.text = text
self.titleNode = ASTextNode()
self.titleNode.maximumNumberOfLines = 0
self.textNode = ASTextNode()
self.textNode.maximumNumberOfLines = 0
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
self.arrowNode = ASImageNode()
self.arrowNode.displaysAsynchronously = false
self.arrowNode.displayWithoutProcessing = true
self.secondAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.avatarNode)
self.addSubnode(self.arrowNode)
self.addSubnode(self.secondAvatarNode)
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.updateTheme(theme)
self.avatarNode.setPeer(context: context, theme: ptheme, peer: fromPeer)
self.secondAvatarNode.setPeer(context: context, theme: ptheme, peer: toPeer)
}
override func updateTheme(_ theme: AlertControllerTheme) {
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
self.textNode.attributedText = parseMarkdownIntoAttributedString(self.text, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor),
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor),
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor),
linkAttribute: { url in
return ("URL", url)
}
), textAlignment: .center)
self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/AlertArrow"), color: theme.secondaryColor)
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width, 270.0)
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
let avatarSize = CGSize(width: 60.0, height: 60.0)
self.avatarNode.updateSize(size: avatarSize)
let avatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) - 44.0, y: origin.y), size: avatarSize)
transition.updateFrame(node: self.avatarNode, frame: avatarFrame)
if let arrowImage = self.arrowNode.image {
let arrowFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - arrowImage.size.width) / 2.0), y: origin.y + floorToScreenPixels((avatarSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size)
transition.updateFrame(node: self.arrowNode, frame: arrowFrame)
}
let secondAvatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) + 44.0, y: origin.y), size: avatarSize)
transition.updateFrame(node: self.secondAvatarNode, frame: secondAvatarFrame)
origin.y += avatarSize.height + 10.0
let titleSize = self.titleNode.measure(CGSize(width: size.width - 32.0, height: size.height))
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
origin.y += titleSize.height + 4.0
let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
origin.y += textSize.height + 10.0
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
effectiveActionLayout = .vertical
}
switch effectiveActionLayout {
case .horizontal:
minActionsWidth += actionTitleSize.width + actionTitleInsets
case .vertical:
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
let contentWidth = max(size.width, minActionsWidth)
var actionsHeight: CGFloat = 0.0
switch effectiveActionLayout {
case .horizontal:
actionsHeight = actionButtonHeight
case .vertical:
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
}
let resultSize = CGSize(width: contentWidth, height: avatarSize.height + titleSize.height + textSize.height + actionsHeight + 16.0 + insets.top + insets.bottom)
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
var actionOffset: CGFloat = 0.0
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
switch effectiveActionLayout {
case .horizontal:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
case .vertical:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
switch effectiveActionLayout {
case .horizontal:
if nodeIndex == self.actionNodes.count - 1 {
currentActionWidth = resultSize.width - actionOffset
} else {
currentActionWidth = actionWidth
}
case .vertical:
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
switch effectiveActionLayout {
case .horizontal:
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += currentActionWidth
case .vertical:
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
return resultSize
}
}
func giftAuctionTransferController(context: AccountContext, fromPeer: EnginePeer, toPeer: EnginePeer, commit: @escaping () -> Void) -> AlertController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
let text: String
if fromPeer.id == context.account.peerId {
text = strings.Gift_AuctionTransfer_TextFromYourself(toPeer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)).string
} else if toPeer.id == context.account.peerId {
text = strings.Gift_AuctionTransfer_TextToYourself(fromPeer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)).string
} else {
text = strings.Gift_AuctionTransfer_Text(fromPeer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder), toPeer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)).string
}
var dismissImpl: ((Bool) -> Void)?
var contentNode: GiftAuctionTransferAlertContentNode?
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?(true)
}), TextAlertAction(type: .defaultAction, title: strings.Gift_AuctionTransfer_Change, action: {
dismissImpl?(true)
commit()
})]
contentNode = GiftAuctionTransferAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, fromPeer: fromPeer, toPeer: toPeer, title: strings.Gift_AuctionTransfer_Title, text: text, actions: actions)
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!)
dismissImpl = { [weak controller] animated in
if animated {
controller?.dismissAnimated()
} else {
controller?.dismiss()
}
}
return controller
}
@@ -0,0 +1,29 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "GiftRemainingCountComponent",
module_name = "GiftRemainingCountComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/AppBundle",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
"//submodules/Components/MultilineTextComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,409 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramCore
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import ComponentFlow
import MultilineTextComponent
import GlassBackgroundComponent
public class GiftRemainingCountComponent: Component {
private let inactiveColor: UIColor
private let activeColors: [UIColor]
private let inactiveTitle: String
private let inactiveValue: String
private let inactiveTitleColor: UIColor
private let activeTitle: String
private let activeValue: String
private let activeTitleColor: UIColor
private let badgeText: String?
private let badgePosition: CGFloat
private let badgeGraphPosition: CGFloat
private let invertProgress: Bool
private let leftString: String
private let groupingSeparator: String
public init(
inactiveColor: UIColor,
activeColors: [UIColor],
inactiveTitle: String,
inactiveValue: String,
inactiveTitleColor: UIColor,
activeTitle: String,
activeValue: String,
activeTitleColor: UIColor,
badgeText: String?,
badgePosition: CGFloat,
badgeGraphPosition: CGFloat,
invertProgress: Bool = false,
leftString: String,
groupingSeparator: String
) {
self.inactiveColor = inactiveColor
self.activeColors = activeColors
self.inactiveTitle = inactiveTitle
self.inactiveValue = inactiveValue
self.inactiveTitleColor = inactiveTitleColor
self.activeTitle = activeTitle
self.activeValue = activeValue
self.activeTitleColor = activeTitleColor
self.badgeText = badgeText
self.badgePosition = badgePosition
self.badgeGraphPosition = badgeGraphPosition
self.invertProgress = invertProgress
self.leftString = leftString
self.groupingSeparator = groupingSeparator
}
public static func ==(lhs: GiftRemainingCountComponent, rhs: GiftRemainingCountComponent) -> Bool {
if lhs.inactiveColor != rhs.inactiveColor {
return false
}
if lhs.activeColors != rhs.activeColors {
return false
}
if lhs.inactiveTitle != rhs.inactiveTitle {
return false
}
if lhs.inactiveValue != rhs.inactiveValue {
return false
}
if lhs.inactiveTitleColor != rhs.inactiveTitleColor {
return false
}
if lhs.activeTitle != rhs.activeTitle {
return false
}
if lhs.activeValue != rhs.activeValue {
return false
}
if lhs.activeTitleColor != rhs.activeTitleColor {
return false
}
if lhs.badgeText != rhs.badgeText {
return false
}
if lhs.badgePosition != rhs.badgePosition {
return false
}
if lhs.badgeGraphPosition != rhs.badgeGraphPosition {
return false
}
if lhs.invertProgress != rhs.invertProgress {
return false
}
if lhs.leftString != rhs.leftString {
return false
}
if lhs.groupingSeparator != rhs.groupingSeparator {
return false
}
return true
}
public final class View: UIView {
private var component: GiftRemainingCountComponent?
private let container: UIView
private let inactiveBackground: SimpleLayer
private let inactiveTitleLabel = ComponentView<Empty>()
private let inactiveValueLabel = ComponentView<Empty>()
private let innerLeftTitleLabel = ComponentView<Empty>()
private let innerRightTitleLabel = ComponentView<Empty>()
private let activeContainer: UIView
private let activeBackground: SimpleLayer
private let activeTitleLabel = ComponentView<Empty>()
private let activeValueLabel = ComponentView<Empty>()
private let activeChromeView = UIImageView()
override init(frame: CGRect) {
self.container = UIView()
self.container.clipsToBounds = true
self.container.layer.cornerRadius = 15.0
self.inactiveBackground = SimpleLayer()
self.activeContainer = UIView()
self.activeContainer.clipsToBounds = true
self.activeContainer.layer.cornerRadius = 15.0
self.activeBackground = SimpleLayer()
self.activeBackground.anchorPoint = CGPoint()
super.init(frame: frame)
self.addSubview(self.container)
self.container.layer.addSublayer(self.inactiveBackground)
self.container.addSubview(self.activeContainer)
self.activeContainer.layer.addSublayer(self.activeBackground)
//self.activeContainer.addSubview(self.activeChromeView)
self.activeChromeView.layer.compositingFilter = "overlayBlendMode"
self.activeChromeView.alpha = 0.8
self.activeChromeView.image = GlassBackgroundView.generateForegroundImage(size: CGSize(width: 30.0, height: 30.0), isDark: false, fillColor: .clear)
self.isUserInteractionEnabled = false
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var previousAvailableSize: CGSize?
func update(component: GiftRemainingCountComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
self.component = component
self.inactiveBackground.backgroundColor = component.inactiveColor.cgColor
self.activeBackground.backgroundColor = component.activeColors.last?.cgColor
let size = CGSize(width: availableSize.width, height: 90.0)
let lineHeight: CGFloat = 30.0
let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - lineHeight), size: CGSize(width: size.width, height: lineHeight))
self.container.frame = containerFrame
let activityPosition: CGFloat = floor(containerFrame.width * component.badgeGraphPosition)
let activeWidth: CGFloat = containerFrame.width - activityPosition
let leftTextColor: UIColor
let rightTextColor: UIColor
if component.invertProgress {
leftTextColor = component.inactiveTitleColor
rightTextColor = component.inactiveTitleColor
} else {
leftTextColor = component.inactiveTitleColor
rightTextColor = component.activeTitleColor
}
if component.invertProgress {
let innerLeftTitleSize = self.innerLeftTitleLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.inactiveTitle,
font: Font.semibold(15.0),
textColor: component.activeTitleColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.innerLeftTitleLabel.view {
if view.superview == nil {
self.activeContainer.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((lineHeight - innerLeftTitleSize.height) / 2.0)), size: innerLeftTitleSize)
}
let innerRightTitleSize = self.innerRightTitleLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.activeValue,
font: Font.semibold(15.0),
textColor: component.activeTitleColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.innerRightTitleLabel.view {
if view.superview == nil {
self.activeContainer.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: containerFrame.width - 12.0 - innerRightTitleSize.width, y: floorToScreenPixels((lineHeight - innerRightTitleSize.height) / 2.0)), size: innerRightTitleSize)
}
}
let inactiveTitleSize = self.inactiveTitleLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.inactiveTitle,
font: Font.semibold(15.0),
textColor: leftTextColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.inactiveTitleLabel.view {
if view.superview == nil {
self.container.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((lineHeight - inactiveTitleSize.height) / 2.0)), size: inactiveTitleSize)
}
let inactiveValueSize = self.inactiveValueLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.inactiveValue,
font: Font.semibold(15.0),
textColor: leftTextColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.inactiveValueLabel.view {
if view.superview == nil {
self.container.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: activityPosition - 12.0 - inactiveValueSize.width, y: floorToScreenPixels((lineHeight - inactiveValueSize.height) / 2.0)), size: inactiveValueSize)
}
let activeTitleSize = self.activeTitleLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.activeTitle,
font: Font.semibold(15.0),
textColor: rightTextColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.activeTitleLabel.view {
if view.superview == nil {
self.container.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: activityPosition + 12.0, y: floorToScreenPixels((lineHeight - activeTitleSize.height) / 2.0)), size: activeTitleSize)
}
let activeValueSize = self.activeValueLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.activeValue,
font: Font.semibold(15.0),
textColor: rightTextColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.activeValueLabel.view {
if view.superview == nil {
self.container.addSubview(view)
if component.invertProgress {
self.container.bringSubviewToFront(self.activeContainer)
}
}
view.frame = CGRect(origin: CGPoint(x: containerFrame.width - 12.0 - activeValueSize.width, y: floorToScreenPixels((lineHeight - activeValueSize.height) / 2.0)), size: activeValueSize)
}
var progressTransition: ComponentTransition = .immediate
if !transition.animation.isImmediate {
progressTransition = .easeInOut(duration: 0.5)
}
if "".isEmpty {
if component.invertProgress {
progressTransition.setFrame(layer: self.inactiveBackground, frame: CGRect(origin: CGPoint(x: activityPosition - 15.0, y: 0.0), size: CGSize(width: size.width - activityPosition + 15.0, height: lineHeight)))
progressTransition.setFrame(view: self.activeContainer, frame: CGRect(origin: .zero, size: CGSize(width: activityPosition, height: lineHeight)))
progressTransition.setBounds(layer: self.activeBackground, bounds: CGRect(origin: .zero, size: CGSize(width: containerFrame.width * 1.35, height: lineHeight)))
} else {
progressTransition.setFrame(layer: self.inactiveBackground, frame: CGRect(origin: .zero, size: CGSize(width: activityPosition, height: lineHeight)))
progressTransition.setFrame(view: self.activeContainer, frame: CGRect(origin: CGPoint(x: activityPosition, y: 0.0), size: CGSize(width: activeWidth, height: lineHeight)))
progressTransition.setFrame(layer: self.activeBackground, frame: CGRect(origin: CGPoint(x: -activityPosition, y: 0.0), size: CGSize(width: containerFrame.width * 1.35, height: lineHeight)))
}
progressTransition.setFrame(view: self.activeChromeView, frame: CGRect(origin: CGPoint(x: -activityPosition, y: 0.0), size: CGSize(width: activeWidth, height: lineHeight)))
if self.activeBackground.animation(forKey: "movement") == nil {
self.activeBackground.position = CGPoint(x: -self.activeContainer.frame.width * 0.35, y: lineHeight / 2.0)
}
}
if self.previousAvailableSize != availableSize {
self.previousAvailableSize = availableSize
var locations: [CGFloat] = []
let delta = 1.0 / CGFloat(component.activeColors.count - 1)
for i in 0 ..< component.activeColors.count {
locations.append(delta * CGFloat(i))
}
let gradient = generateGradientImage(size: CGSize(width: 200.0, height: 60.0), colors: Array(component.activeColors.reversed()), locations: locations, direction: .horizontal)
self.activeBackground.contentsGravity = .resizeAspectFill
self.activeBackground.contents = gradient?.cgImage
self.setupGradientAnimations()
}
return size
}
private func setupGradientAnimations() {
guard let _ = self.component else {
return
}
if let _ = self.activeBackground.animation(forKey: "movement") {
} else {
CATransaction.begin()
let lineOffset = 0.0
let linePreviousValue = self.activeBackground.position.x
var lineNewValue: CGFloat = lineOffset
if linePreviousValue < 0.0 {
lineNewValue = 0.0
} else {
lineNewValue = -self.activeContainer.bounds.width * 0.35
}
lineNewValue -= self.activeContainer.frame.minX
self.activeBackground.position = CGPoint(x: lineNewValue, y: 0.0)
let lineAnimation = CABasicAnimation(keyPath: "position.x")
lineAnimation.duration = 4.5
lineAnimation.fromValue = linePreviousValue
lineAnimation.toValue = lineNewValue
lineAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
self.activeBackground.add(lineAnimation, forKey: "movement")
CATransaction.commit()
}
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
@@ -0,0 +1,59 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "GiftSetupScreen",
module_name = "GiftSetupScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUIPreferences",
"//submodules/TelegramStringFormatting",
"//submodules/AccountContext",
"//submodules/PresentationDataUtils",
"//submodules/Markdown",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/MultilineTextWithEntitiesComponent",
"//submodules/Components/BalancedTextComponent",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/AppBundle",
"//submodules/WallpaperBackgroundNode",
"//submodules/TextFormat",
"//submodules/ChatPresentationInterfaceState",
"//submodules/TelegramUI/Components/TextFieldComponent",
"//submodules/TelegramUI/Components/ListItemComponentAdaptor",
"//submodules/BotPaymentsUI",
"//submodules/TelegramUI/Components/EmojiSuggestionsComponent",
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
"//submodules/InAppPurchaseManager",
"//submodules/Components/BlurredBackgroundComponent",
"//submodules/ProgressNavigationButtonNode",
"//submodules/ConfettiEffect",
"//submodules/TelegramUI/Components/EdgeEffect",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
"//submodules/TelegramUI/Components/MessageInputPanelComponent",
"//submodules/TelegramUI/Components/Gifts/GiftRemainingCountComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,443 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import Postbox
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import WallpaperBackgroundNode
import ListItemComponentAdaptor
final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator {
enum Subject: Equatable {
case premium(months: Int32, amount: Int64, currency: String)
case starGift(gift: StarGift.Gift)
}
let context: AccountContext
let theme: PresentationTheme
let componentTheme: PresentationTheme
let strings: PresentationStrings
let sectionId: ItemListSectionId
let fontSize: PresentationFontSize
let chatBubbleCorners: PresentationChatBubbleCorners
let wallpaper: TelegramWallpaper
let dateTimeFormat: PresentationDateTimeFormat
let nameDisplayOrder: PresentationPersonNameOrder
let peers: [EnginePeer]
let subject: ChatGiftPreviewItem.Subject
let chatPeerId: EnginePeer.Id?
let text: String
let entities: [MessageTextEntity]
let upgradeStars: Int64?
let chargeStars: Int64?
let bottomInset: CGFloat
init(
context: AccountContext,
theme: PresentationTheme,
componentTheme: PresentationTheme,
strings: PresentationStrings,
sectionId: ItemListSectionId,
fontSize: PresentationFontSize,
chatBubbleCorners: PresentationChatBubbleCorners,
wallpaper: TelegramWallpaper,
dateTimeFormat: PresentationDateTimeFormat,
nameDisplayOrder: PresentationPersonNameOrder,
peers: [EnginePeer],
subject: ChatGiftPreviewItem.Subject,
chatPeerId: EnginePeer.Id?,
text: String,
entities: [MessageTextEntity],
upgradeStars: Int64?,
chargeStars: Int64?,
bottomInset: CGFloat = 0.0
) {
self.context = context
self.theme = theme
self.componentTheme = componentTheme
self.strings = strings
self.sectionId = sectionId
self.fontSize = fontSize
self.chatBubbleCorners = chatBubbleCorners
self.wallpaper = wallpaper
self.dateTimeFormat = dateTimeFormat
self.nameDisplayOrder = nameDisplayOrder
self.peers = peers
self.subject = subject
self.chatPeerId = chatPeerId
self.text = text
self.entities = entities
self.upgradeStars = upgradeStars
self.chargeStars = chargeStars
self.bottomInset = bottomInset
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ChatGiftPreviewItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ChatGiftPreviewItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
public func item() -> ListViewItem {
return self
}
public static func ==(lhs: ChatGiftPreviewItem, rhs: ChatGiftPreviewItem) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.componentTheme !== rhs.componentTheme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.fontSize != rhs.fontSize {
return false
}
if lhs.chatBubbleCorners != rhs.chatBubbleCorners {
return false
}
if lhs.wallpaper != rhs.wallpaper {
return false
}
if lhs.dateTimeFormat != rhs.dateTimeFormat {
return false
}
if lhs.nameDisplayOrder != rhs.nameDisplayOrder {
return false
}
if lhs.peers != rhs.peers {
return false
}
if lhs.subject != rhs.subject {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.entities != rhs.entities {
return false
}
if lhs.upgradeStars != rhs.upgradeStars {
return false
}
if lhs.bottomInset != rhs.bottomInset {
return false
}
return true
}
}
final class ChatGiftPreviewItemNode: ListViewItemNode {
private var backgroundNode: WallpaperBackgroundNode?
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let containerNode: ASDisplayNode
private var messageNodes: [ListViewItemNode]?
private var itemHeaderNodes: [ListViewItemNode.HeaderId: ListViewItemHeaderNode] = [:]
private var item: ChatGiftPreviewItem?
private let disposable = MetaDisposable()
private var initialBubbleHeight: CGFloat?
init() {
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.containerNode = ASDisplayNode()
self.containerNode.subnodeTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
super.init(layerBacked: false, dynamicBounce: false)
self.clipsToBounds = true
self.isUserInteractionEnabled = false
self.addSubnode(self.containerNode)
}
deinit {
self.disposable.dispose()
}
func asyncLayout() -> (_ item: ChatGiftPreviewItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentNodes = self.messageNodes
var currentBackgroundNode = self.backgroundNode
return { item, params, neighbors in
if currentBackgroundNode == nil {
currentBackgroundNode = createWallpaperBackgroundNode(context: item.context, forChatDisplay: false)
currentBackgroundNode?.update(wallpaper: item.wallpaper, animated: false)
currentBackgroundNode?.updateBubbleTheme(bubbleTheme: item.componentTheme, bubbleCorners: item.chatBubbleCorners)
}
var insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(1))
let chatPeerId = item.chatPeerId ?? peerId
var items: [ListViewItem] = []
for _ in 0 ..< 1 {
let authorPeerId = item.context.account.peerId
var peers = SimpleDictionary<PeerId, Peer>()
let messages = SimpleDictionary<MessageId, Message>()
for peer in item.peers {
peers[peer.id] = peer._asPeer()
}
let media: [Media]
switch item.subject {
case let .premium(months, amount, currency):
media = [
TelegramMediaAction(
action: .giftPremium(currency: currency, amount: amount, days: months * 30, cryptoCurrency: nil, cryptoAmount: nil, text: item.text, entities: item.entities)
)
]
case let .starGift(gift):
media = [
TelegramMediaAction(
action: .starGift(gift: .generic(gift), convertStars: gift.convertStars, text: item.text, entities: item.entities, nameHidden: false, savedToProfile: false, converted: false, upgraded: false, canUpgrade: gift.upgradeStars != nil, upgradeStars: item.upgradeStars, isRefunded: false, isPrepaidUpgrade: false, upgradeMessageId: nil, peerId: nil, senderId: nil, savedId: nil, prepaidUpgradeHash: nil, giftMessageId: nil, upgradeSeparate: false, isAuctionAcquired: false, toPeerId: nil, number: nil)
)
]
}
let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: chatPeerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[authorPeerId], text: "", attributes: [], media: media, peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false))
}
var nodes: [ListViewItemNode] = []
var currentNodes = currentNodes
if let nodes = currentNodes, nodes.count != items.count {
for node in nodes {
node.removeFromSupernode()
}
currentNodes = nil
}
let transition = ControlledTransition(duration: 0.2, curve: .spring, interactive: false)
if let messageNodes = currentNodes {
nodes = messageNodes
for i in 0 ..< items.count {
let itemNode = messageNodes[i]
items[i].updateNode(async: { $0() }, node: {
return itemNode
}, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .System(duration: 0.2, transition: transition), completion: { (layout, apply) in
itemNode.bounds = CGRect(origin: .zero, size: layout.size)
itemNode.contentSize = layout.contentSize
itemNode.insets = layout.insets
itemNode.isUserInteractionEnabled = false
itemNode.visibility = .visible(1.0, .infinite)
apply(ListViewItemApply(isOnScreen: true))
})
}
} else {
var messageNodes: [ListViewItemNode] = []
for i in 0 ..< items.count {
var itemNode: ListViewItemNode?
items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: true, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in
itemNode = node
apply().1(ListViewItemApply(isOnScreen: true))
})
itemNode!.isUserInteractionEnabled = false
itemNode!.visibility = .visible(1.0, .infinite)
messageNodes.append(itemNode!)
if itemNode!.frame.height > 44.0 {
self.initialBubbleHeight = itemNode!.frame.height
}
}
nodes = messageNodes
}
let baseContentHeight: CGFloat = 370.0
var contentSize = CGSize(width: params.width, height: 4.0 + 4.0)
contentSize.height = baseContentHeight + item.bottomInset
insets = itemListNeighborsGroupedInsets(neighbors, params)
if params.width <= 320.0 {
insets.top = 0.0
}
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: .zero)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
if let currentBackgroundNode {
currentBackgroundNode.update(wallpaper: item.wallpaper, animated: false)
currentBackgroundNode.updateBubbleTheme(bubbleTheme: item.theme, bubbleCorners: item.chatBubbleCorners)
}
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize)
strongSelf.messageNodes = nodes
var totalHeight: CGFloat = 0.0
for node in nodes {
let bubbleHeight: CGFloat
if node.frame.height > 44.0, let initialBubbleHeight = strongSelf.initialBubbleHeight {
bubbleHeight = max(node.frame.height, initialBubbleHeight)
} else {
bubbleHeight = node.frame.height
}
totalHeight += bubbleHeight
}
var originY: CGFloat = floor((baseContentHeight - totalHeight) / 2.0)
originY = contentSize.height - originY - totalHeight
for node in nodes {
if node.supernode == nil {
strongSelf.containerNode.addSubnode(node)
}
let bubbleHeight: CGFloat
// if node.frame.height > 44.0, let initialBubbleHeight = strongSelf.initialBubbleHeight {
// bubbleHeight = max(node.frame.height, initialBubbleHeight)
// } else {
bubbleHeight = node.frame.height
// }
transition.legacyAnimator.transition.updateFrame(node: node, frame: CGRect(origin: CGPoint(x: 0.0, y: originY), size: node.frame.size))
//node.updateFrame(CGRect(origin: CGPoint(x: 0.0, y: originY), size: node.frame.size), within: layoutSize, transition: transition)
originY += bubbleHeight
}
if let currentBackgroundNode = currentBackgroundNode, strongSelf.backgroundNode !== currentBackgroundNode {
strongSelf.backgroundNode = currentBackgroundNode
strongSelf.insertSubnode(currentBackgroundNode, at: 0)
}
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
if params.isStandalone {
strongSelf.topStripeNode.isHidden = true
strongSelf.bottomStripeNode.isHidden = true
strongSelf.maskNode.isHidden = true
} else {
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = 0.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.componentTheme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
}
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
let displayMode: WallpaperDisplayMode
if abs(params.availableHeight - params.width) < 100.0, params.availableHeight > 700.0 {
displayMode = .halfAspectFill
} else {
if backgroundFrame.width > backgroundFrame.height * 4.0 {
if params.availableHeight < 700.0 {
displayMode = .halfAspectFill
} else {
displayMode = .aspectFill
}
} else {
displayMode = .aspectFill
}
}
if let backgroundNode = strongSelf.backgroundNode {
backgroundNode.frame = backgroundFrame
backgroundNode.updateLayout(size: backgroundNode.bounds.size, displayMode: displayMode, transition: .immediate)
}
strongSelf.maskNode.frame = backgroundFrame.insetBy(dx: params.leftInset, dy: 0.0)
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}
@@ -0,0 +1,53 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "GiftStoreScreen",
module_name = "GiftStoreScreen",
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/PlainButtonComponent",
"//submodules/TelegramUI/Components/Premium/PremiumStarComponent",
"//submodules/Components/BlurredBackgroundComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/Gifts/GiftItemComponent",
"//submodules/ConfettiEffect",
"//submodules/InAppPurchaseManager",
"//submodules/TelegramUI/Components/TabSelectorComponent",
"//submodules/TelegramUI/Components/Gifts/GiftSetupScreen",
"//submodules/TelegramUI/Components/Gifts/GiftViewScreen",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/TextFieldComponent",
"//submodules/TelegramUI/Components/Gifts/GiftLoadingShimmerView",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,472 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import PlainButtonComponent
import MultilineTextComponent
import BundleIconComponent
import TextFormat
import AccountContext
import LottieComponent
public final class FilterSelectorComponent: Component {
public struct Colors: Equatable {
public var foreground: UIColor
public var background: UIColor
public init(
foreground: UIColor,
background: UIColor
) {
self.foreground = foreground
self.background = background
}
}
public struct Item: Equatable {
public var id: AnyHashable
public var index: Int
public var iconName: String?
public var title: String
public var action: (UIView) -> Void
public init(
id: AnyHashable,
index: Int = 0,
iconName: String? = nil,
title: String,
action: @escaping (UIView) -> Void
) {
self.id = id
self.index = index
self.iconName = iconName
self.title = title
self.action = action
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
return lhs.id == rhs.id && lhs.index == rhs.index && lhs.iconName == rhs.iconName && lhs.title == rhs.title
}
}
public let context: AccountContext?
public let colors: Colors
public let items: [Item]
public let selectedItemId: AnyHashable?
public init(
context: AccountContext? = nil,
colors: Colors,
items: [Item],
selectedItemId: AnyHashable?
) {
self.context = context
self.colors = colors
self.items = items
self.selectedItemId = selectedItemId
}
public static func ==(lhs: FilterSelectorComponent, rhs: FilterSelectorComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.colors != rhs.colors {
return false
}
if lhs.items != rhs.items {
return false
}
if lhs.selectedItemId != rhs.selectedItemId {
return false
}
return true
}
private final class VisibleItem {
let title = ComponentView<Empty>()
init() {
}
}
public final class View: UIScrollView {
private var component: FilterSelectorComponent?
private weak var state: EmptyComponentState?
private var visibleItems: [AnyHashable: VisibleItem] = [:]
override init(frame: CGRect) {
super.init(frame: frame)
self.showsVerticalScrollIndicator = false
self.showsHorizontalScrollIndicator = false
self.scrollsToTop = false
self.delaysContentTouches = false
self.canCancelContentTouches = true
self.contentInsetAdjustmentBehavior = .never
self.alwaysBounceVertical = false
self.clipsToBounds = false
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
override public func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
func animateIn() {
for (_, item) in self.visibleItems {
if let itemView = item.title.view {
itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
itemView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
}
}
}
func update(component: FilterSelectorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let baseHeight: CGFloat = 28.0
var spacing: CGFloat = 6.0
let itemFont = Font.semibold(14.0)
let allowScroll = true
var innerContentWidth: CGFloat = 0.0
var validIds: [AnyHashable] = []
var index = 0
var itemViews: [AnyHashable: (VisibleItem, CGSize, ComponentTransition)] = [:]
for item in component.items {
var itemTransition = transition
let itemView: VisibleItem
if let current = self.visibleItems[item.id] {
itemView = current
} else {
itemView = VisibleItem()
self.visibleItems[item.id] = itemView
itemTransition = itemTransition.withAnimation(.none)
}
let itemId = item.id
validIds.append(itemId)
let itemSize = itemView.title.update(
transition: transition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(ItemComponent(
context: component.context,
index: item.index,
iconName: item.iconName,
text: item.title,
font: itemFont,
color: component.colors.foreground,
backgroundColor: component.colors.background,
isSelected: itemId == component.selectedItemId
)),
effectAlignment: .center,
minSize: nil,
action: { [weak itemView] in
if let view = itemView?.title.view {
item.action(view)
}
},
animateScale: false
)),
environment: {},
containerSize: CGSize(width: 200.0, height: 100.0)
)
innerContentWidth += itemSize.width
itemViews[item.id] = (itemView, itemSize, itemTransition)
index += 1
}
let estimatedContentWidth = 2.0 * spacing + innerContentWidth + (CGFloat(component.items.count - 1) * spacing)
if estimatedContentWidth > availableSize.width && !allowScroll {
spacing = (availableSize.width - innerContentWidth) / CGFloat(component.items.count + 1)
}
var contentWidth: CGFloat = spacing
for item in component.items {
guard let (itemView, itemSize, itemTransition) = itemViews[item.id] else {
continue
}
if contentWidth > spacing {
contentWidth += spacing
}
let itemFrame = CGRect(origin: CGPoint(x: contentWidth, y: floor((baseHeight - itemSize.height) * 0.5)), size: itemSize)
contentWidth = itemFrame.maxX
if let itemTitleView = itemView.title.view {
if itemTitleView.superview == nil {
itemTitleView.layer.anchorPoint = CGPoint()
self.addSubview(itemTitleView)
}
itemTransition.setPosition(view: itemTitleView, position: itemFrame.origin)
itemTransition.setBounds(view: itemTitleView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size))
}
}
contentWidth += spacing
var removeIds: [AnyHashable] = []
for (id, itemView) in self.visibleItems {
if !validIds.contains(id) {
removeIds.append(id)
itemView.title.view?.removeFromSuperview()
}
}
for id in removeIds {
self.visibleItems.removeValue(forKey: id)
}
self.contentSize = CGSize(width: contentWidth, height: baseHeight)
self.disablesInteractiveTransitionGestureRecognizer = contentWidth > availableSize.width
return CGSize(width: min(contentWidth, availableSize.width), height: baseHeight)
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
extension CGRect {
func interpolate(with other: CGRect, fraction: CGFloat) -> CGRect {
return CGRect(
x: self.origin.x * (1.0 - fraction) + (other.origin.x) * fraction,
y: self.origin.y * (1.0 - fraction) + (other.origin.y) * fraction,
width: self.size.width * (1.0 - fraction) + (other.size.width) * fraction,
height: self.size.height * (1.0 - fraction) + (other.size.height) * fraction
)
}
}
private final class ItemComponent: Component {
let context: AccountContext?
let index: Int
let iconName: String?
let text: String
let font: UIFont
let color: UIColor
let backgroundColor: UIColor
let isSelected: Bool
init(
context: AccountContext?,
index: Int,
iconName: String?,
text: String,
font: UIFont,
color: UIColor,
backgroundColor: UIColor,
isSelected: Bool
) {
self.context = context
self.index = index
self.iconName = iconName
self.text = text
self.font = font
self.color = color
self.backgroundColor = backgroundColor
self.isSelected = isSelected
}
static func ==(lhs: ItemComponent, rhs: ItemComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.index != rhs.index {
return false
}
if lhs.iconName != rhs.iconName {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.font != rhs.font {
return false
}
if lhs.color != rhs.color {
return false
}
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
if lhs.isSelected != rhs.isSelected {
return false
}
return true
}
public final class View: UIView {
private var component: ItemComponent?
private weak var state: EmptyComponentState?
private let background = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private let icon = ComponentView<Empty>()
private var isSelected = false
private var iconName: String?
private let playOnce = ActionSlot<Void>()
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: ItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let previousComponent = self.component
self.component = component
self.state = state
var animateTitleInDirection: CGFloat?
if let previousComponent, previousComponent.text != component.text, !transition.animation.isImmediate, let titleView = self.title.view, let snapshotView = titleView.snapshotView(afterScreenUpdates: false) {
snapshotView.frame = titleView.frame
self.addSubview(snapshotView)
var direction: CGFloat = 1.0
if previousComponent.index < component.index {
direction = -1.0
}
snapshotView.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 6.0 * direction), duration: 0.2, removeOnCompletion: false, additive: true)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
snapshotView.removeFromSuperview()
})
animateTitleInDirection = direction
}
let attributedTitle = NSAttributedString(string: component.text, font: component.font, textColor: component.color)
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(attributedTitle)
)),
environment: {},
containerSize: availableSize
)
let animationName = component.iconName ?? (component.isSelected ? "GiftFilterMenuOpen" : "GiftFilterMenuClose")
let animationSize = component.iconName != nil ? CGSize(width: 22.0, height: 22.0) : CGSize(width: 10.0, height: 22.0)
let iconSize = self.icon.update(
transition: transition,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: animationName),
color: component.color,
playOnce: self.playOnce
)),
environment: {},
containerSize: CGSize(width: 22.0, height: 22.0)
)
var playAnimation = false
if self.isSelected != component.isSelected || self.iconName != component.iconName {
if let iconName = component.iconName {
if component.isSelected {
playAnimation = true
} else if self.iconName != iconName {
playAnimation = true
}
self.iconName = iconName
} else {
playAnimation = true
}
self.isSelected = component.isSelected
}
if playAnimation {
self.playOnce.invoke(Void())
}
let padding: CGFloat = 12.0
var leftPadding = padding
if let _ = component.iconName {
leftPadding -= 4.0
}
let spacing: CGFloat = 4.0
let totalWidth = titleSize.width + animationSize.width + spacing
let size = CGSize(width: totalWidth + leftPadding + padding, height: 28.0)
let backgroundSize = self.background.update(
transition: transition,
component: AnyComponent(RoundedRectangle(
color: component.backgroundColor,
cornerRadius: 14.0
)),
environment: {},
containerSize: size
)
if let backgroundView = self.background.view {
if backgroundView.superview == nil {
self.addSubview(backgroundView)
}
transition.setPosition(view: backgroundView, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0))
transition.setBounds(view: backgroundView, bounds: CGRect(origin: CGPoint(), size: backgroundSize))
}
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
let titlePosition: CGPoint
if let _ = component.iconName {
titlePosition = CGPoint(x: size.width - padding - titleSize.width / 2.0, y: size.height / 2.0)
} else {
titlePosition = CGPoint(x: padding + titleSize.width / 2.0, y: size.height / 2.0)
}
if let animateTitleInDirection {
titleView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
titleView.center = CGPoint(x: titlePosition.x, y: titlePosition.y - 6.0 * animateTitleInDirection)
}
transition.setPosition(view: titleView, position: titlePosition)
titleView.bounds = CGRect(origin: CGPoint(), size: titleSize)
}
if let iconView = self.icon.view {
if iconView.superview == nil {
self.addSubview(iconView)
}
let iconPosition: CGPoint
if let _ = component.iconName {
iconPosition = CGPoint(x: leftPadding + iconSize.width / 2.0, y: size.height / 2.0)
} else {
iconPosition = CGPoint(x: size.width - padding - animationSize.width / 2.0, y: size.height / 2.0)
}
transition.setPosition(view: iconView, position: iconPosition)
transition.setBounds(view: iconView, bounds: CGRect(origin: CGPoint(), size: iconSize))
}
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,601 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import AccountContext
import TelegramPresentationData
import ContextUI
final class GiftAttributeListContextItem: ContextMenuCustomItem {
let context: AccountContext
let attributes: [StarGift.UniqueGift.Attribute]
let selectedAttributes: [ResaleGiftsContext.Attribute]
let attributeCount: [ResaleGiftsContext.Attribute: Int32]
let searchQuery: Signal<String, NoError>
let attributeSelected: (ResaleGiftsContext.Attribute, Bool) -> Void
let selectAll: () -> Void
init(
context: AccountContext,
attributes: [StarGift.UniqueGift.Attribute],
selectedAttributes: [ResaleGiftsContext.Attribute],
attributeCount: [ResaleGiftsContext.Attribute: Int32],
searchQuery: Signal<String, NoError>,
attributeSelected: @escaping (ResaleGiftsContext.Attribute, Bool) -> Void,
selectAll: @escaping () -> Void
) {
self.context = context
self.attributes = attributes
self.selectedAttributes = selectedAttributes
self.attributeCount = attributeCount
self.searchQuery = searchQuery
self.attributeSelected = attributeSelected
self.selectAll = selectAll
}
func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode {
return GiftAttributeListContextItemNode(
presentationData: presentationData,
item: self,
getController: getController,
actionSelected: actionSelected
)
}
}
private func actionForAttribute(attribute: StarGift.UniqueGift.Attribute, presentationData: PresentationData, selectedAttributes: Set<ResaleGiftsContext.Attribute>, searchQuery: String, item: GiftAttributeListContextItem, getController: @escaping () -> ContextControllerProtocol?) -> ContextMenuActionItem? {
let searchComponents = searchQuery.lowercased().components(separatedBy: .whitespaces).filter { !$0.isEmpty }
switch attribute {
case let .model(name, file, _), let .pattern(name, file, _):
let attributeId: ResaleGiftsContext.Attribute
if case .model = attribute {
attributeId = .model(file.fileId.id)
} else {
attributeId = .pattern(file.fileId.id)
}
let isSelected = selectedAttributes.isEmpty || selectedAttributes.contains(attributeId)
var entities: [MessageTextEntity] = []
var entityFiles: [Int64: TelegramMediaFile] = [:]
entities = [
MessageTextEntity(
range: 0..<1,
type: .CustomEmoji(stickerPack: nil, fileId: file.fileId.id)
)
]
entityFiles[file.fileId.id] = file
var title = "# \(name)"
var count = ""
if let counter = item.attributeCount[attributeId] {
count = " \(presentationStringsFormattedNumber(counter, presentationData.dateTimeFormat.groupingSeparator))"
entities.append(
MessageTextEntity(
range: title.count ..< title.count + count.count,
type: .Italic
)
)
title += count
}
let words = title.components(separatedBy: .whitespacesAndNewlines).filter { !$0.isEmpty }
var wordStartIndices: [String.Index] = []
var currentIndex = title.startIndex
for word in words {
while currentIndex < title.endIndex {
let range = title.range(of: word, range: currentIndex..<title.endIndex)
if let range = range {
wordStartIndices.append(range.lowerBound)
currentIndex = range.upperBound
break
}
currentIndex = title.index(after: currentIndex)
}
}
for (wordIndex, word) in words.enumerated() {
let lowercaseWord = word.lowercased()
for component in searchComponents {
if lowercaseWord.hasPrefix(component) {
let startIndex = wordStartIndices[wordIndex]
let prefixRange = startIndex..<title.index(startIndex, offsetBy: min(component.count, word.count))
entities.append(
MessageTextEntity(
range: title.distance(from: title.startIndex, to: prefixRange.lowerBound)..<title.distance(from: title.startIndex, to: prefixRange.upperBound),
type: .Bold
)
)
}
}
}
return ContextMenuActionItem(text: title, entities: entities, entityFiles: entityFiles, enableEntityAnimations: false, parseMarkdown: true, icon: { theme in
return isSelected ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, action: { _, f in
getController()?.dismiss(result: .dismissWithoutContent, completion: nil)
item.attributeSelected(attributeId, false)
}, longPressAction: { _, f in
getController()?.dismiss(result: .dismissWithoutContent, completion: nil)
item.attributeSelected(attributeId, true)
})
case let .backdrop(name, id, innerColor, outerColor, _, _, _):
let attributeId: ResaleGiftsContext.Attribute = .backdrop(id)
let isSelected = selectedAttributes.isEmpty || selectedAttributes.contains(attributeId)
var entities: [MessageTextEntity] = []
var title = " \(name)"
var count = ""
if let counter = item.attributeCount[attributeId] {
count = " \(presentationStringsFormattedNumber(counter, presentationData.dateTimeFormat.groupingSeparator))"
entities.append(
MessageTextEntity(range: title.count ..< title.count + count.count, type: .Italic)
)
title += count
}
let words = title.components(separatedBy: .whitespacesAndNewlines).filter { !$0.isEmpty }
var wordStartIndices: [String.Index] = []
var currentIndex = title.startIndex
for word in words {
while currentIndex < title.endIndex {
let range = title.range(of: word, range: currentIndex..<title.endIndex)
if let range = range {
wordStartIndices.append(range.lowerBound)
currentIndex = range.upperBound
break
}
currentIndex = title.index(after: currentIndex)
}
}
for (wordIndex, word) in words.enumerated() {
let lowercaseWord = word.lowercased()
for component in searchComponents {
if lowercaseWord.hasPrefix(component) {
let startIndex = wordStartIndices[wordIndex]
let prefixRange = startIndex..<title.index(startIndex, offsetBy: min(component.count, word.count))
entities.append(
MessageTextEntity(
range: title.distance(from: title.startIndex, to: prefixRange.lowerBound)..<title.distance(from: title.startIndex, to: prefixRange.upperBound),
type: .Bold
)
)
}
}
}
return ContextMenuActionItem(text: title, entities: entities, icon: { theme in
return isSelected ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, additionalLeftIcon: { _ in
return generateGradientFilledCircleImage(diameter: 24.0, colors: [UIColor(rgb: UInt32(bitPattern: innerColor)).cgColor, UIColor(rgb: UInt32(bitPattern: outerColor)).cgColor])
}, action: { _, f in
getController()?.dismiss(result: .dismissWithoutContent, completion: nil)
item.attributeSelected(attributeId, false)
}, longPressAction: { _, f in
getController()?.dismiss(result: .dismissWithoutContent, completion: nil)
item.attributeSelected(attributeId, true)
})
default:
return nil
}
}
private final class GiftAttributeListContextItemNode: ASDisplayNode, ContextMenuCustomNode, ContextActionNodeProtocol, ASScrollViewDelegate {
private let item: GiftAttributeListContextItem
private let presentationData: PresentationData
private let getController: () -> ContextControllerProtocol?
private let actionSelected: (ContextMenuActionResult) -> Void
private let scrollNode: ASScrollNode
private var actionNodes: [AnyHashable: ContextControllerActionsListActionItemNode] = [:]
private var separatorNodes: [AnyHashable: ASDisplayNode] = [:]
private var searchDisposable: Disposable?
private var searchQuery = ""
private var itemHeights: [AnyHashable: CGFloat] = [:]
private var totalContentHeight: CGFloat = 0
private var itemFrames: [AnyHashable: CGRect] = [:]
init(presentationData: PresentationData, item: GiftAttributeListContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) {
self.item = item
self.presentationData = presentationData.withUpdate(listsFontSize: .regular)
self.getController = getController
self.actionSelected = actionSelected
self.scrollNode = ASScrollNode()
super.init()
self.addSubnode(self.scrollNode)
self.searchDisposable = (item.searchQuery
|> deliverOnMainQueue).start(next: { [weak self] searchQuery in
guard let self, self.searchQuery != searchQuery else {
return
}
self.searchQuery = searchQuery.trimmingCharacters(in: .whitespacesAndNewlines)
self.invalidateLayout()
self.getController()?.requestLayout(transition: .immediate)
})
}
deinit {
self.searchDisposable?.dispose()
}
override func didLoad() {
super.didLoad()
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
self.scrollNode.view.alwaysBounceVertical = false
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.scrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 5.0, right: 0.0)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if let maxWidth = self.maxWidth {
self.updateScrolling(maxWidth: maxWidth)
}
}
enum ItemType {
case selectAll
case attribute(StarGift.UniqueGift.Attribute)
case noResults
case separator
}
private func getVisibleItems(in scrollView: UIScrollView, constrainedWidth: CGFloat) -> [(itemId: AnyHashable, itemType: ItemType, frame: CGRect)] {
let effectiveAttributes: [StarGift.UniqueGift.Attribute]
if self.searchQuery.isEmpty {
effectiveAttributes = self.item.attributes
} else {
effectiveAttributes = filteredAttributes(attributes: self.item.attributes, query: self.searchQuery)
}
var items: [(itemId: AnyHashable, itemType: ItemType, frame: CGRect)] = []
var yOffset: CGFloat = 0
let defaultHeight: CGFloat = 42.0
if self.searchQuery.isEmpty {
let selectAllId = AnyHashable("selectAll")
let height = self.itemHeights[selectAllId] ?? defaultHeight
let frame = CGRect(x: 0, y: yOffset, width: constrainedWidth, height: height)
items.append((selectAllId, .selectAll, frame))
yOffset += height
let separatorId = AnyHashable("separator_selectAll")
let separatorFrame = CGRect(x: 0, y: yOffset, width: constrainedWidth, height: UIScreenPixel)
items.append((separatorId, .separator, separatorFrame))
yOffset += UIScreenPixel
}
for (index, attribute) in effectiveAttributes.enumerated() {
let attributeId = self.getAttributeId(from: attribute)
let height = self.itemHeights[attributeId] ?? defaultHeight
let frame = CGRect(x: 0, y: yOffset, width: constrainedWidth, height: height)
items.append((attributeId, .attribute(attribute), frame))
yOffset += height
if index < effectiveAttributes.count - 1 {
let separatorId = AnyHashable("separator_\(attributeId)")
let separatorFrame = CGRect(x: 0, y: yOffset, width: constrainedWidth, height: UIScreenPixel)
items.append((separatorId, .separator, separatorFrame))
yOffset += UIScreenPixel
}
}
if !self.searchQuery.isEmpty && effectiveAttributes.isEmpty {
let noResultsId = AnyHashable("noResults")
let height = self.itemHeights[noResultsId] ?? defaultHeight
let frame = CGRect(x: 0, y: yOffset, width: constrainedWidth, height: height)
items.append((noResultsId, .noResults, frame))
yOffset += height
}
self.totalContentHeight = yOffset
for (itemId, _, frame) in items {
self.itemFrames[itemId] = frame
}
let visibleBounds = scrollView.bounds.insetBy(dx: 0.0, dy: -100.0)
return items.filter { visibleBounds.intersects($0.frame) }
}
private func getAttributeId(from attribute: StarGift.UniqueGift.Attribute) -> AnyHashable {
switch attribute {
case let .model(_, file, _):
return AnyHashable("model_\(file.fileId.id)")
case let .pattern(_, file, _):
return AnyHashable("pattern_\(file.fileId.id)")
case let .backdrop(_, id, _, _, _, _, _):
return AnyHashable("backdrop_\(id)")
default:
return AnyHashable("unknown")
}
}
private var maxWidth: CGFloat?
private func updateScrolling(maxWidth: CGFloat) {
let scrollView = self.scrollNode.view
let constrainedWidth = scrollView.bounds.width
let visibleItems = self.getVisibleItems(in: scrollView, constrainedWidth: constrainedWidth)
var validNodeIds: Set<AnyHashable> = []
for (itemId, itemType, frame) in visibleItems {
validNodeIds.insert(itemId)
switch itemType {
case .selectAll:
if self.actionNodes[itemId] == nil {
let selectAllAction = ContextMenuActionItem(text: presentationData.strings.Gift_Store_SelectAll, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor)
}, iconPosition: .left, action: { _, f in
self.getController()?.dismiss(result: .dismissWithoutContent, completion: nil)
self.item.selectAll()
})
let actionNode = ContextControllerActionsListActionItemNode(
context: self.item.context,
getController: self.getController,
requestDismiss: self.actionSelected,
requestUpdateAction: { _, _ in },
item: selectAllAction
)
self.actionNodes[itemId] = actionNode
self.scrollNode.addSubnode(actionNode)
}
case let .attribute(attribute):
if self.actionNodes[itemId] == nil {
let selectedAttributes = Set(self.item.selectedAttributes)
guard let action = actionForAttribute(
attribute: attribute,
presentationData: self.presentationData,
selectedAttributes: selectedAttributes,
searchQuery: self.searchQuery,
item: self.item,
getController: self.getController
) else { continue }
let actionNode = ContextControllerActionsListActionItemNode(
context: self.item.context,
getController: self.getController,
requestDismiss: self.actionSelected,
requestUpdateAction: { _, _ in },
item: action
)
self.actionNodes[itemId] = actionNode
self.scrollNode.addSubnode(actionNode)
} else {
let selectedAttributes = Set(self.item.selectedAttributes)
if let action = actionForAttribute(
attribute: attribute,
presentationData: self.presentationData,
selectedAttributes: selectedAttributes,
searchQuery: self.searchQuery,
item: self.item,
getController: self.getController
) {
self.actionNodes[itemId]?.setItem(item: action)
}
}
case .noResults:
if self.actionNodes[itemId] == nil {
let nopAction: ((ContextControllerProtocol?, @escaping (ContextMenuActionResult) -> Void) -> Void)? = nil
let emptyResultsAction = ContextMenuActionItem(
text: presentationData.strings.Gift_Store_NoResults,
textFont: .small,
icon: { _ in return nil },
action: nopAction
)
let actionNode = ContextControllerActionsListActionItemNode(
context: self.item.context,
getController: self.getController,
requestDismiss: self.actionSelected,
requestUpdateAction: { _, _ in },
item: emptyResultsAction
)
self.actionNodes[itemId] = actionNode
self.scrollNode.addSubnode(actionNode)
}
case .separator:
if self.separatorNodes[itemId] == nil {
let separatorNode = ASDisplayNode()
separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor
self.separatorNodes[itemId] = separatorNode
self.scrollNode.addSubnode(separatorNode)
}
}
if let actionNode = self.actionNodes[itemId] {
actionNode.frame = frame
let (minSize, complete) = actionNode.update(presentationData: self.presentationData, constrainedSize: frame.size)
self.itemHeights[itemId] = minSize.height
complete(CGSize(width: maxWidth, height: minSize.height), .immediate)
} else if let separatorNode = self.separatorNodes[itemId] {
separatorNode.frame = frame
}
}
var nodesToRemove: [AnyHashable] = []
for (nodeId, node) in self.actionNodes {
if !validNodeIds.contains(nodeId) {
nodesToRemove.append(nodeId)
node.removeFromSupernode()
}
}
for nodeId in nodesToRemove {
self.actionNodes.removeValue(forKey: nodeId)
}
var separatorsToRemove: [AnyHashable] = []
for (separatorId, separatorNode) in self.separatorNodes {
if !validNodeIds.contains(separatorId) {
separatorsToRemove.append(separatorId)
separatorNode.removeFromSupernode()
}
}
for separatorId in separatorsToRemove {
self.separatorNodes.removeValue(forKey: separatorId)
}
}
private func invalidateLayout() {
self.itemHeights.removeAll()
self.itemFrames.removeAll()
self.totalContentHeight = 0.0
}
func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) {
let minActionsWidth: CGFloat = 250.0
let maxActionsWidth: CGFloat = 300.0
let constrainedWidth = min(constrainedWidth, maxActionsWidth)
let maxWidth = max(constrainedWidth, minActionsWidth)
let maxHeight: CGFloat = min(360.0, constrainedHeight - 108.0)
if self.totalContentHeight == 0 {
let _ = self.getVisibleItems(in: UIScrollView(), constrainedWidth: constrainedWidth)
}
return (CGSize(width: maxWidth, height: min(maxHeight, self.totalContentHeight)), { size, transition in
self.maxWidth = maxWidth
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size))
self.scrollNode.view.contentSize = CGSize(width: size.width, height: self.totalContentHeight)
self.updateScrolling(maxWidth: maxWidth)
})
}
func updateTheme(presentationData: PresentationData) {
}
var isActionEnabled: Bool {
return true
}
func performAction() {
}
func setIsHighlighted(_ value: Bool) {
}
func canBeHighlighted() -> Bool {
return self.isActionEnabled
}
func updateIsHighlighted(isHighlighted: Bool) {
self.setIsHighlighted(isHighlighted)
}
func actionNode(at point: CGPoint) -> ContextActionNodeProtocol {
return self
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
for (_, actionNode) in self.actionNodes {
actionNode.updateIsHighlighted(isHighlighted: false)
}
}
}
private func stringTokens(_ string: String) -> [ValueBoxKey] {
let nsString = string.folding(options: .diacriticInsensitive, locale: .current).lowercased() as NSString
let flag = UInt(kCFStringTokenizerUnitWord)
let tokenizer = CFStringTokenizerCreate(kCFAllocatorDefault, nsString, CFRangeMake(0, nsString.length), flag, CFLocaleCopyCurrent())
var tokenType = CFStringTokenizerAdvanceToNextToken(tokenizer)
var tokens: [ValueBoxKey] = []
var addedTokens = Set<ValueBoxKey>()
while tokenType != [] {
let currentTokenRange = CFStringTokenizerGetCurrentTokenRange(tokenizer)
if currentTokenRange.location >= 0 && currentTokenRange.length != 0 {
let token = ValueBoxKey(length: currentTokenRange.length * 2)
nsString.getCharacters(token.memory.assumingMemoryBound(to: unichar.self), range: NSMakeRange(currentTokenRange.location, currentTokenRange.length))
if !addedTokens.contains(token) {
tokens.append(token)
addedTokens.insert(token)
}
}
tokenType = CFStringTokenizerAdvanceToNextToken(tokenizer)
}
return tokens
}
private func matchStringTokens(_ tokens: [ValueBoxKey], with other: [ValueBoxKey]) -> Bool {
if other.isEmpty {
return false
} else if other.count == 1 {
let otherToken = other[0]
for token in tokens {
if otherToken.isPrefix(to: token) {
return true
}
}
} else {
for otherToken in other {
var found = false
for token in tokens {
if otherToken.isPrefix(to: token) {
found = true
break
}
}
if !found {
return false
}
}
return true
}
return false
}
private func filteredAttributes(attributes: [StarGift.UniqueGift.Attribute], query: String) -> [StarGift.UniqueGift.Attribute] {
let queryTokens = stringTokens(query.lowercased())
var result: [StarGift.UniqueGift.Attribute] = []
for attribute in attributes {
let string: String
switch attribute {
case let .model(name, _, _):
string = name
case let .pattern(name, _, _):
string = name
case let .backdrop(name, _, _, _, _, _, _):
string = name
default:
continue
}
let tokens = stringTokens(string)
if matchStringTokens(tokens, with: queryTokens) {
result.append(attribute)
}
}
return result
}
@@ -0,0 +1,223 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import SwiftSignalKit
import Postbox
import TelegramCore
import AccountContext
import TelegramPresentationData
import ContextUI
import TextFieldComponent
import MultilineTextComponent
import BundleIconComponent
final class SearchContextItem: ContextMenuCustomItem {
let context: AccountContext
let placeholder: String
let value: String
let valueChanged: (String) -> Void
init(
context: AccountContext,
placeholder: String,
value: String,
valueChanged: @escaping (String) -> Void
) {
self.context = context
self.placeholder = placeholder
self.value = value
self.valueChanged = valueChanged
}
func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode {
return SearchContextItemNode(
presentationData: presentationData,
item: self,
getController: getController,
actionSelected: actionSelected
)
}
}
private final class SearchContextItemNode: ASDisplayNode, ContextMenuCustomNode, ContextActionNodeProtocol, ASScrollViewDelegate {
private let item: SearchContextItem
private let presentationData: PresentationData
private let getController: () -> ContextControllerProtocol?
private let actionSelected: (ContextMenuActionResult) -> Void
private let state = EmptyComponentState()
private let icon = ComponentView<Empty>()
private let inputField = ComponentView<Empty>()
private let inputFieldExternalState = TextFieldComponent.ExternalState()
private let inputPlaceholderView = ComponentView<Empty>()
private let inputClear = ComponentView<Empty>()
private var inputText = ""
private var validLayout: CGSize?
init(presentationData: PresentationData, item: SearchContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) {
self.item = item
self.presentationData = presentationData
self.getController = getController
self.actionSelected = actionSelected
super.init()
self.state._updated = { [weak self] transition, _ in
guard let self, let size = self.validLayout else {
return
}
self.internalUpdateLayout(size: size, transition: transition)
}
}
func internalUpdateLayout(size: CGSize, transition: ComponentTransition) {
let iconSize = self.icon.update(
transition: .immediate,
component: AnyComponent(BundleIconComponent(name: "Chat/Context Menu/Search", tintColor: self.presentationData.theme.contextMenu.primaryColor)),
environment: {},
containerSize: size
)
let iconFrame = CGRect(origin: CGPoint(x: 17.0, y: floorToScreenPixels((size.height - iconSize.height) / 2.0)), size: iconSize)
if let iconView = self.icon.view {
if iconView.superview == nil {
self.view.addSubview(iconView)
}
transition.setFrame(view: iconView, frame: iconFrame)
}
let inputInset: CGFloat = 42.0
self.inputField.parentState = self.state
let inputFieldSize = self.inputField.update(
transition: .immediate,
component: AnyComponent(TextFieldComponent(
context: self.item.context,
theme: self.presentationData.theme,
strings: self.presentationData.strings,
externalState: self.inputFieldExternalState,
fontSize: self.presentationData.listsFontSize.baseDisplaySize,
textColor: self.presentationData.theme.contextMenu.primaryColor,
accentColor: self.presentationData.theme.contextMenu.primaryColor,
insets: UIEdgeInsets(top: 8.0, left: 2.0, bottom: 8.0, right: 2.0),
hideKeyboard: false,
customInputView: nil,
resetText: nil,
isOneLineWhenUnfocused: false,
emptyLineHandling: .notAllowed,
formatMenuAvailability: .none,
returnKeyType: .search,
lockedFormatAction: {
},
present: { _ in
},
paste: { _ in
},
returnKeyAction: nil,
backspaceKeyAction: nil
)),
environment: {},
containerSize: CGSize(width: size.width - inputInset - 40.0, height: size.height)
)
let inputFieldFrame = CGRect(origin: CGPoint(x: inputInset, y: floorToScreenPixels((size.height - inputFieldSize.height) / 2.0)), size: inputFieldSize)
if let inputFieldView = self.inputField.view as? TextFieldComponent.View {
if inputFieldView.superview == nil {
self.view.addSubview(inputFieldView)
}
transition.setFrame(view: inputFieldView, frame: inputFieldFrame)
}
if self.inputText != self.inputFieldExternalState.text.string {
self.inputText = self.inputFieldExternalState.text.string
self.item.valueChanged(self.inputText)
}
let inputPlaceholderSize = self.inputPlaceholderView.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(
string: self.item.placeholder,
font: Font.regular(self.presentationData.listsFontSize.baseDisplaySize),
textColor: self.presentationData.theme.contextMenu.secondaryColor
)))
),
environment: {},
containerSize: size
)
let inputPlaceholderFrame = CGRect(origin: CGPoint(x: inputInset + 10.0, y: floorToScreenPixels(inputFieldFrame.midY - inputPlaceholderSize.height / 2.0)), size: inputPlaceholderSize)
if let inputPlaceholderView = self.inputPlaceholderView.view {
if inputPlaceholderView.superview == nil {
inputPlaceholderView.isUserInteractionEnabled = false
self.view.addSubview(inputPlaceholderView)
}
inputPlaceholderView.frame = inputPlaceholderFrame
inputPlaceholderView.isHidden = self.inputFieldExternalState.hasText
}
let inputClearSize = self.inputClear.update(
transition: .immediate,
component: AnyComponent(
Button(
content: AnyComponent(
BundleIconComponent(name: "Components/Search Bar/Clear", tintColor: self.presentationData.theme.contextMenu.secondaryColor, maxSize: CGSize(width: 24.0, height: 24.0))
),
action: { [weak self] in
guard let self else {
return
}
if let inputFieldView = self.inputField.view as? TextFieldComponent.View {
inputFieldView.updateText(NSAttributedString(), selectionRange: 0..<0)
}
}
)
),
environment: {},
containerSize: CGSize(width: 30.0, height: 30.0)
)
let inputClearFrame = CGRect(origin: CGPoint(x: size.width - inputClearSize.width - 16.0, y: floorToScreenPixels(inputFieldFrame.midY - inputClearSize.height / 2.0)), size: inputClearSize)
if let inputClearView = self.inputClear.view {
if inputClearView.superview == nil {
self.view.addSubview(inputClearView)
}
inputClearView.frame = inputClearFrame
inputClearView.isHidden = !self.inputFieldExternalState.hasText
}
}
func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) {
let maxWidth: CGFloat = 220.0
let height: CGFloat = 42.0
return (CGSize(width: maxWidth, height: height), { size, transition in
self.validLayout = size
self.internalUpdateLayout(size: size, transition: ComponentTransition(transition))
})
}
func updateTheme(presentationData: PresentationData) {
}
var isActionEnabled: Bool {
return true
}
func performAction() {
}
func setIsHighlighted(_ value: Bool) {
}
func canBeHighlighted() -> Bool {
return false
}
func updateIsHighlighted(isHighlighted: Bool) {
}
func actionNode(at point: CGPoint) -> ContextActionNodeProtocol {
return self
}
}
@@ -0,0 +1,71 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "GiftViewScreen",
module_name = "GiftViewScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUIPreferences",
"//submodules/AccountContext",
"//submodules/PresentationDataUtils",
"//submodules/Markdown",
"//submodules/TelegramNotices",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/MultilineTextWithEntitiesComponent",
"//submodules/Components/BalancedTextComponent",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/AppBundle",
"//submodules/Components/SheetComponent",
"//submodules/TelegramUI/Components/Stars/StarsAvatarComponent",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
"//submodules/TelegramUI/Components/Gifts/GiftAnimationComponent",
"//submodules/TelegramUI/Components/CheckComponent",
"//submodules/UndoUI",
"//submodules/ConfettiEffect",
"//submodules/TooltipUI",
"//submodules/TelegramUI/Components/Gifts/GiftItemComponent",
"//submodules/MoreButtonNode",
"//submodules/TelegramUI/Components/EmojiStatusComponent",
"//submodules/PasswordSetupUI",
"//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController",
"//submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent",
"//submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent",
"//submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen",
"//submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController",
"//submodules/ActivityIndicator",
"//submodules/TelegramUI/Components/TabSelectorComponent",
"//submodules/TelegramUI/Components/Stars/BalanceNeededScreen",
"//submodules/TelegramUI/Components/ChatThemeScreen",
"//submodules/ImageBlur",
"//submodules/TelegramUI/Components/PeerInfo/ProfileLevelRatingBarComponent",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
"//submodules/TelegramUI/Components/Stories/LiveChat/StoryLiveChatMessageComponent",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/BotPaymentsUI",
"//submodules/TelegramUI/Components/SegmentControlComponent",
"//submodules/TelegramUI/Components/Gifts/GiftRemainingCountComponent",
"//submodules/TelegramUI/Components/Gifts/InfoParagraphComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,177 @@
import Foundation
import UIKit
import Display
import ComponentFlow
private let nondigitsCharacterSet = CharacterSet(charactersIn: "0123456789").inverted
private let labelWidth: CGFloat = 16.0
private let labelHeight: CGFloat = 36.0
private let labelSize = CGSize(width: labelWidth, height: labelHeight)
private let font = Font.with(size: 24.0, design: .round, weight: .semibold, traits: [])
final class BadgeLabelView: UIView {
private class StackView: UIView {
var labels: [UILabel] = []
var currentValue: Int32 = 0
var color: UIColor = .white {
didSet {
for view in self.labels {
view.textColor = self.color
}
}
}
init() {
super.init(frame: CGRect(origin: .zero, size: labelSize))
var height: CGFloat = -labelHeight
for i in -1 ..< 10 {
let label = UILabel()
if i == -1 {
label.text = "9"
} else {
label.text = "\(i)"
}
label.textColor = self.color
label.font = font
label.textAlignment = .center
label.frame = CGRect(x: 0, y: height, width: labelWidth, height: labelHeight)
self.addSubview(label)
self.labels.append(label)
height += labelHeight
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(value: Int32, isFirst: Bool, isLast: Bool, transition: ComponentTransition) {
let previousValue = self.currentValue
self.currentValue = value
self.labels[1].alpha = isFirst && !isLast ? 0.0 : 1.0
if previousValue == 9 && value < 9 {
self.bounds = CGRect(
origin: CGPoint(
x: 0.0,
y: -1.0 * labelSize.height
),
size: labelSize
)
}
let bounds = CGRect(
origin: CGPoint(
x: 0.0,
y: CGFloat(value) * labelSize.height
),
size: labelSize
)
transition.setBounds(view: self, bounds: bounds)
}
}
private var itemViews: [Int: StackView] = [:]
private var staticLabel = UILabel()
init() {
super.init(frame: .zero)
self.clipsToBounds = true
self.isUserInteractionEnabled = false
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var color: UIColor = .white {
didSet {
self.staticLabel.textColor = self.color
for (_, view) in self.itemViews {
view.color = self.color
}
}
}
func update(value: String, transition: ComponentTransition) -> CGSize {
let alphaTransition = ComponentTransition.easeInOut(duration: 0.15)
if value.rangeOfCharacter(from: nondigitsCharacterSet) != nil {
for (_, view) in self.itemViews {
alphaTransition.setAlpha(view: view, alpha: 0.0)
}
if self.staticLabel.superview == nil {
self.staticLabel.alpha = 0.0
self.staticLabel.textColor = self.color
self.staticLabel.font = font
self.addSubview(self.staticLabel)
}
alphaTransition.setAlpha(view: self.staticLabel, alpha: 1.0)
self.staticLabel.text = value
let size = self.staticLabel.sizeThatFits(CGSize(width: 100.0, height: 100.0))
self.staticLabel.frame = CGRect(origin: .zero, size: CGSize(width: size.width, height: labelHeight))
return CGSize(width: ceil(self.staticLabel.bounds.width), height: ceil(self.staticLabel.bounds.height))
} else {
for (_, view) in self.itemViews {
alphaTransition.setAlpha(view: view, alpha: 1.0)
}
alphaTransition.setAlpha(view: self.staticLabel, alpha: 0.0)
}
let string = value
let stringArray = Array(string.map { String($0) }.reversed())
let totalWidth = CGFloat(stringArray.count) * labelWidth
var validIds: [Int] = []
for i in 0 ..< stringArray.count {
validIds.append(i)
let itemView: StackView
var itemTransition = transition
if let current = self.itemViews[i] {
itemView = current
} else {
itemTransition = transition.withAnimation(.none)
itemView = StackView()
itemView.color = self.color
self.itemViews[i] = itemView
self.addSubview(itemView)
}
let digit = Int32(stringArray[i]) ?? 0
itemView.update(value: digit, isFirst: i == stringArray.count - 1, isLast: i == 0, transition: transition)
itemTransition.setFrame(
view: itemView,
frame: CGRect(x: totalWidth - labelWidth * CGFloat(i + 1), y: 0.0, width: labelWidth, height: labelHeight)
)
}
var removeIds: [Int] = []
for (id, itemView) in self.itemViews {
if !validIds.contains(id) {
removeIds.append(id)
transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in
itemView.removeFromSuperview()
})
}
}
for id in removeIds {
self.itemViews.removeValue(forKey: id)
}
return CGSize(width: totalWidth, height: labelHeight)
}
}
@@ -0,0 +1,267 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import TelegramCore
import BundleIconComponent
import MultilineTextComponent
import MoreButtonNode
import AccountContext
import TelegramPresentationData
import TelegramStringFormatting
final class PriceButtonComponent: Component {
let price: CurrencyAmount
let dateTimeFormat: PresentationDateTimeFormat
init(
price: CurrencyAmount,
dateTimeFormat: PresentationDateTimeFormat
) {
self.price = price
self.dateTimeFormat = dateTimeFormat
}
static func ==(lhs: PriceButtonComponent, rhs: PriceButtonComponent) -> Bool {
return lhs.price == rhs.price && lhs.dateTimeFormat == rhs.dateTimeFormat
}
final class View: UIView {
private let backgroundView = UIView()
private let icon = UIImageView()
private let text = ComponentView<Empty>()
private var component: PriceButtonComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundView.clipsToBounds = true
self.addSubview(self.backgroundView)
self.backgroundView.addSubview(self.icon)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: PriceButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let previousComponent = self.component
self.component = component
self.state = state
if previousComponent?.price.currency != component.price.currency {
switch component.price.currency {
case .stars:
self.icon.image = UIImage(bundleImageName: "Premium/Stars/ButtonStar")?.withRenderingMode(.alwaysTemplate)
case .ton:
self.icon.image = UIImage(bundleImageName: "Ads/TonAbout")?.withRenderingMode(.alwaysTemplate)
}
}
var backgroundSize = CGSize(width: 42.0, height: 30.0)
let textSize = self.text.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: formatCurrencyAmountText(component.price, dateTimeFormat: component.dateTimeFormat),
font: Font.semibold(11.0),
textColor: UIColor(rgb: 0xffffff)
))
)),
environment: {},
containerSize: availableSize
)
let textFrame = CGRect(origin: CGPoint(x: 32.0, y: floorToScreenPixels((backgroundSize.height - textSize.height) / 2.0)), size: textSize)
if let textView = self.text.view {
if textView.superview == nil {
self.backgroundView.addSubview(textView)
}
transition.setFrame(view: textView, frame: textFrame)
}
backgroundSize.width += textSize.width
self.backgroundView.layer.cornerRadius = backgroundSize.height / 2.0
let backgroundColor: UIColor = UIColor(rgb: 0xffffff, alpha: 0.1)
transition.setBackgroundColor(view: self.backgroundView, color: backgroundColor)
let backgroundFrame = CGRect(origin: .zero, size: backgroundSize)
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
if let iconSize = self.icon.image?.size {
let iconFrame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((backgroundSize.height - iconSize.height) / 2.0)), size: iconSize)
transition.setFrame(view: self.icon, frame: iconFrame)
}
self.icon.tintColor = UIColor(rgb: 0xffffff)
return backgroundSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class ButtonsComponent: Component {
let theme: PresentationTheme
let isOverlay: Bool
let showMoreButton: Bool
let closePressed: () -> Void
let morePressed: (ASDisplayNode, ContextGesture?) -> Void
init(
theme: PresentationTheme,
isOverlay: Bool,
showMoreButton: Bool,
closePressed: @escaping () -> Void,
morePressed: @escaping (ASDisplayNode, ContextGesture?) -> Void
) {
self.theme = theme
self.isOverlay = isOverlay
self.showMoreButton = showMoreButton
self.closePressed = closePressed
self.morePressed = morePressed
}
static func ==(lhs: ButtonsComponent, rhs: ButtonsComponent) -> Bool {
return lhs.theme === rhs.theme && lhs.isOverlay == rhs.isOverlay && lhs.showMoreButton == rhs.showMoreButton
}
final class View: UIView {
private let backgroundView = UIView()
private let closeButton = HighlightTrackingButton()
private let closeIcon = UIImageView()
private let moreNode = MoreButtonNode(theme: defaultPresentationTheme, size: CGSize(width: 36.0, height: 36.0), encircled: false)
private var component: ButtonsComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundView.clipsToBounds = true
self.addSubview(self.backgroundView)
self.closeIcon.image = generateCloseButtonImage()
self.moreNode.updateColor(.white, transition: .immediate)
self.backgroundView.addSubview(self.moreNode.view)
self.backgroundView.addSubview(self.closeButton)
self.backgroundView.addSubview(self.closeIcon)
self.closeButton.highligthedChanged = { [weak self] highlighted in
guard let self else {
return
}
if highlighted {
self.closeIcon.layer.removeAnimation(forKey: "opacity")
self.closeIcon.alpha = 0.6
} else {
self.closeIcon.alpha = 1.0
self.closeIcon.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2)
}
}
self.closeButton.addTarget(self, action: #selector(self.closePressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func closePressed() {
guard let component = self.component else {
return
}
component.closePressed()
}
func update(component: ButtonsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let backgroundSize = CGSize(width: component.showMoreButton ? 70.0 : 30.0, height: 30.0)
self.backgroundView.layer.cornerRadius = backgroundSize.height / 2.0
let backgroundColor: UIColor = component.isOverlay ? UIColor(rgb: 0xffffff, alpha: 0.1) : UIColor(rgb: 0x808084, alpha: 0.1)
let foregroundColor: UIColor = component.isOverlay ? .white : component.theme.actionSheet.inputClearButtonColor
transition.setBackgroundColor(view: self.backgroundView, color: backgroundColor)
transition.setTintColor(view: self.closeIcon, color: foregroundColor)
let backgroundFrame = CGRect(origin: .zero, size: backgroundSize)
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
transition.setFrame(view: self.moreNode.view, frame: CGRect(origin: CGPoint(x: -7.0, y: -4.0), size: CGSize(width: 36.0, height: 36.0)))
transition.setAlpha(view: self.moreNode.view, alpha: component.showMoreButton ? 1.0 : 0.0)
self.moreNode.action = { [weak self] node, gesture in
guard let self, let component = self.component else {
return
}
component.morePressed(node, gesture)
}
let closeFrame = CGRect(origin: CGPoint(x: backgroundSize.width - 30.0 - (component.showMoreButton ? 3.0 : 0.0), y: 0.0), size: CGSize(width: 30.0, height: 30.0))
transition.setFrame(view: self.closeIcon, frame: closeFrame)
transition.setFrame(view: self.closeButton, frame: closeFrame)
return backgroundSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private func generateCloseButtonImage() -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setLineWidth(2.0)
context.setLineCap(.round)
context.setStrokeColor(UIColor.white.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()
})?.withRenderingMode(.alwaysTemplate)
}
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()
})
}
@@ -0,0 +1,706 @@
import Foundation
import UIKit
import AsyncDisplayKit
import TelegramPresentationData
import ComponentFlow
import AccountContext
import ViewControllerComponent
import TelegramCore
import SwiftSignalKit
import Display
import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import ButtonComponent
import PlainButtonComponent
import Markdown
import BundleIconComponent
import TextFormat
import TelegramStringFormatting
import GlassBarButtonComponent
import GiftItemComponent
import EdgeEffect
private final class GiftAuctionAcquiredScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let gift: StarGift
let acquiredGifts: [GiftAuctionAcquiredGift]
init(
context: AccountContext,
gift: StarGift,
acquiredGifts: [GiftAuctionAcquiredGift]
) {
self.context = context
self.gift = gift
self.acquiredGifts = acquiredGifts
}
static func ==(lhs: GiftAuctionAcquiredScreenComponent, rhs: GiftAuctionAcquiredScreenComponent) -> Bool {
return true
}
private struct ItemLayout: Equatable {
var containerSize: CGSize
var containerInset: CGFloat
var containerCornerRadius: CGFloat
var bottomInset: CGFloat
var topInset: CGFloat
init(containerSize: CGSize, containerInset: CGFloat, containerCornerRadius: CGFloat, bottomInset: CGFloat, topInset: CGFloat) {
self.containerSize = containerSize
self.containerInset = containerInset
self.containerCornerRadius = containerCornerRadius
self.bottomInset = bottomInset
self.topInset = topInset
}
}
private final class ScrollView: UIScrollView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return super.hitTest(point, with: event)
}
}
final class View: UIView, UIScrollViewDelegate {
private let dimView: UIView
private let containerView: UIView
private let backgroundLayer: SimpleLayer
private let navigationBarContainer: SparseContainerView
private let scrollView: ScrollView
private let scrollContentClippingView: SparseContainerView
private let scrollContentView: UIView
private let topEdgeEffectView: EdgeEffectView
private let bottomEdgeEffectView: EdgeEffectView
private let backgroundHandleView: UIImageView
private let closeButton = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private var itemsViews: [Int32: ComponentView<Empty>] = [:]
private let actionButton = ComponentView<Empty>()
private var ignoreScrolling: Bool = false
private var component: GiftAuctionAcquiredScreenComponent?
private weak var state: EmptyComponentState?
private var isUpdating: Bool = false
private var environment: ViewControllerComponentContainer.Environment?
private var itemLayout: ItemLayout?
override init(frame: CGRect) {
self.dimView = UIView()
self.containerView = UIView()
self.containerView.clipsToBounds = true
self.containerView.layer.cornerRadius = 40.0
self.containerView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
self.backgroundLayer = SimpleLayer()
self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
self.backgroundLayer.cornerRadius = 40.0
self.backgroundHandleView = UIImageView()
self.navigationBarContainer = SparseContainerView()
self.scrollView = ScrollView()
self.scrollContentClippingView = SparseContainerView()
self.scrollContentClippingView.clipsToBounds = true
self.scrollContentView = UIView()
self.topEdgeEffectView = EdgeEffectView()
self.topEdgeEffectView.clipsToBounds = true
self.topEdgeEffectView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
self.topEdgeEffectView.layer.cornerRadius = 40.0
self.bottomEdgeEffectView = EdgeEffectView()
super.init(frame: frame)
self.addSubview(self.dimView)
self.addSubview(self.containerView)
self.containerView.layer.addSublayer(self.backgroundLayer)
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.alwaysBounceVertical = true
self.scrollView.scrollsToTop = false
self.scrollView.delegate = self
self.scrollView.clipsToBounds = true
self.containerView.addSubview(self.scrollContentClippingView)
self.scrollContentClippingView.addSubview(self.scrollView)
self.scrollView.addSubview(self.scrollContentView)
self.containerView.addSubview(self.navigationBarContainer)
self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
self.updateScrolling(transition: .immediate)
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.bounds.contains(point) {
return nil
}
if !self.backgroundLayer.frame.contains(point) {
return self.dimView
}
if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) {
return result
}
let result = super.hitTest(point, with: event)
return result
}
@objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
guard let environment = self.environment, let controller = environment.controller() else {
return
}
controller.dismiss()
}
}
private func updateScrolling(transition: ComponentTransition) {
guard let itemLayout = self.itemLayout else {
return
}
var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset
topOffset = max(0.0, topOffset)
transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0))
transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset))
var topOffsetFraction = self.scrollView.bounds.minY / 100.0
topOffsetFraction = max(0.0, min(1.0, topOffsetFraction))
let minScale: CGFloat = (itemLayout.containerSize.width - 6.0 * 2.0) / itemLayout.containerSize.width
let minScaledTranslation: CGFloat = (itemLayout.containerSize.height - itemLayout.containerSize.height * minScale) * 0.5 - 6.0
let minScaledCornerRadius: CGFloat = itemLayout.containerCornerRadius
let scale = minScale * (1.0 - topOffsetFraction) + 1.0 * topOffsetFraction
let scaledTranslation = minScaledTranslation * (1.0 - topOffsetFraction)
let scaledCornerRadius = minScaledCornerRadius * (1.0 - topOffsetFraction) + itemLayout.containerCornerRadius * topOffsetFraction
var containerTransform = CATransform3DIdentity
containerTransform = CATransform3DTranslate(containerTransform, 0.0, scaledTranslation, 0.0)
containerTransform = CATransform3DScale(containerTransform, scale, scale, scale)
transition.setTransform(view: self.containerView, transform: containerTransform)
transition.setCornerRadius(layer: self.containerView.layer, cornerRadius: scaledCornerRadius)
}
func animateIn() {
self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY
self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
if let actionButtonView = self.actionButton.view {
actionButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
self.bottomEdgeEffectView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
func animateOut(completion: @escaping () -> Void) {
let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY
self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in
completion()
})
self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
if let actionButtonView = self.actionButton.view {
actionButtonView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
}
self.bottomEdgeEffectView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
}
private func openPeer(_ peer: EnginePeer, dismiss: Bool = true) {
guard let component = self.component, let controller = self.environment?.controller() as? GiftAuctionAcquiredScreen, let navigationController = controller.navigationController as? NavigationController else {
return
}
let context = component.context
let action = {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(
navigationController: navigationController,
chatController: nil,
context: context,
chatLocation: .peer(peer),
subject: nil,
botStart: nil,
updateTextInputState: nil,
keepStack: .always,
useExisting: true,
purposefulAction: nil,
scrollToEndIfExists: false,
activateMessageSearch: nil,
animated: true
))
}
if dismiss {
controller.dismiss()
Queue.mainQueue().after(0.4, {
action()
})
} else {
action()
}
}
func update(component: GiftAuctionAcquiredScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let environment = environment[ViewControllerComponentContainer.Environment.self].value
let themeUpdated = self.environment?.theme !== environment.theme
let resetScrolling = self.scrollView.bounds.width != availableSize.width
let fillingSize: CGFloat
if case .regular = environment.metrics.widthClass {
fillingSize = min(availableSize.width, 414.0) - environment.safeInsets.left * 2.0
} else {
fillingSize = min(availableSize.width, environment.deviceMetrics.screenSize.width) - environment.safeInsets.left * 2.0
}
let rawSideInset = floor((availableSize.width - fillingSize) * 0.5)
let sideInset: CGFloat = floor((availableSize.width - fillingSize) * 0.5) + 24.0
self.component = component
self.state = state
self.environment = environment
if themeUpdated {
self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
self.backgroundLayer.backgroundColor = environment.theme.actionSheet.opaqueItemBackgroundColor.cgColor
}
transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize))
var contentHeight: CGFloat = 75.0
let tableFont = Font.regular(15.0)
let tableBoldFont = Font.semibold(15.0)
let tableTextColor = environment.theme.list.itemPrimaryTextColor
for gift in component.acquiredGifts {
let id = gift.date
let itemView: ComponentView<Empty>
if let current = self.itemsViews[id] {
itemView = current
} else {
itemView = ComponentView()
self.itemsViews[id] = itemView
}
var items: [TableComponent.Item] = []
var giftSubject: GiftItemComponent.Subject?
var giftTitle: String = ""
if case let .generic(gift) = component.gift {
giftSubject = .starGift(gift: gift, price: "")
giftTitle = gift.title ?? ""
}
if let giftSubject {
let titleString: String
if let number = gift.number {
let fullGiftTitle = "\(giftTitle) #\(formatCollectibleNumber(number, dateTimeFormat: environment.dateTimeFormat))"
titleString = environment.strings.Gift_Acquired_GiftRound(fullGiftTitle, "\(gift.round)").string
} else {
titleString = environment.strings.Gift_Acquired_Round("\(gift.round)").string
}
items.append(.init(
id: "header",
title: nil,
hasBackground: true,
component: AnyComponent(HStack([
AnyComponentWithIdentity(id: "icon", component: AnyComponent(
GiftItemComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
peer: nil,
subject: giftSubject,
mode: .tableIcon
)
)),
AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: titleString, font: tableBoldFont, textColor: tableTextColor)))
)
)
], spacing: 1.0))
))
}
items.append(.init(
id: "recipient",
title: environment.strings.Gift_Acquired_Recipient,
component: AnyComponent(Button(
content: AnyComponent(
PeerCellComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
peer: gift.peer
)
),
action: { [weak self] in
guard let self else {
return
}
self.openPeer(gift.peer, dismiss: false)
}
))
))
items.append(.init(
id: "date",
title: environment.strings.Gift_Acquired_Date,
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: gift.date, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat), font: tableFont, textColor: tableTextColor))))
))
let valueString = "⭐️\(formatStarsAmountText(StarsAmount(value: gift.bidAmount, nanos: 0), dateTimeFormat: environment.dateTimeFormat))"
let valueAttributedString = NSMutableAttributedString(string: valueString, font: tableFont, textColor: tableTextColor)
let range = (valueAttributedString.string as NSString).range(of: "⭐️")
if range.location != NSNotFound {
valueAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
valueAttributedString.addAttribute(.baselineOffset, value: 1.0, range: range)
}
items.append(.init(
id: "bid",
title: environment.strings.Gift_Acquired_AcceptedBid,
component: AnyComponent(HStack([
AnyComponentWithIdentity(id: "stars", component: AnyComponent(MultilineTextWithEntitiesComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
placeholderColor: environment.theme.list.mediaPlaceholderColor,
text: .plain(valueAttributedString),
maximumNumberOfLines: 0
))),
AnyComponentWithIdentity(
id: AnyHashable("info"),
component: AnyComponent(Button(
content: AnyComponent(ButtonContentComponent(
context: component.context,
text: environment.strings.Gift_Acquired_Top("\(gift.position)").string,
color: environment.theme.list.itemAccentColor
)),
action: {
}
))
)
], spacing: 4.0))
))
let itemSize = itemView.update(
transition: transition,
component: AnyComponent(
TableComponent(
theme: environment.theme,
items: items
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
)
let itemFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: itemSize)
if let view = itemView.view {
if view.superview == nil {
self.scrollContentView.addSubview(view)
}
view.frame = itemFrame
}
contentHeight += itemSize.height
contentHeight += 20.0
}
if self.backgroundHandleView.image == nil {
self.backgroundHandleView.image = generateStretchableFilledCircleImage(diameter: 5.0, color: .white)?.withRenderingMode(.alwaysTemplate)
}
self.backgroundHandleView.tintColor = environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(environment.theme.overallDarkAppearance ? 0.2 : 0.07)
let backgroundHandleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - 36.0) * 0.5), y: 5.0), size: CGSize(width: 36.0, height: 5.0))
if self.backgroundHandleView.superview == nil {
self.navigationBarContainer.addSubview(self.backgroundHandleView)
}
transition.setFrame(view: self.backgroundHandleView, frame: backgroundHandleFrame)
let closeButtonSize = self.closeButton.update(
transition: .immediate,
component: AnyComponent(GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0),
backgroundColor: environment.theme.rootController.navigationBar.glassBarButtonBackgroundColor,
isDark: environment.theme.overallDarkAppearance,
state: .generic,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: environment.theme.rootController.navigationBar.glassBarButtonForegroundColor
)
)),
action: { [weak self] _ in
guard let self else {
return
}
self.environment?.controller()?.dismiss()
}
)),
environment: {},
containerSize: CGSize(width: 40.0, height: 40.0)
)
let closeButtonFrame = CGRect(origin: CGPoint(x: rawSideInset + 16.0, y: 16.0), size: closeButtonSize)
if let closeButtonView = self.closeButton.view {
if closeButtonView.superview == nil {
self.navigationBarContainer.addSubview(closeButtonView)
}
transition.setFrame(view: closeButtonView, frame: closeButtonFrame)
}
let containerInset: CGFloat = environment.statusBarHeight + 10.0
var initialContentHeight = contentHeight
let clippingY: CGFloat
let title = self.title
let actionButton = self.actionButton
let titleText = environment.strings.Gift_Acquired_Title(Int32(component.acquiredGifts.count))
let titleSize = title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: titleText, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
)
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: 26.0), size: titleSize)
if let titleView = title.view {
if titleView.superview == nil {
self.navigationBarContainer.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: titleFrame)
}
initialContentHeight = contentHeight
let buttonAttributedString = NSMutableAttributedString(string: environment.strings.Common_OK, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 54.0, sideInset: 32.0)
let actionButtonSize = actionButton.update(
transition: transition,
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: environment.theme.list.itemCheckColors.fillColor,
foreground: environment.theme.list.itemCheckColors.foregroundColor,
pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
cornerRadius: 54.0 * 0.5
),
content: AnyComponentWithIdentity(
id: AnyHashable("ok"),
component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))
),
isEnabled: true,
displaysProgress: false,
action: { [weak self] in
guard let self else {
return
}
self.environment?.controller()?.dismiss()
}
)),
environment: {},
containerSize: CGSize(width: fillingSize - buttonInsets.left - buttonInsets.right, height: 54.0)
)
let edgeEffectHeight: CGFloat = 80.0
let edgeEffectFrame = CGRect(origin: CGPoint(x: rawSideInset, y: 0.0), size: CGSize(width: fillingSize, height: edgeEffectHeight))
transition.setFrame(view: self.topEdgeEffectView, frame: edgeEffectFrame)
self.topEdgeEffectView.update(content: environment.theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 1.0, rect: edgeEffectFrame, edge: .top, edgeSize: edgeEffectFrame.height, transition: transition)
if self.topEdgeEffectView.superview == nil {
self.navigationBarContainer.insertSubview(self.topEdgeEffectView, at: 0)
}
var bottomPanelHeight = 13.0 + buttonInsets.bottom + actionButtonSize.height
let bottomEdgeEffectHeight: CGFloat = bottomPanelHeight
let bottomEdgeEffectFrame = CGRect(origin: CGPoint(x: rawSideInset, y: availableSize.height - bottomEdgeEffectHeight), size: CGSize(width: fillingSize, height: bottomEdgeEffectHeight))
transition.setFrame(view: self.bottomEdgeEffectView, frame: bottomEdgeEffectFrame)
self.bottomEdgeEffectView.update(content: environment.theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 1.0, rect: bottomEdgeEffectFrame, edge: .bottom, edgeSize: bottomEdgeEffectFrame.height, transition: transition)
if self.bottomEdgeEffectView.superview == nil {
self.containerView.addSubview(self.bottomEdgeEffectView)
}
let actionButtonFrame = CGRect(origin: CGPoint(x: rawSideInset + buttonInsets.left, y: availableSize.height - buttonInsets.bottom - actionButtonSize.height), size: actionButtonSize)
bottomPanelHeight -= 1.0
if let actionButtonView = actionButton.view {
if actionButtonView.superview == nil {
self.containerView.addSubview(actionButtonView)
}
transition.setFrame(view: actionButtonView, frame: actionButtonFrame)
}
contentHeight += bottomPanelHeight
initialContentHeight += bottomPanelHeight
clippingY = actionButtonFrame.maxY + 24.0
let topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight)
let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset)
self.scrollContentClippingView.layer.cornerRadius = 38.0
self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, containerCornerRadius: environment.deviceMetrics.screenCornerRadius, bottomInset: environment.safeInsets.bottom, topInset: topInset)
transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight)))
transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0))
transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: CGSize(width: fillingSize, height: availableSize.height)))
let scrollClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: containerInset), size: CGSize(width: availableSize.width, height: clippingY - containerInset))
transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center)
transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size))
self.ignoreScrolling = true
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)))
let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight)
if contentSize != self.scrollView.contentSize {
self.scrollView.contentSize = contentSize
}
if resetScrolling {
self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize)
}
self.ignoreScrolling = false
self.updateScrolling(transition: transition)
transition.setPosition(view: self.containerView, position: CGRect(origin: CGPoint(), size: availableSize).center)
transition.setBounds(view: self.containerView, bounds: CGRect(origin: CGPoint(), size: availableSize))
if let controller = environment.controller(), !controller.automaticallyControlPresentationContextLayout {
let bottomInset: CGFloat = contentHeight - 12.0
let layout = ContainerViewLayout(
size: availableSize,
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0),
safeInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0),
additionalInsets: .zero,
statusBarHeight: environment.statusBarHeight,
inputHeight: nil,
inputHeightIsInteractivellyChanging: false,
inVoiceOver: false
)
controller.presentationContext.containerLayoutUpdated(layout, transition: transition.containedViewLayoutTransition)
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public class GiftAuctionAcquiredScreen: ViewControllerComponentContainer {
private let context: AccountContext
private var didPlayAppearAnimation: Bool = false
private var isDismissed: Bool = false
public init(context: AccountContext, gift: StarGift, acquiredGifts: [GiftAuctionAcquiredGift]) {
self.context = context
super.init(context: context, component: GiftAuctionAcquiredScreenComponent(
context: context,
gift: gift,
acquiredGifts: acquiredGifts
), navigationBarAppearance: .none, theme: .default)
self.statusBar.statusBarStyle = .Ignore
self.navigationPresentation = .flatModal
self.blocksBackgroundWhenInOverlay = true
self.automaticallyControlPresentationContextLayout = false
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.view.disablesInteractiveModalDismiss = true
if !self.didPlayAppearAnimation {
self.didPlayAppearAnimation = true
if let componentView = self.node.hostView.componentView as? GiftAuctionAcquiredScreenComponent.View {
componentView.animateIn()
}
}
}
override public func dismiss(completion: (() -> Void)? = nil) {
if !self.isDismissed {
self.isDismissed = true
if let componentView = self.node.hostView.componentView as? GiftAuctionAcquiredScreenComponent.View {
componentView.animateOut(completion: { [weak self] in
completion?()
self?.dismiss(animated: false)
})
} else {
self.dismiss(animated: false)
}
}
}
}
@@ -0,0 +1,805 @@
import Foundation
import UIKit
import AsyncDisplayKit
import TelegramPresentationData
import ComponentFlow
import AccountContext
import ViewControllerComponent
import TelegramCore
import SwiftSignalKit
import Display
import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import ButtonComponent
import PlainButtonComponent
import Markdown
import BundleIconComponent
import TextFormat
import TelegramStringFormatting
import GlassBarButtonComponent
import GiftItemComponent
import EdgeEffect
import AnimatedTextComponent
private final class GiftAuctionActiveBidsScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
init(
context: AccountContext
) {
self.context = context
}
static func ==(lhs: GiftAuctionActiveBidsScreenComponent, rhs: GiftAuctionActiveBidsScreenComponent) -> Bool {
return true
}
private struct ItemLayout: Equatable {
var containerSize: CGSize
var containerInset: CGFloat
var containerCornerRadius: CGFloat
var bottomInset: CGFloat
var topInset: CGFloat
init(containerSize: CGSize, containerInset: CGFloat, containerCornerRadius: CGFloat, bottomInset: CGFloat, topInset: CGFloat) {
self.containerSize = containerSize
self.containerInset = containerInset
self.containerCornerRadius = containerCornerRadius
self.bottomInset = bottomInset
self.topInset = topInset
}
}
private final class ScrollView: UIScrollView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return super.hitTest(point, with: event)
}
}
final class View: UIView, UIScrollViewDelegate {
private let dimView: UIView
private let containerView: UIView
private let backgroundLayer: SimpleLayer
private let navigationBarContainer: SparseContainerView
private let scrollView: ScrollView
private let scrollContentClippingView: SparseContainerView
private let scrollContentView: UIView
private let topEdgeEffectView: EdgeEffectView
private let backgroundHandleView: UIImageView
private let closeButton = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private var itemsViews: [Int64: ComponentView<Empty>] = [:]
private var auctionStates: [GiftAuctionContext.State] = []
private var auctionStatesDisposable: Disposable?
private var ignoreScrolling: Bool = false
private var giftAuctionTimer: SwiftSignalKit.Timer?
private var component: GiftAuctionActiveBidsScreenComponent?
private weak var state: EmptyComponentState?
private var isUpdating: Bool = false
private var environment: ViewControllerComponentContainer.Environment?
private var itemLayout: ItemLayout?
override init(frame: CGRect) {
self.dimView = UIView()
self.containerView = UIView()
self.containerView.clipsToBounds = true
self.containerView.layer.cornerRadius = 40.0
self.containerView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
self.backgroundLayer = SimpleLayer()
self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
self.backgroundLayer.cornerRadius = 40.0
self.backgroundHandleView = UIImageView()
self.navigationBarContainer = SparseContainerView()
self.scrollView = ScrollView()
self.scrollContentClippingView = SparseContainerView()
self.scrollContentClippingView.clipsToBounds = true
self.scrollContentView = UIView()
self.topEdgeEffectView = EdgeEffectView()
self.topEdgeEffectView.clipsToBounds = true
self.topEdgeEffectView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
self.topEdgeEffectView.layer.cornerRadius = 40.0
super.init(frame: frame)
self.addSubview(self.dimView)
self.addSubview(self.containerView)
self.containerView.layer.addSublayer(self.backgroundLayer)
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.alwaysBounceVertical = true
self.scrollView.scrollsToTop = false
self.scrollView.delegate = self
self.scrollView.clipsToBounds = true
self.containerView.addSubview(self.scrollContentClippingView)
self.scrollContentClippingView.addSubview(self.scrollView)
self.scrollView.addSubview(self.scrollContentView)
self.containerView.addSubview(self.navigationBarContainer)
self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.auctionStatesDisposable?.dispose()
self.giftAuctionTimer?.invalidate()
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
self.updateScrolling(transition: .immediate)
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.bounds.contains(point) {
return nil
}
if !self.backgroundLayer.frame.contains(point) {
return self.dimView
}
if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) {
return result
}
let result = super.hitTest(point, with: event)
return result
}
@objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
guard let environment = self.environment, let controller = environment.controller() else {
return
}
controller.dismiss()
}
}
private func updateScrolling(transition: ComponentTransition) {
guard let itemLayout = self.itemLayout else {
return
}
var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset
topOffset = max(0.0, topOffset)
transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0))
transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset))
var topOffsetFraction = self.scrollView.bounds.minY / 100.0
topOffsetFraction = max(0.0, min(1.0, topOffsetFraction))
let minScale: CGFloat = (itemLayout.containerSize.width - 6.0 * 2.0) / itemLayout.containerSize.width
let minScaledTranslation: CGFloat = (itemLayout.containerSize.height - itemLayout.containerSize.height * minScale) * 0.5 - 6.0
let minScaledCornerRadius: CGFloat = itemLayout.containerCornerRadius
let scale = minScale * (1.0 - topOffsetFraction) + 1.0 * topOffsetFraction
let scaledTranslation = minScaledTranslation * (1.0 - topOffsetFraction)
let scaledCornerRadius = minScaledCornerRadius * (1.0 - topOffsetFraction) + itemLayout.containerCornerRadius * topOffsetFraction
var containerTransform = CATransform3DIdentity
containerTransform = CATransform3DTranslate(containerTransform, 0.0, scaledTranslation, 0.0)
containerTransform = CATransform3DScale(containerTransform, scale, scale, scale)
transition.setTransform(view: self.containerView, transform: containerTransform)
transition.setCornerRadius(layer: self.containerView.layer, cornerRadius: scaledCornerRadius)
}
func animateIn() {
self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY
self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
func animateOut(completion: @escaping () -> Void) {
let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY
self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in
completion()
})
self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
}
func update(component: GiftAuctionActiveBidsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let environment = environment[ViewControllerComponentContainer.Environment.self].value
let themeUpdated = self.environment?.theme !== environment.theme
let resetScrolling = self.scrollView.bounds.width != availableSize.width
let fillingSize: CGFloat
if case .regular = environment.metrics.widthClass {
fillingSize = min(availableSize.width, 414.0) - environment.safeInsets.left * 2.0
} else {
fillingSize = min(availableSize.width, environment.deviceMetrics.screenSize.width) - environment.safeInsets.left * 2.0
}
let rawSideInset: CGFloat = floor((availableSize.width - fillingSize) * 0.5)
let sideInset: CGFloat = rawSideInset + 16.0
if self.component == nil, let giftAuctionsManager = component.context.giftAuctionsManager {
self.auctionStatesDisposable = (giftAuctionsManager.state
|> deliverOnMainQueue).start(next: { [weak self] auctionStates in
guard let self else {
return
}
self.auctionStates = auctionStates.filter { state in
if case .ongoing = state.auctionState {
return true
} else {
return false
}
}
self.state?.updated(transition: .immediate)
if self.auctionStates.isEmpty {
self.environment?.controller()?.dismiss()
}
})
self.giftAuctionTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
self?.state?.updated()
}, queue: Queue.mainQueue())
self.giftAuctionTimer?.start()
}
self.component = component
self.state = state
self.environment = environment
let theme = environment.theme.withModalBlocksBackground()
if themeUpdated {
self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
self.backgroundLayer.backgroundColor = theme.list.blocksBackgroundColor.cgColor
}
transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize))
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
var contentHeight: CGFloat = 75.0
var validKeys: Set<Int64> = Set()
for auctionState in self.auctionStates {
let id = auctionState.gift.giftId
validKeys.insert(id)
let itemView: ComponentView<Empty>
if let current = self.itemsViews[id] {
itemView = current
} else {
itemView = ComponentView()
self.itemsViews[id] = itemView
}
let itemSize = itemView.update(
transition: transition,
component: AnyComponent(
ActiveAuctionComponent(
context: component.context,
theme: theme,
strings: environment.strings,
dateTimeFormat: environment.dateTimeFormat,
state: auctionState,
currentTime: currentTime,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
if let giftAuctionsManager = component.context.giftAuctionsManager {
let _ = (giftAuctionsManager.auctionContext(for: .giftId(id))
|> deliverOnMainQueue).start(next: { [weak self] auction in
guard let self, let component = self.component, let auction, let controller = environment.controller(), let navigationController = controller.navigationController as? NavigationController else {
return
}
let bidController = component.context.sharedContext.makeGiftAuctionBidScreen(context: component.context, toPeerId: auction.currentBidPeerId ?? component.context.account.peerId, text: nil, entities: nil, hideName: false, auctionContext: auction, acquiredGifts: nil)
navigationController.pushViewController(bidController)
})
}
}
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
)
let itemFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: itemSize)
if let view = itemView.view {
if view.superview == nil {
self.scrollContentView.addSubview(view)
}
view.frame = itemFrame
}
contentHeight += itemSize.height
contentHeight += 20.0
}
contentHeight -= 10.0
var removeKeys: [Int64] = []
for (id, item) in self.itemsViews {
if !validKeys.contains(id) {
removeKeys.append(id)
if let itemView = item.view {
transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in
itemView.removeFromSuperview()
})
}
}
}
for id in removeKeys {
self.itemsViews.removeValue(forKey: id)
}
if self.backgroundHandleView.image == nil {
self.backgroundHandleView.image = generateStretchableFilledCircleImage(diameter: 5.0, color: .white)?.withRenderingMode(.alwaysTemplate)
}
self.backgroundHandleView.tintColor = environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(environment.theme.overallDarkAppearance ? 0.2 : 0.07)
let backgroundHandleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - 36.0) * 0.5), y: 5.0), size: CGSize(width: 36.0, height: 5.0))
if self.backgroundHandleView.superview == nil {
self.navigationBarContainer.addSubview(self.backgroundHandleView)
}
transition.setFrame(view: self.backgroundHandleView, frame: backgroundHandleFrame)
let closeButtonSize = self.closeButton.update(
transition: .immediate,
component: AnyComponent(GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0),
backgroundColor: environment.theme.rootController.navigationBar.glassBarButtonBackgroundColor,
isDark: environment.theme.overallDarkAppearance,
state: .generic,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: environment.theme.rootController.navigationBar.glassBarButtonForegroundColor
)
)),
action: { [weak self] _ in
guard let self else {
return
}
self.environment?.controller()?.dismiss()
}
)),
environment: {},
containerSize: CGSize(width: 40.0, height: 40.0)
)
let closeButtonFrame = CGRect(origin: CGPoint(x: rawSideInset + 16.0, y: 16.0), size: closeButtonSize)
if let closeButtonView = self.closeButton.view {
if closeButtonView.superview == nil {
self.navigationBarContainer.addSubview(closeButtonView)
}
transition.setFrame(view: closeButtonView, frame: closeButtonFrame)
}
let containerInset: CGFloat = environment.statusBarHeight + 10.0
contentHeight += environment.safeInsets.bottom
var initialContentHeight = contentHeight
let clippingY: CGFloat
let titleText: String = environment.strings.Gift_ActiveAuctions_Title(Int32(self.auctionStates.count))
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: titleText, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
)
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: 26.0), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
self.navigationBarContainer.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: titleFrame)
}
initialContentHeight = contentHeight
let edgeEffectHeight: CGFloat = 80.0
let edgeEffectFrame = CGRect(origin: CGPoint(x: rawSideInset, y: 0.0), size: CGSize(width: fillingSize, height: edgeEffectHeight))
transition.setFrame(view: self.topEdgeEffectView, frame: edgeEffectFrame)
self.topEdgeEffectView.update(content: environment.theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 1.0, rect: edgeEffectFrame, edge: .top, edgeSize: edgeEffectFrame.height, transition: transition)
if self.topEdgeEffectView.superview == nil {
self.navigationBarContainer.insertSubview(self.topEdgeEffectView, at: 0)
}
clippingY = availableSize.height
let topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight)
let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset)
self.scrollContentClippingView.layer.cornerRadius = 38.0
self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, containerCornerRadius: environment.deviceMetrics.screenCornerRadius, bottomInset: environment.safeInsets.bottom, topInset: topInset)
transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight)))
transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0))
transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: CGSize(width: fillingSize, height: availableSize.height)))
let scrollClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: containerInset), size: CGSize(width: availableSize.width, height: clippingY - containerInset))
transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center)
transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size))
self.ignoreScrolling = true
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)))
let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight)
if contentSize != self.scrollView.contentSize {
self.scrollView.contentSize = contentSize
}
if resetScrolling {
self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize)
}
self.ignoreScrolling = false
self.updateScrolling(transition: transition)
transition.setPosition(view: self.containerView, position: CGRect(origin: CGPoint(), size: availableSize).center)
transition.setBounds(view: self.containerView, bounds: CGRect(origin: CGPoint(), size: availableSize))
if let controller = environment.controller(), !controller.automaticallyControlPresentationContextLayout {
let bottomInset: CGFloat = contentHeight - 12.0
let layout = ContainerViewLayout(
size: availableSize,
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0),
safeInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0),
additionalInsets: .zero,
statusBarHeight: environment.statusBarHeight,
inputHeight: nil,
inputHeightIsInteractivellyChanging: false,
inVoiceOver: false
)
controller.presentationContext.containerLayoutUpdated(layout, transition: transition.containedViewLayoutTransition)
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public class GiftAuctionActiveBidsScreen: ViewControllerComponentContainer {
private let context: AccountContext
private var didPlayAppearAnimation: Bool = false
private var isDismissed: Bool = false
public init(context: AccountContext) {
self.context = context
super.init(context: context, component: GiftAuctionActiveBidsScreenComponent(
context: context
), navigationBarAppearance: .none, theme: .default)
self.statusBar.statusBarStyle = .Ignore
self.navigationPresentation = .flatModal
self.blocksBackgroundWhenInOverlay = true
self.automaticallyControlPresentationContextLayout = false
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.view.disablesInteractiveModalDismiss = true
if !self.didPlayAppearAnimation {
self.didPlayAppearAnimation = true
if let componentView = self.node.hostView.componentView as? GiftAuctionActiveBidsScreenComponent.View {
componentView.animateIn()
}
}
}
override public func dismiss(completion: (() -> Void)? = nil) {
if !self.isDismissed {
self.isDismissed = true
if let componentView = self.node.hostView.componentView as? GiftAuctionActiveBidsScreenComponent.View {
componentView.animateOut(completion: { [weak self] in
completion?()
self?.dismiss(animated: false)
})
} else {
self.dismiss(animated: false)
}
}
}
}
private final class ActiveAuctionComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let dateTimeFormat: PresentationDateTimeFormat
let state: GiftAuctionContext.State
let currentTime: Int32
let action: () -> Void
init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
dateTimeFormat: PresentationDateTimeFormat,
state: GiftAuctionContext.State,
currentTime: Int32,
action: @escaping () -> Void
) {
self.context = context
self.theme = theme
self.strings = strings
self.dateTimeFormat = dateTimeFormat
self.state = state
self.currentTime = currentTime
self.action = action
}
static func ==(lhs: ActiveAuctionComponent, rhs: ActiveAuctionComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.state != rhs.state {
return false
}
if lhs.currentTime != rhs.currentTime {
return false
}
return true
}
final class View: UIView {
private let icon = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private let subtitle = ComponentView<Empty>()
private let button = ComponentView<Empty>()
private var component: ActiveAuctionComponent?
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.cornerRadius = 26.0
self.clipsToBounds = true
}
required init(coder: NSCoder) {
preconditionFailure()
}
func update(component: ActiveAuctionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.backgroundColor = component.theme.list.itemBlocksBackgroundColor
var size = CGSize(width: availableSize.width, height: 0.0)
size.height += 11.0
if case let .generic(gift) = component.state.gift {
let titleSize = self.icon.update(
transition: .immediate,
component: AnyComponent(GiftItemComponent(
context: component.context,
theme: component.theme,
strings: component.strings,
subject: .starGift(gift: gift, price: ""),
mode: .preview
)),
environment: {},
containerSize: CGSize(width: 64.0, height: 64.0)
)
let iconFrame = CGRect(origin: CGPoint(x: 2.0, y: 0.0), size: titleSize)
if let iconView = self.icon.view {
if iconView.superview == nil {
self.addSubview(iconView)
}
iconView.frame = iconFrame
}
}
var endTime = component.currentTime
var titleText: String = ""
var subtitleText: String = ""
var subtitleTextColor = component.theme.list.itemPrimaryTextColor
if case let .ongoing(_, startDate, _, _, _, _, nextRoundDate, _, currentRound, totalRound, _, _) = component.state.auctionState, let myBid = component.state.myState.bidAmount {
titleText = component.strings.Gift_ActiveAuctions_Round("\(currentRound)", "\(totalRound)").string
let bidString = "#\(presentationStringsFormattedNumber(Int32(clamping: myBid), component.dateTimeFormat.groupingSeparator))"
if component.currentTime < startDate {
subtitleText = component.strings.Gift_ActiveAuctions_UpcomingBid
} else if let place = component.state.place, case let .generic(gift) = component.state.gift, let auctionGiftsPerRound = gift.auctionGiftsPerRound, place > auctionGiftsPerRound {
subtitleText = component.strings.Gift_ActiveAuctions_Outbid(bidString).string
subtitleTextColor = component.theme.list.itemDestructiveColor
} else {
subtitleText = component.strings.Gift_ActiveAuctions_Winning(bidString, "\(component.state.place ?? 0)").string
}
endTime = nextRoundDate
}
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: titleText, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)),
maximumNumberOfLines: 2
)),
environment: {},
containerSize: CGSize(width: availableSize.width - 63.0 - 20.0, height: 100.0)
)
let titleFrame = CGRect(origin: CGPoint(x: 63.0, y: size.height), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
titleView.frame = titleFrame
}
size.height += titleSize.height
size.height += 2.0
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let textColor = subtitleTextColor
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: textColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})
let attributedString = parseMarkdownIntoAttributedString(subtitleText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString
if let range = attributedString.string.range(of: "#") {
attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: attributedString.string))
attributedString.addAttribute(.baselineOffset, value: 2.0, range: NSRange(range, in: attributedString.string))
}
let subtitleSize = self.subtitle.update(
transition: .immediate,
component: AnyComponent(MultilineTextWithEntitiesComponent(context: component.context, animationCache: component.context.animationCache, animationRenderer: component.context.animationRenderer, placeholderColor: .clear, text: .plain(attributedString), maximumNumberOfLines: 2
)),
environment: {},
containerSize: CGSize(width: availableSize.width - 63.0 - 20.0, height: 100.0)
)
let subtitleFrame = CGRect(origin: CGPoint(x: 63.0, y: size.height), size: subtitleSize)
if let subtitleView = self.subtitle.view {
if subtitleView.superview == nil {
self.addSubview(subtitleView)
}
subtitleView.frame = subtitleFrame
}
size.height += subtitleSize.height
size.height += 19.0
let endTimeout = max(0, endTime - component.currentTime)
let hours = Int(endTimeout / 3600)
let minutes = Int((endTimeout % 3600) / 60)
let seconds = Int(endTimeout % 60)
var buttonAnimatedTitleItems: [AnimatedTextComponent.Item] = []
if hours > 0 {
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "h", content: .number(hours, minDigits: 1)))
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "colon1", content: .text(":")))
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "m", content: .number(minutes, minDigits: 2)))
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "colon2", content: .text(":")))
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "s", content: .number(seconds, minDigits: 2)))
} else {
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "m", content: .number(minutes, minDigits: 2)))
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "colon2", content: .text(":")))
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "s", content: .number(seconds, minDigits: 2)))
}
let buttonSize = self.button.update(
transition: .spring(duration: 0.2),
component: AnyComponent(
ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: component.theme.list.itemCheckColors.fillColor,
foreground: component.theme.list.itemCheckColors.foregroundColor,
pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: AnyComponentWithIdentity(id: "label", component: AnyComponent(
HStack([
AnyComponentWithIdentity(id: "icon", component: AnyComponent(
BundleIconComponent(name: "Premium/Auction/BidSmall", tintColor: component.theme.list.itemCheckColors.foregroundColor)
)),
AnyComponentWithIdentity(id: "label", component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: component.strings.Gift_ActiveAuctions_RaiseBid, font: Font.semibold(17.0), textColor: component.theme.list.itemCheckColors.foregroundColor)))
)),
AnyComponentWithIdentity(id: "timer", component: AnyComponent(
AnimatedTextComponent(
font: Font.with(size: 17.0, weight: .medium, traits: .monospacedNumbers),
color: component.theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7),
items: buttonAnimatedTitleItems,
noDelay: true
)
))
], spacing: 5.0)
)),
action: {
component.action()
}
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - 32.0, height: 52.0)
)
let buttonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - buttonSize.width) / 2.0), y: size.height), size: buttonSize)
if let buttonView = self.button.view {
if buttonView.superview == nil {
self.addSubview(buttonView)
}
buttonView.frame = buttonFrame
}
size.height += buttonSize.height
size.height += 16.0
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,354 @@
import Foundation
import UIKit
import SwiftSignalKit
import AsyncDisplayKit
import Display
import TelegramCore
import TelegramPresentationData
import AccountContext
import UrlEscaping
import ComponentFlow
import StarsWithdrawalScreen
private final class GiftAuctionCustomBidAlertContentNode: AlertContentNode {
private let theme: PresentationTheme
private let strings: PresentationStrings
private let dateTimeFormat: PresentationDateTimeFormat
private let title: String
private let text: String
private let placeholder: String
private let minValue: Int64
fileprivate var value: Int64
private let titleNode: ASTextNode
private let textNode: ASTextNode
private let backgroundView = UIImageView()
let amountField = ComponentView<Empty>()
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private let disposable = MetaDisposable()
private var validLayout: CGSize?
private let hapticFeedback = HapticFeedback()
var complete: (() -> Void)? {
didSet {
//self.inputFieldNode.complete = self.complete
}
}
override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, actions: [TextAlertAction], title: String, text: String, placeholder: String, minValue: Int64, value: Int64) {
self.theme = ptheme
self.strings = strings
self.dateTimeFormat = dateTimeFormat
self.title = title
self.text = text
self.placeholder = placeholder
self.minValue = minValue
self.value = value
self.titleNode = ASTextNode()
self.titleNode.maximumNumberOfLines = 2
self.textNode = ASTextNode()
self.textNode.maximumNumberOfLines = 8
// self.inputFieldNode = GiftAuctionCustomBidInputFieldNode(theme: ptheme, placeholder: placeholder)
// self.inputFieldNode.text = "\(value)"
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
// self.addSubnode(self.inputFieldNode)
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.updateTheme(theme)
}
deinit {
self.disposable.dispose()
}
override func updateTheme(_ theme: AlertControllerTheme) {
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center)
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width, 270.0)
let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)
let hadValidLayout = self.validLayout != nil
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
let spacing: CGFloat = 5.0
let titleSize = self.titleNode.measure(measureSize)
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
origin.y += titleSize.height + 4.0
let textSize = self.textNode.measure(measureSize)
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
origin.y += textSize.height + 6.0 + spacing
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
effectiveActionLayout = .vertical
}
switch effectiveActionLayout {
case .horizontal:
minActionsWidth += actionTitleSize.width + actionTitleInsets
case .vertical:
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0)
var contentWidth = max(titleSize.width, minActionsWidth)
contentWidth = max(contentWidth, 234.0)
var actionsHeight: CGFloat = 0.0
switch effectiveActionLayout {
case .horizontal:
actionsHeight = actionButtonHeight
case .vertical:
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
}
let resultWidth = contentWidth + insets.left + insets.right
let fieldWidth = resultWidth - 18.0
let amountFieldSize = self.amountField.update(
transition: .immediate,
component: AnyComponent(
AmountFieldComponent(
textColor: self.theme.list.itemPrimaryTextColor,
secondaryColor: self.theme.list.itemSecondaryTextColor,
placeholderColor: self.theme.list.itemPlaceholderTextColor,
accentColor: self.theme.list.itemAccentColor,
value: self.value,
minValue: self.minValue,
forceMinValue: false,
allowZero: false,
maxValue: nil,
placeholderText: self.placeholder,
textFieldOffset: CGPoint(x: -4.0, y: -1.0),
labelText: nil,
currency: .stars,
dateTimeFormat: self.dateTimeFormat,
amountUpdated: { [weak self] value in
guard let self else {
return
}
if let value {
self.value = value
}
},
tag: nil
)
),
environment: {},
containerSize: CGSize(width: fieldWidth, height: 44.0)
)
var amountFieldFrame = CGRect(origin: CGPoint(x: floor((resultWidth - fieldWidth) / 2.0), y: origin.y - 2.0), size: amountFieldSize)
if let amountFieldView = self.amountField.view {
if amountFieldView.superview == nil {
amountFieldView.clipsToBounds = true
self.backgroundView.image = generateStretchableFilledCircleImage(diameter: 12.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0)
self.view.addSubview(self.backgroundView)
self.view.addSubview(amountFieldView)
}
self.backgroundView.frame = amountFieldFrame.insetBy(dx: 7.0, dy: 9.0)
amountFieldFrame.size.width -= 14.0
amountFieldView.frame = amountFieldFrame
}
let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + amountFieldSize.height + actionsHeight + insets.top + insets.bottom + 3.0)
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
var actionOffset: CGFloat = 0.0
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
switch effectiveActionLayout {
case .horizontal:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
case .vertical:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
switch effectiveActionLayout {
case .horizontal:
if nodeIndex == self.actionNodes.count - 1 {
currentActionWidth = resultSize.width - actionOffset
} else {
currentActionWidth = actionWidth
}
case .vertical:
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
switch effectiveActionLayout {
case .horizontal:
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += currentActionWidth
case .vertical:
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
if !hadValidLayout {
if let amountFieldView = self.amountField.view as? AmountFieldComponent.View {
amountFieldView.activateInput()
Queue.mainQueue().justDispatch {
amountFieldView.selectAll()
}
}
}
return resultSize
}
func animateError() {
if let amountFieldView = self.amountField.view as? AmountFieldComponent.View {
self.value = self.minValue
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
amountFieldView.resetValue()
amountFieldView.animateError()
amountFieldView.selectAll()
}
}
func deactivateInput() {
if let amountFieldView = self.amountField.view as? AmountFieldComponent.View {
amountFieldView.deactivateInput()
}
}
}
func giftAuctionCustomBidController(context: AccountContext, title: String, text: String, placeholder: String, action: String, minValue: Int64, value: Int64, apply: @escaping (Int64) -> Void, cancel: @escaping () -> Void) -> AlertController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var dismissImpl: ((Bool) -> Void)?
var applyImpl: (() -> Void)?
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?(true)
cancel()
}), TextAlertAction(type: .defaultAction, title: action, action: {
applyImpl?()
})]
let contentNode = GiftAuctionCustomBidAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, actions: actions, title: title, text: text, placeholder: placeholder, minValue: minValue, value: value)
contentNode.complete = {
applyImpl?()
}
applyImpl = { [weak contentNode] in
guard let contentNode = contentNode else {
return
}
let value = contentNode.value
if value < minValue {
contentNode.animateError()
} else {
dismissImpl?(true)
apply(contentNode.value)
}
}
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
let presentationDataDisposable = context.sharedContext.presentationData.start(next: { [weak controller] presentationData in
controller?.theme = AlertControllerTheme(presentationData: presentationData)
})
controller.dismissed = { byTapOutside in
presentationDataDisposable.dispose()
if byTapOutside {
cancel()
}
}
dismissImpl = { [weak controller, weak contentNode] animated in
contentNode?.deactivateInput()
let _ = contentNode
if animated {
controller?.dismissAnimated()
} else {
controller?.dismiss()
}
}
return controller
}
@@ -0,0 +1,648 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import PresentationDataUtils
import ComponentFlow
import ViewControllerComponent
import SheetComponent
import MultilineTextComponent
import BalancedTextComponent
import BundleIconComponent
import Markdown
import TextFormat
import TelegramStringFormatting
import GlassBarButtonComponent
import ButtonComponent
import LottieComponent
private final class GiftAuctionInfoSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let gift: StarGift
let auctionContext: GiftAuctionContext
let animateOut: ActionSlot<Action<()>>
let getController: () -> ViewController?
init(
context: AccountContext,
gift: StarGift,
auctionContext: GiftAuctionContext,
animateOut: ActionSlot<Action<()>>,
getController: @escaping () -> ViewController?
) {
self.context = context
self.gift = gift
self.auctionContext = auctionContext
self.animateOut = animateOut
self.getController = getController
}
static func ==(lhs: GiftAuctionInfoSheetContent, rhs: GiftAuctionInfoSheetContent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.gift != rhs.gift {
return false
}
return true
}
final class State: ComponentState {
private let context: AccountContext
private let auctionContext: GiftAuctionContext
private let animateOut: ActionSlot<Action<()>>
private let getController: () -> ViewController?
private(set) var rounds: Int32 = 50
fileprivate let playButtonAnimation = ActionSlot<Void>()
private var didPlayAnimation = false
init(
context: AccountContext,
auctionContext: GiftAuctionContext,
animateOut: ActionSlot<Action<()>>,
getController: @escaping () -> ViewController?
) {
self.context = context
self.auctionContext = auctionContext
self.animateOut = animateOut
self.getController = getController
super.init()
let _ = (self.auctionContext.state
|> deliverOnMainQueue).startStandalone(next: { [weak self] state in
if let self, case let .ongoing(_, _, _, _, _, _, _, _, _, totalRounds, _, _) = state?.auctionState {
self.rounds = totalRounds
self.updated()
}
})
}
func playAnimationIfNeeded() {
if !self.didPlayAnimation {
self.didPlayAnimation = true
self.playButtonAnimation.invoke(Void())
}
}
func dismiss(animated: Bool) {
guard let controller = self.getController() as? GiftAuctionInfoScreen else {
return
}
if animated {
self.animateOut.invoke(Action { [weak controller] _ in
controller?.dismiss(completion: nil)
})
} else {
controller.dismiss(animated: false)
}
}
}
func makeState() -> State {
return State(context: self.context, auctionContext: self.auctionContext, animateOut: self.animateOut, getController: self.getController)
}
static var body: Body {
let closeButton = Child(GlassBarButtonComponent.self)
let icon = Child(BundleIconComponent.self)
let title = Child(BalancedTextComponent.self)
let text = Child(BalancedTextComponent.self)
let list = Child(List<Empty>.self)
let button = Child(ButtonComponent.self)
return { context in
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let state = context.state
let controller = environment.controller
let theme = environment.theme
let strings = environment.strings
let sideInset: CGFloat = 30.0 + environment.safeInsets.left
let textSideInset: CGFloat = 30.0 + environment.safeInsets.left
let titleFont = Font.bold(24.0)
let textFont = Font.regular(15.0)
let textColor = theme.actionSheet.primaryTextColor
let secondaryTextColor = theme.actionSheet.secondaryTextColor
let linkColor = theme.actionSheet.controlAccentColor
let spacing: CGFloat = 16.0
var contentSize = CGSize(width: context.availableSize.width, height: 33.0)
var auctionGiftsPerRound: Int32 = 50
if case let .generic(gift) = context.component.gift, let auctionGiftsPerRoundValue = gift.auctionGiftsPerRound {
auctionGiftsPerRound = auctionGiftsPerRoundValue
}
let icon = icon.update(
component: BundleIconComponent(
name: "Premium/Auction/BidLarge",
tintColor: linkColor
),
availableSize: context.availableSize,
transition: context.transition
)
context.add(icon
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + icon.size.height / 2.0))
)
contentSize.height += icon.size.height
contentSize.height += 8.0
let title = title.update(
component: BalancedTextComponent(
text: .plain(NSAttributedString(string: strings.Gift_Auction_Info_Title, font: titleFont, textColor: textColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.1
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, 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 += spacing - 8.0
let text = text.update(
component: BalancedTextComponent(
text: .plain(NSAttributedString(string: strings.Gift_Auction_Info_Description, font: textFont, textColor: secondaryTextColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, 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 += spacing + 9.0
var items: [AnyComponentWithIdentity<Empty>] = []
items.append(
AnyComponentWithIdentity(
id: "top",
component: AnyComponent(ParagraphComponent(
title: strings.Gift_Auction_Info_TopBidders_Title(auctionGiftsPerRound),
titleColor: textColor,
text: strings.Gift_Auction_Info_TopBidders_Text(strings.Gift_Auction_Info_TopBidders_Gifts(auctionGiftsPerRound), strings.Gift_Auction_Info_TopBidders_Rounds(state.rounds), strings.Gift_Auction_Info_TopBidders_Bidders(auctionGiftsPerRound)).string,
textColor: secondaryTextColor,
accentColor: linkColor,
iconName: "Premium/Auction/Drop",
iconColor: linkColor
))
)
)
items.append(
AnyComponentWithIdentity(
id: "carryover",
component: AnyComponent(ParagraphComponent(
title: strings.Gift_Auction_Info_Carryover_Title,
titleColor: textColor,
text: strings.Gift_Auction_Info_Carryover_Text("\(auctionGiftsPerRound)").string,
textColor: secondaryTextColor,
accentColor: linkColor,
iconName: "Premium/Auction/NextDrop",
iconColor: linkColor
))
)
)
items.append(
AnyComponentWithIdentity(
id: "missed",
component: AnyComponent(ParagraphComponent(
title: strings.Gift_Auction_Info_Missed_Title,
titleColor: textColor,
text: strings.Gift_Auction_Info_Missed_Text,
textColor: secondaryTextColor,
accentColor: linkColor,
iconName: "Premium/Auction/Refund",
iconColor: linkColor
))
)
)
let list = list.update(
component: List(items),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 10000.0),
transition: context.transition
)
context.add(list
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + list.size.height / 2.0))
)
contentSize.height += list.size.height
contentSize.height += spacing + 8.0
let closeButton = closeButton.update(
component: GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0),
backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor,
isDark: theme.overallDarkAppearance,
state: .generic,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: theme.rootController.navigationBar.glassBarButtonForegroundColor
)
)),
action: { [weak state] _ in
guard let state else {
return
}
state.dismiss(animated: true)
}
),
availableSize: CGSize(width: 40.0, height: 40.0),
transition: .immediate
)
context.add(closeButton
.position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0))
)
var buttonTitle: [AnyComponentWithIdentity<Empty>] = []
buttonTitle.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "anim_ok"),
color: theme.list.itemCheckColors.foregroundColor,
startingPosition: .begin,
size: CGSize(width: 28.0, height: 28.0),
playOnce: state.playButtonAnimation
))))
buttonTitle.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(ButtonTextContentComponent(
text: strings.Gift_Auction_Info_Understood,
badge: 0,
textColor: theme.list.itemCheckColors.foregroundColor,
badgeBackground: theme.list.itemCheckColors.foregroundColor,
badgeForeground: theme.list.itemCheckColors.fillColor
))))
let button = button.update(
component: ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
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(HStack(buttonTitle, spacing: 2.0))
),
isEnabled: true,
displaysProgress: false,
action: { [weak state] in
guard let state else {
return
}
state.dismiss(animated: true)
if let controller = controller() as? GiftAuctionInfoScreen {
controller.completion?()
}
}
),
availableSize: CGSize(width: context.availableSize.width - 30.0 * 2.0, height: 52.0),
transition: .immediate
)
context.add(button
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0))
)
contentSize.height += button.size.height
contentSize.height += 30.0
state.playAnimationIfNeeded()
return contentSize
}
}
}
final class GiftAuctionInfoSheetComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let gift: StarGift
let auctionContext: GiftAuctionContext
init(
context: AccountContext,
gift: StarGift,
auctionContext: GiftAuctionContext
) {
self.context = context
self.gift = gift
self.auctionContext = auctionContext
}
static func ==(lhs: GiftAuctionInfoSheetComponent, rhs: GiftAuctionInfoSheetComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.gift != rhs.gift {
return false
}
return true
}
static var body: Body {
let sheet = Child(SheetComponent<EnvironmentType>.self)
let animateOut = StoredActionSlot(Action<Void>.self)
let sheetExternalState = SheetComponent<EnvironmentType>.ExternalState()
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(GiftAuctionInfoSheetContent(
context: context.component.context,
gift: context.component.gift,
auctionContext: context.component.auctionContext,
animateOut: animateOut,
getController: controller
)),
style: .glass,
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
followContentSizeChanges: true,
clipsContent: true,
autoAnimateOut: false,
externalState: sheetExternalState,
animateOut: animateOut,
onPan: {
},
willDismiss: {
}
),
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 {
if let controller = controller() as? GiftAuctionInfoScreen {
animateOut.invoke(Action { _ in
controller.dismiss(completion: nil)
})
}
} else {
if let controller = controller() as? GiftAuctionInfoScreen {
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))
)
if let controller = controller(), !controller.automaticallyControlPresentationContextLayout {
var sideInset: CGFloat = 0.0
var bottomInset: CGFloat = max(environment.safeInsets.bottom, sheetExternalState.contentHeight)
if case .regular = environment.metrics.widthClass {
sideInset = floor((context.availableSize.width - 430.0) / 2.0) - 12.0
bottomInset = (context.availableSize.height - sheetExternalState.contentHeight) / 2.0 + sheetExternalState.contentHeight
}
let layout = ContainerViewLayout(
size: context.availableSize,
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0),
safeInsets: UIEdgeInsets(top: 0.0, left: max(sideInset, environment.safeInsets.left), bottom: 0.0, right: max(sideInset, environment.safeInsets.right)),
additionalInsets: .zero,
statusBarHeight: environment.statusBarHeight,
inputHeight: nil,
inputHeightIsInteractivellyChanging: false,
inVoiceOver: false
)
controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition)
}
return context.availableSize
}
}
}
public final class GiftAuctionInfoScreen: ViewControllerComponentContainer {
private let context: AccountContext
fileprivate let completion: (() -> Void)?
public init(
context: AccountContext,
auctionContext: GiftAuctionContext,
completion: (() -> Void)?
) {
self.context = context
self.completion = completion
super.init(
context: context,
component: GiftAuctionInfoSheetComponent(
context: context,
gift: auctionContext.gift,
auctionContext: auctionContext
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
theme: .default
)
self.navigationPresentation = .flatModal
self.automaticallyControlPresentationContextLayout = false
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
public override func viewDidLoad() {
super.viewDidLoad()
self.view.disablesInteractiveModalDismiss = true
}
public override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
}
public func dismissAnimated() {
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
}
private final class ParagraphComponent: CombinedComponent {
let title: String
let titleColor: UIColor
let text: String
let textColor: UIColor
let accentColor: UIColor
let iconName: String
let iconColor: UIColor
let action: () -> Void
public init(
title: String,
titleColor: UIColor,
text: String,
textColor: UIColor,
accentColor: UIColor,
iconName: String,
iconColor: UIColor,
action: @escaping () -> Void = {}
) {
self.title = title
self.titleColor = titleColor
self.text = text
self.textColor = textColor
self.accentColor = accentColor
self.iconName = iconName
self.iconColor = iconColor
self.action = action
}
static func ==(lhs: ParagraphComponent, rhs: ParagraphComponent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.titleColor != rhs.titleColor {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.textColor != rhs.textColor {
return false
}
if lhs.accentColor != rhs.accentColor {
return false
}
if lhs.iconName != rhs.iconName {
return false
}
if lhs.iconColor != rhs.iconColor {
return false
}
return true
}
static var body: Body {
let title = Child(MultilineTextComponent.self)
let text = Child(MultilineTextComponent.self)
let icon = Child(BundleIconComponent.self)
return { context in
let component = context.component
let leftInset: CGFloat = 32.0
let rightInset: CGFloat = 24.0
let textSideInset: CGFloat = leftInset + 8.0
let spacing: CGFloat = 5.0
let textTopInset: CGFloat = 9.0
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.title,
font: Font.semibold(15.0),
textColor: component.titleColor,
paragraphAlignment: .natural
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let textColor = component.textColor
let accentColor = component.accentColor
let markdownAttributes = MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
link: MarkdownAttributeSet(font: textFont, textColor: accentColor),
linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
}
)
let text = text.update(
component: MultilineTextComponent(
text: .markdown(text: component.text, attributes: markdownAttributes),
horizontalAlignment: .natural,
maximumNumberOfLines: 0,
lineSpacing: 0.2,
highlightColor: accentColor.withAlphaComponent(0.1),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { _, _ in
component.action()
}
),
availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: context.availableSize.height),
transition: .immediate
)
let icon = icon.update(
component: BundleIconComponent(
name: component.iconName,
tintColor: component.iconColor
),
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height),
transition: .immediate
)
context.add(title
.position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0))
)
context.add(text
.position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0))
)
context.add(icon
.position(CGPoint(x: 15.0, y: textTopInset + 18.0))
)
return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 20.0)
}
}
}
@@ -0,0 +1,785 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import PresentationDataUtils
import ComponentFlow
import ViewControllerComponent
import SheetComponent
import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import BundleIconComponent
import ButtonComponent
import Markdown
import BalancedTextComponent
import AvatarNode
import TextFormat
import TelegramStringFormatting
import PlainButtonComponent
import GiftItemComponent
import GiftAnimationComponent
import GlassBarButtonComponent
import GiftRemainingCountComponent
import AnimatedTextComponent
private final class GiftAuctionWearPreviewSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let auctionContext: GiftAuctionContext
let attributes: [StarGift.UniqueGift.Attribute]
let animateOut: ActionSlot<Action<()>>
let getController: () -> ViewController?
init(
context: AccountContext,
auctionContext: GiftAuctionContext,
attributes: [StarGift.UniqueGift.Attribute],
animateOut: ActionSlot<Action<()>>,
getController: @escaping () -> ViewController?
) {
self.context = context
self.auctionContext = auctionContext
self.attributes = attributes
self.animateOut = animateOut
self.getController = getController
}
static func ==(lhs: GiftAuctionWearPreviewSheetContent, rhs: GiftAuctionWearPreviewSheetContent) -> Bool {
if lhs.context !== rhs.context {
return false
}
return true
}
final class State: ComponentState {
private let context: AccountContext
private var peerDisposable: Disposable?
var peerMap: [EnginePeer.Id: EnginePeer] = [:]
var cachedSmallChevronImage: (UIImage, PresentationTheme)?
private var disposable: Disposable?
private(set) var giftAuctionState: GiftAuctionContext.State?
private var giftAuctionTimer: SwiftSignalKit.Timer?
private var previewTimer: SwiftSignalKit.Timer?
private(set) var previewModelIndex: Int = 0
private(set) var previewBackdropIndex: Int = 0
private(set) var previewSymbolIndex: Int = 0
private(set) var previewModels: [StarGift.UniqueGift.Attribute] = []
private(set) var previewBackdrops: [StarGift.UniqueGift.Attribute] = []
private(set) var previewSymbols: [StarGift.UniqueGift.Attribute] = []
init(
context: AccountContext,
auctionContext: GiftAuctionContext,
attributes: [StarGift.UniqueGift.Attribute]
) {
self.context = context
super.init()
for attribute in attributes {
switch attribute {
case .model:
self.previewModels.append(attribute)
case .backdrop:
self.previewBackdrops.append(attribute)
case .pattern:
self.previewSymbols.append(attribute)
default:
break
}
}
let peerIds: [EnginePeer.Id] = [context.account.peerId]
self.peerDisposable = (
context.engine.data.get(EngineDataMap(
peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in
return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
}
))
|> deliverOnMainQueue
).startStrict(next: { [weak self] peers in
if let strongSelf = self {
var peersMap: [EnginePeer.Id: EnginePeer] = [:]
for (peerId, maybePeer) in peers {
if let peer = maybePeer {
peersMap[peerId] = peer
}
}
strongSelf.peerMap = peersMap
strongSelf.updated(transition: .immediate)
}
})
self.previewTimer = SwiftSignalKit.Timer(timeout: 3.0, repeat: true, completion: { [weak self] in
guard let self else {
return
}
self.previewTimerTick()
}, queue: Queue.mainQueue())
self.previewTimer?.start()
self.disposable = (auctionContext.state
|> deliverOnMainQueue).start(next: { [weak self] auctionState in
guard let self else {
return
}
self.giftAuctionState = auctionState
self.updated()
})
self.giftAuctionTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
self?.updated()
}, queue: Queue.mainQueue())
self.giftAuctionTimer?.start()
}
deinit {
self.disposable?.dispose()
self.giftAuctionTimer?.invalidate()
self.peerDisposable?.dispose()
self.previewTimer?.invalidate()
}
private func previewTimerTick() {
guard !self.previewModels.isEmpty else { return }
self.previewModelIndex = (self.previewModelIndex + 1) % self.previewModels.count
let previousSymbolIndex = self.previewSymbolIndex
var randomSymbolIndex = previousSymbolIndex
while randomSymbolIndex == previousSymbolIndex && !self.previewSymbols.isEmpty {
randomSymbolIndex = Int.random(in: 0 ..< self.previewSymbols.count)
}
if !self.previewSymbols.isEmpty { self.previewSymbolIndex = randomSymbolIndex }
let previousBackdropIndex = self.previewBackdropIndex
var randomBackdropIndex = previousBackdropIndex
while randomBackdropIndex == previousBackdropIndex && !self.previewBackdrops.isEmpty {
randomBackdropIndex = Int.random(in: 0 ..< self.previewBackdrops.count)
}
if !self.previewBackdrops.isEmpty { self.previewBackdropIndex = randomBackdropIndex }
self.updated(transition: .easeInOut(duration: 0.25))
}
}
func makeState() -> State {
return State(context: self.context, auctionContext: self.auctionContext, attributes: self.attributes)
}
static var body: Body {
let closeButton = Child(GlassBarButtonComponent.self)
let animation = Child(GiftCompositionComponent.self)
let avatar = Child(AvatarComponent.self)
let peerName = Child(MultilineTextComponent.self)
let learnMore = Child(PlainButtonComponent.self)
let initialGift = Child(GiftItemComponent.self)
let upgradedGift = Child(GiftItemComponent.self)
let arrow = Child(BundleIconComponent.self)
let upgradeLabel = Child(MultilineTextComponent.self)
let remainingCount = Child(GiftRemainingCountComponent.self)
let auctionFooter = Child(MultilineTextComponent.self)
let button = Child(ButtonComponent.self)
let giftCompositionExternalState = GiftCompositionComponent.ExternalState()
return { context in
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let component = context.component
let theme = environment.theme
let strings = environment.strings
let nameDisplayOrder = component.context.sharedContext.currentPresentationData.with { $0 }.nameDisplayOrder
let controller = environment.controller
let state = context.state
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
var contentHeight: CGFloat = 0.0
let headerHeight: CGFloat = 226.0
var peerNameString = ""
if let peer = state.peerMap[component.context.account.peerId] {
peerNameString = peer.displayTitle(strings: strings, displayOrder: nameDisplayOrder)
}
let peerName = peerName.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: peerNameString,
font: Font.bold(20.0),
textColor: .white,
paragraphAlignment: .center
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let animationOffset: CGPoint? = CGPoint(x: peerName.size.width / 2.0 + 20.0 - 12.0, y: 79.0)
let animationScale: CGFloat = 0.19
var attributes: [StarGift.UniqueGift.Attribute] = []
if !state.previewModels.isEmpty {
attributes.append(state.previewModels[state.previewModelIndex])
if !state.previewBackdrops.isEmpty {
attributes.append(state.previewBackdrops[state.previewBackdropIndex])
}
if !state.previewSymbols.isEmpty {
attributes.append(state.previewSymbols[state.previewSymbolIndex])
}
}
let animation = animation.update(
component: GiftCompositionComponent(
context: component.context,
theme: environment.theme,
subject: .preview(attributes),
animationOffset: animationOffset,
animationScale: animationScale,
displayAnimationStars: true,
animateScaleOnTransition: false,
externalState: giftCompositionExternalState,
requestUpdate: { [weak state] transition in
state?.updated(transition: transition)
}
),
availableSize: CGSize(width: context.availableSize.width, height: headerHeight),
transition: context.transition
)
context.add(animation
.position(CGPoint(x: context.availableSize.width / 2.0, y: headerHeight / 2.0))
)
contentHeight += headerHeight
if let peer = state.peerMap[component.context.account.peerId] {
let avatar = avatar.update(
component: AvatarComponent(
context: component.context,
theme: theme,
peer: peer
),
environment: {},
availableSize: CGSize(width: 100.0, height: 100.0),
transition: context.transition
)
context.add(avatar
.position(CGPoint(x: context.availableSize.width / 2.0, y: 86.0))
)
}
context.add(peerName
.position(CGPoint(x: context.availableSize.width / 2.0 - 12.0, y: 167.0))
)
var buttonColor: UIColor = .white.withAlphaComponent(0.1)
var secondaryTextColor: UIColor = .white.withAlphaComponent(0.4)
if let backdropAttribute = attributes.first(where: { attribute in
if case .backdrop = attribute {
return true
} else {
return false
}
}), case let .backdrop(_, _, innerColor, _, _, _, _) = backdropAttribute {
buttonColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultipliedBrightnessBy(1.05)
secondaryTextColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.3)
}
let closeButton = closeButton.update(
component: GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0),
backgroundColor: buttonColor,
isDark: false,
state: .tintedGlass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: .white
)
)),
action: { _ in
(controller() as? GiftAuctionWearPreviewScreen)?.dismissAnimated()
}
),
availableSize: CGSize(width: 40.0, height: 40.0),
transition: .immediate
)
context.add(closeButton
.position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0))
)
let textFont = Font.regular(13.0)
let boldTextFont = Font.semibold(13.0)
let textColor = UIColor.white
let linkColor = UIColor.white
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)
})
if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme {
state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, theme)
}
let learnMoreAttributedString = parseMarkdownIntoAttributedString(strings.Gift_WearPreview_LearnMore, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString
if let range = learnMoreAttributedString.string.range(of: ">"), let chevronImage = state.cachedSmallChevronImage?.0 {
learnMoreAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: learnMoreAttributedString.string))
}
let learnMore = learnMore.update(
component: PlainButtonComponent(
content: AnyComponent(BalancedTextComponent(
text: .plain(learnMoreAttributedString),
maximumNumberOfLines: 1,
tintColor: secondaryTextColor
)),
action: {
let controller = component.context.sharedContext.makeGiftWearPreviewScreen(context: component.context, gift: component.auctionContext.gift, attributes: component.attributes)
environment.controller()?.push(controller)
},
animateScale: false
),
availableSize: context.availableSize,
transition: context.transition
)
context.add(learnMore
.position(CGPoint(x: context.availableSize.width / 2.0, y: 198.0))
)
if case let .generic(gift) = component.auctionContext.gift {
let initialGift = initialGift.update(
component: GiftItemComponent(
context: component.context,
theme: theme,
strings: strings,
subject: .starGift(gift: gift, price: ""),
ribbon: GiftItemComponent.Ribbon(text: strings.Gift_WearPreview_Limited, color: .blue),
mode: .thumbnail
),
availableSize: CGSize(width: 120.0, height: 120.0),
transition: context.transition
)
context.add(initialGift
.position(CGPoint(x: sideInset + initialGift.size.width * 0.5, y: contentHeight + 76.0))
)
}
var ribbonColor: GiftItemComponent.Ribbon.Color = .blue
for attribute in attributes {
if case let .backdrop(_, _, innerColor, outerColor, _, _, _) = attribute {
ribbonColor = .custom(outerColor, innerColor)
break
}
}
let upgradedGift = upgradedGift.update(
component: GiftItemComponent(
context: component.context,
theme: theme,
strings: strings,
subject: .preview(attributes: attributes, rarity: 0),
ribbon: GiftItemComponent.Ribbon(text: strings.Gift_WearPreview_Upgraded, color: ribbonColor),
animateChanges: true,
mode: .thumbnail
),
availableSize: CGSize(width: 120.0, height: 120.0),
transition: context.transition
)
context.add(upgradedGift
.position(CGPoint(x: context.availableSize.width - sideInset - upgradedGift.size.width * 0.5, y: contentHeight + 76.0))
)
let arrow = arrow.update(
component: BundleIconComponent(name: "Premium/Auction/Upgrade", tintColor: theme.list.itemSecondaryTextColor),
availableSize: context.availableSize,
transition: context.transition
)
context.add(arrow
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + 56.0))
)
let upgradeLabel = upgradeLabel.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(string: strings.Gift_WearPreview_FreeUpgrade, font: Font.medium(13.0), textColor: theme.list.itemSecondaryTextColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 2,
lineSpacing: 0.1
),
availableSize: context.availableSize,
transition: context.transition
)
context.add(upgradeLabel
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + 94.0))
)
contentHeight += 137.0
if case let .generic(gift) = component.auctionContext.gift, let availability = gift.availability {
var remains: Int32 = availability.remains
if let auctionState = state.giftAuctionState {
switch auctionState.auctionState {
case let .ongoing(_, _, _, _, _, _, _, giftsLeft, _, _, _, _):
remains = giftsLeft
case .finished:
remains = 0
}
}
let total = availability.total
let position = CGFloat(remains) / CGFloat(total)
let sold = total - remains
let remainingCount = remainingCount.update(
component: GiftRemainingCountComponent(
inactiveColor: theme.list.itemBlocksBackgroundColor,
activeColors: [UIColor(rgb: 0x72d6ff), UIColor(rgb: 0x32a0f9)],
inactiveTitle: strings.Gift_Send_Remains(remains),
inactiveValue: "",
inactiveTitleColor: theme.list.itemSecondaryTextColor,
activeTitle: "",
activeValue: sold > 0 ? strings.Gift_Send_Sold(sold) : "",
activeTitleColor: .white,
badgeText: "",
badgePosition: position,
badgeGraphPosition: position,
invertProgress: true,
leftString: "",
groupingSeparator: environment.dateTimeFormat.groupingSeparator
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
transition: context.transition
)
context.add(remainingCount
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight))
)
if let giftsPerRound = gift.auctionGiftsPerRound {
let footerAttributes = MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.list.freeTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.list.freeTextColor),
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.list.itemAccentColor),
linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
}
)
let parsedString = parseMarkdownIntoAttributedString(strings.Gift_Setup_AuctionInfo(environment.strings.Gift_Setup_AuctionInfo_Gifts(giftsPerRound), strings.Gift_Setup_AuctionInfo_Bidders(giftsPerRound)).string, attributes: footerAttributes)
let auctionFooterText = NSMutableAttributedString(attributedString: parsedString)
if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme {
state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: theme.list.itemAccentColor)!, environment.theme)
}
if let range = auctionFooterText.string.range(of: ">"), let chevronImage = state.cachedSmallChevronImage?.0 {
auctionFooterText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: auctionFooterText.string))
}
let auctionFooter = auctionFooter.update(
component: MultilineTextComponent(
text: .plain(auctionFooterText),
maximumNumberOfLines: 0,
highlightColor: theme.list.itemAccentColor.withAlphaComponent(0.1),
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { _, _ in
guard let controller = controller() else {
return
}
let infoController = component.context.sharedContext.makeGiftAuctionInfoScreen(context: component.context, auctionContext: component.auctionContext, completion: nil)
controller.push(infoController)
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 16.0 * 2.0, height: 10000.0),
transition: context.transition
)
context.add(auctionFooter
.position(CGPoint(x: sideInset + 16.0 + auctionFooter.size.width * 0.5, y: contentHeight + 52.0 + auctionFooter.size.height * 0.5))
)
contentHeight += auctionFooter.size.height
}
}
contentHeight += 80.0
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
var startTime = currentTime
var endTime = currentTime
var isUpcoming = false
if let auctionState = state.giftAuctionState {
startTime = auctionState.startDate
endTime = auctionState.endDate
}
var buttonTitle = strings.Gift_Auction_Join
let endTimeout: Int32
if currentTime < startTime {
isUpcoming = true
endTimeout = max(0, startTime - currentTime)
} else {
endTimeout = max(0, endTime - currentTime)
}
let hours = Int(endTimeout / 3600)
let minutes = Int((endTimeout % 3600) / 60)
let seconds = Int(endTimeout % 60)
let rawString: String
if isUpcoming {
buttonTitle = strings.Gift_Auction_EarlyBid
rawString = hours > 0 ? strings.Gift_Auction_StartsInHours : strings.Gift_Auction_StartsInMinutes
} else {
rawString = hours > 0 ? strings.Gift_Auction_TimeLeftHours : strings.Gift_Auction_TimeLeftMinutes
}
var buttonAnimatedTitleItems: [AnimatedTextComponent.Item] = []
var startIndex = rawString.startIndex
while true {
if let range = rawString.range(of: "{", range: startIndex ..< rawString.endIndex) {
if range.lowerBound != startIndex {
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "prefix_\(buttonAnimatedTitleItems.count)", content: .text(String(rawString[startIndex ..< range.lowerBound]))))
}
startIndex = range.upperBound
if let endRange = rawString.range(of: "}", range: startIndex ..< rawString.endIndex) {
let controlString = rawString[range.upperBound ..< endRange.lowerBound]
if controlString == "h" {
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "h", content: .number(hours, minDigits: 2)))
} else if controlString == "m" {
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "m", content: .number(minutes, minDigits: 2)))
} else if controlString == "s" {
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "s", content: .number(seconds, minDigits: 2)))
}
startIndex = endRange.upperBound
}
} else {
break
}
}
if startIndex != rawString.endIndex {
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "suffix_\(buttonAnimatedTitleItems.count)", content: .text(String(rawString[startIndex ..< rawString.endIndex]))))
}
let buttonAttributedString = NSMutableAttributedString(string: buttonTitle, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
let items: [AnyComponentWithIdentity<Empty>] = [
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(AnimatedTextComponent(
font: Font.with(size: 12.0, weight: .medium, traits: .monospacedNumbers),
color: theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7),
items: buttonAnimatedTitleItems,
noDelay: true
)))
]
let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0)
let buttonSize = CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0)
let buttonBackground = ButtonComponent.Background(
style: .glass,
color: theme.list.itemCheckColors.fillColor,
foreground: theme.list.itemCheckColors.foregroundColor,
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
)
let button = button.update(
component: ButtonComponent(
background: buttonBackground,
content: AnyComponentWithIdentity(
id: AnyHashable("bid"),
component: AnyComponent(VStack(items, spacing: 1.0))
),
isEnabled: true,
displaysProgress: false,
action: {
if let controller = controller() as? GiftAuctionWearPreviewScreen {
controller.completion()
controller.dismissAnimated()
}
}),
availableSize: buttonSize,
transition: .spring(duration: 0.2)
)
let buttonFrame = CGRect(origin: CGPoint(x: buttonInsets.left, y: contentHeight), size: button.size)
context.add(button
.position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY))
)
contentHeight += button.size.height
contentHeight += 7.0
let effectiveBottomInset: CGFloat = environment.metrics.isTablet ? 0.0 : environment.safeInsets.bottom
return CGSize(width: context.availableSize.width, height: contentHeight + 5.0 + effectiveBottomInset)
}
}
}
final class GiftAuctionWearPreviewSheetComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let auctionContext: GiftAuctionContext
let attributes: [StarGift.UniqueGift.Attribute]
init(
context: AccountContext,
auctionContext: GiftAuctionContext,
attributes: [StarGift.UniqueGift.Attribute]
) {
self.context = context
self.auctionContext = auctionContext
self.attributes = attributes
}
static func ==(lhs: GiftAuctionWearPreviewSheetComponent, rhs: GiftAuctionWearPreviewSheetComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
return true
}
static var body: Body {
let sheet = Child(SheetComponent<EnvironmentType>.self)
let animateOut = StoredActionSlot(Action<Void>.self)
let sheetExternalState = SheetComponent<EnvironmentType>.ExternalState()
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let theme = environment.theme.withModalBlocksBackground()
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(GiftAuctionWearPreviewSheetContent(
context: context.component.context,
auctionContext: context.component.auctionContext,
attributes: context.component.attributes,
animateOut: animateOut,
getController: controller
)),
style: .glass,
backgroundColor: .color(theme.list.blocksBackgroundColor),
followContentSizeChanges: true,
clipsContent: true,
hasDimView: false,
autoAnimateOut: false,
externalState: sheetExternalState,
animateOut: animateOut,
onPan: {
},
willDismiss: {
if let controller = controller() as? GiftAuctionWearPreviewScreen {
controller.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.3).withUserData(ViewControllerComponentContainer.AnimateOutTransition()))
}
}
),
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 {
if let controller = controller() as? GiftAuctionWearPreviewScreen {
controller.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.3).withUserData(ViewControllerComponentContainer.AnimateOutTransition()))
animateOut.invoke(Action { _ in
controller.dismiss(completion: nil)
})
}
} else {
if let controller = controller() as? GiftAuctionWearPreviewScreen {
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))
)
if let controller = controller(), !controller.automaticallyControlPresentationContextLayout {
var sideInset: CGFloat = 0.0
var bottomInset: CGFloat = max(environment.safeInsets.bottom, sheetExternalState.contentHeight)
if case .regular = environment.metrics.widthClass {
sideInset = floor((context.availableSize.width - 430.0) / 2.0) - 12.0
bottomInset = (context.availableSize.height - sheetExternalState.contentHeight) / 2.0 + sheetExternalState.contentHeight
}
let layout = ContainerViewLayout(
size: context.availableSize,
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0),
safeInsets: UIEdgeInsets(top: 0.0, left: max(sideInset, environment.safeInsets.left), bottom: 0.0, right: max(sideInset, environment.safeInsets.right)),
additionalInsets: .zero,
statusBarHeight: environment.statusBarHeight,
inputHeight: nil,
inputHeightIsInteractivellyChanging: false,
inVoiceOver: false
)
controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition)
}
return context.availableSize
}
}
}
public class GiftAuctionWearPreviewScreen: ViewControllerComponentContainer {
private let context: AccountContext
fileprivate let completion: () -> Void
public init(
context: AccountContext,
auctionContext: GiftAuctionContext,
attributes: [StarGift.UniqueGift.Attribute],
completion: @escaping () -> Void
) {
self.context = context
self.completion = completion
super.init(
context: context,
component: GiftAuctionWearPreviewSheetComponent(context: context, auctionContext: auctionContext, attributes: attributes),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
theme: .default
)
self.navigationPresentation = .flatModal
self.automaticallyControlPresentationContextLayout = false
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func viewDidLoad() {
super.viewDidLoad()
self.view.disablesInteractiveModalDismiss = true
}
public func dismissAnimated() {
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
}
@@ -0,0 +1,528 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import AppBundle
import AvatarNode
import Markdown
import GiftItemComponent
import ChatMessagePaymentAlertController
import ActivityIndicator
import TooltipUI
import MultilineTextComponent
import BalancedTextComponent
import TelegramStringFormatting
private final class GiftOfferAlertContentNode: AlertContentNode {
private let context: AccountContext
private let strings: PresentationStrings
private var presentationTheme: PresentationTheme
private let title: String
private let text: String
private let amount: CurrencyAmount
private let gift: StarGift.UniqueGift
private let titleNode: ASTextNode
private let giftView = ComponentView<Empty>()
private let textNode: ASTextNode
private let arrowNode: ASImageNode
private let avatarNode: AvatarNode
private let tableView = ComponentView<Empty>()
private let valueDelta = ComponentView<Empty>()
private let modelButtonTag = GenericComponentViewTag()
private let backdropButtonTag = GenericComponentViewTag()
private let symbolButtonTag = GenericComponentViewTag()
fileprivate var getController: () -> ViewController? = { return nil}
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private var activityIndicator: ActivityIndicator?
private var validLayout: CGSize?
var inProgress = false {
didSet {
if let size = self.validLayout {
let _ = self.updateLayout(size: size, transition: .immediate)
}
}
}
override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
init(
context: AccountContext,
theme: AlertControllerTheme,
ptheme: PresentationTheme,
strings: PresentationStrings,
gift: StarGift.UniqueGift,
peer: EnginePeer,
title: String,
text: String,
amount: CurrencyAmount,
actions: [TextAlertAction]
) {
self.context = context
self.strings = strings
self.presentationTheme = ptheme
self.title = title
self.text = text
self.amount = amount
self.gift = gift
self.titleNode = ASTextNode()
self.titleNode.maximumNumberOfLines = 0
self.textNode = ASTextNode()
self.textNode.maximumNumberOfLines = 0
self.arrowNode = ASImageNode()
self.arrowNode.displaysAsynchronously = false
self.arrowNode.displayWithoutProcessing = true
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.arrowNode)
self.addSubnode(self.avatarNode)
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.updateTheme(theme)
self.avatarNode.setPeer(context: context, theme: ptheme, peer: peer)
}
override func updateTheme(_ theme: AlertControllerTheme) {
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor)
self.textNode.attributedText = parseMarkdownIntoAttributedString(self.text, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor),
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor),
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor),
linkAttribute: { url in
return ("URL", url)
}
), textAlignment: .center)
self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/AlertArrow"), color: theme.secondaryColor.withAlphaComponent(0.9))
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
fileprivate func dismissAllTooltips() {
guard let controller = self.getController() else {
return
}
controller.window?.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss(inPlace: false)
}
})
controller.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss(inPlace: false)
}
return true
})
}
func showAttributeInfo(tag: Any, text: String) {
guard let controller = self.getController() else {
return
}
self.dismissAllTooltips()
guard let sourceView = self.tableView.findTaggedView(tag: tag), let absoluteLocation = sourceView.superview?.convert(sourceView.center, to: controller.view) else {
return
}
let location = CGRect(origin: CGPoint(x: absoluteLocation.x, y: absoluteLocation.y - 12.0), size: CGSize())
let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: text), style: .wide, location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in
return .dismiss(consume: false)
})
controller.present(tooltipController, in: .current)
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width, 310.0)
let strings = self.strings
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
let avatarSize = CGSize(width: 60.0, height: 60.0)
self.avatarNode.updateSize(size: avatarSize)
let giftFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) - 44.0, y: origin.y), size: avatarSize)
let _ = self.giftView.update(
transition: .immediate,
component: AnyComponent(
GiftItemComponent(
context: self.context,
theme: self.presentationTheme,
strings: strings,
peer: nil,
subject: .uniqueGift(gift: self.gift, price: nil),
mode: .thumbnail
)
),
environment: {},
containerSize: avatarSize
)
if let view = self.giftView.view {
if view.superview == nil {
self.view.addSubview(view)
}
view.frame = giftFrame
}
if let arrowImage = self.arrowNode.image {
let arrowFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - arrowImage.size.width) / 2.0), y: origin.y + floorToScreenPixels((avatarSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size)
transition.updateFrame(node: self.arrowNode, frame: arrowFrame)
}
let avatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) + 44.0, y: origin.y), size: avatarSize)
transition.updateFrame(node: self.avatarNode, frame: avatarFrame)
origin.y += avatarSize.height + 17.0
let titleSize = self.titleNode.measure(CGSize(width: size.width - 32.0, height: size.height))
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
origin.y += titleSize.height + 5.0
let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
origin.y += textSize.height + 10.0
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
let contentWidth = max(size.width, minActionsWidth)
let actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
let tableFont = Font.regular(15.0)
let tableTextColor = self.presentationTheme.list.itemPrimaryTextColor
var tableItems: [TableComponent.Item] = []
let order: [StarGift.UniqueGift.Attribute.AttributeType] = [
.model, .pattern, .backdrop, .originalInfo
]
var attributeMap: [StarGift.UniqueGift.Attribute.AttributeType: StarGift.UniqueGift.Attribute] = [:]
for attribute in self.gift.attributes {
attributeMap[attribute.attributeType] = attribute
}
for type in order {
if let attribute = attributeMap[type] {
let id: String?
let title: String?
let value: NSAttributedString
let percentage: Float?
let tag: AnyObject?
switch attribute {
case let .model(name, _, rarity):
id = "model"
title = strings.Gift_Unique_Model
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
percentage = Float(rarity) * 0.1
tag = self.modelButtonTag
case let .backdrop(name, _, _, _, _, _, rarity):
id = "backdrop"
title = strings.Gift_Unique_Backdrop
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
percentage = Float(rarity) * 0.1
tag = self.backdropButtonTag
case let .pattern(name, _, rarity):
id = "pattern"
title = strings.Gift_Unique_Symbol
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
percentage = Float(rarity) * 0.1
tag = self.symbolButtonTag
case .originalInfo:
continue
}
var items: [AnyComponentWithIdentity<Empty>] = []
items.append(
AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(
MultilineTextComponent(text: .plain(value))
)
)
)
if let percentage, let tag {
items.append(AnyComponentWithIdentity(
id: AnyHashable(1),
component: AnyComponent(Button(
content: AnyComponent(ButtonContentComponent(
context: self.context,
text: formatPercentage(percentage),
color: self.presentationTheme.list.itemAccentColor
)),
action: { [weak self] in
self?.showAttributeInfo(tag: tag, text: strings.Gift_Unique_AttributeDescription(formatPercentage(percentage)).string)
}
).tagged(tag))
))
}
let itemComponent = AnyComponent(
HStack(items, spacing: 4.0)
)
tableItems.append(.init(
id: id,
title: title,
hasBackground: false,
component: itemComponent
))
}
}
let tableSize = self.tableView.update(
transition: .immediate,
component: AnyComponent(
TableComponent(
theme: self.presentationTheme,
items: tableItems,
semiTransparent: true
)
),
environment: {},
containerSize: CGSize(width: contentWidth - 32.0, height: size.height)
)
let tableFrame = CGRect(origin: CGPoint(x: 16.0, y: avatarSize.height + titleSize.height + textSize.height + 60.0), size: tableSize)
if let view = self.tableView.view {
if view.superview == nil {
self.view.addSubview(view)
}
view.frame = tableFrame
}
var valueDeltaHeight: CGFloat = 0.0
if let valueAmount = self.gift.valueUsdAmount {
let resaleConfiguration = StarsSubscriptionConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 })
let usdRate: Double
switch self.amount.currency {
case .stars:
usdRate = Double(resaleConfiguration.usdWithdrawRate) / 1000.0 / 100.0
case .ton:
usdRate = Double(resaleConfiguration.tonUsdRate) / 1000.0 / 1000000.0
}
let offerUsdValue = Double(self.amount.amount.value) * usdRate
let giftUsdValue = Double(valueAmount) / 100.0
let fraction = giftUsdValue / offerUsdValue
let percentage = Int(fraction * 100) - 100
if percentage > 20 {
let textColor = self.presentationTheme.list.itemDestructiveColor
let markdownAttributes = MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: textColor),
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: textColor),
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: textColor),
linkAttribute: { url in
return ("URL", url)
}
)
let valueDeltaSize = self.valueDelta.update(
transition: .immediate,
component: AnyComponent(
BalancedTextComponent(
text: .markdown(text: strings.Chat_GiftPurchaseOffer_AcceptConfirmation_BadValue("\(percentage)%").string, attributes: markdownAttributes),
horizontalAlignment: .center,
maximumNumberOfLines: 0
)
),
environment: {},
containerSize: CGSize(width: contentWidth - 32.0, height: size.height)
)
let valueDeltaFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - valueDeltaSize.width) / 2.0), y: avatarSize.height + titleSize.height + textSize.height + 73.0 + tableSize.height), size: valueDeltaSize)
if let view = self.valueDelta.view {
if view.superview == nil {
self.view.addSubview(view)
}
view.frame = valueDeltaFrame
}
valueDeltaHeight += valueDeltaSize.height + 10.0
}
}
let resultSize = CGSize(width: contentWidth, height: avatarSize.height + titleSize.height + textSize.height + tableSize.height + actionsHeight + valueDeltaHeight + 40.0 + insets.top + insets.bottom)
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
var actionOffset: CGFloat = 0.0
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
do {
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
do {
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
do {
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
if self.inProgress {
let activityIndicator: ActivityIndicator
if let current = self.activityIndicator {
activityIndicator = current
} else {
activityIndicator = ActivityIndicator(type: .custom(self.presentationTheme.list.freeInputField.controlColor, 18.0, 1.5, false))
self.addSubnode(activityIndicator)
}
if let actionNode = self.actionNodes.first {
actionNode.isUserInteractionEnabled = false
actionNode.isHidden = false
let indicatorSize = CGSize(width: 22.0, height: 22.0)
transition.updateFrame(node: activityIndicator, frame: CGRect(origin: CGPoint(x: actionNode.frame.minX + floor((actionNode.frame.width - indicatorSize.width) / 2.0), y: actionNode.frame.minY + floor((actionNode.frame.height - indicatorSize.height) / 2.0)), size: indicatorSize))
}
}
return resultSize
}
}
public func giftOfferAlertController(
context: AccountContext,
gift: StarGift.UniqueGift,
peer: EnginePeer,
amount: CurrencyAmount,
commit: @escaping () -> Void
) -> AlertController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
let title = strings.Chat_GiftPurchaseOffer_AcceptConfirmation_Title
let buttonText: String = strings.Chat_GiftPurchaseOffer_AcceptConfirmation_Confirm
let priceString: String
switch amount.currency {
case .stars:
priceString = strings.Chat_GiftPurchaseOffer_AcceptConfirmation_Text_Stars(Int32(clamping: amount.amount.value))
case .ton:
priceString = "\(amount.amount) TON"
}
let resaleConfiguration = StarsSubscriptionConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let finalPriceString: String
switch amount.currency {
case .stars:
let starsValue = Int32(floor(Float(amount.amount.value) * Float(resaleConfiguration.starGiftCommissionStarsPermille) / 1000.0))
finalPriceString = strings.Chat_GiftPurchaseOffer_AcceptConfirmation_Text_Stars(starsValue)
case .ton:
let tonValue = Int64(Float(amount.amount.value) * Float(resaleConfiguration.starGiftCommissionTonPermille) / 1000.0)
finalPriceString = formatTonAmountText(tonValue, dateTimeFormat: presentationData.dateTimeFormat, maxDecimalPositions: 3) + " TON"
}
let giftTitle = "\(gift.title) #\(formatCollectibleNumber(gift.number, dateTimeFormat: presentationData.dateTimeFormat))"
let text = strings.Chat_GiftPurchaseOffer_AcceptConfirmation_Text(giftTitle, peer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder), priceString, finalPriceString).string
var contentNode: GiftOfferAlertContentNode?
var dismissImpl: ((Bool) -> Void)?
let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: buttonText, action: { [weak contentNode] in
contentNode?.inProgress = true
commit()
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?(true)
})]
contentNode = GiftOfferAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, gift: gift, peer: peer, title: title, text: text, amount: amount, actions: actions)
let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode!, navigationController: nil, chatPeerId: context.account.peerId, showBalance: false)
contentNode?.getController = { [weak controller] in
return controller
}
dismissImpl = { [weak controller] animated in
if animated {
controller?.dismissAnimated()
} else {
controller?.dismiss()
}
}
return controller
}
@@ -0,0 +1,295 @@
import Foundation
import UIKit
import ComponentFlow
import Display
import TelegramPresentationData
import ViewControllerComponent
import SheetComponent
import AccountContext
final class GiftPagerComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
public final class Item: Equatable {
let id: AnyHashable
let subject: GiftViewScreen.Subject
public init(id: AnyHashable, subject: GiftViewScreen.Subject) {
self.id = id
self.subject = subject
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.subject != rhs.subject {
return false
}
return true
}
}
let context: AccountContext
let items: [Item]
let index: Int
let itemSpacing: CGFloat
let isSwitching: Bool
let updated: (CGFloat, Int) -> Void
public init(
context: AccountContext,
items: [Item],
index: Int = 0,
itemSpacing: CGFloat = 0.0,
isSwitching: Bool = false,
updated: @escaping (CGFloat, Int) -> Void
) {
self.context = context
self.items = items
self.index = index
self.itemSpacing = itemSpacing
self.isSwitching = isSwitching
self.updated = updated
}
public static func ==(lhs: GiftPagerComponent, rhs: GiftPagerComponent) -> Bool {
if lhs.items != rhs.items {
return false
}
if lhs.index != rhs.index {
return false
}
if lhs.itemSpacing != rhs.itemSpacing {
return false
}
if lhs.isSwitching != rhs.isSwitching {
return false
}
return true
}
final class View: UIView, UIScrollViewDelegate {
private let dimView: UIView
private let scrollView: UIScrollView
private var itemViews: [AnyHashable: ComponentHostView<EnvironmentType>] = [:]
private var component: GiftPagerComponent?
private var environment: Environment<EnvironmentType>?
override init(frame: CGRect) {
self.dimView = UIView()
self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.4)
self.scrollView = UIScrollView(frame: frame)
self.scrollView.clipsToBounds = true
self.scrollView.isPagingEnabled = true
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.alwaysBounceHorizontal = false
self.scrollView.bounces = false
self.scrollView.layer.cornerRadius = 10.0
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.scrollView.contentInsetAdjustmentBehavior = .never
}
super.init(frame: frame)
self.addSubview(self.dimView)
self.scrollView.delegate = self
self.addSubview(self.scrollView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func animateIn() {
self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
private func animateOut() {
self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
}
private var isSwiping: Bool = false
private var lastScrollTime: TimeInterval = 0
private let swipeInactiveThreshold: TimeInterval = 0.5
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
self.isSwiping = true
self.lastScrollTime = CACurrentMediaTime()
}
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
self.isSwiping = false
}
}
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.isSwiping = false
}
private var ignoreContentOffsetChange = false
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let component = self.component, let environment = self.environment, !self.ignoreContentOffsetChange && !self.isUpdating else {
return
}
if self.isSwiping {
self.lastScrollTime = CACurrentMediaTime()
}
self.ignoreContentOffsetChange = true
let _ = self.update(component: component, availableSize: self.bounds.size, environment: environment, transition: .immediate)
component.updated(self.scrollView.contentOffset.x / (self.scrollView.contentSize.width - self.scrollView.frame.width), component.items.count)
self.ignoreContentOffsetChange = false
}
private var previousIsDisplaying: Bool = false
private var isUpdating = true
func update(component: GiftPagerComponent, availableSize: CGSize, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize), completion: nil)
var validIds: [AnyHashable] = []
let previousComponent = self.component
self.component = component
self.environment = environment
var countDecreased = false
if let previousComponent, previousComponent.items.count != component.items.count {
countDecreased = true
}
let firstTime = self.itemViews.isEmpty || countDecreased
let itemWidth = availableSize.width
let totalWidth = itemWidth * CGFloat(component.items.count) + component.itemSpacing * 2.0 * CGFloat(max(0, component.items.count))
let contentSize = CGSize(width: totalWidth, height: availableSize.height)
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
let scrollFrame = CGRect(origin: CGPoint(x: -component.itemSpacing / 2.0, y: 0.0), size: CGSize(width: availableSize.width + component.itemSpacing * 2.0, height: availableSize.height))
if self.scrollView.frame != scrollFrame {
self.scrollView.frame = scrollFrame
}
if firstTime {
let initialOffset = CGFloat(component.index) * (itemWidth + component.itemSpacing * 2.0)
self.scrollView.contentOffset = CGPoint(x: initialOffset, y: 0.0)
var position: CGFloat
if self.scrollView.contentSize.width > self.scrollView.frame.width {
position = self.scrollView.contentOffset.x / (self.scrollView.contentSize.width - self.scrollView.frame.width)
} else {
position = 0.0
}
component.updated(position, component.items.count)
}
let viewportCenter = self.scrollView.contentOffset.x + availableSize.width * 0.5
let currentTime = CACurrentMediaTime()
let isSwipingActive = self.isSwiping || (currentTime - self.lastScrollTime < self.swipeInactiveThreshold)
var i = 0
for item in component.items {
let itemOriginX = component.itemSpacing * 0.5 + (itemWidth + component.itemSpacing * 2.0) * CGFloat(i)
let itemFrame = CGRect(origin: CGPoint(x: itemOriginX, y: 0.0), size: CGSize(width: itemWidth, height: availableSize.height))
let centerDelta = itemFrame.midX - viewportCenter
let position = centerDelta / (availableSize.width * 0.75)
i += 1
if !isSwipingActive && abs(position) > 0.5 {
continue
} else if isSwipingActive && abs(position) > 1.5 {
continue
}
validIds.append(item.id)
let itemView: ComponentHostView<EnvironmentType>
var itemTransition = transition
if let current = self.itemViews[item.id] {
itemView = current
} else {
itemTransition = transition.withAnimation(.none)
itemView = ComponentHostView<EnvironmentType>()
self.itemViews[item.id] = itemView
self.scrollView.addSubview(itemView)
}
let environment = environment[EnvironmentType.self]
let _ = itemView.update(
transition: itemTransition,
component: AnyComponent(GiftViewSheetComponent(
context: component.context,
subject: item.subject
)),
environment: { environment },
containerSize: availableSize
)
itemView.frame = itemFrame
}
var animateTransitionMovement = false
var removeIds: [AnyHashable] = []
for (id, itemView) in self.itemViews {
if !validIds.contains(id) {
removeIds.append(id)
if countDecreased && !transition.animation.isImmediate {
self.addSubview(itemView)
itemView.center = CGPoint(x: self.scrollView.frame.width / 2.0 - component.itemSpacing, y: self.scrollView.frame.height / 2.0)
transition.setPosition(view: itemView, position: CGPoint(x: itemView.center.x - itemView.frame.width, y: itemView.center.y), completion: { _ in
itemView.removeFromSuperview()
})
animateTransitionMovement = true
} else {
itemView.removeFromSuperview()
}
}
}
for id in removeIds {
self.itemViews.removeValue(forKey: id)
}
if animateTransitionMovement {
transition.animatePosition(view: self.scrollView, from: CGPoint(x: self.scrollView.frame.width, y: 0.0), to: .zero, additive: true)
}
let viewEnvironment = environment[ViewControllerComponentContainer.Environment.self].value
if let _ = transition.userData(ViewControllerComponentContainer.AnimateInTransition.self) {
self.animateIn()
} else if self.previousIsDisplaying, let _ = transition.userData(ViewControllerComponentContainer.AnimateOutTransition.self) {
self.animateOut()
}
self.previousIsDisplaying = viewEnvironment.isVisible
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition)
}
}
@@ -0,0 +1,521 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import AppBundle
import AvatarNode
import Markdown
import GiftItemComponent
import ChatMessagePaymentAlertController
import ActivityIndicator
import TabSelectorComponent
import BundleIconComponent
import MultilineTextComponent
import TelegramStringFormatting
import TooltipUI
private final class GiftPurchaseAlertContentNode: AlertContentNode {
private let context: AccountContext
private let strings: PresentationStrings
private var presentationTheme: PresentationTheme
private let gift: StarGift.UniqueGift
private let peer: EnginePeer
fileprivate var currency: CurrencyAmount.Currency
fileprivate let header = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private let text = ComponentView<Empty>()
private let giftView = ComponentView<Empty>()
private let arrow = ComponentView<Empty>()
private let avatarNode: AvatarNode
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private var activityIndicator: ActivityIndicator?
private var validLayout: CGSize?
var inProgress = false {
didSet {
if let size = self.validLayout {
let _ = self.updateLayout(size: size, transition: .immediate)
}
}
}
var updatedCurrency: (CurrencyAmount.Currency) -> Void = { _ in }
override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
init(
context: AccountContext,
theme: AlertControllerTheme,
presentationTheme: PresentationTheme,
strings: PresentationStrings,
gift: StarGift.UniqueGift,
peer: EnginePeer,
actions: [TextAlertAction]
) {
self.context = context
self.strings = strings
self.presentationTheme = presentationTheme
self.gift = gift
self.peer = peer
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
self.currency = self.gift.resellForTonOnly ? .ton : .stars
super.init()
self.addSubnode(self.avatarNode)
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.updateTheme(theme)
self.avatarNode.setPeer(context: context, theme: presentationTheme, peer: peer)
}
override func updateTheme(_ theme: AlertControllerTheme) {
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
func requestUpdate(transition: ContainedViewLayoutTransition) {
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: transition)
}
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let containerSize = size
var size = size
size.width = min(size.width, 270.0)
var origin = CGPoint(x: 0.0, y: 20.0)
if self.gift.resellForTonOnly {
let headerSize = self.header.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: self.strings.Gift_Buy_AcceptsTonOnly, font: Font.regular(13.0), textColor: self.presentationTheme.actionSheet.secondaryTextColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 2
)
),
environment: {},
containerSize: CGSize(width: size.width - 32.0, height: size.height)
)
let headerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - headerSize.width) / 2.0), y: origin.y), size: headerSize)
if let view = self.header.view {
if view.superview == nil {
self.view.addSubview(view)
}
view.frame = headerFrame
}
origin.y += headerSize.height + 17.0
} else {
origin.y -= 4.0
let headerSize = self.header.update(
transition: ComponentTransition(transition),
component: AnyComponent(TabSelectorComponent(
colors: TabSelectorComponent.Colors(
foreground: self.presentationTheme.list.itemSecondaryTextColor,
selection: self.presentationTheme.list.itemSecondaryTextColor.withMultipliedAlpha(0.15),
simple: true
),
theme: self.presentationTheme,
customLayout: TabSelectorComponent.CustomLayout(
font: Font.medium(14.0),
spacing: 10.0
),
items: [
TabSelectorComponent.Item(
id: AnyHashable(0),
content: .text(self.strings.Gift_Buy_PayInStars)
),
TabSelectorComponent.Item(
id: AnyHashable(1),
content: .text(self.strings.Gift_Buy_PayInTon)
)
],
selectedId: self.currency == .ton ? AnyHashable(1) : AnyHashable(0),
setSelectedId: { [weak self] id in
guard let self else {
return
}
let currency: CurrencyAmount.Currency
if id == AnyHashable(0) {
currency = .stars
} else {
currency = .ton
}
if self.currency != currency {
self.currency = currency
self.updatedCurrency(currency)
self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring))
}
}
)),
environment: {},
containerSize: CGSize(width: containerSize.width - 16.0 * 2.0, height: 100.0)
)
size.width = min(containerSize.width, max(270.0, headerSize.width + 32.0))
let headerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - headerSize.width) / 2.0), y: origin.y), size: headerSize)
if let view = self.header.view {
if view.superview == nil {
self.view.addSubview(view)
}
view.frame = headerFrame
}
origin.y += headerSize.height + 17.0
}
self.validLayout = size
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
var resellPrice: CurrencyAmount?
if let actionNode = self.actionNodes.first {
switch self.currency {
case .stars:
if let resellAmount = self.gift.resellAmounts?.first(where: { $0.currency == .stars }) {
resellPrice = resellAmount
actionNode.action = TextAlertAction(type: .defaultAction, title: self.strings.Gift_Buy_Confirm_BuyFor(Int32(resellAmount.amount.value)), action: actionNode.action.action)
}
case .ton:
if let resellAmount = self.gift.resellAmounts?.first(where: { $0.currency == .ton }) {
resellPrice = resellAmount
let valueString = formatTonAmountText(resellAmount.amount.value, dateTimeFormat: presentationData.dateTimeFormat)
actionNode.action = TextAlertAction(type: .defaultAction, title: self.strings.Gift_Buy_Confirm_BuyForTon(valueString).string, action: actionNode.action.action)
}
}
}
let avatarSize = CGSize(width: 60.0, height: 60.0)
self.avatarNode.updateSize(size: avatarSize)
let giftFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) - 44.0, y: origin.y), size: avatarSize)
let _ = self.giftView.update(
transition: .immediate,
component: AnyComponent(
GiftItemComponent(
context: self.context,
theme: self.presentationTheme,
strings: self.strings,
peer: nil,
subject: .uniqueGift(gift: self.gift, price: nil),
mode: .thumbnail
)
),
environment: {},
containerSize: avatarSize
)
if let view = self.giftView.view {
if view.superview == nil {
self.view.addSubview(view)
}
view.frame = giftFrame
}
let arrowSize = self.arrow.update(
transition: .immediate,
component: AnyComponent(BundleIconComponent(name: "Peer Info/AlertArrow", tintColor: self.presentationTheme.actionSheet.secondaryTextColor)),
environment: {},
containerSize: size
)
let arrowFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - arrowSize.width) / 2.0), y: origin.y + floorToScreenPixels((avatarSize.height - arrowSize.height) / 2.0)), size: arrowSize)
if let view = self.arrow.view {
if view.superview == nil {
self.view.addSubview(view)
}
view.frame = arrowFrame
}
let avatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) + 44.0, y: origin.y), size: avatarSize)
transition.updateFrame(node: self.avatarNode, frame: avatarFrame)
origin.y += avatarSize.height + 17.0
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: self.strings.Gift_Buy_Confirm_Title, font: Font.semibold(17.0), textColor: self.presentationTheme.actionSheet.primaryTextColor)),
horizontalAlignment: .center
)
),
environment: {
},
containerSize: CGSize(width: size.width - 32.0, height: size.height)
)
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize)
if let view = self.title.view {
if view.superview == nil {
self.view.addSubview(view)
}
view.frame = titleFrame
}
origin.y += titleSize.height + 5.0
let giftTitle = "\(self.gift.title) #\(presentationStringsFormattedNumber(self.gift.number, presentationData.dateTimeFormat.groupingSeparator))"
let priceString: String
if let resellPrice {
switch resellPrice.currency {
case .stars:
priceString = self.strings.Gift_Buy_Confirm_Text_Stars(Int32(clamping: resellPrice.amount.value))
case .ton:
priceString = "**\(formatTonAmountText(resellPrice.amount.value, dateTimeFormat: presentationData.dateTimeFormat)) TON**"
}
} else {
priceString = ""
}
let text: String
if self.peer.id == self.context.account.peerId {
text = self.strings.Gift_Buy_Confirm_Text(giftTitle, priceString).string
} else {
text = self.strings.Gift_Buy_Confirm_GiftText(giftTitle, priceString, self.peer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)).string
}
let textSize = self.text.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .markdown(text: text, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: self.presentationTheme.actionSheet.primaryTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: self.presentationTheme.actionSheet.primaryTextColor),
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: self.presentationTheme.actionSheet.primaryTextColor),
linkAttribute: { url in
return ("URL", url)
}
)),
horizontalAlignment: .center,
maximumNumberOfLines: 0
)
),
environment: {
},
containerSize: CGSize(width: size.width - 32.0, height: size.height)
)
let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)
if let view = self.text.view {
if view.superview == nil {
self.view.addSubview(view)
}
view.frame = textFrame
}
origin.y += textSize.height + 10.0
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
let contentWidth = max(size.width, minActionsWidth)
let actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
let resultSize = CGSize(width: contentWidth, height: origin.y + actionsHeight - 26.0 + insets.top + insets.bottom)
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
var actionOffset: CGFloat = 0.0
//let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
/*switch effectiveActionLayout {
case .horizontal:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
case .vertical:*/
do {
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
/*switch effectiveActionLayout {
case .horizontal:
if nodeIndex == self.actionNodes.count - 1 {
currentActionWidth = resultSize.width - actionOffset
} else {
currentActionWidth = actionWidth
}
case .vertical:*/
do {
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
/*switch effectiveActionLayout {
case .horizontal:
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += currentActionWidth
case .vertical:*/
do {
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
if self.inProgress {
let activityIndicator: ActivityIndicator
if let current = self.activityIndicator {
activityIndicator = current
} else {
activityIndicator = ActivityIndicator(type: .custom(self.presentationTheme.list.freeInputField.controlColor, 18.0, 1.5, false))
self.addSubnode(activityIndicator)
}
if let actionNode = self.actionNodes.first {
actionNode.isUserInteractionEnabled = false
actionNode.isHidden = false
let indicatorSize = CGSize(width: 22.0, height: 22.0)
transition.updateFrame(node: activityIndicator, frame: CGRect(origin: CGPoint(x: actionNode.frame.minX + floor((actionNode.frame.width - indicatorSize.width) / 2.0), y: actionNode.frame.minY + floor((actionNode.frame.height - indicatorSize.height) / 2.0)), size: indicatorSize))
}
}
return resultSize
}
}
public func giftPurchaseAlertController(
context: AccountContext,
gift: StarGift.UniqueGift,
peer: EnginePeer,
animateBalanceOverlay: Bool = false,
navigationController: NavigationController?,
commit: @escaping (CurrencyAmount.Currency) -> Void,
dismissed: @escaping () -> Void
) -> AlertController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
var contentNode: GiftPurchaseAlertContentNode?
var dismissImpl: ((Bool) -> Void)?
var commitImpl: (() -> Void)?
let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: "", action: {
commitImpl?()
dismissImpl?(true)
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?(true)
})]
contentNode = GiftPurchaseAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), presentationTheme: presentationData.theme, strings: strings, gift: gift, peer: peer, actions: actions)
let controller = ChatMessagePaymentAlertController(
context: context,
presentationData: presentationData,
contentNode: contentNode!,
navigationController: navigationController,
chatPeerId: context.account.peerId,
showBalance: true,
currency: gift.resellForTonOnly ? .ton : .stars,
animateBalanceOverlay: animateBalanceOverlay
)
controller.dismissed = { _ in
dismissed()
}
dismissImpl = { [weak controller] animated in
if animated {
controller?.dismissAnimated()
} else {
controller?.dismiss()
}
}
commitImpl = { [weak contentNode] in
contentNode?.inProgress = true
commit(contentNode?.currency ?? .stars)
}
contentNode?.updatedCurrency = { [weak controller] currency in
controller?.currency = currency
}
if !gift.resellForTonOnly {
Queue.mainQueue().after(0.3) {
if let headerView = contentNode?.header.view as? TabSelectorComponent.View {
let absoluteFrame = headerView.convert(headerView.bounds, to: nil)
var originX = absoluteFrame.width * 0.75
if let itemFrame = headerView.frameForItem(AnyHashable(1)) {
originX = itemFrame.midX
}
let location = CGRect(origin: CGPoint(x: absoluteFrame.minX + floor(originX), y: absoluteFrame.minY - 8.0), size: CGSize())
let tooltipController = TooltipScreen(account: context.account, sharedContext: context.sharedContext, text: .plain(text: presentationData.strings.Gift_Buy_PayInTon_Tooltip), style: .wide, location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in
return .dismiss(consume: false)
})
controller.present(tooltipController, in: .window(.root))
}
}
}
return controller
}
@@ -0,0 +1,340 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import AppBundle
import Markdown
import ChatMessagePaymentAlertController
import ActivityIndicator
import MultilineTextWithEntitiesComponent
import TelegramStringFormatting
import TextFormat
private final class GiftRemoveInfoAlertContentNode: AlertContentNode {
private let context: AccountContext
private let strings: PresentationStrings
private var presentationTheme: PresentationTheme
private let title: String
private let text: String
private let gift: StarGift.UniqueGift
private let peers: [EnginePeer.Id: EnginePeer]
private let titleNode: ASTextNode
private let textNode: ASTextNode
private let infoBackgroundNode: ASDisplayNode
private let infoView = ComponentView<Empty>()
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private var activityIndicator: ActivityIndicator?
private var validLayout: CGSize?
var inProgress = false {
didSet {
if let size = self.validLayout {
let _ = self.updateLayout(size: size, transition: .immediate)
}
}
}
override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
init(
context: AccountContext,
theme: AlertControllerTheme,
ptheme: PresentationTheme,
strings: PresentationStrings,
gift: StarGift.UniqueGift,
peers: [EnginePeer.Id: EnginePeer],
title: String,
text: String,
actions: [TextAlertAction]
) {
self.context = context
self.strings = strings
self.presentationTheme = ptheme
self.title = title
self.text = text
self.gift = gift
self.peers = peers
self.titleNode = ASTextNode()
self.titleNode.maximumNumberOfLines = 0
self.textNode = ASTextNode()
self.textNode.maximumNumberOfLines = 0
self.infoBackgroundNode = ASDisplayNode()
self.infoBackgroundNode.backgroundColor = ptheme.overallDarkAppearance ? ptheme.list.itemModalBlocksBackgroundColor : ptheme.list.itemPrimaryTextColor.withAlphaComponent(0.04)
self.infoBackgroundNode.cornerRadius = 10.0
self.infoBackgroundNode.displaysAsynchronously = false
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.infoBackgroundNode)
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.updateTheme(theme)
}
override func updateTheme(_ theme: AlertControllerTheme) {
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor)
self.textNode.attributedText = parseMarkdownIntoAttributedString(self.text, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor),
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor),
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor),
linkAttribute: { url in
return ("URL", url)
}
), textAlignment: .center)
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width, 310.0)
let strings = self.strings
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
let titleSize = self.titleNode.measure(CGSize(width: size.width - 32.0, height: size.height))
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
origin.y += titleSize.height + 5.0
let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
origin.y += textSize.height + 10.0
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
let contentWidth = max(size.width, minActionsWidth)
let actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
var infoSize: CGSize = .zero
for attribute in self.gift.attributes {
if case let .originalInfo(senderPeerId, recipientPeerId, date, text, entities) = attribute {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let tableFont = Font.regular(13.0)
let tableBoldFont = Font.semibold(13.0)
let tableItalicFont = Font.italic(13.0)
let tableBoldItalicFont = Font.semiboldItalic(13.0)
let tableMonospaceFont = Font.monospace(13.0)
let tableTextColor = self.presentationTheme.list.itemPrimaryTextColor
let tableLinkColor = self.presentationTheme.list.itemAccentColor
let senderName = senderPeerId.flatMap { self.peers[$0]?.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder) }
let recipientName = self.peers[recipientPeerId]?.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder) ?? ""
let dateString = stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: presentationData.dateTimeFormat, withTime: false)
let value: NSAttributedString
if let text {
let attributedText = stringWithAppliedEntities(text, entities: entities ?? [], baseColor: tableTextColor, linkColor: tableLinkColor, baseFont: tableFont, linkFont: tableFont, boldFont: tableBoldFont, italicFont: tableItalicFont, boldItalicFont: tableBoldItalicFont, fixedFont: tableMonospaceFont, blockQuoteFont: tableFont, message: nil)
let format = senderName != nil ? presentationData.strings.Gift_Unique_OriginalInfoSenderWithText(senderName!, recipientName, dateString, "") : presentationData.strings.Gift_Unique_OriginalInfoWithText(recipientName, dateString, "")
let string = NSMutableAttributedString(string: format.string, font: tableFont, textColor: tableTextColor)
string.replaceCharacters(in: format.ranges[format.ranges.count - 1].range, with: attributedText)
if let _ = senderPeerId {
string.addAttribute(.foregroundColor, value: tableLinkColor, range: format.ranges[0].range)
string.addAttribute(.foregroundColor, value: tableLinkColor, range: format.ranges[1].range)
} else {
string.addAttribute(.foregroundColor, value: tableLinkColor, range: format.ranges[0].range)
}
value = string
} else {
let format = senderName != nil ? presentationData.strings.Gift_Unique_OriginalInfoSender(senderName!, recipientName, dateString) : presentationData.strings.Gift_Unique_OriginalInfo(recipientName, dateString)
let string = NSMutableAttributedString(string: format.string, font: tableFont, textColor: tableTextColor)
if let _ = senderPeerId {
string.addAttribute(.foregroundColor, value: tableLinkColor, range: format.ranges[0].range)
string.addAttribute(.foregroundColor, value: tableLinkColor, range: format.ranges[1].range)
} else {
string.addAttribute(.foregroundColor, value: tableLinkColor, range: format.ranges[0].range)
}
value = string
}
infoSize = self.infoView.update(
transition: .immediate,
component: AnyComponent(
MultilineTextWithEntitiesComponent(
context: self.context,
animationCache: self.context.animationCache,
animationRenderer: self.context.animationRenderer,
placeholderColor: self.presentationTheme.list.mediaPlaceholderColor,
text: .plain(value),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
handleSpoilers: true
)
),
environment: {},
containerSize: CGSize(width: contentWidth - 64.0, height: size.height)
)
let infoFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - infoSize.width) / 2.0), y: titleSize.height + textSize.height + 54.0), size: infoSize)
if let view = self.infoView.view {
if view.superview == nil {
self.view.addSubview(view)
}
view.frame = infoFrame
}
self.infoBackgroundNode.frame = infoFrame.insetBy(dx: -12.0, dy: -12.0)
break
}
}
let resultSize = CGSize(width: contentWidth, height: titleSize.height + textSize.height + infoSize.height + actionsHeight + 46.0 + insets.top + insets.bottom)
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
var actionOffset: CGFloat = 0.0
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
do {
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
do {
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
do {
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
if self.inProgress {
let activityIndicator: ActivityIndicator
if let current = self.activityIndicator {
activityIndicator = current
} else {
activityIndicator = ActivityIndicator(type: .custom(self.presentationTheme.list.freeInputField.controlColor, 18.0, 1.5, false))
self.addSubnode(activityIndicator)
}
if let actionNode = self.actionNodes.first {
actionNode.isHidden = true
let indicatorSize = CGSize(width: 22.0, height: 22.0)
transition.updateFrame(node: activityIndicator, frame: CGRect(origin: CGPoint(x: actionNode.frame.minX + floor((actionNode.frame.width - indicatorSize.width) / 2.0), y: actionNode.frame.minY + floor((actionNode.frame.height - indicatorSize.height) / 2.0)), size: indicatorSize))
}
}
return resultSize
}
}
public func giftRemoveInfoAlertController(
context: AccountContext,
gift: StarGift.UniqueGift,
peers: [EnginePeer.Id: EnginePeer],
removeInfoStars: Int64,
navigationController: NavigationController?,
commit: @escaping () -> Void
) -> AlertController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
let title = strings.Gift_RemoveDetails_Title
let text = strings.Gift_RemoveDetails_Text
let buttonText = strings.Gift_RemoveDetails_Action(" $ \(presentationStringsFormattedNumber(Int32(clamping: removeInfoStars), presentationData.dateTimeFormat.groupingSeparator))").string
var contentNode: GiftRemoveInfoAlertContentNode?
var dismissImpl: ((Bool) -> Void)?
let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: buttonText, action: {
dismissImpl?(true)
commit()
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?(true)
})]
contentNode = GiftRemoveInfoAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, gift: gift, peers: peers, title: title, text: text, actions: actions)
let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode!, navigationController: navigationController, chatPeerId: context.account.peerId, showBalance: removeInfoStars > 0)
dismissImpl = { [weak controller] animated in
if animated {
controller?.dismissAnimated()
} else {
controller?.dismiss()
}
}
return controller
}
@@ -0,0 +1,470 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import AppBundle
import AvatarNode
import Markdown
import GiftItemComponent
import ChatMessagePaymentAlertController
import ActivityIndicator
import TooltipUI
import MultilineTextComponent
import TelegramStringFormatting
private final class GiftTransferAlertContentNode: AlertContentNode {
private let context: AccountContext
private let strings: PresentationStrings
private var presentationTheme: PresentationTheme
private let title: String
private let text: String
private let gift: StarGift.UniqueGift
private let titleNode: ASTextNode
private let giftView = ComponentView<Empty>()
private let textNode: ASTextNode
private let arrowNode: ASImageNode
private let avatarNode: AvatarNode
private let tableView = ComponentView<Empty>()
private let modelButtonTag = GenericComponentViewTag()
private let backdropButtonTag = GenericComponentViewTag()
private let symbolButtonTag = GenericComponentViewTag()
fileprivate var getController: () -> ViewController? = { return nil}
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private var activityIndicator: ActivityIndicator?
private var validLayout: CGSize?
var inProgress = false {
didSet {
if let size = self.validLayout {
let _ = self.updateLayout(size: size, transition: .immediate)
}
}
}
override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
init(
context: AccountContext,
theme: AlertControllerTheme,
ptheme: PresentationTheme,
strings: PresentationStrings,
gift: StarGift.UniqueGift,
peer: EnginePeer,
title: String,
text: String,
actions: [TextAlertAction]
) {
self.context = context
self.strings = strings
self.presentationTheme = ptheme
self.title = title
self.text = text
self.gift = gift
self.titleNode = ASTextNode()
self.titleNode.maximumNumberOfLines = 0
self.textNode = ASTextNode()
self.textNode.maximumNumberOfLines = 0
self.arrowNode = ASImageNode()
self.arrowNode.displaysAsynchronously = false
self.arrowNode.displayWithoutProcessing = true
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.arrowNode)
self.addSubnode(self.avatarNode)
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.updateTheme(theme)
self.avatarNode.setPeer(context: context, theme: ptheme, peer: peer)
}
override func updateTheme(_ theme: AlertControllerTheme) {
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor)
self.textNode.attributedText = parseMarkdownIntoAttributedString(self.text, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor),
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor),
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor),
linkAttribute: { url in
return ("URL", url)
}
), textAlignment: .center)
self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/AlertArrow"), color: theme.secondaryColor.withAlphaComponent(0.9))
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
fileprivate func dismissAllTooltips() {
guard let controller = self.getController() else {
return
}
controller.window?.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss(inPlace: false)
}
})
controller.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss(inPlace: false)
}
return true
})
}
func showAttributeInfo(tag: Any, text: String) {
guard let controller = self.getController() else {
return
}
self.dismissAllTooltips()
guard let sourceView = self.tableView.findTaggedView(tag: tag), let absoluteLocation = sourceView.superview?.convert(sourceView.center, to: controller.view) else {
return
}
let location = CGRect(origin: CGPoint(x: absoluteLocation.x, y: absoluteLocation.y - 12.0), size: CGSize())
let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: text), style: .wide, location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in
return .dismiss(consume: false)
})
controller.present(tooltipController, in: .current)
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width, 310.0)
let strings = self.strings
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
let avatarSize = CGSize(width: 60.0, height: 60.0)
self.avatarNode.updateSize(size: avatarSize)
let giftFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) - 44.0, y: origin.y), size: avatarSize)
let _ = self.giftView.update(
transition: .immediate,
component: AnyComponent(
GiftItemComponent(
context: self.context,
theme: self.presentationTheme,
strings: strings,
peer: nil,
subject: .uniqueGift(gift: self.gift, price: nil),
mode: .thumbnail
)
),
environment: {},
containerSize: avatarSize
)
if let view = self.giftView.view {
if view.superview == nil {
self.view.addSubview(view)
}
view.frame = giftFrame
}
if let arrowImage = self.arrowNode.image {
let arrowFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - arrowImage.size.width) / 2.0), y: origin.y + floorToScreenPixels((avatarSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size)
transition.updateFrame(node: self.arrowNode, frame: arrowFrame)
}
let avatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) + 44.0, y: origin.y), size: avatarSize)
transition.updateFrame(node: self.avatarNode, frame: avatarFrame)
origin.y += avatarSize.height + 17.0
let titleSize = self.titleNode.measure(CGSize(width: size.width - 32.0, height: size.height))
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
origin.y += titleSize.height + 5.0
let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
origin.y += textSize.height + 10.0
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
let contentWidth = max(size.width, minActionsWidth)
let actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
let tableFont = Font.regular(15.0)
let tableTextColor = self.presentationTheme.list.itemPrimaryTextColor
var tableItems: [TableComponent.Item] = []
let order: [StarGift.UniqueGift.Attribute.AttributeType] = [
.model, .pattern, .backdrop, .originalInfo
]
var attributeMap: [StarGift.UniqueGift.Attribute.AttributeType: StarGift.UniqueGift.Attribute] = [:]
for attribute in self.gift.attributes {
attributeMap[attribute.attributeType] = attribute
}
for type in order {
if let attribute = attributeMap[type] {
let id: String?
let title: String?
let value: NSAttributedString
let percentage: Float?
let tag: AnyObject?
switch attribute {
case let .model(name, _, rarity):
id = "model"
title = strings.Gift_Unique_Model
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
percentage = Float(rarity) * 0.1
tag = self.modelButtonTag
case let .backdrop(name, _, _, _, _, _, rarity):
id = "backdrop"
title = strings.Gift_Unique_Backdrop
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
percentage = Float(rarity) * 0.1
tag = self.backdropButtonTag
case let .pattern(name, _, rarity):
id = "pattern"
title = strings.Gift_Unique_Symbol
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
percentage = Float(rarity) * 0.1
tag = self.symbolButtonTag
case .originalInfo:
continue
}
var items: [AnyComponentWithIdentity<Empty>] = []
items.append(
AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(
MultilineTextComponent(text: .plain(value))
)
)
)
if let percentage, let tag {
items.append(AnyComponentWithIdentity(
id: AnyHashable(1),
component: AnyComponent(Button(
content: AnyComponent(ButtonContentComponent(
context: self.context,
text: formatPercentage(percentage),
color: self.presentationTheme.list.itemAccentColor
)),
action: { [weak self] in
self?.showAttributeInfo(tag: tag, text: strings.Gift_Unique_AttributeDescription(formatPercentage(percentage)).string)
}
).tagged(tag))
))
}
let itemComponent = AnyComponent(
HStack(items, spacing: 4.0)
)
tableItems.append(.init(
id: id,
title: title,
hasBackground: false,
component: itemComponent
))
}
}
if let valueAmount = self.gift.valueAmount, let valueCurrency = self.gift.valueCurrency {
tableItems.append(.init(
id: "fiatValue",
title: strings.Gift_Unique_Value,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: "~\(formatCurrencyAmount(valueAmount, currency: valueCurrency))", font: tableFont, textColor: tableTextColor)))
),
insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 12.0)
))
}
let tableSize = self.tableView.update(
transition: .immediate,
component: AnyComponent(
TableComponent(
theme: self.presentationTheme,
items: tableItems
)
),
environment: {},
containerSize: CGSize(width: contentWidth - 32.0, height: size.height)
)
let tableFrame = CGRect(origin: CGPoint(x: 16.0, y: avatarSize.height + titleSize.height + textSize.height + 60.0), size: tableSize)
if let view = self.tableView.view {
if view.superview == nil {
self.view.addSubview(view)
}
view.frame = tableFrame
}
let resultSize = CGSize(width: contentWidth, height: avatarSize.height + titleSize.height + textSize.height + tableSize.height + actionsHeight + 40.0 + insets.top + insets.bottom)
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
var actionOffset: CGFloat = 0.0
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
do {
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
do {
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
do {
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
if self.inProgress {
let activityIndicator: ActivityIndicator
if let current = self.activityIndicator {
activityIndicator = current
} else {
activityIndicator = ActivityIndicator(type: .custom(self.presentationTheme.list.freeInputField.controlColor, 18.0, 1.5, false))
self.addSubnode(activityIndicator)
}
if let actionNode = self.actionNodes.first {
actionNode.isUserInteractionEnabled = false
actionNode.isHidden = false
let indicatorSize = CGSize(width: 22.0, height: 22.0)
transition.updateFrame(node: activityIndicator, frame: CGRect(origin: CGPoint(x: actionNode.frame.minX + floor((actionNode.frame.width - indicatorSize.width) / 2.0), y: actionNode.frame.minY + floor((actionNode.frame.height - indicatorSize.height) / 2.0)), size: indicatorSize))
}
}
return resultSize
}
}
public func giftTransferAlertController(
context: AccountContext,
gift: StarGift.UniqueGift,
peer: EnginePeer,
transferStars: Int64,
navigationController: NavigationController?,
commit: @escaping () -> Void
) -> AlertController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
let title = strings.Gift_Transfer_Confirmation_Title
let text: String
let buttonText: String
if transferStars > 0 {
text = strings.Gift_Transfer_Confirmation_Text("\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))", peer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder), strings.Gift_Transfer_Confirmation_Text_Stars(Int32(clamping: transferStars))).string
buttonText = "\(strings.Gift_Transfer_Confirmation_Transfer) $ \(transferStars)"
} else {
text = strings.Gift_Transfer_Confirmation_TextFree("\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))", peer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)).string
buttonText = strings.Gift_Transfer_Confirmation_TransferFree
}
var contentNode: GiftTransferAlertContentNode?
var dismissImpl: ((Bool) -> Void)?
let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: buttonText, action: { [weak contentNode] in
contentNode?.inProgress = true
commit()
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?(true)
})]
contentNode = GiftTransferAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, gift: gift, peer: peer, title: title, text: text, actions: actions)
let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode!, navigationController: navigationController, chatPeerId: context.account.peerId, showBalance: transferStars > 0)
contentNode?.getController = { [weak controller] in
return controller
}
dismissImpl = { [weak controller] animated in
if animated {
controller?.dismissAnimated()
} else {
controller?.dismiss()
}
}
return controller
}
@@ -0,0 +1,437 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import Markdown
import TextFormat
import TelegramPresentationData
import ViewControllerComponent
import SheetComponent
import BundleIconComponent
import BalancedTextComponent
import MultilineTextComponent
import ButtonComponent
import PlainButtonComponent
import GiftItemComponent
import AccountContext
import GlassBarButtonComponent
private final class SheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let gift: ProfileGiftsContext.State.StarGift
let pinnedGifts: [ProfileGiftsContext.State.StarGift]
let completion: (StarGiftReference) -> Void
let dismiss: () -> Void
init(
context: AccountContext,
gift: ProfileGiftsContext.State.StarGift,
pinnedGifts: [ProfileGiftsContext.State.StarGift],
completion: @escaping (StarGiftReference) -> Void,
dismiss: @escaping () -> Void
) {
self.context = context
self.gift = gift
self.pinnedGifts = pinnedGifts
self.completion = completion
self.dismiss = dismiss
}
static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.gift != rhs.gift {
return false
}
if lhs.pinnedGifts != rhs.pinnedGifts {
return false
}
return true
}
final class State: ComponentState {
var selectedGift: StarGiftReference?
}
func makeState() -> State {
return State()
}
static var body: Body {
let closeButton = Child(GlassBarButtonComponent.self)
let title = Child(BalancedTextComponent.self)
let text = Child(BalancedTextComponent.self)
let gifts = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self)
let button = Child(ButtonComponent.self)
var appliedSelectedGift: StarGiftReference?
return { context in
let environment = context.environment[EnvironmentType.self]
let component = context.component
let state = context.state
let theme = environment.theme
let strings = environment.strings
let textSideInset: CGFloat = 32.0 + environment.safeInsets.left
let titleFont = Font.semibold(17.0)
let subtitleFont = Font.regular(12.0)
let textColor = theme.actionSheet.primaryTextColor
let secondaryTextColor = theme.actionSheet.secondaryTextColor
var contentSize = CGSize(width: context.availableSize.width, height: 18.0)
let closeButton = closeButton.update(
component: GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0),
backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor,
isDark: theme.overallDarkAppearance,
state: .generic,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: theme.rootController.navigationBar.glassBarButtonForegroundColor
)
)),
action: { _ in
component.dismiss()
}
),
availableSize: CGSize(width: 40.0, height: 40.0),
transition: .immediate
)
context.add(closeButton
.position(CGPoint(x: environment.safeInsets.left + 16.0 + closeButton.size.width / 2.0, y: 36.0))
)
let title = title.update(
component: BalancedTextComponent(
text: .plain(NSAttributedString(string: strings.Gift_Unpin_Title, font: titleFont, textColor: textColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 1,
lineSpacing: 0.1
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, 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
let text = text.update(
component: BalancedTextComponent(
text: .plain(NSAttributedString(string: strings.Gift_Unpin_Subtitle, font: subtitleFont, textColor: secondaryTextColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 1,
lineSpacing: 0.2
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, 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 += 17.0
let itemsSideInset = environment.safeInsets.left + 16.0
let spacing: CGFloat = 10.0
let itemsInRow = 3
let width = (context.availableSize.width - itemsSideInset * 2.0 - spacing * CGFloat(itemsInRow - 1)) / CGFloat(itemsInRow)
var updatedGifts: [_UpdatedChildComponent] = []
var index = 0
var nextOriginX = itemsSideInset
for gift in component.pinnedGifts {
guard case let .unique(uniqueGift) = gift.gift else {
continue
}
var alpha: CGFloat = 1.0
var displayGift = uniqueGift
if let selectedGift = state.selectedGift {
alpha = selectedGift == gift.reference ? 1.0 : 0.5
if selectedGift == gift.reference {
if case let .unique(uniqueGift) = component.gift.gift {
displayGift = uniqueGift
}
}
}
var ribbonColor: GiftItemComponent.Ribbon.Color = .blue
for attribute in displayGift.attributes {
if case let .backdrop(_, _, innerColor, outerColor, _, _, _) = attribute {
ribbonColor = .custom(outerColor, innerColor)
break
}
}
let inset: CGFloat = 2.0
updatedGifts.append(
gifts[index].update(
component: AnyComponent(
PlainButtonComponent(
content: AnyComponent(
GiftItemComponent(
context: component.context,
theme: theme,
strings: strings,
subject: .uniqueGift(gift: displayGift, price: nil),
ribbon: GiftItemComponent.Ribbon(text: "#\(displayGift.number)", font: .monospaced, color: ribbonColor),
mode: .grid
)
),
effectAlignment: .center,
action: { [weak state] in
guard let state else {
return
}
if state.selectedGift == gift.reference {
state.selectedGift = nil
} else {
state.selectedGift = gift.reference
}
state.updated(transition: .spring(duration: 0.3))
},
animateAlpha: false
)
),
availableSize: CGSize(width: width + inset * 2.0, height: width + inset * 2.0),
transition: context.transition
)
)
var updatedGift = updatedGifts[index]
.position(CGPoint(x: nextOriginX + updatedGifts[index].size.width / 2.0 - inset, y: contentSize.height + updatedGifts[index].size.height / 2.0 - inset))
.allowsGroupOpacity(true)
.opacity(alpha)
if gift.reference == state.selectedGift && appliedSelectedGift != gift.reference {
updatedGift = updatedGift.update(ComponentTransition.Update({ _, view, transition in
UIView.transition(with: view, duration: 0.3, options: [.transitionFlipFromLeft, .curveEaseOut], animations: {
view.alpha = alpha
})
}))
} else if let appliedSelectedGift, appliedSelectedGift == gift.reference && gift.reference != state.selectedGift {
updatedGift = updatedGift.update(ComponentTransition.Update({ _, view, transition in
UIView.transition(with: view, duration: 0.3, options: [.transitionFlipFromRight, .curveEaseOut], animations: {
view.alpha = alpha
})
}))
}
context.add(updatedGift)
nextOriginX += updatedGifts[index].size.width - inset * 2.0 + spacing
if nextOriginX > context.availableSize.width - itemsSideInset {
contentSize.height += updatedGifts[index].size.height - inset * 2.0 + spacing
nextOriginX = itemsSideInset
}
index += 1
}
contentSize.height += 14.0
let button = button.update(
component: ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: theme.list.itemCheckColors.fillColor,
foreground: theme.list.itemCheckColors.foregroundColor,
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: AnyComponentWithIdentity(
id: AnyHashable("unpin"),
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Unpin_Replace, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))
)
),
isEnabled: state.selectedGift != nil,
displaysProgress: false,
action: { [weak state] in
guard let state else {
return
}
if let selectedGift = state.selectedGift {
component.completion(selectedGift)
component.dismiss()
}
}
),
availableSize: CGSize(width: context.availableSize.width - 30.0 * 2.0, height: 52.0),
transition: context.transition
)
context.add(button
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0))
.cornerRadius(10.0)
)
contentSize.height += button.size.height
contentSize.height += 7.0
let effectiveBottomInset: CGFloat = environment.metrics.isTablet ? 0.0 : environment.safeInsets.bottom
contentSize.height += 5.0 + effectiveBottomInset
appliedSelectedGift = state.selectedGift
return contentSize
}
}
}
private final class SheetContainerComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let gift: ProfileGiftsContext.State.StarGift
let pinnedGifts: [ProfileGiftsContext.State.StarGift]
let completion: (StarGiftReference) -> Void
init(
context: AccountContext,
gift: ProfileGiftsContext.State.StarGift,
pinnedGifts: [ProfileGiftsContext.State.StarGift],
completion: @escaping (StarGiftReference) -> Void
) {
self.context = context
self.gift = gift
self.pinnedGifts = pinnedGifts
self.completion = completion
}
static func ==(lhs: SheetContainerComponent, rhs: SheetContainerComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.gift != rhs.gift {
return false
}
if lhs.pinnedGifts != rhs.pinnedGifts {
return false
}
return true
}
static var body: Body {
let sheet = Child(SheetComponent<EnvironmentType>.self)
let animateOut = StoredActionSlot(Action<Void>.self)
let sheetExternalState = SheetComponent<EnvironmentType>.ExternalState()
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,
gift: context.component.gift,
pinnedGifts: context.component.pinnedGifts,
completion: context.component.completion,
dismiss: {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
}
)),
style: .glass,
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
followContentSizeChanges: true,
externalState: sheetExternalState,
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))
)
if let controller = controller(), !controller.automaticallyControlPresentationContextLayout {
let layout = ContainerViewLayout(
size: context.availableSize,
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: max(environment.safeInsets.bottom, sheetExternalState.contentHeight), right: 0.0),
safeInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right),
additionalInsets: .zero,
statusBarHeight: environment.statusBarHeight,
inputHeight: nil,
inputHeightIsInteractivellyChanging: false,
inVoiceOver: false
)
controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition)
}
return context.availableSize
}
}
}
public class GiftUnpinScreen: ViewControllerComponentContainer {
public init(
context: AccountContext,
gift: ProfileGiftsContext.State.StarGift,
pinnedGifts: [ProfileGiftsContext.State.StarGift],
completion: @escaping (StarGiftReference) -> Void
) {
super.init(
context: context,
component: SheetContainerComponent(
context: context,
gift: gift,
pinnedGifts: pinnedGifts,
completion: completion
),
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()
}
}
}
@@ -0,0 +1,684 @@
import Foundation
import UIKit
import SwiftSignalKit
import Display
import TelegramCore
import TelegramPresentationData
import ComponentFlow
import ComponentDisplayAdapters
import AccountContext
import ViewControllerComponent
import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import BalancedTextComponent
import ButtonComponent
import PresentationDataUtils
import LottieComponent
import ProfileLevelRatingBarComponent
import TextFormat
import TelegramStringFormatting
private final class GiftUpgradeCostScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let upgradePreview: StarGiftUpgradePreview
init(
context: AccountContext,
upgradePreview: StarGiftUpgradePreview
) {
self.context = context
self.upgradePreview = upgradePreview
}
static func ==(lhs: GiftUpgradeCostScreenComponent, rhs: GiftUpgradeCostScreenComponent) -> Bool {
return true
}
private final class ScrollView: UIScrollView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return super.hitTest(point, with: event)
}
}
private struct ItemLayout: Equatable {
var containerSize: CGSize
var containerInset: CGFloat
var bottomInset: CGFloat
var topInset: CGFloat
init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat) {
self.containerSize = containerSize
self.containerInset = containerInset
self.bottomInset = bottomInset
self.topInset = topInset
}
}
final class View: UIView, UIScrollViewDelegate {
private let dimView: UIView
private let backgroundLayer: SimpleLayer
private let navigationBarContainer: SparseContainerView
private let navigationBackgroundView: BlurredBackgroundView
private let navigationBarSeparator: SimpleLayer
private let scrollView: ScrollView
private let scrollContentClippingView: SparseContainerView
private let scrollContentView: UIView
private let closeButton = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private let descriptionText = ComponentView<Empty>()
private let bar = ComponentView<Empty>()
private let table = ComponentView<Empty>()
private let additionalDescription = ComponentView<Empty>()
private let bottomPanelContainer: UIView
private let bottomPanelSeparator: SimpleLayer
private let actionButton = ComponentView<Empty>()
private var isFirstTimeApplyingModalFactor: Bool = true
private var ignoreScrolling: Bool = false
private var component: GiftUpgradeCostScreenComponent?
private weak var state: EmptyComponentState?
private var environment: ViewControllerComponentContainer.Environment?
private var isUpdating: Bool = false
private var itemLayout: ItemLayout?
private var topOffsetDistance: CGFloat?
private var cachedCloseImage: UIImage?
private var upgradePreviewTimer: SwiftSignalKit.Timer?
private var effectiveUpgradePrice: StarGiftUpgradePreview.Price?
override init(frame: CGRect) {
self.dimView = UIView()
self.backgroundLayer = SimpleLayer()
self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
self.backgroundLayer.cornerRadius = 10.0
self.navigationBarContainer = SparseContainerView()
self.navigationBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
self.navigationBarSeparator = SimpleLayer()
self.scrollView = ScrollView()
self.scrollContentClippingView = SparseContainerView()
self.scrollContentClippingView.clipsToBounds = true
self.scrollContentView = UIView()
self.bottomPanelContainer = UIView()
self.bottomPanelSeparator = SimpleLayer()
super.init(frame: frame)
self.addSubview(self.dimView)
self.layer.addSublayer(self.backgroundLayer)
self.scrollView.delaysContentTouches = false
self.scrollView.canCancelContentTouches = true
self.scrollView.clipsToBounds = false
self.scrollView.contentInsetAdjustmentBehavior = .never
if #available(iOS 13.0, *) {
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
}
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.alwaysBounceHorizontal = false
self.scrollView.alwaysBounceVertical = true
self.scrollView.scrollsToTop = false
self.scrollView.delegate = self
self.scrollView.clipsToBounds = true
self.addSubview(self.scrollContentClippingView)
self.scrollContentClippingView.addSubview(self.scrollView)
self.scrollView.addSubview(self.scrollContentView)
self.addSubview(self.navigationBarContainer)
self.addSubview(self.bottomPanelContainer)
self.navigationBarContainer.addSubview(self.navigationBackgroundView)
self.navigationBarContainer.layer.addSublayer(self.navigationBarSeparator)
self.layer.addSublayer(self.bottomPanelSeparator)
self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
self.updateScrolling(transition: .immediate)
}
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.bounds.contains(point) {
return nil
}
if !self.backgroundLayer.frame.contains(point) {
return self.dimView
}
if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) {
return result
}
let result = super.hitTest(point, with: event)
return result
}
@objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
guard let environment = self.environment, let controller = environment.controller() else {
return
}
controller.dismiss()
}
}
func upgradePreviewTimerTick() {
guard let upgradePreview = self.component?.upgradePreview else {
return
}
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
if let currentPrice = self.effectiveUpgradePrice {
if let price = upgradePreview.nextPrices.reversed().first(where: { currentTime >= $0.date }) {
if price.stars != currentPrice.stars {
self.effectiveUpgradePrice = price
if !self.isUpdating {
self.state?.updated(transition: .immediate.withUserData(ProfileLevelRatingBarComponent.TransitionHint(animate: true)))
}
}
} else {
self.upgradePreviewTimer?.invalidate()
self.upgradePreviewTimer = nil
}
} else if let price = upgradePreview.nextPrices.reversed().first(where: { currentTime >= $0.date}) {
self.effectiveUpgradePrice = price
if !self.isUpdating {
self.state?.updated()
}
}
}
private func updateScrolling(transition: ComponentTransition) {
guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else {
return
}
var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset
let titleTransformFraction: CGFloat = max(0.0, min(1.0, -topOffset / 20.0))
let navigationAlpha: CGFloat = titleTransformFraction
transition.setAlpha(view: self.navigationBackgroundView, alpha: navigationAlpha)
transition.setAlpha(layer: self.navigationBarSeparator, alpha: navigationAlpha)
let bottomPanelAlphaDistance: CGFloat = 20.0
let bottomPanelDistance: CGFloat = self.scrollView.contentSize.height - self.scrollView.bounds.maxY
let bottomPanelAlphaFraction: CGFloat = max(0.0, min(1.0, bottomPanelDistance / bottomPanelAlphaDistance))
let bottomPanelAlpha: CGFloat = bottomPanelAlphaFraction
if self.bottomPanelSeparator.opacity != Float(bottomPanelAlpha) {
let alphaTransition = transition
alphaTransition.setAlpha(layer: self.bottomPanelSeparator, alpha: bottomPanelAlpha)
}
topOffset = max(0.0, topOffset)
transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0))
transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset))
let topOffsetDistance: CGFloat = 80.0
self.topOffsetDistance = topOffsetDistance
var topOffsetFraction = topOffset / topOffsetDistance
topOffsetFraction = max(0.0, min(1.0, topOffsetFraction))
let transitionFactor: CGFloat = 1.0 - topOffsetFraction
var modalOverlayTransition = transition
if self.isFirstTimeApplyingModalFactor {
self.isFirstTimeApplyingModalFactor = false
modalOverlayTransition = .spring(duration: 0.5)
}
if self.isUpdating {
DispatchQueue.main.async { [weak controller] in
guard let controller else {
return
}
controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: modalOverlayTransition.containedViewLayoutTransition)
}
} else {
controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: modalOverlayTransition.containedViewLayoutTransition)
}
}
func animateIn() {
self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY
self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.bottomPanelContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.bottomPanelSeparator.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
func animateOut(completion: @escaping () -> Void) {
let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY
self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in
completion()
})
self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
self.bottomPanelContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
self.bottomPanelSeparator.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
if let environment = self.environment, let controller = environment.controller() {
controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .easeInOut))
}
}
func update(component: GiftUpgradeCostScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let environment = environment[ViewControllerComponentContainer.Environment.self].value
let themeUpdated = self.environment?.theme !== environment.theme
let resetScrolling = self.scrollView.bounds.width != availableSize.width
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let isFirstTime = self.component == nil
self.component = component
self.state = state
self.environment = environment
if isFirstTime {
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
if let _ = component.upgradePreview.nextPrices.first(where: { currentTime < $0.date }) {
self.upgradePreviewTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
self?.upgradePreviewTimerTick()
}, queue: Queue.mainQueue())
self.upgradePreviewTimer?.start()
self.upgradePreviewTimerTick()
}
}
if themeUpdated {
self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
self.backgroundLayer.backgroundColor = environment.theme.actionSheet.opaqueItemBackgroundColor.cgColor
self.navigationBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate)
self.navigationBarSeparator.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor
self.bottomPanelSeparator.backgroundColor = environment.theme.rootController.tabBar.separatorColor.cgColor
}
transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize))
var contentHeight: CGFloat = 0.0
let closeImage: UIImage
if let image = self.cachedCloseImage, !themeUpdated {
closeImage = image
} else {
closeImage = generateCloseButtonImage(backgroundColor: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.05), foregroundColor: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.4))!
self.cachedCloseImage = closeImage
}
let closeButtonSize = self.closeButton.update(
transition: transition,
component: AnyComponent(Button(
content: AnyComponent(Image(image: closeImage, size: closeImage.size)),
action: { [weak self] in
guard let self, let controller = self.environment?.controller() else {
return
}
controller.dismiss()
}
).minSize(CGSize(width: 62.0, height: 56.0))),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
let closeButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - environment.safeInsets.right - closeButtonSize.width, y: 0.0), size: closeButtonSize)
if let closeButtonView = self.closeButton.view {
if closeButtonView.superview == nil {
self.navigationBarContainer.addSubview(closeButtonView)
}
transition.setFrame(view: closeButtonView, frame: closeButtonFrame)
}
let containerInset: CGFloat = environment.statusBarHeight + 10.0
let clippingY: CGFloat
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_UpgradeCost_Title, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
)
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: floor((56.0 - titleSize.height) * 0.5)), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
self.navigationBarContainer.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: titleFrame)
}
contentHeight += 56.0
let navigationBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: 54.0))
transition.setFrame(view: self.navigationBackgroundView, frame: navigationBackgroundFrame)
self.navigationBackgroundView.update(size: navigationBackgroundFrame.size, cornerRadius: 10.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition.containedViewLayoutTransition)
transition.setFrame(layer: self.navigationBarSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 54.0), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
var value: CGFloat = 0.0
if let startStars = component.upgradePreview.prices.first?.stars, let endStars = component.upgradePreview.prices.last?.stars {
let effectiveValue = self.effectiveUpgradePrice?.stars ?? endStars
value = (CGFloat(effectiveValue - endStars) / CGFloat(startStars - endStars))
}
value = pow(value, 0.6)
value = min(0.96, 1.0 - value)
let barSize = self.bar.update(
transition: transition,
component: AnyComponent(ProfileLevelRatingBarComponent(
theme: environment.theme,
value: value,
leftLabel: environment.strings.Gift_UpgradeCost_Stars(Int32(clamping: component.upgradePreview.prices.first?.stars ?? 0)),
rightLabel: environment.strings.Gift_UpgradeCost_Stars(Int32(clamping: component.upgradePreview.prices.last?.stars ?? 0)),
badgeValue: "\(self.effectiveUpgradePrice?.stars ?? 0)",
badgeTotal: "",
level: 0,
icon: .stars,
inversed: true
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 110.0)
)
let barFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - barSize.width) * 0.5), y: contentHeight), size: barSize)
if let barView = self.bar.view {
if barView.superview == nil {
self.scrollContentView.addSubview(barView)
}
transition.setFrame(view: barView, frame: barFrame)
}
contentHeight += barSize.height + 25.0
let descriptionSize = self.descriptionText.update(
transition: transition,
component: AnyComponent(BalancedTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Gift_UpgradeCost_Description,
font: Font.regular(15.0),
textColor: environment.theme.list.itemPrimaryTextColor,
paragraphAlignment: .center
)),
horizontalAlignment: .center,
maximumNumberOfLines: 3,
lineSpacing: 0.2
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 50.0, height: .greatestFiniteMagnitude)
)
let descriptionFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - descriptionSize.width) * 0.5), y: contentHeight), size: descriptionSize)
if let descriptionView = self.descriptionText.view {
if descriptionView.superview == nil {
self.scrollContentView.addSubview(descriptionView)
}
transition.setFrame(view: descriptionView, frame: descriptionFrame)
}
contentHeight += descriptionSize.height + 23.0
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
var tableItems: [TableComponent.Item] = []
for price in component.upgradePreview.prices {
if price.date < currentTime {
continue
}
let valueString = "⭐️\(presentationStringsFormattedNumber(abs(Int32(clamping: price.stars)), environment.dateTimeFormat.groupingSeparator))"
let valueAttributedString = NSMutableAttributedString(string: valueString, font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)
let range = (valueAttributedString.string as NSString).range(of: "⭐️")
if range.location != NSNotFound {
valueAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
valueAttributedString.addAttribute(.baselineOffset, value: 1.0, range: range)
}
tableItems.append(TableComponent.Item(
id: price.stars,
title: stringForGiftUpgradeTimestamp(strings: environment.strings, dateTimeFormat: environment.dateTimeFormat, timestamp: price.date),
titleFont: .bold,
component: AnyComponent(MultilineTextWithEntitiesComponent(context: component.context, animationCache: component.context.animationCache, animationRenderer: component.context.animationRenderer, placeholderColor: .white, text: .plain(valueAttributedString)))
))
}
let tableSize = self.table.update(
transition: transition,
component: AnyComponent(TableComponent(
theme: environment.theme,
items: tableItems
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude)
)
let tableFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - tableSize.width) * 0.5), y: contentHeight), size: tableSize)
if let tableView = self.table.view {
if tableView.superview == nil {
self.scrollContentView.addSubview(tableView)
}
transition.setFrame(view: tableView, frame: tableFrame)
}
contentHeight += tableSize.height + 15.0
let additionalDescriptionSize = self.additionalDescription.update(
transition: transition,
component: AnyComponent(BalancedTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Gift_UpgradeCost_AdditionalDescription,
font: Font.regular(13.0),
textColor: environment.theme.list.itemSecondaryTextColor,
paragraphAlignment: .center
)),
horizontalAlignment: .center,
maximumNumberOfLines: 5,
lineSpacing: 0.2
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 50.0, height: .greatestFiniteMagnitude)
)
let additionalDescriptionFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - additionalDescriptionSize.width) * 0.5), y: contentHeight), size: additionalDescriptionSize)
if let additionalDescriptionView = self.additionalDescription.view {
if additionalDescriptionView.superview == nil {
self.scrollContentView.addSubview(additionalDescriptionView)
}
transition.setFrame(view: additionalDescriptionView, frame: additionalDescriptionFrame)
}
contentHeight += additionalDescriptionSize.height + 15.0
let actionButtonTitle: String = environment.strings.Gift_UpgradeCost_Done
var buttonTitle: [AnyComponentWithIdentity<Empty>] = []
let playButtonAnimation = ActionSlot<Void>()
buttonTitle.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "anim_ok"),
color: environment.theme.list.itemCheckColors.foregroundColor,
startingPosition: .begin,
size: CGSize(width: 28.0, height: 28.0),
playOnce: playButtonAnimation
))))
buttonTitle.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(ButtonTextContentComponent(
text: actionButtonTitle,
badge: 0,
textColor: environment.theme.list.itemCheckColors.foregroundColor,
badgeBackground: environment.theme.list.itemCheckColors.foregroundColor,
badgeForeground: environment.theme.list.itemCheckColors.fillColor
))))
let actionButtonSize = self.actionButton.update(
transition: transition,
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
color: environment.theme.list.itemCheckColors.fillColor,
foreground: environment.theme.list.itemCheckColors.foregroundColor,
pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(HStack(buttonTitle, spacing: 2.0))
),
isEnabled: true,
displaysProgress: false,
action: { [weak self] in
guard let self else {
return
}
self.environment?.controller()?.dismiss()
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
)
let bottomPanelHeight = 10.0 + environment.safeInsets.bottom + actionButtonSize.height
let bottomPanelSeparatorFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0), size: CGSize(width: availableSize.width, height: UIScreenPixel))
transition.setFrame(layer: self.bottomPanelSeparator, frame: bottomPanelSeparatorFrame)
let bottomPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight), size: CGSize(width: availableSize.width, height: bottomPanelHeight))
transition.setFrame(view: self.bottomPanelContainer, frame: bottomPanelFrame)
let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: actionButtonSize)
if let actionButtonView = self.actionButton.view {
if actionButtonView.superview == nil {
self.bottomPanelContainer.addSubview(actionButtonView)
playButtonAnimation.invoke(Void())
}
transition.setFrame(view: actionButtonView, frame: actionButtonFrame)
}
contentHeight += bottomPanelHeight
clippingY = bottomPanelFrame.minY - 8.0
let topInset: CGFloat = max(0.0, availableSize.height - containerInset - contentHeight)
let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset)
self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset)
transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight)))
transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0))
transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: availableSize))
let scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset), size: CGSize(width: availableSize.width - sideInset * 2.0, height: clippingY - containerInset))
transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center)
transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size))
self.ignoreScrolling = true
let previousBounds = self.scrollView.bounds
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)))
let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight)
if contentSize != self.scrollView.contentSize {
self.scrollView.contentSize = contentSize
}
if resetScrolling {
self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize)
} else {
if !previousBounds.isEmpty, !transition.animation.isImmediate {
let bounds = self.scrollView.bounds
if bounds.maxY != previousBounds.maxY {
let offsetY = previousBounds.maxY - bounds.maxY
transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true)
}
}
}
self.ignoreScrolling = false
self.updateScrolling(transition: transition)
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public class GiftUpgradeCostScreen: ViewControllerComponentContainer {
private let context: AccountContext
private var isDismissed: Bool = false
public init(
context: AccountContext,
upgradePreview: StarGiftUpgradePreview
) {
self.context = context
super.init(context: context, component: GiftUpgradeCostScreenComponent(
context: context,
upgradePreview: upgradePreview
), navigationBarAppearance: .none, theme: .default)
self.statusBar.statusBarStyle = .Ignore
self.navigationPresentation = .flatModal
self.blocksBackgroundWhenInOverlay = true
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.view.disablesInteractiveModalDismiss = true
if let componentView = self.node.hostView.componentView as? GiftUpgradeCostScreenComponent.View {
componentView.animateIn()
}
}
override public func dismiss(completion: (() -> Void)? = nil) {
if !self.isDismissed {
self.isDismissed = true
if let componentView = self.node.hostView.componentView as? GiftUpgradeCostScreenComponent.View {
componentView.animateOut(completion: { [weak self] in
completion?()
self?.dismiss(animated: false)
})
} else {
self.dismiss(animated: false)
}
}
}
}
@@ -0,0 +1,859 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import PresentationDataUtils
import ComponentFlow
import ViewControllerComponent
import SheetComponent
import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import BundleIconComponent
import Markdown
import BalancedTextComponent
import TextFormat
import TelegramStringFormatting
import StarsAvatarComponent
import PlainButtonComponent
import TooltipUI
import GiftAnimationComponent
import ContextUI
import GiftItemComponent
import GlassBarButtonComponent
private final class GiftValueSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let gift: StarGift
let valueInfo: StarGift.UniqueGift.ValueInfo
let animateOut: ActionSlot<Action<()>>
let getController: () -> ViewController?
init(
context: AccountContext,
gift: StarGift,
valueInfo: StarGift.UniqueGift.ValueInfo,
animateOut: ActionSlot<Action<()>>,
getController: @escaping () -> ViewController?
) {
self.context = context
self.gift = gift
self.valueInfo = valueInfo
self.animateOut = animateOut
self.getController = getController
}
static func ==(lhs: GiftValueSheetContent, rhs: GiftValueSheetContent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.gift != rhs.gift {
return false
}
if lhs.valueInfo != rhs.valueInfo {
return false
}
return true
}
final class State: ComponentState {
let lastSalePriceTag = GenericComponentViewTag()
let floorPriceTag = GenericComponentViewTag()
let averagePriceTag = GenericComponentViewTag()
private let context: AccountContext
private let animateOut: ActionSlot<Action<()>>
private let getController: () -> ViewController?
private var disposable: Disposable?
var initialized = false
var starGiftsMap: [Int64: StarGift.Gift] = [:]
var cachedStarImage: (UIImage, PresentationTheme)?
var cachedSmallStarImage: (UIImage, PresentationTheme)?
var cachedSubtitleStarImage: (UIImage, PresentationTheme)?
var cachedTonImage: (UIImage, PresentationTheme)?
var cachedChevronImage: (UIImage, PresentationTheme)?
var cachedSmallChevronImage: (UIImage, PresentationTheme)?
init(
context: AccountContext,
animateOut: ActionSlot<Action<()>>,
getController: @escaping () -> ViewController?
) {
self.context = context
self.animateOut = animateOut
self.getController = getController
super.init()
self.disposable = (context.engine.payments.cachedStarGifts()
|> deliverOnMainQueue).startStrict(next: { [weak self] starGifts in
if let strongSelf = self {
var starGiftsMap: [Int64: StarGift.Gift] = [:]
if let starGifts {
for gift in starGifts {
if case let .generic(gift) = gift {
starGiftsMap[gift.id] = gift
}
}
}
strongSelf.starGiftsMap = starGiftsMap
strongSelf.updated(transition: .immediate)
}
})
}
deinit {
self.disposable?.dispose()
}
func showAttributeInfo(tag: Any, text: String) {
guard let controller = self.getController() as? GiftValueScreen else {
return
}
controller.dismissAllTooltips()
guard let sourceView = controller.node.hostView.findTaggedView(tag: tag), let absoluteLocation = sourceView.superview?.convert(sourceView.center, to: controller.view) else {
return
}
let location = CGRect(origin: CGPoint(x: absoluteLocation.x, y: absoluteLocation.y - 12.0), size: CGSize())
let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .markdown(text: text), style: .wide, location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in
return .dismiss(consume: false)
})
controller.present(tooltipController, in: .current)
}
func openGiftResale(gift: StarGift.Gift) {
guard let controller = self.getController() as? GiftValueScreen else {
return
}
let storeController = self.context.sharedContext.makeGiftStoreController(
context: self.context,
peerId: self.context.account.peerId,
gift: gift
)
controller.push(storeController)
}
func openGiftFragmentResale(url: String) {
guard let controller = self.getController() as? GiftValueScreen, let navigationController = controller.navigationController as? NavigationController else {
return
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
}
func dismiss(animated: Bool) {
guard let controller = self.getController() as? GiftValueScreen else {
return
}
if animated {
controller.dismissAllTooltips()
self.animateOut.invoke(Action { [weak controller] _ in
controller?.dismiss(completion: nil)
})
} else {
controller.dismiss(animated: false)
}
}
}
func makeState() -> State {
return State(context: self.context, animateOut: self.animateOut, getController: self.getController)
}
static var body: Body {
let closeButton = Child(GlassBarButtonComponent.self)
let animation = Child(GiftCompositionComponent.self)
let titleBackground = Child(RoundedRectangle.self)
let title = Child(MultilineTextComponent.self)
let description = Child(MultilineTextComponent.self)
let table = Child(TableComponent.self)
let telegramSaleButton = Child(PlainButtonComponent.self)
let fragmentSaleButton = Child(PlainButtonComponent.self)
let giftCompositionExternalState = GiftCompositionComponent.ExternalState()
return { context in
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let component = context.component
let theme = environment.theme
let strings = environment.strings
let dateTimeFormat = environment.dateTimeFormat
let state = context.state
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let titleString: String = formatCurrencyAmount(component.valueInfo.value, currency: component.valueInfo.currency)
var giftTitle: String = ""
var giftCollectionTitle: String = ""
var animationFile: TelegramMediaFile?
var giftIconSubject: GiftItemComponent.Subject?
var genericGift: StarGift.Gift?
switch component.gift {
case let .generic(gift):
animationFile = gift.file
giftIconSubject = .starGift(gift: gift, price: "")
case let .unique(gift):
for attribute in gift.attributes {
if case let .model(_, file, _) = attribute {
animationFile = file
}
}
giftCollectionTitle = gift.title
giftTitle = "\(gift.title) #\(formatCollectibleNumber(gift.number, dateTimeFormat: dateTimeFormat))"
if let gift = state.starGiftsMap[gift.giftId] {
giftIconSubject = .starGift(gift: gift, price: "")
genericGift = gift
}
}
var originY: CGFloat = 0.0
let headerHeight: CGFloat = 210.0
let headerSubject: GiftCompositionComponent.Subject?
if let animationFile {
headerSubject = .generic(animationFile)
} else {
headerSubject = nil
}
if let headerSubject {
let animation = animation.update(
component: GiftCompositionComponent(
context: component.context,
theme: environment.theme,
subject: headerSubject,
animationOffset: nil,
animationScale: nil,
displayAnimationStars: false,
externalState: giftCompositionExternalState,
requestUpdate: { [weak state] _ in
state?.updated()
}
),
availableSize: CGSize(width: context.availableSize.width, height: headerHeight),
transition: context.transition
)
context.add(animation
.position(CGPoint(x: context.availableSize.width / 2.0, y: headerHeight / 2.0))
)
}
originY += headerHeight
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: titleString,
font: Font.with(size: 24.0, design: .round, weight: .bold),
textColor: theme.list.itemCheckColors.foregroundColor,
paragraphAlignment: .center
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let titleBackground = titleBackground.update(
component: RoundedRectangle(color: theme.actionSheet.controlAccentColor, cornerRadius: 24.0),
environment: {},
availableSize: CGSize(width: title.size.width + 32.0, height: 48.0),
transition: .immediate
)
context.add(titleBackground
.position(CGPoint(x: context.availableSize.width / 2.0, y: 187.0))
)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: 187.0))
)
var descriptionText: String
if component.valueInfo.valueIsAverage {
descriptionText = strings.Gift_Value_DescriptionAveragePrice(giftCollectionTitle).string
} else {
if component.valueInfo.isLastSaleOnFragment {
descriptionText = strings.Gift_Value_DescriptionLastPriceFragment(giftTitle).string
} else {
descriptionText = strings.Gift_Value_DescriptionLastPriceTelegram(giftTitle).string
}
}
if !descriptionText.isEmpty {
let linkColor = theme.actionSheet.controlAccentColor
if state.cachedSmallStarImage == nil || state.cachedSmallStarImage?.1 !== environment.theme {
state.cachedSmallStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Premium/Stars/ButtonStar"), color: .white)!, theme)
}
if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme {
state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme)
}
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let textColor = theme.list.itemPrimaryTextColor
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)
})
descriptionText = descriptionText.replacingOccurrences(of: " >]", with: "\u{00A0}>]")
let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString
if let range = attributedString.string.range(of: "*"), let starImage = state.cachedSmallStarImage?.0 {
attributedString.addAttribute(.font, value: Font.regular(13.0), range: NSRange(range, in: attributedString.string))
attributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: attributedString.string))
attributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedString.string))
}
if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 {
attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string))
}
let description = description.update(
component: MultilineTextComponent(
text: .plain(attributedString),
horizontalAlignment: .center,
maximumNumberOfLines: 5,
lineSpacing: 0.2,
highlightColor: linkColor.withAlphaComponent(0.1),
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { _, _ in
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 50.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
context.add(description
.position(CGPoint(x: context.availableSize.width / 2.0, y: 231.0 + description.size.height / 2.0))
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
originY += description.size.height
originY += 42.0
} else {
originY += 9.0
}
let tableFont = Font.regular(15.0)
let tableTextColor = theme.list.itemPrimaryTextColor
var tableItems: [TableComponent.Item] = []
tableItems.append(.init(
id: "initialDate",
title: strings.Gift_Value_InitialSale,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: component.valueInfo.initialSaleDate, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
)
))
let valueString = "⭐️\(formatStarsAmountText(StarsAmount(value: component.valueInfo.initialSaleStars, nanos: 0), dateTimeFormat: dateTimeFormat)) (~\(formatCurrencyAmount(component.valueInfo.initialSalePrice, currency: component.valueInfo.currency)))"
let valueAttributedString = NSMutableAttributedString(string: valueString, font: tableFont, textColor: tableTextColor)
let range = (valueAttributedString.string as NSString).range(of: "⭐️")
if range.location != NSNotFound {
valueAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
valueAttributedString.addAttribute(.baselineOffset, value: 1.0, range: range)
}
tableItems.append(.init(
id: "initialPrice",
title: strings.Gift_Value_InitialPrice,
component: AnyComponent(MultilineTextWithEntitiesComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
placeholderColor: theme.list.mediaPlaceholderColor,
text: .plain(valueAttributedString),
maximumNumberOfLines: 0
)),
insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 12.0)
))
if let lastSaleDate = component.valueInfo.lastSaleDate {
tableItems.append(.init(
id: "lastDate",
title: strings.Gift_Value_LastSale,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: lastSaleDate, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
)
))
}
if let lastSalePrice = component.valueInfo.lastSalePrice {
let lastSalePriceString = formatCurrencyAmount(lastSalePrice, currency: component.valueInfo.currency)
let tag = state.lastSalePriceTag
var items: [AnyComponentWithIdentity<Empty>] = []
items.append(
AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: lastSalePriceString, font: tableFont, textColor: tableTextColor)))
)
)
)
let percentage = Int32(floor(Double(lastSalePrice) / Double(component.valueInfo.initialSalePrice) * 100.0 - 100.0))
let percentageString = (percentage > 0 ? "+\(percentage)" : "\(percentage)") + "%"
items.append(AnyComponentWithIdentity(
id: AnyHashable(1),
component: AnyComponent(Button(
content: AnyComponent(ButtonContentComponent(
context: component.context,
text: percentageString,
color: theme.list.itemAccentColor
)),
action: { [weak state] in
state?.showAttributeInfo(tag: tag, text: strings.Gift_Value_LastPriceInfo(lastSalePriceString, giftCollectionTitle).string)
}
).tagged(tag))
))
let itemComponent = AnyComponent(
HStack(items, spacing: 4.0)
)
tableItems.append(.init(
id: "lastPrice",
title: strings.Gift_Value_LastPrice,
hasBackground: false,
component: itemComponent
))
}
if let floorPrice = component.valueInfo.floorPrice {
let floorPriceString = formatCurrencyAmount(floorPrice, currency: component.valueInfo.currency)
let tag = state.floorPriceTag
var items: [AnyComponentWithIdentity<Empty>] = []
items.append(
AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: floorPriceString, font: tableFont, textColor: tableTextColor)))
)
)
)
items.append(AnyComponentWithIdentity(
id: AnyHashable(1),
component: AnyComponent(Button(
content: AnyComponent(ButtonContentComponent(
context: component.context,
text: "?",
color: theme.list.itemAccentColor
)),
action: { [weak state] in
state?.showAttributeInfo(tag: tag, text: strings.Gift_Value_MinimumPriceInfo(floorPriceString, giftCollectionTitle).string)
}
).tagged(tag))
))
let itemComponent = AnyComponent(
HStack(items, spacing: 4.0)
)
tableItems.append(.init(
id: "floorPrice",
title: strings.Gift_Value_MinimumPrice,
hasBackground: false,
component: itemComponent
))
}
if let averagePrice = component.valueInfo.averagePrice {
let averagePriceString = formatCurrencyAmount(averagePrice, currency: component.valueInfo.currency)
let tag = state.averagePriceTag
var items: [AnyComponentWithIdentity<Empty>] = []
items.append(
AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: averagePriceString, font: tableFont, textColor: tableTextColor)))
)
)
)
items.append(AnyComponentWithIdentity(
id: AnyHashable(1),
component: AnyComponent(Button(
content: AnyComponent(ButtonContentComponent(
context: component.context,
text: "?",
color: theme.list.itemAccentColor
)),
action: { [weak state] in
state?.showAttributeInfo(tag: tag, text: strings.Gift_Value_AveragePriceInfo(averagePriceString, giftCollectionTitle).string)
}
).tagged(tag))
))
let itemComponent = AnyComponent(
HStack(items, spacing: 4.0)
)
tableItems.append(.init(
id: "averagePrice",
title: strings.Gift_Value_AveragePrice,
hasBackground: false,
component: itemComponent
))
}
let table = table.update(
component: TableComponent(
theme: environment.theme,
items: tableItems
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude),
transition: context.transition
)
context.add(table
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + table.size.height / 2.0))
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
originY += table.size.height + 23.0
if component.valueInfo.listedCount != nil || component.valueInfo.fragmentListedCount != nil {
originY += 5.0
}
if let listedCount = component.valueInfo.listedCount, let giftIconSubject {
let telegramSaleButton = telegramSaleButton.update(
component: PlainButtonComponent(
content: AnyComponent(
HStack([
AnyComponentWithIdentity(id: "count", component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: presentationStringsFormattedNumber(listedCount, dateTimeFormat.groupingSeparator), font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor)))
)),
AnyComponentWithIdentity(id: "spacing", component: AnyComponent(
Rectangle(color: .clear, width: 8.0, height: 1.0)
)),
AnyComponentWithIdentity(id: "icon", component: AnyComponent(
GiftItemComponent(
context: component.context,
theme: theme,
strings: strings,
peer: nil,
subject: giftIconSubject,
mode: .buttonIcon
)
)),
AnyComponentWithIdentity(id: "label", component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: " \(strings.Gift_Value_ForSaleOnTelegram)", font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor)))
)),
AnyComponentWithIdentity(id: "arrow", component: AnyComponent(
BundleIconComponent(name: "Chat/Context Menu/Arrow", tintColor: theme.actionSheet.controlAccentColor)
))
], spacing: 0.0)
),
action: { [weak state] in
guard let state, let genericGift else {
return
}
state.openGiftResale(gift: genericGift)
},
animateScale: false
),
environment: {},
availableSize: context.availableSize,
transition: .immediate
)
context.add(telegramSaleButton
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + telegramSaleButton.size.height / 2.0))
)
originY += telegramSaleButton.size.height
originY += 12.0
}
if let listedCount = component.valueInfo.fragmentListedCount, let fragmentListedUrl = component.valueInfo.fragmentListedUrl, let giftIconSubject {
if component.valueInfo.listedCount != nil {
originY += 18.0
}
let fragmentSaleButton = fragmentSaleButton.update(
component: PlainButtonComponent(
content: AnyComponent(
HStack([
AnyComponentWithIdentity(id: "count", component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: presentationStringsFormattedNumber(listedCount, dateTimeFormat.groupingSeparator), font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor)))
)),
AnyComponentWithIdentity(id: "spacing", component: AnyComponent(
Rectangle(color: .clear, width: 8.0, height: 1.0)
)),
AnyComponentWithIdentity(id: "icon", component: AnyComponent(
GiftItemComponent(
context: component.context,
theme: theme,
strings: strings,
peer: nil,
subject: giftIconSubject,
mode: .buttonIcon
)
)),
AnyComponentWithIdentity(id: "label", component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: " \(strings.Gift_Value_ForSaleOnFragment)", font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor)))
)),
AnyComponentWithIdentity(id: "arrow", component: AnyComponent(
BundleIconComponent(name: "Chat/Context Menu/Arrow", tintColor: theme.actionSheet.controlAccentColor)
))
], spacing: 0.0)
),
action: { [weak state] in
state?.openGiftFragmentResale(url: fragmentListedUrl)
},
animateScale: false
),
environment: {},
availableSize: context.availableSize,
transition: .immediate
)
context.add(fragmentSaleButton
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + fragmentSaleButton.size.height / 2.0))
)
originY += fragmentSaleButton.size.height
originY += 12.0
}
let closeButton = closeButton.update(
component: GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0),
backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor,
isDark: theme.overallDarkAppearance,
state: .generic,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: theme.rootController.navigationBar.glassBarButtonForegroundColor
)
)),
action: { [weak state] _ in
guard let state else {
return
}
state.dismiss(animated: true)
}
),
availableSize: CGSize(width: 40.0, height: 40.0),
transition: .immediate
)
context.add(closeButton
.position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0))
)
let effectiveBottomInset: CGFloat = environment.metrics.isTablet ? 0.0 : environment.safeInsets.bottom
return CGSize(width: context.availableSize.width, height: originY + 5.0 + effectiveBottomInset)
}
}
}
final class GiftValueSheetComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let gift: StarGift
let valueInfo: StarGift.UniqueGift.ValueInfo
init(
context: AccountContext,
gift: StarGift,
valueInfo: StarGift.UniqueGift.ValueInfo
) {
self.context = context
self.gift = gift
self.valueInfo = valueInfo
}
static func ==(lhs: GiftValueSheetComponent, rhs: GiftValueSheetComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.gift != rhs.gift {
return false
}
if lhs.valueInfo != rhs.valueInfo {
return false
}
return true
}
static var body: Body {
let sheet = Child(SheetComponent<EnvironmentType>.self)
let animateOut = StoredActionSlot(Action<Void>.self)
let sheetExternalState = SheetComponent<EnvironmentType>.ExternalState()
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(GiftValueSheetContent(
context: context.component.context,
gift: context.component.gift,
valueInfo: context.component.valueInfo,
animateOut: animateOut,
getController: controller
)),
style: .glass,
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
followContentSizeChanges: true,
clipsContent: true,
autoAnimateOut: false,
externalState: sheetExternalState,
animateOut: animateOut,
onPan: {
if let controller = controller() as? GiftValueScreen {
controller.dismissAllTooltips()
}
},
willDismiss: {
}
),
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 {
if let controller = controller() as? GiftValueScreen {
controller.dismissAllTooltips()
animateOut.invoke(Action { _ in
controller.dismiss(completion: nil)
})
}
} else {
if let controller = controller() as? GiftValueScreen {
controller.dismissAllTooltips()
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))
)
if let controller = controller(), !controller.automaticallyControlPresentationContextLayout {
var sideInset: CGFloat = 0.0
var bottomInset: CGFloat = max(environment.safeInsets.bottom, sheetExternalState.contentHeight)
if case .regular = environment.metrics.widthClass {
sideInset = floor((context.availableSize.width - 430.0) / 2.0) - 12.0
bottomInset = (context.availableSize.height - sheetExternalState.contentHeight) / 2.0 + sheetExternalState.contentHeight
}
let layout = ContainerViewLayout(
size: context.availableSize,
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0),
safeInsets: UIEdgeInsets(top: 0.0, left: max(sideInset, environment.safeInsets.left), bottom: 0.0, right: max(sideInset, environment.safeInsets.right)),
additionalInsets: .zero,
statusBarHeight: environment.statusBarHeight,
inputHeight: nil,
inputHeightIsInteractivellyChanging: false,
inVoiceOver: false
)
controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition)
}
return context.availableSize
}
}
}
final class GiftValueScreen: ViewControllerComponentContainer {
private let context: AccountContext
private let gift: StarGift
private let valueInfo: StarGift.UniqueGift.ValueInfo
public init(
context: AccountContext,
gift: StarGift,
valueInfo: StarGift.UniqueGift.ValueInfo
) {
self.context = context
self.gift = gift
self.valueInfo = valueInfo
super.init(
context: context,
component: GiftValueSheetComponent(
context: context,
gift: gift,
valueInfo: valueInfo
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
theme: .default
)
self.navigationPresentation = .flatModal
self.automaticallyControlPresentationContextLayout = false
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
public override func viewDidLoad() {
super.viewDidLoad()
self.view.disablesInteractiveModalDismiss = true
}
public override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.dismissAllTooltips()
}
public func dismissAnimated() {
self.dismissAllTooltips()
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
fileprivate func dismissAllTooltips() {
self.window?.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss(inPlace: false)
}
})
self.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss(inPlace: false)
}
return true
})
}
}
@@ -0,0 +1,404 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import AppBundle
import Markdown
import GiftItemComponent
import StarsAvatarComponent
import PasswordSetupUI
import OwnershipTransferController
import PresentationDataUtils
private final class GiftWithdrawAlertContentNode: AlertContentNode {
private let context: AccountContext
private let strings: PresentationStrings
private var presentationTheme: PresentationTheme
private let title: String
private let text: String
private let gift: StarGift.UniqueGift
private let titleNode: ASTextNode
private let giftView = ComponentView<Empty>()
private let textNode: ASTextNode
private let arrowNode: ASImageNode
private let avatarView = ComponentView<Empty>()
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private var validLayout: CGSize?
override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
init(
context: AccountContext,
theme: AlertControllerTheme,
ptheme: PresentationTheme,
strings: PresentationStrings,
gift: StarGift.UniqueGift,
title: String,
text: String,
actions: [TextAlertAction]
) {
self.context = context
self.strings = strings
self.presentationTheme = ptheme
self.title = title
self.text = text
self.gift = gift
self.titleNode = ASTextNode()
self.titleNode.maximumNumberOfLines = 0
self.textNode = ASTextNode()
self.textNode.maximumNumberOfLines = 0
self.arrowNode = ASImageNode()
self.arrowNode.displaysAsynchronously = false
self.arrowNode.displayWithoutProcessing = true
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.arrowNode)
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.updateTheme(theme)
}
override func updateTheme(_ theme: AlertControllerTheme) {
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor)
self.textNode.attributedText = parseMarkdownIntoAttributedString(self.text, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor),
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor),
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor),
linkAttribute: { url in
return ("URL", url)
}
), textAlignment: .center)
self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/AlertArrow"), color: theme.secondaryColor)
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width, 270.0)
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
let avatarSize = CGSize(width: 60.0, height: 60.0)
let giftFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) - 44.0, y: origin.y), size: avatarSize)
let _ = self.giftView.update(
transition: .immediate,
component: AnyComponent(
GiftItemComponent(
context: self.context,
theme: self.presentationTheme,
strings: self.strings,
peer: nil,
subject: .uniqueGift(gift: self.gift, price: nil),
mode: .thumbnail
)
),
environment: {},
containerSize: avatarSize
)
if let view = self.giftView.view {
if view.superview == nil {
self.view.addSubview(view)
}
view.frame = giftFrame
}
if let arrowImage = self.arrowNode.image {
let arrowFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - arrowImage.size.width) / 2.0), y: origin.y + floorToScreenPixels((avatarSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size)
transition.updateFrame(node: self.arrowNode, frame: arrowFrame)
}
let avatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) + 44.0, y: origin.y), size: avatarSize)
let _ = self.avatarView.update(
transition: .immediate,
component: AnyComponent(
StarsAvatarComponent(
context: self.context,
theme: self.presentationTheme,
peer: .transactionPeer(.fragment),
photo: nil,
media: [],
gift: nil,
backgroundColor: .clear,
size: avatarSize
)
),
environment: {},
containerSize: avatarSize
)
if let view = self.avatarView.view {
if view.superview == nil {
self.view.addSubview(view)
}
view.frame = avatarFrame
}
origin.y += avatarSize.height + 17.0
let titleSize = self.titleNode.measure(CGSize(width: size.width - 32.0, height: size.height))
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
origin.y += titleSize.height + 5.0
let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
origin.y += textSize.height + 10.0
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
var effectiveActionLayout = TextAlertContentActionLayout.vertical
if !"".isEmpty {
// Silence the warning
effectiveActionLayout = .horizontal
}
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
let contentWidth = max(size.width, minActionsWidth)
let actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
let resultSize = CGSize(width: contentWidth, height: avatarSize.height + titleSize.height + textSize.height + actionsHeight + 24.0 + insets.top + insets.bottom)
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
var actionOffset: CGFloat = 0.0
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
switch effectiveActionLayout {
case .horizontal:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
case .vertical:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
switch effectiveActionLayout {
case .horizontal:
if nodeIndex == self.actionNodes.count - 1 {
currentActionWidth = resultSize.width - actionOffset
} else {
currentActionWidth = actionWidth
}
case .vertical:
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
switch effectiveActionLayout {
case .horizontal:
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += currentActionWidth
case .vertical:
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
return resultSize
}
}
public func giftWithdrawAlertController(context: AccountContext, gift: StarGift.UniqueGift, commit: @escaping () -> Void) -> AlertController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
let title = strings.Gift_Withdraw_Title
let text = strings.Gift_Withdraw_Text("\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))").string
let buttonText = strings.Gift_Withdraw_Proceed
var dismissImpl: ((Bool) -> Void)?
let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: buttonText, action: {
dismissImpl?(true)
commit()
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?(true)
})]
let contentNode = GiftWithdrawAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, gift: gift, title: title, text: text, actions: actions)
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
dismissImpl = { [weak controller] animated in
if animated {
controller?.dismissAnimated()
} else {
controller?.dismiss()
}
}
return controller
}
public func confirmGiftWithdrawalController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, reference: StarGiftReference, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void) -> ViewController {
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
var dismissImpl: (() -> Void)?
var proceedImpl: (() -> Void)?
let disposable = MetaDisposable()
let contentNode = ChannelOwnershipTransferAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, title: presentationData.strings.Gift_Withdraw_EnterPassword_Title, text: presentationData.strings.Gift_Withdraw_EnterPassword_Text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?()
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Gift_Withdraw_EnterPassword_Done, action: {
proceedImpl?()
})])
contentNode.complete = {
proceedImpl?()
}
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller, weak contentNode] presentationData in
controller?.theme = AlertControllerTheme(presentationData: presentationData)
contentNode?.theme = presentationData.theme
})
controller.dismissed = { _ in
presentationDataDisposable.dispose()
disposable.dispose()
}
dismissImpl = { [weak controller, weak contentNode] in
contentNode?.dismissInput()
controller?.dismissAnimated()
}
proceedImpl = { [weak contentNode] in
guard let contentNode = contentNode else {
return
}
contentNode.updateIsChecking(true)
let signal = context.engine.payments.requestStarGiftWithdrawalUrl(reference: reference, password: contentNode.password)
disposable.set((signal |> deliverOnMainQueue).start(next: { url in
dismissImpl?()
completion(url)
}, error: { [weak contentNode] error in
var errorTextAndActions: (String, [TextAlertAction])?
switch error {
case .invalidPassword:
contentNode?.animateError()
case .limitExceeded:
errorTextAndActions = (presentationData.strings.TwoStepAuth_FloodError, [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
default:
errorTextAndActions = (presentationData.strings.Login_UnknownError, [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
}
contentNode?.updateIsChecking(false)
if let (text, actions) = errorTextAndActions {
dismissImpl?()
present(textAlertController(context: context, title: nil, text: text, actions: actions), nil)
}
}))
}
return controller
}
public func giftWithdrawalController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, reference: StarGiftReference, initialError: RequestStarGiftWithdrawalError, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void) -> ViewController {
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
let theme = AlertControllerTheme(presentationData: presentationData)
var title: NSAttributedString? = NSAttributedString(string: presentationData.strings.Gift_Withdraw_SecurityCheck, font: Font.semibold(presentationData.listsFontSize.itemListBaseFontSize), textColor: theme.primaryColor, paragraphAlignment: .center)
var text = presentationData.strings.Gift_Withdraw_SecurityRequirements
let textFontSize = presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0
var actions: [TextAlertAction] = []
switch initialError {
case .requestPassword:
return confirmGiftWithdrawalController(context: context, updatedPresentationData: updatedPresentationData, reference: reference, present: present, completion: completion)
case .twoStepAuthTooFresh, .authSessionTooFresh:
text = text + presentationData.strings.Gift_Withdraw_ComeBackLater
actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]
case .twoStepAuthMissing:
actions = [TextAlertAction(type: .genericAction, title: presentationData.strings.Gift_Withdraw_SetupTwoStepAuth, action: {
let controller = SetupTwoStepVerificationController(context: context, initialState: .automatic, stateUpdated: { update, shouldDismiss, controller in
if shouldDismiss {
controller.dismiss()
}
})
present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})]
default:
title = nil
text = presentationData.strings.Login_UnknownError
actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]
}
let body = MarkdownAttributeSet(font: Font.regular(textFontSize), textColor: theme.primaryColor)
let bold = MarkdownAttributeSet(font: Font.semibold(textFontSize), textColor: theme.primaryColor)
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center)
return richTextAlertController(context: context, title: title, text: attributedText, actions: actions)
}
@@ -0,0 +1,496 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import ImageBlur
private let additionalInset: CGFloat = 4.0
private let maskInset: CGFloat = 8.0
final class SlotsComponent<ChildEnvironment: Equatable>: Component {
public typealias EnvironmentType = ChildEnvironment
private let item: AnyComponent<ChildEnvironment>
private let items: [AnyComponentWithIdentity<ChildEnvironment>]
private let isAnimating: Bool
private let tintColor: UIColor?
private let verticalOffset: CGFloat
private let motionBlur: Bool
private let size: CGSize
public init(
item: AnyComponent<ChildEnvironment>,
items: [AnyComponentWithIdentity<ChildEnvironment>],
isAnimating: Bool,
tintColor: UIColor? = nil,
verticalOffset: CGFloat = 0.0,
motionBlur: Bool = true,
size: CGSize
) {
self.item = item
self.items = items
self.isAnimating = isAnimating
self.tintColor = tintColor
self.verticalOffset = verticalOffset
self.motionBlur = motionBlur
self.size = size
}
public static func == (lhs: SlotsComponent<ChildEnvironment>, rhs: SlotsComponent<ChildEnvironment>) -> Bool {
if lhs.item != rhs.item {
return false
}
if lhs.items != rhs.items {
return false
}
if lhs.isAnimating != rhs.isAnimating {
return false
}
if lhs.tintColor != rhs.tintColor {
return false
}
if lhs.verticalOffset != rhs.verticalOffset {
return false
}
if lhs.motionBlur != rhs.motionBlur {
return false
}
if lhs.size != rhs.size {
return false
}
return true
}
public final class View: UIView {
private var itemViews: [AnyHashable: ComponentView<ChildEnvironment>] = [:]
private var motionBlurLayers: [AnyHashable: SimpleLayer] = [:]
private var order: [AnyHashable] = []
private let containerView = UIView()
private let maskLayer = SimpleGradientLayer()
private enum SpinState {
case idle
case spinning
case decelerating
case settled
}
private var spinState: SpinState = .idle
private var isAnimating = false
private var animationLink: SharedDisplayLinkDriver.Link?
private var currentIds = Set<AnyHashable>()
private var lastSpawnTime: Double?
private var currentInterval: Double = 0.09
private var motionBlurFactor: CGFloat = 1.0
private var decelQueue: [AnyComponentWithIdentity<ChildEnvironment>] = []
private var decelTotalSteps: Int = 0
private var decelStepIndex: Int = 0
private let minSpawnInterval: Double = 0.10
private let maxSpawnInterval: Double = 0.80
private let baseAnimDuration: Double = 0.18
private let maxAnimDuration: Double = 0.5
private var component: SlotsComponent?
private var environment: Environment<ChildEnvironment>?
private var availableSize: CGSize?
@inline(__always) private func lerp(_ a: Double, _ b: Double, _ t: Double) -> Double { a + (b - a) * t }
@inline(__always) private func clamp01(_ x: Double) -> Double { max(0.0, min(1.0, x)) }
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(self.containerView)
self.containerView.clipsToBounds = true
self.maskLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
self.maskLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
let fade: CGFloat = 0.2
self.maskLayer.locations = [0.0, NSNumber(value: Float(fade)), NSNumber(value: Float(1.0 - fade)), 1.0]
self.maskLayer.colors = [
UIColor.black.withAlphaComponent(0.0).cgColor,
UIColor.black.withAlphaComponent(1.0).cgColor,
UIColor.black.withAlphaComponent(1.0).cgColor,
UIColor.black.withAlphaComponent(0.0).cgColor
]
self.containerView.layer.mask = self.maskLayer
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func spawnRandomSlot(availableSize: CGSize) {
guard var items = self.component?.items, !items.isEmpty else { return }
items = items.filter { !self.currentIds.contains($0.id) }
guard let randomItem = items.randomElement() else { return }
self.spawnSlot(item: randomItem, availableSize: availableSize) {}
}
private func spawnSlot(
item: AnyComponentWithIdentity<ChildEnvironment>,
isFinal: Bool = false,
availableSize: CGSize,
animDuration: Double? = nil,
completion: @escaping () -> Void
) {
guard let component = self.component, let environment = self.environment else { return }
self.currentIds.insert(item.id)
self.lastSpawnTime = CACurrentMediaTime()
let itemView = self.itemViews[item.id] ?? ComponentView<ChildEnvironment>()
self.itemViews[item.id] = itemView
let size = itemView.update(
transition: .immediate,
component: item.component,
environment: { environment[ChildEnvironment.self] },
containerSize: availableSize
)
if let view = itemView.view {
if view.superview == nil {
if let tintColor = component.tintColor {
view.layer.layerTintColor = tintColor.cgColor
}
view.layer.allowsGroupOpacity = true
self.containerView.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: 0.0, y: -size.height - additionalInset), size: size)
let travelDistance = (size.height + maskInset + additionalInset) * 2.0
let pitch = (size.height + additionalInset)
if isFinal {
var finalFrame = view.frame
finalFrame.origin.y = maskInset
view.frame = finalFrame
let fromY = size.height + maskInset + additionalInset
let overshoot: CGFloat = 7.0
let anim = CAKeyframeAnimation(keyPath: "position.y")
anim.isAdditive = true
anim.values = [ fromY, -overshoot, 0.0 ]
anim.keyTimes = [0.0, 0.7, 1.0]
anim.timingFunctions = [
CAMediaTimingFunction(name: .easeOut),
CAMediaTimingFunction(name: .easeInEaseOut)
]
anim.duration = 0.5
CATransaction.begin()
CATransaction.setCompletionBlock {
completion()
self.currentIds.remove(item.id)
self.finishSettled()
}
view.layer.add(anim, forKey: "finalOvershoot")
CATransaction.commit()
}
else {
let duration: Double = animDuration ?? baseAnimDuration
view.layer.animatePosition(
from: CGPoint(x: 0.0, y: (size.height + maskInset + additionalInset) * 2.0),
to: .zero,
duration: duration,
timingFunction: CAMediaTimingFunctionName.linear.rawValue,
additive: true,
completion: { _ in
completion()
self.currentIds.remove(item.id)
}
)
let intervalForConstantSpacing = Double(pitch / travelDistance) * duration
self.currentInterval = intervalForConstantSpacing
}
}
self.setMotionBlurFactor(id: item.id, factor: self.motionBlurFactor, transition: .immediate)
}
private func setMotionBlurFactor(id: AnyHashable, factor: CGFloat, transition: ComponentTransition) {
guard let component = self.component, component.motionBlur, let itemView = self.itemViews[id]?.view else {
return
}
if factor != 0.0 {
let motionBlurLayer: SimpleLayer
if let current = self.motionBlurLayers[id] {
motionBlurLayer = current
} else {
motionBlurLayer = SimpleLayer()
let image = generateImage(itemView.bounds.size, rotatedContext: { size, context in
UIGraphicsPushContext(context)
defer { UIGraphicsPopContext() }
context.clear(CGRect(origin: CGPoint(), size: size))
itemView.layer.render(in: context)
})
if let image {
motionBlurLayer.contents = verticalBlurredImage(image, radius: 8.0)?.cgImage
}
motionBlurLayer.contentsScale = itemView.layer.contentsScale
self.motionBlurLayers[id] = motionBlurLayer
itemView.layer.addSublayer(motionBlurLayer)
motionBlurLayer.position = CGPoint(x: itemView.bounds.size.width * 0.5, y: itemView.bounds.size.height * 0.5)
motionBlurLayer.bounds = CGRect(origin: CGPoint(), size: itemView.bounds.size)
if let tintColor = component.tintColor {
motionBlurLayer.layerTintColor = tintColor.cgColor
}
}
let scaleFactor = 1.0 * (1.0 - factor) + 1.25 * factor
let opacityFactor = 1.0 * (1.0 - factor) + 0.6 * factor
transition.setTransform(layer: motionBlurLayer, transform: CATransform3DMakeScale(1.0, scaleFactor, 1.0))
transition.setAlpha(layer: itemView.layer, alpha: opacityFactor)
} else if let motionBlurLayer = self.motionBlurLayers[id] {
self.motionBlurLayers.removeValue(forKey: id)
transition.setAlpha(layer: motionBlurLayer, alpha: 0.0, completion: { [weak motionBlurLayer] _ in
motionBlurLayer?.removeFromSuperlayer()
})
transition.setTransform(layer: motionBlurLayer, transform: CATransform3DIdentity)
}
}
private func beginSpinning() {
self.spinState = .spinning
self.isAnimating = true
self.motionBlurFactor = 1.0
self.currentInterval = 0.1
self.ensureDisplayLink()
}
private func beginDeceleration() {
guard let component = self.component, self.spinState == .spinning || self.spinState == .decelerating else { return }
self.spinState = .decelerating
self.isAnimating = false
var queue: [AnyComponentWithIdentity<ChildEnvironment>] = []
if !component.items.isEmpty {
let shuffled = Array(component.items.shuffled().prefix(3))
queue.append(contentsOf: shuffled)
}
queue.append(AnyComponentWithIdentity(id: "final", component: component.item))
self.decelQueue = queue
self.decelTotalSteps = queue.count
self.decelStepIndex = 0
self.motionBlurFactor = max(self.motionBlurFactor, 0.0001)
self.ensureDisplayLink()
}
private func ensureDisplayLink() {
guard self.animationLink == nil else {
return
}
self.animationLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] _ in
self?.tick()
})
}
private func invalidateDisplayLinkIfIdle() {
if self.spinState == .idle || self.spinState == .settled {
self.animationLink?.invalidate()
self.animationLink = nil
}
}
private func applyEdge3DSquish() {
let H = self.containerView.bounds.height
guard H > 0 else { return }
CATransaction.begin()
CATransaction.setDisableActions(true)
for (_, cv) in self.itemViews {
guard let v = cv.view, v.superview === self.containerView else { continue }
let midY = liveMidY(in: self.containerView, of: v)
let scaleY = edgeScaleY(for: midY, containerHeight: H)
var t = CATransform3DIdentity
t = CATransform3DScale(t, 1.0, scaleY, 1.0)
v.layer.transform = t
}
CATransaction.commit()
}
private func tick() {
guard let availableSize = self.availableSize else {
return
}
let now = CACurrentMediaTime()
switch self.spinState {
case .spinning:
if let last = self.lastSpawnTime, now - last >= self.currentInterval || self.lastSpawnTime == nil {
self.spawnRandomSlot(availableSize: availableSize)
}
case .decelerating:
let t = clamp01(self.decelTotalSteps > 1 ? Double(self.decelStepIndex) / Double(self.decelTotalSteps - 1) : 1.0)
if let last = self.lastSpawnTime, now - last >= self.currentInterval {
if !self.decelQueue.isEmpty {
let next = self.decelQueue.removeFirst()
let isFinal = self.decelQueue.isEmpty
let animDuration = isFinal ? nil : lerp(baseAnimDuration, maxAnimDuration, t)
self.spawnSlot(item: next, isFinal: isFinal, availableSize: availableSize, animDuration: animDuration, completion: {})
self.motionBlurFactor = CGFloat(1.0 - t)
self.decelStepIndex += 1
}
} else if self.lastSpawnTime == nil {
if !self.decelQueue.isEmpty {
let next = self.decelQueue.removeFirst()
self.spawnSlot(item: next, availableSize: availableSize) {}
}
}
case .settled, .idle:
self.invalidateDisplayLinkIfIdle()
}
self.applyEdge3DSquish()
}
private func finishSettled() {
for (id, _) in self.motionBlurLayers {
self.setMotionBlurFactor(id: id, factor: 0.0, transition: .easeInOut(duration: 0.2))
}
self.motionBlurLayers.removeAll()
self.spinState = .settled
self.decelQueue.removeAll()
self.invalidateDisplayLinkIfIdle()
}
func update(
component: SlotsComponent,
availableSize: CGSize,
state: EmptyComponentState,
environment: Environment<ChildEnvironment>,
transition: ComponentTransition
) -> CGSize {
self.component = component
self.environment = environment
self.availableSize = availableSize
let size = component.size
self.containerView.frame = CGRect(origin: CGPoint(x: 0.0, y: component.verticalOffset), size: size).insetBy(dx: 0.0, dy: -maskInset)
self.maskLayer.frame = CGRect(origin: .zero, size: self.containerView.bounds.size)
let wasAnimating = self.isAnimating
let nowAnimating = component.isAnimating
if nowAnimating && !wasAnimating {
self.beginSpinning()
self.spawnRandomSlot(availableSize: availableSize)
} else if !nowAnimating && wasAnimating {
self.beginDeceleration()
} else if nowAnimating && self.spinState == .settled {
self.beginSpinning()
self.spawnRandomSlot(availableSize: availableSize)
}
if let tintColor = component.tintColor {
for (id, itemView) in self.itemViews {
if let itemLayer = itemView.view?.layer {
transition.setTintColor(layer: itemLayer, color: tintColor)
}
if let blurLayer = self.motionBlurLayers[id] {
transition.setTintColor(layer: blurLayer, color: tintColor)
}
}
}
return size
}
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private let minScaleYAtEdge: CGFloat = 0.7
private let squishFalloff: CGFloat = 0.12
private func smoothstep(_ x: CGFloat) -> CGFloat {
let t = max(0.0, min(1.0, x))
return t * t * (3.0 - 2.0 * t)
}
private func liveMidY(in container: UIView, of view: UIView) -> CGFloat {
if let pres = view.layer.presentation() {
let p = container.layer.convert(pres.position, from: view.layer.superlayer)
return p.y
}
return view.center.y
}
private func edgeScaleY(for midY: CGFloat, containerHeight H: CGFloat) -> CGFloat {
guard H > 0 else { return 1.0 }
let d = abs((midY - H * 0.5) / (H * 0.5))
let uRaw = (d - squishFalloff) / (1.0 - squishFalloff)
let u = smoothstep(max(0.0, min(1.0, uRaw)))
return (1.0 - u) + minScaleYAtEdge * u
}
final class SpacerComponent: Component {
let size: CGSize
init(
size: CGSize
) {
self.size = size
}
static func ==(lhs: SpacerComponent, rhs: SpacerComponent) -> Bool {
return lhs.size == rhs.size
}
final class View: UIView {
private var component: SpacerComponent?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: SpacerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
return component.size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,357 @@
import Foundation
import UIKit
import ComponentFlow
import Display
import TelegramPresentationData
import MultilineTextComponent
final class TableComponent: CombinedComponent {
class Item: Equatable {
enum TitleFont {
case regular
case bold
}
public let id: AnyHashable
public let title: String?
public let titleFont: TitleFont
public let hasBackground: Bool
public let component: AnyComponent<Empty>
public let insets: UIEdgeInsets?
public init<IdType: Hashable>(id: IdType, title: String?, titleFont: TitleFont = .regular, hasBackground: Bool = false, component: AnyComponent<Empty>, insets: UIEdgeInsets? = nil) {
self.id = AnyHashable(id)
self.title = title
self.titleFont = titleFont
self.hasBackground = hasBackground
self.component = component
self.insets = insets
}
public static func == (lhs: Item, rhs: Item) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.titleFont != rhs.titleFont {
return false
}
if lhs.hasBackground != rhs.hasBackground {
return false
}
if lhs.component != rhs.component {
return false
}
if lhs.insets != rhs.insets {
return false
}
return true
}
}
private let theme: PresentationTheme
private let items: [Item]
private let semiTransparent: Bool
public init(theme: PresentationTheme, items: [Item], semiTransparent: Bool = false) {
self.theme = theme
self.items = items
self.semiTransparent = semiTransparent
}
public static func ==(lhs: TableComponent, rhs: TableComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.items != rhs.items {
return false
}
if lhs.semiTransparent != rhs.semiTransparent {
return false
}
return true
}
final class State: ComponentState {
var cachedLastBackgroundImage: (UIImage, PresentationTheme)?
var cachedLeftColumnImage: (UIImage, PresentationTheme)?
var cachedBorderImage: (UIImage, PresentationTheme)?
}
func makeState() -> State {
return State()
}
public static var body: Body {
let leftColumnBackground = Child(Image.self)
let lastBackground = Child(Image.self)
let verticalBorder = Child(Rectangle.self)
let titleChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self)
let valueChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self)
let borderChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self)
let outerBorder = Child(Image.self)
return { context in
let verticalPadding: CGFloat = 11.0
let horizontalPadding: CGFloat = 12.0
let borderWidth: CGFloat = 1.0
let backgroundColor = context.component.theme.actionSheet.opaqueItemBackgroundColor
let borderColor = backgroundColor.mixedWith(context.component.theme.list.itemBlocksSeparatorColor, alpha: 0.6)
var secondaryBackgroundColor = context.component.theme.overallDarkAppearance ? context.component.theme.list.itemModalBlocksBackgroundColor : context.component.theme.list.itemInputField.backgroundColor
if context.component.semiTransparent {
secondaryBackgroundColor = borderColor.withMultipliedAlpha(0.5)
}
var leftColumnWidth: CGFloat = 0.0
var updatedTitleChildren: [Int: _UpdatedChildComponent] = [:]
var updatedValueChildren: [(_UpdatedChildComponent, UIEdgeInsets)] = []
var updatedBorderChildren: [_UpdatedChildComponent] = []
var i = 0
for item in context.component.items {
guard let title = item.title else {
i += 1
continue
}
let titleChild = titleChildren[item.id].update(
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: title, font: item.titleFont == .bold ? Font.semibold(15.0) : Font.regular(15.0), textColor: context.component.theme.list.itemPrimaryTextColor))
)),
availableSize: context.availableSize,
transition: context.transition
)
updatedTitleChildren[i] = titleChild
if titleChild.size.width > leftColumnWidth {
leftColumnWidth = titleChild.size.width
}
i += 1
}
leftColumnWidth = max(100.0, leftColumnWidth + horizontalPadding * 2.0)
let rightColumnWidth = context.availableSize.width - leftColumnWidth
i = 0
var rowHeights: [Int: CGFloat] = [:]
var totalHeight: CGFloat = 0.0
var innerTotalHeight: CGFloat = 0.0
var innerTotalOffset: CGFloat = 0.0
var hasRowBackground = false
var rowBackgroundIsLast = false
for item in context.component.items {
let insets: UIEdgeInsets
if let customInsets = item.insets {
insets = customInsets
} else {
insets = UIEdgeInsets(top: 0.0, left: horizontalPadding, bottom: 0.0, right: horizontalPadding)
}
var titleHeight: CGFloat = 0.0
if let titleChild = updatedTitleChildren[i] {
titleHeight = titleChild.size.height
}
let availableValueWidth: CGFloat
if titleHeight > 0.0 {
availableValueWidth = rightColumnWidth
} else {
availableValueWidth = context.availableSize.width
}
let valueChild = valueChildren[item.id].update(
component: item.component,
availableSize: CGSize(width: availableValueWidth - insets.left - insets.right, height: context.availableSize.height),
transition: context.transition
)
updatedValueChildren.append((valueChild, insets))
let rowHeight = max(40.0, max(titleHeight, valueChild.size.height) + verticalPadding * 2.0)
rowHeights[i] = rowHeight
totalHeight += rowHeight
if titleHeight > 0.0 {
innerTotalHeight += rowHeight
} else if i == 0 {
innerTotalOffset += rowHeight
}
if i < context.component.items.count - 1 {
let borderChild = borderChildren[item.id].update(
component: AnyComponent(Rectangle(color: borderColor)),
availableSize: CGSize(width: context.availableSize.width, height: borderWidth),
transition: context.transition
)
updatedBorderChildren.append(borderChild)
}
if item.hasBackground {
if i != 0 {
rowBackgroundIsLast = true
}
hasRowBackground = true
}
i += 1
}
let borderRadius: CGFloat = 14.0
if hasRowBackground {
let lastBackgroundImage: UIImage
if let (currentImage, theme) = context.state.cachedLastBackgroundImage, theme === context.component.theme {
lastBackgroundImage = currentImage
} else {
lastBackgroundImage = generateImage(CGSize(width: borderRadius * 2.0 + 4.0, height: borderRadius * 2.0 + 4.0), rotatedContext: { size, context in
let bounds = CGRect(origin: .zero, size: CGSize(width: size.width, height: size.height + borderRadius))
context.clear(bounds)
let path = CGPath(roundedRect: bounds.insetBy(dx: borderWidth / 2.0, dy: borderWidth / 2.0).insetBy(dx: 0.0, dy: rowBackgroundIsLast ? -borderRadius * 2.0 : 0.0), cornerWidth: borderRadius, cornerHeight: borderRadius, transform: nil)
context.setFillColor(secondaryBackgroundColor.cgColor)
context.addPath(path)
context.fillPath()
})!.stretchableImage(withLeftCapWidth: Int(borderRadius), topCapHeight: Int(borderRadius))
context.state.cachedLastBackgroundImage = (lastBackgroundImage, context.component.theme)
}
let lastRowHeight: CGFloat
let position: CGFloat
if !rowBackgroundIsLast {
lastRowHeight = rowHeights[0] ?? 0
position = lastRowHeight / 2.0
} else {
lastRowHeight = rowHeights[i - 1] ?? 0
position = totalHeight - lastRowHeight / 2.0
}
let lastBackground = lastBackground.update(
component: Image(image: lastBackgroundImage),
availableSize: CGSize(width: context.availableSize.width, height: lastRowHeight),
transition: context.transition
)
context.add(
lastBackground
.position(CGPoint(x: context.availableSize.width / 2.0, y: position))
)
}
let leftColumnImage: UIImage
if let (currentImage, theme) = context.state.cachedLeftColumnImage, theme === context.component.theme {
leftColumnImage = currentImage
} else {
leftColumnImage = generateImage(CGSize(width: borderRadius * 2.0 + 4.0, height: borderRadius * 2.0 + 4.0), rotatedContext: { size, context in
var bounds = CGRect(origin: .zero, size: CGSize(width: size.width + borderRadius, height: size.height))
context.clear(bounds)
var offset: CGFloat = 0.0
if hasRowBackground {
offset = rowBackgroundIsLast ? borderRadius : -borderRadius
bounds.origin.y += offset
bounds.size.height += borderRadius
}
let path = CGPath(roundedRect: bounds.insetBy(dx: borderWidth / 2.0, dy: borderWidth / 2.0), cornerWidth: borderRadius, cornerHeight: borderRadius, transform: nil)
context.setFillColor(secondaryBackgroundColor.cgColor)
context.addPath(path)
context.fillPath()
})!.stretchableImage(withLeftCapWidth: Int(borderRadius), topCapHeight: Int(borderRadius))
context.state.cachedLeftColumnImage = (leftColumnImage, context.component.theme)
}
let leftColumnBackground = leftColumnBackground.update(
component: Image(image: leftColumnImage),
availableSize: CGSize(width: leftColumnWidth, height: innerTotalHeight),
transition: context.transition
)
context.add(leftColumnBackground
.position(CGPoint(x: leftColumnWidth / 2.0, y: innerTotalOffset + innerTotalHeight / 2.0))
)
let borderImage: UIImage
if let (currentImage, theme) = context.state.cachedBorderImage, theme === context.component.theme {
borderImage = currentImage
} else {
borderImage = generateImage(CGSize(width: borderRadius * 2.0 + 4.0, height: borderRadius * 2.0 + 4.0), rotatedContext: { size, context in
let bounds = CGRect(origin: .zero, size: size)
context.clear(bounds)
let path = CGPath(roundedRect: bounds.insetBy(dx: borderWidth / 2.0, dy: borderWidth / 2.0), cornerWidth: borderRadius, cornerHeight: borderRadius, transform: nil)
context.setBlendMode(.clear)
context.addPath(path)
context.fillPath()
context.setBlendMode(.normal)
context.setStrokeColor(borderColor.cgColor)
context.setLineWidth(borderWidth)
context.addPath(path)
context.strokePath()
})!.stretchableImage(withLeftCapWidth: Int(borderRadius), topCapHeight: Int(borderRadius))
context.state.cachedBorderImage = (borderImage, context.component.theme)
}
let outerBorder = outerBorder.update(
component: Image(image: borderImage),
availableSize: CGSize(width: context.availableSize.width, height: totalHeight),
transition: context.transition
)
context.add(outerBorder
.position(CGPoint(x: context.availableSize.width / 2.0, y: totalHeight / 2.0))
)
let verticalBorder = verticalBorder.update(
component: Rectangle(color: borderColor),
availableSize: CGSize(width: borderWidth, height: innerTotalHeight),
transition: context.transition
)
context.add(
verticalBorder
.position(CGPoint(x: leftColumnWidth - borderWidth / 2.0, y: innerTotalOffset + innerTotalHeight / 2.0))
)
i = 0
var originY: CGFloat = 0.0
for (valueChild, valueInsets) in updatedValueChildren {
let rowHeight = rowHeights[i] ?? 0.0
let valueFrame: CGRect
if let titleChild = updatedTitleChildren[i] {
let titleFrame = CGRect(origin: CGPoint(x: horizontalPadding, y: originY + verticalPadding), size: titleChild.size)
context.add(titleChild
.position(titleFrame.center)
)
valueFrame = CGRect(origin: CGPoint(x: leftColumnWidth + valueInsets.left, y: originY + verticalPadding), size: valueChild.size)
} else {
if hasRowBackground && rowBackgroundIsLast {
valueFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((context.availableSize.width - valueChild.size.width) / 2.0), y: originY + verticalPadding), size: valueChild.size)
} else {
valueFrame = CGRect(origin: CGPoint(x: horizontalPadding, y: originY + verticalPadding), size: valueChild.size)
}
}
context.add(valueChild
.position(valueFrame.center)
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
if i < updatedBorderChildren.count {
let borderChild = updatedBorderChildren[i]
context.add(borderChild
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + rowHeight - borderWidth / 2.0))
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
}
originY += rowHeight
i += 1
}
return CGSize(width: context.availableSize.width, height: totalHeight)
}
}
}
@@ -0,0 +1,28 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "InfoParagraphComponent",
module_name = "InfoParagraphComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/AppBundle",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/Markdown",
"//submodules/TextFormat",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,187 @@
import Foundation
import UIKit
import Display
import TelegramPresentationData
import ComponentFlow
import MultilineTextComponent
import BundleIconComponent
import Markdown
import TextFormat
public final class InfoParagraphComponent: CombinedComponent {
let title: String
let titleColor: UIColor
let text: String
let textColor: UIColor
let accentColor: UIColor
let iconName: String
let iconColor: UIColor
let badge: String?
let action: () -> Void
public init(
title: String,
titleColor: UIColor,
text: String,
textColor: UIColor,
accentColor: UIColor,
iconName: String,
iconColor: UIColor,
badge: String? = nil,
action: @escaping () -> Void = {}
) {
self.title = title
self.titleColor = titleColor
self.text = text
self.textColor = textColor
self.accentColor = accentColor
self.iconName = iconName
self.iconColor = iconColor
self.badge = badge
self.action = action
}
public static func ==(lhs: InfoParagraphComponent, rhs: InfoParagraphComponent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.titleColor != rhs.titleColor {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.textColor != rhs.textColor {
return false
}
if lhs.accentColor != rhs.accentColor {
return false
}
if lhs.iconName != rhs.iconName {
return false
}
if lhs.iconColor != rhs.iconColor {
return false
}
if lhs.badge != rhs.badge {
return false
}
return true
}
public static var body: Body {
let title = Child(MultilineTextComponent.self)
let text = Child(MultilineTextComponent.self)
let icon = Child(BundleIconComponent.self)
let badgeBackground = Child(RoundedRectangle.self)
let badgeText = Child(MultilineTextComponent.self)
return { context in
let component = context.component
let leftInset: CGFloat = 32.0
let rightInset: CGFloat = 24.0
let textSideInset: CGFloat = leftInset + 8.0
let spacing: CGFloat = 5.0
let textTopInset: CGFloat = 9.0
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.title,
font: Font.semibold(15.0),
textColor: component.titleColor,
paragraphAlignment: .natural
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let textColor = component.textColor
let accentColor = component.accentColor
let markdownAttributes = MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
link: MarkdownAttributeSet(font: textFont, textColor: accentColor),
linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
}
)
let text = text.update(
component: MultilineTextComponent(
text: .markdown(text: component.text, attributes: markdownAttributes),
horizontalAlignment: .natural,
maximumNumberOfLines: 0,
lineSpacing: 0.2,
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { _, _ in
component.action()
}
),
availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: context.availableSize.height),
transition: .immediate
)
let icon = icon.update(
component: BundleIconComponent(
name: component.iconName,
tintColor: component.iconColor
),
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height),
transition: .immediate
)
context.add(title
.position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0))
)
if let badge = component.badge {
let badgeText = badgeText.update(
component: MultilineTextComponent(text: .plain(NSAttributedString(string: badge, font: Font.semibold(11.0), textColor: .white))),
availableSize: context.availableSize,
transition: context.transition
)
let badgeWidth = badgeText.size.width + 7.0
let badgeBackground = badgeBackground.update(
component: RoundedRectangle(
color: component.accentColor,
cornerRadius: 5.0),
availableSize: CGSize(width: badgeWidth, height: 16.0),
transition: context.transition
)
context.add(badgeBackground
.position(CGPoint(x: textSideInset + title.size.width + badgeWidth / 2.0 + 5.0, y: textTopInset + title.size.height / 2.0))
)
context.add(badgeText
.position(CGPoint(x: textSideInset + title.size.width + badgeWidth / 2.0 + 5.0, y: textTopInset + title.size.height / 2.0))
)
}
context.add(text
.position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0))
)
context.add(icon
.position(CGPoint(x: 15.0, y: textTopInset + 18.0))
)
return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 20.0)
}
}
}