Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2026-03-24 07:13:53 +01:00
commit ed6511a79e
22 changed files with 565 additions and 80 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

@ -1382,7 +1382,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog
)),
hasEntityKeyboard: hasEntityKeyboard,
gesture: gesture,
sourceSendButton: node,
sourceSendButton: node.view,
textInputView: textInputNode.textView,
emojiViewProvider: textInputPanelNode.emojiViewProvider,
completion: {

View file

@ -78,9 +78,9 @@ public func makeChatSendMessageActionSheetController(
peerId: EnginePeer.Id?,
params: SendMessageActionSheetControllerParams,
hasEntityKeyboard: Bool,
gesture: ContextGesture,
sourceSendButton: ASDisplayNode,
textInputView: UITextView,
gesture: ContextGesture?,
sourceSendButton: UIView,
textInputView: UITextView?,
emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?,
wallpaperBackgroundNode: WallpaperBackgroundNode? = nil,
completion: @escaping () -> Void,

View file

@ -59,9 +59,9 @@ final class ChatSendMessageContextScreenComponent: Component {
let peerId: EnginePeer.Id?
let params: SendMessageActionSheetControllerParams
let hasEntityKeyboard: Bool
let gesture: ContextGesture
let sourceSendButton: ASDisplayNode
let textInputView: UITextView
let gesture: ContextGesture?
let sourceSendButton: UIView
let textInputView: UITextView?
let emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?
let wallpaperBackgroundNode: WallpaperBackgroundNode?
let completion: () -> Void
@ -80,9 +80,9 @@ final class ChatSendMessageContextScreenComponent: Component {
peerId: EnginePeer.Id?,
params: SendMessageActionSheetControllerParams,
hasEntityKeyboard: Bool,
gesture: ContextGesture,
sourceSendButton: ASDisplayNode,
textInputView: UITextView,
gesture: ContextGesture?,
sourceSendButton: UIView,
textInputView: UITextView?,
emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?,
wallpaperBackgroundNode: WallpaperBackgroundNode?,
completion: @escaping () -> Void,
@ -286,7 +286,7 @@ final class ChatSendMessageContextScreenComponent: Component {
guard let self, let component = self.component else {
return
}
let stableSourceSendButtonFrame = convertFrame(component.sourceSendButton.bounds, from: component.sourceSendButton.view, to: self)
let stableSourceSendButtonFrame = convertFrame(component.sourceSendButton.bounds, from: component.sourceSendButton, to: self)
if self.stableSourceSendButtonFrame != stableSourceSendButtonFrame {
self.stableSourceSendButtonFrame = stableSourceSendButtonFrame
if !self.isUpdating {
@ -331,7 +331,7 @@ final class ChatSendMessageContextScreenComponent: Component {
self.mediaCaptionIsAbove = editMessage.mediaCaptionIsAbove?.0 ?? false
}
component.gesture.externalUpdated = { [weak self] view, location in
component.gesture?.externalUpdated = { [weak self] view, location in
guard let self, let actionsStackNode = self.actionsStackNode else {
return
}
@ -346,7 +346,7 @@ final class ChatSendMessageContextScreenComponent: Component {
}
actionsStackNode.highlightGestureMoved(location: self.convert(localPoint, to: actionsStackNode.view))
}
component.gesture.externalEnded = { [weak self] viewAndLocation in
component.gesture?.externalEnded = { [weak self] viewAndLocation in
guard let self, let actionsStackNode = self.actionsStackNode else {
return
}
@ -389,7 +389,7 @@ final class ChatSendMessageContextScreenComponent: Component {
var isMessageVisible: Bool = mediaPreview != nil
let textString: NSAttributedString
if let attributedText = component.textInputView.attributedText {
if let attributedText = component.textInputView?.attributedText {
textString = attributedText
if textString.length != 0 {
isMessageVisible = true
@ -423,23 +423,27 @@ final class ChatSendMessageContextScreenComponent: Component {
let sourceSendButtonFrame: CGRect
switch self.presentationAnimationState {
case .animatedOut:
sourceSendButtonFrame = convertFrame(component.sourceSendButton.bounds, from: component.sourceSendButton.view, to: self)
sourceSendButtonFrame = convertFrame(component.sourceSendButton.bounds, from: component.sourceSendButton, to: self)
self.stableSourceSendButtonFrame = sourceSendButtonFrame
default:
if let stableSourceSendButtonFrame = self.stableSourceSendButtonFrame {
sourceSendButtonFrame = stableSourceSendButtonFrame
} else {
sourceSendButtonFrame = convertFrame(component.sourceSendButton.bounds, from: component.sourceSendButton.view, to: self)
sourceSendButtonFrame = convertFrame(component.sourceSendButton.bounds, from: component.sourceSendButton, to: self)
self.stableSourceSendButtonFrame = sourceSendButtonFrame
}
}
let sendButtonScale: CGFloat
switch self.presentationAnimationState {
case .initial:
sendButtonScale = 0.75
default:
if component.sourceSendButton is ContextExtractedContentContainingView {
sendButtonScale = 1.0
} else {
switch self.presentationAnimationState {
case .initial:
sendButtonScale = 0.75
default:
sendButtonScale = 1.0
}
}
var reminders = false
@ -731,7 +735,12 @@ final class ChatSendMessageContextScreenComponent: Component {
wallpaperBackgroundNode.alpha = 0.0
}
let localSourceTextInputViewFrame = convertFrame(component.textInputView.bounds, from: component.textInputView, to: self)
let localSourceTextInputViewFrame: CGRect
if let textInputView = component.textInputView {
localSourceTextInputViewFrame = convertFrame(textInputView.bounds, from: textInputView, to: self)
} else {
localSourceTextInputViewFrame = convertFrame(component.sourceSendButton.bounds, from: component.sourceSendButton, to: self)
}
let sourceMessageTextInsets = UIEdgeInsets(top: 7.0, left: 12.0, bottom: 6.0, right: 20.0)
let sourceBackgroundSize = CGSize(width: localSourceTextInputViewFrame.width + 32.0, height: localSourceTextInputViewFrame.height + 4.0)
@ -1116,7 +1125,12 @@ final class ChatSendMessageContextScreenComponent: Component {
}
}
let sendButtonSize = CGSize(width: min(sourceSendButtonFrame.width, 40.0), height: sourceSendButtonFrame.height)
let sendButtonSize: CGSize
if component.sourceSendButton is ContextExtractedContentContainingView {
sendButtonSize = sourceSendButtonFrame.size
} else {
sendButtonSize = CGSize(width: min(sourceSendButtonFrame.width, 40.0), height: sourceSendButtonFrame.height)
}
var readySendButtonFrame = CGRect(origin: CGPoint(x: sourceSendButtonFrame.maxX - sendButtonSize.width, y: sourceSendButtonFrame.minY), size: sendButtonSize)
var sourceActionsStackFrame = CGRect(origin: CGPoint(x: readySendButtonFrame.minX + 1.0 - 8.0 - actionsStackSize.width, y: sourceMessageItemFrame.maxY + messageActionsSpacing), size: actionsStackSize)
@ -1134,23 +1148,36 @@ final class ChatSendMessageContextScreenComponent: Component {
}
}
var readyActionsStackFrame = CGRect(origin: CGPoint(x: readySendButtonFrame.minX + 1.0 - actionsStackSize.width, y: readyMessageItemFrame.maxY + messageActionsSpacing), size: actionsStackSize)
var readyActionsStackFrame: CGRect
if component.sourceSendButton is ContextExtractedContentContainingView {
readyActionsStackFrame = CGRect(origin: CGPoint(x: readySendButtonFrame.minX + 1.0 - 8.0 - actionsStackSize.width, y: readyMessageItemFrame.maxY + messageActionsSpacing + 4.0), size: actionsStackSize)
} else {
readyActionsStackFrame = CGRect(origin: CGPoint(x: readySendButtonFrame.minX + 1.0 - actionsStackSize.width, y: readyMessageItemFrame.maxY + messageActionsSpacing), size: actionsStackSize)
}
if !isMessageVisible {
readyActionsStackFrame.origin.y = readySendButtonFrame.maxY - readyActionsStackFrame.height - 5.0
if component.sourceSendButton is ContextExtractedContentContainingView {
readyActionsStackFrame.origin.y = readySendButtonFrame.maxY - readyActionsStackFrame.height
} else {
readyActionsStackFrame.origin.y = readySendButtonFrame.maxY - readyActionsStackFrame.height - 5.0
}
}
let bottomOverflow = readyActionsStackFrame.maxY - (availableSize.height - environment.safeInsets.bottom)
if bottomOverflow > 0.0 {
readyMessageItemFrame.origin.y -= bottomOverflow
readyActionsStackFrame.origin.y -= bottomOverflow
readySendButtonFrame.origin.y -= bottomOverflow
if !(component.sourceSendButton is ContextExtractedContentContainingView) {
readySendButtonFrame.origin.y -= bottomOverflow
}
}
let inputCoverOverflow = readyMessageItemFrame.maxY + 7.0 - (availableSize.height - environment.inputHeight)
if inputCoverOverflow > 0.0 {
readyMessageItemFrame.origin.y -= inputCoverOverflow
readyActionsStackFrame.origin.y -= inputCoverOverflow
readySendButtonFrame.origin.y -= inputCoverOverflow
if !(component.sourceSendButton is ContextExtractedContentContainingView) {
readySendButtonFrame.origin.y -= inputCoverOverflow
}
}
if let mediaPreview {
@ -1337,14 +1364,14 @@ final class ChatSendMessageContextScreenComponent: Component {
return
}
if mediaPreview == nil {
component.textInputView.isHidden = true
component.textInputView?.isHidden = true
}
component.sourceSendButton.isHidden = true
})
}
} else {
if mediaPreview == nil {
component.textInputView.isHidden = true
component.textInputView?.isHidden = true
}
component.sourceSendButton.isHidden = true
}
@ -1356,7 +1383,7 @@ final class ChatSendMessageContextScreenComponent: Component {
if self.animateOutToEmpty {
if mediaPreview == nil {
component.textInputView.isHidden = false
component.textInputView?.isHidden = false
}
component.sourceSendButton.isHidden = false
@ -1378,7 +1405,7 @@ final class ChatSendMessageContextScreenComponent: Component {
self.performedActionsOnAnimateOut = true
if let component = self.component, !self.animateOutToEmpty {
if mediaPreview == nil {
component.textInputView.isHidden = false
component.textInputView?.isHidden = false
}
component.sourceSendButton.isHidden = false
}
@ -1431,9 +1458,9 @@ public class ChatSendMessageContextScreen: ViewControllerComponentContainer, Cha
peerId: EnginePeer.Id?,
params: SendMessageActionSheetControllerParams,
hasEntityKeyboard: Bool,
gesture: ContextGesture,
sourceSendButton: ASDisplayNode,
textInputView: UITextView,
gesture: ContextGesture?,
sourceSendButton: UIView,
textInputView: UITextView?,
emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?,
wallpaperBackgroundNode: WallpaperBackgroundNode?,
completion: @escaping () -> Void,

View file

@ -34,7 +34,8 @@ final class SendButton: HighlightTrackingButton {
private var previousIsAnimatedIn: Bool?
private var sourceCustomContentView: UIView?
private weak var extractedContainerView: ContextExtractedContentContainingView?
init(kind: Kind) {
self.kind = kind
@ -64,12 +65,58 @@ final class SendButton: HighlightTrackingButton {
context: AccountContext,
presentationData: PresentationData,
backgroundNode: WallpaperBackgroundNode?,
sourceSendButton: ASDisplayNode,
sourceSendButton: UIView,
isAnimatedIn: Bool,
isLoadingEffectAnimation: Bool,
size: CGSize,
transition: ComponentTransition
) {
if let extractedContainer = sourceSendButton as? ContextExtractedContentContainingView {
self.containerView.isHidden = true
self.sourceCustomContentView?.isHidden = true
let contentView = extractedContainer.contentView
if self.extractedContainerView !== extractedContainer {
self.extractedContainerView = extractedContainer
self.previousIsAnimatedIn = nil
}
if self.previousIsAnimatedIn != isAnimatedIn {
self.previousIsAnimatedIn = isAnimatedIn
if isAnimatedIn {
extractedContainer.willUpdateIsExtractedToContextPreview?(true, .animated(duration: 0.3, curve: .spring))
extractedContainer.isExtractedToContextPreview = true
extractedContainer.isExtractedToContextPreviewUpdated?(true)
extractedContainer.layer.removeAnimation(forKey: "extractedContentReturn")
self.addSubview(contentView)
} else {
extractedContainer.willUpdateIsExtractedToContextPreview?(false, .animated(duration: 0.3, curve: .spring))
extractedContainer.isExtractedToContextPreview = false
extractedContainer.isExtractedToContextPreviewUpdated?(false)
transition.attachAnimation(view: self, id: "extractedContentReturn", completion: { [weak extractedContainer] _ in
guard let extractedContainer else {
return
}
extractedContainer.addSubview(extractedContainer.contentView)
})
}
}
if contentView.superview === self {
transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(), size: size))
}
return
}
self.containerView.isHidden = false
self.extractedContainerView = nil
let innerSize: CGSize
if size.height == 40.0 {
innerSize = CGSize(width: size.width - 3.0 * 2.0, height: size.height - 3.0 * 2.0)
@ -93,7 +140,7 @@ final class SendButton: HighlightTrackingButton {
self.sourceCustomContentView = nil
}
if let sourceSendButton = sourceSendButton as? ChatSendMessageActionSheetControllerSourceSendButtonNode {
if let sourceSendButton = sourceSendButton.asyncdisplaykit_node as? ChatSendMessageActionSheetControllerSourceSendButtonNode {
if let sourceCustomContentView = sourceSendButton.makeCustomContents() {
self.sourceCustomContentView = sourceCustomContentView
sourceCustomContentView.alpha = sourceCustomContentViewAlpha

View file

@ -484,7 +484,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case 61:
self = .copyProtectionRequest(hasExpired: decoder.decodeBoolForKey("hasExpired", orElse: false), previousValue: decoder.decodeBoolForKey("previousValue", orElse: false), newValue: decoder.decodeBoolForKey("newValue", orElse: false))
case 62:
self = .managedBotCreated(botId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(decoder.decodeInt64ForKey("botId", orElse: 0))))
self = .managedBotCreated(botId: PeerId(decoder.decodeInt64ForKey("botId", orElse: 0)))
case 63:
self = .pollOptionAppended(decoder.decodeObjectForKey("option", decoder: { TelegramMediaPollOption(decoder: $0) }) as! TelegramMediaPollOption)
default:

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

@ -509,7 +509,6 @@ func _internal_composeAIMessage(account: Account, text: TextWithEntities, mode:
let inputText: Api.TextWithEntities = .textWithEntities(Api.TextWithEntities.Cons_textWithEntities(text: text.text, entities: apiEntitiesFromMessageTextEntities(text.entities, associatedPeers: SimpleDictionary())))
return account.network.request(Api.functions.messages.composeMessageWithAI(flags: flags, text: inputText, translateToLang: translateToLang, changeTone: changeTone))
|> delay(0.4, queue: .mainQueue())
|> `catch` { error -> Signal<Api.messages.ComposedMessageWithAI, TelegramAIComposeMessageError> in
if error.errorDescription == "AICOMPOSE_FLOOD_PREMIUM" {
return .fail(.nonPremiumFlood)

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

@ -389,7 +389,8 @@ public final class ButtonComponent: Component {
public let allowActionWhenDisabled: Bool
public let displaysProgress: Bool
public let action: () -> Void
public let longPressAction: (() -> Void)?
public init(
background: Background,
content: AnyComponentWithIdentity<Empty>,
@ -399,7 +400,8 @@ public final class ButtonComponent: Component {
tintWhenDisabled: Bool = true,
allowActionWhenDisabled: Bool = false,
displaysProgress: Bool = false,
action: @escaping () -> Void
action: @escaping () -> Void,
longPressAction: (() -> Void)? = nil
) {
self.background = background
self.content = content
@ -410,6 +412,7 @@ public final class ButtonComponent: Component {
self.allowActionWhenDisabled = allowActionWhenDisabled
self.displaysProgress = displaysProgress
self.action = action
self.longPressAction = longPressAction
}
public static func ==(lhs: ButtonComponent, rhs: ButtonComponent) -> Bool {
@ -437,6 +440,9 @@ public final class ButtonComponent: Component {
if lhs.displaysProgress != rhs.displaysProgress {
return false
}
if (lhs.longPressAction == nil) != (rhs.longPressAction == nil) {
return false
}
return true
}
@ -462,6 +468,7 @@ public final class ButtonComponent: Component {
private var contentItem: ContentItem?
private var activityIndicator: ActivityIndicator?
private var longPressGesture: UILongPressGestureRecognizer?
override init(frame: CGRect) {
self.containerView = UIView()
@ -479,7 +486,12 @@ public final class ButtonComponent: Component {
self.addSubview(self.button)
self.button.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.longPressed(_:)))
longPressGesture.isEnabled = false
self.longPressGesture = longPressGesture
self.button.addGestureRecognizer(longPressGesture)
self.button.highligthedChanged = { [weak self] highlighted in
if let self, let component = self.component, component.isEnabled {
switch component.background.style {
@ -530,6 +542,13 @@ public final class ButtonComponent: Component {
}
component.action()
}
@objc private func longPressed(_ gesture: UILongPressGestureRecognizer) {
guard gesture.state == .began, let component = self.component else {
return
}
component.longPressAction?()
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return super.hitTest(point, with: event)
@ -538,6 +557,8 @@ public final class ButtonComponent: Component {
func update(component: ButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.componentState = state
self.longPressGesture?.isEnabled = component.longPressAction != nil
self.button.isEnabled = (component.isEnabled || component.allowActionWhenDisabled) && !component.displaysProgress

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

@ -765,7 +765,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
)),
hasEntityKeyboard: hasEntityKeyboard,
gesture: gesture,
sourceSendButton: node,
sourceSendButton: node.view,
textInputView: textInputNode.textView,
emojiViewProvider: textInputPanelNode.emojiViewProvider,
completion: {

View file

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

View file

@ -22,6 +22,8 @@ import ListActionItemComponent
import ToastComponent
import TelegramNotices
import Markdown
import TelegramUIPreferences
import ChatSendMessageActionUI
final class TextProcessingContentComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -40,6 +42,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 +54,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 +65,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
@ -94,7 +99,7 @@ final class TextProcessingContentComponent: Component {
private var currentContent: (mode: Mode, view: ComponentView<Empty>)?
private var currentMode: Mode = .translate
private var currentMode: Mode = .stylize
override init(frame: CGRect) {
self.currentContentBackground = UIImageView()
@ -106,7 +111,6 @@ final class TextProcessingContentComponent: Component {
self.addSubview(self.currentContentBackground)
self.addSubview(self.currentContentContainer)
self.translateState.resultUpdated = { [weak self] _ in
self?.externalStatesUpdated()
}
@ -163,6 +167,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
@ -202,6 +230,7 @@ final class TextProcessingContentComponent: Component {
}
if self.currentMode != .translate {
self.currentMode = .translate
self.saveState()
self.externalStatesUpdated()
}
if !self.isUpdating {
@ -223,6 +252,7 @@ final class TextProcessingContentComponent: Component {
}
if self.currentMode != .stylize {
self.currentMode = .stylize
self.saveState()
let _ = ApplicationSpecificNotice.incrementAITextProcessingStyleSelection(accountManager: component.context.sharedContext.accountManager).startStandalone()
self.externalStatesUpdated()
}
@ -245,6 +275,7 @@ final class TextProcessingContentComponent: Component {
}
if self.currentMode != .fix {
self.currentMode = .fix
self.saveState()
self.externalStatesUpdated()
}
if !self.isUpdating {
@ -498,6 +529,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 +540,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 +550,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
@ -561,6 +595,122 @@ private final class TextProcessingSheetComponent: Component {
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
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.component = component
@ -587,6 +737,7 @@ private final class TextProcessingSheetComponent: Component {
let performMainAction: () -> Void
var performSendAction: ((TextWithEntities) -> Void)?
var hasLongPressActions = false
let isMainActionEnabled: Bool
//TODO:localize (for AI: don't remove this line)
let actionButtonTitle: String
@ -614,10 +765,11 @@ private final class TextProcessingSheetComponent: Component {
}
} else {
switch component.mode {
case let .edit(completion, send):
case let .edit(_, completion, send, sendContextActions):
actionButtonTitle = "Apply"
performSendAction = send
isMainActionEnabled = !self.contentExternalState.isProcessing
hasLongPressActions = sendContextActions != nil
performMainAction = { [weak self] in
guard let self else {
return
@ -663,6 +815,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()
@ -737,7 +890,13 @@ private final class TextProcessingSheetComponent: Component {
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),
@ -890,11 +1049,31 @@ private final class TextProcessingSheetComponent: Component {
}
public class TextProcessingScreen: ViewControllerComponentContainer {
public final class SendContextActions {
public let peerId: EnginePeer.Id
public let send: (TextWithEntities, ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.SendParameters?) -> Void
public let schedule: (TextWithEntities, ChatSendMessageActionSheetController.SendParameters?) -> Void
public init(peerId: EnginePeer.Id, send: @escaping (TextWithEntities, ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.SendParameters?) -> Void, schedule: @escaping (TextWithEntities, ChatSendMessageActionSheetController.SendParameters?) -> Void) {
self.peerId = peerId
self.send = send
self.schedule = schedule
}
}
public enum Mode {
case edit(completion: (TextWithEntities) -> Void, send: ((TextWithEntities) -> Void)?)
case edit(saveRestoreStateId: EnginePeer.Id?, completion: (TextWithEntities) -> Void, send: ((TextWithEntities) -> Void)?, sendContextActions: SendContextActions?)
case translate(fromLanguage: String?)
}
struct EditState: Codable {
var selectedMode: Int32
init(selectedMode: Int32) {
self.selectedMode = selectedMode
}
}
private let context: AccountContext
public init(
@ -908,8 +1087,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 +1104,7 @@ public class TextProcessingScreen: ViewControllerComponentContainer {
ignoredTranslationLanguages: ignoredTranslationLanguages,
styles: styles,
inputText: inputText,
initialEditState: initialEditState,
shouldDisplayStyleNotice: shouldDisplayStyleNotice,
copyCurrentResult: copyResult,
translateChat: translateChat
@ -1075,19 +1261,22 @@ private final class ActionButtonsComponent: Component {
let actionButtonShowsIncreaseLimit: Bool
let action: (() -> Void)?
let sendAction: (() -> Void)?
let longPressSendAction: ((UIView) -> Void)?
init(
theme: PresentationTheme,
actionTitle: String,
actionButtonShowsIncreaseLimit: Bool,
action: (() -> Void)?,
sendAction: (() -> Void)?
sendAction: (() -> Void)?,
longPressSendAction: ((UIView) -> Void)?
) {
self.theme = theme
self.actionTitle = actionTitle
self.actionButtonShowsIncreaseLimit = actionButtonShowsIncreaseLimit
self.action = action
self.sendAction = sendAction
self.longPressSendAction = longPressSendAction
}
static func ==(lhs: ActionButtonsComponent, rhs: ActionButtonsComponent) -> Bool {
@ -1106,18 +1295,24 @@ private final class ActionButtonsComponent: Component {
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) {
@ -1205,6 +1400,12 @@ private final class ActionButtonsComponent: Component {
return
}
component.sendAction?()
},
longPressAction: component.longPressSendAction == nil ? nil : { [weak self] in
guard let self else {
return
}
component.longPressSendAction?(self.extractedContainerView)
}
)),
environment: {},
@ -1213,13 +1414,16 @@ private final class ActionButtonsComponent: Component {
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.addSubview(sendButtonView)
self.extractedContainerView.contentView.addSubview(sendButtonView)
}
transition.setPosition(view: sendButtonView, position: sendButtonFrame.center)
transition.setBounds(view: sendButtonView, bounds: CGRect(origin: CGPoint(), size: sendButtonFrame.size))
transition.setAlpha(view: sendButtonView, alpha: component.sendAction != nil ? 1.0 : 0.0)
transition.setScale(view: sendButtonView, scale: component.sendAction != nil ? 1.0 : 0.001)
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)
}

View file

@ -146,7 +146,7 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no
)),
hasEntityKeyboard: hasEntityKeyboard,
gesture: gesture,
sourceSendButton: node,
sourceSendButton: node.view,
textInputView: textInputView,
emojiViewProvider: selfController.chatDisplayNode.textInputPanelNode?.emojiViewProvider,
wallpaperBackgroundNode: selfController.chatDisplayNode.backgroundNode,
@ -242,7 +242,7 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no
)),
hasEntityKeyboard: hasEntityKeyboard,
gesture: gesture,
sourceSendButton: node,
sourceSendButton: node.view,
textInputView: textInputView,
emojiViewProvider: selfController.chatDisplayNode.textInputPanelNode?.emojiViewProvider,
wallpaperBackgroundNode: selfController.chatDisplayNode.backgroundNode,

View file

@ -4077,8 +4077,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
mode: .translate(fromLanguage: language),
ignoredTranslationLanguages: translationSettings.ignoredLanguages ?? [],
inputText: TextWithEntities(text: text.string, entities: entities ?? []),
copyResult: canCopy ? { text in
copyResult: canCopy ? { [weak self] text in
guard let self else {
return
}
storeMessageTextInPasteboard(text.text, entities: text.entities)
let infoText = self.presentationData.strings.Conversation_TextCopied
self.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: infoText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in
return true
}), in: .current)
} : nil,
translateChat: { [weak self] toLang in
guard let self else {

View file

@ -66,6 +66,7 @@ import GroupCallHeaderPanelComponent
import PresentationDataUtils
import TextProcessingScreen
import Pasteboard
import UndoUI
final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem {
let itemNode: OverlayMediaItemNode
@ -4462,7 +4463,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
}
let effectiveInputText: NSAttributedString
if effectivePresentationInterfaceState.interfaceState.editMessage != nil && effectivePresentationInterfaceState.interfaceState.postSuggestionState != nil {
if effectivePresentationInterfaceState.interfaceState.editMessage != nil {
effectiveInputText = expandedInputStateAttributedString(effectivePresentationInterfaceState.interfaceState.effectiveInputState.inputText)
} else {
effectiveInputText = expandedInputStateAttributedString(effectivePresentationInterfaceState.interfaceState.composeInputState.inputText)
@ -4493,6 +4494,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
@ -4500,7 +4502,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
controller.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
return state.updatedInterfaceState { interfaceState in
return interfaceState.withUpdatedEffectiveInputState(ChatTextInputState(inputText: chatInputStateStringWithAppliedEntities(text.text, entities: text.entities)))
}
}.updatedInputMode({ _ in return .text })
})
},
send: { [weak self] text in
@ -4513,16 +4515,61 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
}
})
self.sendCurrentMessage()
},
sendContextActions: self.chatLocation.peerId.flatMap { peerId in return TextProcessingScreen.SendContextActions(
peerId: peerId,
send: { [weak self] text, mode, parameters in
guard let self, let controller = self.controller else {
return
}
controller.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
return state.updatedInterfaceState { interfaceState in
return interfaceState.withUpdatedEffectiveInputState(ChatTextInputState(inputText: chatInputStateStringWithAppliedEntities(text.text, entities: text.entities)))
}
})
switch mode {
case .generic:
controller.controllerInteraction?.sendCurrentMessage(false, parameters?.effect.flatMap(ChatSendMessageEffect.init))
case .silently:
controller.controllerInteraction?.sendCurrentMessage(true, parameters?.effect.flatMap(ChatSendMessageEffect.init))
case .whenOnline:
controller.chatDisplayNode.sendCurrentMessage(scheduleTime: scheduleWhenOnlineTimestamp, messageEffect: parameters?.effect.flatMap(ChatSendMessageEffect.init)) { [weak self] in
guard let self, let controller = self.controller else {
return
}
controller.updateChatPresentationInterfaceState(animated: true, interactive: false, saveInterfaceState: controller.presentationInterfaceState.subject != .scheduledMessages, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedPostSuggestionState(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) }
})
controller.openScheduledMessages()
}
}
},
schedule: { [weak self] text, params in
guard let self, let controller = self.controller else {
return
}
controller.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
return state.updatedInterfaceState { interfaceState in
return interfaceState.withUpdatedEffectiveInputState(ChatTextInputState(inputText: chatInputStateStringWithAppliedEntities(text.text, entities: text.entities)))
}
})
controller.controllerInteraction?.scheduleCurrentMessage(params)
}
)
}
),
ignoredTranslationLanguages: translationSettings.ignoredLanguages ?? [],
inputText: TextWithEntities(text: inputText.string, entities: entities),
copyResult: { [weak self] text in
guard let self else {
guard let self, let controller = self.controller else {
return
}
let _ = self
storeMessageTextInPasteboard(text.text, entities: text.entities)
let infoText = controller.presentationData.strings.Conversation_TextCopied
controller.present(UndoOverlayController(presentationData: controller.presentationData, content: .copy(text: infoText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in
return true
}), in: .current)
},
translateChat: nil
))

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

@ -258,7 +258,7 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, context: Accou
if queryItem.name == "url" {
url = value
} else if queryItem.name == "text" {
text = value
text = value.replacingOccurrences(of: "+", with: " ")
} else if queryItem.name == "to" && peerName != "share" {
to = value
}
@ -601,7 +601,7 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, context: Accou
for queryItem in components.queryItems ?? [] {
if let value = queryItem.value {
if queryItem.name == "name" {
title = value
title = value.replacingOccurrences(of: "+", with: " ")
}
}
}

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