Files
Telegram-iOS/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingScreen.swift
T
2026-04-22 00:32:34 +04:00

2301 lines
107 KiB
Swift

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
import ListSectionComponent
import ListActionItemComponent
import ToastComponent
import TelegramNotices
import Markdown
import TelegramUIPreferences
import ChatSendMessageActionUI
import ContextUI
import EmojiStatusComponent
final class TextProcessingContentComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
final class ExternalState {
fileprivate(set) var isProcessing: Bool = false
fileprivate(set) var nonPremiumFloodTriggered: Bool = false
fileprivate(set) var result: TextWithEntities?
init() {
}
}
let externalState: ExternalState
let context: AccountContext
let mode: TextProcessingScreen.Mode
let previewIconFile: TelegramMediaFile?
let styles: [TextProcessingScreen.Style]
let inputText: TextWithEntities
let initialEditState: TextProcessingScreen.EditState?
let ignoredTranslationLanguages: [String]
let shouldDisplayStyleNotice: Bool
let copyCurrentResult: (() -> Void)?
let translateChat: ((String) -> Void)?
let displayLanguageSelectionMenu: (UIView, String, TelegramComposeAIMessageMode.StyleId, Bool, @escaping (String, TelegramComposeAIMessageMode.StyleReference) -> Void) -> Void
let newStyleAdded: (TelegramComposeAIMessageMode.CloudStyle) -> Void
let styleUpdated: (TelegramComposeAIMessageMode.CloudStyle) -> Void
let styleDeleted: (TelegramComposeAIMessageMode.StyleId) -> Void
let dismiss: (@escaping () -> Void) -> Void
init(
externalState: ExternalState,
context: AccountContext,
mode: TextProcessingScreen.Mode,
previewIconFile: TelegramMediaFile?,
styles: [TextProcessingScreen.Style],
inputText: TextWithEntities,
initialEditState: TextProcessingScreen.EditState?,
ignoredTranslationLanguages: [String],
shouldDisplayStyleNotice: Bool,
copyCurrentResult: (() -> Void)?,
translateChat: ((String) -> Void)?,
displayLanguageSelectionMenu: @escaping (UIView, String, TelegramComposeAIMessageMode.StyleId, Bool, @escaping (String, TelegramComposeAIMessageMode.StyleReference) -> Void) -> Void,
newStyleAdded: @escaping (TelegramComposeAIMessageMode.CloudStyle) -> Void,
styleUpdated: @escaping (TelegramComposeAIMessageMode.CloudStyle) -> Void,
styleDeleted: @escaping (TelegramComposeAIMessageMode.StyleId) -> Void,
dismiss: @escaping (@escaping () -> Void) -> Void
) {
self.externalState = externalState
self.styles = styles
self.context = context
self.mode = mode
self.previewIconFile = previewIconFile
self.inputText = inputText
self.initialEditState = initialEditState
self.ignoredTranslationLanguages = ignoredTranslationLanguages
self.shouldDisplayStyleNotice = shouldDisplayStyleNotice
self.copyCurrentResult = copyCurrentResult
self.translateChat = translateChat
self.displayLanguageSelectionMenu = displayLanguageSelectionMenu
self.newStyleAdded = newStyleAdded
self.styleUpdated = styleUpdated
self.styleDeleted = styleDeleted
self.dismiss = dismiss
}
static func ==(lhs: TextProcessingContentComponent, rhs: TextProcessingContentComponent) -> Bool {
if lhs.styles != rhs.styles {
return false
}
return true
}
private enum Mode {
case translate
case stylize
case fix
}
final class View: UIView {
private var component: TextProcessingContentComponent?
private var environment: ViewControllerComponentContainer.Environment?
private weak var state: EmptyComponentState?
private var isUpdating: Bool = false
private let modeTabs = ComponentView<Empty>()
private let actionsSection = ComponentView<Empty>()
private var previewIcon: ComponentView<Empty>?
private var previewTitle: ComponentView<Empty>?
private var previewDescription: ComponentView<Empty>?
private let currentContentBackground: UIImageView
private let currentContentContainer: UIView
private var currentContentHeader: (id: AnyHashable, view: ComponentView<Empty>)?
private var currentContentFooter: (id: AnyHashable, view: ComponentView<Empty>)?
private let translateState = TextProcessingTranslateContentComponent.ExternalState()
private let stylizeState = TextProcessingTranslateContentComponent.ExternalState()
private let fixState = TextProcessingTranslateContentComponent.ExternalState()
private var currentContent: (mode: Mode, view: ComponentView<Empty>)?
private var currentMode: Mode = .translate
private var isRequestingStylePreview: Bool = false
private var currentStylePreview: AIMessageStylePreview?
private var currentStylePreviewDisposable: Disposable?
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] _ in
self?.externalStatesUpdated()
}
self.translateState.isProcessingUpdated = { [weak self] _ in
self?.externalStatesUpdated()
}
self.translateState.nonPremiumFloodTriggeredUpdated = { [weak self] _ in
self?.externalStatesUpdated()
}
self.stylizeState.resultUpdated = { [weak self] _ in
self?.externalStatesUpdated()
}
self.stylizeState.isProcessingUpdated = { [weak self] _ in
self?.externalStatesUpdated()
}
self.stylizeState.nonPremiumFloodTriggeredUpdated = { [weak self] _ in
self?.externalStatesUpdated()
}
self.fixState.resultUpdated = { [weak self] _ in
self?.externalStatesUpdated()
}
self.fixState.isProcessingUpdated = { [weak self] _ in
self?.externalStatesUpdated()
}
self.fixState.nonPremiumFloodTriggeredUpdated = { [weak self] _ in
self?.externalStatesUpdated()
}
}
required init?(coder: NSCoder) {
preconditionFailure()
}
deinit {
self.currentStylePreviewDisposable?.dispose()
}
private func externalStatesUpdated() {
guard let component = self.component else {
return
}
switch self.currentMode {
case .translate:
component.externalState.isProcessing = self.translateState.isProcessing
component.externalState.result = self.translateState.result?.text
case .stylize:
component.externalState.isProcessing = self.stylizeState.isProcessing
component.externalState.result = self.stylizeState.result?.text
case .fix:
component.externalState.isProcessing = self.fixState.isProcessing
component.externalState.result = self.fixState.result?.text
}
component.externalState.nonPremiumFloodTriggered = self.translateState.nonPremiumFloodTriggered || self.stylizeState.nonPremiumFloodTriggered || self.fixState.nonPremiumFloodTriggered
/*#if DEBUG
component.externalState.nonPremiumFloodTriggered = true
#endif*/
}
private func saveState() {
guard let component = self.component else {
return
}
if case let .edit(saveRestoreStateId, _, _, _) = component.mode, let saveRestoreStateId {
let mappedMode: Int32
switch self.currentMode {
case .translate:
mappedMode = 0
case .stylize:
mappedMode = 1
case .fix:
mappedMode = 2
}
let state = TextProcessingScreen.EditState(
selectedMode: mappedMode
)
let _ = component.context.engine.preferences.update(id: ApplicationSpecificPreferencesKeys.textProcessingEditingState(peerId: saveRestoreStateId), { _ in
return EnginePreferencesEntry(state)
})
}
}
private func requestShareStyle(id: TelegramComposeAIMessageMode.StyleReference) {
guard let component = self.component else {
return
}
guard let style = component.styles.first(where: { .style($0.reference.id) == id.id }) else {
return
}
guard let slug = style.slug else {
return
}
let shareController = component.context.sharedContext.makeShareController(context: component.context, params: ShareControllerParams(subject: .url("https://t.me/addstyle/\(slug)")))
self.environment?.controller()?.present(shareController, in: .window(.root))
}
private func requestEditStyle(id: TelegramComposeAIMessageMode.StyleReference) {
Task { @MainActor [weak self] in
guard let self else {
return
}
guard let component = self.component, let environment = self.environment else {
return
}
guard let style = component.styles.first(where: { .style($0.reference.id) == id.id }) else {
return
}
environment.controller()?.push(await TextStyleEditScreen(
context: component.context,
mode: .edit(style.cloudStyle),
completion: { [weak self] style in
guard let self, let component = self.component else {
return
}
component.styleUpdated(style)
},
styleDeleted: { [weak self] in
guard let self, let component = self.component else {
return
}
component.styleDeleted(style.id.id)
}
))
}
}
private func requestDeleteStyle(id: TelegramComposeAIMessageMode.StyleReference) {
guard let component = self.component, let environment = self.environment else {
return
}
guard let style = component.styles.first(where: { .style($0.reference.id) == id.id }) else {
return
}
guard case let .custom(style) = style.cloudStyle.content else {
return
}
environment.controller()?.push(textAlertController(
context: component.context,
title: "Delete Style",
text: "Are you sure you want to delete this style? It will be removed for everyone who installed it.",
actions: [
TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}),
TextAlertAction(type: .destructiveAction, title: environment.strings.Common_Delete, action: { [weak self] in
guard let self, let component = self.component else {
return
}
let _ = component.context.engine.messages.deleteAITextStyle(id: style.id, accessHash: style.accessHash).startStandalone()
component.styleDeleted(id.id)
}),
]
))
}
private func openStyleContextMenu(id: TelegramComposeAIMessageMode.StyleReference, gesture: ContextGesture, sourceView: ContextExtractedContentContainingView) {
guard let component = self.component, let environment = self.environment else {
return
}
guard let style = component.styles.first(where: { .style($0.reference.id) == id.id }) else {
return
}
var items: [ContextMenuItem] = []
if style.isAuthor {
items.append(.action(ContextMenuActionItem(
text: "Edit Style",
icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor)
},
action: { [weak self] c, _ in
c?.dismiss(completion: { [weak self] in
guard let self else {
return
}
self.requestEditStyle(id: id)
})
})
))
}
items.append(.action(ContextMenuActionItem(
text: "Share Style",
icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor)
},
action: { [weak self] c, _ in
c?.dismiss(completion: { [weak self] in
guard let self else {
return
}
self.requestShareStyle(id: id)
})
})
))
if style.isAuthor {
items.append(.action(ContextMenuActionItem(
text: "Delete Style",
textColor: .destructive,
icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
},
action: { [weak self] c, _ in
c?.dismiss(completion: { [weak self] in
guard let self else {
return
}
self.requestDeleteStyle(id: id)
})
})
))
}
final class ContextExtractedContentSourceImpl: ContextExtractedContentSource {
let keepInPlace: Bool = false
let ignoreContentTouches: Bool = false
let blurBackground: Bool = false
let actionsHorizontalAlignment: ContextActionsHorizontalAlignment = .center
private let contentView: ContextExtractedContentContainingView
init(contentView: ContextExtractedContentContainingView) {
self.contentView = contentView
}
func takeView() -> ContextControllerTakeViewInfo? {
return ContextControllerTakeViewInfo(containingItem: .view(self.contentView), contentAreaInScreenSpace: UIScreen.main.bounds)
}
func putBack() -> ContextControllerPutBackViewInfo? {
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
}
}
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
let controller = makeContextController(
presentationData: presentationData,
source: .extracted(ContextExtractedContentSourceImpl(contentView: sourceView)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture
)
environment.controller()?.presentInGlobalOverlay(controller)
}
private func requestStylePreview() {
guard let component = self.component else {
return
}
guard case let .preview(style, _, _, _) = component.mode else {
return
}
var index = 0
if let currentStylePreview = self.currentStylePreview, let currentIndex = currentStylePreview.index {
var maxPreviewCount = 3
if let data = component.context.currentAppConfiguration.with({ $0 }).data, let value = data["aicompose_tone_examples_num"] as? Double {
maxPreviewCount = max(1, Int(value))
}
index = (currentIndex + 1) % maxPreviewCount
}
self.isRequestingStylePreview = true
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.4))
}
self.currentStylePreviewDisposable?.dispose()
self.currentStylePreviewDisposable = (component.context.engine.messages.requestAIMessageStylePreview(reference: TelegramComposeAIMessageMode.CloudStyle(content: .custom(style)).reference, index: index) |> deliverOnMainQueue).startStrict(next: { [weak self] result in
guard let self, let result else {
return
}
self.isRequestingStylePreview = false
self.currentStylePreview = result
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.4))
}
})
}
func update(component: TextProcessingContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.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
let isFirstTime = self.component == nil
self.component = component
self.environment = environment
self.state = state
if isFirstTime {
self.stylizeState.displayStyleTooltip = component.shouldDisplayStyleNotice
switch component.mode {
case .edit:
self.currentMode = .stylize
case let .preview(_, _, initialPreview, _):
self.currentMode = .stylize
self.currentStylePreview = initialPreview
self.requestStylePreview()
case .translate:
self.currentMode = .translate
}
}
let sideInset: CGFloat = 16.0
var contentHeight: CGFloat = 0.0
if case let .preview(style, _, _, _) = component.mode {
contentHeight += 40.0
if let previewIconFile = component.previewIconFile {
let previewIcon: ComponentView<Empty>
if let current = self.previewIcon {
previewIcon = current
} else {
previewIcon = ComponentView()
self.previewIcon = previewIcon
}
let previewIconSize = CGSize(width: 60.0, height: 60.0)
let _ = previewIcon.update(
transition: transition,
component: AnyComponent(EmojiStatusComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
content: .animation(
content: .file(file: previewIconFile),
size: previewIconSize,
placeholderColor: environment.theme.list.mediaPlaceholderColor,
themeColor: environment.theme.list.itemPrimaryTextColor,
loopMode: .count(1)
),
isVisibleForAnimations: true,
action: nil
)),
environment: {},
containerSize: previewIconSize
)
let previewIconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - previewIconSize.width) * 0.5), y: contentHeight), size: previewIconSize)
if let previewIconView = previewIcon.view {
if previewIconView.superview == nil {
self.addSubview(previewIconView)
previewIconView.isUserInteractionEnabled = false
}
transition.setFrame(view: previewIconView, frame: previewIconFrame)
}
contentHeight += previewIconSize.height
contentHeight += 10.0
}
let previewTitle: ComponentView<Empty>
if let current = self.previewTitle {
previewTitle = current
} else {
previewTitle = ComponentView()
self.previewTitle = previewTitle
}
let previewDescription: ComponentView<Empty>
if let current = self.previewDescription {
previewDescription = current
} else {
previewDescription = ComponentView()
self.previewDescription = previewDescription
}
let titleSize = previewTitle.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: style.title, font: Font.bold(30.0), textColor: environment.theme.list.itemPrimaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 100.0)
)
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentHeight), size: titleSize)
if let previewTitleView = previewTitle.view {
if previewTitleView.superview == nil {
previewTitleView.isUserInteractionEnabled = false
self.addSubview(previewTitleView)
}
transition.setPosition(view: previewTitleView, position: titleFrame.center)
previewTitleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
}
contentHeight += titleSize.height + 5.0
let descriptionSize = previewDescription.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "Add this style to instantly\nrewrite your messages.", font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.12
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 100.0)
)
let descriptionFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - descriptionSize.width) * 0.5), y: contentHeight), size: descriptionSize)
if let previewDescriptionView = previewDescription.view {
if previewDescriptionView.superview == nil {
previewDescriptionView.isUserInteractionEnabled = false
self.addSubview(previewDescriptionView)
}
transition.setPosition(view: previewDescriptionView, position: descriptionFrame.center)
previewDescriptionView.bounds = CGRect(origin: CGPoint(), size: descriptionFrame.size)
}
contentHeight += descriptionSize.height
contentHeight += 30.0
} else {
contentHeight += 82.0
}
switch component.mode {
case .edit:
contentHeight += 3.0
var tabs: [TabBarComponent.Item] = []
tabs.append(TabBarComponent.Item(
content: .customItem(TabBarComponent.Item.Content.CustomItem(
id: "translate",
title: environment.strings.TextProcessing_TabTranslate,
icon: .bundleIcon(name: "TextProcessing/TabTranslate")
)),
action: { [weak self] _ in
guard let self else {
return
}
if self.currentMode != .translate {
self.currentMode = .translate
self.saveState()
self.externalStatesUpdated()
}
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: environment.strings.TextProcessing_TabStylize,
icon: .bundleIcon(name: "TextProcessing/TabStylize")
)),
action: { [weak self] _ in
guard let self, let component = self.component else {
return
}
if self.currentMode != .stylize {
self.currentMode = .stylize
self.saveState()
let _ = ApplicationSpecificNotice.incrementAITextProcessingStyleSelection(accountManager: component.context.sharedContext.accountManager).startStandalone()
self.externalStatesUpdated()
}
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: environment.strings.TextProcessing_TabFix,
icon: .bundleIcon(name: "TextProcessing/TabFix")
)),
action: { [weak self] _ in
guard let self else {
return
}
if self.currentMode != .fix {
self.currentMode = .fix
self.saveState()
self.externalStatesUpdated()
}
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,
isLiftedStateEnabled: 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
case .translate, .preview:
break
}
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 contentExternalState: TextProcessingTranslateContentComponent.ExternalState
let contentComponent: AnyComponent<Empty>
switch self.currentMode {
case .translate:
contentExternalState = self.translateState
contentComponent = AnyComponent(TextProcessingTranslateContentComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
styles: component.styles,
externalState: self.translateState,
inputText: component.inputText,
mode: .translate(ignoredLanguages: component.ignoredTranslationLanguages),
copyAction: component.copyCurrentResult,
displayLanguageSelectionMenu: component.displayLanguageSelectionMenu,
createStyle: {
},
openStyleContextMenu: { _, _, _ in
},
present: { [weak self] c, a in
self?.environment?.controller()?.present(c, in: .window(.root), with: a)
},
rootViewForTextSelection: { [weak self] in
return self?.environment?.controller()?.view
},
openPeer: { _ in
},
requestAnotherPreviewExample: {
}
))
case .stylize:
var inputText = component.inputText
var isPreview = false
var fromText: TextWithEntities?
var toText: TextWithEntities?
var isRequestingPreview: Bool = false
var authorPeer: EnginePeer?
var userCount: Int = 0
if case let .preview(style, authorPeerValue, _, _) = component.mode {
isPreview = true
inputText = TextWithEntities(text: "", entities: [])
authorPeer = authorPeerValue
userCount = style.userCount ?? 0
isRequestingPreview = self.isRequestingStylePreview
if let currentStylePreview = self.currentStylePreview {
fromText = currentStylePreview.from
toText = currentStylePreview.to
}
}
contentExternalState = self.stylizeState
contentComponent = AnyComponent(TextProcessingTranslateContentComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
styles: component.styles,
externalState: self.stylizeState,
inputText: inputText,
mode: isPreview ? .preview(from: fromText, to: toText, authorPeer: authorPeer, userCount: userCount, isRequesting: isRequestingPreview) : .stylize,
copyAction: component.copyCurrentResult,
displayLanguageSelectionMenu: component.displayLanguageSelectionMenu,
createStyle: { [weak self] in
Task { @MainActor in
guard let self else {
return
}
guard let component = self.component, let environment = self.environment else {
return
}
environment.controller()?.push(await TextStyleEditScreen(
context: component.context,
mode: .create,
completion: { [weak self] style in
guard let self, let component = self.component else {
return
}
if let contentComponentView = self.currentContent?.view.view as? TextProcessingTranslateContentComponent.View {
contentComponentView.scrollStylesToStart()
}
component.newStyleAdded(style)
},
styleDeleted: {
}
))
}
},
openStyleContextMenu: { [weak self] styleId, gesture, sourceView in
guard let self else {
return
}
self.openStyleContextMenu(id: styleId, gesture: gesture, sourceView: sourceView)
},
present: { [weak self] c, a in
self?.environment?.controller()?.present(c, in: .window(.root), with: a)
},
rootViewForTextSelection: { [weak self] in
return self?.environment?.controller()?.view
},
openPeer: { [weak self] peer in
guard let self, let component = self.component, let environment = self.environment else {
return
}
guard let navigationController = environment.controller()?.navigationController as? NavigationController else {
return
}
let context = component.context
component.dismiss({ [weak context, weak navigationController] in
guard let context, let navigationController else {
return
}
if let peerInfoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
navigationController.pushViewController(peerInfoController)
}
})
},
requestAnotherPreviewExample: { [weak self] in
guard let self else {
return
}
self.requestStylePreview()
}
))
case .fix:
contentExternalState = self.fixState
contentComponent = AnyComponent(TextProcessingTranslateContentComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
styles: component.styles,
externalState: self.fixState,
inputText: component.inputText,
mode: .fix,
copyAction: component.copyCurrentResult,
displayLanguageSelectionMenu: component.displayLanguageSelectionMenu,
createStyle: {
},
openStyleContextMenu: { _, _, _ in
},
present: { [weak self] c, a in
self?.environment?.controller()?.present(c, in: .window(.root), with: a)
},
rootViewForTextSelection: { [weak self] in
return self?.environment?.controller()?.view
},
openPeer: { _ in
},
requestAnotherPreviewExample: {
}
))
}
if let sectionHeader = contentExternalState.sectionHeader {
let headerView: ComponentView<Empty>
var headerTransition = transition
if let current = self.currentContentHeader, current.id == sectionHeader.id {
headerView = current.view
} else {
headerTransition = headerTransition.withAnimation(.none)
if let currentContentHeader = self.currentContentHeader {
self.currentContentHeader = nil
if let componentView = currentContentHeader.view.view {
alphaTransition.setAlpha(view: componentView, alpha: 0.0, completion: { [weak componentView] _ in
componentView?.removeFromSuperview()
})
}
}
headerView = ComponentView()
self.currentContentHeader = (sectionHeader.id, headerView)
}
let headerSideInset: CGFloat = sideInset + 16.0
let headerSize = headerView.update(
transition: .immediate,
component: sectionHeader.component,
environment: {},
containerSize: CGSize(width: availableSize.width - headerSideInset * 2.0, height: 10000.0)
)
let headerFrame = CGRect(origin: CGPoint(x: headerSideInset, y: contentHeight), size: headerSize)
if let headerComponentView = headerView.view {
if headerComponentView.superview == nil {
headerComponentView.layer.anchorPoint = CGPoint()
self.addSubview(headerComponentView)
alphaTransition.animateAlpha(view: headerComponentView, from: 0.0, to: 1.0)
}
headerTransition.setPosition(view: headerComponentView, position: headerFrame.origin)
headerComponentView.bounds = CGRect(origin: CGPoint(), size: headerFrame.size)
}
contentHeight += headerSize.height + 7.0
} else {
if let currentContentHeader = self.currentContentHeader {
self.currentContentHeader = nil
if let componentView = currentContentHeader.view.view {
alphaTransition.setAlpha(view: componentView, alpha: 0.0, completion: { [weak componentView] _ in
componentView?.removeFromSuperview()
})
}
}
}
let content: ComponentView<Empty>
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
if let sectionFooter = contentExternalState.sectionFooter {
let footerView: ComponentView<Empty>
var footerTransition = transition
if let current = self.currentContentFooter, current.id == sectionFooter.id {
footerView = current.view
} else {
footerTransition = footerTransition.withAnimation(.none)
if let currentContentFooter = self.currentContentFooter {
self.currentContentFooter = nil
if let componentView = currentContentFooter.view.view {
alphaTransition.setAlpha(view: componentView, alpha: 0.0, completion: { [weak componentView] _ in
componentView?.removeFromSuperview()
})
}
}
footerView = ComponentView()
self.currentContentFooter = (sectionFooter.id, footerView)
}
let headerSideInset: CGFloat = sideInset + 16.0
let footerSize = footerView.update(
transition: .immediate,
component: sectionFooter.component,
environment: {},
containerSize: CGSize(width: availableSize.width - headerSideInset * 2.0, height: 10000.0)
)
if contentHeight != 0.0 {
contentHeight += 8.0 - UIScreenPixel
}
let footerFrame = CGRect(origin: CGPoint(x: headerSideInset, y: contentHeight), size: footerSize)
if let footerComponentView = footerView.view {
if footerComponentView.superview == nil {
footerComponentView.layer.anchorPoint = CGPoint()
self.addSubview(footerComponentView)
alphaTransition.animateAlpha(view: footerComponentView, from: 0.0, to: 1.0)
}
footerTransition.setPosition(view: footerComponentView, position: footerFrame.origin)
footerComponentView.bounds = CGRect(origin: CGPoint(), size: footerFrame.size)
}
contentHeight += footerSize.height
} else {
if let currentContentFooter = self.currentContentFooter {
self.currentContentFooter = nil
if let componentView = currentContentFooter.view.view {
alphaTransition.setAlpha(view: componentView, alpha: 0.0, completion: { [weak componentView] _ in
componentView?.removeFromSuperview()
})
}
}
}
var actionsSectionItems: [AnyComponentWithIdentity<Empty>] = []
if case .translate = component.mode {
if let copyTranslation = component.copyCurrentResult {
actionsSectionItems.append(AnyComponentWithIdentity(id: "copy", component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
style: .glass,
title: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Translate_CopyTranslation,
font: Font.regular(17.0),
textColor: environment.theme.list.itemAccentColor
)),
maximumNumberOfLines: 1
)
),
leftIcon: .custom(AnyComponentWithIdentity(id: "icon", component: AnyComponent(BundleIconComponent(name: "Chat/Context Menu/Copy", tintColor: environment.theme.list.itemAccentColor))), false),
action: { _ in
copyTranslation()
}
))))
}
if let translateChat = component.translateChat {
actionsSectionItems.append(AnyComponentWithIdentity(id: "translate", component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
style: .glass,
title: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Localization_TranslateEntireChat,
font: Font.regular(17.0),
textColor: environment.theme.list.itemAccentColor
)),
maximumNumberOfLines: 1
)
),
leftIcon: .custom(AnyComponentWithIdentity(id: "icon", component: AnyComponent(BundleIconComponent(name: "Chat/Context Menu/Translate", tintColor: environment.theme.list.itemAccentColor))), false),
action: { [weak self] _ in
guard let self, let language = self.translateState.result?.language else {
return
}
translateChat(language)
}
))))
}
}
if !actionsSectionItems.isEmpty {
contentHeight += 24.0
let actionsSectionSize = self.actionsSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
style: .glass,
header: nil,
footer: nil,
items: actionsSectionItems
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height)
)
let actionsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: actionsSectionSize)
self.actionsSection.parentState = state
if let actionsSectionView = self.actionsSection.view {
if actionsSectionView.superview == nil {
self.addSubview(actionsSectionView)
}
transition.setFrame(view: actionsSectionView, frame: actionsSectionFrame)
}
contentHeight += actionsSectionSize.height + 3.0
}
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<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class TextProcessingSheetComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let mode: TextProcessingScreen.Mode
let ignoredTranslationLanguages: [String]
let initialStyles: [TextProcessingScreen.Style]
let inputText: TextWithEntities
let initialEditState: TextProcessingScreen.EditState?
let shouldDisplayStyleNotice: Bool
let previewIconFile: TelegramMediaFile?
let copyCurrentResult: ((TextWithEntities) -> Void)?
let translateChat: ((String) -> Void)?
init(
context: AccountContext,
mode: TextProcessingScreen.Mode,
ignoredTranslationLanguages: [String],
initialStyles: [TextProcessingScreen.Style],
inputText: TextWithEntities,
initialEditState: TextProcessingScreen.EditState?,
shouldDisplayStyleNotice: Bool,
previewIconFile: TelegramMediaFile?,
copyCurrentResult: ((TextWithEntities) -> Void)?,
translateChat: ((String) -> Void)?
) {
self.context = context
self.mode = mode
self.ignoredTranslationLanguages = ignoredTranslationLanguages
self.initialStyles = initialStyles
self.inputText = inputText
self.initialEditState = initialEditState
self.shouldDisplayStyleNotice = shouldDisplayStyleNotice
self.previewIconFile = previewIconFile
self.copyCurrentResult = copyCurrentResult
self.translateChat = translateChat
}
static func ==(lhs: TextProcessingSheetComponent, rhs: TextProcessingSheetComponent) -> Bool {
return true
}
final class View: UIView {
private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, ResizableSheetComponentEnvironment)>()
private var toast: ComponentView<Empty>?
private var languageSelectionMenu: ComponentView<Empty>?
private let animateOut = ActionSlot<Action<Void>>()
private let contentExternalState = TextProcessingContentComponent.ExternalState()
private var styles: [TextProcessingScreen.Style] = []
private final class LanguageSelectionMenuData {
let sourceView: UIView
let currentLanguage: String
let currentStyle: TelegramComposeAIMessageMode.StyleId
let displayStyle: Bool
let completion: (String, TelegramComposeAIMessageMode.StyleReference) -> Void
init(sourceView: UIView, currentLanguage: String, currentStyle: TelegramComposeAIMessageMode.StyleId, displayStyle: Bool, completion: @escaping (String, TelegramComposeAIMessageMode.StyleReference) -> Void) {
self.sourceView = sourceView
self.currentLanguage = currentLanguage
self.currentStyle = currentStyle
self.displayStyle = displayStyle
self.completion = completion
}
}
private var languageSelectionMenuData: LanguageSelectionMenuData?
private var component: TextProcessingSheetComponent?
private var environment: ViewControllerComponentContainer.Environment?
private weak var state: EmptyComponentState?
private var isUpdating: Bool = false
private var actionDisposable: Disposable?
private var isPerformingMainAction: Bool = false
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.actionDisposable?.dispose()
}
private func displayLongPressSendMenu(sourceSendButton: UIView) {
Task { @MainActor [weak self, weak sourceSendButton] in
guard let self, let sourceSendButton, let component = self.component, case let .edit(_, _, _, sendContextActions) = component.mode, let sendContextActions else {
return
}
guard let controller = self.environment?.controller() else {
return
}
let peerId = sendContextActions.peerId
let previousSupportedOrientations = controller.supportedOrientations
let availableMessageEffects = await (component.context.availableMessageEffects |> take(1)).get()
let hasPremium = await (component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: component.context.account.peerId))
|> map { peer -> Bool in
guard case let .user(user) = peer else {
return false
}
return user.isPremium
}).get()
let peerStatus = await (component.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Presence(id: peerId)
)).get()
guard let peer = await (component.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
)).get() else {
return
}
let initialData = await ChatSendMessageContextScreen.initialData(context: component.context, currentMessageEffectId: nil).get()
var sendWhenOnlineAvailable = false
if let peerStatus, case let .present(until) = peerStatus.status {
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
if currentTime > until {
sendWhenOnlineAvailable = true
}
}
if peerId.namespace == Namespaces.Peer.CloudUser && peerId.id._internalGetInt64Value() == 777000 {
sendWhenOnlineAvailable = false
}
let messageActionsController = makeChatSendMessageActionSheetController(
initialData: initialData,
context: component.context,
updatedPresentationData: nil,
peerId: peerId,
params: .sendMessage(SendMessageActionSheetControllerParams.SendMessage(
isScheduledMessages: false,
mediaPreview: nil,
mediaCaptionIsAbove: nil,
messageEffect: (nil, { [weak self] updatedEffect in
guard let self else {
return
}
let _ = self
let _ = updatedEffect
}),
attachment: false,
canSendWhenOnline: sendWhenOnlineAvailable,
forwardMessageIds: [],
canMakePaidContent: false,
currentPrice: nil,
hasTimers: false,
sendPaidMessageStars: nil,
isMonoforum: peer._asPeer().isMonoForum
)),
hasEntityKeyboard: false,
gesture: nil,
sourceSendButton: sourceSendButton,
textInputView: UITextView(),
emojiViewProvider: nil,
completion: { [weak self] in
guard let self else {
return
}
self.environment?.controller()?.supportedOrientations = previousSupportedOrientations
},
sendMessage: { [weak self] mode, parameters in
guard let self, let result = self.contentExternalState.result else {
return
}
sendContextActions.send(result, mode, parameters)
let controller = self.environment?.controller
self.animateOut.invoke(Action { _ in
if let controller = controller?() {
controller.dismiss(completion: nil)
}
})
},
schedule: { [weak self] params in
guard let self, let result = self.contentExternalState.result else {
return
}
sendContextActions.schedule(result, params)
let controller = self.environment?.controller
self.animateOut.invoke(Action { _ in
if let controller = controller?() {
controller.dismiss(completion: nil)
}
})
}, editPrice: { _ in
}, openPremiumPaywall: { [weak self] c in
guard let self else {
return
}
self.environment?.controller()?.push(c)
},
reactionItems: nil,
availableMessageEffects: availableMessageEffects,
isPremium: hasPremium
)
controller.present(messageActionsController, in: .window(.root))
}
}
func update(component: TextProcessingSheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
if self.component == nil {
self.styles = component.initialStyles
}
self.component = component
self.state = state
let environmentValue = environment[ViewControllerComponentContainer.Environment.self].value
self.environment = environmentValue
let controller = environmentValue.controller
let theme = environmentValue.theme
let dismiss: (Bool) -> Void = { [weak self] animated in
if animated {
self?.animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
} else {
if let controller = controller() {
controller.dismiss(completion: nil)
}
}
}
let performMainAction: () -> Void
var performSendAction: ((TextWithEntities) -> Void)?
var hasLongPressActions = false
let isMainActionEnabled: Bool
let actionButtonTitle: String
var actionButtonShowsIncreaseLimit = false
if self.contentExternalState.nonPremiumFloodTriggered {
isMainActionEnabled = true
actionButtonTitle = environmentValue.strings.TextProcessing_ActionTitleNonPremium
actionButtonShowsIncreaseLimit = true
performMainAction = { [weak self] in
guard let self, let component = self.component else {
return
}
let context = component.context
var replaceImpl: ((ViewController) -> Void)?
let controller = component.context.sharedContext.makePremiumDemoController(context: component.context, subject: .aiTools, forceDark: false, action: {
let controller = component.context.sharedContext.makePremiumIntroController(context: context, source: .aiTools, forceDark: false, dismissed: nil)
replaceImpl?(controller)
}, dismissed: nil)
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
self.environment?.controller()?.push(controller)
}
} else {
switch component.mode {
case let .edit(_, completion, send, sendContextActions):
actionButtonTitle = environmentValue.strings.TextProcessing_ActionApply
performSendAction = send
isMainActionEnabled = !self.contentExternalState.isProcessing
hasLongPressActions = sendContextActions != nil
performMainAction = { [weak self] in
guard let self else {
return
}
if let result = self.contentExternalState.result {
completion(result)
}
dismiss(true)
}
case .translate:
actionButtonTitle = environmentValue.strings.TextProcessing_ActionClose
isMainActionEnabled = true
performMainAction = {
dismiss(true)
}
case let .preview(style, _, _, added):
actionButtonTitle = "Add Style"
isMainActionEnabled = !self.isPerformingMainAction
performMainAction = { [weak self] in
guard let self, let component = self.component else {
return
}
self.isPerformingMainAction = true
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.4))
}
self.actionDisposable?.dispose()
self.actionDisposable = (component.context.engine.messages.installAIMessageStyle(style: style) |> deliverOnMainQueue).startStrict(error: { [weak self] _ in
guard let self else {
return
}
self.isPerformingMainAction = false
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.4))
}
}, completed: {
dismiss(true)
added()
})
}
}
}
let copyCurrentResult = component.copyCurrentResult
let copyCurrentResultImpl: () -> Void = { [weak self] in
guard let self else {
return
}
if let result = self.contentExternalState.result {
copyCurrentResult?(result)
dismiss(true)
}
}
var displayInfoButton = true
let titleString: String
switch component.mode {
case .edit:
titleString = environmentValue.strings.TextProcessing_TitleEdit
case .translate:
titleString = environmentValue.strings.TextProcessing_TitleTranslate
case .preview:
titleString = ""
displayInfoButton = false
}
let sheetSize = self.sheet.update(
transition: transition,
component: AnyComponent(ResizableSheetComponent<ViewControllerComponentContainer.Environment>(
content: AnyComponent<ViewControllerComponentContainer.Environment>(TextProcessingContentComponent(
externalState: self.contentExternalState,
context: component.context,
mode: component.mode,
previewIconFile: component.previewIconFile,
styles: self.styles,
inputText: component.inputText,
initialEditState: component.initialEditState,
ignoredTranslationLanguages: component.ignoredTranslationLanguages,
shouldDisplayStyleNotice: component.shouldDisplayStyleNotice,
copyCurrentResult: component.copyCurrentResult != nil ? {
copyCurrentResultImpl()
} : nil,
translateChat: component.translateChat.flatMap { translateChat in
{ language in
translateChat(language)
dismiss(true)
}
},
displayLanguageSelectionMenu: { [weak self] sourceView, currentLanguage, currentStyle, displayStyle, completion in
guard let self else {
return
}
self.languageSelectionMenuData = LanguageSelectionMenuData(sourceView: sourceView, currentLanguage: currentLanguage, currentStyle: currentStyle, displayStyle: displayStyle, completion: completion)
self.state?.updated(transition: .immediate)
},
newStyleAdded: { [weak self] style in
Task { @MainActor in
guard let self, let component = self.component else {
return
}
var authorPeer: EnginePeer?
if case let .custom(style) = style.content, let authorId = style.authorId {
authorPeer = await component.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: authorId)
).get()
}
self.styles.insert(TextProcessingScreen.Style(cloudStyle: style, authorPeer: authorPeer), at: 0)
self.state?.updated(transition: .spring(duration: 0.4))
}
},
styleUpdated: { [weak self] style in
Task { @MainActor in
guard let self, let component = self.component else {
return
}
guard let index = self.styles.firstIndex(where: { $0.id.id == style.id }) else {
return
}
var authorPeer: EnginePeer?
if case let .custom(style) = style.content, let authorId = style.authorId {
authorPeer = await component.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: authorId)
).get()
}
self.styles[index] = TextProcessingScreen.Style(cloudStyle: style, authorPeer: authorPeer)
self.state?.updated(transition: .immediate)
}
},
styleDeleted: { [weak self] id in
guard let self else {
return
}
guard let index = self.styles.firstIndex(where: { $0.id.id == id }) else {
return
}
self.styles.remove(at: index)
self.state?.updated(transition: .spring(duration: 0.4))
},
dismiss: { [weak self] completion in
self?.animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
completion()
})
}
)),
titleItem: titleString.isEmpty ? nil : AnyComponent(TitleComponent(
theme: theme,
title: titleString,
isProcessing: self.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)
}
)
),
rightItem: displayInfoButton ? AnyComponent(
GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: nil,
isDark: theme.overallDarkAppearance,
state: .glass,
component: AnyComponentWithIdentity(id: "info", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Info",
tintColor: theme.chat.inputPanel.panelControlColor
)
)),
action: { [weak self] _ in
guard let self, let component = self.component, let environment = self.environment else {
return
}
environment.controller()?.push(component.context.sharedContext.makeCocoonInfoScreen(context: component.context))
}
)
) : nil,
hasTopEdgeEffect: displayInfoButton,
bottomItem: AnyComponent(
ActionButtonsComponent(
theme: theme,
strings: environmentValue.strings,
actionTitle: actionButtonTitle,
actionButtonShowsIncreaseLimit: actionButtonShowsIncreaseLimit,
action: isMainActionEnabled ? performMainAction : nil,
sendAction: performSendAction.flatMap { [weak self] performSendAction in
return {
guard let self else {
return
}
if let result = self.contentExternalState.result {
performSendAction(result)
dismiss(true)
}
}
},
longPressSendAction: (performSendAction != nil && hasLongPressActions) ? { [weak self] sourceView in
guard let self else {
return
}
self.displayLongPressSendMenu(sourceSendButton: sourceView)
} : nil
)
),
backgroundColor: .color(theme.list.blocksBackgroundColor),
animateOut: self.animateOut
)),
environment: {
environmentValue
ResizableSheetComponentEnvironment(
theme: theme,
statusBarHeight: environmentValue.statusBarHeight,
safeInsets: environmentValue.safeInsets,
inputHeight: 0.0,
metrics: environmentValue.metrics,
deviceMetrics: environmentValue.deviceMetrics,
isDisplaying: environmentValue.isVisible,
isCentered: environmentValue.metrics.widthClass == .regular,
screenSize: availableSize,
regularMetricsSize: nil,
dismiss: { animated in
dismiss(animated)
}
)
},
containerSize: availableSize
)
self.sheet.parentState = state
if let sheetView = self.sheet.view {
if sheetView.superview == nil {
self.addSubview(sheetView)
}
transition.setFrame(view: sheetView, frame: CGRect(origin: .zero, size: sheetSize))
}
if self.contentExternalState.nonPremiumFloodTriggered {
let sideInset: CGFloat = 8.0
let toast: ComponentView<Empty>
var toastTransition = transition
if let current = self.toast {
toast = current
} else {
toastTransition = toastTransition.withAnimation(.none)
toast = ComponentView()
self.toast = toast
}
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
let playOnce = ActionSlot<Void>()
let toastSize = toast.update(
transition: toastTransition,
component: AnyComponent(ToastContentComponent(
icon: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "PremiumStar"),
startingPosition: .begin,
size: CGSize(width: 32.0, height: 32.0),
playOnce: playOnce
)),
content: AnyComponent(VStack([
AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: environmentValue.strings.TextProcessing_LimitToast_Title, font: Font.semibold(14.0), textColor: .white)),
))),
AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent(
text: .markdown(text: environmentValue.strings.TextProcessing_LimitToast_Text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in nil })),
maximumNumberOfLines: 0
)))
], alignment: .left, spacing: 6.0)),
insets: UIEdgeInsets(top: 10.0, left: 12.0, bottom: 10.0, right: 10.0),
iconSpacing: 12.0
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height)
)
if let toastView = toast.view {
if toastView.superview == nil, let sheetView = self.sheet.view as? ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View {
sheetView.containerView.addSubview(toastView)
if !transition.animation.isImmediate {
toastView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
if let toastView = toastView as? ToastContentComponent.View, let iconView = toastView.iconView as? LottieComponent.View {
iconView.playOnce()
}
}
toastTransition.setFrame(view: toastView, frame: CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - 94.0 - toastSize.height), size: toastSize))
}
} else {
if let toast = self.toast {
self.toast = nil
if let toastView = toast.view {
toastView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak toastView] _ in
toastView?.removeFromSuperview()
})
}
}
}
if let languageSelectionMenuDataValue = self.languageSelectionMenuData {
let languageSelectionMenu: ComponentView<Empty>
if let current = self.languageSelectionMenu {
languageSelectionMenu = current
} else {
languageSelectionMenu = ComponentView<Empty>()
self.languageSelectionMenu = languageSelectionMenu
}
let menuSize = languageSelectionMenu.update(
transition: transition,
component: AnyComponent(TextProcessingLanguageSelectionComponent(
context: component.context,
theme: theme,
strings: environmentValue.strings,
sourceView: languageSelectionMenuDataValue.sourceView,
topLanguages: [],
selectedLanguageCode: languageSelectionMenuDataValue.currentLanguage,
currentStyle: languageSelectionMenuDataValue.currentStyle,
displayStyles: languageSelectionMenuDataValue.displayStyle ? self.styles : nil,
completion: languageSelectionMenuDataValue.completion,
dismissed: { [weak self] in
guard let self else {
return
}
self.languageSelectionMenuData = nil
self.state?.updated(transition: .immediate)
},
inputHeight: environmentValue.inputHeight
)),
environment: {},
containerSize: availableSize
)
languageSelectionMenu.parentState = state
if let menuView = languageSelectionMenu.view {
if menuView.superview == nil {
self.addSubview(menuView)
}
transition.setFrame(view: menuView, frame: CGRect(origin: .zero, size: menuSize))
}
} else if let languageSelectionMenu = self.languageSelectionMenu {
self.languageSelectionMenu = nil
languageSelectionMenu.view?.removeFromSuperview()
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public class TextProcessingScreen: ViewControllerComponentContainer {
public typealias SendContextActions = TextProcessingScreenSendContextActions
public typealias Mode = TextProcessingScreenMode
struct EditState: Codable {
var selectedMode: Int32
init(selectedMode: Int32) {
self.selectedMode = selectedMode
}
}
public final class Style: Equatable {
public let reference: TelegramComposeAIMessageMode.CloudStyle.Reference
public let title: String
public let emojiFileId: Int64?
public let emojiFile: TelegramMediaFile?
public let isAuthor: Bool
public let slug: String?
public let cloudStyle: TelegramComposeAIMessageMode.CloudStyle
public let authorPeer: EnginePeer?
public var id: TelegramComposeAIMessageMode.StyleReference {
return .style(self.reference)
}
public init(reference: TelegramComposeAIMessageMode.CloudStyle.Reference, title: String, emojiFileId: Int64?, emojiFile: TelegramMediaFile?, isAuthor: Bool, slug: String?, cloudStyle: TelegramComposeAIMessageMode.CloudStyle, authorPeer: EnginePeer?) {
self.reference = reference
self.title = title
self.emojiFileId = emojiFileId
self.emojiFile = emojiFile
self.isAuthor = isAuthor
self.slug = slug
self.cloudStyle = cloudStyle
self.authorPeer = authorPeer
}
convenience init(cloudStyle: TelegramComposeAIMessageMode.CloudStyle, authorPeer: EnginePeer?) {
let title: String
let emojiFileId: Int64?
var isAuthor = false
var slug: String?
switch cloudStyle.content {
case let .standard(standard):
title = standard.title
emojiFileId = standard.emojiFileId
case let .custom(custom):
title = custom.title
emojiFileId = custom.emojiFileId
isAuthor = custom.isCreator
slug = custom.slug
}
self.init(
reference: cloudStyle.reference,
title: title,
emojiFileId: emojiFileId,
emojiFile: nil,
isAuthor: isAuthor,
slug: slug,
cloudStyle: cloudStyle,
authorPeer: authorPeer
)
}
public static func ==(lhs: Style, rhs: Style) -> Bool {
if lhs.reference != rhs.reference {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.emojiFileId != rhs.emojiFileId {
return false
}
if lhs.emojiFile != rhs.emojiFile {
return false
}
if lhs.cloudStyle != rhs.cloudStyle {
return false
}
return true
}
}
private let context: AccountContext
public init(
context: AccountContext,
theme: PresentationTheme? = nil,
mode: Mode,
inputText: TextWithEntities,
copyResult: ((TextWithEntities) -> Void)?,
translateChat: ((String) -> Void)?
) async {
self.context = context
let rawStyles = await context.engine.messages.composeAIMessageStyles().get()
var styles: [Style] = []
let resolvedEmojiFiles: [Int64: TelegramMediaFile] = await context.engine.stickers.resolveInlineStickersLocal(fileIds: Array(Set(rawStyles.compactMap({ style in
switch style.content {
case let .standard(standard):
return standard.emojiFileId
case let .custom(custom):
return custom.emojiFileId
}
})))).get()
for value in rawStyles {
let title: String
let emojiFileId: Int64?
var isAuthor = false
var slug: String?
var authorPeer: EnginePeer?
switch value.content {
case let .standard(standard):
title = standard.title
emojiFileId = standard.emojiFileId
case let .custom(custom):
title = custom.title
emojiFileId = custom.emojiFileId
isAuthor = custom.isCreator
slug = custom.slug
if let authorId = custom.authorId {
authorPeer = await context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: authorId)
).get()
}
}
styles.append(Style(
reference: value.reference,
title: title,
emojiFileId: emojiFileId,
emojiFile: emojiFileId.flatMap { resolvedEmojiFiles[$0] },
isAuthor: isAuthor,
slug: slug,
cloudStyle: value,
authorPeer: authorPeer
))
}
var shouldDisplayStyleNotice = false
var previewIconFile: TelegramMediaFile?
if case let .preview(style, _, _, _) = mode {
if let emojiFileId = style.emojiFileId {
previewIconFile = await context.engine.stickers.resolveInlineStickersLocal(fileIds: [emojiFileId]).get().first?.value
}
} else {
shouldDisplayStyleNotice = await ApplicationSpecificNotice.getAITextProcessingStyleSelection(accountManager: context.sharedContext.accountManager).get() < 3
}
var initialEditState: EditState?
if case let .edit(saveRestoreStateId, _, _, _) = mode, let saveRestoreStateId {
initialEditState = await context.engine.data.get(
TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: ApplicationSpecificPreferencesKeys.textProcessingEditingState(peerId: saveRestoreStateId))
).get()?.get(EditState.self)
}
let sharedDataEntries = await context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings]).get()
let translationSettings: TranslationSettings
if let value = sharedDataEntries.entries[ApplicationSpecificSharedDataKeys.translationSettings], let parsedValue = value.get(TranslationSettings.self) {
translationSettings = parsedValue
} else {
translationSettings = .defaultSettings
}
super.init(
context: context,
component: TextProcessingSheetComponent(
context: context,
mode: mode,
ignoredTranslationLanguages: translationSettings.ignoredLanguages ?? [],
initialStyles: styles,
inputText: inputText,
initialEditState: initialEditState,
shouldDisplayStyleNotice: shouldDisplayStyleNotice,
previewIconFile: previewIconFile,
copyCurrentResult: copyResult,
translateChat: translateChat
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
theme: theme.flatMap({ .custom($0) }) ?? .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<ViewControllerComponentContainer.Environment>.View.Tag()) as? ResizableSheetComponent<ViewControllerComponentContainer.Environment>.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<Empty>?
private let title = ComponentView<Empty>()
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<Empty>, 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<Empty>
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<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class ActionButtonsComponent: Component {
let theme: PresentationTheme
let strings: PresentationStrings
let actionTitle: String
let actionButtonShowsIncreaseLimit: Bool
let action: (() -> Void)?
let sendAction: (() -> Void)?
let longPressSendAction: ((UIView) -> Void)?
init(
theme: PresentationTheme,
strings: PresentationStrings,
actionTitle: String,
actionButtonShowsIncreaseLimit: Bool,
action: (() -> Void)?,
sendAction: (() -> Void)?,
longPressSendAction: ((UIView) -> Void)?
) {
self.theme = theme
self.strings = strings
self.actionTitle = actionTitle
self.actionButtonShowsIncreaseLimit = actionButtonShowsIncreaseLimit
self.action = action
self.sendAction = sendAction
self.longPressSendAction = longPressSendAction
}
static func ==(lhs: ActionButtonsComponent, rhs: ActionButtonsComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.actionTitle != rhs.actionTitle {
return false
}
if lhs.actionButtonShowsIncreaseLimit != rhs.actionButtonShowsIncreaseLimit {
return false
}
if (lhs.action == nil) != (rhs.action == nil) {
return false
}
if (lhs.sendAction == nil) != (rhs.sendAction == nil) {
return false
}
if (lhs.longPressSendAction == nil) != (rhs.longPressSendAction == nil) {
return false
}
return true
}
final class View: UIView {
private let actionButton = ComponentView<Empty>()
private let sendButton = ComponentView<Empty>()
private let extractedContainerView = ContextExtractedContentContainingView()
private var component: ActionButtonsComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(self.extractedContainerView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: ActionButtonsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let spacing: CGFloat = 10.0
var actionButtonWidth: CGFloat = availableSize.width
if component.sendAction != nil {
actionButtonWidth -= 52.0 + spacing
}
var actionButtonContents: [AnyComponentWithIdentity<Empty>] = []
actionButtonContents.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.actionTitle, font: Font.semibold(17.0), textColor: component.theme.list.itemCheckColors.foregroundColor))
))))
if component.actionButtonShowsIncreaseLimit {
actionButtonContents.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(IncreaseLimitBadgeComponent(
title: component.strings.TextProcessing_ActionValueNonPremium,
fillColor: component.theme.list.itemCheckColors.foregroundColor,
foregroundColor: .clear
))))
}
let actionButtonSize = self.actionButton.update(
transition: transition,
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: component.theme.list.itemCheckColors.fillColor,
foreground: component.theme.list.itemCheckColors.foregroundColor,
pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(HStack(
actionButtonContents,
spacing: 6.0
))
),
isEnabled: component.action != nil,
displaysProgress: false,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.action?()
}
)),
environment: {},
containerSize: CGSize(width: actionButtonWidth, height: availableSize.height)
)
let actionButtonFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: actionButtonSize)
if let actionButtonView = self.actionButton.view {
if actionButtonView.superview == nil {
self.addSubview(actionButtonView)
}
transition.setFrame(view: actionButtonView, frame: actionButtonFrame)
}
let sendButtonSize = self.sendButton.update(
transition: transition,
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: component.theme.list.itemCheckColors.fillColor,
foreground: component.theme.list.itemCheckColors.foregroundColor,
pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(TransformContents(content: AnyComponent(BundleIconComponent(
name: "TextProcessing/SendIcon",
tintColor: component.theme.list.itemCheckColors.foregroundColor
)), translation: CGPoint(x: -2.0, y: 0.0)))
),
contentInsets: UIEdgeInsets(),
isEnabled: component.action != nil,
displaysProgress: false,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.sendAction?()
},
longPressAction: component.longPressSendAction == nil ? nil : { [weak self] in
guard let self else {
return
}
component.longPressSendAction?(self.extractedContainerView)
}
)),
environment: {},
containerSize: CGSize(width: 52.0, height: 52.0)
)
let sendButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - sendButtonSize.width, y: 0.0), size: sendButtonSize)
if let sendButtonView = self.sendButton.view {
if sendButtonView.superview == nil {
self.extractedContainerView.contentView.addSubview(sendButtonView)
}
sendButtonView.frame = CGRect(origin: CGPoint(), size: sendButtonFrame.size)
}
transition.setPosition(view: self.extractedContainerView, position: sendButtonFrame.center)
transition.setBounds(view: self.extractedContainerView, bounds: CGRect(origin: CGPoint(), size: sendButtonFrame.size))
transition.setPosition(view: self.extractedContainerView.contentView, position: CGPoint(x: sendButtonFrame.width * 0.5, y: sendButtonFrame.height * 0.5))
transition.setBounds(view: self.extractedContainerView.contentView, bounds: CGRect(origin: CGPoint(), size: sendButtonFrame.size))
transition.setAlpha(view: self.extractedContainerView, alpha: component.sendAction != nil ? 1.0 : 0.0)
transition.setScale(view: self.extractedContainerView, scale: component.sendAction != nil ? 1.0 : 0.001)
return CGSize(width: availableSize.width, height: actionButtonSize.height)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class IncreaseLimitBadgeComponent: Component {
let title: String
let fillColor: UIColor
let foregroundColor: UIColor
init(
title: String,
fillColor: UIColor,
foregroundColor: UIColor
) {
self.title = title
self.fillColor = fillColor
self.foregroundColor = foregroundColor
}
static func ==(lhs: IncreaseLimitBadgeComponent, rhs: IncreaseLimitBadgeComponent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.fillColor != rhs.fillColor {
return false
}
if lhs.foregroundColor != rhs.foregroundColor {
return false
}
return true
}
final class View: UIView {
private let iconView: UIImageView
private var component: IncreaseLimitBadgeComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.iconView = UIImageView()
super.init(frame: frame)
self.addSubview(self.iconView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: IncreaseLimitBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let leftInset: CGFloat = 4.0
let rightInset: CGFloat = 3.0
let topInset: CGFloat = 1.0
let bottomInset: CGFloat = 0.0
let text = NSAttributedString(string: component.title, font: Font.with(size: 14.0, design: .round, weight: .semibold), textColor: .clear)
let rawTextSize = text.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: [.usesLineFragmentOrigin], context: nil)
let textSize = CGSize(width: ceil(rawTextSize.width), height: ceil(rawTextSize.height))
let backgroundSize = CGSize(width: leftInset + rightInset + textSize.width, height: topInset + bottomInset + textSize.height)
self.iconView.image = generateImage(backgroundSize, rotatedContext: { size, context in
UIGraphicsPushContext(context)
defer {
UIGraphicsPopContext()
}
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(component.fillColor.cgColor)
UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: 4.0).fill()
if component.foregroundColor.alpha != 1.0 {
context.setBlendMode(.copy)
}
text.draw(at: CGPoint(x: leftInset, y: topInset))
})
self.iconView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: backgroundSize)
return backgroundSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}