This commit is contained in:
Isaac 2026-03-22 01:41:58 +03:00
parent 40e1e6a116
commit 28323206b3
6 changed files with 118 additions and 57 deletions

View file

@ -1775,7 +1775,7 @@ public extension TelegramEngine {
return _internal_composeAIMessageStyles(account: self.account)
}
public func composeAIMessage(text: TextWithEntities, mode: TelegramComposeAIMessageMode) -> Signal<TelegramAIComposeMessageResult?, NoError> {
public func composeAIMessage(text: TextWithEntities, mode: TelegramComposeAIMessageMode) -> Signal<TelegramAIComposeMessageResult, TelegramAIComposeMessageError> {
return _internal_composeAIMessage(account: self.account, text: text, mode: mode)
}
}

View file

@ -471,7 +471,12 @@ func _internal_composeAIMessageStyles(account: Account) -> Signal<[TelegramCompo
}
}
func _internal_composeAIMessage(account: Account, text: TextWithEntities, mode: TelegramComposeAIMessageMode) -> Signal<TelegramAIComposeMessageResult?, NoError> {
public enum TelegramAIComposeMessageError {
case generic
case nonPremiumFlood
}
func _internal_composeAIMessage(account: Account, text: TextWithEntities, mode: TelegramComposeAIMessageMode) -> Signal<TelegramAIComposeMessageResult, TelegramAIComposeMessageError> {
var flags: Int32 = 0
var translateToLang: String?
var changeTone: String?
@ -505,14 +510,14 @@ func _internal_composeAIMessage(account: Account, text: TextWithEntities, mode:
return account.network.request(Api.functions.messages.composeMessageWithAI(flags: flags, text: inputText, translateToLang: translateToLang, changeTone: changeTone))
|> delay(0.4, queue: .mainQueue())
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.ComposedMessageWithAI?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<TelegramAIComposeMessageResult?, NoError> in
guard let result else {
return .single(nil)
|> `catch` { error -> Signal<Api.messages.ComposedMessageWithAI, TelegramAIComposeMessageError> in
if error.errorDescription == "AICOMPOSE_FLOOD_PREMIUM" {
return .fail(.nonPremiumFlood)
} else {
return .fail(.generic)
}
}
|> mapToSignal { result -> Signal<TelegramAIComposeMessageResult, TelegramAIComposeMessageError> in
switch result {
case let .composedMessageWithAI(composedMessageWithAI):
var diffRanges: [Range<Int>] = []

View file

@ -3558,7 +3558,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
transition.updateFrame(view: aiButton.button, frame: aiButtonFrame)
transition.updateFrame(view: aiButton.icon, frame: image.size.centered(in: aiButtonFrame))
}
let aiButtonAlpha: CGFloat = actualTextFieldFrame.height >= 78.0 ? 1.0 : 0.0
let aiButtonAlpha: CGFloat = actualTextFieldFrame.height >= 70.0 ? 1.0 : 0.0
transition.updateAlpha(layer: aiButton.button.layer, alpha: aiButtonAlpha)
transition.updateAlpha(layer: aiButton.icon.layer, alpha: aiButtonAlpha)
} else if let aiButton = self.aiButton {

View file

@ -348,6 +348,7 @@ public final class TabBarComponent: Component {
public let theme: PresentationTheme
public let tintSelectedItem: Bool
public let isLiftedStateEnabled: Bool
public let strings: PresentationStrings
public let items: [Item]
public let search: Search?
@ -357,6 +358,7 @@ public final class TabBarComponent: Component {
public init(
theme: PresentationTheme,
tintSelectedItem: Bool = true,
isLiftedStateEnabled: Bool = true,
strings: PresentationStrings,
items: [Item],
search: Search?,
@ -365,6 +367,7 @@ public final class TabBarComponent: Component {
) {
self.theme = theme
self.tintSelectedItem = tintSelectedItem
self.isLiftedStateEnabled = isLiftedStateEnabled
self.strings = strings
self.items = items
self.search = search
@ -379,6 +382,9 @@ public final class TabBarComponent: Component {
if lhs.tintSelectedItem != rhs.tintSelectedItem {
return false
}
if lhs.isLiftedStateEnabled != rhs.isLiftedStateEnabled {
return false
}
if lhs.strings !== rhs.strings {
return false
}
@ -874,7 +880,7 @@ public final class TabBarComponent: Component {
lensSelection.x = max(0.0, min(lensSelection.x, lensSize.width - lensSelection.width))
self.liquidLensView.update(size: lensSize, selectionOrigin: CGPoint(x: lensSelection.x, y: 0.0), selectionSize: CGSize(width: lensSelection.width, height: lensSize.height), inset: 4.0, isDark: component.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, isCollapsed: isLensCollapsed, transition: transition)
self.liquidLensView.update(size: lensSize, selectionOrigin: CGPoint(x: lensSelection.x, y: 0.0), selectionSize: CGSize(width: lensSelection.width, height: lensSize.height), inset: 4.0, isDark: component.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil && component.isLiftedStateEnabled, isCollapsed: isLensCollapsed, transition: transition)
var size = tabsSize

View file

@ -25,6 +25,7 @@ final class TextProcessingContentComponent: Component {
final class ExternalState {
fileprivate(set) var isProcessing: Bool = false
fileprivate(set) var nonPremiumFloodTriggered: Bool = false
fileprivate(set) var result: TextWithEntities?
init() {
@ -115,6 +116,12 @@ final class TextProcessingContentComponent: Component {
component.externalState.isProcessing = isProcessing
}
}
self.translateState.nonPremiumFloodTriggeredUpdated = { [weak self] _ in
guard let self else {
return
}
self.nonPremiumFloodTriggeredUpdated()
}
self.stylizeState.resultUpdated = { [weak self] result in
guard let self, let component = self.component else {
return
@ -131,6 +138,12 @@ final class TextProcessingContentComponent: Component {
component.externalState.isProcessing = isProcessing
}
}
self.stylizeState.nonPremiumFloodTriggeredUpdated = { [weak self] _ in
guard let self else {
return
}
self.nonPremiumFloodTriggeredUpdated()
}
self.fixState.resultUpdated = { [weak self] result in
guard let self, let component = self.component else {
return
@ -147,11 +160,24 @@ final class TextProcessingContentComponent: Component {
component.externalState.isProcessing = isProcessing
}
}
self.fixState.nonPremiumFloodTriggeredUpdated = { [weak self] _ in
guard let self else {
return
}
self.nonPremiumFloodTriggeredUpdated()
}
}
required init?(coder: NSCoder) {
preconditionFailure()
}
private func nonPremiumFloodTriggeredUpdated() {
guard let component = self.component else {
return
}
component.externalState.nonPremiumFloodTriggered = self.translateState.nonPremiumFloodTriggered || self.stylizeState.nonPremiumFloodTriggered || self.fixState.nonPremiumFloodTriggered
}
func update(component: TextProcessingContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
@ -250,6 +276,7 @@ final class TextProcessingContentComponent: Component {
component: AnyComponent(TabBarComponent(
theme: environment.theme,
tintSelectedItem: false,
isLiftedStateEnabled: false,
strings: environment.strings,
items: tabs,
search: nil,
@ -564,23 +591,49 @@ private final class TextProcessingSheetComponent: Component {
let performMainAction: () -> Void
var performSendAction: ((TextWithEntities) -> Void)?
let isMainActionEnabled: Bool
switch component.mode {
case let .edit(completion, send):
performSendAction = send
isMainActionEnabled = !self.contentExternalState.isProcessing
//TODO:localize (for AI: don't remove this line)
let actionButtonTitle: String
if self.contentExternalState.nonPremiumFloodTriggered {
isMainActionEnabled = true
actionButtonTitle = "Increase Limit"
performMainAction = { [weak self] in
guard let self else {
guard let self, let component = self.component else {
return
}
if let result = self.contentExternalState.result {
completion(result)
let context = component.context
var replaceImpl: ((ViewController) -> Void)?
let controller = component.context.sharedContext.makePremiumDemoController(context: component.context, subject: .doubleLimits, forceDark: false, action: {
let controller = component.context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil)
replaceImpl?(controller)
}, dismissed: nil)
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
dismiss(true)
self.environment?.controller()?.push(controller)
}
case .translate:
isMainActionEnabled = true
performMainAction = {
dismiss(true)
} else {
switch component.mode {
case let .edit(completion, send):
actionButtonTitle = "Apply"
performSendAction = send
isMainActionEnabled = !self.contentExternalState.isProcessing
performMainAction = { [weak self] in
guard let self else {
return
}
if let result = self.contentExternalState.result {
completion(result)
}
dismiss(true)
}
case .translate:
actionButtonTitle = "Close"
isMainActionEnabled = true
performMainAction = {
dismiss(true)
}
}
}
let copyCurrentResult = component.copyCurrentResult
@ -594,16 +647,12 @@ private final class TextProcessingSheetComponent: Component {
}
}
//TODO:localize (for AI: don't remove this line)
let titleString: String
let actionButtonTitle: String
switch component.mode {
case .edit:
titleString = "AI Editor"
actionButtonTitle = "Apply"
case .translate:
titleString = "Translation"
actionButtonTitle = "Close"
}
let sheetSize = self.sheet.update(

View file

@ -59,6 +59,15 @@ final class TextProcessingTranslateContentComponent: Component {
}
var isProcessingUpdated: ((Bool) -> Void)?
fileprivate(set) var nonPremiumFloodTriggered: Bool = false {
didSet {
if self.isProcessing != oldValue {
self.nonPremiumFloodTriggeredUpdated?(self.nonPremiumFloodTriggered)
}
}
}
var nonPremiumFloodTriggeredUpdated: ((Bool) -> Void)?
init() {
}
}
@ -161,52 +170,33 @@ final class TextProcessingTranslateContentComponent: Component {
}
if let result = component.externalState.result, result.text == nil, self.processDisposable == nil {
let mappedMode: TelegramComposeAIMessageMode?
switch component.mode {
case .translate:
component.externalState.isProcessing = true
self.processDisposable = (component.context.engine.messages.composeAIMessage(
text: component.inputText,
mode: .translate(toLanguage: result.language, emojify: component.externalState.emojify, style: component.externalState.style)
) |> deliverOnMainQueue).startStrict(next: { [weak self] processedText in
guard let self, let component = self.component, let processedText else {
return
}
component.externalState.isProcessing = false
component.externalState.result = (result.language, processedText.text, processedText.diffRanges)
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.4))
}
})
mappedMode = .translate(toLanguage: result.language, emojify: component.externalState.emojify, style: component.externalState.style)
case .stylize:
if !component.externalState.emojify && component.externalState.style == .neutral {
mappedMode = nil
component.externalState.isProcessing = false
component.externalState.result = (result.language, component.inputText, [])
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.4))
}
} else {
component.externalState.isProcessing = true
self.processDisposable = (component.context.engine.messages.composeAIMessage(
text: component.inputText,
mode: .stylize(emojify: component.externalState.emojify, style: component.externalState.style)
) |> deliverOnMainQueue).startStrict(next: { [weak self] processedText in
guard let self, let component = self.component, let processedText else {
return
}
component.externalState.isProcessing = false
component.externalState.result = (result.language, processedText.text, processedText.diffRanges)
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.4))
}
})
mappedMode = .stylize(emojify: component.externalState.emojify, style: component.externalState.style)
}
case .fix:
mappedMode = .proofread
}
if let mappedMode {
component.externalState.isProcessing = true
self.processDisposable = (component.context.engine.messages.composeAIMessage(
text: component.inputText,
mode: .proofread
mode: mappedMode
) |> deliverOnMainQueue).startStrict(next: { [weak self] processedText in
guard let self, let component = self.component, let processedText else {
guard let self, let component = self.component else {
return
}
component.externalState.isProcessing = false
@ -214,6 +204,17 @@ final class TextProcessingTranslateContentComponent: Component {
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.4))
}
}, error: { [weak self] error in
guard let self, let component = self.component else {
return
}
component.externalState.isProcessing = false
if case .nonPremiumFlood = error {
component.externalState.nonPremiumFloodTriggered = true
}
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.4))
}
})
}
}