import Foundation import UIKit import Display import ComponentFlow import SwiftSignalKit import TelegramCore import Markdown import TextFormat import TelegramPresentationData import ViewControllerComponent import SheetComponent import BundleIconComponent import BalancedTextComponent import MultilineTextComponent import ButtonComponent import PlainButtonComponent import GiftItemComponent import AccountContext private final class SheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let gifts: [ProfileGiftsContext.State.StarGift] let completion: (StarGiftReference) -> Void let dismiss: () -> Void init( context: AccountContext, gifts: [ProfileGiftsContext.State.StarGift], completion: @escaping (StarGiftReference) -> Void, dismiss: @escaping () -> Void ) { self.context = context self.gifts = gifts self.completion = completion self.dismiss = dismiss } static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.gifts != rhs.gifts { return false } return true } final class State: ComponentState { var selectedGift: StarGiftReference? } func makeState() -> State { return State() } static var body: Body { let closeButton = Child(Button.self) let title = Child(BalancedTextComponent.self) let text = Child(BalancedTextComponent.self) let gifts = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) let button = Child(ButtonComponent.self) return { context in let environment = context.environment[EnvironmentType.self] let component = context.component let state = context.state let theme = environment.theme let strings = environment.strings let sideInset: CGFloat = 16.0 + environment.safeInsets.left let textSideInset: CGFloat = 32.0 + environment.safeInsets.left let titleFont = Font.semibold(17.0) let subtitleFont = Font.regular(12.0) let textColor = theme.actionSheet.primaryTextColor let secondaryTextColor = theme.actionSheet.secondaryTextColor var contentSize = CGSize(width: context.availableSize.width, height: 10.0) let closeButton = closeButton.update( component: Button( content: AnyComponent(Text(text: strings.Common_Cancel, font: Font.regular(17.0), color: theme.actionSheet.controlAccentColor)), action: { [weak component] in component?.dismiss() } ), availableSize: CGSize(width: 100.0, height: 30.0), transition: .immediate ) context.add(closeButton .position(CGPoint(x: environment.safeInsets.left + 16.0 + closeButton.size.width / 2.0, y: 28.0)) ) let title = title.update( component: BalancedTextComponent( text: .plain(NSAttributedString(string: strings.Gift_Unpin_Title, font: titleFont, textColor: textColor)), horizontalAlignment: .center, maximumNumberOfLines: 1, lineSpacing: 0.1 ), availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), transition: .immediate ) context.add(title .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0)) ) contentSize.height += title.size.height let text = text.update( component: BalancedTextComponent( text: .plain(NSAttributedString(string: strings.Gift_Unpin_Subtitle, font: subtitleFont, textColor: secondaryTextColor)), horizontalAlignment: .center, maximumNumberOfLines: 1, lineSpacing: 0.2 ), availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), transition: .immediate ) context.add(text .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + text.size.height / 2.0)) ) contentSize.height += text.size.height contentSize.height += 17.0 let itemsSideInset = environment.safeInsets.left + 16.0 let spacing: CGFloat = 10.0 let itemsInRow = 3 let width = (context.availableSize.width - itemsSideInset * 2.0 - spacing * CGFloat(itemsInRow - 1)) / CGFloat(itemsInRow) var updatedGifts: [_UpdatedChildComponent] = [] var index = 0 var nextOriginX = itemsSideInset for gift in component.gifts { guard case let .unique(uniqueGift) = gift.gift else { continue } var ribbonColor: GiftItemComponent.Ribbon.Color = .blue for attribute in uniqueGift.attributes { if case let .backdrop(_, innerColor, outerColor, _, _, _) = attribute { ribbonColor = .custom(outerColor, innerColor) break } } updatedGifts.append( gifts[index].update( component: AnyComponent( PlainButtonComponent( content: AnyComponent( GiftItemComponent( 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, mode: .grid ) ), effectAlignment: .center, action: { [weak state] in guard let state else { return } state.selectedGift = gift.reference state.updated(transition: .spring(duration: 0.3)) }, animateAlpha: false ) ), availableSize: CGSize(width: width, height: width), 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 if nextOriginX > context.availableSize.width - itemsSideInset { contentSize.height += updatedGifts[index].size.height + spacing nextOriginX = itemsSideInset } index += 1 } 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( color: theme.list.itemCheckColors.fillColor, foreground: theme.list.itemCheckColors.foregroundColor, pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) ), content: buttonContent, isEnabled: state.selectedGift != nil, displaysProgress: false, action: { [weak state] in guard let state else { return } if let selectedGift = state.selectedGift { component.completion(selectedGift) component.dismiss() } } ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), transition: context.transition ) context.add(button .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0)) .cornerRadius(10.0) ) contentSize.height += button.size.height contentSize.height += 7.0 let effectiveBottomInset: CGFloat = environment.metrics.isTablet ? 0.0 : environment.safeInsets.bottom contentSize.height += 5.0 + effectiveBottomInset return contentSize } } } private final class SheetContainerComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let gifts: [ProfileGiftsContext.State.StarGift] let completion: (StarGiftReference) -> Void init( context: AccountContext, gifts: [ProfileGiftsContext.State.StarGift], completion: @escaping (StarGiftReference) -> Void ) { self.context = context self.gifts = gifts self.completion = completion } static func ==(lhs: SheetContainerComponent, rhs: SheetContainerComponent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.gifts != rhs.gifts { return false } return true } static var body: Body { let sheet = Child(SheetComponent.self) let animateOut = StoredActionSlot(Action.self) let sheetExternalState = SheetComponent.ExternalState() return { context in let environment = context.environment[EnvironmentType.self] let controller = environment.controller let sheet = sheet.update( component: SheetComponent( content: AnyComponent(SheetContent( context: context.component.context, gifts: context.component.gifts, completion: context.component.completion, dismiss: { animateOut.invoke(Action { _ in if let controller = controller() { controller.dismiss(completion: nil) } }) } )), backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), followContentSizeChanges: true, externalState: sheetExternalState, animateOut: animateOut ), environment: { environment SheetComponentEnvironment( isDisplaying: environment.value.isVisible, isCentered: environment.metrics.widthClass == .regular, hasInputHeight: !environment.inputHeight.isZero, regularMetricsSize: CGSize(width: 430.0, height: 900.0), dismiss: { animated in if animated { animateOut.invoke(Action { _ in if let controller = controller() { controller.dismiss(completion: nil) } }) } else { if let controller = controller() { controller.dismiss(completion: nil) } } } ) }, availableSize: context.availableSize, transition: context.transition ) context.add(sheet .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) ) if let controller = controller(), !controller.automaticallyControlPresentationContextLayout { let layout = ContainerViewLayout( size: context.availableSize, metrics: environment.metrics, deviceMetrics: environment.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: max(environment.safeInsets.bottom, sheetExternalState.contentHeight), right: 0.0), safeInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right), additionalInsets: .zero, statusBarHeight: environment.statusBarHeight, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false ) controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition) } return context.availableSize } } } 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], completion: @escaping (StarGiftReference) -> Void ) { self.context = context self.gifts = gifts self.completion = completion super.init( context: context, component: SheetContainerComponent( context: context, gifts: gifts, completion: completion ), navigationBarAppearance: .none, statusBarStyle: .ignore, theme: .default ) self.navigationPresentation = .flatModal } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } public func dismissAnimated() { if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { view.dismissAnimated() } } }