Various improvements

This commit is contained in:
Ilya Laktyushin 2025-09-22 06:42:19 +04:00
parent efe304afe6
commit a80a706379
22 changed files with 1232 additions and 643 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -58,6 +58,7 @@ swift_library(
"//submodules/TelegramUI/Components/BottomButtonPanelComponent",
"//submodules/PromptUI",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
"//submodules/TelegramUI/Components/PeerInfo/CollectionTabItemComponent"
],
visibility = [
"//visibility:public",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.