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( content: LottieComponent.AppBundleContent(
name: "anim_profilemore" name: "anim_profilemore"
), ),
color: .white color: .white,
size: CGSize(width: 34.0, height: 34.0)
)), )),
background: AnyComponent( background: AnyComponent(
GlassBackgroundComponent(size: CGSize(width: navigationButtonDiameter, height: navigationButtonDiameter), isDark: false, tintColor: .init(kind: .panel, color: panelColor)) 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, transition: .immediate,
component: AnyComponent(PlainButtonComponent( component: AnyComponent(PlainButtonComponent(
content: AnyComponent(Image( content: AnyComponent(Image(
image: closeButtonImage(dark: false) image: closeButtonImage(dark: false),
contentMode: .center
)), )),
background: AnyComponent( background: AnyComponent(
GlassBackgroundComponent(size: CGSize(width: navigationButtonDiameter, height: navigationButtonDiameter), isDark: false, tintColor: .init(kind: .panel, color: panelColor)) 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.setLineWidth(2.0)
context.setLineCap(.round) context.setLineCap(.round)
context.setLineJoin(.round)
context.setStrokeColor(UIColor.white.cgColor) context.setStrokeColor(UIColor.white.cgColor)
context.move(to: CGPoint(x: 7.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: 10.0)) context.addLine(to: CGPoint(x: 14.0, y: 8.0 + UIScreenPixel))
context.addLine(to: CGPoint(x: 21.0 - UIScreenPixel, y: 16.0 + UIScreenPixel)) context.addLine(to: CGPoint(x: 22.0 + UIScreenPixel, y: 17.0))
context.strokePath() context.strokePath()
}) })
} }

View File

@ -440,6 +440,7 @@ public final class ButtonComponent: Component {
private var component: ButtonComponent? private var component: ButtonComponent?
private weak var componentState: EmptyComponentState? private weak var componentState: EmptyComponentState?
private var containerView: UIView
private var shimmeringView: ButtonShimmeringView? private var shimmeringView: ButtonShimmeringView?
private var chromeView: UIImageView? private var chromeView: UIImageView?
private var contentItem: ContentItem? private var contentItem: ContentItem?
@ -447,18 +448,38 @@ public final class ButtonComponent: Component {
private var activityIndicator: ActivityIndicator? private var activityIndicator: ActivityIndicator?
override init(frame: CGRect) { override init(frame: CGRect) {
self.containerView = UIView()
self.containerView.clipsToBounds = true
self.containerView.isUserInteractionEnabled = false
super.init(frame: frame) super.init(frame: frame)
self.addSubview(self.containerView)
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.highligthedChanged = { [weak self] highlighted in self.highligthedChanged = { [weak self] highlighted in
if let self, let component = self.component, component.isEnabled { if let self, let component = self.component, component.isEnabled {
if highlighted { switch component.background.style {
self.layer.removeAnimation(forKey: "opacity") case .glass:
self.alpha = 0.7 let transition = ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut))
} else { if highlighted {
self.alpha = 1.0 let highlightedColor = component.background.color.withMultiplied(hue: 1.0, saturation: 0.77, brightness: 1.01)
self.layer.animateAlpha(from: 7, to: 1.0, duration: 0.2) 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 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 var cornerRadius: CGFloat = component.background.cornerRadius
if case .glass = component.background.style, component.background.cornerRadius == 10.0 { if case .glass = component.background.style, component.background.cornerRadius == 10.0 {
cornerRadius = availableSize.height * 0.5 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 var contentAlpha: CGFloat = 1.0
if component.displaysProgress { if component.displaysProgress {
@ -525,7 +546,7 @@ public final class ButtonComponent: Component {
contentTransition = .immediate contentTransition = .immediate
animateIn = true animateIn = true
contentView.isUserInteractionEnabled = false contentView.isUserInteractionEnabled = false
self.addSubview(contentView) self.containerView.addSubview(contentView)
contentItem.view.parentState = state 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 = ActivityIndicator(type: .custom(component.background.foreground, 22.0, 2.0, true))
activityIndicator.view.alpha = 0.0 activityIndicator.view.alpha = 0.0
self.activityIndicator = activityIndicator self.activityIndicator = activityIndicator
self.addSubview(activityIndicator.view) self.containerView.addSubview(activityIndicator.view)
} }
let indicatorSize = CGSize(width: 22.0, height: 22.0) let indicatorSize = CGSize(width: 22.0, height: 22.0)
transition.setAlpha(view: activityIndicator.view, alpha: 1.0) transition.setAlpha(view: activityIndicator.view, alpha: 1.0)
@ -586,7 +607,7 @@ public final class ButtonComponent: Component {
shimmeringTransition = .immediate shimmeringTransition = .immediate
shimmeringView = ButtonShimmeringView(frame: .zero) shimmeringView = ButtonShimmeringView(frame: .zero)
self.shimmeringView = shimmeringView 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) shimmeringView.update(size: availableSize, background: component.background, cornerRadius: component.background.cornerRadius, transition: shimmeringTransition)
shimmeringTransition.setFrame(view: shimmeringView, frame: CGRect(origin: .zero, size: availableSize)) shimmeringTransition.setFrame(view: shimmeringView, frame: CGRect(origin: .zero, size: availableSize))
@ -607,9 +628,9 @@ public final class ButtonComponent: Component {
chromeView = UIImageView() chromeView = UIImageView()
self.chromeView = chromeView self.chromeView = chromeView
if let shimmeringView = self.shimmeringView { if let shimmeringView = self.shimmeringView {
self.insertSubview(chromeView, aboveSubview: shimmeringView) self.containerView.insertSubview(chromeView, aboveSubview: shimmeringView)
} else { } else {
self.insertSubview(chromeView, at: 0) self.containerView.insertSubview(chromeView, at: 0)
} }
chromeView.layer.compositingFilter = "overlayBlendMode" 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 return availableSize
} }
} }

View File

@ -561,6 +561,7 @@ public final class GiftItemComponent: Component {
} }
} }
var tonButtonColor: UIColor = .clear
if case .generic = component.mode { if case .generic = component.mode {
if let title = component.title { if let title = component.title {
let titleSize = self.title.update( let titleSize = self.title.update(
@ -630,6 +631,7 @@ public final class GiftItemComponent: Component {
price = priceValue ?? component.strings.Gift_Options_Gift_Transfer price = priceValue ?? component.strings.Gift_Options_Gift_Transfer
tinted = true tinted = true
} }
tonButtonColor = buttonColor
let buttonSize = self.button.update( let buttonSize = self.button.update(
transition: transition, transition: transition,
@ -657,29 +659,7 @@ public final class GiftItemComponent: Component {
transition.setFrame(view: buttonView, frame: buttonFrame) 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 { if let label = component.label {
let labelColor = component.theme.overallDarkAppearance ? UIColor(rgb: 0xffc337) : UIColor(rgb: 0xd3720a) let labelColor = component.theme.overallDarkAppearance ? UIColor(rgb: 0xffc337) : UIColor(rgb: 0xd3720a)
let attributes = MarkdownAttributes( 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 { if let ribbon = component.ribbon {
let ribbonFontSize: CGFloat let ribbonFontSize: CGFloat
if case .profile = component.mode { if case .profile = component.mode {
@ -995,7 +998,7 @@ public final class GiftItemComponent: Component {
} else { } else {
resellBackgroundTransition = .immediate 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 resellBackground.clipsToBounds = true
self.resellBackground = resellBackground 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 lineWidth: CGFloat = 2.0
let selectionFrame = backgroundFrame.insetBy(dx: 3.0, dy: 3.0) let selectionFrame = backgroundFrame.insetBy(dx: 3.0, dy: 3.0)
@ -1030,7 +1034,9 @@ public final class GiftItemComponent: Component {
} else { } else {
selectionLayer = SimpleShapeLayer() selectionLayer = SimpleShapeLayer()
self.selectionLayer = selectionLayer 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) self.layer.insertSublayer(selectionLayer, below: self.ribbon.layer)
} else { } else {
self.layer.addSublayer(selectionLayer) self.layer.addSublayer(selectionLayer)
@ -1058,6 +1064,8 @@ public final class GiftItemComponent: Component {
selectionLayer.removeFromSuperlayer() selectionLayer.removeFromSuperlayer()
}) })
} }
default:
break
} }
if case .select = component.mode { 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/Gifts/GiftViewScreen",
"//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/TextFieldComponent", "//submodules/TelegramUI/Components/TextFieldComponent",
"//submodules/TelegramUI/Components/Gifts/GiftLoadingShimmerView",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -26,6 +26,7 @@ import GiftViewScreen
import UndoUI import UndoUI
import ContextUI import ContextUI
import LottieComponent import LottieComponent
import GiftLoadingShimmerView
private let minimumCountToDisplayFilters = 18 private let minimumCountToDisplayFilters = 18
@ -71,7 +72,7 @@ final class GiftStoreScreenComponent: Component {
final class View: UIView, UIScrollViewDelegate { final class View: UIView, UIScrollViewDelegate {
private let topOverscrollLayer = SimpleLayer() private let topOverscrollLayer = SimpleLayer()
private let scrollView: ScrollView private let scrollView: ScrollView
private let loadingNode: LoadingShimmerNode private let loadingView: GiftLoadingShimmerView
private let emptyResultsAnimation = ComponentView<Empty>() private let emptyResultsAnimation = ComponentView<Empty>()
private let emptyResultsTitle = ComponentView<Empty>() private let emptyResultsTitle = ComponentView<Empty>()
private let clearFilters = ComponentView<Empty>() private let clearFilters = ComponentView<Empty>()
@ -88,7 +89,7 @@ final class GiftStoreScreenComponent: Component {
private let title = ComponentView<Empty>() private let title = ComponentView<Empty>()
private let subtitle = 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 let filterSelector = ComponentView<Empty>()
private var isUpdating: Bool = false private var isUpdating: Bool = false
@ -117,13 +118,13 @@ final class GiftStoreScreenComponent: Component {
} }
self.scrollView.alwaysBounceVertical = true self.scrollView.alwaysBounceVertical = true
self.loadingNode = LoadingShimmerNode() self.loadingView = GiftLoadingShimmerView()
super.init(frame: frame) super.init(frame: frame)
self.scrollView.delegate = self self.scrollView.delegate = self
self.addSubview(self.scrollView) self.addSubview(self.scrollView)
self.addSubview(self.loadingNode.view) self.addSubview(self.loadingView)
self.scrollView.layer.addSublayer(self.topOverscrollLayer) self.scrollView.layer.addSublayer(self.topOverscrollLayer)
} }
@ -211,14 +212,14 @@ final class GiftStoreScreenComponent: Component {
var itemTransition = transition var itemTransition = transition
let visibleItem: ComponentView<Empty> let visibleItem: ComponentView<Empty>
if let current = self.starsItems[itemId] { if let current = self.giftItems[itemId] {
visibleItem = current visibleItem = current
} else { } else {
visibleItem = ComponentView() visibleItem = ComponentView()
if !transition.animation.isImmediate { if !transition.animation.isImmediate {
itemTransition = .immediate itemTransition = .immediate
} }
self.starsItems[itemId] = visibleItem self.giftItems[itemId] = visibleItem
} }
var ribbon: GiftItemComponent.Ribbon? var ribbon: GiftItemComponent.Ribbon?
@ -235,7 +236,10 @@ final class GiftStoreScreenComponent: Component {
color: ribbonColor 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( let _ = visibleItem.update(
transition: itemTransition, transition: itemTransition,
component: AnyComponent( component: AnyComponent(
@ -306,7 +310,7 @@ final class GiftStoreScreenComponent: Component {
} }
var removeIds: [AnyHashable] = [] var removeIds: [AnyHashable] = []
for (id, item) in self.starsItems { for (id, item) in self.giftItems {
if !validIds.contains(id) { if !validIds.contains(id) {
removeIds.append(id) removeIds.append(id)
if let itemView = item.view { if let itemView = item.view {
@ -322,7 +326,7 @@ final class GiftStoreScreenComponent: Component {
} }
} }
for id in removeIds { 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 { if view.superview == nil {
view.alpha = 0.0 view.alpha = 0.0
fadeTransition.setAlpha(view: view, alpha: 1.0) fadeTransition.setAlpha(view: view, alpha: 1.0)
self.insertSubview(view, belowSubview: self.loadingNode.view) self.insertSubview(view, belowSubview: self.loadingView)
view.playOnce() view.playOnce()
} }
view.bounds = CGRect(origin: .zero, size: emptyResultsAnimationFrame.size) view.bounds = CGRect(origin: .zero, size: emptyResultsAnimationFrame.size)
@ -419,7 +423,7 @@ final class GiftStoreScreenComponent: Component {
if view.superview == nil { if view.superview == nil {
view.alpha = 0.0 view.alpha = 0.0
fadeTransition.setAlpha(view: view, alpha: 1.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) view.bounds = CGRect(origin: .zero, size: emptyResultsTitleFrame.size)
ComponentTransition.immediate.setPosition(view: view, position: emptyResultsTitleFrame.center) ComponentTransition.immediate.setPosition(view: view, position: emptyResultsTitleFrame.center)
@ -1206,12 +1210,12 @@ final class GiftStoreScreenComponent: Component {
self.updateScrolling(transition: transition) self.updateScrolling(transition: transition)
if isLoading && self.showLoading { if isLoading && self.showLoading {
self.loadingNode.update(size: availableSize, theme: environment.theme, showFilters: !showingFilters, transition: .immediate) self.loadingView.update(size: availableSize, theme: environment.theme, showFilters: !showingFilters, transition: .immediate)
loadingTransition.setAlpha(view: self.loadingNode.view, alpha: 1.0) loadingTransition.setAlpha(view: self.loadingView, alpha: 1.0)
} else { } 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 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) containerSize: CGSize(width: availableSize.width - headerSideInset * 2.0, height: availableSize.height)
) )
if contentHeight != 0.0 { if contentHeight != 0.0 {
contentHeight += 7.0 contentHeight += 8.0 - UIScreenPixel
} }
if let footerView = footer.view { if let footerView = footer.view {
if footerView.superview == nil { 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/TelegramUI/Components/BottomButtonPanelComponent",
"//submodules/PromptUI", "//submodules/PromptUI",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView", "//submodules/TelegramUI/Components/EmojiTextAttachmentView",
"//submodules/TelegramUI/Components/PeerInfo/CollectionTabItemComponent"
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -34,6 +34,7 @@ import BundleIconComponent
import EmojiTextAttachmentView import EmojiTextAttachmentView
import TextFormat import TextFormat
import PromptUI import PromptUI
import CollectionTabItemComponent
public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate { public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate {
public enum GiftCollection: Equatable { public enum GiftCollection: Equatable {
@ -94,12 +95,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
private var panelSeparator: ASDisplayNode? private var panelSeparator: ASDisplayNode?
private var panelButton: ComponentView<Empty>? private var panelButton: ComponentView<Empty>?
private var panelCheck: 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 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? 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 { private final class ContextControllerContentSourceImpl: ContextControllerContentSource {
let controller: ViewController let controller: ViewController
weak var sourceView: UIView? weak var sourceView: UIView?

View File

@ -39,6 +39,7 @@ swift_library(
"//submodules/TelegramUI/Components/ListActionItemComponent", "//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/Settings/ThemeCarouselItem", "//submodules/TelegramUI/Components/Settings/ThemeCarouselItem",
"//submodules/Components/MultilineTextComponent", "//submodules/Components/MultilineTextComponent",
"//submodules/Components/BalancedTextComponent",
"//submodules/TelegramUI/Components/EmojiStatusSelectionComponent", "//submodules/TelegramUI/Components/EmojiStatusSelectionComponent",
"//submodules/TelegramUI/Components/DynamicCornerRadiusView", "//submodules/TelegramUI/Components/DynamicCornerRadiusView",
"//submodules/Components/ComponentDisplayAdapters", "//submodules/Components/ComponentDisplayAdapters",
@ -55,6 +56,10 @@ swift_library(
"//submodules/TelegramUI/Components/TabSelectorComponent", "//submodules/TelegramUI/Components/TabSelectorComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/TextFormat", "//submodules/TextFormat",
"//submodules/TelegramUI/Components/PeerInfo/CollectionTabItemComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/Gifts/GiftItemComponent",
"//submodules/TelegramUI/Components/Gifts/GiftLoadingShimmerView"
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -1065,6 +1065,7 @@ final class ChannelAppearanceScreenComponent: Component {
componentTheme: environment.theme, componentTheme: environment.theme,
strings: environment.strings, strings: environment.strings,
topInset: environment.statusBarHeight, topInset: environment.statusBarHeight,
bottomInset: 0.0,
sectionId: 0, sectionId: 0,
peer: peer, peer: peer,
subtitleString: contentsData.subscriberCount.flatMap { subtitleString: contentsData.subscriberCount.flatMap {

View File

@ -2,16 +2,24 @@ import Foundation
import UIKit import UIKit
import Display import Display
import ComponentFlow import ComponentFlow
import SwiftSignalKit
import TelegramCore import TelegramCore
import GiftItemComponent import GiftItemComponent
import PlainButtonComponent import PlainButtonComponent
import TelegramPresentationData import TelegramPresentationData
import AccountContext import AccountContext
import TabSelectorComponent
import CollectionTabItemComponent
import LottieComponent
import MultilineTextComponent
import BalancedTextComponent
import GiftLoadingShimmerView
final class GiftListItemComponent: Component { final class GiftListItemComponent: Component {
let context: AccountContext let context: AccountContext
let theme: PresentationTheme let theme: PresentationTheme
let gifts: [StarGift.UniqueGift] let gifts: [StarGift.UniqueGift]
let starGifts: [StarGift]
let selectedId: Int64? let selectedId: Int64?
let selectionUpdated: (StarGift.UniqueGift) -> Void let selectionUpdated: (StarGift.UniqueGift) -> Void
let tag: AnyObject? let tag: AnyObject?
@ -20,6 +28,7 @@ final class GiftListItemComponent: Component {
context: AccountContext, context: AccountContext,
theme: PresentationTheme, theme: PresentationTheme,
gifts: [StarGift.UniqueGift], gifts: [StarGift.UniqueGift],
starGifts: [StarGift],
selectedId: Int64?, selectedId: Int64?,
selectionUpdated: @escaping (StarGift.UniqueGift) -> Void, selectionUpdated: @escaping (StarGift.UniqueGift) -> Void,
tag: AnyObject? tag: AnyObject?
@ -27,6 +36,7 @@ final class GiftListItemComponent: Component {
self.context = context self.context = context
self.theme = theme self.theme = theme
self.gifts = gifts self.gifts = gifts
self.starGifts = starGifts
self.selectedId = selectedId self.selectedId = selectedId
self.selectionUpdated = selectionUpdated self.selectionUpdated = selectionUpdated
self.tag = tag self.tag = tag
@ -39,6 +49,9 @@ final class GiftListItemComponent: Component {
if lhs.gifts != rhs.gifts { if lhs.gifts != rhs.gifts {
return false return false
} }
if lhs.starGifts != rhs.starGifts {
return false
}
if lhs.selectedId != rhs.selectedId { if lhs.selectedId != rhs.selectedId {
return false return false
} }
@ -59,11 +72,30 @@ final class GiftListItemComponent: Component {
return false 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 var giftItems: [AnyHashable: ComponentView<Empty>] = [:]
private(set) var visibleBounds: CGRect?
private var cachedChevronImage: (UIImage, PresentationTheme)?
private var component: GiftListItemComponent? private var component: GiftListItemComponent?
private var state: EmptyComponentState? private var state: EmptyComponentState?
private var isUpdating: Bool = false
override public init(frame: CGRect) { override public init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
} }
@ -72,30 +104,288 @@ final class GiftListItemComponent: Component {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
private var visibleBounds: CGRect? deinit {
self.resaleGiftsDisposable.dispose()
}
func updateVisibleBounds(_ bounds: CGRect) { func updateVisibleBounds(_ bounds: CGRect) {
self.visibleBounds = bounds 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 { 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.component = component
self.state = state 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 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 var index: Int32 = 0
for gift in component.gifts { for gift in effectiveGifts {
var isVisible = false var isVisible = false
if let visibleBounds = self.visibleBounds, visibleBounds.intersects(itemFrame) { if let visibleBounds = self.visibleBounds, visibleBounds.intersects(itemFrame) {
isVisible = true isVisible = true
@ -115,6 +405,27 @@ final class GiftListItemComponent: Component {
itemTransition = .immediate 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( let _ = visibleItem.update(
transition: itemTransition, transition: itemTransition,
component: AnyComponent( component: AnyComponent(
@ -123,13 +434,13 @@ final class GiftListItemComponent: Component {
GiftItemComponent( GiftItemComponent(
context: component.context, context: component.context,
theme: component.theme, theme: component.theme,
strings: component.context.sharedContext.currentPresentationData.with { $0 }.strings, strings: presentationData.strings,
peer: nil, peer: nil,
subject: .uniqueGift(gift: gift, price: nil), subject: subject,
ribbon: nil, ribbon: ribbon,
isHidden: false, isHidden: false,
isSelected: gift.id == component.selectedId, isSelected: gift.id == component.selectedId,
mode: .grid mode: self.selectedGiftId != 0 ? .generic : .grid
) )
), ),
effectAlignment: .center, effectAlignment: .center,
@ -147,7 +458,11 @@ final class GiftListItemComponent: Component {
) )
if let itemView = visibleItem.view { if let itemView = visibleItem.view {
if itemView.superview == nil { 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 { if !transition.animation.isImmediate {
itemView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25) 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)) 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 { if itemFrame.maxX > availableSize.width {
itemFrame.origin.x = sideInset itemFrame.origin.x = sideInset
itemFrame.origin.y += itemFrame.height + spacing itemFrame.origin.y += itemFrame.height + rowSpacing
} }
index += 1 index += 1
} }
@ -185,6 +500,23 @@ final class GiftListItemComponent: Component {
self.giftItems.removeValue(forKey: id) 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) return CGSize(width: availableSize.width, height: contentHeight)
} }
} }

View File

@ -24,6 +24,7 @@ final class PeerNameColorProfilePreviewItem: ListViewItem, ItemListItem, ListIte
let componentTheme: PresentationTheme let componentTheme: PresentationTheme
let strings: PresentationStrings let strings: PresentationStrings
let topInset: CGFloat let topInset: CGFloat
let bottomInset: CGFloat
let sectionId: ItemListSectionId let sectionId: ItemListSectionId
let peer: EnginePeer? let peer: EnginePeer?
let subtitleString: String? let subtitleString: String?
@ -31,12 +32,13 @@ final class PeerNameColorProfilePreviewItem: ListViewItem, ItemListItem, ListIte
let nameDisplayOrder: PresentationPersonNameOrder let nameDisplayOrder: PresentationPersonNameOrder
let showBackground: Bool 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.context = context
self.theme = theme self.theme = theme
self.componentTheme = componentTheme self.componentTheme = componentTheme
self.strings = strings self.strings = strings
self.topInset = topInset self.topInset = topInset
self.bottomInset = bottomInset
self.sectionId = sectionId self.sectionId = sectionId
self.peer = peer self.peer = peer
self.subtitleString = subtitleString self.subtitleString = subtitleString
@ -150,7 +152,7 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode {
return { [weak self] item, params, neighbors in return { [weak self] item, params, neighbors in
let separatorHeight = UIScreenPixel 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) var insets = itemListNeighborsGroupedInsets(neighbors, params)
if params.width <= 320.0 { if params.width <= 320.0 {
insets.top = 0.0 insets.top = 0.0
@ -169,7 +171,7 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode {
} }
self.item = item 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.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
self.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor self.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor

View File

@ -35,12 +35,21 @@ import TabSelectorComponent
import WallpaperResources import WallpaperResources
import EdgeEffect import EdgeEffect
import TextFormat import TextFormat
import TelegramStringFormatting
private let giftListTag = GenericComponentViewTag() private let giftListTag = GenericComponentViewTag()
final class UserAppearanceScreenComponent: Component { final class UserAppearanceScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment typealias EnvironmentType = ViewControllerComponentContainer.Environment
public final class TransitionHint {
public let animateTabChange: Bool
public init(animateTabChange: Bool) {
self.animateTabChange = animateTabChange
}
}
let context: AccountContext let context: AccountContext
init( init(
@ -59,13 +68,16 @@ final class UserAppearanceScreenComponent: Component {
private final class ContentsData { private final class ContentsData {
let peer: EnginePeer? let peer: EnginePeer?
let gifts: [StarGift.UniqueGift] let gifts: [StarGift.UniqueGift]
let starGifts: [StarGift]
init( init(
peer: EnginePeer?, peer: EnginePeer?,
gifts: [StarGift.UniqueGift] gifts: [StarGift.UniqueGift],
starGifts: [StarGift]
) { ) {
self.peer = peer self.peer = peer
self.gifts = gifts self.gifts = gifts
self.starGifts = starGifts
} }
static func get(context: AccountContext) -> Signal<ContentsData, NoError> { static func get(context: AccountContext) -> Signal<ContentsData, NoError> {
@ -73,9 +85,10 @@ final class UserAppearanceScreenComponent: Component {
context.engine.data.subscribe( context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId) 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] = [] var gifts: [StarGift.UniqueGift] = []
for orderedView in view.orderedItemListsViews { for orderedView in view.orderedItemListsViews {
if orderedView.collectionId == Namespaces.OrderedItemList.CloudUniqueStarGifts { if orderedView.collectionId == Namespaces.OrderedItemList.CloudUniqueStarGifts {
@ -89,7 +102,8 @@ final class UserAppearanceScreenComponent: Component {
} }
return ContentsData( return ContentsData(
peer: peer, peer: peer,
gifts: gifts gifts: gifts,
starGifts: starGifts ?? []
) )
} }
} }
@ -142,6 +156,7 @@ final class UserAppearanceScreenComponent: Component {
} }
final class View: UIView, UIScrollViewDelegate { final class View: UIView, UIScrollViewDelegate {
private let containerView = UIView()
private let topOverscrollLayer = SimpleLayer() private let topOverscrollLayer = SimpleLayer()
private let scrollView: ScrollView private let scrollView: ScrollView
private let actionButton = ComponentView<Empty>() private let actionButton = ComponentView<Empty>()
@ -181,9 +196,11 @@ final class UserAppearanceScreenComponent: Component {
private var cachedIconFiles: [Int64: TelegramMediaFile] = [:] private var cachedIconFiles: [Int64: TelegramMediaFile] = [:]
private var selectedNameGift: StarGift.UniqueGift?
private var updatedPeerNameColor: PeerColor? private var updatedPeerNameColor: PeerColor?
private var updatedPeerNameEmoji: Int64?? private var updatedPeerNameEmoji: Int64??
private var selectedProfileGift: StarGift.UniqueGift?
private var updatedPeerProfileColor: PeerNameColor?? private var updatedPeerProfileColor: PeerNameColor??
private var updatedPeerProfileEmoji: Int64?? private var updatedPeerProfileEmoji: Int64??
private var updatedPeerStatus: PeerEmojiStatus?? private var updatedPeerStatus: PeerEmojiStatus??
@ -198,6 +215,9 @@ final class UserAppearanceScreenComponent: Component {
private weak var emojiStatusSelectionController: ViewController? private weak var emojiStatusSelectionController: ViewController?
private var cachedChevronImage: (UIImage, PresentationTheme)? private var cachedChevronImage: (UIImage, PresentationTheme)?
private var cachedStarImage: (UIImage, PresentationTheme)?
private var cachedSubtitleStarImage: (UIImage, PresentationTheme)?
private var cachedTonImage: (UIImage, PresentationTheme)?
override init(frame: CGRect) { override init(frame: CGRect) {
self.scrollView = ScrollView() self.scrollView = ScrollView()
@ -216,8 +236,10 @@ final class UserAppearanceScreenComponent: Component {
super.init(frame: frame) super.init(frame: frame)
self.addSubview(self.containerView)
self.scrollView.delegate = self self.scrollView.delegate = self
self.addSubview(self.scrollView) self.containerView.addSubview(self.scrollView)
self.scrollView.layer.addSublayer(self.topOverscrollLayer) 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 { if let giftListView = self.profileGiftsSection.findTaggedView(tag: giftListTag) as? GiftListItemComponent.View {
let rect = self.scrollView.convert(self.scrollView.bounds, to: giftListView) let rect = self.scrollView.convert(self.scrollView.bounds, to: giftListView)
let visibleRect = giftListView.bounds.intersection(rect) 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: case .name:
if let giftListView = self.nameGiftsSection.findTaggedView(tag: giftListTag) as? GiftListItemComponent.View { if let giftListView = self.nameGiftsSection.findTaggedView(tag: giftListTag) as? GiftListItemComponent.View {
let rect = self.scrollView.convert(self.scrollView.bounds, to: giftListView) let rect = self.scrollView.convert(self.scrollView.bounds, to: giftListView)
let visibleRect = giftListView.bounds.intersection(rect) let visibleRect = giftListView.bounds.intersection(rect)
giftListView.updateVisibleBounds(visibleRect) if !self.isUpdating {
} giftListView.updateVisibleBounds(visibleRect)
} else if giftListView.visibleBounds == nil {
let bottomContentOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height) Queue.mainQueue().justDispatch {
if bottomContentOffset < 320.0 { giftListView.updateVisibleBounds(visibleRect)
self.starGiftsContext?.loadMore() }
}
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 { if case .collectible = resolvedState.nameColor {
self.updatedPeerNameColor = .preset(.blue) self.updatedPeerNameColor = .preset(.blue)
} }
self.selectedNameGift = nil
if let result { if let result {
self.updatedPeerNameEmoji = result.fileId.id self.updatedPeerNameEmoji = result.fileId.id
} else { } else {
@ -671,13 +708,21 @@ final class UserAppearanceScreenComponent: Component {
let environment = environment[EnvironmentType.self].value let environment = environment[EnvironmentType.self].value
let themeUpdated = self.environment?.theme !== environment.theme let themeUpdated = self.environment?.theme !== environment.theme
self.environment = environment self.environment = environment
self.component = component self.component = component
self.state = state 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 } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
if themeUpdated { if themeUpdated {
self.backgroundColor = environment.theme.list.blocksBackgroundColor self.backgroundColor = environment.theme.list.blocksBackgroundColor
self.scrollView.backgroundColor = environment.theme.list.blocksBackgroundColor
} }
if self.cachedChevronImage == nil || self.cachedChevronImage?.1 !== environment.theme { 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 { guard let contentsData = self.contentsData, var peer = contentsData.peer, let resolvedState = self.resolveState() else {
return availableSize 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) { 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() 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( let tabSelectorSize = self.tabSelector.update(
transition: transition, transition: transition,
component: AnyComponent( component: AnyComponent(
TabSelectorComponent( TabSelectorComponent(
colors: TabSelectorComponent.Colors( colors: TabSelectorComponent.Colors(
foreground: environment.theme.list.itemSecondaryTextColor, foreground: environment.theme.list.itemAccentColor,
selection: environment.theme.list.itemSecondaryTextColor.withMultipliedAlpha(0.15), selection: environment.theme.list.itemAccentColor.withMultipliedAlpha(0.1),
normal: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.78),
simple: true simple: true
), ),
theme: environment.theme, theme: environment.theme,
customLayout: TabSelectorComponent.CustomLayout(font: Font.semibold(16.0)),
items: [ items: [
TabSelectorComponent.Item(id: Section.profile.rawValue, title: "Profile"), TabSelectorComponent.Item(id: Section.profile.rawValue, title: environment.strings.ProfileColorSetup_TitleProfile),
TabSelectorComponent.Item(id: Section.name.rawValue, title: "Name") TabSelectorComponent.Item(id: Section.name.rawValue, title: environment.strings.ProfileColorSetup_TitleName)
], ],
selectedId: self.currentSection.rawValue, selectedId: self.currentSection.rawValue,
setSelectedId: { [weak self] value in setSelectedId: { [weak self] value in
@ -806,8 +870,11 @@ final class UserAppearanceScreenComponent: Component {
return return
} }
if let intValue = value.base as? Int32 { if let intValue = value.base as? Int32 {
self.currentSection = Section(rawValue: intValue) ?? .profile let updatedSection = Section(rawValue: intValue) ?? .profile
self.state?.updated(transition: .easeInOut(duration: 0.3)) 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 { if let nameGiftsSectionView = self.nameGiftsSection.view, nameGiftsSectionView.superview != nil {
nameGiftsSectionView.removeFromSuperview() nameGiftsSectionView.removeFromSuperview()
} }
var hasHeaderColor = false
if resolvedState.profileColor != nil {
hasHeaderColor = true
}
if case .starGift = resolvedState.emojiStatus?.content {
hasHeaderColor = true
}
let profilePreviewSize = self.profilePreview.update( let profilePreviewSize = self.profilePreview.update(
transition: transition, transition: previewTransition,
component: AnyComponent(TopBottomCornersComponent(topCornerRadius: itemCornerRadius, bottomCornerRadius: !self.scrolledUp ? itemCornerRadius : 0.0, component: AnyComponent(ListItemComponentAdaptor( component: AnyComponent(TopBottomCornersComponent(topCornerRadius: itemCornerRadius, bottomCornerRadius: !self.scrolledUp ? itemCornerRadius : 0.0, component: AnyComponent(ListItemComponentAdaptor(
itemGenerator: PeerNameColorProfilePreviewItem( itemGenerator: PeerNameColorProfilePreviewItem(
context: component.context, context: component.context,
theme: environment.theme, theme: environment.theme,
componentTheme: environment.theme, componentTheme: environment.theme,
strings: environment.strings, strings: environment.strings,
topInset: 0.0, topInset: 28.0,
bottomInset: 15.0 + UIScreenPixel,
sectionId: 0, sectionId: 0,
peer: peer, peer: peer,
subtitleString: environment.strings.Presence_online, subtitleString: environment.strings.Presence_online,
files: self.cachedIconFiles, files: self.cachedIconFiles,
nameDisplayOrder: presentationData.nameDisplayOrder, 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) 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) let profilePreviewFrame = CGRect(origin: CGPoint(x: sideInset, y: environment.navigationHeight + 12.0), size: profilePreviewSize)
if let profilePreviewView = self.profilePreview.view { if let profilePreviewView = self.profilePreview.view {
if profilePreviewView.superview == nil { if profilePreviewView.superview == nil {
self.addSubview(profilePreviewView) profilePreviewView.isUserInteractionEnabled = false
self.containerView.addSubview(profilePreviewView)
} }
transition.setFrame(view: profilePreviewView, frame: profilePreviewFrame) transition.setFrame(view: profilePreviewView, frame: profilePreviewFrame)
} }
@ -918,7 +979,6 @@ final class UserAppearanceScreenComponent: Component {
component: AnyComponent(ListSectionComponent( component: AnyComponent(ListSectionComponent(
theme: environment.theme, theme: environment.theme,
style: .glass, 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, header: nil,
footer: AnyComponent(MultilineTextComponent( footer: AnyComponent(MultilineTextComponent(
text: .plain(previewFooterText), text: .plain(previewFooterText),
@ -937,7 +997,7 @@ final class UserAppearanceScreenComponent: Component {
return return
} }
self.currentSection = .name self.currentSection = .name
self.state?.updated(transition: .easeInOut(duration: 0.3)) self.state?.updated(transition: .easeInOut(duration: 0.3).withUserData(TransitionHint(animateTabChange: true)))
} }
)), )),
items: [ items: [
@ -951,6 +1011,7 @@ final class UserAppearanceScreenComponent: Component {
guard let self, let value, let resolvedState = self.resolveState() else { guard let self, let value, let resolvedState = self.resolveState() else {
return return
} }
self.selectedProfileGift = nil
self.updatedPeerProfileColor = value self.updatedPeerProfileColor = value
if case .starGift = resolvedState.emojiStatus?.content { if case .starGift = resolvedState.emojiStatus?.content {
self.updatedPeerStatus = .some(nil) self.updatedPeerStatus = .some(nil)
@ -1025,7 +1086,7 @@ final class UserAppearanceScreenComponent: Component {
guard let self, let resolvedState = self.resolveState() else { guard let self, let resolvedState = self.resolveState() else {
return return
} }
self.selectedProfileGift = nil
self.updatedPeerProfileColor = .some(nil) self.updatedPeerProfileColor = .some(nil)
self.updatedPeerProfileEmoji = .some(nil) self.updatedPeerProfileEmoji = .some(nil)
if case .starGift = resolvedState.emojiStatus?.content { if case .starGift = resolvedState.emojiStatus?.content {
@ -1062,92 +1123,87 @@ final class UserAppearanceScreenComponent: Component {
contentHeight += sectionSpacing contentHeight += sectionSpacing
} }
if !contentsData.gifts.isEmpty { var selectedGiftId: Int64?
var selectedGiftId: Int64? if let status = resolvedState.emojiStatus, case let .starGift(id, _, _, _, _, _, _, _, _) = status.content {
if let status = resolvedState.emojiStatus, case let .starGift(id, _, _, _, _, _, _, _, _) = status.content { selectedGiftId = id
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
} }
//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: case .name:
var transition = transition var transition = transition
if self.namePreview.view == nil { if self.namePreview.view == nil {
@ -1208,7 +1264,7 @@ final class UserAppearanceScreenComponent: Component {
} }
let namePreviewSize = self.namePreview.update( let namePreviewSize = self.namePreview.update(
transition: transition, transition: previewTransition,
component: AnyComponent(TopBottomCornersComponent(topCornerRadius: itemCornerRadius, bottomCornerRadius: !self.scrolledUp ? itemCornerRadius : 0.0, component: AnyComponent(ListItemComponentAdaptor( component: AnyComponent(TopBottomCornersComponent(topCornerRadius: itemCornerRadius, bottomCornerRadius: !self.scrolledUp ? itemCornerRadius : 0.0, component: AnyComponent(ListItemComponentAdaptor(
itemGenerator: PeerNameColorChatPreviewItem( itemGenerator: PeerNameColorChatPreviewItem(
context: component.context, 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) let namePreviewFrame = CGRect(origin: CGPoint(x: sideInset, y: environment.navigationHeight + 12.0), size: namePreviewSize)
if let namePreviewView = self.namePreview.view { if let namePreviewView = self.namePreview.view {
if namePreviewView.superview == nil { if namePreviewView.superview == nil {
self.addSubview(namePreviewView) namePreviewView.isUserInteractionEnabled = false
self.containerView.addSubview(namePreviewView)
} }
transition.setFrame(view: namePreviewView, frame: namePreviewFrame) transition.setFrame(view: namePreviewView, frame: namePreviewFrame)
} }
@ -1267,6 +1324,7 @@ final class UserAppearanceScreenComponent: Component {
self.updatedPeerNameEmoji = .some(nil) self.updatedPeerNameEmoji = .some(nil)
} }
self.updatedPeerNameColor = .preset(value) self.updatedPeerNameColor = .preset(value)
self.selectedNameGift = nil
self.state?.updated(transition: .spring(duration: 0.4)) self.state?.updated(transition: .spring(duration: 0.4))
}, },
sectionId: 0 sectionId: 0
@ -1308,83 +1366,141 @@ final class UserAppearanceScreenComponent: Component {
contentHeight += nameColorSectionSize.height contentHeight += nameColorSectionSize.height
contentHeight += sectionSpacing contentHeight += sectionSpacing
if !self.starGifts.isEmpty { var selectedGiftId: Int64?
var selectedGiftId: Int64? if case let .collectible(collectibleColor) = resolvedState.nameColor {
if case let .collectible(collectibleColor) = resolvedState.nameColor { selectedGiftId = collectibleColor.collectibleId
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
} }
//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 contentHeight += bottomContentInset
//TODO:localize //TODO:localize
let buttonSideInset: CGFloat = 36.0 let buttonSideInset: CGFloat = environment.safeInsets.left + 36.0
let buttonTitle = "Apply Style" // environment.strings.Channel_Appearance_ApplyButton var buttonTitle = "Apply Style" // environment.strings.Channel_Appearance_ApplyButton
// if let emojiStatus = resolvedState.emojiStatus, case .starGift = emojiStatus.content, resolvedState.changes.contains(.emojiStatus) { var buttonAttributedSubtitleString: NSMutableAttributedString?
// buttonTitle = environment.strings.NameColor_WearCollectible
// } 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( let buttonSize = self.actionButton.update(
transition: transition, transition: transition,
component: AnyComponent(ButtonComponent( component: AnyComponent(ButtonComponent(
@ -1426,7 +1542,7 @@ final class UserAppearanceScreenComponent: Component {
transition.setAlpha(view: buttonView, alpha: 1.0) 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)) 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) 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) 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 { if self.scrollView.contentSize != contentSize {
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 { transition.setFrame(view: self.containerView, frame: CGRect(origin: .zero, size: availableSize))
// self.scrollView.verticalScrollIndicatorInsets = scrollInsets
// }
if !previousBounds.isEmpty, !transition.animation.isImmediate { if !previousBounds.isEmpty, !transition.animation.isImmediate {
let bounds = self.scrollView.bounds let bounds = self.scrollView.bounds

View File

@ -36,15 +36,18 @@ public final class TabSelectorComponent: Component {
public struct Colors: Equatable { public struct Colors: Equatable {
public var foreground: UIColor public var foreground: UIColor
public var selection: UIColor public var selection: UIColor
public var normal: UIColor?
public var simple: Bool public var simple: Bool
public init( public init(
foreground: UIColor, foreground: UIColor,
selection: UIColor, selection: UIColor,
normal: UIColor? = nil,
simple: Bool = false simple: Bool = false
) { ) {
self.foreground = foreground self.foreground = foreground
self.selection = selection self.selection = selection
self.normal = normal
self.simple = simple self.simple = simple
} }
} }
@ -58,7 +61,7 @@ public final class TabSelectorComponent: Component {
public var verticalInset: CGFloat public var verticalInset: CGFloat
public var allowScroll: Bool 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.font = font
self.spacing = spacing self.spacing = spacing
self.innerSpacing = innerSpacing self.innerSpacing = innerSpacing
@ -611,6 +614,9 @@ public final class TabSelectorComponent: Component {
if case .component = item.content { if case .component = item.content {
useSelectionFraction = true useSelectionFraction = true
} }
if let _ = component.colors.normal {
useSelectionFraction = true
}
let itemSize = itemView.title.update( let itemSize = itemView.title.update(
transition: itemTransition, transition: itemTransition,
@ -619,6 +625,7 @@ public final class TabSelectorComponent: Component {
content: item.content, content: item.content,
font: itemFont, font: itemFont,
color: component.colors.foreground, color: component.colors.foreground,
normalColor: component.colors.normal,
selectedColor: component.colors.selection, selectedColor: component.colors.selection,
selectionFraction: useSelectionFraction ? selectionFraction : 0.0 selectionFraction: useSelectionFraction ? selectionFraction : 0.0
)), )),
@ -805,6 +812,7 @@ private final class ItemComponent: CombinedComponent {
let content: TabSelectorComponent.Item.Content let content: TabSelectorComponent.Item.Content
let font: UIFont let font: UIFont
let color: UIColor let color: UIColor
let normalColor: UIColor?
let selectedColor: UIColor let selectedColor: UIColor
let selectionFraction: CGFloat let selectionFraction: CGFloat
@ -813,6 +821,7 @@ private final class ItemComponent: CombinedComponent {
content: TabSelectorComponent.Item.Content, content: TabSelectorComponent.Item.Content,
font: UIFont, font: UIFont,
color: UIColor, color: UIColor,
normalColor: UIColor?,
selectedColor: UIColor, selectedColor: UIColor,
selectionFraction: CGFloat selectionFraction: CGFloat
) { ) {
@ -820,6 +829,7 @@ private final class ItemComponent: CombinedComponent {
self.content = content self.content = content
self.font = font self.font = font
self.color = color self.color = color
self.normalColor = normalColor
self.selectedColor = selectedColor self.selectedColor = selectedColor
self.selectionFraction = selectionFraction self.selectionFraction = selectionFraction
} }
@ -837,6 +847,9 @@ private final class ItemComponent: CombinedComponent {
if lhs.color != rhs.color { if lhs.color != rhs.color {
return false return false
} }
if lhs.normalColor != rhs.normalColor {
return false
}
if lhs.selectedColor != rhs.selectedColor { if lhs.selectedColor != rhs.selectedColor {
return false return false
} }
@ -856,7 +869,7 @@ private final class ItemComponent: CombinedComponent {
switch component.content { switch component.content {
case let .text(text): 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: "⭐️") var range = (attributedTitle.string as NSString).range(of: "⭐️")
if range.location != NSNotFound { if range.location != NSNotFound {
attributedTitle.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range) 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) .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: "⭐️") range = (selectedAttributedTitle.string as NSString).range(of: "⭐️")
if range.location != NSNotFound { if range.location != NSNotFound {
selectedAttributedTitle.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range) 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"> "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"> <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> <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="1.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"/>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 879 B

After

Width:  |  Height:  |  Size: 811 B

Binary file not shown.