import Foundation import UIKit import Display import ComponentFlow import SwiftSignalKit import TelegramCore import Markdown import TextFormat import TelegramPresentationData import TelegramStringFormatting import ViewControllerComponent import SheetComponent import BundleIconComponent import BalancedTextComponent import MultilineTextComponent import LottieComponent import ButtonComponent import AccountContext private final class SheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let configuration: AccountFreezeConfiguration let submitAppeal: () -> Void let dismiss: () -> Void init( context: AccountContext, configuration: AccountFreezeConfiguration, submitAppeal: @escaping () -> Void, dismiss: @escaping () -> Void ) { self.context = context self.configuration = configuration self.submitAppeal = submitAppeal self.dismiss = dismiss } static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool { if lhs.context !== rhs.context { return false } return true } final class State: ComponentState { var cachedChevronImage: (UIImage, PresentationTheme)? var cachedCloseImage: (UIImage, PresentationTheme)? let playOnce = ActionSlot() private var didPlayAnimation = false func playAnimationIfNeeded() { guard !self.didPlayAnimation else { return } self.didPlayAnimation = true self.playOnce.invoke(Void()) } } func makeState() -> State { return State() } static var body: Body { let animation = Child(LottieComponent.self) let title = Child(BalancedTextComponent.self) let list = Child(List.self) let actionButton = Child(ButtonComponent.self) let closeButton = 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 = 30.0 + environment.safeInsets.left let titleFont = Font.semibold(20.0) let textColor = theme.actionSheet.primaryTextColor let secondaryTextColor = theme.actionSheet.secondaryTextColor let linkColor = theme.actionSheet.controlAccentColor let spacing: CGFloat = 16.0 var contentSize = CGSize(width: context.availableSize.width, height: 32.0) let animationHeight: CGFloat = 120.0 let animation = animation.update( component: LottieComponent( content: LottieComponent.AppBundleContent(name: "Banned"), startingPosition: .begin, playOnce: state.playOnce ), environment: {}, availableSize: CGSize(width: animationHeight, height: animationHeight), transition: .immediate ) context.add(animation .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + animation.size.height / 2.0)) ) contentSize.height += animation.size.height contentSize.height += spacing + 5.0 let title = title.update( component: BalancedTextComponent( text: .plain(NSAttributedString(string: "Your Account is Frozen", font: titleFont, textColor: textColor)), horizontalAlignment: .center, maximumNumberOfLines: 0, 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 contentSize.height += spacing - 2.0 //TODO:localize var items: [AnyComponentWithIdentity] = [] items.append( AnyComponentWithIdentity( id: "ads", component: AnyComponent(ParagraphComponent( title: "Violation of Terms", titleColor: textColor, text: "Your account was frozen for breaking Telegram's Terms and Conditions.", textColor: secondaryTextColor, iconName: "Account Freeze/Violation", iconColor: linkColor )) ) ) items.append( AnyComponentWithIdentity( id: "split", component: AnyComponent(ParagraphComponent( title: "Read-Only Mode", titleColor: textColor, text: "You can access your account but can't send messages or take actions.", textColor: secondaryTextColor, iconName: "Ads/Privacy", iconColor: linkColor )) ) ) let dateString = stringForFullDate(timestamp: component.configuration.freezeUntilDate ?? 0, strings: strings, dateTimeFormat: environment.dateTimeFormat) items.append( AnyComponentWithIdentity( id: "withdrawal", component: AnyComponent(ParagraphComponent( title: "Appeal Before Deactivation", titleColor: textColor, text: "Appeal via [@SpamBot]() before \(dateString), or your account will be deleted.", textColor: secondaryTextColor, iconName: "Account Freeze/Appeal", iconColor: linkColor, action: { component.submitAppeal() Queue.mainQueue().after(1.0) { component.dismiss() } } )) ) ) let list = list.update( component: List(items), availableSize: CGSize(width: context.availableSize.width - sideInset, height: 10000.0), transition: context.transition ) context.add(list .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + list.size.height / 2.0)) ) contentSize.height += list.size.height contentSize.height += spacing + 2.0 let buttonAttributedString = NSMutableAttributedString(string: "Submit an Appeal", font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) let actionButton = actionButton.update( component: ButtonComponent( background: ButtonComponent.Background( color: environment.theme.list.itemCheckColors.fillColor, foreground: environment.theme.list.itemCheckColors.foregroundColor, pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), cornerRadius: 10.0 ), content: AnyComponentWithIdentity( id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString))) ), isEnabled: true, displaysProgress: false, action: { component.submitAppeal() Queue.mainQueue().after(1.0) { component.dismiss() } } ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), transition: context.transition ) context.add(actionButton .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + actionButton.size.height / 2.0)) .cornerRadius(10.0) ) contentSize.height += actionButton.size.height contentSize.height += 8.0 let closeAttributedString = NSMutableAttributedString(string: "Understood", font: Font.regular(17.0), textColor: environment.theme.list.itemCheckColors.fillColor, paragraphAlignment: .center) let closeButton = closeButton.update( component: ButtonComponent( background: ButtonComponent.Background( color: .clear, foreground: .clear, pressedColor: .clear, cornerRadius: 10.0 ), content: AnyComponentWithIdentity( id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(text: .plain(closeAttributedString))) ), isEnabled: true, displaysProgress: false, action: { component.dismiss() } ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), transition: context.transition ) context.add(closeButton .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + actionButton.size.height / 2.0)) ) contentSize.height += closeButton.size.height if environment.safeInsets.bottom > 0 { contentSize.height += environment.safeInsets.bottom + 5.0 } else { contentSize.height += 12.0 } state.playAnimationIfNeeded() return contentSize } } } private final class SheetContainerComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let configuration: AccountFreezeConfiguration let submitAppeal: () -> Void init( context: AccountContext, configuration: AccountFreezeConfiguration, submitAppeal: @escaping () -> Void ) { self.context = context self.configuration = configuration self.submitAppeal = submitAppeal } static func ==(lhs: SheetContainerComponent, rhs: SheetContainerComponent) -> Bool { if lhs.context !== rhs.context { 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, configuration: context.component.configuration, submitAppeal: context.component.submitAppeal, 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 final class AccountFreezeInfoScreen: ViewControllerComponentContainer { private let context: AccountContext public init( context: AccountContext ) { self.context = context let configuration = AccountFreezeConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) var submitAppealImpl: (() -> Void)? super.init( context: context, component: SheetContainerComponent( context: context, configuration: configuration, submitAppeal: { submitAppealImpl?() } ), navigationBarAppearance: .none, statusBarStyle: .ignore, theme: .default ) self.navigationPresentation = .flatModal submitAppealImpl = { [weak self] in guard let self, let url = configuration.freezeAppealUrl else { return } context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: false, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: self.navigationController as? NavigationController, dismissInput: {}) } } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override public func viewDidLoad() { super.viewDidLoad() self.view.disablesInteractiveModalDismiss = true } public func dismissAnimated() { if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { view.dismissAnimated() } } } private final class ParagraphComponent: CombinedComponent { let title: String let titleColor: UIColor let text: String let textColor: UIColor let iconName: String let iconColor: UIColor let action: (() -> Void)? public init( title: String, titleColor: UIColor, text: String, textColor: UIColor, iconName: String, iconColor: UIColor, action: (() -> Void)? = nil ) { self.title = title self.titleColor = titleColor self.text = text self.textColor = textColor self.iconName = iconName self.iconColor = iconColor self.action = action } static func ==(lhs: ParagraphComponent, rhs: ParagraphComponent) -> Bool { if lhs.title != rhs.title { return false } if lhs.titleColor != rhs.titleColor { return false } if lhs.text != rhs.text { return false } if lhs.textColor != rhs.textColor { return false } if lhs.iconName != rhs.iconName { return false } if lhs.iconColor != rhs.iconColor { return false } return true } static var body: Body { let title = Child(MultilineTextComponent.self) let text = Child(MultilineTextComponent.self) let icon = Child(BundleIconComponent.self) return { context in let component = context.component let leftInset: CGFloat = 64.0 let rightInset: CGFloat = 32.0 let textSideInset: CGFloat = leftInset + 8.0 let spacing: CGFloat = 5.0 let textTopInset: CGFloat = 9.0 let title = title.update( component: MultilineTextComponent( text: .plain(NSAttributedString( string: component.title, font: Font.semibold(15.0), textColor: component.titleColor, paragraphAlignment: .natural )), horizontalAlignment: .center, maximumNumberOfLines: 1 ), availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) let textFont = Font.regular(15.0) let boldTextFont = Font.semibold(15.0) let textColor = component.textColor let linkColor = component.iconColor let markdownAttributes = MarkdownAttributes( body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) } ) let text = text.update( component: MultilineTextComponent( text: .markdown(text: component.text, attributes: markdownAttributes), horizontalAlignment: .natural, maximumNumberOfLines: 0, lineSpacing: 0.2, highlightColor: linkColor.withAlphaComponent(0.1), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) } else { return nil } }, tapAction: { attributes, _ in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { component.action?() } } ), availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: context.availableSize.height), transition: .immediate ) let icon = icon.update( component: BundleIconComponent( name: component.iconName, tintColor: component.iconColor ), availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), transition: .immediate ) context.add(title .position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0)) ) context.add(text .position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0)) ) context.add(icon .position(CGPoint(x: 47.0, y: textTopInset + 18.0)) ) return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 18.0) } } } private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(backgroundColor.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) context.setLineWidth(2.0) context.setLineCap(.round) context.setStrokeColor(foregroundColor.cgColor) context.move(to: CGPoint(x: 10.0, y: 10.0)) context.addLine(to: CGPoint(x: 20.0, y: 20.0)) context.strokePath() context.move(to: CGPoint(x: 20.0, y: 10.0)) context.addLine(to: CGPoint(x: 10.0, y: 20.0)) context.strokePath() }) }