Improved pinned gift replace panel

This commit is contained in:
Ilya Laktyushin 2025-03-27 15:13:08 +04:00
parent b19e056a37
commit 19e37d149b
7 changed files with 131 additions and 87 deletions

View File

@ -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";

View File

@ -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

View File

@ -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

View File

@ -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<Int64>()
@ -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

View File

@ -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<Empty>
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<EnvironmentType>(
content: AnyComponent<EnvironmentType>(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,

View File

@ -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))"
}

View File

@ -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