@@ -382,12 +382,102 @@ private final class LoadingProgressNode: ASDisplayNode {
382382 }
383383}
384384
385+ private final class BadgeNode : ASDisplayNode {
386+ private var fillColor : UIColor
387+ private var strokeColor : UIColor
388+ private var textColor : UIColor
389+
390+ private let textNode : ImmediateTextNode
391+ private let backgroundNode : ASImageNode
392+
393+ private let font : UIFont = Font . with ( size: 15.0 , design: . round, weight: . bold)
394+
395+ var text : String = " " {
396+ didSet {
397+ self . textNode. attributedText = NSAttributedString ( string: self . text, font: self . font, textColor: self . textColor)
398+ self . invalidateCalculatedLayout ( )
399+ }
400+ }
401+
402+ init ( fillColor: UIColor , strokeColor: UIColor , textColor: UIColor ) {
403+ self . fillColor = fillColor
404+ self . strokeColor = strokeColor
405+ self . textColor = textColor
406+
407+ self . textNode = ImmediateTextNode ( )
408+ self . textNode. isUserInteractionEnabled = false
409+ self . textNode. displaysAsynchronously = false
410+
411+ self . backgroundNode = ASImageNode ( )
412+ self . backgroundNode. isLayerBacked = true
413+ self . backgroundNode. displayWithoutProcessing = true
414+ self . backgroundNode. displaysAsynchronously = false
415+ self . backgroundNode. image = generateStretchableFilledCircleImage ( diameter: 18.0 , color: fillColor, strokeColor: nil , strokeWidth: 1.0 )
416+
417+ super. init ( )
418+
419+ self . addSubnode ( self . backgroundNode)
420+ self . addSubnode ( self . textNode)
421+
422+ self . isUserInteractionEnabled = false
423+ }
424+
425+ func updateTheme( fillColor: UIColor , strokeColor: UIColor , textColor: UIColor ) {
426+ self . fillColor = fillColor
427+ self . strokeColor = strokeColor
428+ self . textColor = textColor
429+ self . backgroundNode. image = generateStretchableFilledCircleImage ( diameter: 18.0 , color: fillColor, strokeColor: strokeColor, strokeWidth: 1.0 )
430+ self . textNode. attributedText = NSAttributedString ( string: self . text, font: self . font, textColor: self . textColor)
431+ }
432+
433+ func animateBump( incremented: Bool ) {
434+ if incremented {
435+ let firstTransition = ContainedViewLayoutTransition . animated ( duration: 0.1 , curve: . easeInOut)
436+ firstTransition. updateTransformScale ( layer: self . backgroundNode. layer, scale: 1.2 )
437+ firstTransition. updateTransformScale ( layer: self . textNode. layer, scale: 1.2 , completion: { finished in
438+ if finished {
439+ let secondTransition = ContainedViewLayoutTransition . animated ( duration: 0.1 , curve: . easeInOut)
440+ secondTransition. updateTransformScale ( layer: self . backgroundNode. layer, scale: 1.0 )
441+ secondTransition. updateTransformScale ( layer: self . textNode. layer, scale: 1.0 )
442+ }
443+ } )
444+ } else {
445+ let firstTransition = ContainedViewLayoutTransition . animated ( duration: 0.1 , curve: . easeInOut)
446+ firstTransition. updateTransformScale ( layer: self . backgroundNode. layer, scale: 0.8 )
447+ firstTransition. updateTransformScale ( layer: self . textNode. layer, scale: 0.8 , completion: { finished in
448+ if finished {
449+ let secondTransition = ContainedViewLayoutTransition . animated ( duration: 0.1 , curve: . easeInOut)
450+ secondTransition. updateTransformScale ( layer: self . backgroundNode. layer, scale: 1.0 )
451+ secondTransition. updateTransformScale ( layer: self . textNode. layer, scale: 1.0 )
452+ }
453+ } )
454+ }
455+ }
456+
457+ func animateOut( ) {
458+ let timingFunction = CAMediaTimingFunctionName . easeInEaseOut. rawValue
459+ self . backgroundNode. layer. animateScale ( from: 1.0 , to: 0.1 , duration: 0.3 , delay: 0.0 , timingFunction: timingFunction, removeOnCompletion: true , completion: nil )
460+ self . textNode. layer. animateScale ( from: 1.0 , to: 0.1 , duration: 0.3 , delay: 0.0 , timingFunction: timingFunction, removeOnCompletion: true , completion: nil )
461+ }
462+
463+ func update( _ constrainedSize: CGSize ) -> CGSize {
464+ let badgeSize = self . textNode. updateLayout ( constrainedSize)
465+ let backgroundSize = CGSize ( width: max ( 18.0 , badgeSize. width + 8.0 ) , height: 18.0 )
466+ let backgroundFrame = CGRect ( origin: CGPoint ( ) , size: backgroundSize)
467+ self . backgroundNode. frame = backgroundFrame
468+ self . textNode. frame = CGRect ( origin: CGPoint ( x: floorToScreenPixels ( backgroundFrame. midX - badgeSize. width / 2.0 ) , y: floorToScreenPixels ( ( backgroundFrame. size. height - badgeSize. height) / 2.0 ) - UIScreenPixel) , size: badgeSize)
469+
470+ return backgroundSize
471+ }
472+ }
473+
385474private final class MainButtonNode : HighlightTrackingButtonNode {
386475 private var state : AttachmentMainButtonState
387476 private var size : CGSize ?
388477
389478 private let backgroundAnimationNode : ASImageNode
390479 fileprivate let textNode : ImmediateTextNode
480+ private var badgeNode : BadgeNode ?
391481 private let statusNode : SemanticStatusNode
392482 private var progressNode : ASImageNode ?
393483
@@ -616,6 +706,7 @@ private final class MainButtonNode: HighlightTrackingButtonNode {
616706 progressNode. image = generateIndefiniteActivityIndicatorImage ( color: state. textColor, diameter: diameter, lineWidth: 3.0 )
617707 }
618708
709+ var textFrame : CGRect = . zero
619710 if let text = state. text {
620711 let font : UIFont
621712 switch state. font {
@@ -627,13 +718,7 @@ private final class MainButtonNode: HighlightTrackingButtonNode {
627718 self . textNode. attributedText = NSAttributedString ( string: text, font: font, textColor: state. textColor)
628719
629720 let textSize = self . textNode. updateLayout ( size)
630- let textFrame = CGRect ( origin: CGPoint ( x: floorToScreenPixels ( ( size. width - textSize. width) / 2.0 ) , y: floorToScreenPixels ( ( size. height - textSize. height) / 2.0 ) ) , size: textSize)
631- if self . textNode. frame. width. isZero {
632- self . textNode. frame = textFrame
633- } else {
634- self . textNode. bounds = CGRect ( origin: . zero, size: textSize)
635- transition. updatePosition ( node: self . textNode, position: textFrame. center)
636- }
721+ textFrame = CGRect ( origin: CGPoint ( x: floorToScreenPixels ( ( size. width - textSize. width) / 2.0 ) , y: floorToScreenPixels ( ( size. height - textSize. height) / 2.0 ) ) , size: textSize)
637722
638723 switch state. background {
639724 case let . color( backgroundColor) :
@@ -669,6 +754,40 @@ private final class MainButtonNode: HighlightTrackingButtonNode {
669754 }
670755 }
671756
757+ if let badge = state. badge {
758+ let badgeNode : BadgeNode
759+ var badgeTransition = transition
760+ if let current = self . badgeNode {
761+ badgeNode = current
762+ } else {
763+ badgeTransition = . immediate
764+ var textColor : UIColor
765+ switch state. background {
766+ case let . color( backgroundColor) :
767+ textColor = backgroundColor
768+ case . premium:
769+ textColor = UIColor ( rgb: 0x0077ff )
770+ }
771+ badgeNode = BadgeNode ( fillColor: state. textColor, strokeColor: . clear, textColor: textColor)
772+ self . badgeNode = badgeNode
773+ self . addSubnode ( badgeNode)
774+ }
775+ badgeNode. text = badge
776+ let badgeSize = badgeNode. update ( CGSize ( width: 100.0 , height: 100.0 ) )
777+ textFrame. origin. x -= badgeSize. width / 2.0
778+ badgeTransition. updateFrame ( node: badgeNode, frame: CGRect ( origin: CGPoint ( x: textFrame. maxX + 6.0 , y: textFrame. minY + floorToScreenPixels( ( textFrame. height - badgeSize. height) * 0.5 ) ) , size: badgeSize) )
779+ } else if let badgeNode = self . badgeNode {
780+ self . badgeNode = nil
781+ badgeNode. removeFromSupernode ( )
782+ }
783+
784+ if self . textNode. frame. width. isZero {
785+ self . textNode. frame = textFrame
786+ } else {
787+ self . textNode. bounds = CGRect ( origin: . zero, size: textFrame. size)
788+ transition. updatePosition ( node: self . textNode, position: textFrame. center)
789+ }
790+
672791 if previousState. progress != state. progress {
673792 if state. progress == . center {
674793 self . transitionToProgress ( )
0 commit comments