import Foundation import UIKit import SwiftSignalKit import Display import TelegramPresentationData import ComponentFlow import ComponentDisplayAdapters import AccountContext import ViewControllerComponent import MultilineTextComponent import ButtonComponent import BundleIconComponent import TelegramCore import PresentationDataUtils import ResizableSheetComponent import GlassBarButtonComponent import TabBarComponent import TranslateUI import LottieComponent final class TextProcessingContentComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment final class ExternalState { fileprivate(set) var isProcessing: Bool = false fileprivate(set) var result: TextWithEntities? init() { } } let externalState: ExternalState let context: AccountContext let inputText: TextWithEntities let copyCurrentResult: () -> Void let displayLanguageSelectionMenu: (UIView, String, TelegramComposeAIMessageMode.Style, Bool, @escaping (String, TelegramComposeAIMessageMode.Style) -> Void) -> Void init( externalState: ExternalState, context: AccountContext, inputText: TextWithEntities, copyCurrentResult: @escaping () -> Void, displayLanguageSelectionMenu: @escaping (UIView, String, TelegramComposeAIMessageMode.Style, Bool, @escaping (String, TelegramComposeAIMessageMode.Style) -> Void) -> Void ) { self.externalState = externalState self.context = context self.inputText = inputText self.copyCurrentResult = copyCurrentResult self.displayLanguageSelectionMenu = displayLanguageSelectionMenu } static func ==(lhs: TextProcessingContentComponent, rhs: TextProcessingContentComponent) -> Bool { return true } private enum Mode { case translate case stylize case fix } final class View: UIView { private var component: TextProcessingContentComponent? private weak var state: EmptyComponentState? private var isUpdating: Bool = false private let modeTabs = ComponentView() private let currentContentBackground: UIImageView private let currentContentContainer: UIView private let translateState = TextProcessingTranslateContentComponent.ExternalState() private let stylizeState = TextProcessingTranslateContentComponent.ExternalState() private let fixState = TextProcessingTranslateContentComponent.ExternalState() private var currentContent: (mode: Mode, view: ComponentView)? private var currentMode: Mode = .translate override init(frame: CGRect) { self.currentContentBackground = UIImageView() self.currentContentContainer = UIView() self.currentContentContainer.clipsToBounds = true super.init(frame: frame) self.addSubview(self.currentContentBackground) self.addSubview(self.currentContentContainer) self.translateState.resultUpdated = { [weak self] result in guard let self, let component = self.component else { return } if case .translate = self.currentMode { component.externalState.result = result?.text } } self.translateState.isProcessingUpdated = { [weak self] isProcessing in guard let self, let component = self.component else { return } if case .translate = self.currentMode { component.externalState.isProcessing = isProcessing } } self.stylizeState.resultUpdated = { [weak self] result in guard let self, let component = self.component else { return } if case .stylize = self.currentMode { component.externalState.result = result?.text } } self.stylizeState.isProcessingUpdated = { [weak self] isProcessing in guard let self, let component = self.component else { return } if case .stylize = self.currentMode { component.externalState.isProcessing = isProcessing } } self.fixState.resultUpdated = { [weak self] result in guard let self, let component = self.component else { return } if case .fix = self.currentMode { component.externalState.result = result?.text } } self.fixState.isProcessingUpdated = { [weak self] isProcessing in guard let self, let component = self.component else { return } if case .fix = self.currentMode { component.externalState.isProcessing = isProcessing } } } required init?(coder: NSCoder) { preconditionFailure() } func update(component: TextProcessingContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false } let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.2) let environment = environment[ViewControllerComponentContainer.Environment.self].value self.component = component self.state = state let sideInset: CGFloat = 16.0 var contentHeight: CGFloat = 0.0 contentHeight += 85.0 var tabs: [TabBarComponent.Item] = [] tabs.append(TabBarComponent.Item( content: .customItem(TabBarComponent.Item.Content.CustomItem( id: "translate", title: "Translate", icon: .bundleIcon(name: "TextProcessing/TabTranslate") )), action: { [weak self] _ in guard let self else { return } if self.currentMode != .translate { self.currentMode = .translate } if !self.isUpdating { self.state?.updated(transition: .spring(duration: 0.4)) } }, doubleTapAction: nil, contextAction: nil )) tabs.append(TabBarComponent.Item( content: .customItem(TabBarComponent.Item.Content.CustomItem( id: "stylize", title: "Style", icon: .bundleIcon(name: "TextProcessing/TabStylize") )), action: { [weak self] _ in guard let self else { return } if self.currentMode != .stylize { self.currentMode = .stylize } if !self.isUpdating { self.state?.updated(transition: .spring(duration: 0.4)) } }, doubleTapAction: nil, contextAction: nil )) tabs.append(TabBarComponent.Item( content: .customItem(TabBarComponent.Item.Content.CustomItem( id: "fix", title: "Fix", icon: .bundleIcon(name: "TextProcessing/TabFix") )), action: { [weak self] _ in guard let self else { return } if self.currentMode != .fix { self.currentMode = .fix } if !self.isUpdating { self.state?.updated(transition: .spring(duration: 0.4)) } }, doubleTapAction: nil, contextAction: nil )) let currentModeId: String switch self.currentMode { case .translate: currentModeId = "translate" case .stylize: currentModeId = "stylize" case .fix: currentModeId = "fix" } let modeTabsSize = self.modeTabs.update( transition: transition, component: AnyComponent(TabBarComponent( theme: environment.theme, tintSelectedItem: false, strings: environment.strings, items: tabs, search: nil, selectedId: currentModeId, outerInsets: UIEdgeInsets() )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 62.0) ) let modeTabsFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: modeTabsSize) if let modeTabsView = self.modeTabs.view { if modeTabsView.superview == nil { self.modeTabs.parentState = state self.addSubview(modeTabsView) } transition.setFrame(view: modeTabsView, frame: modeTabsFrame) } contentHeight += modeTabsSize.height contentHeight += 24.0 if let currentContent = self.currentContent, currentContent.mode != self.currentMode { if let currentContentView = currentContent.view.view { transition.setAlpha(view: currentContentView, alpha: 0.0, completion: { [weak currentContentView] _ in currentContentView?.removeFromSuperview() }) } self.currentContent = nil } let contentComponent: AnyComponent switch self.currentMode { case .translate: contentComponent = AnyComponent(TextProcessingTranslateContentComponent( context: component.context, theme: environment.theme, strings: environment.strings, externalState: self.translateState, inputText: component.inputText, mode: .translate, copyAction: component.copyCurrentResult, displayLanguageSelectionMenu: component.displayLanguageSelectionMenu )) case .stylize: contentComponent = AnyComponent(TextProcessingTranslateContentComponent( context: component.context, theme: environment.theme, strings: environment.strings, externalState: self.stylizeState, inputText: component.inputText, mode: .stylize, copyAction: component.copyCurrentResult, displayLanguageSelectionMenu: component.displayLanguageSelectionMenu )) case .fix: contentComponent = AnyComponent(TextProcessingTranslateContentComponent( context: component.context, theme: environment.theme, strings: environment.strings, externalState: self.fixState, inputText: component.inputText, mode: .fix, copyAction: component.copyCurrentResult, displayLanguageSelectionMenu: component.displayLanguageSelectionMenu )) } let content: ComponentView var contentTransition = transition if let current = self.currentContent { content = current.view } else { content = ComponentView() self.currentContent = (self.currentMode, content) contentTransition = contentTransition.withAnimation(.none) } let contentSize = content.update( transition: contentTransition, component: contentComponent, environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000000.0) ) if let contentView = content.view { if contentView.superview == nil { content.parentState = state self.currentContentContainer.addSubview(contentView) contentView.layer.allowsGroupOpacity = true contentView.alpha = 0.0 } alphaTransition.setAlpha(view: contentView, alpha: 1.0) contentTransition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(), size: contentSize)) } let contentFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: contentSize) transition.setFrame(view: self.currentContentContainer, frame: contentFrame) if self.currentContentBackground.image == nil { self.currentContentBackground.image = generateStretchableFilledCircleImage(diameter: 60.0, color: .white)?.withRenderingMode(.alwaysTemplate) } self.currentContentBackground.tintColor = environment.theme.list.itemBlocksBackgroundColor transition.setFrame(view: self.currentContentBackground, frame: contentFrame) contentHeight += contentSize.height contentHeight += 106.0 return CGSize(width: availableSize.width, height: contentHeight) } } func makeView() -> View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } private final class TextProcessingSheetComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let inputText: TextWithEntities let copyCurrentResult: (TextWithEntities) -> Void let completion: (TextWithEntities) -> Void init( context: AccountContext, inputText: TextWithEntities, copyCurrentResult: @escaping (TextWithEntities) -> Void, completion: @escaping (TextWithEntities) -> Void ) { self.context = context self.inputText = inputText self.copyCurrentResult = copyCurrentResult self.completion = completion } static func ==(lhs: TextProcessingSheetComponent, rhs: TextProcessingSheetComponent) -> Bool { return true } static var body: Body { let sheet = Child(ResizableSheetComponent.self) let languageSelectionMenu = Child(TextProcessingLanguageSelectionComponent.self) let animateOut = StoredActionSlot(Action.self) let contentExternalState = TextProcessingContentComponent.ExternalState() class LanguageSelectionMenuData { let sourceView: UIView let currentLanguage: String let currentStyle: TelegramComposeAIMessageMode.Style let displayStyle: Bool let completion: (String, TelegramComposeAIMessageMode.Style) -> Void init(sourceView: UIView, currentLanguage: String, currentStyle: TelegramComposeAIMessageMode.Style, displayStyle: Bool, completion: @escaping (String, TelegramComposeAIMessageMode.Style) -> Void) { self.sourceView = sourceView self.currentLanguage = currentLanguage self.currentStyle = currentStyle self.displayStyle = displayStyle self.completion = completion } } var languageSelectionMenuData: LanguageSelectionMenuData? return { context in let environment = context.environment[EnvironmentType.self] let controller = environment.controller let theme = environment.theme let state = context.state let dismiss: (Bool) -> Void = { 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) } } } let completion = context.component.completion let performMainAction: () -> Void = { if let result = contentExternalState.result { completion(result) } dismiss(true) } let copyCurrentResult = context.component.copyCurrentResult let copyCurrentResultImpl: () -> Void = { if let result = contentExternalState.result { copyCurrentResult(result) } } //TODO:localize (for AI: don't remove this line) let titleString: String = "AI Editor" //TODO:localize (for AI: don't remove this line) let actionButtonTitle: String = "Apply" let sheet = sheet.update( component: ResizableSheetComponent( content: AnyComponent(TextProcessingContentComponent( externalState: contentExternalState, context: context.component.context, inputText: context.component.inputText, copyCurrentResult: { copyCurrentResultImpl() }, displayLanguageSelectionMenu: { [weak state] sourceView, currentLanguage, currentStyle, displayStyle, completion in languageSelectionMenuData = LanguageSelectionMenuData(sourceView: sourceView, currentLanguage: currentLanguage, currentStyle: currentStyle, displayStyle: displayStyle, completion: completion) state?.updated(transition: .immediate) } )), titleItem: AnyComponent(TitleComponent( theme: theme, title: titleString, isProcessing: contentExternalState.isProcessing )), leftItem: AnyComponent( GlassBarButtonComponent( size: CGSize(width: 44.0, height: 44.0), backgroundColor: nil, isDark: theme.overallDarkAppearance, state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", tintColor: theme.chat.inputPanel.panelControlColor ) )), action: { _ in dismiss(true) } ) ), bottomItem: AnyComponent( ButtonComponent( background: ButtonComponent.Background( style: .glass, color: theme.list.itemCheckColors.fillColor, foreground: theme.list.itemCheckColors.foregroundColor, pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) ), content: AnyComponentWithIdentity( id: AnyHashable(0), component: AnyComponent(ButtonTextContentComponent( text: actionButtonTitle, badge: 0, textColor: theme.list.itemCheckColors.foregroundColor, badgeBackground: theme.list.itemCheckColors.foregroundColor, badgeForeground: theme.list.itemCheckColors.fillColor )) ), isEnabled: !contentExternalState.isProcessing, displaysProgress: false, action: { performMainAction() } ) ), backgroundColor: .color(theme.list.blocksBackgroundColor), animateOut: animateOut ), environment: { environment ResizableSheetComponentEnvironment( theme: theme, statusBarHeight: environment.statusBarHeight, safeInsets: environment.safeInsets, metrics: environment.metrics, deviceMetrics: environment.deviceMetrics, isDisplaying: environment.value.isVisible, isCentered: environment.metrics.widthClass == .regular, screenSize: context.availableSize, regularMetricsSize: nil, dismiss: { animated in dismiss(animated) } ) }, 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 languageSelectionMenuDataValue = languageSelectionMenuData { let languageSelectionMenu = languageSelectionMenu.update( component: TextProcessingLanguageSelectionComponent( theme: environment.theme, strings: environment.strings, sourceView: languageSelectionMenuDataValue.sourceView, topLanguages: [], selectedLanguageCode: languageSelectionMenuDataValue.currentLanguage, currentStyle: languageSelectionMenuDataValue.currentStyle, displayStyles: languageSelectionMenuDataValue.displayStyle, completion: languageSelectionMenuDataValue.completion, dismissed: { [weak state] in languageSelectionMenuData = nil state?.updated(transition: .immediate) } ), availableSize: context.availableSize, transition: context.transition ) context.add(languageSelectionMenu .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) ) } return context.availableSize } } } public class TextProcessingScreen: ViewControllerComponentContainer { private let context: AccountContext public init( context: AccountContext, inputText: TextWithEntities, copyResult: @escaping (TextWithEntities) -> Void, completion: @escaping (TextWithEntities) -> Void ) { self.context = context super.init( context: context, component: TextProcessingSheetComponent( context: context, inputText: inputText, copyCurrentResult: copyResult, completion: completion ), navigationBarAppearance: .none, statusBarStyle: .ignore, theme: .default ) self.statusBar.statusBarStyle = .Ignore self.navigationPresentation = .flatModal self.blocksBackgroundWhenInOverlay = true } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { } public func dismissAnimated() { if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent.View.Tag()) as? ResizableSheetComponent.View { view.dismissAnimated() } } } private final class TitleComponent: Component { let theme: PresentationTheme let title: String let isProcessing: Bool init( theme: PresentationTheme, title: String, isProcessing: Bool ) { self.theme = theme self.title = title self.isProcessing = isProcessing } static func ==(lhs: TitleComponent, rhs: TitleComponent) -> Bool { if lhs.theme !== rhs.theme { return false } if lhs.title != rhs.title { return false } if lhs.isProcessing != rhs.isProcessing { return false } return true } final class View: UIView { private var animationIcon: ComponentView? private let title = ComponentView() private var component: TitleComponent? private weak var state: EmptyComponentState? override init(frame: CGRect) { super.init(frame: frame) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func update(component: TitleComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)) )), environment: {}, containerSize: CGSize(width: availableSize.width, height: 100.0) ) let titleFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { titleView.isUserInteractionEnabled = false self.addSubview(titleView) } titleView.frame = titleFrame } if component.isProcessing { let animationIcon: ComponentView var animationIconTransition = transition if let current = self.animationIcon { animationIcon = current } else { animationIconTransition = animationIconTransition.withAnimation(.none) animationIcon = ComponentView() self.animationIcon = animationIcon } let animationIconSize = animationIcon.update( transition: animationIconTransition, component: AnyComponent(LottieComponent( content: LottieComponent.AppBundleContent( name: "SparklesEmoji" ), placeholderColor: nil, startingPosition: .begin, size: CGSize(width: 30.0, height: 30.0), loop: true )), environment: {}, containerSize: CGSize(width: 30.0, height: 30.0) ) let animationIconFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: titleFrame.minY + floorToScreenPixels((titleFrame.height - animationIconSize.height) * 0.5) - 2.0), size: animationIconSize) if let animationIconView = animationIcon.view { if animationIconView.superview == nil { self.addSubview(animationIconView) animationIconView.alpha = 0.0 } animationIconTransition.setFrame(view: animationIconView, frame: animationIconFrame) transition.setAlpha(view: animationIconView, alpha: 1.0) } } else { if let animationIcon = self.animationIcon { self.animationIcon = nil if let animationIconView = animationIcon.view { transition.setAlpha(view: animationIconView, alpha: 0.0, completion: { [weak animationIconView] _ in animationIconView?.removeFromSuperview() }) } } } return titleSize } } func makeView() -> View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } }