This commit is contained in:
Isaac 2026-03-23 21:33:34 +08:00
parent 675696da2b
commit 0aac8d47ac
11 changed files with 197 additions and 19 deletions

View file

@ -1537,14 +1537,18 @@ public protocol SharedAccountContext: AnyObject {
func makeLoginEmailSetupController(context: AccountContext, blocking: Bool, emailPattern: String?, canAutoDismissIfNeeded: Bool, navigationController: NavigationController?, completion: @escaping () -> Void, dismiss: @escaping () -> Void) -> ViewController
func makePasskeySetupController(context: AccountContext, displaySkip: Bool, navigationController: NavigationController?, completion: @escaping () -> Void, dismiss: @escaping () -> Void) -> ViewController
func makeChatCustomRankSetupScreen(context: AccountContext, peerId: EnginePeer.Id, participantId: EnginePeer.Id, rank: String?, role: ChatRankInfoScreenRole) -> ViewController
func makePeerCopyProtectionInfoScreen(context: AccountContext, completion: @escaping () -> Void) -> ViewController
func makeChatRankInfoScreen(context: AccountContext, chatPeer: EnginePeer, userPeer: EnginePeer, role: ChatRankInfoScreenRole, rank: String, canChange: Bool, completion: @escaping () -> Void) -> ViewController
func makeChatRankPreviewItem(context: AccountContext, peer: EnginePeer, rank: String, rankRole: ChatRankInfoScreenRole, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, sectionId: Int32) -> ListViewItem
func makeCreateBotScreen(
context: AccountContext,
parentBot: EnginePeer.Id,
initialUsername: String?,
initialTitle: String?,
openAutomatically: Bool,
completion: @escaping (EnginePeer.Id?) -> Void
) async -> ViewController?
func navigateToCurrentCall()
var hasOngoingCall: ValuePromise<Bool> { get }

View file

@ -1778,6 +1778,26 @@ public extension TelegramEngine {
public func composeAIMessage(text: TextWithEntities, mode: TelegramComposeAIMessageMode) -> Signal<TelegramAIComposeMessageResult, TelegramAIComposeMessageError> {
return _internal_composeAIMessage(account: self.account, text: text, mode: mode)
}
public func requestMiniAppButton(peerId: EnginePeer.Id, requestId: String) -> Signal<ReplyMarkupButton?, NoError> {
let account = self.account
return self.account.postbox.transaction { transaction -> Api.InputUser? in
return transaction.getPeer(peerId).flatMap(apiInputUser)
}
|> mapToSignal { inputUser -> Signal<ReplyMarkupButton?, NoError> in
guard let inputUser else {
return .single(nil)
}
return account.network.request(Api.functions.bots.getRequestedWebViewButton(bot: inputUser, webappReqId: requestId))
|> map { result -> ReplyMarkupButton in
return ReplyMarkupButton(apiButton: result)
}
|> map(Optional.init)
|> `catch` { _ -> Signal<ReplyMarkupButton?, NoError> in
return .single(nil)
}
}
}
}
}

View file

@ -2,6 +2,8 @@ import Foundation
import SwiftSignalKit
import Postbox
public typealias EnginePreferencesEntry = PreferencesEntry
public extension TelegramEngine {
final class Preferences {
private let account: Account

View file

@ -315,17 +315,23 @@ private final class CreateBotSheetComponent: Component {
let parentPeer: EnginePeer
let initialUsername: String?
let initialTitle: String?
let openAutomatically: Bool
let completion: (EnginePeer.Id) -> Void
init(
context: AccountContext,
parentPeer: EnginePeer,
initialUsername: String?,
initialTitle: String?
initialTitle: String?,
openAutomatically: Bool,
completion: @escaping (EnginePeer.Id) -> Void
) {
self.context = context
self.parentPeer = parentPeer
self.initialUsername = initialUsername
self.initialTitle = initialTitle
self.openAutomatically = openAutomatically
self.completion = completion
}
static func ==(lhs: CreateBotSheetComponent, rhs: CreateBotSheetComponent) -> Bool {
@ -410,14 +416,14 @@ private final class CreateBotSheetComponent: Component {
self.animateOut.invoke(Action { [weak controller, weak navigationController] _ in
if let controller, let navigationController {
controller.dismiss(completion: { [weak navigationController] in
guard let navigationController else {
return
if component.openAutomatically, let navigationController {
component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(
navigationController: navigationController,
context: context,
chatLocation: .peer(botPeer)
))
}
component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(
navigationController: navigationController,
context: context,
chatLocation: .peer(botPeer)
))
component.completion(botPeer.id)
})
}
})
@ -573,7 +579,9 @@ public class CreateBotScreen: ViewControllerComponentContainer {
context: AccountContext,
parentBot: EnginePeer.Id,
initialUsername: String?,
initialTitle: String?
initialTitle: String?,
openAutomatically: Bool,
completion: @escaping (EnginePeer.Id) -> Void
) async {
self.context = context
@ -589,7 +597,9 @@ public class CreateBotScreen: ViewControllerComponentContainer {
context: context,
parentPeer: parentPeer,
initialUsername: initialUsername,
initialTitle: initialTitle
initialTitle: initialTitle,
openAutomatically: openAutomatically,
completion: completion
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,

View file

@ -39,6 +39,7 @@ swift_library(
"//submodules/TelegramUI/Components/ToastComponent",
"//submodules/TelegramNotices",
"//submodules/Markdown",
"//submodules/TelegramUIPreferences",
],
visibility = [
"//visibility:public",

View file

@ -22,6 +22,7 @@ import ListActionItemComponent
import ToastComponent
import TelegramNotices
import Markdown
import TelegramUIPreferences
final class TextProcessingContentComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -40,6 +41,7 @@ final class TextProcessingContentComponent: Component {
let mode: TextProcessingScreen.Mode
let styles: [TelegramComposeAIMessageMode.Style]
let inputText: TextWithEntities
let initialEditState: TextProcessingScreen.EditState?
let shouldDisplayStyleNotice: Bool
let copyCurrentResult: (() -> Void)?
let translateChat: ((String) -> Void)?
@ -51,6 +53,7 @@ final class TextProcessingContentComponent: Component {
mode: TextProcessingScreen.Mode,
styles: [TelegramComposeAIMessageMode.Style],
inputText: TextWithEntities,
initialEditState: TextProcessingScreen.EditState?,
shouldDisplayStyleNotice: Bool,
copyCurrentResult: (() -> Void)?,
translateChat: ((String) -> Void)?,
@ -61,6 +64,7 @@ final class TextProcessingContentComponent: Component {
self.context = context
self.mode = mode
self.inputText = inputText
self.initialEditState = initialEditState
self.shouldDisplayStyleNotice = shouldDisplayStyleNotice
self.copyCurrentResult = copyCurrentResult
self.translateChat = translateChat
@ -106,7 +110,6 @@ final class TextProcessingContentComponent: Component {
self.addSubview(self.currentContentBackground)
self.addSubview(self.currentContentContainer)
self.translateState.resultUpdated = { [weak self] _ in
self?.externalStatesUpdated()
}
@ -163,6 +166,30 @@ final class TextProcessingContentComponent: Component {
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)
})
}
}
func update(component: TextProcessingContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
@ -498,6 +525,7 @@ private final class TextProcessingSheetComponent: Component {
let ignoredTranslationLanguages: [String]
let styles: [TelegramComposeAIMessageMode.Style]
let inputText: TextWithEntities
let initialEditState: TextProcessingScreen.EditState?
let shouldDisplayStyleNotice: Bool
let copyCurrentResult: ((TextWithEntities) -> Void)?
let translateChat: ((String) -> Void)?
@ -508,6 +536,7 @@ private final class TextProcessingSheetComponent: Component {
ignoredTranslationLanguages: [String],
styles: [TelegramComposeAIMessageMode.Style],
inputText: TextWithEntities,
initialEditState: TextProcessingScreen.EditState?,
shouldDisplayStyleNotice: Bool,
copyCurrentResult: ((TextWithEntities) -> Void)?,
translateChat: ((String) -> Void)?
@ -517,6 +546,7 @@ private final class TextProcessingSheetComponent: Component {
self.ignoredTranslationLanguages = ignoredTranslationLanguages
self.styles = styles
self.inputText = inputText
self.initialEditState = initialEditState
self.shouldDisplayStyleNotice = shouldDisplayStyleNotice
self.copyCurrentResult = copyCurrentResult
self.translateChat = translateChat
@ -614,7 +644,7 @@ private final class TextProcessingSheetComponent: Component {
}
} else {
switch component.mode {
case let .edit(completion, send):
case let .edit(_, completion, send):
actionButtonTitle = "Apply"
performSendAction = send
isMainActionEnabled = !self.contentExternalState.isProcessing
@ -663,6 +693,7 @@ private final class TextProcessingSheetComponent: Component {
mode: component.mode,
styles: component.styles,
inputText: component.inputText,
initialEditState: component.initialEditState,
shouldDisplayStyleNotice: component.shouldDisplayStyleNotice,
copyCurrentResult: component.copyCurrentResult != nil ? {
copyCurrentResultImpl()
@ -891,10 +922,18 @@ private final class TextProcessingSheetComponent: Component {
public class TextProcessingScreen: ViewControllerComponentContainer {
public enum Mode {
case edit(completion: (TextWithEntities) -> Void, send: ((TextWithEntities) -> Void)?)
case edit(saveRestoreStateId: EnginePeer.Id?, completion: (TextWithEntities) -> Void, send: ((TextWithEntities) -> Void)?)
case translate(fromLanguage: String?)
}
struct EditState: Codable {
var selectedMode: Int32
init(selectedMode: Int32) {
self.selectedMode = selectedMode
}
}
private let context: AccountContext
public init(
@ -908,8 +947,14 @@ public class TextProcessingScreen: ViewControllerComponentContainer {
self.context = context
let styles = await context.engine.messages.composeAIMessageStyles().get()
let 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)
}
super.init(
context: context,
@ -919,6 +964,7 @@ public class TextProcessingScreen: ViewControllerComponentContainer {
ignoredTranslationLanguages: ignoredTranslationLanguages,
styles: styles,
inputText: inputText,
initialEditState: initialEditState,
shouldDisplayStyleNotice: shouldDisplayStyleNotice,
copyCurrentResult: copyResult,
translateChat: translateChat

View file

@ -4482,6 +4482,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
self.controller?.push(await TextProcessingScreen(
context: self.context,
mode: .edit(
saveRestoreStateId: self.chatLocation.peerId,
completion: { [weak self] text in
guard let self, let controller = self.controller else {
return

View file

@ -1944,7 +1944,10 @@ func openResolvedUrlImpl(
context: context,
parentBot: parentBot,
initialUsername: username,
initialTitle: title
initialTitle: title,
openAutomatically: true,
completion: { _ in
}
) else {
return
}

View file

@ -100,6 +100,7 @@ import ChatParticipantRightsScreen
import PeerCopyProtectionInfoScreen
import ChatRankInfoScreen
import RankChatPreviewItem
import CreateBotScreen
private final class AccountUserInterfaceInUseContext {
let subscribers = Bag<(Bool) -> Void>()
@ -4363,6 +4364,24 @@ public final class SharedAccountContextImpl: SharedAccountContext {
)
return RankChatPreviewItem(context: context, systemStyle: .glass, theme: theme, componentTheme: theme, strings: strings, sectionId: sectionId, fontSize: fontSize, chatBubbleCorners: chatBubbleCorners, wallpaper: wallpaper, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameOrder, messageItems: [messageItem])
}
public func makeCreateBotScreen(
context: AccountContext,
parentBot: EnginePeer.Id,
initialUsername: String?,
initialTitle: String?,
openAutomatically: Bool,
completion: @escaping (EnginePeer.Id?) -> Void
) async -> ViewController? {
return await CreateBotScreen(
context: context,
parentBot: parentBot,
initialUsername: initialUsername,
initialTitle: initialTitle,
openAutomatically: openAutomatically,
completion: completion
)
}
}
private func peerInfoControllerImpl(context: AccountContext, updatedPresentationData: (PresentationData, Signal<PresentationData, NoError>)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, requestsContext: PeerInvitationImportersContext? = nil) -> ViewController? {

View file

@ -9,6 +9,7 @@ private enum ApplicationSpecificPreferencesKeyValues: Int32 {
case widgetSettings = 19
case mediaAutoSaveSettings = 20
case ageVerificationState = 21
case textProcessingEditingState = 22
}
public struct ApplicationSpecificPreferencesKeys {
@ -18,6 +19,13 @@ public struct ApplicationSpecificPreferencesKeys {
public static let widgetSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.widgetSettings.rawValue)
public static let mediaAutoSaveSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.mediaAutoSaveSettings.rawValue)
public static let ageVerificationState = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.ageVerificationState.rawValue)
public static func textProcessingEditingState(peerId: PeerId) -> ValueBoxKey {
let key = ValueBoxKey(length: 4 + 8)
key.setInt32(0, value: ApplicationSpecificPreferencesKeyValues.textProcessingEditingState.rawValue)
key.setInt64(4, value: peerId.toInt64())
return key
}
}
private enum ApplicationSpecificSharedDataKeyValues: Int32 {

View file

@ -1930,6 +1930,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
self.controller?.verifyAgeCompletion?(Int(ageValue))
}
}
case "web_app_request_chat":
if let json, let requestId = json["req_id"] as? String {
self.requestChat(requestId: requestId)
}
default:
break
}
@ -2299,6 +2303,66 @@ public final class WebAppController: ViewController, AttachmentContainable {
})
}
fileprivate func requestChat(requestId: String) {
guard let controller = self.controller, !self.dismissed else {
return
}
let _ = (self.context.engine.messages.requestMiniAppButton(peerId: controller.botId, requestId: requestId)
|> deliverOnMainQueue).startStandalone(next: { [weak self] button in
guard let self, let button else {
return
}
switch button.action {
case let .requestPeer(peerType, buttonId, maxQuantity):
let _ = maxQuantity
switch peerType {
case let .createBot(createBot):
Task { @MainActor [weak self] in
guard let self, let controller = self.controller else {
return
}
let createBotScreen = await self.context.sharedContext.makeCreateBotScreen(
context: self.context,
parentBot: controller.botId,
initialUsername: createBot.suggestedUsername,
initialTitle: createBot.suggestedName,
openAutomatically: false,
completion: { [weak self] resultId in
guard let self, let controller = self.controller else {
return
}
if let resultId {
let _ = self.context.engine.peers.sendBotRequestedPeer(peerId: controller.botId, requestId: requestId, buttonId: buttonId, requestedPeerIds: [resultId]
).startStandalone(error: { [weak self] _ in
guard let self else {
return
}
self.webView?.sendEvent(name: "requested_chat_failed", data: nil)
}, completed: { [weak self] in
guard let self else {
return
}
self.webView?.sendEvent(name: "requested_chat_sent", data: nil)
})
} else {
self.webView?.sendEvent(name: "requested_chat_failed", data: nil)
}
}
)
if let createBotScreen {
controller.push(createBotScreen)
}
}
default:
break
}
default:
break
}
})
}
fileprivate func invokeCustomMethod(requestId: String, method: String, params: String) {
guard let controller = self.controller, !self.dismissed else {
return