mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Merge branches 'master' and 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
583e794a53
30 changed files with 803 additions and 219 deletions
|
|
@ -1304,6 +1304,23 @@ public protocol ChannelMembersSearchController: ViewController {
|
|||
var copyInviteLink: (() -> Void)? { get set }
|
||||
}
|
||||
|
||||
public final class TextProcessingScreenSendContextActions {
|
||||
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 TextProcessingScreenMode {
|
||||
case edit(saveRestoreStateId: EnginePeer.Id?, completion: (TextWithEntities) -> Void, send: ((TextWithEntities) -> Void)?, sendContextActions: TextProcessingScreenSendContextActions?)
|
||||
case translate(fromLanguage: String?)
|
||||
}
|
||||
|
||||
public protocol SharedAccountContext: AnyObject {
|
||||
var sharedContainerPath: String { get }
|
||||
var basePath: String { get }
|
||||
|
|
@ -1541,6 +1558,14 @@ public protocol SharedAccountContext: AnyObject {
|
|||
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 makeTextProcessingScreen(
|
||||
context: AccountContext,
|
||||
mode: TextProcessingScreenMode,
|
||||
ignoredTranslationLanguages: [String],
|
||||
inputText: TextWithEntities,
|
||||
copyResult: ((TextWithEntities) -> Void)?,
|
||||
translateChat: ((String) -> Void)?
|
||||
) async -> ViewController
|
||||
func makeCreateBotScreen(
|
||||
context: AccountContext,
|
||||
parentBot: EnginePeer.Id,
|
||||
|
|
|
|||
|
|
@ -274,6 +274,10 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
|||
private var transparentTextInputBackgroundImage: UIImage?
|
||||
private let actionButtons: AttachmentTextInputActionButtonsNode
|
||||
private let counterTextNode: ImmediateTextNode
|
||||
|
||||
private var aiButton: (button: HighlightTrackingButton, icon: UIImageView)?
|
||||
private var heightDependentAiButtonAlpha: CGFloat = 0.0
|
||||
public var isAIEnabled: Bool = false
|
||||
|
||||
public var opaqueActionButtons: ASDisplayNode {
|
||||
return self.actionButtons
|
||||
|
|
@ -284,6 +288,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
|||
private var validLayout: (CGFloat, CGFloat, CGFloat, UIEdgeInsets, CGFloat, LayoutMetrics, Bool)?
|
||||
|
||||
public var sendMessage: (AttachmentTextInputPanelSendMode, ChatSendMessageActionSheetController.SendParameters?) -> Void = { _, _ in }
|
||||
public var invokeAICompose: (() -> Void)?
|
||||
public var updateHeight: (Bool) -> Void = { _ in }
|
||||
|
||||
private var updatingInputState = false
|
||||
|
|
@ -722,10 +727,16 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
|||
guard self.isUserInteractionEnabled else {
|
||||
return nil
|
||||
}
|
||||
if let aiButton = self.aiButton, aiButton.button.alpha > 0.0 {
|
||||
let aiButtonPoint = self.view.convert(point, to: aiButton.button)
|
||||
if aiButton.button.bounds.contains(aiButtonPoint) {
|
||||
return aiButton.button
|
||||
}
|
||||
}
|
||||
if !self.inputModeView.isHidden, let result = self.inputModeView.hitTest(self.view.convert(point, to: self.inputModeView), with: event) {
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
|
|
@ -1039,7 +1050,62 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
|||
|
||||
transition.updateFrame(layer: self.textInputBackgroundNode.layer, frame: CGRect(x: leftInset + textFieldInsets.left, y: textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right + composeButtonsOffset - textBackgroundInset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom))
|
||||
transition.updateFrame(layer: self.textInputBackgroundImageNode.layer, frame: CGRect(x: 0.0, y: 0.0, width: baseWidth - textFieldInsets.left - textFieldInsets.right + composeButtonsOffset - textBackgroundInset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom))
|
||||
|
||||
|
||||
if self.isAIEnabled {
|
||||
let aiButton: (button: HighlightTrackingButton, icon: UIImageView)
|
||||
if let current = self.aiButton {
|
||||
aiButton = current
|
||||
} else {
|
||||
aiButton = (HighlightTrackingButton(), UIImageView())
|
||||
self.aiButton = aiButton
|
||||
aiButton.button.highligthedChanged = { [weak self] highlighted in
|
||||
guard let self, let aiButton = self.aiButton else {
|
||||
return
|
||||
}
|
||||
if highlighted {
|
||||
aiButton.icon.alpha = 0.6
|
||||
} else {
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||
transition.updateAlpha(layer: aiButton.icon.layer, alpha: 1.0)
|
||||
}
|
||||
}
|
||||
aiButton.button.addTarget(self, action: #selector(self.aiButtonPressed), for: .touchUpInside)
|
||||
aiButton.button.addSubview(aiButton.icon)
|
||||
aiButton.icon.image = UIImage(bundleImageName: "Chat/Input/Text/InputAIIcon")?.withRenderingMode(.alwaysTemplate)
|
||||
self.textInputBackgroundNode.view.addSubview(aiButton.icon)
|
||||
self.textInputBackgroundNode.view.addSubview(aiButton.button)
|
||||
}
|
||||
|
||||
if let presentationInterfaceState = self.presentationInterfaceState {
|
||||
aiButton.icon.tintColor = presentationInterfaceState.theme.chat.inputPanel.inputControlColor
|
||||
}
|
||||
if let image = aiButton.icon.image {
|
||||
let aiButtonSize = CGSize(width: 40.0, height: 40.0)
|
||||
let aiButtonFrame = CGRect(origin: CGPoint(x: baseWidth - rightInset - aiButtonSize.width - 12.0, y: -1.0), size: aiButtonSize)
|
||||
transition.updateFrame(view: aiButton.button, frame: aiButtonFrame)
|
||||
transition.updateFrame(view: aiButton.icon, frame: image.size.centered(in: aiButtonFrame))
|
||||
}
|
||||
|
||||
var aiButtonAlpha: CGFloat = textInputHeight >= 70.0 ? 1.0 : 0.0
|
||||
self.heightDependentAiButtonAlpha = aiButtonAlpha
|
||||
if !inputHasText {
|
||||
aiButtonAlpha = 0.0
|
||||
}
|
||||
ComponentTransition(transition).setAlpha(view: aiButton.button, alpha: aiButtonAlpha)
|
||||
ComponentTransition(transition).setAlpha(view: aiButton.icon, alpha: aiButtonAlpha)
|
||||
} else if let aiButton = self.aiButton {
|
||||
self.aiButton = nil
|
||||
let aiButtonView = aiButton.button
|
||||
let aiButtonIconView = aiButton.icon
|
||||
transition.updateAlpha(layer: aiButton.button.layer, alpha: 0.0, completion: { [weak aiButtonView] _ in
|
||||
aiButtonView?.removeFromSuperview()
|
||||
})
|
||||
transition.updateAlpha(layer: aiButton.icon.layer, alpha: 0.0, completion: { [weak aiButtonIconView] _ in
|
||||
aiButtonIconView?.removeFromSuperview()
|
||||
})
|
||||
self.heightDependentAiButtonAlpha = 0.0
|
||||
}
|
||||
|
||||
var textInputViewRealInsets = UIEdgeInsets()
|
||||
if let presentationInterfaceState = self.presentationInterfaceState {
|
||||
textInputViewRealInsets = calculateTextFieldRealInsets(presentationInterfaceState, glass: self.glass)
|
||||
|
|
@ -1114,11 +1180,22 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
|||
self.updateTextNodeText(animated: true)
|
||||
|
||||
self.updateCounterTextNode(transition: .immediate)
|
||||
|
||||
|
||||
if let aiButton = self.aiButton {
|
||||
var aiButtonAlpha: CGFloat = self.heightDependentAiButtonAlpha
|
||||
if let attributedText = textInputNode.attributedText, attributedText.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
aiButtonAlpha = 0.0
|
||||
} else if textInputNode.attributedText == nil {
|
||||
aiButtonAlpha = 0.0
|
||||
}
|
||||
ComponentTransition(.immediate).setAlpha(view: aiButton.button, alpha: aiButtonAlpha)
|
||||
ComponentTransition(.immediate).setAlpha(view: aiButton.icon, alpha: aiButtonAlpha)
|
||||
}
|
||||
|
||||
self.skipUpdate = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc public func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
|
||||
self.chatInputTextNodeDidUpdateText()
|
||||
}
|
||||
|
|
@ -1908,6 +1985,10 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
|||
public func chatInputTextNodeBackspaceWhileEmpty() {
|
||||
}
|
||||
|
||||
@objc private func aiButtonPressed() {
|
||||
self.invokeAICompose?()
|
||||
}
|
||||
|
||||
@objc func sendButtonPressed() {
|
||||
let inputTextMaxLength: Int32?
|
||||
if let maxCaptionLength = self.maxCaptionLength {
|
||||
|
|
|
|||
|
|
@ -748,6 +748,57 @@ public class AttachmentController: ViewController, MinimizableController {
|
|||
}
|
||||
}
|
||||
}
|
||||
self.panel.invokeAICompose = { [weak self] in
|
||||
Task { @MainActor in
|
||||
guard let self, let controller = self.controller, let mediaPickerContext = self.mediaPickerContext else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let caption = await mediaPickerContext.caption.get() else {
|
||||
return
|
||||
}
|
||||
if caption.length == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
let sharedDataEntries = await controller.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
|
||||
}
|
||||
|
||||
let textProcessingScreen = await controller.context.sharedContext.makeTextProcessingScreen(
|
||||
context: controller.context,
|
||||
mode: .edit(
|
||||
saveRestoreStateId: nil,
|
||||
completion: { [weak self] text in
|
||||
//TODO:localize
|
||||
guard let self, let mediaPickerContext = self.mediaPickerContext else {
|
||||
return
|
||||
}
|
||||
self.panel.updateCaption(NSAttributedString(string: text.text))
|
||||
mediaPickerContext.setCaption(NSAttributedString(string: text.text))
|
||||
},
|
||||
send: { [weak self] text in
|
||||
//TODO:localize
|
||||
guard let self, let mediaPickerContext = self.mediaPickerContext else {
|
||||
return
|
||||
}
|
||||
mediaPickerContext.setCaption(NSAttributedString(string: text.text))
|
||||
mediaPickerContext.send(mode: .generic, attachmentMode: .media, parameters: nil)
|
||||
},
|
||||
sendContextActions: nil
|
||||
),
|
||||
ignoredTranslationLanguages: translationSettings.ignoredLanguages ?? [],
|
||||
inputText: TextWithEntities(text: caption.string, entities: []),
|
||||
copyResult: nil,
|
||||
translateChat: nil
|
||||
)
|
||||
self.controller?.push(textProcessingScreen)
|
||||
}
|
||||
}
|
||||
|
||||
self.panel.onMainButtonPressed = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
|
|
|
|||
|
|
@ -1065,6 +1065,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog
|
|||
var beganTextEditing: () -> Void = {}
|
||||
var textUpdated: (NSAttributedString) -> Void = { _ in }
|
||||
var sendMessagePressed: (AttachmentTextInputPanelSendMode, ChatSendMessageActionSheetController.SendParameters?) -> Void = { _, _ in }
|
||||
var invokeAICompose: () -> Void = {}
|
||||
var requestLayout: () -> Void = {}
|
||||
var present: (ViewController) -> Void = { _ in }
|
||||
var presentInGlobalOverlay: (ViewController) -> Void = { _ in }
|
||||
|
|
@ -2227,12 +2228,18 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog
|
|||
strongSelf.present(c)
|
||||
}
|
||||
}, makeEntityInputView: self.makeEntityInputView)
|
||||
textInputPanelNode.isAIEnabled = true
|
||||
textInputPanelNode.interfaceInteraction = self.interfaceInteraction
|
||||
textInputPanelNode.sendMessage = { [weak self] mode, messageEffect in
|
||||
if let strongSelf = self {
|
||||
strongSelf.sendMessagePressed(mode, messageEffect)
|
||||
}
|
||||
}
|
||||
textInputPanelNode.invokeAICompose = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.invokeAICompose()
|
||||
}
|
||||
}
|
||||
textInputPanelNode.focusUpdated = { [weak self] focus in
|
||||
if let strongSelf = self, focus {
|
||||
strongSelf.beganTextEditing()
|
||||
|
|
|
|||
|
|
@ -319,6 +319,7 @@ public class CheckLayer: CALayer {
|
|||
super.init()
|
||||
|
||||
self.isOpaque = false
|
||||
self.rasterizationScale = UIScreenScale
|
||||
}
|
||||
|
||||
public override init(layer: Any) {
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ public final class MultilineTextWithEntitiesComponent: Component {
|
|||
|
||||
public final class View: UIView {
|
||||
var spoilerTextNode: ImmediateTextNodeWithEntities?
|
||||
let textNode: ImmediateTextNodeWithEntities
|
||||
public let textNode: ImmediateTextNodeWithEntities
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
self.textNode = ImmediateTextNodeWithEntities()
|
||||
|
|
|
|||
|
|
@ -466,7 +466,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
|
|||
topOffsetFraction = 1.0
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
#if DEBUG && false
|
||||
if "".isEmpty {
|
||||
topOffsetFraction = 1.0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,23 @@ import UIKit
|
|||
import AsyncDisplayKit
|
||||
|
||||
public final class ContextMenuControllerPresentationArguments {
|
||||
public let sourceNodeAndRect: () -> (ASDisplayNode, CGRect, ASDisplayNode, CGRect)?
|
||||
public let sourceViewAndRect: () -> (UIView, CGRect, UIView, CGRect)?
|
||||
public let bounce: Bool
|
||||
|
||||
public init(sourceNodeAndRect: @escaping () -> (ASDisplayNode, CGRect, ASDisplayNode, CGRect)?, bounce: Bool = true) {
|
||||
self.sourceNodeAndRect = sourceNodeAndRect
|
||||
public init(sourceViewAndRect: @escaping () -> (UIView, CGRect, UIView, CGRect)?, bounce: Bool = true) {
|
||||
self.sourceViewAndRect = sourceViewAndRect
|
||||
self.bounce = bounce
|
||||
}
|
||||
|
||||
public convenience init(sourceNodeAndRect: @escaping () -> (ASDisplayNode, CGRect, ASDisplayNode, CGRect)?, bounce: Bool = true) {
|
||||
self.init(sourceViewAndRect: {
|
||||
if let (view1, rect1, view2, rect2) = sourceNodeAndRect() {
|
||||
return (view1.view, rect1, view2.view, rect2)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}, bounce: bounce)
|
||||
}
|
||||
}
|
||||
|
||||
public protocol ContextMenuController: ViewController, StandalonePresentableController {
|
||||
|
|
|
|||
|
|
@ -459,18 +459,18 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
|
|||
)
|
||||
self.textNode.visibility = true
|
||||
|
||||
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: defaultDarkPresentationTheme.list.itemAccentColor.withMultipliedAlpha(0.5), knob: defaultDarkPresentationTheme.list.itemAccentColor, isDark: true), strings: presentationData.strings, textNode: self.textNode, updateIsActive: { [weak self] value in
|
||||
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: defaultDarkPresentationTheme.list.itemAccentColor.withMultipliedAlpha(0.5), knob: defaultDarkPresentationTheme.list.itemAccentColor, isDark: true), strings: presentationData.strings, textNodeOrView: .node(self.textNode), updateIsActive: { [weak self] value in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = self
|
||||
}, present: { c, a in
|
||||
present(c, a)
|
||||
}, rootNode: { [weak self] in
|
||||
}, rootView: { [weak self] in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
return self.controllerInteraction?.controller()?.displayNode
|
||||
return self.controllerInteraction?.controller()?.displayNode.view
|
||||
}, externalKnobSurface: self.textSelectionKnobSurface, performAction: { [weak self] text, action in
|
||||
guard let self else {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -238,6 +238,11 @@ public final class TelegramMediaPoll: Media, Equatable {
|
|||
peerIds.append(contentsOf: voter.recentVoters)
|
||||
}
|
||||
}
|
||||
for option in options {
|
||||
if let addedBy = option.addedBy {
|
||||
peerIds.append(addedBy)
|
||||
}
|
||||
}
|
||||
return peerIds
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -69,6 +69,8 @@ func _internal_addressNameAvailability(account: Account, domain: AddressNameDoma
|
|||
|> `catch` { error -> Signal<AddressNameAvailability, NoError> in
|
||||
if error.errorDescription == "USERNAME_PURCHASE_AVAILABLE" {
|
||||
return .single(.purchaseAvailable)
|
||||
} else if error.errorDescription == "USERNAME_OCCUPIED" {
|
||||
return .single(.taken)
|
||||
} else {
|
||||
return .single(.invalid)
|
||||
}
|
||||
|
|
@ -87,6 +89,8 @@ func _internal_addressNameAvailability(account: Account, domain: AddressNameDoma
|
|||
|> `catch` { error -> Signal<AddressNameAvailability, NoError> in
|
||||
if error.errorDescription == "USERNAME_PURCHASE_AVAILABLE" {
|
||||
return .single(.purchaseAvailable)
|
||||
} else if error.errorDescription == "USERNAME_OCCUPIED" {
|
||||
return .single(.taken)
|
||||
} else {
|
||||
return .single(.invalid)
|
||||
}
|
||||
|
|
@ -104,6 +108,8 @@ func _internal_addressNameAvailability(account: Account, domain: AddressNameDoma
|
|||
|> `catch` { error -> Signal<AddressNameAvailability, NoError> in
|
||||
if error.errorDescription == "USERNAME_PURCHASE_AVAILABLE" {
|
||||
return .single(.purchaseAvailable)
|
||||
} else if error.errorDescription == "USERNAME_OCCUPIED" {
|
||||
return .single(.taken)
|
||||
} else {
|
||||
return .single(.invalid)
|
||||
}
|
||||
|
|
@ -124,6 +130,8 @@ func _internal_addressNameAvailability(account: Account, domain: AddressNameDoma
|
|||
|> `catch` { error -> Signal<AddressNameAvailability, NoError> in
|
||||
if error.errorDescription == "USERNAME_PURCHASE_AVAILABLE" {
|
||||
return .single(.purchaseAvailable)
|
||||
} else if error.errorDescription == "USERNAME_OCCUPIED" {
|
||||
return .single(.taken)
|
||||
} else {
|
||||
return .single(.invalid)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -156,12 +156,12 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode
|
|||
let selectionColor: UIColor = item.presentationData.theme.theme.chat.message.incoming.textSelectionColor
|
||||
let knobColor: UIColor = item.presentationData.theme.theme.chat.message.incoming.textSelectionKnobColor
|
||||
|
||||
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: knobColor, isDark: item.presentationData.theme.theme.overallDarkAppearance), strings: item.presentationData.strings, textNode: self.textNode, updateIsActive: { [weak self] value in
|
||||
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: knobColor, isDark: item.presentationData.theme.theme.overallDarkAppearance), strings: item.presentationData.strings, textNodeOrView: .node(self.textNode), updateIsActive: { [weak self] value in
|
||||
self?.updateIsTextSelectionActive?(value)
|
||||
}, present: { [weak self] c, a in
|
||||
self?.item?.controllerInteraction.presentGlobalOverlayController(c, a)
|
||||
}, rootNode: { [weak rootNode] in
|
||||
return rootNode
|
||||
}, rootView: { [weak rootNode] in
|
||||
return rootNode?.view
|
||||
}, performAction: { [weak self] text, action in
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -2049,12 +2049,12 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||
knobColor = item.presentationData.theme.theme.chat.message.outgoing.textSelectionKnobColor
|
||||
}
|
||||
|
||||
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: knobColor, isDark: item.presentationData.theme.theme.overallDarkAppearance), strings: item.presentationData.strings, textNode: self.textNode, updateIsActive: { [weak self] value in
|
||||
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: knobColor, isDark: item.presentationData.theme.theme.overallDarkAppearance), strings: item.presentationData.strings, textNodeOrView: .node(self.textNode), updateIsActive: { [weak self] value in
|
||||
self?.updateIsTextSelectionActive?(value)
|
||||
}, present: { [weak self] c, a in
|
||||
self?.arguments?.controllerInteraction.presentGlobalOverlayController(c, a)
|
||||
}, rootNode: { [weak rootNode] in
|
||||
return rootNode
|
||||
}, rootView: { [weak rootNode] in
|
||||
return rootNode?.view
|
||||
}, performAction: { [weak self] text, action in
|
||||
guard let strongSelf = self, let item = strongSelf.arguments else {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1622,7 +1622,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||
knobColor = item.presentationData.theme.theme.chat.message.outgoing.textSelectionKnobColor
|
||||
}
|
||||
|
||||
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: knobColor, isDark: item.presentationData.theme.theme.overallDarkAppearance), strings: item.presentationData.strings, textNode: self.textNode.textNode, updateIsActive: { [weak self] value in
|
||||
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: knobColor, isDark: item.presentationData.theme.theme.overallDarkAppearance), strings: item.presentationData.strings, textNodeOrView: .node(self.textNode.textNode), updateIsActive: { [weak self] value in
|
||||
self?.updateIsTextSelectionActive?(value)
|
||||
}, present: { [weak self] c, a in
|
||||
guard let self, let item = self.item else {
|
||||
|
|
@ -1634,8 +1634,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||
} else {
|
||||
item.controllerInteraction.presentGlobalOverlayController(c, a)
|
||||
}
|
||||
}, rootNode: { [weak rootNode] in
|
||||
return rootNode
|
||||
}, rootView: { [weak rootNode] in
|
||||
return rootNode?.view
|
||||
}, performAction: { [weak self] text, action in
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -247,6 +247,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
|||
private let counterTextNode: ImmediateTextNode
|
||||
|
||||
private var aiButton: (button: HighlightTrackingButton, icon: UIImageView)?
|
||||
private var heightDependentAiButtonAlpha: CGFloat = 0.0
|
||||
|
||||
public let menuButton: HighlightTrackingButtonNode
|
||||
private let menuButtonBackgroundView: GlassBackgroundView
|
||||
|
|
@ -3558,9 +3559,17 @@ 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 >= 70.0 ? 1.0 : 0.0
|
||||
transition.updateAlpha(layer: aiButton.button.layer, alpha: aiButtonAlpha)
|
||||
transition.updateAlpha(layer: aiButton.icon.layer, alpha: aiButtonAlpha)
|
||||
var aiButtonAlpha: CGFloat = actualTextFieldFrame.height >= 70.0 ? 1.0 : 0.0
|
||||
self.heightDependentAiButtonAlpha = aiButtonAlpha
|
||||
var inputHasText = false
|
||||
if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, !attributedText.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
inputHasText = true
|
||||
}
|
||||
if !inputHasText {
|
||||
aiButtonAlpha = 0.0
|
||||
}
|
||||
ComponentTransition(transition).setAlpha(view: aiButton.button, alpha: aiButtonAlpha)
|
||||
ComponentTransition(transition).setAlpha(view: aiButton.icon, alpha: aiButtonAlpha)
|
||||
} else if let aiButton = self.aiButton {
|
||||
self.aiButton = nil
|
||||
let aiButtonView = aiButton.button
|
||||
|
|
@ -3571,6 +3580,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
|||
transition.updateAlpha(layer: aiButton.icon.layer, alpha: 0.0, completion: { [weak aiButtonIconView] _ in
|
||||
aiButtonIconView?.removeFromSuperview()
|
||||
})
|
||||
self.heightDependentAiButtonAlpha = 0.0
|
||||
}
|
||||
|
||||
let containerFrame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: contentHeight + 64.0))
|
||||
|
|
@ -4385,6 +4395,16 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
|||
}
|
||||
}
|
||||
|
||||
if let aiButton = self.aiButton {
|
||||
let transition: ContainedViewLayoutTransition = .immediate
|
||||
var aiButtonAlpha: CGFloat = self.heightDependentAiButtonAlpha
|
||||
if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
aiButtonAlpha = 0.0
|
||||
}
|
||||
ComponentTransition(transition).setAlpha(view: aiButton.button, alpha: aiButtonAlpha)
|
||||
ComponentTransition(transition).setAlpha(view: aiButton.icon, alpha: aiButtonAlpha)
|
||||
}
|
||||
|
||||
self.updateTextHeight(animated: animated)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -438,10 +438,10 @@ final class InnerTextSelectionTipContainerNode: ASDisplayNode {
|
|||
|
||||
super.init()
|
||||
|
||||
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: presentationData.theme.contextMenu.primaryColor.withAlphaComponent(0.15), knob: presentationData.theme.contextMenu.primaryColor, knobDiameter: 8.0, isDark: presentationData.theme.overallDarkAppearance), strings: presentationData.strings, textNode: self.textNode.textNode, updateIsActive: { _ in
|
||||
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: presentationData.theme.contextMenu.primaryColor.withAlphaComponent(0.15), knob: presentationData.theme.contextMenu.primaryColor, knobDiameter: 8.0, isDark: presentationData.theme.overallDarkAppearance), strings: presentationData.strings, textNodeOrView: .node(self.textNode.textNode), updateIsActive: { _ in
|
||||
}, present: { _, _ in
|
||||
}, rootNode: { [weak self] in
|
||||
return self
|
||||
}, rootView: { [weak self] in
|
||||
return self?.view
|
||||
}, performAction: { _, _ in
|
||||
})
|
||||
self.textSelectionNode = textSelectionNode
|
||||
|
|
|
|||
|
|
@ -85,13 +85,13 @@ public final class ContextMenuControllerImpl: ViewController, KeyShortcutRespond
|
|||
} else {
|
||||
self.layout = layout
|
||||
|
||||
if let presentationArguments = self.presentationArguments as? ContextMenuControllerPresentationArguments, let (sourceNode, sourceRect, containerNode, containerRect) = presentationArguments.sourceNodeAndRect() {
|
||||
if let presentationArguments = self.presentationArguments as? ContextMenuControllerPresentationArguments, let (sourceView, sourceRect, containerView, containerRect) = presentationArguments.sourceViewAndRect() {
|
||||
if self.skipCoordnateConversion {
|
||||
self.contextMenuNode.sourceRect = sourceRect
|
||||
self.contextMenuNode.containerRect = containerRect
|
||||
} else {
|
||||
self.contextMenuNode.sourceRect = sourceNode.view.convert(sourceRect, to: nil)
|
||||
self.contextMenuNode.containerRect = containerNode.view.convert(containerRect, to: nil)
|
||||
self.contextMenuNode.sourceRect = sourceView.convert(sourceRect, to: nil)
|
||||
self.contextMenuNode.containerRect = containerView.convert(containerRect, to: nil)
|
||||
}
|
||||
} else {
|
||||
self.contextMenuNode.sourceRect = nil
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ final class CreateBotContentComponent: Component {
|
|||
final class ExternalState {
|
||||
var name: String = ""
|
||||
var username: String = ""
|
||||
var usernameIsChecked: Bool = false
|
||||
|
||||
init() {
|
||||
}
|
||||
|
|
@ -54,6 +55,13 @@ final class CreateBotContentComponent: Component {
|
|||
static func ==(lhs: CreateBotContentComponent, rhs: CreateBotContentComponent) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
private enum UsernameCheckingStatus {
|
||||
case checking
|
||||
case valid
|
||||
case invalid
|
||||
case taken
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private var component: CreateBotContentComponent?
|
||||
|
|
@ -67,7 +75,19 @@ final class CreateBotContentComponent: Component {
|
|||
private let usernameSection = ComponentView<Empty>()
|
||||
|
||||
private let usernameInputState = ListMultilineTextFieldItemComponent.ExternalState()
|
||||
private let usernameInputTag = ListMultilineTextFieldItemComponent.Tag()
|
||||
private let nameInputState = ListMultilineTextFieldItemComponent.ExternalState()
|
||||
private let nameInputTag = ListMultilineTextFieldItemComponent.Tag()
|
||||
|
||||
private var usernameCheckingStatus: (username: String, status: UsernameCheckingStatus)? {
|
||||
didSet {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.externalState.usernameIsChecked = self.usernameCheckingStatus?.status == .valid
|
||||
}
|
||||
}
|
||||
private var usernameCheckingDisposable: Disposable?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
|
@ -77,6 +97,11 @@ final class CreateBotContentComponent: Component {
|
|||
return
|
||||
}
|
||||
component.externalState.username = self.usernameInputState.text.string
|
||||
|
||||
self.inputUsernameUpdated()
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
}
|
||||
self.nameInputState.updated = { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
|
|
@ -89,6 +114,47 @@ final class CreateBotContentComponent: Component {
|
|||
required init?(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.usernameCheckingDisposable?.dispose()
|
||||
}
|
||||
|
||||
private func inputUsernameUpdated() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
let username = self.usernameInputState.text.string.lowercased() + "bot"
|
||||
if let usernameCheckingStatus = self.usernameCheckingStatus, usernameCheckingStatus.username == username {
|
||||
return
|
||||
}
|
||||
self.usernameCheckingDisposable?.dispose()
|
||||
self.usernameCheckingDisposable = nil
|
||||
|
||||
guard case .success = CreateBotSheetComponent.View.validatedUsername(inputUsername: username) else {
|
||||
self.usernameCheckingStatus = (username, .invalid)
|
||||
return
|
||||
}
|
||||
|
||||
self.usernameCheckingStatus = (username, .checking)
|
||||
self.usernameCheckingDisposable = (component.context.engine.peers.addressNameAvailability(domain: .bot(component.parentPeer.id), name: username) |> deliverOnMainQueue).startStrict(next: { [weak self] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
switch result {
|
||||
case .available:
|
||||
self.usernameCheckingStatus = (username, .valid)
|
||||
case .invalid:
|
||||
self.usernameCheckingStatus = (username, .invalid)
|
||||
case .purchaseAvailable:
|
||||
self.usernameCheckingStatus = (username, .invalid)
|
||||
case .taken:
|
||||
self.usernameCheckingStatus = (username, .taken)
|
||||
}
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func update(component: CreateBotContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
|
|
@ -210,10 +276,24 @@ final class CreateBotContentComponent: Component {
|
|||
autocapitalizationType: .words,
|
||||
autocorrectionType: .no,
|
||||
characterLimit: 64,
|
||||
rightAccessory: ListMultilineTextFieldItemComponent.RightAccessory(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(EditLabelComponent(theme: environment.theme, strings: environment.strings))), insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 0.0)),
|
||||
rightAccessory: ListMultilineTextFieldItemComponent.RightAccessory(component: AnyComponentWithIdentity(
|
||||
id: 0,
|
||||
component: AnyComponent(EditLabelComponent(
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
action: { [weak self] in
|
||||
guard let self, let itemView = self.nameSection.findTaggedView(tag: self.nameInputTag) as? ListMultilineTextFieldItemComponent.View else {
|
||||
return
|
||||
}
|
||||
itemView.activateInput()
|
||||
}
|
||||
))),
|
||||
insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 0.0)
|
||||
),
|
||||
emptyLineHandling: .notAllowed,
|
||||
updated: { _ in },
|
||||
textUpdateTransition: .immediate
|
||||
textUpdateTransition: .immediate,
|
||||
tag: self.nameInputTag
|
||||
)))
|
||||
]
|
||||
)),
|
||||
|
|
@ -231,14 +311,58 @@ final class CreateBotContentComponent: Component {
|
|||
contentHeight += nameSectionSize.height + 22.0
|
||||
|
||||
var initialUsername = ""
|
||||
var botSuffix = "bot"
|
||||
if let value = component.initialUsername {
|
||||
if value.hasSuffix("bot") {
|
||||
if value.lowercased().hasSuffix("bot") {
|
||||
botSuffix = String(value[value.index(value.endIndex, offsetBy: -3)...])
|
||||
initialUsername = String(value[value.startIndex ..< value.index(value.endIndex, offsetBy: -3)])
|
||||
} else {
|
||||
initialUsername = value
|
||||
}
|
||||
}
|
||||
|
||||
let usernameFooterString: NSAttributedString
|
||||
switch CreateBotSheetComponent.View.validatedUsername(inputUsername: "\(self.usernameInputState.text.string)" + botSuffix) {
|
||||
case let .success(value):
|
||||
switch self.usernameCheckingStatus?.status ?? .valid {
|
||||
case .checking:
|
||||
usernameFooterString = NSAttributedString(
|
||||
string: "Checking...",
|
||||
font: Font.regular(13.0),
|
||||
textColor: environment.theme.list.freeTextColor
|
||||
)
|
||||
case .invalid:
|
||||
let errorText = "You can only use **a-z**, **0-9** and underscores."
|
||||
usernameFooterString = parseMarkdownIntoAttributedString(errorText, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemDestructiveColor), bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.itemDestructiveColor), link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemDestructiveColor), linkAttribute: { contents in
|
||||
return ("URL", contents)
|
||||
}))
|
||||
case .taken:
|
||||
let errorText = "This username is already taken."
|
||||
usernameFooterString = parseMarkdownIntoAttributedString(errorText, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemDestructiveColor), bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.itemDestructiveColor), link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemDestructiveColor), linkAttribute: { contents in
|
||||
return ("URL", contents)
|
||||
}))
|
||||
case .valid:
|
||||
usernameFooterString = NSAttributedString(
|
||||
string: "Link: t.me/\(value)",
|
||||
font: Font.regular(13.0),
|
||||
textColor: environment.theme.list.freeTextColor
|
||||
)
|
||||
}
|
||||
case let .failure(error):
|
||||
let errorText: String
|
||||
switch error {
|
||||
case .insufficientLength:
|
||||
errorText = "A username must have at least 5 characters."
|
||||
case .startsWithNumber:
|
||||
errorText = "A username can't start with a number"
|
||||
case .unsupportedCharacters:
|
||||
errorText = "You can only use **a-z**, **0-9** and underscores."
|
||||
}
|
||||
usernameFooterString = parseMarkdownIntoAttributedString(errorText, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemDestructiveColor), bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.itemDestructiveColor), link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemDestructiveColor), linkAttribute: { contents in
|
||||
return ("URL", contents)
|
||||
}))
|
||||
}
|
||||
|
||||
let usernameSectionSize = self.usernameSection.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ListSectionComponent(
|
||||
|
|
@ -252,7 +376,10 @@ final class CreateBotContentComponent: Component {
|
|||
)),
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
footer: nil,
|
||||
footer: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(usernameFooterString),
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
items: [
|
||||
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListMultilineTextFieldItemComponent(
|
||||
externalState: usernameInputState,
|
||||
|
|
@ -268,11 +395,25 @@ final class CreateBotContentComponent: Component {
|
|||
keyboardType: .asciiCapable,
|
||||
characterLimit: 32,
|
||||
prefix: NSAttributedString(string: "@", font: Font.regular(17.0), textColor: environment.theme.list.itemPrimaryTextColor),
|
||||
suffix: NSAttributedString(string: "bot", font: Font.regular(17.0), textColor: environment.theme.list.itemSecondaryTextColor),
|
||||
rightAccessory: ListMultilineTextFieldItemComponent.RightAccessory(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(EditLabelComponent(theme: environment.theme, strings: environment.strings))), insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 0.0)),
|
||||
suffix: NSAttributedString(string: botSuffix, font: Font.regular(17.0), textColor: environment.theme.list.itemSecondaryTextColor),
|
||||
rightAccessory: ListMultilineTextFieldItemComponent.RightAccessory(component: AnyComponentWithIdentity(
|
||||
id: 0,
|
||||
component: AnyComponent(EditLabelComponent(
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
action: { [weak self] in
|
||||
guard let self, let itemView = self.usernameSection.findTaggedView(tag: self.usernameInputTag) as? ListMultilineTextFieldItemComponent.View else {
|
||||
return
|
||||
}
|
||||
itemView.activateInput()
|
||||
}
|
||||
))),
|
||||
insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 0.0)
|
||||
),
|
||||
emptyLineHandling: .notAllowed,
|
||||
updated: { _ in },
|
||||
textUpdateTransition: .immediate
|
||||
textUpdateTransition: .immediate,
|
||||
tag: self.usernameInputTag,
|
||||
)))
|
||||
]
|
||||
)),
|
||||
|
|
@ -294,6 +435,7 @@ final class CreateBotContentComponent: Component {
|
|||
|
||||
component.externalState.name = self.nameInputState.text.string
|
||||
component.externalState.username = self.usernameInputState.text.string
|
||||
component.externalState.usernameIsChecked = self.usernameCheckingStatus?.status == .valid
|
||||
|
||||
return CGSize(width: availableSize.width, height: contentHeight)
|
||||
}
|
||||
|
|
@ -316,7 +458,7 @@ private final class CreateBotSheetComponent: Component {
|
|||
let initialUsername: String?
|
||||
let initialTitle: String?
|
||||
let openAutomatically: Bool
|
||||
let completion: (EnginePeer.Id) -> Void
|
||||
let completion: (EnginePeer.Id?) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
|
|
@ -324,7 +466,7 @@ private final class CreateBotSheetComponent: Component {
|
|||
initialUsername: String?,
|
||||
initialTitle: String?,
|
||||
openAutomatically: Bool,
|
||||
completion: @escaping (EnginePeer.Id) -> Void
|
||||
completion: @escaping (EnginePeer.Id?) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.parentPeer = parentPeer
|
||||
|
|
@ -349,6 +491,7 @@ private final class CreateBotSheetComponent: Component {
|
|||
|
||||
private var isCreating: Bool = false
|
||||
private var actionDisposable: Disposable?
|
||||
private var isCompleted: Bool = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
|
@ -362,28 +505,94 @@ private final class CreateBotSheetComponent: Component {
|
|||
self.actionDisposable?.dispose()
|
||||
}
|
||||
|
||||
private func validatedParams() -> (name: String, username: String)? {
|
||||
if self.contentExternalState.name.isEmpty {
|
||||
return nil
|
||||
func attemptNavigation(complete: @escaping () -> Void) -> Bool {
|
||||
guard let component = self.component else {
|
||||
return true
|
||||
}
|
||||
if self.contentExternalState.username.isEmpty {
|
||||
return nil
|
||||
if self.isCreating {
|
||||
return false
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let _ = presentationData
|
||||
let alertController = textAlertController(
|
||||
context: component.context,
|
||||
title: "Unsaved Changes",
|
||||
text: "You have not finished creating a bot.",
|
||||
actions: [
|
||||
TextAlertAction(type: .genericAction, title: "Cancel", action: {
|
||||
}),
|
||||
TextAlertAction(type: .destructiveAction, title: "Discard", action: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
if !self.isCompleted {
|
||||
self.isCompleted = true
|
||||
component.completion(nil)
|
||||
}
|
||||
let controller = self.environment?.controller
|
||||
self.animateOut.invoke(Action { _ in
|
||||
if let controller = controller?() {
|
||||
controller.dismiss(completion: nil)
|
||||
}
|
||||
})
|
||||
})
|
||||
]
|
||||
)
|
||||
self.environment?.controller()?.present(alertController, in: .window(.root))
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
enum UsernameValidationError: Error {
|
||||
case insufficientLength
|
||||
case unsupportedCharacters
|
||||
case startsWithNumber
|
||||
}
|
||||
|
||||
static func validatedUsername(inputUsername: String) -> Result<String, UsernameValidationError> {
|
||||
var isUsernameValid = true
|
||||
var usernameCharacters = CharacterSet(charactersIn: "a".unicodeScalars.first! ... "z".unicodeScalars.first!)
|
||||
usernameCharacters.insert(charactersIn: "A".unicodeScalars.first! ... "Z".unicodeScalars.first!)
|
||||
usernameCharacters.insert(charactersIn: "0".unicodeScalars.first! ... "9".unicodeScalars.first!)
|
||||
usernameCharacters.insert("_")
|
||||
for c in self.contentExternalState.username.unicodeScalars {
|
||||
for c in inputUsername.unicodeScalars {
|
||||
if !usernameCharacters.contains(c) {
|
||||
isUsernameValid = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isUsernameValid {
|
||||
return .failure(.unsupportedCharacters)
|
||||
}
|
||||
if let first = inputUsername.unicodeScalars.first {
|
||||
if CharacterSet.decimalDigits.contains(first) {
|
||||
return .failure(.startsWithNumber)
|
||||
}
|
||||
}
|
||||
if inputUsername.count < 5 {
|
||||
return .failure(.insufficientLength)
|
||||
}
|
||||
return .success(inputUsername)
|
||||
}
|
||||
|
||||
static func validatedParams(inputName: String, inputUsername: String) -> (name: String, username: String)? {
|
||||
if inputName.isEmpty {
|
||||
return nil
|
||||
}
|
||||
return (self.contentExternalState.name, self.contentExternalState.username)
|
||||
guard case let .success(username) = validatedUsername(inputUsername: inputUsername) else {
|
||||
return nil
|
||||
}
|
||||
return (inputName, username)
|
||||
}
|
||||
|
||||
private func validatedParams() -> (name: String, username: String)? {
|
||||
if !self.contentExternalState.usernameIsChecked {
|
||||
return nil
|
||||
}
|
||||
return CreateBotSheetComponent.View.validatedParams(inputName: contentExternalState.name, inputUsername: self.contentExternalState.username)
|
||||
}
|
||||
|
||||
private func performCreateBot() {
|
||||
|
|
@ -413,6 +622,7 @@ private final class CreateBotSheetComponent: Component {
|
|||
return
|
||||
}
|
||||
let context = component.context
|
||||
self.isCompleted = true
|
||||
self.animateOut.invoke(Action { [weak controller, weak navigationController] _ in
|
||||
if let controller, let navigationController {
|
||||
controller.dismiss(completion: { [weak navigationController] in
|
||||
|
|
@ -466,8 +676,15 @@ private final class CreateBotSheetComponent: Component {
|
|||
let theme = environmentValue.theme
|
||||
|
||||
let dismiss: (Bool) -> Void = { [weak self] animated in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
if !self.isCompleted {
|
||||
self.isCompleted = true
|
||||
component.completion(nil)
|
||||
}
|
||||
if animated {
|
||||
self?.animateOut.invoke(Action { _ in
|
||||
self.animateOut.invoke(Action { _ in
|
||||
if let controller = controller() {
|
||||
controller.dismiss(completion: nil)
|
||||
}
|
||||
|
|
@ -544,7 +761,17 @@ private final class CreateBotSheetComponent: Component {
|
|||
isCentered: environmentValue.metrics.widthClass == .regular,
|
||||
screenSize: availableSize,
|
||||
regularMetricsSize: nil,
|
||||
dismiss: { animated in
|
||||
dismiss: { [weak self] animated in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if animated {
|
||||
if !self.attemptNavigation(complete: {
|
||||
dismiss(animated)
|
||||
}) {
|
||||
return
|
||||
}
|
||||
}
|
||||
dismiss(animated)
|
||||
}
|
||||
)
|
||||
|
|
@ -581,7 +808,7 @@ public class CreateBotScreen: ViewControllerComponentContainer {
|
|||
initialUsername: String?,
|
||||
initialTitle: String?,
|
||||
openAutomatically: Bool,
|
||||
completion: @escaping (EnginePeer.Id) -> Void
|
||||
completion: @escaping (EnginePeer.Id?) -> Void
|
||||
) async {
|
||||
self.context = context
|
||||
|
||||
|
|
@ -609,6 +836,14 @@ public class CreateBotScreen: ViewControllerComponentContainer {
|
|||
self.statusBar.statusBarStyle = .Ignore
|
||||
self.navigationPresentation = .flatModal
|
||||
self.blocksBackgroundWhenInOverlay = true
|
||||
|
||||
self.attemptNavigation = { [weak self] complete in
|
||||
guard let self, let componentView = self.node.hostView.componentView as? CreateBotSheetComponent.View else {
|
||||
return true
|
||||
}
|
||||
|
||||
return componentView.attemptNavigation(complete: complete)
|
||||
}
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
|
|
@ -777,13 +1012,16 @@ private final class ActionButtonsComponent: Component {
|
|||
private final class EditLabelComponent: Component {
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let action: () -> Void
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings
|
||||
strings: PresentationStrings,
|
||||
action: @escaping () -> Void
|
||||
) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.action = action
|
||||
}
|
||||
|
||||
static func ==(lhs: EditLabelComponent, rhs: EditLabelComponent) -> Bool {
|
||||
|
|
@ -805,12 +1043,23 @@ private final class EditLabelComponent: Component {
|
|||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:))))
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func onTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
if case .ended = recognizer.state {
|
||||
component.action()
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: EditLabelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
|
@ -846,6 +1095,7 @@ private final class EditLabelComponent: Component {
|
|||
)
|
||||
if let backgroundView = self.background.view {
|
||||
if backgroundView.superview == nil {
|
||||
backgroundView.isUserInteractionEnabled = false
|
||||
self.addSubview(backgroundView)
|
||||
}
|
||||
transition.setFrame(view: backgroundView, frame: backgroundFrame)
|
||||
|
|
@ -853,6 +1103,7 @@ private final class EditLabelComponent: Component {
|
|||
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
titleView.isUserInteractionEnabled = false
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
transition.setPosition(view: titleView, position: titleFrame.center)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,11 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
|||
case legacy
|
||||
}
|
||||
|
||||
public final class Tag {
|
||||
public init() {
|
||||
}
|
||||
}
|
||||
|
||||
public final class ExternalState {
|
||||
public fileprivate(set) var hasText: Bool = false
|
||||
public fileprivate(set) var text: NSAttributedString = NSAttributedString()
|
||||
|
|
|
|||
|
|
@ -126,9 +126,11 @@ public extension PeerInfoScreenImpl {
|
|||
commit()
|
||||
}
|
||||
controller.videoCompletion = { [weak parentController] image, url, values, markup, commit in
|
||||
guard let parentController else {
|
||||
return
|
||||
}
|
||||
resultImage = image
|
||||
let _ = parentController
|
||||
updateProfileVideo(context: context, image: image, video: nil, values: nil, markup: markup, mode: mode, uploadStatus: uploadStatusPromise)
|
||||
updateProfileVideo(parentController: parentController, context: context, peer: peer, image: image, video: nil, values: nil, markup: markup, mode: mode, uploadStatus: uploadStatusPromise)
|
||||
commit()
|
||||
}
|
||||
parentController.push(controller)
|
||||
|
|
@ -193,7 +195,7 @@ public extension PeerInfoScreenImpl {
|
|||
case let .video(video, coverImage, values, _, _):
|
||||
if let coverImage {
|
||||
resultImage = coverImage
|
||||
updateProfileVideo(context: context, image: coverImage, video: video, values: values, markup: nil, mode: mode, uploadStatus: uploadStatusPromise)
|
||||
updateProfileVideo(parentController: parentController, context: context, peer: peer, image: coverImage, video: video, values: values, markup: nil, mode: mode, uploadStatus: uploadStatusPromise)
|
||||
}
|
||||
commit({})
|
||||
default:
|
||||
|
|
@ -283,52 +285,24 @@ public extension PeerInfoScreenImpl {
|
|||
|
||||
if case .complete = result {
|
||||
dismissStatus?()
|
||||
|
||||
/*let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id))
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
|
||||
if let strongSelf = self, let peer {
|
||||
switch mode {
|
||||
case .fallback:
|
||||
(strongSelf.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: nil, text: strongSelf.presentationData.strings.Privacy_ProfilePhoto_PublicPhotoSuccess, round: true, undoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
|
||||
case .custom:
|
||||
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, title: nil, text: strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_SuccessPhotoText(peer.compactDisplayTitle).string, action: nil, duration: 5), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
|
||||
|
||||
let _ = (strongSelf.context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, peerId: strongSelf.peerId, fetch: peerInfoProfilePhotos(context: strongSelf.context, peerId: strongSelf.peerId)) |> ignoreValues).startStandalone()
|
||||
case .suggest:
|
||||
if let navigationController = (strongSelf.navigationController as? NavigationController) {
|
||||
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), keepStack: .default, completion: { _ in
|
||||
}))
|
||||
}
|
||||
case .accept:
|
||||
(strongSelf.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: strongSelf.presentationData.strings.Conversation_SuggestedPhotoSuccess, text: strongSelf.presentationData.strings.Conversation_SuggestedPhotoSuccessText, round: true, undoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] action in
|
||||
if case .info = action {
|
||||
self?.parentController?.openSettings(edit: false)
|
||||
}
|
||||
return false
|
||||
}), in: .current)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
})*/
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
static func updateProfileVideo(context: AccountContext, image: UIImage, video: Any?, values: Any?, markup: UploadPeerPhotoMarkup?) {
|
||||
updateProfileVideo(context: context, image: image, video: video as? MediaEditorScreenImpl.MediaResult.VideoResult, values: values as? MediaEditorValues, markup: markup, mode: .generic, uploadStatus: nil)
|
||||
static func updateProfileVideo(parentController: ViewController, context: AccountContext, peer: EnginePeer, image: UIImage, video: Any?, values: Any?, markup: UploadPeerPhotoMarkup?) {
|
||||
updateProfileVideo(parentController: parentController, context: context, peer: peer, image: image, video: video as? MediaEditorScreenImpl.MediaResult.VideoResult, values: values as? MediaEditorValues, markup: markup, mode: .generic, uploadStatus: nil)
|
||||
}
|
||||
|
||||
static func updateProfileVideo(context: AccountContext, image: UIImage, video: MediaEditorScreenImpl.MediaResult.VideoResult?, values: MediaEditorValues?, markup: UploadPeerPhotoMarkup?, mode: PeerInfoAvatarEditingMode, uploadStatus: Promise<PeerInfoAvatarUploadStatus>?) {
|
||||
/*var uploadVideo = true
|
||||
static func updateProfileVideo(parentController: ViewController, context: AccountContext, peer: EnginePeer, image: UIImage, video: MediaEditorScreenImpl.MediaResult.VideoResult?, values: MediaEditorValues?, markup: UploadPeerPhotoMarkup?, mode: PeerInfoAvatarEditingMode, uploadStatus: Promise<PeerInfoAvatarUploadStatus>?) {
|
||||
var uploadVideo = true
|
||||
if let _ = markup {
|
||||
if let data = self.context.currentAppConfiguration.with({ $0 }).data, let uploadVideoValue = data["upload_markup_video"] as? Bool, uploadVideoValue {
|
||||
if let data = context.currentAppConfiguration.with({ $0 }).data, let uploadVideoValue = data["upload_markup_video"] as? Bool, uploadVideoValue {
|
||||
uploadVideo = true
|
||||
} else {
|
||||
uploadVideo = false
|
||||
}
|
||||
}
|
||||
guard let photoResource = self.setupProfilePhotoUpload(image: image, mode: mode, indefiniteProgress: !uploadVideo) else {
|
||||
guard let photoResource = sharedSetupProfilePhotoUpload(context: context, image: image, mode: mode) else {
|
||||
uploadStatus?.set(.single(.done))
|
||||
return
|
||||
}
|
||||
|
|
@ -338,8 +312,8 @@ public extension PeerInfoScreenImpl {
|
|||
videoStartTimestamp = coverImageTimestamp - (values.videoTrimRange?.lowerBound ?? 0.0)
|
||||
}
|
||||
|
||||
let account = self.context.account
|
||||
let context = self.context
|
||||
let account = context.account
|
||||
let context = context
|
||||
|
||||
let videoResource: Signal<TelegramMediaResource?, UploadPeerPhotoError>
|
||||
if uploadVideo, let video, let values {
|
||||
|
|
@ -388,10 +362,7 @@ public extension PeerInfoScreenImpl {
|
|||
let tempFile = EngineTempBox.shared.tempFile(fileName: "video.mp4")
|
||||
let videoExport = MediaEditorVideoExport(postbox: context.account.postbox, subject: exportSubject, configuration: configuration, outputPath: tempFile.path, textScale: 2.0)
|
||||
let _ = (videoExport.status
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] status in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|> deliverOnMainQueue).startStandalone(next: { status in
|
||||
switch status {
|
||||
case .completed:
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: tempFile.path), options: .mappedIfSafe) {
|
||||
|
|
@ -401,11 +372,8 @@ public extension PeerInfoScreenImpl {
|
|||
subscriber.putCompletion()
|
||||
}
|
||||
EngineTempBox.shared.dispose(tempFile)
|
||||
case let .progress(progress):
|
||||
Queue.mainQueue().async {
|
||||
self.controllerNode.state = self.controllerNode.state.withAvatarUploadProgress(.value(CGFloat(progress * 0.45)))
|
||||
self.requestLayout(transition: .immediate)
|
||||
}
|
||||
case .progress:
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
|
@ -422,100 +390,41 @@ public extension PeerInfoScreenImpl {
|
|||
}
|
||||
|
||||
var dismissStatus: (() -> Void)?
|
||||
if [.suggest, .fallback, .accept].contains(mode) {
|
||||
let statusController = OverlayStatusController(theme: self.presentationData.theme, type: .loading(cancelled: { [weak self] in
|
||||
self?.controllerNode.updateAvatarDisposable.set(nil)
|
||||
if "".isEmpty {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
|
||||
let statusController = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||
dismissStatus?()
|
||||
}))
|
||||
dismissStatus = { [weak statusController] in
|
||||
statusController?.dismiss()
|
||||
}
|
||||
if let topController = self.navigationController?.topViewController as? ViewController {
|
||||
topController.presentInGlobalOverlay(statusController)
|
||||
} else if let topController = self.parentController?.topViewController as? ViewController {
|
||||
if let topController = parentController.navigationController?.topViewController as? ViewController {
|
||||
topController.presentInGlobalOverlay(statusController)
|
||||
} else {
|
||||
self.presentInGlobalOverlay(statusController)
|
||||
parentController.presentInGlobalOverlay(statusController)
|
||||
}
|
||||
}
|
||||
|
||||
let peerId = self.peerId
|
||||
let isSettings = self.isSettings
|
||||
let isMyProfile = self.isMyProfile
|
||||
self.controllerNode.updateAvatarDisposable.set((videoResource
|
||||
let peerId = peer.id
|
||||
let updateAvatarDisposable = MetaDisposable()
|
||||
updateAvatarDisposable.set((videoResource
|
||||
|> mapToSignal { videoResource -> Signal<UpdatePeerPhotoStatus, UploadPeerPhotoError> in
|
||||
if isSettings || isMyProfile {
|
||||
if case .fallback = mode {
|
||||
return context.engine.accountData.updateFallbackPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in
|
||||
return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations)
|
||||
})
|
||||
} else {
|
||||
return context.engine.accountData.updateAccountPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in
|
||||
return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations)
|
||||
})
|
||||
}
|
||||
} else if case .custom = mode {
|
||||
return context.engine.contacts.updateContactPhoto(peerId: peerId, resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mode: .custom, mapResourceToAvatarSizes: { resource, representations in
|
||||
return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations)
|
||||
})
|
||||
} else if case .suggest = mode {
|
||||
return context.engine.contacts.updateContactPhoto(peerId: peerId, resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mode: .suggest, mapResourceToAvatarSizes: { resource, representations in
|
||||
return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations)
|
||||
})
|
||||
} else {
|
||||
return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: photoResource), video: videoResource.flatMap { context.engine.peers.uploadedPeerVideo(resource: $0) |> map(Optional.init) }, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in
|
||||
return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations)
|
||||
})
|
||||
}
|
||||
return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: photoResource), video: videoResource.flatMap { context.engine.peers.uploadedPeerVideo(resource: $0) |> map(Optional.init) }, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in
|
||||
return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations)
|
||||
})
|
||||
}
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] result in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|> deliverOnMainQueue).startStandalone(next: { result in
|
||||
switch result {
|
||||
case .complete:
|
||||
uploadStatus?.set(.single(.done))
|
||||
strongSelf.controllerNode.state = strongSelf.controllerNode.state.withUpdatingAvatar(nil).withAvatarUploadProgress(nil)
|
||||
case let .progress(value):
|
||||
uploadStatus?.set(.single(.progress(value)))
|
||||
strongSelf.controllerNode.state = strongSelf.controllerNode.state.withAvatarUploadProgress(.value(CGFloat(0.45 + value * 0.55)))
|
||||
}
|
||||
if let (layout, navigationHeight) = strongSelf.controllerNode.validLayout {
|
||||
strongSelf.controllerNode.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false)
|
||||
}
|
||||
|
||||
if case .complete = result {
|
||||
dismissStatus?()
|
||||
|
||||
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.peerId))
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
|
||||
if let strongSelf = self, let peer {
|
||||
switch mode {
|
||||
case .fallback:
|
||||
(strongSelf.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: nil, text: strongSelf.presentationData.strings.Privacy_ProfilePhoto_PublicVideoSuccess, round: true, undoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
|
||||
case .custom:
|
||||
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, title: nil, text: strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_SuccessVideoText(peer.compactDisplayTitle).string, action: nil, duration: 5), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
|
||||
|
||||
let _ = (strongSelf.context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, peerId: strongSelf.peerId, fetch: peerInfoProfilePhotos(context: strongSelf.context, peerId: strongSelf.peerId)) |> ignoreValues).startStandalone()
|
||||
case .suggest:
|
||||
if let navigationController = (strongSelf.navigationController as? NavigationController) {
|
||||
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), keepStack: .default, completion: { _ in
|
||||
}))
|
||||
}
|
||||
case .accept:
|
||||
(strongSelf.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: strongSelf.presentationData.strings.Conversation_SuggestedVideoSuccess, text: strongSelf.presentationData.strings.Conversation_SuggestedVideoSuccessText, round: true, undoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] action in
|
||||
if case .info = action {
|
||||
self?.parentController?.openSettings(edit: false)
|
||||
}
|
||||
return false
|
||||
}), in: .current)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}))*/
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -896,7 +896,7 @@ final class StoryContentCaptionComponent: Component {
|
|||
self.textSelectionKnobContainer.addSubview(textSelectionKnobSurface)
|
||||
}
|
||||
|
||||
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: component.theme.list.itemAccentColor, isDark: true), strings: component.strings, textNode: textNode, updateIsActive: { [weak self] value in
|
||||
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: component.theme.list.itemAccentColor, isDark: true), strings: component.strings, textNodeOrView: .node(textNode), updateIsActive: { [weak self] value in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
|
@ -912,8 +912,8 @@ final class StoryContentCaptionComponent: Component {
|
|||
return
|
||||
}
|
||||
component.controller()?.presentInGlobalOverlay(c, with: a)
|
||||
}, rootNode: { [weak controller] in
|
||||
return controller?.displayNode
|
||||
}, rootView: { [weak controller] in
|
||||
return controller?.displayNode.view
|
||||
}, externalKnobSurface: self.textSelectionKnobSurface, performAction: { [weak self] text, action in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -41,6 +41,9 @@ swift_library(
|
|||
"//submodules/Markdown",
|
||||
"//submodules/TelegramUIPreferences",
|
||||
"//submodules/ChatSendMessageActionUI",
|
||||
"//submodules/TextSelectionNode",
|
||||
"//submodules/Pasteboard",
|
||||
"//submodules/Speak",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ final class TextProcessingContentComponent: Component {
|
|||
|
||||
final class View: UIView {
|
||||
private var component: TextProcessingContentComponent?
|
||||
private var environment: ViewControllerComponentContainer.Environment?
|
||||
private weak var state: EmptyComponentState?
|
||||
private var isUpdating: Bool = false
|
||||
|
||||
|
|
@ -99,7 +100,7 @@ final class TextProcessingContentComponent: Component {
|
|||
|
||||
private var currentContent: (mode: Mode, view: ComponentView<Empty>)?
|
||||
|
||||
private var currentMode: Mode = .stylize
|
||||
private var currentMode: Mode = .translate
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.currentContentBackground = UIImageView()
|
||||
|
|
@ -204,9 +205,16 @@ final class TextProcessingContentComponent: Component {
|
|||
|
||||
if self.component == nil {
|
||||
self.stylizeState.displayStyleTooltip = component.shouldDisplayStyleNotice
|
||||
switch component.mode {
|
||||
case .edit:
|
||||
self.currentMode = .stylize
|
||||
case .translate:
|
||||
self.currentMode = .translate
|
||||
}
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.environment = environment
|
||||
self.state = state
|
||||
|
||||
let sideInset: CGFloat = 16.0
|
||||
|
|
@ -345,7 +353,13 @@ final class TextProcessingContentComponent: Component {
|
|||
inputText: component.inputText,
|
||||
mode: .translate,
|
||||
copyAction: component.copyCurrentResult,
|
||||
displayLanguageSelectionMenu: component.displayLanguageSelectionMenu
|
||||
displayLanguageSelectionMenu: component.displayLanguageSelectionMenu,
|
||||
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
|
||||
}
|
||||
))
|
||||
case .stylize:
|
||||
contentComponent = AnyComponent(TextProcessingTranslateContentComponent(
|
||||
|
|
@ -357,7 +371,13 @@ final class TextProcessingContentComponent: Component {
|
|||
inputText: component.inputText,
|
||||
mode: .stylize,
|
||||
copyAction: component.copyCurrentResult,
|
||||
displayLanguageSelectionMenu: component.displayLanguageSelectionMenu
|
||||
displayLanguageSelectionMenu: component.displayLanguageSelectionMenu,
|
||||
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
|
||||
}
|
||||
))
|
||||
case .fix:
|
||||
contentComponent = AnyComponent(TextProcessingTranslateContentComponent(
|
||||
|
|
@ -369,7 +389,13 @@ final class TextProcessingContentComponent: Component {
|
|||
inputText: component.inputText,
|
||||
mode: .fix,
|
||||
copyAction: component.copyCurrentResult,
|
||||
displayLanguageSelectionMenu: component.displayLanguageSelectionMenu
|
||||
displayLanguageSelectionMenu: component.displayLanguageSelectionMenu,
|
||||
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
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
|
|
@ -1049,22 +1075,9 @@ 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 typealias SendContextActions = TextProcessingScreenSendContextActions
|
||||
|
||||
public enum Mode {
|
||||
case edit(saveRestoreStateId: EnginePeer.Id?, completion: (TextWithEntities) -> Void, send: ((TextWithEntities) -> Void)?, sendContextActions: SendContextActions?)
|
||||
case translate(fromLanguage: String?)
|
||||
}
|
||||
public typealias Mode = TextProcessingScreenMode
|
||||
|
||||
struct EditState: Codable {
|
||||
var selectedMode: Int32
|
||||
|
|
|
|||
|
|
@ -13,10 +13,14 @@ import TextFormat
|
|||
import PlainButtonComponent
|
||||
import CheckComponent
|
||||
import ShimmerEffect
|
||||
import TextSelectionNode
|
||||
import Pasteboard
|
||||
import Speak
|
||||
|
||||
final class TextProcessingTextAreaComponent: Component {
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let titlePrefix: String
|
||||
let title: String
|
||||
let titleAction: ((UIView) -> Void)?
|
||||
|
|
@ -26,10 +30,13 @@ final class TextProcessingTextAreaComponent: Component {
|
|||
let text: TextWithEntities?
|
||||
let loadingStateMeasuringText: String?
|
||||
let textCorrectionRanges: [Range<Int>]
|
||||
let present: (ViewController, Any?) -> Void
|
||||
let rootViewForTextSelection: () -> UIView?
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
titlePrefix: String,
|
||||
title: String,
|
||||
titleAction: ((UIView) -> Void)?,
|
||||
|
|
@ -38,10 +45,13 @@ final class TextProcessingTextAreaComponent: Component {
|
|||
emojify: (value: Bool, toggle: () -> Void)?,
|
||||
text: TextWithEntities?,
|
||||
loadingStateMeasuringText: String?,
|
||||
textCorrectionRanges: [Range<Int>]
|
||||
textCorrectionRanges: [Range<Int>],
|
||||
present: @escaping (ViewController, Any?) -> Void,
|
||||
rootViewForTextSelection: @escaping () -> UIView?
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.titlePrefix = titlePrefix
|
||||
self.isExpanded = isExpanded
|
||||
self.copyAction = copyAction
|
||||
|
|
@ -51,6 +61,8 @@ final class TextProcessingTextAreaComponent: Component {
|
|||
self.text = text
|
||||
self.loadingStateMeasuringText = loadingStateMeasuringText
|
||||
self.textCorrectionRanges = textCorrectionRanges
|
||||
self.present = present
|
||||
self.rootViewForTextSelection = rootViewForTextSelection
|
||||
}
|
||||
|
||||
static func ==(lhs: TextProcessingTextAreaComponent, rhs: TextProcessingTextAreaComponent) -> Bool {
|
||||
|
|
@ -60,6 +72,9 @@ final class TextProcessingTextAreaComponent: Component {
|
|||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.titlePrefix != rhs.titlePrefix {
|
||||
return false
|
||||
}
|
||||
|
|
@ -115,15 +130,29 @@ final class TextProcessingTextAreaComponent: Component {
|
|||
private let measureLoadingText = ComponentView<Empty>()
|
||||
private var shimmerEffectNode: ShimmerEffectNode?
|
||||
|
||||
private var textSelectionNode: TextSelectionNode?
|
||||
private let textSelectionContainer: UIView
|
||||
private let textSelectionKnobContainer: UIView
|
||||
private let textSelectionKnobSurface: UIView
|
||||
|
||||
private var currentSpeechHolder: SpeechSynthesizerHolder?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.textContainer = UIView()
|
||||
self.textContainer.clipsToBounds = true
|
||||
|
||||
self.textSelectionContainer = UIView()
|
||||
self.textSelectionContainer.isUserInteractionEnabled = false
|
||||
self.textSelectionKnobContainer = UIView()
|
||||
self.textSelectionKnobSurface = UIView()
|
||||
self.textSelectionKnobContainer.addSubview(self.textSelectionKnobSurface)
|
||||
|
||||
self.titleButton = HighlightTrackingButton()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.textContainer)
|
||||
self.addSubview(self.textSelectionContainer)
|
||||
self.addSubview(self.titleButton)
|
||||
|
||||
self.titleButton.highligthedChanged = { [weak self] highighed in
|
||||
|
|
@ -459,6 +488,96 @@ final class TextProcessingTextAreaComponent: Component {
|
|||
}
|
||||
}
|
||||
|
||||
if component.text != nil, component.isExpanded?.value ?? true, let textView = self.text.view as? MultilineTextWithEntitiesComponent.View {
|
||||
let textSelectionNode: TextSelectionNode
|
||||
if let current = self.textSelectionNode {
|
||||
textSelectionNode = current
|
||||
} else {
|
||||
textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: component.theme.list.itemAccentColor.withMultipliedAlpha(0.5), knob: component.theme.list.itemAccentColor, isDark: component.theme.overallDarkAppearance), strings: component.strings, textNodeOrView: .node(textView.textNode), updateIsActive: { [weak self] value in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = self
|
||||
}, present: { [weak self] c, a in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.present(c, a)
|
||||
}, rootView: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return nil
|
||||
}
|
||||
return component.rootViewForTextSelection()
|
||||
}, externalKnobSurface: self.textSelectionKnobSurface, performAction: { [weak self] text, action in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
switch action {
|
||||
case .copy:
|
||||
storeAttributedTextInPasteboard(text)
|
||||
case .share:
|
||||
let shareController = component.context.sharedContext.makeShareController(context: component.context, subject: .text(text.string), forceExternal: true, shareStory: nil, enqueued: nil, actionCompleted: nil)
|
||||
component.present(shareController, nil)
|
||||
case .lookup:
|
||||
let controller = UIReferenceLibraryViewController(term: text.string)
|
||||
if let window = self.window {
|
||||
controller.popoverPresentationController?.sourceView = window
|
||||
controller.popoverPresentationController?.sourceRect = CGRect(origin: CGPoint(x: window.bounds.width / 2.0, y: window.bounds.size.height - 1.0), size: CGSize(width: 1.0, height: 1.0))
|
||||
window.rootViewController?.present(controller, animated: true)
|
||||
}
|
||||
case .speak:
|
||||
if let speechHolder = speakText(context: component.context, text: text.string) {
|
||||
speechHolder.completion = { [weak self, weak speechHolder] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if self.currentSpeechHolder === speechHolder {
|
||||
self.currentSpeechHolder = nil
|
||||
}
|
||||
}
|
||||
self.currentSpeechHolder = speechHolder
|
||||
}
|
||||
case .translate:
|
||||
break
|
||||
case .quote:
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
textSelectionNode.enableLookup = true
|
||||
textSelectionNode.enableTranslate = false
|
||||
textSelectionNode.menuSkipCoordnateConversion = false
|
||||
textSelectionNode.canBeginSelection = { _ in
|
||||
return true
|
||||
}
|
||||
self.textSelectionNode = textSelectionNode
|
||||
|
||||
self.textSelectionContainer.insertSubview(self.textSelectionKnobContainer, at: 0)
|
||||
self.textContainer.insertSubview(textSelectionNode.highlightAreaNode.view, belowSubview: textView)
|
||||
self.textContainer.insertSubview(textSelectionNode.view, aboveSubview: textView)
|
||||
}
|
||||
|
||||
textSelectionNode.frame = textView.frame
|
||||
textSelectionNode.highlightAreaNode.frame = textView.frame
|
||||
self.textSelectionKnobSurface.frame = textView.frame
|
||||
} else {
|
||||
for subview in Array(self.textSelectionContainer.subviews) {
|
||||
subview.removeFromSuperview()
|
||||
}
|
||||
if self.textSelectionKnobContainer.superview != nil {
|
||||
self.textSelectionKnobContainer.removeFromSuperview()
|
||||
}
|
||||
if let textSelectionNode = self.textSelectionNode {
|
||||
self.textSelectionNode = nil
|
||||
textSelectionNode.view.removeFromSuperview()
|
||||
|
||||
if textSelectionNode.highlightAreaNode.view.superview != nil {
|
||||
textSelectionNode.highlightAreaNode.view.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if component.text == nil {
|
||||
let shimmerEffectNode: ShimmerEffectNode
|
||||
if let current = self.shimmerEffectNode {
|
||||
|
|
@ -568,6 +687,7 @@ final class TextProcessingTextAreaComponent: Component {
|
|||
}
|
||||
|
||||
transition.setFrame(view: self.textContainer, frame: textContainerFrame)
|
||||
transition.setFrame(view: self.textSelectionContainer, frame: textContainerFrame)
|
||||
contentHeight += textContainerFrame.height
|
||||
|
||||
contentHeight += bottomInset
|
||||
|
|
|
|||
|
|
@ -83,6 +83,8 @@ final class TextProcessingTranslateContentComponent: Component {
|
|||
let mode: Mode
|
||||
let copyAction: (() -> Void)?
|
||||
let displayLanguageSelectionMenu: (UIView, String, TelegramComposeAIMessageMode.StyleId, Bool, @escaping (String, TelegramComposeAIMessageMode.StyleId) -> Void) -> Void
|
||||
let present: (ViewController, Any?) -> Void
|
||||
let rootViewForTextSelection: () -> UIView?
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
|
|
@ -93,7 +95,9 @@ final class TextProcessingTranslateContentComponent: Component {
|
|||
inputText: TextWithEntities,
|
||||
mode: Mode,
|
||||
copyAction: (() -> Void)?,
|
||||
displayLanguageSelectionMenu: @escaping (UIView, String, TelegramComposeAIMessageMode.StyleId, Bool, @escaping (String, TelegramComposeAIMessageMode.StyleId) -> Void) -> Void
|
||||
displayLanguageSelectionMenu: @escaping (UIView, String, TelegramComposeAIMessageMode.StyleId, Bool, @escaping (String, TelegramComposeAIMessageMode.StyleId) -> Void) -> Void,
|
||||
present: @escaping (ViewController, Any?) -> Void,
|
||||
rootViewForTextSelection: @escaping () -> UIView?
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
|
|
@ -104,6 +108,8 @@ final class TextProcessingTranslateContentComponent: Component {
|
|||
self.mode = mode
|
||||
self.copyAction = copyAction
|
||||
self.displayLanguageSelectionMenu = displayLanguageSelectionMenu
|
||||
self.present = present
|
||||
self.rootViewForTextSelection = rootViewForTextSelection
|
||||
}
|
||||
|
||||
static func ==(lhs: TextProcessingTranslateContentComponent, rhs: TextProcessingTranslateContentComponent) -> Bool {
|
||||
|
|
@ -354,6 +360,7 @@ final class TextProcessingTranslateContentComponent: Component {
|
|||
component: AnyComponent(TextProcessingTextAreaComponent(
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
titlePrefix: fromPrefix,
|
||||
title: localizedLanguageName(strings: component.strings, language: component.externalState.sourceLanguage ?? ""),
|
||||
titleAction: nil,
|
||||
|
|
@ -373,7 +380,9 @@ final class TextProcessingTranslateContentComponent: Component {
|
|||
emojify: nil,
|
||||
text: component.inputText,
|
||||
loadingStateMeasuringText: nil,
|
||||
textCorrectionRanges: []
|
||||
textCorrectionRanges: [],
|
||||
present: component.present,
|
||||
rootViewForTextSelection: component.rootViewForTextSelection
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height)
|
||||
|
|
@ -396,6 +405,7 @@ final class TextProcessingTranslateContentComponent: Component {
|
|||
component: AnyComponent(TextProcessingTextAreaComponent(
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
titlePrefix: toPrefix,
|
||||
title: toTitle,
|
||||
titleAction: component.mode == .translate ? { [weak self] sourceView in
|
||||
|
|
@ -441,7 +451,9 @@ final class TextProcessingTranslateContentComponent: Component {
|
|||
) : nil,
|
||||
text: component.externalState.result?.text,
|
||||
loadingStateMeasuringText: component.inputText.text,
|
||||
textCorrectionRanges: component.mode == .fix ? (component.externalState.result?.textCorrectionRanges ?? []) : []
|
||||
textCorrectionRanges: component.mode == .fix ? (component.externalState.result?.textCorrectionRanges ?? []) : [],
|
||||
present: component.present,
|
||||
rootViewForTextSelection: component.rootViewForTextSelection
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height)
|
||||
|
|
|
|||
|
|
@ -4463,7 +4463,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
}
|
||||
|
||||
let effectiveInputText: NSAttributedString
|
||||
var isEdit = false
|
||||
if effectivePresentationInterfaceState.interfaceState.editMessage != nil {
|
||||
isEdit = true
|
||||
effectiveInputText = expandedInputStateAttributedString(effectivePresentationInterfaceState.interfaceState.effectiveInputState.inputText)
|
||||
} else {
|
||||
effectiveInputText = expandedInputStateAttributedString(effectivePresentationInterfaceState.interfaceState.composeInputState.inputText)
|
||||
|
|
@ -4516,7 +4518,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
})
|
||||
self.sendCurrentMessage()
|
||||
},
|
||||
sendContextActions: self.chatLocation.peerId.flatMap { peerId in return TextProcessingScreen.SendContextActions(
|
||||
sendContextActions: isEdit ? nil : 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 {
|
||||
|
|
|
|||
|
|
@ -506,6 +506,7 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode {
|
|||
})
|
||||
|
||||
view.setImage(image?.withRenderingMode(.alwaysOriginal), for: [])
|
||||
view.setImage(generateTintedImage(image: image, color: interfaceState.theme.rootController.navigationBar.accentTextColor.withAlphaComponent(0.7))?.withRenderingMode(.alwaysOriginal), for: [.highlighted])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ import ChatParticipantRightsScreen
|
|||
import PeerCopyProtectionInfoScreen
|
||||
import ChatRankInfoScreen
|
||||
import RankChatPreviewItem
|
||||
import TextProcessingScreen
|
||||
import CreateBotScreen
|
||||
|
||||
private final class AccountUserInterfaceInUseContext {
|
||||
|
|
@ -4365,6 +4366,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 makeTextProcessingScreen(
|
||||
context: AccountContext,
|
||||
mode: TextProcessingScreenMode,
|
||||
ignoredTranslationLanguages: [String],
|
||||
inputText: TextWithEntities,
|
||||
copyResult: ((TextWithEntities) -> Void)?,
|
||||
translateChat: ((String) -> Void)?
|
||||
) async -> ViewController {
|
||||
return await TextProcessingScreen(
|
||||
context: context,
|
||||
mode: mode,
|
||||
ignoredTranslationLanguages: ignoredTranslationLanguages,
|
||||
inputText: inputText,
|
||||
copyResult: copyResult,
|
||||
translateChat: translateChat
|
||||
)
|
||||
}
|
||||
|
||||
public func makeCreateBotScreen(
|
||||
context: AccountContext,
|
||||
parentBot: EnginePeer.Id,
|
||||
|
|
|
|||
|
|
@ -216,15 +216,56 @@ public enum TextSelectionAction: Equatable {
|
|||
}
|
||||
|
||||
public final class TextSelectionNode: ASDisplayNode {
|
||||
public enum TextNodeOrView {
|
||||
case node(TextNodeProtocol)
|
||||
case view(TextView)
|
||||
|
||||
var view: UIView {
|
||||
switch self {
|
||||
case let .node(node):
|
||||
return node.view
|
||||
case let .view(view):
|
||||
return view
|
||||
}
|
||||
}
|
||||
|
||||
var currentText: NSAttributedString? {
|
||||
switch self {
|
||||
case let .node(node):
|
||||
return node.currentText
|
||||
case let .view(view):
|
||||
return view.cachedLayout?.attributedString
|
||||
}
|
||||
}
|
||||
|
||||
func attributesAtPoint(_ point: CGPoint, orNearest: Bool = false) -> (Int, [NSAttributedString.Key: Any])? {
|
||||
switch self {
|
||||
case let .node(node):
|
||||
return node.attributesAtPoint(point, orNearest: orNearest)
|
||||
case let .view(view):
|
||||
return view.attributesAtPoint(point, orNearest: orNearest)
|
||||
}
|
||||
}
|
||||
|
||||
func textRangeRects(in range: NSRange) -> (rects: [CGRect], start: TextRangeRectEdge, end: TextRangeRectEdge)? {
|
||||
switch self {
|
||||
case let .node(node):
|
||||
return node.textRangeRects(in: range)
|
||||
case let .view(view):
|
||||
return view.cachedLayout?.rangeRects(in: range)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let theme: TextSelectionTheme
|
||||
private let strings: PresentationStrings
|
||||
private let textNode: TextNodeProtocol
|
||||
private let textNodeOrView: TextNodeOrView
|
||||
private let updateIsActive: (Bool) -> Void
|
||||
public var canBeginSelection: (CGPoint) -> Bool = { _ in true }
|
||||
public var updateRange: ((NSRange?) -> Void)?
|
||||
public var presentMenu: ((UIView, CGPoint, [ContextMenuAction]) -> Void)?
|
||||
private let present: (ViewController, Any?) -> Void
|
||||
private let rootNode: () -> ASDisplayNode?
|
||||
private let rootView: () -> UIView?
|
||||
private let performAction: (NSAttributedString, TextSelectionAction) -> Void
|
||||
private var highlightOverlay: LinkHighlightingNode?
|
||||
private let leftKnob: ASImageNode
|
||||
|
|
@ -252,13 +293,13 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||
|
||||
private weak var contextMenu: ContextMenuController?
|
||||
|
||||
public init(theme: TextSelectionTheme, strings: PresentationStrings, textNode: TextNodeProtocol, updateIsActive: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void, rootNode: @escaping () -> ASDisplayNode?, externalKnobSurface: UIView? = nil, performAction: @escaping (NSAttributedString, TextSelectionAction) -> Void) {
|
||||
public init(theme: TextSelectionTheme, strings: PresentationStrings, textNodeOrView: TextNodeOrView, updateIsActive: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void, rootView: @escaping () -> UIView?, externalKnobSurface: UIView? = nil, performAction: @escaping (NSAttributedString, TextSelectionAction) -> Void) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.textNode = textNode
|
||||
self.textNodeOrView = textNodeOrView
|
||||
self.updateIsActive = updateIsActive
|
||||
self.present = present
|
||||
self.rootNode = rootNode
|
||||
self.rootView = rootView
|
||||
self.performAction = performAction
|
||||
self.leftKnob = ASImageNode()
|
||||
self.leftKnob.isUserInteractionEnabled = false
|
||||
|
|
@ -306,8 +347,8 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||
return
|
||||
}
|
||||
|
||||
let mappedPoint = strongSelf.view.convert(point, to: strongSelf.textNode.view)
|
||||
if let stringIndex = strongSelf.textNode.attributesAtPoint(mappedPoint, orNearest: true)?.0 {
|
||||
let mappedPoint = strongSelf.view.convert(point, to: strongSelf.textNodeOrView.view)
|
||||
if let stringIndex = strongSelf.textNodeOrView.attributesAtPoint(mappedPoint, orNearest: true)?.0 {
|
||||
var updatedLeft = currentRange.0
|
||||
var updatedRight = currentRange.1
|
||||
switch knob {
|
||||
|
|
@ -335,15 +376,15 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||
strongSelf.displayMenu()
|
||||
}
|
||||
recognizer.beginSelection = { [weak self] point in
|
||||
guard let strongSelf = self, let attributedString = strongSelf.textNode.currentText else {
|
||||
guard let strongSelf = self, let attributedString = strongSelf.textNodeOrView.currentText else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.dismissSelection()
|
||||
|
||||
let mappedPoint = strongSelf.view.convert(point, to: strongSelf.textNode.view)
|
||||
let mappedPoint = strongSelf.view.convert(point, to: strongSelf.textNodeOrView.view)
|
||||
var resultRange: NSRange?
|
||||
if let stringIndex = strongSelf.textNode.attributesAtPoint(mappedPoint, orNearest: false)?.0 {
|
||||
if let stringIndex = strongSelf.textNodeOrView.attributesAtPoint(mappedPoint, orNearest: false)?.0 {
|
||||
let string = attributedString.string as NSString
|
||||
|
||||
let inputRange = CFRangeMake(0, string.length)
|
||||
|
|
@ -398,7 +439,7 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||
}
|
||||
|
||||
public func pretendInitiateSelection() {
|
||||
guard let attributedString = self.textNode.currentText else {
|
||||
guard let attributedString = self.textNodeOrView.currentText else {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -432,7 +473,7 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||
}
|
||||
|
||||
public func pretendExtendSelection(to index: Int) {
|
||||
guard let endRangeRect = self.textNode.textRangeRects(in: NSRange(location: index, length: 1))?.rects.first else {
|
||||
guard let endRangeRect = self.textNodeOrView.textRangeRects(in: NSRange(location: index, length: 1))?.rects.first else {
|
||||
return
|
||||
}
|
||||
let startPoint = self.rightKnob.frame.center
|
||||
|
|
@ -449,7 +490,7 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||
}
|
||||
|
||||
public func setSelection(range: NSRange, displayMenu: Bool) {
|
||||
guard let attributedString = self.textNode.currentText else {
|
||||
guard let attributedString = self.textNodeOrView.currentText else {
|
||||
return
|
||||
}
|
||||
let range = self.convertSelectionFromOriginalText(attributedString: attributedString, range: range)
|
||||
|
|
@ -561,7 +602,7 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||
}
|
||||
|
||||
public func getSelection() -> NSRange? {
|
||||
guard let currentRange = self.currentRange, let attributedString = self.textNode.currentText else {
|
||||
guard let currentRange = self.currentRange, let attributedString = self.textNodeOrView.currentText else {
|
||||
return nil
|
||||
}
|
||||
let range = NSRange(location: min(currentRange.0, currentRange.1), length: max(currentRange.0, currentRange.1) - min(currentRange.0, currentRange.1))
|
||||
|
|
@ -574,7 +615,7 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||
var rects: (rects: [CGRect], start: TextRangeRectEdge, end: TextRangeRectEdge)?
|
||||
|
||||
if let range {
|
||||
if var rectsValue = self.textNode.textRangeRects(in: range) {
|
||||
if var rectsValue = self.textNodeOrView.textRangeRects(in: range) {
|
||||
var rectList = rectsValue.rects
|
||||
if rectList.count > 1 {
|
||||
for i in 0 ..< rectList.count - 1 {
|
||||
|
|
@ -683,7 +724,7 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||
}
|
||||
|
||||
private func displayMenu() {
|
||||
guard let currentRects = self.currentRects, !currentRects.isEmpty, let currentRange = self.currentRange, let attributedString = self.textNode.currentText else {
|
||||
guard let currentRects = self.currentRects, !currentRects.isEmpty, let currentRange = self.currentRange, let attributedString = self.textNodeOrView.currentText else {
|
||||
return
|
||||
}
|
||||
let range = NSRange(location: min(currentRange.0, currentRange.1), length: max(currentRange.0, currentRange.1) - min(currentRange.0, currentRange.1))
|
||||
|
|
@ -778,15 +819,15 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||
return true
|
||||
}
|
||||
self.contextMenu = contextMenu
|
||||
self.present(contextMenu, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
|
||||
guard let strongSelf = self, let rootNode = strongSelf.rootNode() else {
|
||||
self.present(contextMenu, ContextMenuControllerPresentationArguments(sourceViewAndRect: { [weak self] in
|
||||
guard let strongSelf = self, let rootView = strongSelf.rootView() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if strongSelf.menuSkipCoordnateConversion {
|
||||
return (strongSelf, strongSelf.view.convert(completeRect, to: rootNode.view), rootNode, rootNode.bounds)
|
||||
return (strongSelf.view, strongSelf.view.convert(completeRect, to: rootView), rootView, rootView.bounds)
|
||||
} else {
|
||||
return (strongSelf, completeRect, rootNode, rootNode.bounds)
|
||||
return (strongSelf.view, completeRect, rootView, rootView.bounds)
|
||||
}
|
||||
}, bounce: false))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2350,8 +2350,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||
}
|
||||
}
|
||||
)
|
||||
if let createBotScreen {
|
||||
controller.push(createBotScreen)
|
||||
if let createBotScreen, let navigationController = controller.getNavigationController() {
|
||||
navigationController.pushViewController(createBotScreen)
|
||||
}
|
||||
}
|
||||
default:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue