From 19e37d149b52b88cdcb4510ad45d51f2babc89b2 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 27 Mar 2025 15:13:08 +0400 Subject: [PATCH] Improved pinned gift replace panel --- .../Telegram-iOS/en.lproj/Localizable.strings | 1 + .../Source/Base/CombinedComponent.swift | 9 ++ .../Source/Base/Transition.swift | 4 +- .../Sources/GiftItemComponent.swift | 42 +++-- .../Sources/GiftUnpinScreen.swift | 149 ++++++++++-------- .../Sources/PeerInfoGiftsPaneNode.swift | 9 +- .../Sources/GiftListItemComponent.swift | 4 +- 7 files changed, 131 insertions(+), 87 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 041039351f..6a20dc96be 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -14094,6 +14094,7 @@ Sorry for the inconvenience."; "Gift.Unpin.Title" = "Too Manu Pinned Gifts"; "Gift.Unpin.Subtitle" = "Select a gift to unpin below:"; "Gift.Unpin.Unpin" = "Unpin"; +"Gift.Unpin.Replace" = "Replace"; "ChatList.Search.Ad" = "Ad"; diff --git a/submodules/ComponentFlow/Source/Base/CombinedComponent.swift b/submodules/ComponentFlow/Source/Base/CombinedComponent.swift index 4f43d6cff3..92505f2900 100644 --- a/submodules/ComponentFlow/Source/Base/CombinedComponent.swift +++ b/submodules/ComponentFlow/Source/Base/CombinedComponent.swift @@ -180,6 +180,7 @@ public final class _UpdatedChildComponent { var _opacity: CGFloat? var _cornerRadius: CGFloat? var _clipsToBounds: Bool? + var _allowsGroupOpacity: Bool? var _shadow: Shadow? fileprivate var transitionAppear: ComponentTransition.Appear? @@ -262,6 +263,11 @@ public final class _UpdatedChildComponent { return self } + @discardableResult public func allowsGroupOpacity(_ allowsGroupOpacity: Bool) -> _UpdatedChildComponent { + self._allowsGroupOpacity = allowsGroupOpacity + return self + } + @discardableResult public func shadow(_ shadow: Shadow?) -> _UpdatedChildComponent { self._shadow = shadow return self @@ -712,6 +718,9 @@ public extension CombinedComponent { updatedChild.view.alpha = updatedChild._opacity ?? 1.0 updatedChild.view.clipsToBounds = updatedChild._clipsToBounds ?? false updatedChild.view.layer.cornerRadius = updatedChild._cornerRadius ?? 0.0 + if let allowsGroupOpacity = updatedChild._allowsGroupOpacity { + updatedChild.view.layer.allowsGroupOpacity = allowsGroupOpacity + } if let shadow = updatedChild._shadow { updatedChild.view.layer.shadowColor = shadow.color.withAlphaComponent(1.0).cgColor updatedChild.view.layer.shadowRadius = shadow.radius diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index 202a3ddcc2..5dde9d68e0 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -490,9 +490,9 @@ public struct ComponentTransition { self.setScaleWithSpring(layer: view.layer, scale: scale, delay: delay, completion: completion) } - public func setScale(layer: CALayer, scale: CGFloat, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { + public func setScale(layer: CALayer, scale: CGFloat, delay: Double = 0.0, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { let currentTransform: CATransform3D - if layer.animation(forKey: "transform") != nil || layer.animation(forKey: "transform.scale") != nil { + if beginWithCurrentState, layer.animation(forKey: "transform") != nil || layer.animation(forKey: "transform.scale") != nil { currentTransform = layer.presentation()?.transform ?? layer.transform } else { currentTransform = layer.transform diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index 118669cffa..d148d5ce77 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -235,6 +235,8 @@ public final class GiftItemComponent: Component { private var animationLayer: InlineStickerItemLayer? private var selectionLayer: SimpleShapeLayer? + private var animationFile: TelegramMediaFile? + private var disposables = DisposableSet() private var fetchedFiles = Set() @@ -280,7 +282,7 @@ public final class GiftItemComponent: Component { let previousComponent = self.component self.component = component self.componentState = state - + self.isGestureEnabled = component.contextAction != nil var themeUpdated = false @@ -317,6 +319,10 @@ public final class GiftItemComponent: Component { iconSize = CGSize(width: floor(size.width * 0.6), height: floor(size.width * 0.6)) cornerRadius = 4.0 } + var backgroundSize = size + if case .grid = component.mode { + backgroundSize = CGSize(width: backgroundSize.width - 4.0, height: backgroundSize.height - 4.0) + } self.backgroundLayer.cornerRadius = cornerRadius @@ -413,7 +419,14 @@ public final class GiftItemComponent: Component { } } - if self.animationLayer == nil, let emoji { + var animationTransition = transition + if self.animationFile != animationFile, let emoji { + animationTransition = .immediate + self.animationFile = animationFile + if let animationLayer = self.animationLayer { + self.animationLayer = nil + animationLayer.removeFromSuperlayer() + } let animationLayer = InlineStickerItemLayer( context: .account(component.context), userLocation: .other, @@ -434,7 +447,7 @@ public final class GiftItemComponent: Component { let animationFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - iconSize.width) / 2.0), y: component.mode == .generic ? animationOffset : floorToScreenPixels((size.height - iconSize.height) / 2.0)), size: iconSize) if let animationLayer = self.animationLayer { - transition.setFrame(layer: animationLayer, frame: animationFrame) + animationTransition.setFrame(layer: animationLayer, frame: animationFrame) } if let backgroundColor { @@ -445,14 +458,14 @@ public final class GiftItemComponent: Component { subject: .custom(backgroundColor, secondBackgroundColor, patternColor, patternFile?.fileId.id), files: files, isDark: false, - avatarCenter: CGPoint(x: size.width / 2.0, y: animationFrame.midY), + avatarCenter: CGPoint(x: backgroundSize.width / 2.0, y: animationFrame.midY), avatarScale: 1.0, - defaultHeight: size.height, + defaultHeight: backgroundSize.height, avatarTransitionFraction: 0.0, patternTransitionFraction: 0.0 )), environment: {}, - containerSize: availableSize + containerSize: backgroundSize ) if let backgroundView = self.patternView.view { if backgroundView.superview == nil { @@ -463,7 +476,7 @@ public final class GiftItemComponent: Component { backgroundView.clipsToBounds = true self.insertSubview(backgroundView, at: 1) } - backgroundView.frame = CGRect(origin: .zero, size: size) + backgroundView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - backgroundSize.width) / 2.0), y: floorToScreenPixels((size.height - backgroundSize.height) / 2.0)), size: backgroundSize) } } @@ -634,11 +647,17 @@ public final class GiftItemComponent: Component { } self.ribbon.image = generateGradientTintedImage(image: UIImage(bundleImageName: "Premium/GiftRibbon"), colors: ribbon.color.colors(theme: component.theme), direction: direction) } + + var ribbonOffset: CGPoint = CGPoint(x: 2.0, y: -2.0) + if case .grid = component.mode { + ribbonOffset = .zero + } + if let ribbonImage = self.ribbon.image { - self.ribbon.frame = CGRect(origin: CGPoint(x: size.width - ribbonImage.size.width + 2.0, y: -2.0), size: ribbonImage.size) + self.ribbon.frame = CGRect(origin: CGPoint(x: size.width - ribbonImage.size.width + ribbonOffset.x, y: ribbonOffset.y), size: ribbonImage.size) } ribbonTextView.transform = CGAffineTransform(rotationAngle: .pi / 4.0) - ribbonTextView.center = CGPoint(x: size.width - 20.0, y: 20.0) + ribbonTextView.center = CGPoint(x: size.width - 22.0 + ribbonOffset.x, y: 22.0 + ribbonOffset.y) } } else { if self.ribbonText.view?.superview != nil { @@ -676,7 +695,8 @@ public final class GiftItemComponent: Component { self.backgroundLayer.backgroundColor = component.theme.list.itemBlocksBackgroundColor.cgColor } - transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: .zero, size: size)) + let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - backgroundSize.width) / 2.0), y: floorToScreenPixels((size.height - backgroundSize.height) / 2.0)), size: backgroundSize) + transition.setFrame(layer: self.backgroundLayer, frame: backgroundFrame) transition.setFrame(view: self.containerButton, frame: CGRect(origin: .zero, size: size)) var iconBackgroundSize: CGSize? @@ -785,7 +805,7 @@ public final class GiftItemComponent: Component { if case .grid = component.mode { let lineWidth: CGFloat = 2.0 - let selectionFrame = CGRect(origin: .zero, size: size).insetBy(dx: 3.0, dy: 3.0) + let selectionFrame = backgroundFrame.insetBy(dx: 3.0, dy: 3.0) if component.isSelected { let selectionLayer: SimpleShapeLayer diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUnpinScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUnpinScreen.swift index 42fa1867f5..0070e7f7b3 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUnpinScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUnpinScreen.swift @@ -21,18 +21,21 @@ private final class SheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext - let gifts: [ProfileGiftsContext.State.StarGift] + let gift: ProfileGiftsContext.State.StarGift + let pinnedGifts: [ProfileGiftsContext.State.StarGift] let completion: (StarGiftReference) -> Void let dismiss: () -> Void init( context: AccountContext, - gifts: [ProfileGiftsContext.State.StarGift], + gift: ProfileGiftsContext.State.StarGift, + pinnedGifts: [ProfileGiftsContext.State.StarGift], completion: @escaping (StarGiftReference) -> Void, dismiss: @escaping () -> Void ) { self.context = context - self.gifts = gifts + self.gift = gift + self.pinnedGifts = pinnedGifts self.completion = completion self.dismiss = dismiss } @@ -41,7 +44,10 @@ private final class SheetContent: CombinedComponent { if lhs.context !== rhs.context { return false } - if lhs.gifts != rhs.gifts { + if lhs.gift != rhs.gift { + return false + } + if lhs.pinnedGifts != rhs.pinnedGifts { return false } return true @@ -62,6 +68,8 @@ private final class SheetContent: CombinedComponent { let text = Child(BalancedTextComponent.self) let gifts = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) let button = Child(ButtonComponent.self) + + var appliedSelectedGift: StarGiftReference? return { context in let environment = context.environment[EnvironmentType.self] @@ -134,18 +142,30 @@ private final class SheetContent: CombinedComponent { var updatedGifts: [_UpdatedChildComponent] = [] var index = 0 var nextOriginX = itemsSideInset - for gift in component.gifts { + for gift in component.pinnedGifts { guard case let .unique(uniqueGift) = gift.gift else { continue } + var alpha: CGFloat = 1.0 + var displayGift = uniqueGift + if let selectedGift = state.selectedGift { + alpha = selectedGift == gift.reference ? 1.0 : 0.5 + if selectedGift == gift.reference { + if case let .unique(uniqueGift) = component.gift.gift { + displayGift = uniqueGift + } + } + } + var ribbonColor: GiftItemComponent.Ribbon.Color = .blue - for attribute in uniqueGift.attributes { + for attribute in displayGift.attributes { if case let .backdrop(_, innerColor, outerColor, _, _, _) = attribute { ribbonColor = .custom(outerColor, innerColor) break } } + let inset: CGFloat = 2.0 updatedGifts.append( gifts[index].update( component: AnyComponent( @@ -155,9 +175,8 @@ private final class SheetContent: CombinedComponent { context: component.context, theme: theme, strings: strings, - subject: .uniqueGift(gift: uniqueGift), - ribbon: GiftItemComponent.Ribbon(text: "#\(uniqueGift.number)", font: .monospaced, color: ribbonColor), - isSelected: state.selectedGift == gift.reference, + subject: .uniqueGift(gift: displayGift), + ribbon: GiftItemComponent.Ribbon(text: "#\(displayGift.number)", font: .monospaced, color: ribbonColor), mode: .grid ) ), @@ -166,23 +185,45 @@ private final class SheetContent: CombinedComponent { guard let state else { return } - state.selectedGift = gift.reference + if state.selectedGift == gift.reference { + state.selectedGift = nil + } else { + state.selectedGift = gift.reference + } state.updated(transition: .spring(duration: 0.3)) }, animateAlpha: false ) ), - availableSize: CGSize(width: width, height: width), + availableSize: CGSize(width: width + inset * 2.0, height: width + inset * 2.0), transition: context.transition ) ) - context.add(updatedGifts[index] - .position(CGPoint(x: nextOriginX + updatedGifts[index].size.width / 2.0, y: contentSize.height + updatedGifts[index].size.height / 2.0)) - ) - nextOriginX += updatedGifts[index].size.width + spacing + var updatedGift = updatedGifts[index] + .position(CGPoint(x: nextOriginX + updatedGifts[index].size.width / 2.0 - inset, y: contentSize.height + updatedGifts[index].size.height / 2.0 - inset)) + .allowsGroupOpacity(true) + .opacity(alpha) + + if gift.reference == state.selectedGift && appliedSelectedGift != gift.reference { + updatedGift = updatedGift.update(ComponentTransition.Update({ _, view, transition in + UIView.transition(with: view, duration: 0.3, options: [.transitionFlipFromLeft, .curveEaseOut], animations: { + view.alpha = alpha + }) + })) + } else if let appliedSelectedGift, appliedSelectedGift == gift.reference && gift.reference != state.selectedGift { + updatedGift = updatedGift.update(ComponentTransition.Update({ _, view, transition in + UIView.transition(with: view, duration: 0.3, options: [.transitionFlipFromRight, .curveEaseOut], animations: { + view.alpha = alpha + }) + })) + } + + context.add(updatedGift) + + nextOriginX += updatedGifts[index].size.width - inset * 2.0 + spacing if nextOriginX > context.availableSize.width - itemsSideInset { - contentSize.height += updatedGifts[index].size.height + spacing + contentSize.height += updatedGifts[index].size.height - inset * 2.0 + spacing nextOriginX = itemsSideInset } @@ -190,42 +231,6 @@ private final class SheetContent: CombinedComponent { } contentSize.height += 14.0 - - - var giftTitle: String? - if let selectedGift = state.selectedGift, let gift = component.gifts.first(where: { $0.reference == selectedGift }) { - if case let .unique(uniqueGift) = gift.gift { - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - giftTitle = "\(uniqueGift.title) #\(presentationStringsFormattedNumber(uniqueGift.number, presentationData.dateTimeFormat.groupingSeparator))" - } - } - - let buttonContent: AnyComponentWithIdentity - if let giftTitle { - buttonContent = AnyComponentWithIdentity( - id: AnyHashable("unpinGift"), - component: AnyComponent( - VStack([ - AnyComponentWithIdentity( - id: AnyHashable("unpin"), - component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Unpin_Unpin, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) - ), - AnyComponentWithIdentity( - id: AnyHashable(giftTitle), - component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: giftTitle, font: Font.regular(13.0), textColor: theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7), paragraphAlignment: .center)))) - ) - ], spacing: 0.0) - ) - ) - } else { - buttonContent = AnyComponentWithIdentity( - id: AnyHashable("unpin"), - component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Unpin_Unpin, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center))) - ) - ) - } - let button = button.update( component: ButtonComponent( background: ButtonComponent.Background( @@ -233,7 +238,12 @@ private final class SheetContent: CombinedComponent { foreground: theme.list.itemCheckColors.foregroundColor, pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) ), - content: buttonContent, + content: AnyComponentWithIdentity( + id: AnyHashable("unpin"), + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Unpin_Replace, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center))) + ) + ), isEnabled: state.selectedGift != nil, displaysProgress: false, action: { [weak state] in @@ -258,6 +268,8 @@ private final class SheetContent: CombinedComponent { let effectiveBottomInset: CGFloat = environment.metrics.isTablet ? 0.0 : environment.safeInsets.bottom contentSize.height += 5.0 + effectiveBottomInset + + appliedSelectedGift = state.selectedGift return contentSize } @@ -268,16 +280,19 @@ private final class SheetContainerComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext - let gifts: [ProfileGiftsContext.State.StarGift] + let gift: ProfileGiftsContext.State.StarGift + let pinnedGifts: [ProfileGiftsContext.State.StarGift] let completion: (StarGiftReference) -> Void init( context: AccountContext, - gifts: [ProfileGiftsContext.State.StarGift], + gift: ProfileGiftsContext.State.StarGift, + pinnedGifts: [ProfileGiftsContext.State.StarGift], completion: @escaping (StarGiftReference) -> Void ) { self.context = context - self.gifts = gifts + self.gift = gift + self.pinnedGifts = pinnedGifts self.completion = completion } @@ -285,7 +300,10 @@ private final class SheetContainerComponent: CombinedComponent { if lhs.context !== rhs.context { return false } - if lhs.gifts != rhs.gifts { + if lhs.gift != rhs.gift { + return false + } + if lhs.pinnedGifts != rhs.pinnedGifts { return false } return true @@ -306,7 +324,8 @@ private final class SheetContainerComponent: CombinedComponent { component: SheetComponent( content: AnyComponent(SheetContent( context: context.component.context, - gifts: context.component.gifts, + gift: context.component.gift, + pinnedGifts: context.component.pinnedGifts, completion: context.component.completion, dismiss: { animateOut.invoke(Action { _ in @@ -374,24 +393,18 @@ private final class SheetContainerComponent: CombinedComponent { public class GiftUnpinScreen: ViewControllerComponentContainer { - private let context: AccountContext - private let gifts: [ProfileGiftsContext.State.StarGift] - private let completion: (StarGiftReference) -> Void - public init( context: AccountContext, - gifts: [ProfileGiftsContext.State.StarGift], + gift: ProfileGiftsContext.State.StarGift, + pinnedGifts: [ProfileGiftsContext.State.StarGift], completion: @escaping (StarGiftReference) -> Void ) { - self.context = context - self.gifts = gifts - self.completion = completion - super.init( context: context, component: SheetContainerComponent( context: context, - gifts: gifts, + gift: gift, + pinnedGifts: pinnedGifts, completion: completion ), navigationBarAppearance: .none, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index 8fa0aef2d7..5344a720b0 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -376,12 +376,13 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } private func displayUnpinScreen(gift: ProfileGiftsContext.State.StarGift, completion: (() -> Void)? = nil) { - guard let gifts = self.profileGifts.currentState?.gifts.filter({ $0.pinnedToTop }), let presentationData = self.currentParams?.presentationData else { + guard let pinnedGifts = self.profileGifts.currentState?.gifts.filter({ $0.pinnedToTop }), let presentationData = self.currentParams?.presentationData else { return } let controller = GiftUnpinScreen( - context: context, - gifts: gifts, + context: self.context, + gift: gift, + pinnedGifts: pinnedGifts, completion: { [weak self] unpinnedReference in guard let self else { return @@ -389,7 +390,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr completion?() var replacingTitle = "" - for gift in gifts { + for gift in pinnedGifts { if gift.reference == unpinnedReference, case let .unique(uniqueGift) = gift.gift { replacingTitle = "\(uniqueGift.title) #\(presentationStringsFormattedNumber(uniqueGift.number, presentationData.dateTimeFormat.groupingSeparator))" } diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/GiftListItemComponent.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/GiftListItemComponent.swift index 3739f54bc2..8db0756d2d 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/GiftListItemComponent.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/GiftListItemComponent.swift @@ -143,7 +143,7 @@ final class GiftListItemComponent: Component { ) ), environment: {}, - containerSize: itemFrame.size + containerSize: itemFrame.insetBy(dx: -2.0, dy: -2.0).size ) if let itemView = visibleItem.view { if itemView.superview == nil { @@ -154,7 +154,7 @@ final class GiftListItemComponent: Component { itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } } - itemTransition.setFrame(view: itemView, frame: itemFrame) + itemTransition.setFrame(view: itemView, frame: itemFrame.insetBy(dx: -2.0, dy: -2.0)) } } itemFrame.origin.x += itemFrame.width + spacing