From a80a706379f607831140d8abe6e674c6366e1d46 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 22 Sep 2025 06:42:19 +0400 Subject: [PATCH] Various improvements --- .../Sources/VideoChatScreen.swift | 6 +- .../Sources/VoiceChatHeaderButton.swift | 7 +- .../Sources/ButtonComponent.swift | 50 +- .../Sources/GiftItemComponent.swift | 60 +- .../Gifts/GiftLoadingShimmerView/BUILD | 36 ++ .../Sources/GiftLoadingShimmerView.swift | 200 +++++++ .../Components/Gifts/GiftStoreScreen/BUILD | 1 + .../Sources/GiftStoreScreen.swift | 34 +- .../Sources/LoadingShimmerComponent.swift | 195 ------- .../Sources/ListSectionComponent.swift | 2 +- .../PeerInfo/CollectionTabItemComponent/BUILD | 28 + .../Sources/CollectionTabItemComponent.swift | 168 ++++++ .../PeerInfoVisualMediaPaneNode/BUILD | 1 + .../Sources/PeerInfoGiftsPaneNode.swift | 164 +----- .../Settings/PeerNameColorScreen/BUILD | 5 + .../Sources/ChannelAppearanceScreen.swift | 1 + .../Sources/GiftListItemComponent.swift | 370 ++++++++++++- .../PeerNameColorProfilePreviewItem.swift | 8 +- .../Sources/UserApperanceScreen.swift | 512 +++++++++++------- .../Sources/TabSelectorComponent.swift | 24 +- .../TextArrowRight.imageset/chevron.right.svg | 3 +- .../TelegramUI/Resources/Animations/Style.tgs | Bin 0 -> 15444 bytes 22 files changed, 1232 insertions(+), 643 deletions(-) create mode 100644 submodules/TelegramUI/Components/Gifts/GiftLoadingShimmerView/BUILD create mode 100644 submodules/TelegramUI/Components/Gifts/GiftLoadingShimmerView/Sources/GiftLoadingShimmerView.swift delete mode 100644 submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/LoadingShimmerComponent.swift create mode 100644 submodules/TelegramUI/Components/PeerInfo/CollectionTabItemComponent/BUILD create mode 100644 submodules/TelegramUI/Components/PeerInfo/CollectionTabItemComponent/Sources/CollectionTabItemComponent.swift create mode 100644 submodules/TelegramUI/Resources/Animations/Style.tgs 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 0000000000000000000000000000000000000000..578bb1f6bf6aa196afc8d7939dfbb113ca087d19 GIT binary patch literal 15444 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 KTkd?DpaKAOWAfMl literal 0 HcmV?d00001