mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
TextProcessing: style create/edit/delete, AI previews, and integration
Wires up TextProcessingScreen to the edit/delete AI text style facades, renders AI text style webpage previews in chat bubbles, and exposes a shared makeEmojiStatusSelectionController on SharedAccountContext so the screen can present the emoji picker when assigning a style emoji. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d01bbc6529
commit
684796144c
17 changed files with 723 additions and 110 deletions
|
|
@ -1321,6 +1321,13 @@ public enum TextProcessingScreenMode {
|
|||
case translate(fromLanguage: String?)
|
||||
}
|
||||
|
||||
public enum EmojiStatusSelectionControllerMode {
|
||||
case statusSelection
|
||||
case backgroundSelection(completion: (TelegramMediaFile?) -> Void)
|
||||
case customStatusSelection(completion: (TelegramMediaFile?, Int32?) -> Void)
|
||||
case quickReactionSelection(completion: () -> Void)
|
||||
}
|
||||
|
||||
public protocol SharedAccountContext: AnyObject {
|
||||
var sharedContainerPath: String { get }
|
||||
var basePath: String { get }
|
||||
|
|
@ -1575,6 +1582,7 @@ public protocol SharedAccountContext: AnyObject {
|
|||
openAutomatically: Bool,
|
||||
completion: @escaping (EnginePeer.Id?) -> Void
|
||||
) async -> ViewController?
|
||||
func makeEmojiStatusSelectionController(context: AccountContext, mode: EmojiStatusSelectionControllerMode, sourceView: UIView, emojiContent: Signal<AnyObject, NoError>, currentSelection: Int64?, color: UIColor?, destinationItemView: @escaping () -> UIView?) -> ViewController
|
||||
|
||||
func navigateToCurrentCall()
|
||||
var hasOngoingCall: ValuePromise<Bool> { get }
|
||||
|
|
|
|||
|
|
@ -277,6 +277,15 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible
|
|||
}
|
||||
}
|
||||
|
||||
public var pinToEdgeWithInset: Bool {
|
||||
switch self.content {
|
||||
case let .message(message, _, _, _, _):
|
||||
return !message.effectivelyIncoming(self.context.account.peerId)
|
||||
case let .group(messages):
|
||||
return !messages[0].0.effectivelyIncoming(self.context.account.peerId)
|
||||
}
|
||||
}
|
||||
|
||||
public init(presentationData: ChatPresentationData, context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, content: ChatMessageItemContent, disableDate: Bool = false, additionalContent: ChatMessageItemAdditionalContent? = nil) {
|
||||
self.presentationData = presentationData
|
||||
self.context = context
|
||||
|
|
|
|||
|
|
@ -1054,6 +1054,9 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||
|
||||
if previousAnimateGlyphCount != nil || strongSelf.textRevealAnimationState != nil || hasDraft || hadDraft {
|
||||
if strongSelf.textNode.textNode.revealCharacterCount == nil {
|
||||
if hasDraft {
|
||||
strongSelf.statusNode?.alpha = 0.0
|
||||
}
|
||||
strongSelf.textNode.textNode.updateRevealCharacterCount(count: previousAnimateGlyphCount ?? 0, animated: false)
|
||||
}
|
||||
strongSelf.updateTextRevealAnimation(previousGlyphCount: previousAnimateGlyphCount ?? 0)
|
||||
|
|
@ -1084,8 +1087,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||
return
|
||||
}
|
||||
|
||||
var duration: Double = Double(toCount - fromCount) / 20.0
|
||||
duration = max(0.1, min(duration, 5.0))
|
||||
var duration: Double = Double(toCount - fromCount) / 40.0
|
||||
duration = max(0.1, min(duration, 1.0))
|
||||
|
||||
self.textRevealAnimationState = TextRevealAnimationState(
|
||||
fromCount: fromCount,
|
||||
|
|
@ -1108,6 +1111,11 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||
self.textRevealLink = nil
|
||||
|
||||
self.textNode.textNode.updateRevealCharacterCount(count: nil, animated: false)
|
||||
|
||||
if let statusNode = self.statusNode {
|
||||
ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut).updateAlpha(node: statusNode, alpha: 1.0)
|
||||
}
|
||||
|
||||
self.requestFullUpdate?()
|
||||
} else {
|
||||
var requestUpdate = false
|
||||
|
|
|
|||
|
|
@ -522,6 +522,8 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
|
|||
actionTitle = item.presentationData.strings.Chat_ContactChannel
|
||||
case "telegram_newbot":
|
||||
actionTitle = item.presentationData.strings.Chat_CreateBotLink
|
||||
case "telegram_aicomposetone":
|
||||
actionTitle = "VIEW STYLE"
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1430,12 +1430,7 @@ public final class EmojiStatusSelectionController: ViewController {
|
|||
}
|
||||
}
|
||||
|
||||
public enum Mode {
|
||||
case statusSelection
|
||||
case backgroundSelection(completion: (TelegramMediaFile?) -> Void)
|
||||
case customStatusSelection(completion: (TelegramMediaFile?, Int32?) -> Void)
|
||||
case quickReactionSelection(completion: () -> Void)
|
||||
}
|
||||
public typealias Mode = EmojiStatusSelectionControllerMode
|
||||
|
||||
private let context: AccountContext
|
||||
private weak var sourceView: UIView?
|
||||
|
|
|
|||
|
|
@ -2998,9 +2998,9 @@ final class TextContentItemLayer: SimpleLayer {
|
|||
self.updateRevealMask(animateNewSegments: false)
|
||||
})
|
||||
} else if "".isEmpty {
|
||||
ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut)).animateBlur(layer: snippetLayer, fromRadius: 8.0, toRadius: 0.0)
|
||||
ComponentTransition(animation: .curve(duration: 0.22, curve: .easeInOut)).animateBlur(layer: snippetLayer, fromRadius: 3.0, toRadius: 0.0)
|
||||
snippetLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
snippetLayer.animatePosition(from: CGPoint(x: 0.0, y: 10.0), to: CGPoint(), duration: 0.2, additive: true)
|
||||
snippetLayer.animatePosition(from: CGPoint(x: 0.0, y: 6.0), to: CGPoint(), duration: 0.2, additive: true)
|
||||
snippetLayer.animateScale(from: 0.5, to: 1.0, duration: 0.2, completion: { [weak self, weak snippetLayer] _ in
|
||||
guard let self, let snippetLayer else {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
|||
public let initialText: String
|
||||
public let resetText: ResetText?
|
||||
public let placeholder: String
|
||||
public let placeholderDefinesMinHeight: Bool
|
||||
public let autocapitalizationType: UITextAutocapitalizationType
|
||||
public let autocorrectionType: UITextAutocorrectionType
|
||||
public let keyboardType: UIKeyboardType
|
||||
|
|
@ -117,6 +118,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
|||
initialText: String,
|
||||
resetText: ResetText? = nil,
|
||||
placeholder: String,
|
||||
placeholderDefinesMinHeight: Bool = false,
|
||||
autocapitalizationType: UITextAutocapitalizationType = .sentences,
|
||||
autocorrectionType: UITextAutocorrectionType = .default,
|
||||
keyboardType: UIKeyboardType = .default,
|
||||
|
|
@ -144,6 +146,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
|||
self.initialText = initialText
|
||||
self.resetText = resetText
|
||||
self.placeholder = placeholder
|
||||
self.placeholderDefinesMinHeight = placeholderDefinesMinHeight
|
||||
self.autocapitalizationType = autocapitalizationType
|
||||
self.autocorrectionType = autocorrectionType
|
||||
self.keyboardType = keyboardType
|
||||
|
|
@ -186,6 +189,9 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
|||
if lhs.placeholder != rhs.placeholder {
|
||||
return false
|
||||
}
|
||||
if lhs.placeholderDefinesMinHeight != rhs.placeholderDefinesMinHeight {
|
||||
return false
|
||||
}
|
||||
if lhs.autocapitalizationType != rhs.autocapitalizationType {
|
||||
return false
|
||||
}
|
||||
|
|
@ -469,7 +475,20 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
|||
containerSize: CGSize(width: availableSize.width - textFieldRightInset, height: availableSize.height)
|
||||
)
|
||||
|
||||
let size = CGSize(width: availableSize.width, height: textFieldSize.height - 1.0)
|
||||
let placeholderSize = self.placeholder.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: component.placeholder.isEmpty ? " " : component.placeholder, font: Font.regular(17.0), textColor: component.theme.list.itemPlaceholderTextColor)),
|
||||
maximumNumberOfLines: component.placeholderDefinesMinHeight ? 0 : 1
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
|
||||
)
|
||||
|
||||
var size = CGSize(width: availableSize.width, height: textFieldSize.height - 1.0)
|
||||
if component.placeholderDefinesMinHeight {
|
||||
size.height = max(size.height, placeholderSize.height + verticalInset * 2.0 - 1.0)
|
||||
}
|
||||
let textFieldFrame = CGRect(origin: CGPoint(), size: textFieldSize)
|
||||
|
||||
if let textFieldView = self.textField.view {
|
||||
|
|
@ -480,14 +499,6 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
|||
transition.setFrame(view: textFieldView, frame: textFieldFrame)
|
||||
}
|
||||
|
||||
let placeholderSize = self.placeholder.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: component.placeholder.isEmpty ? " " : component.placeholder, font: Font.regular(17.0), textColor: component.theme.list.itemPlaceholderTextColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
|
||||
)
|
||||
let placeholderFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: placeholderSize)
|
||||
if let placeholderView = self.placeholder.view {
|
||||
if placeholderView.superview == nil {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/InteractiveTextComponent",
|
||||
"//submodules/TelegramUI/Components/EmojiStatusComponent",
|
||||
"//submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent",
|
||||
"//submodules/TelegramUI/Components/EntityKeyboard",
|
||||
"//submodules/TelegramNotices",
|
||||
"//submodules/Markdown",
|
||||
"//submodules/TelegramUIPreferences",
|
||||
|
|
@ -46,6 +47,7 @@ swift_library(
|
|||
"//submodules/TextSelectionNode",
|
||||
"//submodules/Pasteboard",
|
||||
"//submodules/Speak",
|
||||
"//submodules/ContextUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import TelegramNotices
|
|||
import Markdown
|
||||
import TelegramUIPreferences
|
||||
import ChatSendMessageActionUI
|
||||
import ContextUI
|
||||
|
||||
final class TextProcessingContentComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
|
@ -49,6 +50,8 @@ final class TextProcessingContentComponent: Component {
|
|||
let translateChat: ((String) -> Void)?
|
||||
let displayLanguageSelectionMenu: (UIView, String, TelegramComposeAIMessageMode.StyleId, Bool, @escaping (String, TelegramComposeAIMessageMode.StyleReference) -> Void) -> Void
|
||||
let newStyleAdded: (TelegramComposeAIMessageMode.CloudStyle) -> Void
|
||||
let styleUpdated: (TelegramComposeAIMessageMode.CloudStyle) -> Void
|
||||
let styleDeleted: (TelegramComposeAIMessageMode.StyleId) -> Void
|
||||
|
||||
init(
|
||||
externalState: ExternalState,
|
||||
|
|
@ -62,7 +65,9 @@ final class TextProcessingContentComponent: Component {
|
|||
copyCurrentResult: (() -> Void)?,
|
||||
translateChat: ((String) -> Void)?,
|
||||
displayLanguageSelectionMenu: @escaping (UIView, String, TelegramComposeAIMessageMode.StyleId, Bool, @escaping (String, TelegramComposeAIMessageMode.StyleReference) -> Void) -> Void,
|
||||
newStyleAdded: @escaping (TelegramComposeAIMessageMode.CloudStyle) -> Void
|
||||
newStyleAdded: @escaping (TelegramComposeAIMessageMode.CloudStyle) -> Void,
|
||||
styleUpdated: @escaping (TelegramComposeAIMessageMode.CloudStyle) -> Void,
|
||||
styleDeleted: @escaping (TelegramComposeAIMessageMode.StyleId) -> Void
|
||||
) {
|
||||
self.externalState = externalState
|
||||
self.styles = styles
|
||||
|
|
@ -76,6 +81,8 @@ final class TextProcessingContentComponent: Component {
|
|||
self.translateChat = translateChat
|
||||
self.displayLanguageSelectionMenu = displayLanguageSelectionMenu
|
||||
self.newStyleAdded = newStyleAdded
|
||||
self.styleUpdated = styleUpdated
|
||||
self.styleDeleted = styleDeleted
|
||||
}
|
||||
|
||||
static func ==(lhs: TextProcessingContentComponent, rhs: TextProcessingContentComponent) -> Bool {
|
||||
|
|
@ -201,6 +208,158 @@ final class TextProcessingContentComponent: Component {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func requestShareStyle(id: TelegramComposeAIMessageMode.StyleReference) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
guard let style = component.styles.first(where: { .style($0.reference.id) == id.id }) else {
|
||||
return
|
||||
}
|
||||
guard let slug = style.slug else {
|
||||
return
|
||||
}
|
||||
let shareController = component.context.sharedContext.makeShareController(context: component.context, params: ShareControllerParams(subject: .url("https://t.me/addstyle/\(slug)")))
|
||||
self.environment?.controller()?.present(shareController, in: .window(.root))
|
||||
}
|
||||
|
||||
private func requestEditStyle(id: TelegramComposeAIMessageMode.StyleReference) {
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
guard let component = self.component, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
guard let style = component.styles.first(where: { .style($0.reference.id) == id.id }) else {
|
||||
return
|
||||
}
|
||||
environment.controller()?.push(await TextStyleEditScreen(
|
||||
context: component.context,
|
||||
mode: .edit(style.cloudStyle),
|
||||
completion: { [weak self] style in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.styleUpdated(style)
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
private func requestDeleteStyle(id: TelegramComposeAIMessageMode.StyleReference) {
|
||||
guard let component = self.component, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
guard let style = component.styles.first(where: { .style($0.reference.id) == id.id }) else {
|
||||
return
|
||||
}
|
||||
guard case let .custom(style) = style.cloudStyle.content else {
|
||||
return
|
||||
}
|
||||
|
||||
environment.controller()?.push(textAlertController(
|
||||
context: component.context,
|
||||
title: "Delete Style",
|
||||
text: "Are you sure you want to delete this style? It will be removed for everyone who installed it.",
|
||||
actions: [
|
||||
TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}),
|
||||
TextAlertAction(type: .destructiveAction, title: environment.strings.Common_Delete, action: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
let _ = component.context.engine.messages.deleteAITextStyle(id: style.id, accessHash: style.accessHash).startStandalone()
|
||||
component.styleDeleted(id.id)
|
||||
}),
|
||||
]
|
||||
))
|
||||
}
|
||||
|
||||
private func openStyleContextMenu(id: TelegramComposeAIMessageMode.StyleReference, gesture: ContextGesture, sourceView: ContextExtractedContentContainingView) {
|
||||
guard let component = self.component, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let style = component.styles.first(where: { .style($0.reference.id) == id.id }) else {
|
||||
return
|
||||
}
|
||||
|
||||
var items: [ContextMenuItem] = []
|
||||
if style.isAuthor {
|
||||
items.append(.action(ContextMenuActionItem(
|
||||
text: "Edit Style",
|
||||
icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor)
|
||||
},
|
||||
action: { [weak self] c, _ in
|
||||
c?.dismiss(completion: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.requestEditStyle(id: id)
|
||||
})
|
||||
})
|
||||
))
|
||||
}
|
||||
items.append(.action(ContextMenuActionItem(
|
||||
text: "Share Style",
|
||||
icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor)
|
||||
},
|
||||
action: { [weak self] c, _ in
|
||||
c?.dismiss(completion: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.requestShareStyle(id: id)
|
||||
})
|
||||
})
|
||||
))
|
||||
if style.isAuthor {
|
||||
items.append(.action(ContextMenuActionItem(
|
||||
text: "Delete Style",
|
||||
textColor: .destructive,
|
||||
icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
|
||||
},
|
||||
action: { [weak self] c, _ in
|
||||
c?.dismiss(completion: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.requestDeleteStyle(id: id)
|
||||
})
|
||||
})
|
||||
))
|
||||
}
|
||||
|
||||
final class ContextExtractedContentSourceImpl: ContextExtractedContentSource {
|
||||
let keepInPlace: Bool = false
|
||||
let ignoreContentTouches: Bool = false
|
||||
let blurBackground: Bool = false
|
||||
|
||||
private let contentView: ContextExtractedContentContainingView
|
||||
|
||||
init(contentView: ContextExtractedContentContainingView) {
|
||||
self.contentView = contentView
|
||||
}
|
||||
|
||||
func takeView() -> ContextControllerTakeViewInfo? {
|
||||
return ContextControllerTakeViewInfo(containingItem: .view(self.contentView), contentAreaInScreenSpace: UIScreen.main.bounds)
|
||||
}
|
||||
|
||||
func putBack() -> ContextControllerPutBackViewInfo? {
|
||||
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
|
||||
}
|
||||
}
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
|
||||
let controller = makeContextController(
|
||||
presentationData: presentationData,
|
||||
source: .extracted(ContextExtractedContentSourceImpl(contentView: sourceView)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture
|
||||
)
|
||||
environment.controller()?.presentInGlobalOverlay(controller)
|
||||
}
|
||||
|
||||
func update(component: TextProcessingContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
|
|
@ -363,8 +522,10 @@ final class TextProcessingContentComponent: Component {
|
|||
mode: .translate(ignoredLanguages: component.ignoredTranslationLanguages),
|
||||
copyAction: component.copyCurrentResult,
|
||||
displayLanguageSelectionMenu: component.displayLanguageSelectionMenu,
|
||||
createStyle: {},
|
||||
editStyle: { _ in },
|
||||
createStyle: {
|
||||
},
|
||||
openStyleContextMenu: { _, _, _ in
|
||||
},
|
||||
present: { [weak self] c, a in
|
||||
self?.environment?.controller()?.present(c, in: .window(.root), with: a)
|
||||
},
|
||||
|
|
@ -384,21 +545,30 @@ final class TextProcessingContentComponent: Component {
|
|||
copyAction: component.copyCurrentResult,
|
||||
displayLanguageSelectionMenu: component.displayLanguageSelectionMenu,
|
||||
createStyle: { [weak self] in
|
||||
guard let self, let component = self.component, let environment = self.environment else {
|
||||
Task { @MainActor in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
guard let component = self.component, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
environment.controller()?.push(await TextStyleEditScreen(
|
||||
context: component.context,
|
||||
mode: .create,
|
||||
completion: { [weak self] style in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.newStyleAdded(style)
|
||||
}
|
||||
))
|
||||
}
|
||||
},
|
||||
openStyleContextMenu: { [weak self] styleId, gesture, sourceView in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
environment.controller()?.push(TextStyleEditScreen(
|
||||
context: component.context,
|
||||
initialText: nil,
|
||||
completion: { [weak self] style in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.newStyleAdded(style)
|
||||
}
|
||||
))
|
||||
},
|
||||
editStyle: { _ in
|
||||
self.openStyleContextMenu(id: styleId, gesture: gesture, sourceView: sourceView)
|
||||
},
|
||||
present: { [weak self] c, a in
|
||||
self?.environment?.controller()?.present(c, in: .window(.root), with: a)
|
||||
|
|
@ -418,8 +588,10 @@ final class TextProcessingContentComponent: Component {
|
|||
mode: .fix,
|
||||
copyAction: component.copyCurrentResult,
|
||||
displayLanguageSelectionMenu: component.displayLanguageSelectionMenu,
|
||||
createStyle: {},
|
||||
editStyle: { _ in },
|
||||
createStyle: {
|
||||
},
|
||||
openStyleContextMenu: { _, _, _ in
|
||||
},
|
||||
present: { [weak self] c, a in
|
||||
self?.environment?.controller()?.present(c, in: .window(.root), with: a)
|
||||
},
|
||||
|
|
@ -875,7 +1047,27 @@ private final class TextProcessingSheetComponent: Component {
|
|||
return
|
||||
}
|
||||
self.styles.append(TextProcessingScreen.Style(cloudStyle: style))
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
},
|
||||
styleUpdated: { [weak self] style in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
guard let index = self.styles.firstIndex(where: { $0.id.id == style.id }) else {
|
||||
return
|
||||
}
|
||||
self.styles[index] = TextProcessingScreen.Style(cloudStyle: style)
|
||||
self.state?.updated(transition: .immediate)
|
||||
},
|
||||
styleDeleted: { [weak self] id in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
guard let index = self.styles.firstIndex(where: { $0.id.id == id }) else {
|
||||
return
|
||||
}
|
||||
self.styles.remove(at: index)
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
)),
|
||||
titleItem: AnyComponent(TitleComponent(
|
||||
|
|
@ -1116,21 +1308,29 @@ public class TextProcessingScreen: ViewControllerComponentContainer {
|
|||
public let title: String
|
||||
public let emojiFileId: Int64?
|
||||
public let emojiFile: TelegramMediaFile?
|
||||
public let isAuthor: Bool
|
||||
public let slug: String?
|
||||
public let cloudStyle: TelegramComposeAIMessageMode.CloudStyle
|
||||
|
||||
public var id: TelegramComposeAIMessageMode.StyleReference {
|
||||
return .style(self.reference)
|
||||
}
|
||||
|
||||
public init(reference: TelegramComposeAIMessageMode.CloudStyle.Reference, title: String, emojiFileId: Int64?, emojiFile: TelegramMediaFile?) {
|
||||
public init(reference: TelegramComposeAIMessageMode.CloudStyle.Reference, title: String, emojiFileId: Int64?, emojiFile: TelegramMediaFile?, isAuthor: Bool, slug: String?, cloudStyle: TelegramComposeAIMessageMode.CloudStyle) {
|
||||
self.reference = reference
|
||||
self.title = title
|
||||
self.emojiFileId = emojiFileId
|
||||
self.emojiFile = emojiFile
|
||||
self.isAuthor = isAuthor
|
||||
self.slug = slug
|
||||
self.cloudStyle = cloudStyle
|
||||
}
|
||||
|
||||
convenience init(cloudStyle: TelegramComposeAIMessageMode.CloudStyle) {
|
||||
let title: String
|
||||
let emojiFileId: Int64?
|
||||
var isAuthor = false
|
||||
var slug: String?
|
||||
switch cloudStyle.content {
|
||||
case let .standard(standard):
|
||||
title = standard.title
|
||||
|
|
@ -1138,12 +1338,17 @@ public class TextProcessingScreen: ViewControllerComponentContainer {
|
|||
case let .custom(custom):
|
||||
title = custom.title
|
||||
emojiFileId = custom.emojiFileId
|
||||
isAuthor = custom.isCreator
|
||||
slug = custom.slug
|
||||
}
|
||||
self.init(
|
||||
reference: cloudStyle.reference,
|
||||
title: title,
|
||||
emojiFileId: emojiFileId,
|
||||
emojiFile: nil
|
||||
emojiFile: nil,
|
||||
isAuthor: isAuthor,
|
||||
slug: slug,
|
||||
cloudStyle: cloudStyle
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1160,6 +1365,9 @@ public class TextProcessingScreen: ViewControllerComponentContainer {
|
|||
if lhs.emojiFile != rhs.emojiFile {
|
||||
return false
|
||||
}
|
||||
if lhs.cloudStyle != rhs.cloudStyle {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -1189,6 +1397,8 @@ public class TextProcessingScreen: ViewControllerComponentContainer {
|
|||
for value in rawStyles {
|
||||
let title: String
|
||||
let emojiFileId: Int64?
|
||||
var isAuthor = false
|
||||
var slug: String?
|
||||
switch value.content {
|
||||
case let .standard(standard):
|
||||
title = standard.title
|
||||
|
|
@ -1196,12 +1406,17 @@ public class TextProcessingScreen: ViewControllerComponentContainer {
|
|||
case let .custom(custom):
|
||||
title = custom.title
|
||||
emojiFileId = custom.emojiFileId
|
||||
isAuthor = custom.isCreator
|
||||
slug = custom.slug
|
||||
}
|
||||
styles.append(Style(
|
||||
reference: value.reference,
|
||||
title: title,
|
||||
emojiFileId: emojiFileId,
|
||||
emojiFile: emojiFileId.flatMap { resolvedEmojiFiles[$0] }
|
||||
emojiFile: emojiFileId.flatMap { resolvedEmojiFiles[$0] },
|
||||
isAuthor: isAuthor,
|
||||
slug: slug,
|
||||
cloudStyle: value
|
||||
))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ final class TextProcessingStyleSelectionComponent: Component {
|
|||
let selectedStyle: TelegramComposeAIMessageMode.StyleId
|
||||
let updateStyle: (TelegramComposeAIMessageMode.StyleReference) -> Void
|
||||
let createStyle: () -> Void
|
||||
let editStyle: (TelegramComposeAIMessageMode.CloudStyle.Id) -> Void
|
||||
let openStyleContextMenu: (TelegramComposeAIMessageMode.StyleReference, ContextGesture, ContextExtractedContentContainingView) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
|
|
@ -27,7 +27,7 @@ final class TextProcessingStyleSelectionComponent: Component {
|
|||
selectedStyle: TelegramComposeAIMessageMode.StyleId,
|
||||
updateStyle: @escaping (TelegramComposeAIMessageMode.StyleReference) -> Void,
|
||||
createStyle: @escaping () -> Void,
|
||||
editStyle: @escaping (TelegramComposeAIMessageMode.CloudStyle.Id) -> Void
|
||||
openStyleContextMenu: @escaping (TelegramComposeAIMessageMode.StyleReference, ContextGesture, ContextExtractedContentContainingView) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
|
|
@ -36,7 +36,7 @@ final class TextProcessingStyleSelectionComponent: Component {
|
|||
self.selectedStyle = selectedStyle
|
||||
self.updateStyle = updateStyle
|
||||
self.createStyle = createStyle
|
||||
self.editStyle = editStyle
|
||||
self.openStyleContextMenu = openStyleContextMenu
|
||||
}
|
||||
|
||||
static func ==(lhs: TextProcessingStyleSelectionComponent, rhs: TextProcessingStyleSelectionComponent) -> Bool {
|
||||
|
|
@ -66,12 +66,19 @@ final class TextProcessingStyleSelectionComponent: Component {
|
|||
private weak var state: EmptyComponentState?
|
||||
private var isUpdating: Bool = false
|
||||
|
||||
private let contextGestureContainerView: ContextControllerSourceView
|
||||
private let scrollView: ScrollView
|
||||
private var itemViews: [TelegramComposeAIMessageMode.StyleId: ComponentView<Empty>] = [:]
|
||||
private let createStyleItemView = ComponentView<Empty>()
|
||||
private let selectedBackgroundView: UIImageView
|
||||
|
||||
private var itemWithActiveContextGesture: TelegramComposeAIMessageMode.StyleId?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.contextGestureContainerView = ContextControllerSourceView()
|
||||
self.contextGestureContainerView.isGestureEnabled = true
|
||||
self.contextGestureContainerView.useSublayerTransformForActivation = true
|
||||
|
||||
self.scrollView = ScrollView()
|
||||
self.selectedBackgroundView = UIImageView()
|
||||
self.selectedBackgroundView.isHidden = true
|
||||
|
|
@ -89,16 +96,79 @@ final class TextProcessingStyleSelectionComponent: Component {
|
|||
self.scrollView.alwaysBounceVertical = false
|
||||
self.scrollView.scrollsToTop = false
|
||||
self.scrollView.clipsToBounds = false
|
||||
self.addSubview(self.scrollView)
|
||||
|
||||
self.addSubview(self.contextGestureContainerView)
|
||||
|
||||
self.scrollView.addSubview(self.selectedBackgroundView)
|
||||
self.contextGestureContainerView.addSubview(self.scrollView)
|
||||
|
||||
self.scrollView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:))))
|
||||
|
||||
self.contextGestureContainerView.shouldBegin = { [weak self] point in
|
||||
guard let self, let component = self.component else {
|
||||
return false
|
||||
}
|
||||
guard let (itemId, itemView) = self.item(at: point) else {
|
||||
return false
|
||||
}
|
||||
guard let item = component.styles.first(where: { .style($0.reference.id) == itemId }) else {
|
||||
return false
|
||||
}
|
||||
guard case .custom = item.reference else {
|
||||
return false
|
||||
}
|
||||
|
||||
self.itemWithActiveContextGesture = itemId
|
||||
self.contextGestureContainerView.targetLayerForActivationProgress = itemView.view?.layer
|
||||
|
||||
let startPoint = point
|
||||
self.contextGestureContainerView.contextGesture?.externalUpdated = { [weak self] _, point in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let dist = sqrt(pow(startPoint.x - point.x, 2.0) + pow(startPoint.y - point.y, 2.0))
|
||||
if dist > 10.0 {
|
||||
self.contextGestureContainerView.contextGesture?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
self.contextGestureContainerView.activated = { [weak self] gesture, _ in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
guard let itemWithActiveContextGesture = self.itemWithActiveContextGesture else {
|
||||
return
|
||||
}
|
||||
|
||||
var itemView: ItemComponent.View?
|
||||
itemView = self.itemViews[itemWithActiveContextGesture]?.view as? ItemComponent.View
|
||||
|
||||
guard let itemView else {
|
||||
return
|
||||
}
|
||||
guard let item = component.styles.first(where: { .style($0.reference.id) == itemWithActiveContextGesture }) else {
|
||||
return
|
||||
}
|
||||
component.openStyleContextMenu(item.id, gesture, itemView.contextContainerView)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
private func item(at point: CGPoint) -> (id: TelegramComposeAIMessageMode.StyleId, itemView: ComponentView<Empty>)? {
|
||||
for (id, itemView) in self.itemViews {
|
||||
if let itemComponentView = itemView.view {
|
||||
if itemComponentView.bounds.contains(self.scrollView.convert(self.convert(point, to: self.scrollView), to: itemComponentView)) {
|
||||
return (id, itemView)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@objc private func onTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
guard let component = self.component else {
|
||||
|
|
@ -211,6 +281,7 @@ final class TextProcessingStyleSelectionComponent: Component {
|
|||
self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize)
|
||||
self.scrollView.contentSize = CGSize(width: contentWidth, height: availableSize.height)
|
||||
self.scrollView.alwaysBounceHorizontal = contentWidth > availableSize.width
|
||||
self.contextGestureContainerView.frame = CGRect(origin: CGPoint(), size: availableSize)
|
||||
|
||||
// Second pass: position items centered in their slots
|
||||
var selectedItemFrame: CGRect?
|
||||
|
|
@ -223,11 +294,16 @@ final class TextProcessingStyleSelectionComponent: Component {
|
|||
let slotOriginX = CGFloat(i) * slotWidth
|
||||
let itemX = slotOriginX + floor((slotWidth - naturalSize.width) * 0.5)
|
||||
let itemFrame = CGRect(origin: CGPoint(x: itemX, y: 0.0), size: naturalSize)
|
||||
if let itemComponentView = itemView.view {
|
||||
if let itemComponentView = itemView.view as? ItemComponent.View {
|
||||
var itemTransition = transition
|
||||
if itemComponentView.superview == nil {
|
||||
self.scrollView.addSubview(itemComponentView)
|
||||
itemTransition = itemTransition.withAnimation(.none)
|
||||
transition.animateScale(view: itemComponentView, from: 0.001, to: 1.0)
|
||||
transition.animateAlpha(view: itemComponentView, from: 0.0, to: 1.0)
|
||||
}
|
||||
transition.setFrame(view: itemComponentView, frame: itemFrame)
|
||||
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
|
||||
itemComponentView.applySize(measuredSize: naturalSize, size: itemFrame.size, transition: itemTransition)
|
||||
}
|
||||
if .style(style.reference.id) == component.selectedStyle {
|
||||
selectedItemFrame = CGRect(origin: CGPoint(x: slotOriginX, y: -5.0), size: CGSize(width: slotWidth, height: availableSize.height + 5.0 + 3.0))
|
||||
|
|
@ -251,7 +327,12 @@ final class TextProcessingStyleSelectionComponent: Component {
|
|||
for (id, itemView) in self.itemViews {
|
||||
if !component.styles.contains(where: { .style($0.reference.id) == id }) {
|
||||
removedIds.append(id)
|
||||
itemView.view?.removeFromSuperview()
|
||||
if let itemComponentView = itemView.view {
|
||||
transition.setAlpha(view: itemComponentView, alpha: 0.0, completion: { [weak itemComponentView] _ in
|
||||
itemComponentView?.removeFromSuperview()
|
||||
})
|
||||
transition.setScale(view: itemComponentView, scale: 0.001)
|
||||
}
|
||||
}
|
||||
}
|
||||
for id in removedIds {
|
||||
|
|
@ -333,6 +414,9 @@ private final class ItemComponent: Component {
|
|||
}
|
||||
|
||||
final class View: UIView {
|
||||
let contextContainerView: ContextExtractedContentContainingView
|
||||
let containerView: UIView
|
||||
|
||||
private var imageIcon: ComponentView<Empty>?
|
||||
private let title = ComponentView<Empty>()
|
||||
|
||||
|
|
@ -340,13 +424,26 @@ private final class ItemComponent: Component {
|
|||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.contextContainerView = ContextExtractedContentContainingView()
|
||||
self.containerView = UIView()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.contextContainerView)
|
||||
self.contextContainerView.contentView.addSubview(self.containerView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func applySize(measuredSize: CGSize, size: CGSize, transition: ComponentTransition) {
|
||||
transition.setFrame(view: self.containerView, frame: measuredSize.centered(in: CGRect(origin: CGPoint(), size: size)))
|
||||
transition.setFrame(view: self.contextContainerView, frame: CGRect(origin: CGPoint(), size: size))
|
||||
transition.setFrame(view: self.contextContainerView.contentView, frame: CGRect(origin: CGPoint(), size: size))
|
||||
self.contextContainerView.contentRect = CGRect(origin: CGPoint(x: 0.0, y: -5.0), size: CGSize(width: size.width, height: size.height + 5.0 + 3.0))
|
||||
}
|
||||
|
||||
func update(component: ItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
let previousComponent = self.component
|
||||
self.component = component
|
||||
|
|
@ -421,7 +518,7 @@ private final class ItemComponent: Component {
|
|||
let iconFrame = CGRect(origin: CGPoint(x: floor((contentWidth - iconSize.width) * 0.5), y: -3.0), size: iconSize)
|
||||
if let imageIconView = imageIcon.view {
|
||||
if imageIconView.superview == nil {
|
||||
self.addSubview(imageIconView)
|
||||
self.containerView.addSubview(imageIconView)
|
||||
}
|
||||
iconTransition.setFrame(view: imageIconView, frame: iconFrame)
|
||||
}
|
||||
|
|
@ -429,12 +526,13 @@ private final class ItemComponent: Component {
|
|||
let titleFrame = CGRect(origin: CGPoint(x: floor((contentWidth - titleSize.width) * 0.5), y: availableSize.height - 5.0 - titleSize.height), size: titleSize)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
self.addSubview(titleView)
|
||||
self.containerView.addSubview(titleView)
|
||||
}
|
||||
titleView.frame = titleFrame
|
||||
}
|
||||
|
||||
return CGSize(width: contentWidth, height: availableSize.height)
|
||||
|
||||
let size = CGSize(width: contentWidth, height: availableSize.height)
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -493,7 +591,7 @@ private final class CreateItemComponent: Component {
|
|||
let iconSize = self.icon.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(BundleIconComponent(
|
||||
name: "Chat/Context Menu/Add",
|
||||
name: "TextProcessing/NewStyle",
|
||||
tintColor: iconTintColor
|
||||
)),
|
||||
environment: {},
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ final class TextProcessingTranslateContentComponent: Component {
|
|||
let copyAction: (() -> Void)?
|
||||
let displayLanguageSelectionMenu: (UIView, String, TelegramComposeAIMessageMode.StyleId, Bool, @escaping (String, TelegramComposeAIMessageMode.StyleReference) -> Void) -> Void
|
||||
let createStyle: () -> Void
|
||||
let editStyle: (TelegramComposeAIMessageMode.CloudStyle.Id) -> Void
|
||||
let openStyleContextMenu: (TelegramComposeAIMessageMode.StyleReference, ContextGesture, ContextExtractedContentContainingView) -> Void
|
||||
let present: (ViewController, Any?) -> Void
|
||||
let rootViewForTextSelection: () -> UIView?
|
||||
|
||||
|
|
@ -120,7 +120,7 @@ final class TextProcessingTranslateContentComponent: Component {
|
|||
copyAction: (() -> Void)?,
|
||||
displayLanguageSelectionMenu: @escaping (UIView, String, TelegramComposeAIMessageMode.StyleId, Bool, @escaping (String, TelegramComposeAIMessageMode.StyleReference) -> Void) -> Void,
|
||||
createStyle: @escaping () -> Void,
|
||||
editStyle: @escaping (TelegramComposeAIMessageMode.CloudStyle.Id) -> Void,
|
||||
openStyleContextMenu: @escaping (TelegramComposeAIMessageMode.StyleReference, ContextGesture, ContextExtractedContentContainingView) -> Void,
|
||||
present: @escaping (ViewController, Any?) -> Void,
|
||||
rootViewForTextSelection: @escaping () -> UIView?
|
||||
) {
|
||||
|
|
@ -134,7 +134,7 @@ final class TextProcessingTranslateContentComponent: Component {
|
|||
self.copyAction = copyAction
|
||||
self.displayLanguageSelectionMenu = displayLanguageSelectionMenu
|
||||
self.createStyle = createStyle
|
||||
self.editStyle = editStyle
|
||||
self.openStyleContextMenu = openStyleContextMenu
|
||||
self.present = present
|
||||
self.rootViewForTextSelection = rootViewForTextSelection
|
||||
}
|
||||
|
|
@ -394,11 +394,11 @@ final class TextProcessingTranslateContentComponent: Component {
|
|||
}
|
||||
component.createStyle()
|
||||
},
|
||||
editStyle: { [weak self] styleId in
|
||||
openStyleContextMenu: { [weak self] styleId, gesture, sourceView in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.editStyle(styleId)
|
||||
component.openStyleContextMenu(styleId, gesture, sourceView)
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ import ListSectionComponent
|
|||
import Markdown
|
||||
import TelegramUIPreferences
|
||||
import ListMultilineTextFieldItemComponent
|
||||
import CheckComponent
|
||||
import PlainButtonComponent
|
||||
import EntityKeyboard
|
||||
import EmojiStatusComponent
|
||||
|
||||
final class TextStyleEditContentComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
|
@ -26,20 +30,22 @@ final class TextStyleEditContentComponent: Component {
|
|||
final class ExternalState {
|
||||
let titleInputState = ListMultilineTextFieldItemComponent.ExternalState()
|
||||
let textInputState = ListMultilineTextFieldItemComponent.ExternalState()
|
||||
var isLinkToProfileEnabled: Bool = false
|
||||
var emojiFile: TelegramMediaFile?
|
||||
}
|
||||
|
||||
let externalState: ExternalState
|
||||
let context: AccountContext
|
||||
let initialText: TextWithEntities?
|
||||
let mode: TextStyleEditScreen.Mode
|
||||
|
||||
init(
|
||||
externalState: ExternalState,
|
||||
context: AccountContext,
|
||||
initialText: TextWithEntities?
|
||||
mode: TextStyleEditScreen.Mode
|
||||
) {
|
||||
self.externalState = externalState
|
||||
self.context = context
|
||||
self.initialText = initialText
|
||||
self.mode = mode
|
||||
}
|
||||
|
||||
static func ==(lhs: TextStyleEditContentComponent, rhs: TextStyleEditContentComponent) -> Bool {
|
||||
|
|
@ -59,9 +65,11 @@ final class TextStyleEditContentComponent: Component {
|
|||
private var isUpdating: Bool = false
|
||||
|
||||
private let iconBackground = ComponentView<Empty>()
|
||||
private let icon = ComponentView<Empty>()
|
||||
private let emptyIcon = ComponentView<Empty>()
|
||||
private var emojiIcon: ComponentView<Empty>?
|
||||
private let titleSection = ComponentView<Empty>()
|
||||
private let textSection = ComponentView<Empty>()
|
||||
private let linkOption = ComponentView<Empty>()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
|
@ -85,7 +93,10 @@ final class TextStyleEditContentComponent: Component {
|
|||
var resetText: String?
|
||||
if self.component == nil {
|
||||
resetTitle = ""
|
||||
resetText = component.initialText?.text
|
||||
if case let .edit(style) = component.mode, case let .custom(style) = style.content {
|
||||
resetTitle = style.title
|
||||
resetText = style.prompt ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
self.component = component
|
||||
|
|
@ -102,10 +113,60 @@ final class TextStyleEditContentComponent: Component {
|
|||
let iconBackgroundSize = CGSize(width: 100.0, height: 100.0)
|
||||
let _ = self.iconBackground.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(FilledRoundedRectangleComponent(
|
||||
color: environment.theme.list.itemBlocksBackgroundColor,
|
||||
cornerRadius: .minEdge,
|
||||
smoothCorners: false
|
||||
component: AnyComponent(PlainButtonComponent(
|
||||
content: AnyComponent(FilledRoundedRectangleComponent(
|
||||
color: environment.theme.list.itemBlocksBackgroundColor,
|
||||
cornerRadius: .minEdge,
|
||||
smoothCorners: false
|
||||
)),
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
guard let iconBackgroundView = self.iconBackground.view else {
|
||||
return
|
||||
}
|
||||
self.environment?.controller()?.present(component.context.sharedContext.makeEmojiStatusSelectionController(
|
||||
context: component.context,
|
||||
mode: .backgroundSelection(completion: { [weak self] file in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.externalState.emojiFile = file
|
||||
self.state?.updated(transition: .immediate)
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
}),
|
||||
sourceView: iconBackgroundView,
|
||||
emojiContent: EmojiPagerContentComponent.emojiInputData(
|
||||
context: component.context,
|
||||
animationCache: component.context.animationCache,
|
||||
animationRenderer: component.context.animationRenderer,
|
||||
isStandalone: false,
|
||||
subject: .emoji,
|
||||
hasTrending: false,
|
||||
topReactionItems: [],
|
||||
areUnicodeEmojiEnabled: false,
|
||||
areCustomEmojiEnabled: true,
|
||||
chatPeerId: component.context.account.peerId,
|
||||
selectedItems: Set()
|
||||
) |> map { $0 },
|
||||
currentSelection: nil,
|
||||
color: nil,
|
||||
destinationItemView: { [weak self] in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
return self.emojiIcon?.view
|
||||
}
|
||||
), in: .window(.root))
|
||||
},
|
||||
animateAlpha: false,
|
||||
animateScale: false,
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: iconBackgroundSize
|
||||
|
|
@ -118,20 +179,73 @@ final class TextStyleEditContentComponent: Component {
|
|||
transition.setFrame(view: iconBackgroundView, frame: iconBackgroundFrame)
|
||||
}
|
||||
|
||||
let iconSize = self.icon.update(
|
||||
let emptyIconSize = self.emptyIcon.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "📝", font: Font.regular(42.0), textColor: .black))
|
||||
component: AnyComponent(BundleIconComponent(
|
||||
name: "TextProcessing/EditEmojiPlaceholder",
|
||||
tintColor: environment.theme.list.controlSecondaryColor
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: iconBackgroundFrame.size
|
||||
)
|
||||
let iconFrame = iconSize.centered(in: iconBackgroundFrame)
|
||||
if let iconView = self.icon.view {
|
||||
if iconView.superview == nil {
|
||||
self.addSubview(iconView)
|
||||
let emptyIconFrame = emptyIconSize.centered(in: iconBackgroundFrame)
|
||||
if let emptyIconView = self.emptyIcon.view {
|
||||
if emptyIconView.superview == nil {
|
||||
self.addSubview(emptyIconView)
|
||||
emptyIconView.isUserInteractionEnabled = false
|
||||
}
|
||||
transition.setFrame(view: emptyIconView, frame: emptyIconFrame)
|
||||
transition.setAlpha(view: emptyIconView, alpha: component.externalState.emojiFile == nil ? 1.0 : 0.0)
|
||||
}
|
||||
|
||||
if let emojiFile = component.externalState.emojiFile {
|
||||
var emojiIconTransition = transition
|
||||
let emojiIcon: ComponentView<Empty>
|
||||
if let current = self.emojiIcon {
|
||||
emojiIcon = current
|
||||
} else {
|
||||
emojiIconTransition = emojiIconTransition.withAnimation(.none)
|
||||
emojiIcon = ComponentView()
|
||||
self.emojiIcon = emojiIcon
|
||||
}
|
||||
let emojiSize = CGSize(width: 70.0, height: 70.0)
|
||||
let emojiIconSize = emojiIcon.update(
|
||||
transition: emojiIconTransition,
|
||||
component: AnyComponent(EmojiStatusComponent(
|
||||
context: component.context,
|
||||
animationCache: component.context.animationCache,
|
||||
animationRenderer: component.context.animationRenderer,
|
||||
content: .animation(
|
||||
content: .file(file: emojiFile),
|
||||
size: emojiSize,
|
||||
placeholderColor: environment.theme.list.mediaPlaceholderColor,
|
||||
themeColor: environment.theme.list.itemPrimaryTextColor,
|
||||
loopMode: .count(1)
|
||||
),
|
||||
isVisibleForAnimations: true,
|
||||
action: nil
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: emojiSize
|
||||
)
|
||||
let emojiIconFrame = emojiIconSize.centered(in: iconBackgroundFrame)
|
||||
if let emojiIconView = emojiIcon.view {
|
||||
if emojiIconView.superview == nil {
|
||||
self.addSubview(emojiIconView)
|
||||
emojiIconView.isUserInteractionEnabled = false
|
||||
}
|
||||
emojiIconTransition.setFrame(view: emojiIconView, frame: emojiIconFrame)
|
||||
}
|
||||
} else {
|
||||
if let emojiIcon = self.emojiIcon {
|
||||
self.emojiIcon = nil
|
||||
if let emojiIconView = emojiIcon.view {
|
||||
transition.setAlpha(view: emojiIconView, alpha: 0.0, completion: { [weak emojiIconView] _ in
|
||||
emojiIconView?.removeFromSuperview()
|
||||
})
|
||||
transition.setScale(view: emojiIconView, scale: 0.001)
|
||||
}
|
||||
}
|
||||
transition.setFrame(view: iconView, frame: iconFrame)
|
||||
}
|
||||
|
||||
contentHeight += iconBackgroundSize.height + iconSpacing
|
||||
|
|
@ -148,7 +262,7 @@ final class TextStyleEditContentComponent: Component {
|
|||
resetText: resetTitle.flatMap { resetTitle in
|
||||
return ListMultilineTextFieldItemComponent.ResetText(value: resetTitle)
|
||||
},
|
||||
placeholder: "Title",
|
||||
placeholder: "Style Name (for example, \"Pirate\")",
|
||||
autocapitalizationType: .none,
|
||||
autocorrectionType: .no,
|
||||
characterLimit: 256,
|
||||
|
|
@ -192,7 +306,8 @@ final class TextStyleEditContentComponent: Component {
|
|||
resetText: resetText.flatMap { resetText in
|
||||
return ListMultilineTextFieldItemComponent.ResetText(value: resetText)
|
||||
},
|
||||
placeholder: "Text",
|
||||
placeholder: "Instructions (for example, \"Write like a swashbuckling pirate. Use arr, ye, matey, and talk about treasure, the sea, and rum\")",
|
||||
placeholderDefinesMinHeight: true,
|
||||
autocapitalizationType: .none,
|
||||
autocorrectionType: .no,
|
||||
characterLimit: 4096,
|
||||
|
|
@ -222,8 +337,57 @@ final class TextStyleEditContentComponent: Component {
|
|||
transition.setFrame(view: textSectionView, frame: textSectionFrame)
|
||||
}
|
||||
contentHeight += textSectionSize.height
|
||||
contentHeight += 23.0
|
||||
|
||||
contentHeight += 106.0
|
||||
let checkTheme = CheckComponent.Theme(
|
||||
backgroundColor: environment.theme.list.itemCheckColors.fillColor,
|
||||
strokeColor: environment.theme.list.itemCheckColors.foregroundColor,
|
||||
borderColor: environment.theme.list.itemCheckColors.strokeColor,
|
||||
overlayBorder: false,
|
||||
hasInset: false,
|
||||
hasShadow: false
|
||||
)
|
||||
let linkOptionSize = self.linkOption.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(PlainButtonComponent(
|
||||
content: AnyComponent(HStack([
|
||||
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(CheckComponent(
|
||||
theme: checkTheme,
|
||||
size: CGSize(width: 18.0, height: 18.0),
|
||||
selected: component.externalState.isLinkToProfileEnabled
|
||||
))),
|
||||
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "Add a link to my account", font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor))
|
||||
)))
|
||||
], spacing: 10.0)),
|
||||
effectAlignment: .center,
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
component.externalState.isLinkToProfileEnabled = !component.externalState.isLinkToProfileEnabled
|
||||
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
},
|
||||
animateAlpha: false,
|
||||
animateScale: false
|
||||
)),
|
||||
environment: {
|
||||
},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
|
||||
)
|
||||
let linkOptionFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - linkOptionSize.width) * 0.5), y: contentHeight), size: linkOptionSize)
|
||||
if let linkOptionView = self.linkOption.view {
|
||||
if linkOptionView.superview == nil {
|
||||
self.addSubview(linkOptionView)
|
||||
}
|
||||
transition.setFrame(view: linkOptionView, frame: linkOptionFrame)
|
||||
}
|
||||
contentHeight += linkOptionSize.height
|
||||
|
||||
contentHeight += 104.0
|
||||
|
||||
let _ = alphaTransition
|
||||
|
||||
|
|
@ -244,16 +408,19 @@ private final class TextStyleEditSheetComponent: Component {
|
|||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let initialText: TextWithEntities?
|
||||
let mode: TextStyleEditScreen.Mode
|
||||
let initialEmojiFile: TelegramMediaFile?
|
||||
let completion: (TelegramComposeAIMessageMode.CloudStyle) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
initialText: TextWithEntities?,
|
||||
mode: TextStyleEditScreen.Mode,
|
||||
initialEmojiFile: TelegramMediaFile?,
|
||||
completion: @escaping (TelegramComposeAIMessageMode.CloudStyle) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.initialText = initialText
|
||||
self.mode = mode
|
||||
self.initialEmojiFile = initialEmojiFile
|
||||
self.completion = completion
|
||||
}
|
||||
|
||||
|
|
@ -302,36 +469,78 @@ private final class TextStyleEditSheetComponent: Component {
|
|||
if self.contentState.titleInputState.text.string.isEmpty || self.contentState.textInputState.text.string.isEmpty {
|
||||
return
|
||||
}
|
||||
guard let emojiFile = self.contentState.emojiFile else {
|
||||
return
|
||||
}
|
||||
|
||||
self.createDisposable?.dispose()
|
||||
self.createDisposable = (component.context.engine.messages.createAITextStyle(
|
||||
displayAuthor: false,
|
||||
emojiFileId: nil,
|
||||
title: self.contentState.titleInputState.text.string,
|
||||
prompt: self.contentState.textInputState.text.string
|
||||
)
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] result in
|
||||
guard let self, let component = self.component, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
let controller = environment.controller
|
||||
|
||||
self.animateOut.invoke(Action { _ in
|
||||
if let controller = controller() {
|
||||
controller.dismiss(completion: nil)
|
||||
|
||||
switch component.mode {
|
||||
case .create:
|
||||
self.createDisposable = (component.context.engine.messages.createAITextStyle(
|
||||
displayAuthor: self.contentState.isLinkToProfileEnabled,
|
||||
emojiFileId: emojiFile.fileId.id,
|
||||
title: self.contentState.titleInputState.text.string,
|
||||
prompt: self.contentState.textInputState.text.string
|
||||
)
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] result in
|
||||
guard let self, let component = self.component, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
let controller = environment.controller
|
||||
|
||||
self.animateOut.invoke(Action { _ in
|
||||
if let controller = controller() {
|
||||
controller.dismiss(completion: nil)
|
||||
}
|
||||
})
|
||||
|
||||
component.completion(result)
|
||||
}, error: { [weak self] error in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = self
|
||||
})
|
||||
|
||||
component.completion(result)
|
||||
}, error: { [weak self] error in
|
||||
guard let self else {
|
||||
case let .edit(style):
|
||||
guard case let .custom(style) = style.content else {
|
||||
return
|
||||
}
|
||||
let _ = self
|
||||
})
|
||||
self.createDisposable = (component.context.engine.messages.editAITextStyle(
|
||||
id: style.id,
|
||||
accessHash: style.accessHash,
|
||||
displayAuthor: self.contentState.isLinkToProfileEnabled,
|
||||
emojiFileId: emojiFile.fileId.id,
|
||||
title: self.contentState.titleInputState.text.string,
|
||||
prompt: self.contentState.textInputState.text.string
|
||||
)
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] result in
|
||||
guard let self, let component = self.component, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
let controller = environment.controller
|
||||
|
||||
self.animateOut.invoke(Action { _ in
|
||||
if let controller = controller() {
|
||||
controller.dismiss(completion: nil)
|
||||
}
|
||||
})
|
||||
|
||||
component.completion(result)
|
||||
}, error: { [weak self] error in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = self
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: TextStyleEditSheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||
if self.component == nil {
|
||||
self.contentState.emojiFile = component.initialEmojiFile
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
|
|
@ -360,10 +569,17 @@ private final class TextStyleEditSheetComponent: Component {
|
|||
}
|
||||
self.performCreateStyle()
|
||||
}
|
||||
let isMainActionEnabled = self.contentState.titleInputState.hasText && self.contentState.textInputState.hasText
|
||||
let actionButtonTitle: String = "Create"
|
||||
|
||||
let titleString: String = "New Style"
|
||||
let isMainActionEnabled = self.contentState.titleInputState.hasText && self.contentState.textInputState.hasText && self.contentState.emojiFile != nil
|
||||
let actionButtonTitle: String
|
||||
let titleString: String
|
||||
switch component.mode {
|
||||
case .create:
|
||||
titleString = "New Style"
|
||||
actionButtonTitle = "Create"
|
||||
case .edit:
|
||||
titleString = "Edit Style"
|
||||
actionButtonTitle = "Save"
|
||||
}
|
||||
|
||||
let sheetSize = self.sheet.update(
|
||||
transition: transition,
|
||||
|
|
@ -371,7 +587,7 @@ private final class TextStyleEditSheetComponent: Component {
|
|||
content: AnyComponent<ViewControllerComponentContainer.Environment>(TextStyleEditContentComponent(
|
||||
externalState: self.contentState,
|
||||
context: component.context,
|
||||
initialText: component.initialText
|
||||
mode: component.mode
|
||||
)),
|
||||
titleItem: AnyComponent(TitleComponent(
|
||||
theme: theme,
|
||||
|
|
@ -448,21 +664,32 @@ private final class TextStyleEditSheetComponent: Component {
|
|||
}
|
||||
|
||||
public class TextStyleEditScreen: ViewControllerComponentContainer {
|
||||
public enum Mode {
|
||||
case create
|
||||
case edit(TelegramComposeAIMessageMode.CloudStyle)
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme? = nil,
|
||||
initialText: TextWithEntities?,
|
||||
mode: Mode,
|
||||
completion: @escaping (TelegramComposeAIMessageMode.CloudStyle) -> Void
|
||||
) {
|
||||
) async {
|
||||
self.context = context
|
||||
|
||||
var initialEmojiFile: TelegramMediaFile?
|
||||
if case let .edit(style) = mode, case let .custom(style) = style.content, let emojiFileId = style.emojiFileId {
|
||||
initialEmojiFile = await context.engine.stickers.resolveInlineStickers(fileIds: [emojiFileId]).get()[emojiFileId]
|
||||
}
|
||||
|
||||
super.init(
|
||||
context: context,
|
||||
component: TextStyleEditSheetComponent(
|
||||
context: context,
|
||||
initialText: initialText,
|
||||
mode: mode,
|
||||
initialEmojiFile: initialEmojiFile,
|
||||
completion: completion
|
||||
),
|
||||
navigationBarAppearance: .none,
|
||||
|
|
|
|||
12
submodules/TelegramUI/Images.xcassets/TextProcessing/EditEmojiPlaceholder.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/TextProcessing/EditEmojiPlaceholder.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "setemoji.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/TextProcessing/EditEmojiPlaceholder.imageset/setemoji.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/TextProcessing/EditEmojiPlaceholder.imageset/setemoji.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/TextProcessing/NewStyle.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/TextProcessing/NewStyle.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "addstyle.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/TextProcessing/NewStyle.imageset/addstyle.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/TextProcessing/NewStyle.imageset/addstyle.pdf
vendored
Normal file
Binary file not shown.
|
|
@ -103,6 +103,8 @@ import PollStatsScreen
|
|||
import RankChatPreviewItem
|
||||
import TextProcessingScreen
|
||||
import CreateBotScreen
|
||||
import EmojiStatusSelectionComponent
|
||||
import EntityKeyboard
|
||||
|
||||
private final class AccountUserInterfaceInUseContext {
|
||||
let subscribers = Bag<(Bool) -> Void>()
|
||||
|
|
@ -4415,6 +4417,18 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||
completion: completion
|
||||
)
|
||||
}
|
||||
|
||||
public func makeEmojiStatusSelectionController(context: AccountContext, mode: EmojiStatusSelectionControllerMode, sourceView: UIView, emojiContent: Signal<AnyObject, NoError>, currentSelection: Int64?, color: UIColor?, destinationItemView: @escaping () -> UIView?) -> ViewController {
|
||||
return EmojiStatusSelectionController(
|
||||
context: context,
|
||||
mode: mode,
|
||||
sourceView: sourceView,
|
||||
emojiContent: emojiContent |> map { $0 as! EmojiPagerContentComponent },
|
||||
currentSelection: currentSelection,
|
||||
color: color,
|
||||
destinationItemView: destinationItemView
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func peerInfoControllerImpl(context: AccountContext, updatedPresentationData: (PresentationData, Signal<PresentationData, NoError>)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, requestsContext: PeerInvitationImportersContext? = nil) -> ViewController? {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue