mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-16 19:30:29 +00:00
Various improvements
This commit is contained in:
parent
efe304afe6
commit
a80a706379
@ -2239,7 +2239,8 @@ final class VideoChatScreenComponent: Component {
|
||||
content: LottieComponent.AppBundleContent(
|
||||
name: "anim_profilemore"
|
||||
),
|
||||
color: .white
|
||||
color: .white,
|
||||
size: CGSize(width: 34.0, height: 34.0)
|
||||
)),
|
||||
background: AnyComponent(
|
||||
GlassBackgroundComponent(size: CGSize(width: navigationButtonDiameter, height: navigationButtonDiameter), isDark: false, tintColor: .init(kind: .panel, color: panelColor))
|
||||
@ -2261,7 +2262,8 @@ final class VideoChatScreenComponent: Component {
|
||||
transition: .immediate,
|
||||
component: AnyComponent(PlainButtonComponent(
|
||||
content: AnyComponent(Image(
|
||||
image: closeButtonImage(dark: false)
|
||||
image: closeButtonImage(dark: false),
|
||||
contentMode: .center
|
||||
)),
|
||||
background: AnyComponent(
|
||||
GlassBackgroundComponent(size: CGSize(width: navigationButtonDiameter, height: navigationButtonDiameter), isDark: false, tintColor: .init(kind: .panel, color: panelColor))
|
||||
|
||||
@ -55,11 +55,12 @@ func closeButtonImage(dark: Bool) -> UIImage? {
|
||||
|
||||
context.setLineWidth(2.0)
|
||||
context.setLineCap(.round)
|
||||
context.setLineJoin(.round)
|
||||
context.setStrokeColor(UIColor.white.cgColor)
|
||||
|
||||
context.move(to: CGPoint(x: 7.0 + UIScreenPixel, y: 16.0 + UIScreenPixel))
|
||||
context.addLine(to: CGPoint(x: 14.0, y: 10.0))
|
||||
context.addLine(to: CGPoint(x: 21.0 - UIScreenPixel, y: 16.0 + UIScreenPixel))
|
||||
context.move(to: CGPoint(x: 6.0 - UIScreenPixel, y: 17.0))
|
||||
context.addLine(to: CGPoint(x: 14.0, y: 8.0 + UIScreenPixel))
|
||||
context.addLine(to: CGPoint(x: 22.0 + UIScreenPixel, y: 17.0))
|
||||
context.strokePath()
|
||||
})
|
||||
}
|
||||
|
||||
@ -440,6 +440,7 @@ public final class ButtonComponent: Component {
|
||||
private var component: ButtonComponent?
|
||||
private weak var componentState: EmptyComponentState?
|
||||
|
||||
private var containerView: UIView
|
||||
private var shimmeringView: ButtonShimmeringView?
|
||||
private var chromeView: UIImageView?
|
||||
private var contentItem: ContentItem?
|
||||
@ -447,18 +448,38 @@ public final class ButtonComponent: Component {
|
||||
private var activityIndicator: ActivityIndicator?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.containerView = UIView()
|
||||
self.containerView.clipsToBounds = true
|
||||
self.containerView.isUserInteractionEnabled = false
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.containerView)
|
||||
|
||||
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
|
||||
self.highligthedChanged = { [weak self] highlighted in
|
||||
if let self, let component = self.component, component.isEnabled {
|
||||
if highlighted {
|
||||
self.layer.removeAnimation(forKey: "opacity")
|
||||
self.alpha = 0.7
|
||||
} else {
|
||||
self.alpha = 1.0
|
||||
self.layer.animateAlpha(from: 7, to: 1.0, duration: 0.2)
|
||||
switch component.background.style {
|
||||
case .glass:
|
||||
let transition = ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut))
|
||||
if highlighted {
|
||||
let highlightedColor = component.background.color.withMultiplied(hue: 1.0, saturation: 0.77, brightness: 1.01)
|
||||
transition.setBackgroundColor(view: self.containerView, color: highlightedColor)
|
||||
transition.setScale(view: self.containerView, scale: 1.05)
|
||||
|
||||
} else {
|
||||
transition.setBackgroundColor(view: self.containerView, color: component.background.color)
|
||||
transition.setScale(view: self.containerView, scale: 1.0)
|
||||
}
|
||||
case .legacy:
|
||||
if highlighted {
|
||||
self.containerView.layer.removeAnimation(forKey: "opacity")
|
||||
self.containerView.alpha = 0.7
|
||||
} else {
|
||||
self.containerView.alpha = 1.0
|
||||
self.containerView.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -485,13 +506,13 @@ public final class ButtonComponent: Component {
|
||||
|
||||
self.isEnabled = (component.isEnabled || component.allowActionWhenDisabled) && !component.displaysProgress
|
||||
|
||||
transition.setBackgroundColor(view: self, color: component.background.color)
|
||||
transition.setBackgroundColor(view: self.containerView, color: component.background.color)
|
||||
|
||||
var cornerRadius: CGFloat = component.background.cornerRadius
|
||||
if case .glass = component.background.style, component.background.cornerRadius == 10.0 {
|
||||
cornerRadius = availableSize.height * 0.5
|
||||
}
|
||||
transition.setCornerRadius(layer: self.layer, cornerRadius: cornerRadius)
|
||||
transition.setCornerRadius(layer: self.containerView.layer, cornerRadius: cornerRadius)
|
||||
|
||||
var contentAlpha: CGFloat = 1.0
|
||||
if component.displaysProgress {
|
||||
@ -525,7 +546,7 @@ public final class ButtonComponent: Component {
|
||||
contentTransition = .immediate
|
||||
animateIn = true
|
||||
contentView.isUserInteractionEnabled = false
|
||||
self.addSubview(contentView)
|
||||
self.containerView.addSubview(contentView)
|
||||
|
||||
contentItem.view.parentState = state
|
||||
}
|
||||
@ -563,7 +584,7 @@ public final class ButtonComponent: Component {
|
||||
activityIndicator = ActivityIndicator(type: .custom(component.background.foreground, 22.0, 2.0, true))
|
||||
activityIndicator.view.alpha = 0.0
|
||||
self.activityIndicator = activityIndicator
|
||||
self.addSubview(activityIndicator.view)
|
||||
self.containerView.addSubview(activityIndicator.view)
|
||||
}
|
||||
let indicatorSize = CGSize(width: 22.0, height: 22.0)
|
||||
transition.setAlpha(view: activityIndicator.view, alpha: 1.0)
|
||||
@ -586,7 +607,7 @@ public final class ButtonComponent: Component {
|
||||
shimmeringTransition = .immediate
|
||||
shimmeringView = ButtonShimmeringView(frame: .zero)
|
||||
self.shimmeringView = shimmeringView
|
||||
self.insertSubview(shimmeringView, at: 0)
|
||||
self.containerView.insertSubview(shimmeringView, at: 0)
|
||||
}
|
||||
shimmeringView.update(size: availableSize, background: component.background, cornerRadius: component.background.cornerRadius, transition: shimmeringTransition)
|
||||
shimmeringTransition.setFrame(view: shimmeringView, frame: CGRect(origin: .zero, size: availableSize))
|
||||
@ -607,9 +628,9 @@ public final class ButtonComponent: Component {
|
||||
chromeView = UIImageView()
|
||||
self.chromeView = chromeView
|
||||
if let shimmeringView = self.shimmeringView {
|
||||
self.insertSubview(chromeView, aboveSubview: shimmeringView)
|
||||
self.containerView.insertSubview(chromeView, aboveSubview: shimmeringView)
|
||||
} else {
|
||||
self.insertSubview(chromeView, at: 0)
|
||||
self.containerView.insertSubview(chromeView, at: 0)
|
||||
}
|
||||
|
||||
chromeView.layer.compositingFilter = "overlayBlendMode"
|
||||
@ -624,6 +645,9 @@ public final class ButtonComponent: Component {
|
||||
})
|
||||
}
|
||||
|
||||
transition.setPosition(view: self.containerView, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0))
|
||||
transition.setBoundsSize(view: self.containerView, size: availableSize)
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
@ -561,6 +561,7 @@ public final class GiftItemComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
var tonButtonColor: UIColor = .clear
|
||||
if case .generic = component.mode {
|
||||
if let title = component.title {
|
||||
let titleSize = self.title.update(
|
||||
@ -630,6 +631,7 @@ public final class GiftItemComponent: Component {
|
||||
price = priceValue ?? component.strings.Gift_Options_Gift_Transfer
|
||||
tinted = true
|
||||
}
|
||||
tonButtonColor = buttonColor
|
||||
|
||||
let buttonSize = self.button.update(
|
||||
transition: transition,
|
||||
@ -657,29 +659,7 @@ public final class GiftItemComponent: Component {
|
||||
transition.setFrame(view: buttonView, frame: buttonFrame)
|
||||
}
|
||||
|
||||
if case let .uniqueGift(gift, _) = component.subject, gift.resellForTonOnly {
|
||||
let tonSize = self.ton.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
ZStack([
|
||||
AnyComponentWithIdentity(id: "background", component: AnyComponent(RoundedRectangle(color: buttonColor, cornerRadius: 12.0))),
|
||||
AnyComponentWithIdentity(id: "icon", component: AnyComponent(BundleIconComponent(name: "Premium/TonGift", tintColor: .white)))
|
||||
])
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 24.0, height: 24.0)
|
||||
)
|
||||
let tonFrame = CGRect(origin: CGPoint(x: 4.0, y: 4.0), size: tonSize)
|
||||
if let tonView = self.ton.view {
|
||||
if tonView.superview == nil {
|
||||
self.addSubview(tonView)
|
||||
}
|
||||
transition.setFrame(view: tonView, frame: tonFrame)
|
||||
}
|
||||
} else if let tonView = self.ton.view, tonView.superview != nil {
|
||||
tonView.removeFromSuperview()
|
||||
}
|
||||
|
||||
|
||||
if let label = component.label {
|
||||
let labelColor = component.theme.overallDarkAppearance ? UIColor(rgb: 0xffc337) : UIColor(rgb: 0xd3720a)
|
||||
let attributes = MarkdownAttributes(
|
||||
@ -720,6 +700,29 @@ public final class GiftItemComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
if case .generic = component.mode, case let .uniqueGift(gift, _) = component.subject, gift.resellForTonOnly {
|
||||
let tonSize = self.ton.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
ZStack([
|
||||
AnyComponentWithIdentity(id: "background", component: AnyComponent(RoundedRectangle(color: tonButtonColor, cornerRadius: 12.0))),
|
||||
AnyComponentWithIdentity(id: "icon", component: AnyComponent(BundleIconComponent(name: "Premium/TonGift", tintColor: .white)))
|
||||
])
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 24.0, height: 24.0)
|
||||
)
|
||||
let tonFrame = CGRect(origin: CGPoint(x: 4.0, y: 4.0), size: tonSize)
|
||||
if let tonView = self.ton.view {
|
||||
if tonView.superview == nil {
|
||||
self.addSubview(tonView)
|
||||
}
|
||||
transition.setFrame(view: tonView, frame: tonFrame)
|
||||
}
|
||||
} else if let tonView = self.ton.view, tonView.superview != nil {
|
||||
tonView.removeFromSuperview()
|
||||
}
|
||||
|
||||
if let ribbon = component.ribbon {
|
||||
let ribbonFontSize: CGFloat
|
||||
if case .profile = component.mode {
|
||||
@ -995,7 +998,7 @@ public final class GiftItemComponent: Component {
|
||||
} else {
|
||||
resellBackgroundTransition = .immediate
|
||||
|
||||
resellBackground = BlurredBackgroundView(color: UIColor(rgb: 0x000000, alpha: 0.3), enableBlur: true) //UIVisualEffectView(effect: blurEffect)
|
||||
resellBackground = BlurredBackgroundView(color: UIColor(rgb: 0x000000, alpha: 0.3), enableBlur: true)
|
||||
resellBackground.clipsToBounds = true
|
||||
self.resellBackground = resellBackground
|
||||
|
||||
@ -1019,7 +1022,8 @@ public final class GiftItemComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
if case .grid = component.mode {
|
||||
switch component.mode {
|
||||
case .generic, .grid:
|
||||
let lineWidth: CGFloat = 2.0
|
||||
let selectionFrame = backgroundFrame.insetBy(dx: 3.0, dy: 3.0)
|
||||
|
||||
@ -1030,7 +1034,9 @@ public final class GiftItemComponent: Component {
|
||||
} else {
|
||||
selectionLayer = SimpleShapeLayer()
|
||||
self.selectionLayer = selectionLayer
|
||||
if self.ribbon.layer.superlayer != nil {
|
||||
if self.ton.view?.superview != nil {
|
||||
self.layer.insertSublayer(selectionLayer, below: self.ton.view?.layer)
|
||||
} else if self.ribbon.layer.superlayer != nil {
|
||||
self.layer.insertSublayer(selectionLayer, below: self.ribbon.layer)
|
||||
} else {
|
||||
self.layer.addSublayer(selectionLayer)
|
||||
@ -1058,6 +1064,8 @@ public final class GiftItemComponent: Component {
|
||||
selectionLayer.removeFromSuperlayer()
|
||||
})
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if case .select = component.mode {
|
||||
|
||||
@ -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,200 @@
|
||||
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.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))
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,6 +45,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/Gifts/GiftViewScreen",
|
||||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
"//submodules/TelegramUI/Components/TextFieldComponent",
|
||||
"//submodules/TelegramUI/Components/Gifts/GiftLoadingShimmerView",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -26,6 +26,7 @@ import GiftViewScreen
|
||||
import UndoUI
|
||||
import ContextUI
|
||||
import LottieComponent
|
||||
import GiftLoadingShimmerView
|
||||
|
||||
private let minimumCountToDisplayFilters = 18
|
||||
|
||||
@ -71,7 +72,7 @@ final class GiftStoreScreenComponent: Component {
|
||||
final class View: UIView, UIScrollViewDelegate {
|
||||
private let topOverscrollLayer = SimpleLayer()
|
||||
private let scrollView: ScrollView
|
||||
private let loadingNode: LoadingShimmerNode
|
||||
private let loadingView: GiftLoadingShimmerView
|
||||
private let emptyResultsAnimation = ComponentView<Empty>()
|
||||
private let emptyResultsTitle = ComponentView<Empty>()
|
||||
private let clearFilters = ComponentView<Empty>()
|
||||
@ -88,7 +89,7 @@ final class GiftStoreScreenComponent: Component {
|
||||
private let title = ComponentView<Empty>()
|
||||
private let subtitle = ComponentView<Empty>()
|
||||
|
||||
private var starsItems: [AnyHashable: ComponentView<Empty>] = [:]
|
||||
private var giftItems: [AnyHashable: ComponentView<Empty>] = [:]
|
||||
private let filterSelector = ComponentView<Empty>()
|
||||
|
||||
private var isUpdating: Bool = false
|
||||
@ -117,13 +118,13 @@ final class GiftStoreScreenComponent: Component {
|
||||
}
|
||||
self.scrollView.alwaysBounceVertical = true
|
||||
|
||||
self.loadingNode = LoadingShimmerNode()
|
||||
self.loadingView = GiftLoadingShimmerView()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.scrollView.delegate = self
|
||||
self.addSubview(self.scrollView)
|
||||
self.addSubview(self.loadingNode.view)
|
||||
self.addSubview(self.loadingView)
|
||||
|
||||
self.scrollView.layer.addSublayer(self.topOverscrollLayer)
|
||||
}
|
||||
@ -211,14 +212,14 @@ final class GiftStoreScreenComponent: Component {
|
||||
|
||||
var itemTransition = transition
|
||||
let visibleItem: ComponentView<Empty>
|
||||
if let current = self.starsItems[itemId] {
|
||||
if let current = self.giftItems[itemId] {
|
||||
visibleItem = current
|
||||
} else {
|
||||
visibleItem = ComponentView()
|
||||
if !transition.animation.isImmediate {
|
||||
itemTransition = .immediate
|
||||
}
|
||||
self.starsItems[itemId] = visibleItem
|
||||
self.giftItems[itemId] = visibleItem
|
||||
}
|
||||
|
||||
var ribbon: GiftItemComponent.Ribbon?
|
||||
@ -235,7 +236,10 @@ final class GiftStoreScreenComponent: Component {
|
||||
color: ribbonColor
|
||||
)
|
||||
|
||||
let subject: GiftItemComponent.Subject = .uniqueGift(gift: uniqueGift, price: "# \(presentationStringsFormattedNumber(Int32(uniqueGift.resellAmounts?.first(where: { $0.currency == .stars })?.amount.value ?? 0), environment.dateTimeFormat.groupingSeparator))")
|
||||
let subject: GiftItemComponent.Subject = .uniqueGift(
|
||||
gift: uniqueGift,
|
||||
price: "# \(presentationStringsFormattedNumber(Int32(uniqueGift.resellAmounts?.first(where: { $0.currency == .stars })?.amount.value ?? 0), environment.dateTimeFormat.groupingSeparator))"
|
||||
)
|
||||
let _ = visibleItem.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(
|
||||
@ -306,7 +310,7 @@ final class GiftStoreScreenComponent: Component {
|
||||
}
|
||||
|
||||
var removeIds: [AnyHashable] = []
|
||||
for (id, item) in self.starsItems {
|
||||
for (id, item) in self.giftItems {
|
||||
if !validIds.contains(id) {
|
||||
removeIds.append(id)
|
||||
if let itemView = item.view {
|
||||
@ -322,7 +326,7 @@ final class GiftStoreScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
for id in removeIds {
|
||||
self.starsItems.removeValue(forKey: id)
|
||||
self.giftItems.removeValue(forKey: id)
|
||||
}
|
||||
}
|
||||
|
||||
@ -409,7 +413,7 @@ final class GiftStoreScreenComponent: Component {
|
||||
if view.superview == nil {
|
||||
view.alpha = 0.0
|
||||
fadeTransition.setAlpha(view: view, alpha: 1.0)
|
||||
self.insertSubview(view, belowSubview: self.loadingNode.view)
|
||||
self.insertSubview(view, belowSubview: self.loadingView)
|
||||
view.playOnce()
|
||||
}
|
||||
view.bounds = CGRect(origin: .zero, size: emptyResultsAnimationFrame.size)
|
||||
@ -419,7 +423,7 @@ final class GiftStoreScreenComponent: Component {
|
||||
if view.superview == nil {
|
||||
view.alpha = 0.0
|
||||
fadeTransition.setAlpha(view: view, alpha: 1.0)
|
||||
self.insertSubview(view, belowSubview: self.loadingNode.view)
|
||||
self.insertSubview(view, belowSubview: self.loadingView)
|
||||
}
|
||||
view.bounds = CGRect(origin: .zero, size: emptyResultsTitleFrame.size)
|
||||
ComponentTransition.immediate.setPosition(view: view, position: emptyResultsTitleFrame.center)
|
||||
@ -1206,12 +1210,12 @@ final class GiftStoreScreenComponent: Component {
|
||||
self.updateScrolling(transition: transition)
|
||||
|
||||
if isLoading && self.showLoading {
|
||||
self.loadingNode.update(size: availableSize, theme: environment.theme, showFilters: !showingFilters, transition: .immediate)
|
||||
loadingTransition.setAlpha(view: self.loadingNode.view, alpha: 1.0)
|
||||
self.loadingView.update(size: availableSize, theme: environment.theme, showFilters: !showingFilters, transition: .immediate)
|
||||
loadingTransition.setAlpha(view: self.loadingView, alpha: 1.0)
|
||||
} else {
|
||||
loadingTransition.setAlpha(view: self.loadingNode.view, alpha: 0.0)
|
||||
loadingTransition.setAlpha(view: self.loadingView, alpha: 0.0)
|
||||
}
|
||||
transition.setFrame(view: self.loadingNode.view, frame: CGRect(origin: CGPoint(x: 0.0, y: environment.navigationHeight), size: availableSize))
|
||||
transition.setFrame(view: self.loadingView, frame: CGRect(origin: CGPoint(x: 0.0, y: environment.navigationHeight), size: availableSize))
|
||||
|
||||
return availableSize
|
||||
}
|
||||
|
||||
@ -1,195 +0,0 @@
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import TelegramPresentationData
|
||||
|
||||
private final class SearchShimmerEffectNode: ASDisplayNode {
|
||||
private var currentBackgroundColor: UIColor?
|
||||
private var currentForegroundColor: UIColor?
|
||||
private let imageNodeContainer: ASDisplayNode
|
||||
private let imageNode: ASImageNode
|
||||
|
||||
private var absoluteLocation: (CGRect, CGSize)?
|
||||
private var isCurrentlyInHierarchy = false
|
||||
private var shouldBeAnimating = false
|
||||
|
||||
override init() {
|
||||
self.imageNodeContainer = ASDisplayNode()
|
||||
self.imageNodeContainer.isLayerBacked = true
|
||||
|
||||
self.imageNode = ASImageNode()
|
||||
self.imageNode.isLayerBacked = true
|
||||
self.imageNode.displaysAsynchronously = false
|
||||
self.imageNode.displayWithoutProcessing = true
|
||||
self.imageNode.contentMode = .scaleToFill
|
||||
|
||||
super.init()
|
||||
|
||||
self.isLayerBacked = true
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.imageNodeContainer.addSubnode(self.imageNode)
|
||||
self.addSubnode(self.imageNodeContainer)
|
||||
}
|
||||
|
||||
override func didEnterHierarchy() {
|
||||
super.didEnterHierarchy()
|
||||
|
||||
self.isCurrentlyInHierarchy = true
|
||||
self.updateAnimation()
|
||||
}
|
||||
|
||||
override func didExitHierarchy() {
|
||||
super.didExitHierarchy()
|
||||
|
||||
self.isCurrentlyInHierarchy = false
|
||||
self.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.imageNode.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: CGPoint(), size: size))
|
||||
|
||||
context.clip(to: CGRect(origin: CGPoint(), 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: CGGradientDrawingOptions())
|
||||
})
|
||||
}
|
||||
|
||||
func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
|
||||
if let absoluteLocation = self.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 {
|
||||
if self.shouldBeAnimating {
|
||||
self.imageNode.layer.removeAnimation(forKey: "shimmer")
|
||||
self.addImageAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
if frameUpdated {
|
||||
self.imageNodeContainer.frame = CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: containerSize)
|
||||
}
|
||||
|
||||
self.updateAnimation()
|
||||
}
|
||||
|
||||
private func updateAnimation() {
|
||||
let shouldBeAnimating = self.isCurrentlyInHierarchy && self.absoluteLocation != nil
|
||||
if shouldBeAnimating != self.shouldBeAnimating {
|
||||
self.shouldBeAnimating = shouldBeAnimating
|
||||
if shouldBeAnimating {
|
||||
self.addImageAnimation()
|
||||
} else {
|
||||
self.imageNode.layer.removeAnimation(forKey: "shimmer")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func addImageAnimation() {
|
||||
guard let containerSize = self.absoluteLocation?.1 else {
|
||||
return
|
||||
}
|
||||
let gradientHeight: CGFloat = 250.0
|
||||
self.imageNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -gradientHeight), size: CGSize(width: containerSize.width, height: gradientHeight))
|
||||
let animation = self.imageNode.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.height + gradientHeight) as NSNumber, keyPath: "position.y", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 1.3 * 1.0, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true)
|
||||
animation.repeatCount = Float.infinity
|
||||
animation.beginTime = 1.0
|
||||
self.imageNode.layer.add(animation, forKey: "shimmer")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final class LoadingShimmerNode: ASDisplayNode {
|
||||
private let backgroundColorNode: ASDisplayNode
|
||||
private let effectNode: SearchShimmerEffectNode
|
||||
private let maskNode: ASImageNode
|
||||
private var currentParams: (size: CGSize, theme: PresentationTheme, showFilters: Bool)?
|
||||
|
||||
override init() {
|
||||
self.backgroundColorNode = ASDisplayNode()
|
||||
self.effectNode = SearchShimmerEffectNode()
|
||||
self.maskNode = ASImageNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.allowsGroupOpacity = true
|
||||
self.isUserInteractionEnabled = false
|
||||
|
||||
self.addSubnode(self.backgroundColorNode)
|
||||
self.addSubnode(self.effectNode)
|
||||
self.addSubnode(self.maskNode)
|
||||
}
|
||||
|
||||
func update(size: CGSize, theme: PresentationTheme, showFilters: Bool, transition: ContainedViewLayoutTransition) {
|
||||
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.backgroundColorNode.backgroundColor = color
|
||||
|
||||
self.maskNode.image = generateImage(size, rotatedContext: { size, context in
|
||||
context.setFillColor(theme.list.blocksBackgroundColor.cgColor)
|
||||
context.fill(CGRect(origin: CGPoint(), 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 {
|
||||
context.addPath(CGPath(roundedRect: CGRect(origin: CGPoint(x: sideInset + (filterWidth + filterSpacing) * CGFloat(i), y: 0.0), size: CGSize(width: filterWidth, height: 28.0)), 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.effectNode.update(backgroundColor: color, foregroundColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4))
|
||||
self.effectNode.updateAbsoluteRect(CGRect(origin: CGPoint(), size: size), within: size)
|
||||
}
|
||||
transition.updateFrame(node: self.backgroundColorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size))
|
||||
transition.updateFrame(node: self.maskNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size))
|
||||
transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size))
|
||||
}
|
||||
}
|
||||
@ -527,7 +527,7 @@ public final class ListSectionComponent: Component {
|
||||
containerSize: CGSize(width: availableSize.width - headerSideInset * 2.0, height: availableSize.height)
|
||||
)
|
||||
if contentHeight != 0.0 {
|
||||
contentHeight += 7.0
|
||||
contentHeight += 8.0 - UIScreenPixel
|
||||
}
|
||||
if let footerView = footer.view {
|
||||
if footerView.superview == nil {
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "CollectionTabItemComponent",
|
||||
module_name = "CollectionTabItemComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/TelegramUI/Components/TabSelectorComponent",
|
||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
@ -0,0 +1,168 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import MultilineTextComponent
|
||||
import BundleIconComponent
|
||||
import TabSelectorComponent
|
||||
import EmojiTextAttachmentView
|
||||
import TextFormat
|
||||
import AccountContext
|
||||
|
||||
public final class CollectionTabItemComponent: Component {
|
||||
public typealias EnvironmentType = TabSelectorComponent.ItemEnvironment
|
||||
|
||||
public enum Icon: Equatable {
|
||||
case collection(TelegramMediaFile)
|
||||
case add
|
||||
}
|
||||
|
||||
public let context: AccountContext
|
||||
public let icon: Icon?
|
||||
public let title: String
|
||||
public let theme: PresentationTheme
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
icon: Icon?,
|
||||
title: String,
|
||||
theme: PresentationTheme
|
||||
) {
|
||||
self.context = context
|
||||
self.icon = icon
|
||||
self.title = title
|
||||
self.theme = theme
|
||||
}
|
||||
|
||||
public static func ==(lhs: CollectionTabItemComponent, rhs: CollectionTabItemComponent) -> Bool {
|
||||
if lhs.icon != rhs.icon {
|
||||
return false
|
||||
}
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let title = ComponentView<Empty>()
|
||||
private let icon = ComponentView<Empty>()
|
||||
private var iconLayer: InlineStickerItemLayer?
|
||||
|
||||
private var component: CollectionTabItemComponent?
|
||||
|
||||
func update(component: CollectionTabItemComponent, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
|
||||
let environment = environment[EnvironmentType.self].value
|
||||
|
||||
let iconSpacing: CGFloat = 3.0
|
||||
|
||||
let normalColor = component.theme.list.itemSecondaryTextColor
|
||||
let selectedColor = component.theme.list.freeTextColor
|
||||
let effectiveColor = normalColor.mixedWith(selectedColor, alpha: environment.selectionFraction)
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: component.title, font: Font.medium(14.0), textColor: effectiveColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 100.0)
|
||||
)
|
||||
|
||||
var iconOffset: CGFloat = 0.0
|
||||
var iconSize = CGSize()
|
||||
if let icon = component.icon {
|
||||
switch icon {
|
||||
case let .collection(file):
|
||||
iconSize = CGSize(width: 16.0, height: 16.0)
|
||||
|
||||
let iconLayer: InlineStickerItemLayer
|
||||
if let current = self.iconLayer {
|
||||
iconLayer = current
|
||||
} else {
|
||||
iconLayer = InlineStickerItemLayer(
|
||||
context: component.context,
|
||||
userLocation: .other,
|
||||
attemptSynchronousLoad: true,
|
||||
emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file),
|
||||
file: file,
|
||||
cache: component.context.animationCache,
|
||||
renderer: component.context.animationRenderer,
|
||||
placeholderColor: component.theme.list.mediaPlaceholderColor,
|
||||
pointSize: iconSize,
|
||||
loopCount: 1
|
||||
)
|
||||
self.layer.addSublayer(iconLayer)
|
||||
self.iconLayer = iconLayer
|
||||
}
|
||||
let iconFrame = CGRect(origin: CGPoint(x: iconOffset, y: floorToScreenPixels((titleSize.height - iconSize.height) * 0.5)), size: iconSize)
|
||||
iconLayer.frame = iconFrame
|
||||
case .add:
|
||||
iconSize = self.icon.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(BundleIconComponent(
|
||||
name: "Chat/Input/Media/PanelBadgeAdd",
|
||||
tintColor: component.theme.list.itemSecondaryTextColor
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 100.0, height: 100.0)
|
||||
)
|
||||
let iconFrame = CGRect(origin: CGPoint(x: iconOffset, y: floorToScreenPixels((titleSize.height - iconSize.height) * 0.5)), size: iconSize)
|
||||
if let iconView = self.icon.view {
|
||||
if iconView.superview == nil {
|
||||
iconView.isUserInteractionEnabled = false
|
||||
self.addSubview(iconView)
|
||||
}
|
||||
iconView.frame = iconFrame
|
||||
}
|
||||
}
|
||||
|
||||
iconOffset += iconSize.width + iconSpacing
|
||||
} else {
|
||||
if let iconLayer = self.iconLayer {
|
||||
self.iconLayer = nil
|
||||
iconLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||
iconLayer.removeFromSuperlayer()
|
||||
})
|
||||
iconLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
if let iconView = self.icon.view {
|
||||
iconView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: iconOffset, y: 0.0), size: titleSize)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
titleView.isUserInteractionEnabled = false
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
titleView.frame = titleFrame
|
||||
}
|
||||
|
||||
let size: CGSize
|
||||
if let _ = component.icon {
|
||||
size = CGSize(width: iconSize.width + iconSpacing + titleSize.width, height: titleSize.height)
|
||||
} else {
|
||||
size = titleSize
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
@ -58,6 +58,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/BottomButtonPanelComponent",
|
||||
"//submodules/PromptUI",
|
||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
|
||||
"//submodules/TelegramUI/Components/PeerInfo/CollectionTabItemComponent"
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -34,6 +34,7 @@ import BundleIconComponent
|
||||
import EmojiTextAttachmentView
|
||||
import TextFormat
|
||||
import PromptUI
|
||||
import CollectionTabItemComponent
|
||||
|
||||
public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate {
|
||||
public enum GiftCollection: Equatable {
|
||||
@ -94,12 +95,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
private var panelSeparator: ASDisplayNode?
|
||||
private var panelButton: ComponentView<Empty>?
|
||||
private var panelCheck: ComponentView<Empty>?
|
||||
|
||||
private let emptyResultsClippingView = UIView()
|
||||
private let emptyResultsAnimation = ComponentView<Empty>()
|
||||
private let emptyResultsTitle = ComponentView<Empty>()
|
||||
private let emptyResultsAction = ComponentView<Empty>()
|
||||
|
||||
|
||||
private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData)?
|
||||
|
||||
private var theme: PresentationTheme?
|
||||
@ -1479,162 +1475,6 @@ private func cancelContextGestures(view: UIView) {
|
||||
}
|
||||
}
|
||||
|
||||
private final class CollectionTabItemComponent: Component {
|
||||
typealias EnvironmentType = TabSelectorComponent.ItemEnvironment
|
||||
|
||||
enum Icon: Equatable {
|
||||
case collection(TelegramMediaFile)
|
||||
case add
|
||||
}
|
||||
|
||||
let context: AccountContext
|
||||
let icon: Icon?
|
||||
let title: String
|
||||
let theme: PresentationTheme
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
icon: Icon?,
|
||||
title: String,
|
||||
theme: PresentationTheme
|
||||
) {
|
||||
self.context = context
|
||||
self.icon = icon
|
||||
self.title = title
|
||||
self.theme = theme
|
||||
}
|
||||
|
||||
static func ==(lhs: CollectionTabItemComponent, rhs: CollectionTabItemComponent) -> Bool {
|
||||
if lhs.icon != rhs.icon {
|
||||
return false
|
||||
}
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let title = ComponentView<Empty>()
|
||||
private let icon = ComponentView<Empty>()
|
||||
private var iconLayer: InlineStickerItemLayer?
|
||||
|
||||
private var component: CollectionTabItemComponent?
|
||||
|
||||
func update(component: CollectionTabItemComponent, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
|
||||
let environment = environment[EnvironmentType.self].value
|
||||
|
||||
let iconSpacing: CGFloat = 3.0
|
||||
|
||||
let normalColor = component.theme.list.itemSecondaryTextColor
|
||||
let selectedColor = component.theme.list.freeTextColor
|
||||
let effectiveColor = normalColor.mixedWith(selectedColor, alpha: environment.selectionFraction)
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: component.title, font: Font.medium(14.0), textColor: effectiveColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 100.0)
|
||||
)
|
||||
|
||||
var iconOffset: CGFloat = 0.0
|
||||
var iconSize = CGSize()
|
||||
if let icon = component.icon {
|
||||
switch icon {
|
||||
case let .collection(file):
|
||||
iconSize = CGSize(width: 16.0, height: 16.0)
|
||||
|
||||
let iconLayer: InlineStickerItemLayer
|
||||
if let current = self.iconLayer {
|
||||
iconLayer = current
|
||||
} else {
|
||||
iconLayer = InlineStickerItemLayer(
|
||||
context: component.context,
|
||||
userLocation: .other,
|
||||
attemptSynchronousLoad: true,
|
||||
emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file),
|
||||
file: file,
|
||||
cache: component.context.animationCache,
|
||||
renderer: component.context.animationRenderer,
|
||||
placeholderColor: component.theme.list.mediaPlaceholderColor,
|
||||
pointSize: iconSize,
|
||||
loopCount: 1
|
||||
)
|
||||
self.layer.addSublayer(iconLayer)
|
||||
self.iconLayer = iconLayer
|
||||
}
|
||||
let iconFrame = CGRect(origin: CGPoint(x: iconOffset, y: floorToScreenPixels((titleSize.height - iconSize.height) * 0.5)), size: iconSize)
|
||||
iconLayer.frame = iconFrame
|
||||
case .add:
|
||||
iconSize = self.icon.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(BundleIconComponent(
|
||||
name: "Chat/Input/Media/PanelBadgeAdd",
|
||||
tintColor: component.theme.list.itemSecondaryTextColor
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 100.0, height: 100.0)
|
||||
)
|
||||
let iconFrame = CGRect(origin: CGPoint(x: iconOffset, y: floorToScreenPixels((titleSize.height - iconSize.height) * 0.5)), size: iconSize)
|
||||
if let iconView = self.icon.view {
|
||||
if iconView.superview == nil {
|
||||
iconView.isUserInteractionEnabled = false
|
||||
self.addSubview(iconView)
|
||||
}
|
||||
iconView.frame = iconFrame
|
||||
}
|
||||
}
|
||||
|
||||
iconOffset += iconSize.width + iconSpacing
|
||||
} else {
|
||||
if let iconLayer = self.iconLayer {
|
||||
self.iconLayer = nil
|
||||
iconLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||
iconLayer.removeFromSuperlayer()
|
||||
})
|
||||
iconLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
if let iconView = self.icon.view {
|
||||
iconView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: iconOffset, y: 0.0), size: titleSize)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
titleView.isUserInteractionEnabled = false
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
titleView.frame = titleFrame
|
||||
}
|
||||
|
||||
let size: CGSize
|
||||
if let _ = component.icon {
|
||||
size = CGSize(width: iconSize.width + iconSpacing + titleSize.width, height: titleSize.height)
|
||||
} else {
|
||||
size = titleSize
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private final class ContextControllerContentSourceImpl: ContextControllerContentSource {
|
||||
let controller: ViewController
|
||||
weak var sourceView: UIView?
|
||||
|
||||
@ -39,6 +39,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/ListActionItemComponent",
|
||||
"//submodules/TelegramUI/Components/Settings/ThemeCarouselItem",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/BalancedTextComponent",
|
||||
"//submodules/TelegramUI/Components/EmojiStatusSelectionComponent",
|
||||
"//submodules/TelegramUI/Components/DynamicCornerRadiusView",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
@ -55,6 +56,10 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/TabSelectorComponent",
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/TelegramUI/Components/PeerInfo/CollectionTabItemComponent",
|
||||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
"//submodules/TelegramUI/Components/Gifts/GiftItemComponent",
|
||||
"//submodules/TelegramUI/Components/Gifts/GiftLoadingShimmerView"
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -1065,6 +1065,7 @@ final class ChannelAppearanceScreenComponent: Component {
|
||||
componentTheme: environment.theme,
|
||||
strings: environment.strings,
|
||||
topInset: environment.statusBarHeight,
|
||||
bottomInset: 0.0,
|
||||
sectionId: 0,
|
||||
peer: peer,
|
||||
subtitleString: contentsData.subscriberCount.flatMap {
|
||||
|
||||
@ -2,16 +2,24 @@ import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import GiftItemComponent
|
||||
import PlainButtonComponent
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import TabSelectorComponent
|
||||
import CollectionTabItemComponent
|
||||
import LottieComponent
|
||||
import MultilineTextComponent
|
||||
import BalancedTextComponent
|
||||
import GiftLoadingShimmerView
|
||||
|
||||
final class GiftListItemComponent: Component {
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let gifts: [StarGift.UniqueGift]
|
||||
let starGifts: [StarGift]
|
||||
let selectedId: Int64?
|
||||
let selectionUpdated: (StarGift.UniqueGift) -> Void
|
||||
let tag: AnyObject?
|
||||
@ -20,6 +28,7 @@ final class GiftListItemComponent: Component {
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
gifts: [StarGift.UniqueGift],
|
||||
starGifts: [StarGift],
|
||||
selectedId: Int64?,
|
||||
selectionUpdated: @escaping (StarGift.UniqueGift) -> Void,
|
||||
tag: AnyObject?
|
||||
@ -27,6 +36,7 @@ final class GiftListItemComponent: Component {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.gifts = gifts
|
||||
self.starGifts = starGifts
|
||||
self.selectedId = selectedId
|
||||
self.selectionUpdated = selectionUpdated
|
||||
self.tag = tag
|
||||
@ -39,6 +49,9 @@ final class GiftListItemComponent: Component {
|
||||
if lhs.gifts != rhs.gifts {
|
||||
return false
|
||||
}
|
||||
if lhs.starGifts != rhs.starGifts {
|
||||
return false
|
||||
}
|
||||
if lhs.selectedId != rhs.selectedId {
|
||||
return false
|
||||
}
|
||||
@ -59,11 +72,30 @@ final class GiftListItemComponent: Component {
|
||||
return false
|
||||
}
|
||||
|
||||
private let tabSelector = ComponentView<Empty>()
|
||||
|
||||
private var selectedGiftId: Int64 = 0
|
||||
private var resaleGiftsContexts: [Int64: ResaleGiftsContext] = [:]
|
||||
private var resaleGiftsState: ResaleGiftsContext.State?
|
||||
private var resaleGiftsDisposable = MetaDisposable()
|
||||
|
||||
private let emptyResultsAnimation = ComponentView<Empty>()
|
||||
private let emptyResultsText = ComponentView<Empty>()
|
||||
private let emptyResultsAction = ComponentView<Empty>()
|
||||
|
||||
private let loadingView = GiftLoadingShimmerView()
|
||||
|
||||
private var giftItems: [AnyHashable: ComponentView<Empty>] = [:]
|
||||
|
||||
private(set) var visibleBounds: CGRect?
|
||||
|
||||
private var cachedChevronImage: (UIImage, PresentationTheme)?
|
||||
|
||||
private var component: GiftListItemComponent?
|
||||
private var state: EmptyComponentState?
|
||||
|
||||
private var isUpdating: Bool = false
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
@ -72,30 +104,288 @@ final class GiftListItemComponent: Component {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private var visibleBounds: CGRect?
|
||||
deinit {
|
||||
self.resaleGiftsDisposable.dispose()
|
||||
}
|
||||
|
||||
func updateVisibleBounds(_ bounds: CGRect) {
|
||||
self.visibleBounds = bounds
|
||||
self.state?.updated()
|
||||
if !self.isUpdating {
|
||||
self.state?.updated()
|
||||
}
|
||||
}
|
||||
|
||||
func loadMore() -> Bool {
|
||||
guard self.selectedGiftId != 0 else {
|
||||
return false
|
||||
}
|
||||
if let resaleGiftsContext = self.resaleGiftsContexts[self.selectedGiftId] {
|
||||
resaleGiftsContext.loadMore()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func setSelectedGift(id: Int64) {
|
||||
guard let component = self.component, self.selectedGiftId != id else {
|
||||
return
|
||||
}
|
||||
|
||||
self.selectedGiftId = id
|
||||
|
||||
if id == 0 {
|
||||
self.resaleGiftsState = nil
|
||||
self.resaleGiftsDisposable.set(nil)
|
||||
} else {
|
||||
let resaleGiftsContext: ResaleGiftsContext
|
||||
if let current = self.resaleGiftsContexts[id] {
|
||||
resaleGiftsContext = current
|
||||
} else {
|
||||
resaleGiftsContext = ResaleGiftsContext(account: component.context.account, giftId: id)
|
||||
self.resaleGiftsContexts[id] = resaleGiftsContext
|
||||
}
|
||||
|
||||
var isFirstTime = true
|
||||
self.resaleGiftsDisposable.set((resaleGiftsContext.state
|
||||
|> deliverOnMainQueue).start(next: { [weak self] state in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.resaleGiftsState = state
|
||||
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: isFirstTime ? .easeInOut(duration: 0.25) : .immediate)
|
||||
}
|
||||
isFirstTime = false
|
||||
}))
|
||||
}
|
||||
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .easeInOut(duration: 0.25))
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: GiftListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
self.isUpdating = false
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let sideInset: CGFloat = 16.0
|
||||
let topInset: CGFloat = 13.0
|
||||
let spacing: CGFloat = 10.0
|
||||
let itemsInRow = 3
|
||||
let rowsCount = Int(ceil(CGFloat(component.gifts.count) / CGFloat(itemsInRow)))
|
||||
|
||||
let itemWidth = floorToScreenPixels((availableSize.width - sideInset * 2.0 - spacing * CGFloat(itemsInRow - 1)) / CGFloat(itemsInRow))
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let sideInset: CGFloat = self.selectedGiftId != 0 ? 18.0 : 16.0
|
||||
let edgeInset: CGFloat = 16.0
|
||||
var topInset: CGFloat = edgeInset
|
||||
let columnSpacing: CGFloat = self.selectedGiftId != 0 ? 14.0 : 10.0
|
||||
let rowSpacing: CGFloat = 10.0
|
||||
let itemsInRow = 3
|
||||
|
||||
//TODO:localize
|
||||
var tabSelectorItems: [TabSelectorComponent.Item] = []
|
||||
tabSelectorItems.append(TabSelectorComponent.Item(
|
||||
id: AnyHashable(Int64(0)),
|
||||
title: "My Gifts"
|
||||
))
|
||||
|
||||
for gift in component.starGifts {
|
||||
guard case let .generic(gift) = gift, let title = gift.title else {
|
||||
continue
|
||||
}
|
||||
tabSelectorItems.append(TabSelectorComponent.Item(
|
||||
id: AnyHashable(gift.id),
|
||||
content: .component(AnyComponent(
|
||||
CollectionTabItemComponent(
|
||||
context: component.context,
|
||||
icon: .collection(gift.file),
|
||||
title: title,
|
||||
theme: component.theme
|
||||
)
|
||||
)),
|
||||
isReorderable: false,
|
||||
contextAction: nil
|
||||
))
|
||||
}
|
||||
|
||||
let tabSelectorSize = self.tabSelector.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(TabSelectorComponent(
|
||||
context: component.context,
|
||||
colors: TabSelectorComponent.Colors(
|
||||
foreground: component.theme.list.itemSecondaryTextColor,
|
||||
selection: component.theme.list.itemSecondaryTextColor.withMultipliedAlpha(0.15),
|
||||
simple: true
|
||||
),
|
||||
theme: component.theme,
|
||||
customLayout: TabSelectorComponent.CustomLayout(
|
||||
font: Font.medium(14.0),
|
||||
spacing: 2.0
|
||||
),
|
||||
items: tabSelectorItems,
|
||||
selectedId: AnyHashable(self.selectedGiftId),
|
||||
reorderItem: nil,
|
||||
setSelectedId: { [weak self] id in
|
||||
guard let self, let idValue = id.base as? Int64 else {
|
||||
return
|
||||
}
|
||||
self.setSelectedGift(id: idValue)
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - 14.0 * 2.0, height: 50.0)
|
||||
)
|
||||
if let tabSelectorView = self.tabSelector.view {
|
||||
if tabSelectorView.superview == nil {
|
||||
tabSelectorView.alpha = 1.0
|
||||
self.insertSubview(tabSelectorView, at: 0)
|
||||
}
|
||||
transition.setFrame(view: tabSelectorView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - tabSelectorSize.width) / 2.0), y: topInset), size: tabSelectorSize))
|
||||
|
||||
topInset += tabSelectorSize.height + 16.0
|
||||
}
|
||||
|
||||
var effectiveGifts: [StarGift.UniqueGift] = []
|
||||
var isLoading = false
|
||||
if self.selectedGiftId == 0 {
|
||||
effectiveGifts = component.gifts
|
||||
} else if let resaleGiftsState = self.resaleGiftsState {
|
||||
var uniqueGifts: [StarGift.UniqueGift] = []
|
||||
for gift in resaleGiftsState.gifts {
|
||||
if case let .unique(uniqueGift) = gift {
|
||||
uniqueGifts.append(uniqueGift)
|
||||
}
|
||||
}
|
||||
effectiveGifts = uniqueGifts
|
||||
|
||||
if effectiveGifts.isEmpty, case .loading = resaleGiftsState.dataState {
|
||||
isLoading = true
|
||||
}
|
||||
}
|
||||
|
||||
let rowsCount = Int(ceil(CGFloat(effectiveGifts.count) / CGFloat(itemsInRow)))
|
||||
let itemWidth = floorToScreenPixels((availableSize.width - sideInset * 2.0 - columnSpacing * CGFloat(itemsInRow - 1)) / CGFloat(itemsInRow))
|
||||
let itemHeight: CGFloat = self.selectedGiftId == 0 ? itemWidth : 154.0
|
||||
var validIds: [AnyHashable] = []
|
||||
var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset), size: CGSize(width: itemWidth, height: itemWidth))
|
||||
var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset), size: CGSize(width: itemWidth, height: itemHeight))
|
||||
|
||||
var contentHeight = topInset + edgeInset + itemHeight * CGFloat(rowsCount) + rowSpacing * CGFloat(rowsCount - 1)
|
||||
|
||||
let contentHeight = topInset * 2.0 + itemWidth * CGFloat(rowsCount) + spacing * CGFloat(rowsCount - 1)
|
||||
let fadeTransition: ComponentTransition = .easeInOut(duration: 0.25)
|
||||
if self.selectedGiftId == 0 && effectiveGifts.isEmpty {
|
||||
let emptyTextSpacing: CGFloat = 16.0
|
||||
let emptyAnimationHeight: CGFloat = 100.0
|
||||
|
||||
let emptyResultsAnimationSize = self.emptyResultsAnimation.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(LottieComponent(
|
||||
content: LottieComponent.AppBundleContent(name: "Style")
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: emptyAnimationHeight, height: emptyAnimationHeight)
|
||||
)
|
||||
//TODO:localize
|
||||
let emptyResultsTextSize = self.emptyResultsText.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
BalancedTextComponent(
|
||||
text: .plain(NSAttributedString(string: "You don't have any gifts you can use as styles for your profile.", font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude)
|
||||
)
|
||||
|
||||
if self.cachedChevronImage == nil || self.cachedChevronImage?.1 !== component.theme {
|
||||
self.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: component.theme.list.itemAccentColor)!, component.theme)
|
||||
}
|
||||
|
||||
let buttonAttributedString = NSMutableAttributedString(string: "Browse gifts available for purchase >", font: Font.regular(15.0), textColor: component.theme.list.itemAccentColor, paragraphAlignment: .center)
|
||||
if let range = buttonAttributedString.string.range(of: ">"), let chevronImage = self.cachedChevronImage?.0 {
|
||||
buttonAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: buttonAttributedString.string))
|
||||
}
|
||||
|
||||
let emptyResultsActionSize = self.emptyResultsAction.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
PlainButtonComponent(
|
||||
content: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(buttonAttributedString),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if case let .generic(gift) = component.starGifts.first {
|
||||
self.setSelectedGift(id: gift.id)
|
||||
}
|
||||
},
|
||||
animateScale: false
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 3.0, height: 50.0)
|
||||
)
|
||||
|
||||
let emptyTotalHeight = emptyResultsAnimationSize.height + emptyTextSpacing + emptyResultsTextSize.height + emptyTextSpacing + emptyResultsActionSize.height
|
||||
let emptyAnimationY = topInset
|
||||
|
||||
let emptyResultsAnimationFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - emptyResultsAnimationSize.width) / 2.0), y: emptyAnimationY), size: emptyResultsAnimationSize)
|
||||
let emptyResultsTextFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - emptyResultsTextSize.width) / 2.0), y: emptyResultsAnimationFrame.maxY + emptyTextSpacing), size: emptyResultsTextSize)
|
||||
let emptyResultsActionFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - emptyResultsActionSize.width) / 2.0), y: emptyResultsTextFrame.maxY + emptyTextSpacing), size: emptyResultsActionSize)
|
||||
|
||||
if let view = self.emptyResultsAnimation.view as? LottieComponent.View {
|
||||
if view.superview == nil {
|
||||
view.alpha = 0.0
|
||||
fadeTransition.setAlpha(view: view, alpha: 1.0)
|
||||
self.addSubview(view)
|
||||
view.playOnce()
|
||||
}
|
||||
view.frame = emptyResultsAnimationFrame
|
||||
}
|
||||
if let view = self.emptyResultsText.view {
|
||||
if view.superview == nil {
|
||||
view.alpha = 0.0
|
||||
fadeTransition.setAlpha(view: view, alpha: 1.0)
|
||||
self.addSubview(view)
|
||||
}
|
||||
view.frame = emptyResultsTextFrame
|
||||
}
|
||||
if let view = self.emptyResultsAction.view {
|
||||
if view.superview == nil {
|
||||
view.alpha = 0.0
|
||||
fadeTransition.setAlpha(view: view, alpha: 1.0)
|
||||
self.addSubview(view)
|
||||
}
|
||||
view.frame = emptyResultsActionFrame
|
||||
}
|
||||
|
||||
contentHeight = topInset + emptyTotalHeight + 21.0
|
||||
} else {
|
||||
if let view = self.emptyResultsAnimation.view {
|
||||
fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in
|
||||
view.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
if let view = self.emptyResultsText.view {
|
||||
fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in
|
||||
view.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
if let view = self.emptyResultsAction.view {
|
||||
fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in
|
||||
view.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var index: Int32 = 0
|
||||
for gift in component.gifts {
|
||||
for gift in effectiveGifts {
|
||||
var isVisible = false
|
||||
if let visibleBounds = self.visibleBounds, visibleBounds.intersects(itemFrame) {
|
||||
isVisible = true
|
||||
@ -115,6 +405,27 @@ final class GiftListItemComponent: Component {
|
||||
itemTransition = .immediate
|
||||
}
|
||||
|
||||
let subject: GiftItemComponent.Subject = .uniqueGift(
|
||||
gift: gift,
|
||||
price: self.selectedGiftId != 0 ? "# \(presentationStringsFormattedNumber(Int32(gift.resellAmounts?.first(where: { $0.currency == .stars })?.amount.value ?? 0), presentationData.dateTimeFormat.groupingSeparator))" : nil
|
||||
)
|
||||
|
||||
var ribbon: GiftItemComponent.Ribbon?
|
||||
if self.selectedGiftId != 0 {
|
||||
var ribbonColor: GiftItemComponent.Ribbon.Color = .blue
|
||||
for attribute in gift.attributes {
|
||||
if case let .backdrop(_, _, innerColor, outerColor, _, _, _) = attribute {
|
||||
ribbonColor = .custom(outerColor, innerColor)
|
||||
break
|
||||
}
|
||||
}
|
||||
ribbon = GiftItemComponent.Ribbon(
|
||||
text: "#\(gift.number)",
|
||||
font: .monospaced,
|
||||
color: ribbonColor
|
||||
)
|
||||
}
|
||||
|
||||
let _ = visibleItem.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(
|
||||
@ -123,13 +434,13 @@ final class GiftListItemComponent: Component {
|
||||
GiftItemComponent(
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
strings: component.context.sharedContext.currentPresentationData.with { $0 }.strings,
|
||||
strings: presentationData.strings,
|
||||
peer: nil,
|
||||
subject: .uniqueGift(gift: gift, price: nil),
|
||||
ribbon: nil,
|
||||
subject: subject,
|
||||
ribbon: ribbon,
|
||||
isHidden: false,
|
||||
isSelected: gift.id == component.selectedId,
|
||||
mode: .grid
|
||||
mode: self.selectedGiftId != 0 ? .generic : .grid
|
||||
)
|
||||
),
|
||||
effectAlignment: .center,
|
||||
@ -147,7 +458,11 @@ final class GiftListItemComponent: Component {
|
||||
)
|
||||
if let itemView = visibleItem.view {
|
||||
if itemView.superview == nil {
|
||||
self.addSubview(itemView)
|
||||
if self.loadingView.superview != nil {
|
||||
self.insertSubview(itemView, at: self.subviews.count - 2)
|
||||
} else {
|
||||
self.insertSubview(itemView, at: self.subviews.count - 1)
|
||||
}
|
||||
|
||||
if !transition.animation.isImmediate {
|
||||
itemView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25)
|
||||
@ -157,10 +472,10 @@ final class GiftListItemComponent: Component {
|
||||
itemTransition.setFrame(view: itemView, frame: itemFrame.insetBy(dx: -2.0, dy: -2.0))
|
||||
}
|
||||
}
|
||||
itemFrame.origin.x += itemFrame.width + spacing
|
||||
itemFrame.origin.x += itemFrame.width + columnSpacing
|
||||
if itemFrame.maxX > availableSize.width {
|
||||
itemFrame.origin.x = sideInset
|
||||
itemFrame.origin.y += itemFrame.height + spacing
|
||||
itemFrame.origin.y += itemFrame.height + rowSpacing
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
@ -185,6 +500,23 @@ final class GiftListItemComponent: Component {
|
||||
self.giftItems.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
let loadingTransition: ComponentTransition = .easeInOut(duration: 0.25)
|
||||
if isLoading {
|
||||
if let tabSelectorView = self.tabSelector.view {
|
||||
if self.subviews.last !== tabSelectorView || self.loadingView.superview == nil {
|
||||
self.addSubview(self.loadingView)
|
||||
self.addSubview(tabSelectorView)
|
||||
}
|
||||
}
|
||||
contentHeight = 568.0
|
||||
let loadingSize = CGSize(width: availableSize.width, height: contentHeight)
|
||||
self.loadingView.update(size: loadingSize, theme: component.theme, isPlain: true, transition: .immediate)
|
||||
transition.setFrame(view: self.loadingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset - 50.0), size: loadingSize))
|
||||
loadingTransition.setAlpha(view: self.loadingView, alpha: 1.0)
|
||||
} else {
|
||||
loadingTransition.setAlpha(view: self.loadingView, alpha: 0.0)
|
||||
}
|
||||
|
||||
return CGSize(width: availableSize.width, height: contentHeight)
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,6 +24,7 @@ final class PeerNameColorProfilePreviewItem: ListViewItem, ItemListItem, ListIte
|
||||
let componentTheme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let topInset: CGFloat
|
||||
let bottomInset: CGFloat
|
||||
let sectionId: ItemListSectionId
|
||||
let peer: EnginePeer?
|
||||
let subtitleString: String?
|
||||
@ -31,12 +32,13 @@ final class PeerNameColorProfilePreviewItem: ListViewItem, ItemListItem, ListIte
|
||||
let nameDisplayOrder: PresentationPersonNameOrder
|
||||
let showBackground: Bool
|
||||
|
||||
init(context: AccountContext, theme: PresentationTheme, componentTheme: PresentationTheme, strings: PresentationStrings, topInset: CGFloat, sectionId: ItemListSectionId, peer: EnginePeer?, subtitleString: String? = nil, files: [Int64: TelegramMediaFile], nameDisplayOrder: PresentationPersonNameOrder, showBackground: Bool) {
|
||||
init(context: AccountContext, theme: PresentationTheme, componentTheme: PresentationTheme, strings: PresentationStrings, topInset: CGFloat, bottomInset: CGFloat, sectionId: ItemListSectionId, peer: EnginePeer?, subtitleString: String? = nil, files: [Int64: TelegramMediaFile], nameDisplayOrder: PresentationPersonNameOrder, showBackground: Bool) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.componentTheme = componentTheme
|
||||
self.strings = strings
|
||||
self.topInset = topInset
|
||||
self.bottomInset = bottomInset
|
||||
self.sectionId = sectionId
|
||||
self.peer = peer
|
||||
self.subtitleString = subtitleString
|
||||
@ -150,7 +152,7 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode {
|
||||
return { [weak self] item, params, neighbors in
|
||||
let separatorHeight = UIScreenPixel
|
||||
|
||||
let contentSize = CGSize(width: params.width, height: 210.0 + item.topInset)
|
||||
let contentSize = CGSize(width: params.width, height: 210.0 + item.topInset + item.bottomInset)
|
||||
var insets = itemListNeighborsGroupedInsets(neighbors, params)
|
||||
if params.width <= 320.0 {
|
||||
insets.top = 0.0
|
||||
@ -169,7 +171,7 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode {
|
||||
}
|
||||
self.item = item
|
||||
|
||||
self.backgroundNode.backgroundColor = item.theme.rootController.navigationBar.opaqueBackgroundColor
|
||||
self.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
|
||||
self.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
|
||||
self.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
|
||||
|
||||
|
||||
@ -35,12 +35,21 @@ import TabSelectorComponent
|
||||
import WallpaperResources
|
||||
import EdgeEffect
|
||||
import TextFormat
|
||||
import TelegramStringFormatting
|
||||
|
||||
private let giftListTag = GenericComponentViewTag()
|
||||
|
||||
final class UserAppearanceScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
public final class TransitionHint {
|
||||
public let animateTabChange: Bool
|
||||
|
||||
public init(animateTabChange: Bool) {
|
||||
self.animateTabChange = animateTabChange
|
||||
}
|
||||
}
|
||||
|
||||
let context: AccountContext
|
||||
|
||||
init(
|
||||
@ -59,13 +68,16 @@ final class UserAppearanceScreenComponent: Component {
|
||||
private final class ContentsData {
|
||||
let peer: EnginePeer?
|
||||
let gifts: [StarGift.UniqueGift]
|
||||
let starGifts: [StarGift]
|
||||
|
||||
init(
|
||||
peer: EnginePeer?,
|
||||
gifts: [StarGift.UniqueGift]
|
||||
gifts: [StarGift.UniqueGift],
|
||||
starGifts: [StarGift]
|
||||
) {
|
||||
self.peer = peer
|
||||
self.gifts = gifts
|
||||
self.starGifts = starGifts
|
||||
}
|
||||
|
||||
static func get(context: AccountContext) -> Signal<ContentsData, NoError> {
|
||||
@ -73,9 +85,10 @@ final class UserAppearanceScreenComponent: Component {
|
||||
context.engine.data.subscribe(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)
|
||||
),
|
||||
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudUniqueStarGifts], namespaces: [Namespaces.ItemCollection.CloudDice], aroundIndex: nil, count: 10000000)
|
||||
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudUniqueStarGifts], namespaces: [Namespaces.ItemCollection.CloudDice], aroundIndex: nil, count: 10000000),
|
||||
context.engine.payments.cachedStarGifts()
|
||||
)
|
||||
|> map { peer, view -> ContentsData in
|
||||
|> map { peer, view, starGifts -> ContentsData in
|
||||
var gifts: [StarGift.UniqueGift] = []
|
||||
for orderedView in view.orderedItemListsViews {
|
||||
if orderedView.collectionId == Namespaces.OrderedItemList.CloudUniqueStarGifts {
|
||||
@ -89,7 +102,8 @@ final class UserAppearanceScreenComponent: Component {
|
||||
}
|
||||
return ContentsData(
|
||||
peer: peer,
|
||||
gifts: gifts
|
||||
gifts: gifts,
|
||||
starGifts: starGifts ?? []
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -142,6 +156,7 @@ final class UserAppearanceScreenComponent: Component {
|
||||
}
|
||||
|
||||
final class View: UIView, UIScrollViewDelegate {
|
||||
private let containerView = UIView()
|
||||
private let topOverscrollLayer = SimpleLayer()
|
||||
private let scrollView: ScrollView
|
||||
private let actionButton = ComponentView<Empty>()
|
||||
@ -181,9 +196,11 @@ final class UserAppearanceScreenComponent: Component {
|
||||
|
||||
private var cachedIconFiles: [Int64: TelegramMediaFile] = [:]
|
||||
|
||||
private var selectedNameGift: StarGift.UniqueGift?
|
||||
private var updatedPeerNameColor: PeerColor?
|
||||
private var updatedPeerNameEmoji: Int64??
|
||||
|
||||
private var selectedProfileGift: StarGift.UniqueGift?
|
||||
private var updatedPeerProfileColor: PeerNameColor??
|
||||
private var updatedPeerProfileEmoji: Int64??
|
||||
private var updatedPeerStatus: PeerEmojiStatus??
|
||||
@ -198,6 +215,9 @@ final class UserAppearanceScreenComponent: Component {
|
||||
private weak var emojiStatusSelectionController: ViewController?
|
||||
|
||||
private var cachedChevronImage: (UIImage, PresentationTheme)?
|
||||
private var cachedStarImage: (UIImage, PresentationTheme)?
|
||||
private var cachedSubtitleStarImage: (UIImage, PresentationTheme)?
|
||||
private var cachedTonImage: (UIImage, PresentationTheme)?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.scrollView = ScrollView()
|
||||
@ -216,8 +236,10 @@ final class UserAppearanceScreenComponent: Component {
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.containerView)
|
||||
|
||||
self.scrollView.delegate = self
|
||||
self.addSubview(self.scrollView)
|
||||
self.containerView.addSubview(self.scrollView)
|
||||
|
||||
self.scrollView.layer.addSublayer(self.topOverscrollLayer)
|
||||
|
||||
@ -308,18 +330,32 @@ final class UserAppearanceScreenComponent: Component {
|
||||
if let giftListView = self.profileGiftsSection.findTaggedView(tag: giftListTag) as? GiftListItemComponent.View {
|
||||
let rect = self.scrollView.convert(self.scrollView.bounds, to: giftListView)
|
||||
let visibleRect = giftListView.bounds.intersection(rect)
|
||||
giftListView.updateVisibleBounds(visibleRect)
|
||||
if !self.isUpdating {
|
||||
giftListView.updateVisibleBounds(visibleRect)
|
||||
} else if giftListView.visibleBounds == nil {
|
||||
Queue.mainQueue().justDispatch {
|
||||
giftListView.updateVisibleBounds(visibleRect)
|
||||
}
|
||||
}
|
||||
}
|
||||
case .name:
|
||||
if let giftListView = self.nameGiftsSection.findTaggedView(tag: giftListTag) as? GiftListItemComponent.View {
|
||||
let rect = self.scrollView.convert(self.scrollView.bounds, to: giftListView)
|
||||
let visibleRect = giftListView.bounds.intersection(rect)
|
||||
giftListView.updateVisibleBounds(visibleRect)
|
||||
}
|
||||
|
||||
let bottomContentOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height)
|
||||
if bottomContentOffset < 320.0 {
|
||||
self.starGiftsContext?.loadMore()
|
||||
if !self.isUpdating {
|
||||
giftListView.updateVisibleBounds(visibleRect)
|
||||
} else if giftListView.visibleBounds == nil {
|
||||
Queue.mainQueue().justDispatch {
|
||||
giftListView.updateVisibleBounds(visibleRect)
|
||||
}
|
||||
}
|
||||
|
||||
let bottomContentOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height)
|
||||
if bottomContentOffset < 320.0 {
|
||||
if !giftListView.loadMore() {
|
||||
self.starGiftsContext?.loadMore()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -599,6 +635,7 @@ final class UserAppearanceScreenComponent: Component {
|
||||
if case .collectible = resolvedState.nameColor {
|
||||
self.updatedPeerNameColor = .preset(.blue)
|
||||
}
|
||||
self.selectedNameGift = nil
|
||||
if let result {
|
||||
self.updatedPeerNameEmoji = result.fileId.id
|
||||
} else {
|
||||
@ -671,13 +708,21 @@ final class UserAppearanceScreenComponent: Component {
|
||||
let environment = environment[EnvironmentType.self].value
|
||||
let themeUpdated = self.environment?.theme !== environment.theme
|
||||
self.environment = environment
|
||||
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let theme = environment.theme
|
||||
|
||||
var animateTabChange = false
|
||||
if let hint = transition.userData(TransitionHint.self) {
|
||||
animateTabChange = hint.animateTabChange
|
||||
}
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
if themeUpdated {
|
||||
self.backgroundColor = environment.theme.list.blocksBackgroundColor
|
||||
self.scrollView.backgroundColor = environment.theme.list.blocksBackgroundColor
|
||||
}
|
||||
|
||||
if self.cachedChevronImage == nil || self.cachedChevronImage?.1 !== environment.theme {
|
||||
@ -722,7 +767,7 @@ final class UserAppearanceScreenComponent: Component {
|
||||
guard let contentsData = self.contentsData, var peer = contentsData.peer, let resolvedState = self.resolveState() else {
|
||||
return availableSize
|
||||
}
|
||||
|
||||
|
||||
if let currentTheme = self.currentTheme, (self.resolvedCurrentTheme?.reference != currentTheme || self.resolvedCurrentTheme?.isDark != environment.theme.overallDarkAppearance), (self.resolvingCurrentTheme?.reference != currentTheme || self.resolvingCurrentTheme?.isDark != environment.theme.overallDarkAppearance) {
|
||||
self.resolvingCurrentTheme?.disposable.dispose()
|
||||
|
||||
@ -785,20 +830,39 @@ final class UserAppearanceScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
var previewTransition = transition
|
||||
let transitionScale = (availableSize.height - 3.0) / availableSize.height
|
||||
if animateTabChange, let snapshotView = self.containerView.snapshotView(afterScreenUpdates: false) {
|
||||
self.insertSubview(snapshotView, belowSubview: self.containerView)
|
||||
snapshotView.layer.animateScale(from: 1.0, to: transitionScale, duration: 0.12, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { completed in
|
||||
snapshotView.removeFromSuperview()
|
||||
})
|
||||
|
||||
self.scrollView.contentOffset = CGPoint(x: 0.0, y: 0.0)
|
||||
|
||||
self.containerView.layer.animateScale(from: transitionScale, to: 1.0, duration: 0.15, delay: 0.1, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.containerView.layer.allowsGroupOpacity = true
|
||||
self.containerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, completion: { completed in
|
||||
self.containerView.layer.allowsGroupOpacity = false
|
||||
})
|
||||
previewTransition = .immediate
|
||||
}
|
||||
|
||||
let tabSelectorSize = self.tabSelector.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
TabSelectorComponent(
|
||||
colors: TabSelectorComponent.Colors(
|
||||
foreground: environment.theme.list.itemSecondaryTextColor,
|
||||
selection: environment.theme.list.itemSecondaryTextColor.withMultipliedAlpha(0.15),
|
||||
foreground: environment.theme.list.itemAccentColor,
|
||||
selection: environment.theme.list.itemAccentColor.withMultipliedAlpha(0.1),
|
||||
normal: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.78),
|
||||
simple: true
|
||||
),
|
||||
theme: environment.theme,
|
||||
customLayout: TabSelectorComponent.CustomLayout(font: Font.semibold(16.0)),
|
||||
items: [
|
||||
TabSelectorComponent.Item(id: Section.profile.rawValue, title: "Profile"),
|
||||
TabSelectorComponent.Item(id: Section.name.rawValue, title: "Name")
|
||||
TabSelectorComponent.Item(id: Section.profile.rawValue, title: environment.strings.ProfileColorSetup_TitleProfile),
|
||||
TabSelectorComponent.Item(id: Section.name.rawValue, title: environment.strings.ProfileColorSetup_TitleName)
|
||||
],
|
||||
selectedId: self.currentSection.rawValue,
|
||||
setSelectedId: { [weak self] value in
|
||||
@ -806,8 +870,11 @@ final class UserAppearanceScreenComponent: Component {
|
||||
return
|
||||
}
|
||||
if let intValue = value.base as? Int32 {
|
||||
self.currentSection = Section(rawValue: intValue) ?? .profile
|
||||
self.state?.updated(transition: .easeInOut(duration: 0.3))
|
||||
let updatedSection = Section(rawValue: intValue) ?? .profile
|
||||
if self.currentSection != updatedSection {
|
||||
self.currentSection = updatedSection
|
||||
self.state?.updated(transition: .easeInOut(duration: 0.3).withUserData(TransitionHint(animateTabChange: true)))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
@ -850,30 +917,23 @@ final class UserAppearanceScreenComponent: Component {
|
||||
if let nameGiftsSectionView = self.nameGiftsSection.view, nameGiftsSectionView.superview != nil {
|
||||
nameGiftsSectionView.removeFromSuperview()
|
||||
}
|
||||
|
||||
var hasHeaderColor = false
|
||||
if resolvedState.profileColor != nil {
|
||||
hasHeaderColor = true
|
||||
}
|
||||
if case .starGift = resolvedState.emojiStatus?.content {
|
||||
hasHeaderColor = true
|
||||
}
|
||||
|
||||
|
||||
let profilePreviewSize = self.profilePreview.update(
|
||||
transition: transition,
|
||||
transition: previewTransition,
|
||||
component: AnyComponent(TopBottomCornersComponent(topCornerRadius: itemCornerRadius, bottomCornerRadius: !self.scrolledUp ? itemCornerRadius : 0.0, component: AnyComponent(ListItemComponentAdaptor(
|
||||
itemGenerator: PeerNameColorProfilePreviewItem(
|
||||
context: component.context,
|
||||
theme: environment.theme,
|
||||
componentTheme: environment.theme,
|
||||
strings: environment.strings,
|
||||
topInset: 0.0,
|
||||
topInset: 28.0,
|
||||
bottomInset: 15.0 + UIScreenPixel,
|
||||
sectionId: 0,
|
||||
peer: peer,
|
||||
subtitleString: environment.strings.Presence_online,
|
||||
files: self.cachedIconFiles,
|
||||
nameDisplayOrder: presentationData.nameDisplayOrder,
|
||||
showBackground: false
|
||||
showBackground: true
|
||||
),
|
||||
params: ListViewItemLayoutParams(width: availableSize.width - sideInset * 2.0, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true)
|
||||
)))),
|
||||
@ -883,7 +943,8 @@ final class UserAppearanceScreenComponent: Component {
|
||||
let profilePreviewFrame = CGRect(origin: CGPoint(x: sideInset, y: environment.navigationHeight + 12.0), size: profilePreviewSize)
|
||||
if let profilePreviewView = self.profilePreview.view {
|
||||
if profilePreviewView.superview == nil {
|
||||
self.addSubview(profilePreviewView)
|
||||
profilePreviewView.isUserInteractionEnabled = false
|
||||
self.containerView.addSubview(profilePreviewView)
|
||||
}
|
||||
transition.setFrame(view: profilePreviewView, frame: profilePreviewFrame)
|
||||
}
|
||||
@ -918,7 +979,6 @@ final class UserAppearanceScreenComponent: Component {
|
||||
component: AnyComponent(ListSectionComponent(
|
||||
theme: environment.theme,
|
||||
style: .glass,
|
||||
background: .range(from: 0, corners: DynamicCornerRadiusView.Corners(minXMinY: !hasHeaderColor ? itemCornerRadius : 0.0, maxXMinY: !hasHeaderColor ? itemCornerRadius : 0.0, minXMaxY: itemCornerRadius, maxXMaxY: itemCornerRadius)),
|
||||
header: nil,
|
||||
footer: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(previewFooterText),
|
||||
@ -937,7 +997,7 @@ final class UserAppearanceScreenComponent: Component {
|
||||
return
|
||||
}
|
||||
self.currentSection = .name
|
||||
self.state?.updated(transition: .easeInOut(duration: 0.3))
|
||||
self.state?.updated(transition: .easeInOut(duration: 0.3).withUserData(TransitionHint(animateTabChange: true)))
|
||||
}
|
||||
)),
|
||||
items: [
|
||||
@ -951,6 +1011,7 @@ final class UserAppearanceScreenComponent: Component {
|
||||
guard let self, let value, let resolvedState = self.resolveState() else {
|
||||
return
|
||||
}
|
||||
self.selectedProfileGift = nil
|
||||
self.updatedPeerProfileColor = value
|
||||
if case .starGift = resolvedState.emojiStatus?.content {
|
||||
self.updatedPeerStatus = .some(nil)
|
||||
@ -1025,7 +1086,7 @@ final class UserAppearanceScreenComponent: Component {
|
||||
guard let self, let resolvedState = self.resolveState() else {
|
||||
return
|
||||
}
|
||||
|
||||
self.selectedProfileGift = nil
|
||||
self.updatedPeerProfileColor = .some(nil)
|
||||
self.updatedPeerProfileEmoji = .some(nil)
|
||||
if case .starGift = resolvedState.emojiStatus?.content {
|
||||
@ -1062,92 +1123,87 @@ final class UserAppearanceScreenComponent: Component {
|
||||
contentHeight += sectionSpacing
|
||||
}
|
||||
|
||||
if !contentsData.gifts.isEmpty {
|
||||
var selectedGiftId: Int64?
|
||||
if let status = resolvedState.emojiStatus, case let .starGift(id, _, _, _, _, _, _, _, _) = status.content {
|
||||
selectedGiftId = id
|
||||
}
|
||||
let giftsSectionSize = self.profileGiftsSection.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ListSectionComponent(
|
||||
theme: environment.theme,
|
||||
style: .glass,
|
||||
header: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: environment.strings.NameColor_GiftTitle,
|
||||
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
||||
textColor: environment.theme.list.freeTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
footer: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: environment.strings.NameColor_GiftInfo,
|
||||
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
||||
textColor: environment.theme.list.freeTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
items: [
|
||||
AnyComponentWithIdentity(id: 0, component: AnyComponent(
|
||||
GiftListItemComponent(
|
||||
context: component.context,
|
||||
theme: environment.theme,
|
||||
gifts: contentsData.gifts,
|
||||
selectedId: selectedGiftId,
|
||||
selectionUpdated: { [weak self] gift in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
var fileId: Int64?
|
||||
var patternFileId: Int64?
|
||||
var innerColor: Int32?
|
||||
var outerColor: Int32?
|
||||
var patternColor: Int32?
|
||||
var textColor: Int32?
|
||||
for attribute in gift.attributes {
|
||||
switch attribute {
|
||||
case let .model(_, file, _):
|
||||
fileId = file.fileId.id
|
||||
self.cachedIconFiles[file.fileId.id] = file
|
||||
case let .pattern(_, file, _):
|
||||
patternFileId = file.fileId.id
|
||||
self.cachedIconFiles[file.fileId.id] = file
|
||||
case let .backdrop(_, _, innerColorValue, outerColorValue, patternColorValue, textColorValue, _):
|
||||
innerColor = innerColorValue
|
||||
outerColor = outerColorValue
|
||||
patternColor = patternColorValue
|
||||
textColor = textColorValue
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
if let fileId, let patternFileId, let innerColor, let outerColor, let patternColor, let textColor {
|
||||
self.updatedPeerProfileColor = .some(nil)
|
||||
self.updatedPeerProfileEmoji = .some(nil)
|
||||
self.updatedPeerStatus = .some(PeerEmojiStatus(content: .starGift(id: gift.id, fileId: fileId, title: gift.title, slug: gift.slug, patternFileId: patternFileId, innerColor: innerColor, outerColor: outerColor, patternColor: patternColor, textColor: textColor), expirationDate: nil))
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
},
|
||||
tag: giftListTag
|
||||
)
|
||||
)),
|
||||
],
|
||||
displaySeparators: false
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
||||
)
|
||||
let giftsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: giftsSectionSize)
|
||||
if let giftsSectionView = self.profileGiftsSection.view {
|
||||
if giftsSectionView.superview == nil {
|
||||
self.scrollView.addSubview(giftsSectionView)
|
||||
}
|
||||
transition.setFrame(view: giftsSectionView, frame: giftsSectionFrame)
|
||||
}
|
||||
contentHeight += giftsSectionSize.height
|
||||
contentHeight += sectionSpacing
|
||||
var selectedGiftId: Int64?
|
||||
if let status = resolvedState.emojiStatus, case let .starGift(id, _, _, _, _, _, _, _, _) = status.content {
|
||||
selectedGiftId = id
|
||||
}
|
||||
//TODO:localize
|
||||
self.profileGiftsSection.parentState = self.state
|
||||
let giftsSectionSize = self.profileGiftsSection.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ListSectionComponent(
|
||||
theme: environment.theme,
|
||||
style: .glass,
|
||||
header: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: "Your Gifts".uppercased(),
|
||||
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
||||
textColor: environment.theme.list.freeTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
footer: nil,
|
||||
items: [
|
||||
AnyComponentWithIdentity(id: 0, component: AnyComponent(
|
||||
GiftListItemComponent(
|
||||
context: component.context,
|
||||
theme: environment.theme,
|
||||
gifts: contentsData.gifts,
|
||||
starGifts: contentsData.starGifts,
|
||||
selectedId: selectedGiftId,
|
||||
selectionUpdated: { [weak self] gift in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
var fileId: Int64?
|
||||
var patternFileId: Int64?
|
||||
var innerColor: Int32?
|
||||
var outerColor: Int32?
|
||||
var patternColor: Int32?
|
||||
var textColor: Int32?
|
||||
for attribute in gift.attributes {
|
||||
switch attribute {
|
||||
case let .model(_, file, _):
|
||||
fileId = file.fileId.id
|
||||
self.cachedIconFiles[file.fileId.id] = file
|
||||
case let .pattern(_, file, _):
|
||||
patternFileId = file.fileId.id
|
||||
self.cachedIconFiles[file.fileId.id] = file
|
||||
case let .backdrop(_, _, innerColorValue, outerColorValue, patternColorValue, textColorValue, _):
|
||||
innerColor = innerColorValue
|
||||
outerColor = outerColorValue
|
||||
patternColor = patternColorValue
|
||||
textColor = textColorValue
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
if let fileId, let patternFileId, let innerColor, let outerColor, let patternColor, let textColor {
|
||||
self.selectedProfileGift = gift
|
||||
self.updatedPeerProfileColor = .some(nil)
|
||||
self.updatedPeerProfileEmoji = .some(nil)
|
||||
self.updatedPeerStatus = .some(PeerEmojiStatus(content: .starGift(id: gift.id, fileId: fileId, title: gift.title, slug: gift.slug, patternFileId: patternFileId, innerColor: innerColor, outerColor: outerColor, patternColor: patternColor, textColor: textColor), expirationDate: nil))
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
},
|
||||
tag: giftListTag
|
||||
)
|
||||
)),
|
||||
],
|
||||
displaySeparators: false
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
||||
)
|
||||
let giftsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: giftsSectionSize)
|
||||
if let giftsSectionView = self.profileGiftsSection.view {
|
||||
if giftsSectionView.superview == nil {
|
||||
self.scrollView.addSubview(giftsSectionView)
|
||||
}
|
||||
transition.setFrame(view: giftsSectionView, frame: giftsSectionFrame)
|
||||
}
|
||||
contentHeight += giftsSectionSize.height
|
||||
contentHeight += sectionSpacing
|
||||
case .name:
|
||||
var transition = transition
|
||||
if self.namePreview.view == nil {
|
||||
@ -1208,7 +1264,7 @@ final class UserAppearanceScreenComponent: Component {
|
||||
}
|
||||
|
||||
let namePreviewSize = self.namePreview.update(
|
||||
transition: transition,
|
||||
transition: previewTransition,
|
||||
component: AnyComponent(TopBottomCornersComponent(topCornerRadius: itemCornerRadius, bottomCornerRadius: !self.scrolledUp ? itemCornerRadius : 0.0, component: AnyComponent(ListItemComponentAdaptor(
|
||||
itemGenerator: PeerNameColorChatPreviewItem(
|
||||
context: component.context,
|
||||
@ -1231,7 +1287,8 @@ final class UserAppearanceScreenComponent: Component {
|
||||
let namePreviewFrame = CGRect(origin: CGPoint(x: sideInset, y: environment.navigationHeight + 12.0), size: namePreviewSize)
|
||||
if let namePreviewView = self.namePreview.view {
|
||||
if namePreviewView.superview == nil {
|
||||
self.addSubview(namePreviewView)
|
||||
namePreviewView.isUserInteractionEnabled = false
|
||||
self.containerView.addSubview(namePreviewView)
|
||||
}
|
||||
transition.setFrame(view: namePreviewView, frame: namePreviewFrame)
|
||||
}
|
||||
@ -1267,6 +1324,7 @@ final class UserAppearanceScreenComponent: Component {
|
||||
self.updatedPeerNameEmoji = .some(nil)
|
||||
}
|
||||
self.updatedPeerNameColor = .preset(value)
|
||||
self.selectedNameGift = nil
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
},
|
||||
sectionId: 0
|
||||
@ -1308,83 +1366,141 @@ final class UserAppearanceScreenComponent: Component {
|
||||
contentHeight += nameColorSectionSize.height
|
||||
contentHeight += sectionSpacing
|
||||
|
||||
if !self.starGifts.isEmpty {
|
||||
var selectedGiftId: Int64?
|
||||
if case let .collectible(collectibleColor) = resolvedState.nameColor {
|
||||
selectedGiftId = collectibleColor.collectibleId
|
||||
}
|
||||
let giftsSectionSize = self.nameGiftsSection.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ListSectionComponent(
|
||||
theme: environment.theme,
|
||||
style: .glass,
|
||||
header: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: environment.strings.NameColor_GiftTitle,
|
||||
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
||||
textColor: environment.theme.list.freeTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
footer: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: environment.strings.NameColor_GiftInfo,
|
||||
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
||||
textColor: environment.theme.list.freeTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
items: [
|
||||
AnyComponentWithIdentity(id: 0, component: AnyComponent(
|
||||
GiftListItemComponent(
|
||||
context: component.context,
|
||||
theme: environment.theme,
|
||||
gifts: self.starGifts,
|
||||
selectedId: selectedGiftId,
|
||||
selectionUpdated: { [weak self] gift in
|
||||
guard let self, let peerColor = gift.peerColor else {
|
||||
return
|
||||
}
|
||||
|
||||
self.updatedPeerNameColor = .collectible(peerColor)
|
||||
self.updatedPeerNameEmoji = peerColor.backgroundEmojiId
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
},
|
||||
tag: giftListTag
|
||||
)
|
||||
)),
|
||||
],
|
||||
displaySeparators: false
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
||||
)
|
||||
let giftsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: giftsSectionSize)
|
||||
if let giftsSectionView = self.nameGiftsSection.view {
|
||||
if giftsSectionView.superview == nil {
|
||||
self.scrollView.addSubview(giftsSectionView)
|
||||
}
|
||||
transition.setFrame(view: giftsSectionView, frame: giftsSectionFrame)
|
||||
}
|
||||
contentHeight += giftsSectionSize.height
|
||||
contentHeight += sectionSpacing
|
||||
var selectedGiftId: Int64?
|
||||
if case let .collectible(collectibleColor) = resolvedState.nameColor {
|
||||
selectedGiftId = collectibleColor.collectibleId
|
||||
}
|
||||
//TODO:localize
|
||||
|
||||
var peerColorStarGifts: [StarGift] = []
|
||||
for gift in contentsData.starGifts {
|
||||
if case let .generic(genericGift) = gift, genericGift.flags.contains(.peerColorAvailable), let resale = genericGift.availability?.resale, resale > 0 {
|
||||
peerColorStarGifts.append(gift)
|
||||
}
|
||||
}
|
||||
|
||||
self.nameGiftsSection.parentState = self.state
|
||||
let giftsSectionSize = self.nameGiftsSection.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ListSectionComponent(
|
||||
theme: environment.theme,
|
||||
style: .glass,
|
||||
header: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: "Your Gifts".uppercased(),
|
||||
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
||||
textColor: environment.theme.list.freeTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
footer: nil,
|
||||
items: [
|
||||
AnyComponentWithIdentity(id: 0, component: AnyComponent(
|
||||
GiftListItemComponent(
|
||||
context: component.context,
|
||||
theme: environment.theme,
|
||||
gifts: self.starGifts,
|
||||
starGifts: peerColorStarGifts,
|
||||
selectedId: selectedGiftId,
|
||||
selectionUpdated: { [weak self] gift in
|
||||
guard let self, let peerColor = gift.peerColor else {
|
||||
return
|
||||
}
|
||||
self.selectedNameGift = gift
|
||||
self.updatedPeerNameColor = .collectible(peerColor)
|
||||
self.updatedPeerNameEmoji = peerColor.backgroundEmojiId
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
},
|
||||
tag: giftListTag
|
||||
)
|
||||
)),
|
||||
],
|
||||
displaySeparators: false
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
||||
)
|
||||
let giftsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: giftsSectionSize)
|
||||
if let giftsSectionView = self.nameGiftsSection.view {
|
||||
if giftsSectionView.superview == nil {
|
||||
self.scrollView.addSubview(giftsSectionView)
|
||||
}
|
||||
transition.setFrame(view: giftsSectionView, frame: giftsSectionFrame)
|
||||
}
|
||||
contentHeight += giftsSectionSize.height
|
||||
contentHeight += sectionSpacing
|
||||
}
|
||||
|
||||
contentHeight += bottomContentInset
|
||||
|
||||
//TODO:localize
|
||||
let buttonSideInset: CGFloat = 36.0
|
||||
let buttonTitle = "Apply Style" // environment.strings.Channel_Appearance_ApplyButton
|
||||
// if let emojiStatus = resolvedState.emojiStatus, case .starGift = emojiStatus.content, resolvedState.changes.contains(.emojiStatus) {
|
||||
// buttonTitle = environment.strings.NameColor_WearCollectible
|
||||
// }
|
||||
let buttonSideInset: CGFloat = environment.safeInsets.left + 36.0
|
||||
var buttonTitle = "Apply Style" // environment.strings.Channel_Appearance_ApplyButton
|
||||
var buttonAttributedSubtitleString: NSMutableAttributedString?
|
||||
|
||||
if let gift = self.selectedProfileGift, let resellAmounts = gift.resellAmounts, let starsAmount = resellAmounts.first(where: { $0.currency == .stars }) {
|
||||
let resellAmount: CurrencyAmount
|
||||
if gift.resellForTonOnly {
|
||||
resellAmount = resellAmounts.first(where: { $0.currency == .ton }) ?? starsAmount
|
||||
} else {
|
||||
resellAmount = starsAmount
|
||||
}
|
||||
|
||||
if self.cachedStarImage == nil || self.cachedStarImage?.1 !== theme {
|
||||
self.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: theme.list.itemCheckColors.foregroundColor)!, theme)
|
||||
}
|
||||
if self.cachedTonImage == nil || self.cachedTonImage?.1 !== theme {
|
||||
self.cachedTonImage = (generateTintedImage(image: UIImage(bundleImageName: "Ads/TonAbout"), color: theme.list.itemCheckColors.foregroundColor)!, theme)
|
||||
}
|
||||
if self.cachedSubtitleStarImage == nil || self.cachedSubtitleStarImage?.1 !== environment.theme {
|
||||
self.cachedSubtitleStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/StarsCount"), color: .white)!, theme)
|
||||
}
|
||||
|
||||
var buyString = environment.strings.Gift_View_BuyFor
|
||||
let currencySymbol: String
|
||||
let currencyAmount: String
|
||||
switch resellAmount.currency {
|
||||
case .stars:
|
||||
currencySymbol = "#"
|
||||
currencyAmount = formatStarsAmountText(resellAmount.amount, dateTimeFormat: environment.dateTimeFormat)
|
||||
case .ton:
|
||||
currencySymbol = "$"
|
||||
currencyAmount = formatTonAmountText(resellAmount.amount.value, dateTimeFormat: environment.dateTimeFormat, maxDecimalPositions: nil)
|
||||
|
||||
buttonAttributedSubtitleString = NSMutableAttributedString(string: environment.strings.Gift_View_EqualsTo(" # \(formatStarsAmountText(starsAmount.amount, dateTimeFormat: environment.dateTimeFormat))").string, font: Font.medium(11.0), textColor: theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7), paragraphAlignment: .center)
|
||||
|
||||
}
|
||||
buyString += " \(currencySymbol) \(currencyAmount)"
|
||||
buttonTitle = buyString
|
||||
}
|
||||
|
||||
let buttonAttributedString = NSMutableAttributedString(string: buttonTitle, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
|
||||
if let range = buttonAttributedString.string.range(of: "#"), let starImage = self.cachedStarImage?.0 {
|
||||
buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string))
|
||||
buttonAttributedString.addAttribute(.foregroundColor, value: theme.list.itemCheckColors.foregroundColor, range: NSRange(range, in: buttonAttributedString.string))
|
||||
buttonAttributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: buttonAttributedString.string))
|
||||
buttonAttributedString.addAttribute(.kern, value: 2.0, range: NSRange(range, in: buttonAttributedString.string))
|
||||
}
|
||||
if let range = buttonAttributedString.string.range(of: "$"), let tonImage = self.cachedTonImage?.0 {
|
||||
buttonAttributedString.addAttribute(.attachment, value: tonImage, range: NSRange(range, in: buttonAttributedString.string))
|
||||
buttonAttributedString.addAttribute(.foregroundColor, value: theme.list.itemCheckColors.foregroundColor, range: NSRange(range, in: buttonAttributedString.string))
|
||||
buttonAttributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: buttonAttributedString.string))
|
||||
buttonAttributedString.addAttribute(.kern, value: 2.0, range: NSRange(range, in: buttonAttributedString.string))
|
||||
}
|
||||
if let buttonAttributedSubtitleString, let range = buttonAttributedSubtitleString.string.range(of: "#"), let starImage = self.cachedSubtitleStarImage?.0 {
|
||||
buttonAttributedSubtitleString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedSubtitleString.string))
|
||||
buttonAttributedSubtitleString.addAttribute(.foregroundColor, value: theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7), range: NSRange(range, in: buttonAttributedSubtitleString.string))
|
||||
buttonAttributedSubtitleString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: buttonAttributedSubtitleString.string))
|
||||
buttonAttributedSubtitleString.addAttribute(.kern, value: 2.0, range: NSRange(range, in: buttonAttributedSubtitleString.string))
|
||||
}
|
||||
|
||||
var buttonContents: [AnyComponentWithIdentity<Empty>] = [
|
||||
AnyComponentWithIdentity(id: AnyHashable(buttonTitle), component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString))))
|
||||
]
|
||||
if let buttonAttributedSubtitleString {
|
||||
buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedSubtitleString)))))
|
||||
}
|
||||
|
||||
var buttonContents: [AnyComponentWithIdentity<Empty>] = []
|
||||
buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(buttonTitle), component: AnyComponent(
|
||||
Text(text: buttonTitle, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor)
|
||||
)))
|
||||
|
||||
let buttonSize = self.actionButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ButtonComponent(
|
||||
@ -1426,7 +1542,7 @@ final class UserAppearanceScreenComponent: Component {
|
||||
transition.setAlpha(view: buttonView, alpha: 1.0)
|
||||
}
|
||||
|
||||
let edgeEffectHeight: CGFloat = availableSize.height - buttonY + 8.0
|
||||
let edgeEffectHeight: CGFloat = availableSize.height - buttonY + 24.0
|
||||
let edgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - edgeEffectHeight), size: CGSize(width: availableSize.width, height: edgeEffectHeight))
|
||||
transition.setFrame(view: self.edgeEffectView, frame: edgeEffectFrame)
|
||||
self.edgeEffectView.update(content: environment.theme.list.blocksBackgroundColor, isInverted: false, rect: edgeEffectFrame, edge: .bottom, edgeSize: edgeEffectFrame.height, containerSize: availableSize, transition: transition)
|
||||
@ -1440,10 +1556,8 @@ final class UserAppearanceScreenComponent: Component {
|
||||
if self.scrollView.contentSize != contentSize {
|
||||
self.scrollView.contentSize = contentSize
|
||||
}
|
||||
// let scrollInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: availableSize.height - bottomPanelFrame.minY, right: 0.0)
|
||||
// if self.scrollView.verticalScrollIndicatorInsets != scrollInsets {
|
||||
// self.scrollView.verticalScrollIndicatorInsets = scrollInsets
|
||||
// }
|
||||
|
||||
transition.setFrame(view: self.containerView, frame: CGRect(origin: .zero, size: availableSize))
|
||||
|
||||
if !previousBounds.isEmpty, !transition.animation.isImmediate {
|
||||
let bounds = self.scrollView.bounds
|
||||
|
||||
@ -36,15 +36,18 @@ public final class TabSelectorComponent: Component {
|
||||
public struct Colors: Equatable {
|
||||
public var foreground: UIColor
|
||||
public var selection: UIColor
|
||||
public var normal: UIColor?
|
||||
public var simple: Bool
|
||||
|
||||
public init(
|
||||
foreground: UIColor,
|
||||
selection: UIColor,
|
||||
normal: UIColor? = nil,
|
||||
simple: Bool = false
|
||||
) {
|
||||
self.foreground = foreground
|
||||
self.selection = selection
|
||||
self.normal = normal
|
||||
self.simple = simple
|
||||
}
|
||||
}
|
||||
@ -58,7 +61,7 @@ public final class TabSelectorComponent: Component {
|
||||
public var verticalInset: CGFloat
|
||||
public var allowScroll: Bool
|
||||
|
||||
public init(font: UIFont, spacing: CGFloat, innerSpacing: CGFloat? = nil, fillWidth: Bool = false, lineSelection: Bool = false, verticalInset: CGFloat = 0.0, allowScroll: Bool = true) {
|
||||
public init(font: UIFont, spacing: CGFloat = 2.0, innerSpacing: CGFloat? = nil, fillWidth: Bool = false, lineSelection: Bool = false, verticalInset: CGFloat = 0.0, allowScroll: Bool = true) {
|
||||
self.font = font
|
||||
self.spacing = spacing
|
||||
self.innerSpacing = innerSpacing
|
||||
@ -611,6 +614,9 @@ public final class TabSelectorComponent: Component {
|
||||
if case .component = item.content {
|
||||
useSelectionFraction = true
|
||||
}
|
||||
if let _ = component.colors.normal {
|
||||
useSelectionFraction = true
|
||||
}
|
||||
|
||||
let itemSize = itemView.title.update(
|
||||
transition: itemTransition,
|
||||
@ -619,6 +625,7 @@ public final class TabSelectorComponent: Component {
|
||||
content: item.content,
|
||||
font: itemFont,
|
||||
color: component.colors.foreground,
|
||||
normalColor: component.colors.normal,
|
||||
selectedColor: component.colors.selection,
|
||||
selectionFraction: useSelectionFraction ? selectionFraction : 0.0
|
||||
)),
|
||||
@ -805,6 +812,7 @@ private final class ItemComponent: CombinedComponent {
|
||||
let content: TabSelectorComponent.Item.Content
|
||||
let font: UIFont
|
||||
let color: UIColor
|
||||
let normalColor: UIColor?
|
||||
let selectedColor: UIColor
|
||||
let selectionFraction: CGFloat
|
||||
|
||||
@ -813,6 +821,7 @@ private final class ItemComponent: CombinedComponent {
|
||||
content: TabSelectorComponent.Item.Content,
|
||||
font: UIFont,
|
||||
color: UIColor,
|
||||
normalColor: UIColor?,
|
||||
selectedColor: UIColor,
|
||||
selectionFraction: CGFloat
|
||||
) {
|
||||
@ -820,6 +829,7 @@ private final class ItemComponent: CombinedComponent {
|
||||
self.content = content
|
||||
self.font = font
|
||||
self.color = color
|
||||
self.normalColor = normalColor
|
||||
self.selectedColor = selectedColor
|
||||
self.selectionFraction = selectionFraction
|
||||
}
|
||||
@ -837,6 +847,9 @@ private final class ItemComponent: CombinedComponent {
|
||||
if lhs.color != rhs.color {
|
||||
return false
|
||||
}
|
||||
if lhs.normalColor != rhs.normalColor {
|
||||
return false
|
||||
}
|
||||
if lhs.selectedColor != rhs.selectedColor {
|
||||
return false
|
||||
}
|
||||
@ -856,7 +869,7 @@ private final class ItemComponent: CombinedComponent {
|
||||
|
||||
switch component.content {
|
||||
case let .text(text):
|
||||
let attributedTitle = NSMutableAttributedString(string: text, font: component.font, textColor: component.color)
|
||||
let attributedTitle = NSMutableAttributedString(string: text, font: component.font, textColor: component.normalColor ?? component.color)
|
||||
var range = (attributedTitle.string as NSString).range(of: "⭐️")
|
||||
if range.location != NSNotFound {
|
||||
attributedTitle.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
|
||||
@ -879,7 +892,12 @@ private final class ItemComponent: CombinedComponent {
|
||||
.opacity(1.0 - component.selectionFraction)
|
||||
)
|
||||
|
||||
let selectedAttributedTitle = NSMutableAttributedString(string: text, font: component.font, textColor: component.selectedColor)
|
||||
var selectedColor = component.selectedColor
|
||||
if let _ = component.normalColor {
|
||||
selectedColor = component.color
|
||||
}
|
||||
|
||||
let selectedAttributedTitle = NSMutableAttributedString(string: text, font: component.font, textColor: selectedColor)
|
||||
range = (selectedAttributedTitle.string as NSString).range(of: "⭐️")
|
||||
if range.location != NSNotFound {
|
||||
selectedAttributedTitle.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
|
||||
|
||||
@ -5,7 +5,6 @@ PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="7.16309" height="12.7222">
|
||||
<g>
|
||||
<rect height="12.7222" opacity="0" width="7.16309" x="0" y="0"/>
|
||||
<path d="M7.16309 6.35742C7.16309 6.17432 7.08984 6.00586 6.95068 5.87402L1.1499 0.19043C1.01807 0.065918 0.856934 0 0.666504 0C0.292969 0 0 0.285645 0 0.666504C0 0.849609 0.0732422 1.01807 0.19043 1.14258L5.52246 6.35742L0.19043 11.5723C0.0732422 11.6968 0 11.8579 0 12.0483C0 12.4292 0.292969 12.7148 0.666504 12.7148C0.856934 12.7148 1.01807 12.6489 1.1499 12.5171L6.95068 6.84082C7.08984 6.70166 7.16309 6.54053 7.16309 6.35742Z" fill="#ffffff" fill-opacity="0.85"/>
|
||||
<path d="M7.16309 6.35742C7.16309 6.17432 7.08984 6.00586 6.95068 5.87402L1.1499 0.19043C1.01807 0.065918 0.856934 0 0.666504 0C0.292969 0 0 0.285645 0 0.666504C0 0.849609 0.0732422 1.01807 0.19043 1.14258L5.52246 6.35742L0.19043 11.5723C0.0732422 11.6968 0 11.8579 0 12.0483C0 12.4292 0.292969 12.7148 0.666504 12.7148C0.856934 12.7148 1.01807 12.6489 1.1499 12.5171L6.95068 6.84082C7.08984 6.70166 7.16309 6.54053 7.16309 6.35742Z" fill="#ffffff" fill-opacity="1.0"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 879 B After Width: | Height: | Size: 811 B |
BIN
submodules/TelegramUI/Resources/Animations/Style.tgs
Normal file
BIN
submodules/TelegramUI/Resources/Animations/Style.tgs
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user