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:
Isaac 2026-04-18 15:49:56 +02:00
parent d01bbc6529
commit 684796144c
17 changed files with 723 additions and 110 deletions

View file

@ -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 }

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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?

View file

@ -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

View file

@ -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 {

View file

@ -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",

View file

@ -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
))
}

View file

@ -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: {},

View file

@ -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: {},

View file

@ -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,

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "setemoji.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "addstyle.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -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? {