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