Various improvements

This commit is contained in:
Ilya Laktyushin 2025-03-02 04:55:34 +04:00
parent 2adffc2ebc
commit f541820b46
9 changed files with 476 additions and 382 deletions

View File

@ -802,7 +802,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.context,
params.width,
item.presentationData,
presentationStringsFormattedNumber(additionalStats.balances.availableBalance, item.presentationData.dateTimeFormat.groupingSeparator),
formatStarsAmountText(additionalStats.balances.availableBalance, dateTimeFormat: item.presentationData.dateTimeFormat),
" ",
(additionalStats.balances.availableBalance == StarsAmount.zero ? "" : "\(formatTonUsdValue(additionalStats.balances.availableBalance.value, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
.stars
@ -812,7 +812,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.context,
params.width,
item.presentationData,
presentationStringsFormattedNumber(additionalStats.balances.currentBalance, item.presentationData.dateTimeFormat.groupingSeparator),
formatStarsAmountText(additionalStats.balances.currentBalance, dateTimeFormat: item.presentationData.dateTimeFormat),
" ",
(additionalStats.balances.currentBalance == StarsAmount.zero ? "" : "\(formatTonUsdValue(additionalStats.balances.currentBalance.value, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
.stars
@ -822,7 +822,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.context,
params.width,
item.presentationData,
presentationStringsFormattedNumber(additionalStats.balances.overallRevenue, item.presentationData.dateTimeFormat.groupingSeparator),
formatStarsAmountText(additionalStats.balances.overallRevenue, dateTimeFormat: item.presentationData.dateTimeFormat),
" ",
(additionalStats.balances.overallRevenue == StarsAmount.zero ? "" : "\(formatTonUsdValue(additionalStats.balances.overallRevenue.value, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
.stars
@ -871,7 +871,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.context,
params.width,
item.presentationData,
presentationStringsFormattedNumber(stats.balances.availableBalance, item.presentationData.dateTimeFormat.groupingSeparator),
formatStarsAmountText(stats.balances.availableBalance, dateTimeFormat: item.presentationData.dateTimeFormat),
item.presentationData.strings.Monetization_StarsProceeds_Available,
(stats.balances.availableBalance == StarsAmount.zero ? "" : "\(formatTonUsdValue(stats.balances.availableBalance.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
.stars
@ -881,7 +881,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.context,
params.width,
item.presentationData,
presentationStringsFormattedNumber(stats.balances.currentBalance, item.presentationData.dateTimeFormat.groupingSeparator),
formatStarsAmountText(stats.balances.currentBalance, dateTimeFormat: item.presentationData.dateTimeFormat),
item.presentationData.strings.Monetization_StarsProceeds_Current,
(stats.balances.currentBalance == StarsAmount.zero ? "" : "\(formatTonUsdValue(stats.balances.currentBalance.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
.stars
@ -891,7 +891,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.context,
params.width,
item.presentationData,
presentationStringsFormattedNumber(stats.balances.overallRevenue, item.presentationData.dateTimeFormat.groupingSeparator),
formatStarsAmountText(stats.balances.overallRevenue, dateTimeFormat: item.presentationData.dateTimeFormat),
item.presentationData.strings.Monetization_StarsProceeds_Total,
(stats.balances.overallRevenue == StarsAmount.zero ? "" : "\(formatTonUsdValue(stats.balances.overallRevenue.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
.stars

View File

@ -20,6 +20,7 @@ swift_library(
"//submodules/TelegramUI/Components/MultiAnimationRenderer",
"//submodules/TelegramUI/Components/AnimationCache",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/HierarchyTrackingLayer",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
"//submodules/Utils/LokiRng",
"//submodules/TextFormat",

View File

@ -11,53 +11,7 @@ import SwiftSignalKit
import EmojiTextAttachmentView
import LokiRng
import TextFormat
private final class PatternContentsTarget: MultiAnimationRenderTarget {
private let imageUpdated: (Bool) -> Void
init(imageUpdated: @escaping (Bool) -> Void) {
self.imageUpdated = imageUpdated
super.init()
}
required init(coder: NSCoder) {
preconditionFailure()
}
override func transitionToContents(_ contents: AnyObject, didLoop: Bool) {
let hadContents = self.contents != nil
self.contents = contents
self.imageUpdated(hadContents)
}
}
private func windowFunction(t: CGFloat) -> CGFloat {
return bezierPoint(0.6, 0.0, 0.4, 1.0, t)
}
private func patternScaleValueAt(fraction: CGFloat, t: CGFloat, reverse: Bool) -> CGFloat {
let windowSize: CGFloat = 0.8
let effectiveT: CGFloat
let windowStartOffset: CGFloat
let windowEndOffset: CGFloat
if reverse {
effectiveT = 1.0 - t
windowStartOffset = 1.0
windowEndOffset = -windowSize
} else {
effectiveT = t
windowStartOffset = -windowSize
windowEndOffset = 1.0
}
let windowPosition = (1.0 - fraction) * windowStartOffset + fraction * windowEndOffset
let windowT = max(0.0, min(windowSize, effectiveT - windowPosition)) / windowSize
let localT = 1.0 - windowFunction(t: windowT)
return localT
}
import HierarchyTrackingLayer
public final class PeerInfoGiftsCoverComponent: Component {
public let context: AccountContext
@ -65,11 +19,13 @@ public final class PeerInfoGiftsCoverComponent: Component {
public let giftsContext: ProfileGiftsContext
public let hasBackground: Bool
public let avatarCenter: CGPoint
public let avatarScale: CGFloat
public let defaultHeight: CGFloat
public let avatarTransitionFraction: CGFloat
public let patternTransitionFraction: CGFloat
public let statusBarHeight: CGFloat
public let topLeftButtonsSize: CGSize
public let topRightButtonsSize: CGSize
public let titleWidth: CGFloat
public let hasButtons: Bool
public let action: (ProfileGiftsContext.State.StarGift) -> Void
public init(
context: AccountContext,
@ -77,22 +33,26 @@ public final class PeerInfoGiftsCoverComponent: Component {
giftsContext: ProfileGiftsContext,
hasBackground: Bool,
avatarCenter: CGPoint,
avatarScale: CGFloat,
defaultHeight: CGFloat,
avatarTransitionFraction: CGFloat,
patternTransitionFraction: CGFloat,
hasButtons: Bool
statusBarHeight: CGFloat,
topLeftButtonsSize: CGSize,
topRightButtonsSize: CGSize,
titleWidth: CGFloat,
hasButtons: Bool,
action: @escaping (ProfileGiftsContext.State.StarGift) -> Void
) {
self.context = context
self.peerId = peerId
self.giftsContext = giftsContext
self.hasBackground = hasBackground
self.avatarCenter = avatarCenter
self.avatarScale = avatarScale
self.defaultHeight = defaultHeight
self.avatarTransitionFraction = avatarTransitionFraction
self.patternTransitionFraction = patternTransitionFraction
self.statusBarHeight = statusBarHeight
self.topLeftButtonsSize = topLeftButtonsSize
self.topRightButtonsSize = topRightButtonsSize
self.titleWidth = titleWidth
self.hasButtons = hasButtons
self.action = action
}
public static func ==(lhs: PeerInfoGiftsCoverComponent, rhs: PeerInfoGiftsCoverComponent) -> Bool {
@ -108,16 +68,19 @@ public final class PeerInfoGiftsCoverComponent: Component {
if lhs.avatarCenter != rhs.avatarCenter {
return false
}
if lhs.avatarScale != rhs.avatarScale {
return false
}
if lhs.defaultHeight != rhs.defaultHeight {
return false
}
if lhs.avatarTransitionFraction != rhs.avatarTransitionFraction {
return false
}
if lhs.patternTransitionFraction != rhs.patternTransitionFraction {
if lhs.statusBarHeight != rhs.statusBarHeight {
return false
}
if lhs.topLeftButtonsSize != rhs.topLeftButtonsSize {
return false
}
if lhs.topRightButtonsSize != rhs.topRightButtonsSize {
return false
}
if lhs.titleWidth != rhs.titleWidth {
return false
}
if lhs.hasButtons != rhs.hasButtons {
@ -127,11 +90,6 @@ public final class PeerInfoGiftsCoverComponent: Component {
}
public final class View: UIView {
private let avatarBackgroundPatternContentsLayer: SimpleGradientLayer
private let avatarBackgroundPatternMaskLayer: SimpleLayer
private let avatarBackgroundGradientLayer: SimpleGradientLayer
private let backgroundPatternContainer: UIView
private var currentSize: CGSize?
private var component: PeerInfoGiftsCoverComponent?
private var state: EmptyComponentState?
@ -141,33 +99,37 @@ public final class PeerInfoGiftsCoverComponent: Component {
private var appliedGiftIds: [Int64] = []
private var iconLayers: [AnyHashable: GiftIconLayer] = [:]
private var iconPositions: [PositionGenerator.Position] = []
private let seed = UInt(Date().timeIntervalSince1970)
private let trackingLayer = HierarchyTrackingLayer()
private var isCurrentlyInHierarchy = false
private var isUpdating = false
override public init(frame: CGRect) {
self.avatarBackgroundGradientLayer = SimpleGradientLayer()
self.avatarBackgroundGradientLayer.opacity = 0.0
self.avatarBackgroundGradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5)
self.avatarBackgroundGradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
self.avatarBackgroundGradientLayer.type = .radial
self.avatarBackgroundPatternContentsLayer = SimpleGradientLayer()
self.avatarBackgroundPatternContentsLayer.startPoint = CGPoint(x: 0.5, y: 0.5)
self.avatarBackgroundPatternContentsLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
self.avatarBackgroundPatternContentsLayer.type = .radial
self.avatarBackgroundPatternMaskLayer = SimpleLayer()
self.backgroundPatternContainer = UIView()
super.init(frame: frame)
self.clipsToBounds = true
self.avatarBackgroundPatternContentsLayer.mask = self.avatarBackgroundPatternMaskLayer
self.layer.addSublayer(self.avatarBackgroundPatternContentsLayer)
self.addSubview(self.backgroundPatternContainer)
self.layer.addSublayer(self.trackingLayer)
self.trackingLayer.didEnterHierarchy = { [weak self] in
guard let self else {
return
}
self.isCurrentlyInHierarchy = true
self.updateAnimations()
}
self.trackingLayer.didExitHierarchy = { [weak self] in
guard let self else {
return
}
self.isCurrentlyInHierarchy = false
}
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapped(_:))))
}
required public init?(coder aDecoder: NSCoder) {
@ -178,7 +140,38 @@ public final class PeerInfoGiftsCoverComponent: Component {
self.giftsDisposable?.dispose()
}
private var isUpdating = false
@objc private func tapped(_ gestureRecognizer: UITapGestureRecognizer) {
guard let component = self.component else {
return
}
let location = gestureRecognizer.location(in: self)
for (_, iconLayer) in self.iconLayers {
if iconLayer.frame.contains(location) {
component.action(iconLayer.gift)
break
}
}
}
public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
for (_, iconLayer) in self.iconLayers {
if iconLayer.frame.contains(point) {
return true
}
}
return false
}
func updateAnimations() {
var index = 0
for (_, iconLayer) in self.iconLayers {
if self.isCurrentlyInHierarchy {
iconLayer.startAnimations(index: index)
}
index += 1
}
}
func update(component: PeerInfoGiftsCoverComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
@ -202,26 +195,30 @@ public final class PeerInfoGiftsCoverComponent: Component {
}
}
if previousCurrentSize?.width != availableSize.width || (previousComponent != nil && previousComponent?.hasBackground != component.hasBackground) || self.appliedGiftIds != giftIds {
if !giftIds.isEmpty && (self.iconPositions.isEmpty || previousCurrentSize?.width != availableSize.width || (previousComponent != nil && previousComponent?.hasBackground != component.hasBackground) || self.appliedGiftIds != giftIds) {
var excludeRects: [CGRect] = []
excludeRects.append(CGRect(origin: .zero, size: CGSize(width: 50.0, height: 90.0)))
excludeRects.append(CGRect(origin: CGPoint(x: availableSize.width - 105.0, y: 0.0), size: CGSize(width: 105.0, height: 90.0)))
excludeRects.append(CGRect(origin: CGPoint(x: floor((availableSize.width - 390.0) / 2.0), y: 0.0), size: CGSize(width: 390.0, height: 50.0)))
excludeRects.append(CGRect(origin: CGPoint(x: floor((availableSize.width - 280.0) / 2.0), y: component.avatarCenter.y + 56.0), size: CGSize(width: 280.0, height: 65.0)))
if component.statusBarHeight > 0.0 {
excludeRects.append(CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: component.statusBarHeight + 4.0)))
}
excludeRects.append(CGRect(origin: CGPoint(x: 0.0, y: component.statusBarHeight), size: component.topLeftButtonsSize))
excludeRects.append(CGRect(origin: CGPoint(x: availableSize.width - component.topRightButtonsSize.width, y: component.statusBarHeight), size: component.topRightButtonsSize))
excludeRects.append(CGRect(origin: CGPoint(x: floor((availableSize.width - component.titleWidth) / 2.0), y: component.avatarCenter.y + 56.0), size: CGSize(width: component.titleWidth, height: 72.0)))
if component.hasButtons {
excludeRects.append(CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - 81.0), size: CGSize(width: availableSize.width, height: 81.0)))
}
let positionGenerator = PositionGenerator(
containerSize: availableSize,
avatarFrame: CGSize(width: 100, height: 100).centered(around: component.avatarCenter),
minDistance: 75.0,
maxDistance: availableSize.width / 2.0,
padding: 12.0,
seed: UInt(Date().timeIntervalSince1970),
excludeRects: excludeRects
centerFrame: CGSize(width: 100, height: 100).centered(around: component.avatarCenter),
exclusionZones: excludeRects,
minimumDistance: 42.0,
edgePadding: 5.0,
seed: self.seed
)
self.iconPositions = positionGenerator.generatePositions(count: 9, viewSize: iconSize)
let start = CACurrentMediaTime()
self.iconPositions = positionGenerator.generatePositions(count: 12, itemSize: iconSize)
print("generated icon positions in \( CACurrentMediaTime() - start )s")
}
self.appliedGiftIds = giftIds
@ -257,22 +254,13 @@ public final class PeerInfoGiftsCoverComponent: Component {
}
})
}
let avatarPatternFrame = CGSize(width: 380.0, height: floor(component.defaultHeight * 1.0)).centered(around: component.avatarCenter)
transition.setFrame(layer: self.avatarBackgroundPatternContentsLayer, frame: avatarPatternFrame)
self.avatarBackgroundPatternContentsLayer.colors = [
UIColor.red.withAlphaComponent(0.6).cgColor,
UIColor.red.withAlphaComponent(0.0).cgColor
]
let backgroundPatternContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height), size: CGSize(width: availableSize.width, height: 0.0))
transition.containedViewLayoutTransition.updateFrameAdditive(view: self.backgroundPatternContainer, frame: backgroundPatternContainerFrame)
transition.setAlpha(view: self.backgroundPatternContainer, alpha: component.patternTransitionFraction)
var validIds = Set<AnyHashable>()
var index = 0
for gift in self.gifts.prefix(9) {
for gift in self.gifts.prefix(12) {
guard index < self.iconPositions.count else {
break
}
let id: AnyHashable
if case let .unique(uniqueGift) = gift.gift {
id = uniqueGift.slug
@ -289,12 +277,13 @@ public final class PeerInfoGiftsCoverComponent: Component {
} else {
iconTransition = .immediate
iconLayer = GiftIconLayer(context: component.context, gift: gift, size: iconSize, glowing: component.hasBackground)
iconLayer.startHovering()
self.iconLayers[id] = iconLayer
self.layer.addSublayer(iconLayer)
iconLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
iconLayer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
iconLayer.startAnimations(index: index)
}
iconLayer.glowing = component.hasBackground
@ -350,202 +339,6 @@ public final class PeerInfoGiftsCoverComponent: Component {
}
}
private class PositionGenerator {
private let containerSize: CGSize
private let avatarFrame: CGRect
private let padding: CGFloat
private let minDistance: CGFloat
private let maxDistance: CGFloat
private let rng: LokiRng
private let excludeRects: [CGRect]
struct Position {
let center: CGPoint
let scale: CGFloat
}
init(
containerSize: CGSize,
avatarFrame: CGRect,
minDistance: CGFloat,
maxDistance: CGFloat,
padding: CGFloat,
seed: UInt,
excludeRects: [CGRect] = []
) {
self.containerSize = containerSize
self.avatarFrame = avatarFrame
self.minDistance = minDistance
self.maxDistance = maxDistance
self.padding = padding
self.rng = LokiRng(seed0: seed, seed1: 0, seed2: 0)
self.excludeRects = excludeRects
}
func generatePositions(count: Int, viewSize: CGSize) -> [Position] {
let safeCount = min(max(count, 1), 12) // Ensure between 1 and 12
var positions: [Position] = []
let distanceRanges = calculateDistanceRanges(count: safeCount)
for i in 0..<safeCount {
let minDist = distanceRanges[i].0
let maxDist = distanceRanges[i].1
let isEven = i % 2 == 0
var attempts = 0
let maxAttempts = 20
var currentMaxDist = maxDist
var result: CGPoint?
while result == nil && attempts < maxAttempts {
attempts += 1
if let position = generateSinglePosition(
viewSize: viewSize,
minDist: minDist,
maxDist: currentMaxDist,
rightSide: !isEven
) {
let isFarEnough = positions.isEmpty || positions.allSatisfy { existingPosition in
let distance = hypot(position.x - existingPosition.center.x, position.y - existingPosition.center.y)
let minRequiredDistance = max(viewSize.width, viewSize.height) / 2 + max(viewSize.width, viewSize.height) / 2 + padding
return distance > minRequiredDistance
}
if isFarEnough {
result = position
break
}
}
if attempts % 5 == 0 && result == nil {
currentMaxDist *= 1.2
}
}
if result == nil {
if let lastChancePosition = self.generateSinglePosition(
viewSize: viewSize,
minDist: minDist,
maxDist: maxDist * 2.0,
rightSide: !isEven
) {
result = lastChancePosition
} else {
let defaultX = self.avatarFrame.center.x + (isEven ? -1 : 1) * (minDist + CGFloat(i * 20))
let defaultY = self.avatarFrame.center.y + CGFloat(i * 15)
let defaultPosition = CGPoint(x: defaultX, y: defaultY)
result = defaultPosition
}
}
if let result {
let distance = hypot(result.x - self.avatarFrame.center.x, result.y - self.avatarFrame.center.y)
let baseScale = min(1.0, max(0.77, 1.0 - (distance - 75.0) / 75.0))
let randomFactor = 0.14 + (1.0 - baseScale) * 0.2
let randomValue = -randomFactor + CGFloat(self.rng.next()) * 2.0 * randomFactor
let finalScale = min(1.2, max(baseScale * 0.65, baseScale + randomValue))
positions.append(Position(center: result, scale: finalScale))
}
}
return positions.map {
Position(center: $0.center.offsetBy(dx: -self.avatarFrame.center.x, dy: -self.avatarFrame.center.y), scale: $0.scale)
}
}
private func calculateDistanceRanges(count: Int) -> [(CGFloat, CGFloat)] {
var ranges: [(CGFloat, CGFloat)] = []
let totalRange = self.maxDistance - self.minDistance
for _ in 0..<4 {
let min = self.minDistance
let max = self.minDistance + (totalRange * 0.12)
ranges.append((min, max))
}
for _ in 0..<4 {
let min = self.minDistance + (totalRange * 0.19)
let max = self.minDistance + (totalRange * 0.55)
ranges.append((min, max))
}
for _ in 0..<4 {
let min = self.minDistance + (totalRange * 0.6)
let max = self.minDistance + (totalRange * 0.9)
ranges.append((min, max))
}
return ranges
}
private func generateSinglePosition(viewSize: CGSize, minDist: CGFloat, maxDist: CGFloat, rightSide: Bool) -> CGPoint? {
let avatarCenter = avatarFrame.center
for _ in 0..<50 {
let baseAngle: CGFloat
let angleSpread: CGFloat
if rightSide {
baseAngle = 0
angleSpread = .pi / 2
} else {
baseAngle = .pi
angleSpread = .pi / 2
}
let angleOffset = (CGFloat(rng.next()) * 2.0 - 1.0) * angleSpread
let angle = baseAngle + angleOffset
let distance = minDist + CGFloat(rng.next()) * (maxDist - minDist)
let x = avatarCenter.x + cos(angle) * distance
let y = avatarCenter.y + sin(angle) * distance
let position = CGPoint(x: x, y: y)
let viewFrame = CGRect(
x: position.x - viewSize.width / 2,
y: position.y - viewSize.height / 2,
width: viewSize.width,
height: viewSize.height
)
if isFrameWithinBounds(viewFrame) && !isFrameInExclusionZone(viewFrame) {
return CGPoint(x: round(position.x), y: round(position.y))
}
}
return nil
}
private func isFrameWithinBounds(_ frame: CGRect) -> Bool {
return frame.minX >= self.padding &&
frame.minY >= self.padding &&
frame.maxX <= self.containerSize.width - self.padding &&
frame.maxY <= self.containerSize.height - self.padding
}
private func isFrameInExclusionZone(_ frame: CGRect) -> Bool {
if frame.intersects(avatarFrame) {
return true
}
let padding: CGFloat = -8.0
for excludeRect in self.excludeRects {
if frame.intersects(excludeRect.insetBy(dx: padding, dy: padding)) {
return true
}
}
return false
}
}
private var shadowImage: UIImage? = {
return generateImage(CGSize(width: 44.0, height: 44.0), rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
@ -616,10 +409,9 @@ private final class StarsEffectLayer: SimpleLayer {
}
}
private class GiftIconLayer: SimpleLayer {
private let context: AccountContext
private let gift: ProfileGiftsContext.State.StarGift
let gift: ProfileGiftsContext.State.StarGift
private let size: CGSize
var glowing: Bool {
didSet {
@ -780,26 +572,273 @@ private class GiftIconLayer: SimpleLayer {
self.animationLayer.frame = CGRect(origin: .zero, size: self.bounds.size)
}
func startHovering(distance: CGFloat = 3.0, duration: TimeInterval = 4.0, timingFunction: CAMediaTimingFunction = CAMediaTimingFunction(name: .easeInEaseOut)) {
let hoverAnimation = CABasicAnimation(keyPath: "transform.translation.y")
hoverAnimation.duration = duration
hoverAnimation.fromValue = -distance
hoverAnimation.toValue = distance
hoverAnimation.autoreverses = true
hoverAnimation.repeatCount = .infinity
hoverAnimation.timingFunction = timingFunction
hoverAnimation.beginTime = Double.random(in: 0.0 ..< 12.0)
hoverAnimation.isAdditive = true
self.add(hoverAnimation, forKey: "hover")
func startAnimations(index: Int) {
let beginTime = Double(index) * 1.5
let glowAnimation = CABasicAnimation(keyPath: "transform.scale")
glowAnimation.duration = duration
glowAnimation.fromValue = 1.0
glowAnimation.toValue = 1.2
glowAnimation.autoreverses = true
glowAnimation.repeatCount = .infinity
glowAnimation.timingFunction = timingFunction
glowAnimation.beginTime = Double.random(in: 0.0 ..< 12.0)
self.shadowLayer.add(glowAnimation, forKey: "glow")
if self.animation(forKey: "hover") == nil {
let upDistance = CGFloat.random(in: 1.0 ..< 2.0)
let downDistance = CGFloat.random(in: 1.0 ..< 2.0)
let hoverDuration = TimeInterval.random(in: 3.5 ..< 4.5)
let hoverAnimation = CABasicAnimation(keyPath: "transform.translation.y")
hoverAnimation.duration = duration
hoverAnimation.fromValue = -upDistance
hoverAnimation.toValue = downDistance
hoverAnimation.autoreverses = true
hoverAnimation.repeatCount = .infinity
hoverAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
hoverAnimation.beginTime = beginTime
hoverAnimation.isAdditive = true
self.add(hoverAnimation, forKey: "hover")
}
if self.animationLayer.animation(forKey: "wiggle") == nil {
let fromRotationAngle = CGFloat.random(in: 0.025 ..< 0.05)
let toRotationAngle = CGFloat.random(in: 0.025 ..< 0.05)
let wiggleDuration = TimeInterval.random(in: 2.0 ..< 3.0)
let wiggleAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
wiggleAnimation.duration = wiggleDuration
wiggleAnimation.fromValue = -fromRotationAngle
wiggleAnimation.toValue = toRotationAngle
wiggleAnimation.autoreverses = true
wiggleAnimation.repeatCount = .infinity
wiggleAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
wiggleAnimation.beginTime = beginTime
wiggleAnimation.isAdditive = true
self.animationLayer.add(wiggleAnimation, forKey: "wiggle")
}
if self.shadowLayer.animation(forKey: "glow") == nil {
let glowDuration = TimeInterval.random(in: 2.0 ..< 3.0)
let glowAnimation = CABasicAnimation(keyPath: "transform.scale")
glowAnimation.duration = glowDuration
glowAnimation.fromValue = 1.0
glowAnimation.toValue = 1.2
glowAnimation.autoreverses = true
glowAnimation.repeatCount = .infinity
glowAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
glowAnimation.beginTime = beginTime
self.shadowLayer.add(glowAnimation, forKey: "glow")
}
}
}
private struct PositionGenerator {
struct Position {
let center: CGPoint
let scale: CGFloat
}
let containerSize: CGSize
let centerFrame: CGRect
let exclusionZones: [CGRect]
let minimumDistance: CGFloat
let edgePadding: CGFloat
let scaleRange: (min: CGFloat, max: CGFloat)
let innerOrbitRange: (min: CGFloat, max: CGFloat)
let outerOrbitRange: (min: CGFloat, max: CGFloat)
let innerOrbitCount: Int
private let lokiRng: LokiRng
init(
containerSize: CGSize,
centerFrame: CGRect,
exclusionZones: [CGRect],
minimumDistance: CGFloat,
edgePadding: CGFloat,
seed: UInt,
scaleRange: (min: CGFloat, max: CGFloat) = (0.7, 1.15),
innerOrbitRange: (min: CGFloat, max: CGFloat) = (1.4, 2.2),
outerOrbitRange: (min: CGFloat, max: CGFloat) = (2.5, 3.6),
innerOrbitCount: Int = 4
) {
self.containerSize = containerSize
self.centerFrame = centerFrame
self.exclusionZones = exclusionZones
self.minimumDistance = minimumDistance
self.edgePadding = edgePadding
self.scaleRange = scaleRange
self.innerOrbitRange = innerOrbitRange
self.outerOrbitRange = outerOrbitRange
self.innerOrbitCount = innerOrbitCount
self.lokiRng = LokiRng(seed0: seed, seed1: 0, seed2: 0)
}
func generatePositions(count: Int, itemSize: CGSize) -> [Position] {
var positions: [Position] = []
let centerPoint = CGPoint(x: self.centerFrame.midX, y: self.centerFrame.midY)
let centerRadius = min(self.centerFrame.width, self.centerFrame.height) / 2.0
let maxAttempts = count * 200
var attempts = 0
var leftPositions = 0
var rightPositions = 0
let innerCount = min(self.innerOrbitCount, count)
while positions.count < innerCount && attempts < maxAttempts {
attempts += 1
let placeOnLeftSide = rightPositions > leftPositions
let orbitRangeSize = self.innerOrbitRange.max - self.innerOrbitRange.min
let orbitDistanceFactor = self.innerOrbitRange.min + orbitRangeSize * CGFloat(self.lokiRng.next())
let orbitDistance = orbitDistanceFactor * centerRadius
let angleRange: CGFloat = placeOnLeftSide ? .pi : .pi
let angleOffset: CGFloat = placeOnLeftSide ? .pi/2 : -(.pi/2)
let angle = angleOffset + angleRange * CGFloat(self.lokiRng.next())
let absoluteX = centerPoint.x + orbitDistance * cos(angle)
let absoluteY = centerPoint.y + orbitDistance * sin(angle)
let absolutePosition = CGPoint(x: absoluteX, y: absoluteY)
if absolutePosition.x - itemSize.width/2 < self.edgePadding ||
absolutePosition.x + itemSize.width/2 > self.containerSize.width - self.edgePadding ||
absolutePosition.y - itemSize.height/2 < self.edgePadding ||
absolutePosition.y + itemSize.height/2 > self.containerSize.height - self.edgePadding {
continue
}
let relativePosition = CGPoint(
x: absolutePosition.x - centerPoint.x,
y: absolutePosition.y - centerPoint.y
)
let itemRect = CGRect(
x: absolutePosition.x - itemSize.width/2,
y: absolutePosition.y - itemSize.height/2,
width: itemSize.width,
height: itemSize.height
)
if self.isValidPosition(itemRect, existingPositions: positions.map { self.posToAbsolute($0.center, centerPoint: centerPoint) }, itemSize: itemSize) {
let scaleRangeSize = max(self.scaleRange.min + 0.1, 0.75) - self.scaleRange.max
let scale = self.scaleRange.max + scaleRangeSize * CGFloat(self.lokiRng.next())
positions.append(Position(center: relativePosition, scale: scale))
if absolutePosition.x < centerPoint.x {
leftPositions += 1
} else {
rightPositions += 1
}
}
}
let maxPossibleDistance = hypot(self.containerSize.width, self.containerSize.height) / 2
while positions.count < count && attempts < maxAttempts {
attempts += 1
let placeOnLeftSide = rightPositions >= leftPositions
let orbitRangeSize = self.outerOrbitRange.max - self.outerOrbitRange.min
let orbitDistanceFactor = self.outerOrbitRange.min + orbitRangeSize * CGFloat(self.lokiRng.next())
let orbitDistance = orbitDistanceFactor * centerRadius
let angleRange: CGFloat = placeOnLeftSide ? .pi : .pi
let angleOffset: CGFloat = placeOnLeftSide ? .pi/2 : -(.pi/2)
let angle = angleOffset + angleRange * CGFloat(self.lokiRng.next())
let absoluteX = centerPoint.x + orbitDistance * cos(angle)
let absoluteY = centerPoint.y + orbitDistance * sin(angle)
let absolutePosition = CGPoint(x: absoluteX, y: absoluteY)
if absolutePosition.x - itemSize.width/2 < self.edgePadding ||
absolutePosition.x + itemSize.width/2 > self.containerSize.width - self.edgePadding ||
absolutePosition.y - itemSize.height/2 < self.edgePadding ||
absolutePosition.y + itemSize.height/2 > self.containerSize.height - self.edgePadding {
continue
}
let relativePosition = CGPoint(
x: absolutePosition.x - centerPoint.x,
y: absolutePosition.y - centerPoint.y
)
let itemRect = CGRect(
x: absolutePosition.x - itemSize.width/2,
y: absolutePosition.y - itemSize.height/2,
width: itemSize.width,
height: itemSize.height
)
if self.isValidPosition(itemRect, existingPositions: positions.map { self.posToAbsolute($0.center, centerPoint: centerPoint) }, itemSize: itemSize) {
let distance = hypot(absolutePosition.x - centerPoint.x, absolutePosition.y - centerPoint.y)
let normalizedDistance = min(distance / maxPossibleDistance, 1.0)
let scale = self.scaleRange.max - normalizedDistance * (self.scaleRange.max - self.scaleRange.min)
positions.append(Position(center: relativePosition, scale: scale))
if absolutePosition.x < centerPoint.x {
leftPositions += 1
} else {
rightPositions += 1
}
}
}
return positions
}
private func posToAbsolute(_ relativePos: CGPoint, centerPoint: CGPoint) -> CGPoint {
return CGPoint(x: relativePos.x + centerPoint.x, y: relativePos.y + centerPoint.y)
}
private func isValidPosition(_ rect: CGRect, existingPositions: [CGPoint], itemSize: CGSize) -> Bool {
if rect.minX < self.edgePadding || rect.maxX > self.containerSize.width - self.edgePadding ||
rect.minY < self.edgePadding || rect.maxY > self.containerSize.height - self.edgePadding {
return false
}
for zone in self.exclusionZones {
if rect.intersects(zone) {
return false
}
}
let effectiveMinDistance = existingPositions.count > 5 ? max(self.minimumDistance * 0.7, 10.0) : self.minimumDistance
for existingPosition in existingPositions {
let distance = hypot(existingPosition.x - rect.midX, existingPosition.y - rect.midY)
if distance < effectiveMinDistance {
return false
}
}
return true
}
}
private func windowFunction(t: CGFloat) -> CGFloat {
return bezierPoint(0.6, 0.0, 0.4, 1.0, t)
}
private func patternScaleValueAt(fraction: CGFloat, t: CGFloat, reverse: Bool) -> CGFloat {
let windowSize: CGFloat = 0.8
let effectiveT: CGFloat
let windowStartOffset: CGFloat
let windowEndOffset: CGFloat
if reverse {
effectiveT = 1.0 - t
windowStartOffset = 1.0
windowEndOffset = -windowSize
} else {
effectiveT = t
windowStartOffset = -windowSize
windowEndOffset = 1.0
}
let windowPosition = (1.0 - fraction) * windowStartOffset + fraction * windowEndOffset
let windowT = max(0.0, min(windowSize, effectiveT - windowPosition)) / windowSize
let localT = 1.0 - windowFunction(t: windowT)
return localT
}

View File

@ -154,6 +154,7 @@ swift_library(
"//submodules/TelegramUI/Components/MediaEditorScreen",
"//submodules/TelegramUI/Components/CameraScreen",
"//submodules/TelegramUI/Components/PeerInfo/VerifyAlertController",
"//submodules/TelegramUI/Components/Gifts/GiftViewScreen",
],
visibility = [
"//visibility:public",

View File

@ -924,13 +924,13 @@ final class PeerInfoHeaderNode: ASDisplayNode {
isVisibleForAnimations: true,
useSharedAnimation: true,
action: { [weak self] in
guard let strongSelf = self else {
guard let self else {
return
}
if let uniqueGiftSlug {
strongSelf.openUniqueGift?(strongSelf.titleStatusIconView, uniqueGiftSlug)
if let uniqueGiftSlug, !self.isSettings {
self.openUniqueGift?(self.titleStatusIconView, uniqueGiftSlug)
} else {
strongSelf.displayPremiumIntro?(strongSelf.titleStatusIconView, currentEmojiStatus, strongSelf.emojiStatusFileAndPackTitle.get(), false)
self.displayPremiumIntro?(self.titleStatusIconView, currentEmojiStatus, self.emojiStatusFileAndPackTitle.get(), false)
}
},
emojiFileUpdated: { [weak self] emojiFile in
@ -985,13 +985,13 @@ final class PeerInfoHeaderNode: ASDisplayNode {
isVisibleForAnimations: true,
useSharedAnimation: true,
action: { [weak self] in
guard let strongSelf = self else {
guard let self else {
return
}
if let uniqueGiftSlug {
strongSelf.openUniqueGift?(strongSelf.titleExpandedStatusIconView, uniqueGiftSlug)
if let uniqueGiftSlug, !self.isSettings {
self.openUniqueGift?(self.titleExpandedStatusIconView, uniqueGiftSlug)
} else {
strongSelf.displayPremiumIntro?(strongSelf.titleExpandedStatusIconView, currentEmojiStatus, strongSelf.emojiStatusFileAndPackTitle.get(), true)
self.displayPremiumIntro?(self.titleExpandedStatusIconView, currentEmojiStatus, self.emojiStatusFileAndPackTitle.get(), true)
}
}
)),
@ -2306,7 +2306,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
subject: backgroundCoverSubject,
files: [:],
isDark: presentationData.theme.overallDarkAppearance,
avatarCenter: apparentAvatarFrame.center,
avatarCenter: apparentAvatarFrame.center.offsetBy(dx: bannerInset, dy: 0.0),
avatarScale: avatarScale,
defaultHeight: backgroundDefaultHeight,
gradientCenter: CGPoint(x: 0.5, y: buttonKeys.isEmpty ? 0.5 : 0.45),
@ -2321,9 +2321,9 @@ final class PeerInfoHeaderNode: ASDisplayNode {
self.backgroundBannerView.addSubview(backgroundCoverView)
}
if additive {
transition.updateFrameAdditive(view: backgroundCoverView, frame: CGRect(origin: CGPoint(x: -3.0, y: bannerFrame.height - backgroundCoverSize.height - bannerInset), size: backgroundCoverSize))
transition.updateFrameAdditive(view: backgroundCoverView, frame: CGRect(origin: CGPoint(x: -bannerInset, y: bannerFrame.height - backgroundCoverSize.height - bannerInset), size: backgroundCoverSize))
} else {
transition.updateFrame(view: backgroundCoverView, frame: CGRect(origin: CGPoint(x: 0.0, y: bannerFrame.height - backgroundCoverSize.height - bannerInset), size: backgroundCoverSize))
transition.updateFrame(view: backgroundCoverView, frame: CGRect(origin: CGPoint(x: -bannerInset, y: bannerFrame.height - backgroundCoverSize.height - bannerInset), size: backgroundCoverSize))
}
if backgroundCoverAnimateIn {
if !self.isAvatarExpanded {
@ -2351,24 +2351,32 @@ final class PeerInfoHeaderNode: ASDisplayNode {
giftsContext: profileGiftsContext,
hasBackground: hasBackground,
avatarCenter: apparentAvatarFrame.center,
avatarScale: avatarScale,
defaultHeight: backgroundDefaultHeight,
avatarTransitionFraction: max(0.0, min(1.0, titleCollapseFraction + transitionFraction * 2.0)),
patternTransitionFraction: buttonsTransitionFraction * backgroundTransitionFraction,
hasButtons: !buttonKeys.isEmpty
statusBarHeight: statusBarHeight,
topLeftButtonsSize: CGSize(width: (self.isSettings ? 57.0 : 47.0), height: 46.0),
topRightButtonsSize: CGSize(width: 76.0 + (self.isMyProfile ? 38.0 : 0.0), height: 46.0),
titleWidth: titleFrame.width + 42.0,
hasButtons: !buttonKeys.isEmpty,
action: { [weak self] gift in
guard let self, case let .unique(gift) = gift.gift else {
return
}
self.openUniqueGift?(self.view, gift.slug)
}
)),
environment: {},
containerSize: CGSize(width: width + bannerInset * 2.0, height: apparentBackgroundHeight + bannerInset)
containerSize: CGSize(width: width, height: apparentBackgroundHeight)
)
if let giftsCoverView = self.giftsCover.view as? PeerInfoGiftsCoverComponent.View {
if giftsCoverView.superview == nil {
self.backgroundBannerView.addSubview(giftsCoverView)
self.view.insertSubview(giftsCoverView, aboveSubview: self.backgroundBannerView)
}
if additive {
transition.updateFrameAdditive(view: giftsCoverView, frame: CGRect(origin: CGPoint(x: -3.0, y: bannerFrame.height - giftsCoverSize.height - bannerInset), size: giftsCoverSize))
transition.updateFrameAdditive(view: giftsCoverView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: giftsCoverSize))
} else {
transition.updateFrame(view: giftsCoverView, frame: CGRect(origin: CGPoint(x: 0.0, y: bannerFrame.height - giftsCoverSize.height - bannerInset), size: giftsCoverSize))
transition.updateFrame(view: giftsCoverView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: giftsCoverSize))
}
navigationTransition.updateAlpha(layer: giftsCoverView.layer, alpha: backgroundBannerAlpha)
}
}
@ -2490,6 +2498,10 @@ final class PeerInfoHeaderNode: ASDisplayNode {
return result
}
if let giftsCoverView = self.giftsCover.view, giftsCoverView.alpha > 0.0, giftsCoverView.point(inside: self.view.convert(point, to: giftsCoverView), with: event) {
return giftsCoverView
}
if result == self.view || result == self.regularContentNode.view || result == self.editingContentNode.view {
return nil
}

View File

@ -111,6 +111,7 @@ import UIKitRuntimeUtils
import OldChannelsController
import UrlHandling
import VerifyAlertController
import GiftViewScreen
public enum PeerInfoAvatarEditingMode {
case generic
@ -4641,10 +4642,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
strongSelf.emojiStatusSelectionController = emojiStatusSelectionController
strongSelf.controller?.present(emojiStatusSelectionController, in: .window(.root))
}
self.headerNode.openUniqueGift = { [weak self] sourceView, _ in
self?.headerNode.displayPremiumIntro?(sourceView, nil, .single(nil), false)
}
} else {
if peerId == context.account.peerId {
self.privacySettings.set(.single(nil) |> then(context.engine.privacy.requestAccountPrivacySettings() |> map(Optional.init)))
@ -4731,13 +4728,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
controller.present(tooltipController, in: .current)
}
self.headerNode.openUniqueGift = { [weak self] _, slug in
guard let self else {
return
}
self.openUrl(url: "https://t.me/nft/\(slug)", concealed: false, external: false)
}
self.headerNode.displayStatusPremiumIntro = { [weak self] in
guard let self else {
return
@ -4834,6 +4824,64 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
}
self.headerNode.openUniqueGift = { [weak self] _, slug in
guard let self, let profileGifts = self.data?.profileGiftsContext else {
return
}
var found = false
if let state = profileGifts.currentState {
for gift in state.gifts {
if case let .unique(uniqueGift) = gift.gift, uniqueGift.slug == slug {
found = true
let controller = GiftViewScreen(
context: self.context,
subject: .profileGift(self.peerId, gift),
updateSavedToProfile: { [weak profileGifts] reference, added in
guard let profileGifts else {
return
}
profileGifts.updateStarGiftAddedToProfile(reference: reference, added: added)
},
convertToStars: { [weak profileGifts] in
guard let profileGifts, let reference = gift.reference else {
return
}
profileGifts.convertStarGift(reference: reference)
},
transferGift: { [weak profileGifts] prepaid, peerId in
guard let profileGifts, let reference = gift.reference else {
return
}
profileGifts.transferStarGift(prepaid: prepaid, reference: reference, peerId: peerId)
},
upgradeGift: { [weak profileGifts] formId, keepOriginalInfo in
guard let profileGifts, let reference = gift.reference else {
return .never()
}
return profileGifts.upgradeStarGift(formId: formId, reference: reference, keepOriginalInfo: keepOriginalInfo)
},
shareStory: { [weak self] uniqueGift in
guard let self, let controller = self.controller else {
return
}
Queue.mainQueue().after(0.15) {
let shareController = self.context.sharedContext.makeStorySharingScreen(context: self.context, subject: .gift(uniqueGift), parentController: controller)
controller.push(shareController)
}
}
)
self.controller?.push(controller)
break
}
}
}
if !found {
self.openUrl(url: "https://t.me/nft/\(slug)", concealed: false, external: false)
}
}
self.headerNode.avatarListNode.listContainerNode.currentIndexUpdated = { [weak self] in
self?.updateNavigation(transition: .immediate, additive: true, animateHeader: true)
}

View File

@ -676,7 +676,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
let buttonSideInset = sideInset + 16.0
let buttonSize = CGSize(width: size.width - buttonSideInset * 2.0, height: 50.0)
var bottomPanelHeight = bottomInset + buttonSize.height + 8.0
var bottomPanelHeight = max(8.0, bottomInset) + buttonSize.height + 8.0
if params.visibleHeight < 110.0 {
scrollOffset -= bottomPanelHeight
}
@ -902,7 +902,6 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
)
if let view = footerText.view {
if view.superview == nil {
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.buttonPressed)))
self.scrollNode.view.addSubview(view)
}
transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: floor((size.width - footerTextSize.width) / 2.0), y: contentHeight), size: footerTextSize))

View File

@ -81,7 +81,7 @@ final class StarsOverviewItemComponent: Component {
valueOffset += icon.size.width
}
let valueString = presentationStringsFormattedNumber(component.value, component.dateTimeFormat.groupingSeparator)
let valueString = formatStarsAmountText(component.value, dateTimeFormat: component.dateTimeFormat)
let usdValueString = formatTonUsdValue(component.value.value, divide: false, rate: component.rate, dateTimeFormat: component.dateTimeFormat)
let valueSize = self.value.update(

View File

@ -2517,7 +2517,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
self.actionButtons.updateAbsoluteRect(CGRect(x: rect.origin.x + actionButtonsFrame.origin.x, y: rect.origin.y + actionButtonsFrame.origin.y, width: actionButtonsFrame.width, height: actionButtonsFrame.height), within: containerSize, transition: transition)
}
let slowModeButtonFrame = CGRect(origin: CGPoint(x: hideOffset.x + width - rightInset - 5.0 - slowModeButtonSize.width + composeButtonsOffset, y: hideOffset.y + panelHeight - minimalHeight + 6.0), size: slowModeButtonSize)
transition.updateFrame(node: self.slowModeButton, frame: slowModeButtonFrame)
@ -2767,12 +2766,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
mediaInputDisabled = false
}
var mediaInputIsActive = false
if case .media = interfaceState.inputMode {
mediaInputIsActive = true
}
self.actionButtons.micButton.fadeDisabled = mediaInputDisabled || mediaInputIsActive
self.actionButtons.micButton.fadeDisabled = mediaInputDisabled
var viewOnceIsVisible = false
if let recordingState = interfaceState.inputTextPanelState.mediaRecordingState {