diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 589bbbca27..77d9e73f0d 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -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, currentSelection: Int64?, color: UIColor?, destinationItemView: @escaping () -> UIView?) -> ViewController func navigateToCurrentCall() var hasOngoingCall: ValuePromise { get } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift index 9bb1a15fdd..a49e4bc5f4 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index 333178e1de..54ae06af67 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift index 12e5aa8f2e..44b3ad59a6 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -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 } diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift index 9c2f87ad96..45f06a2c75 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -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? diff --git a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift index d7b8b56750..b83ddbbc6c 100644 --- a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift +++ b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift @@ -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 diff --git a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift index 8be6a995f9..ca3ab4dff2 100644 --- a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift +++ b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/TextProcessingScreen/BUILD b/submodules/TelegramUI/Components/TextProcessingScreen/BUILD index 4a69781df2..01d69d5704 100644 --- a/submodules/TelegramUI/Components/TextProcessingScreen/BUILD +++ b/submodules/TelegramUI/Components/TextProcessingScreen/BUILD @@ -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", diff --git a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingScreen.swift b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingScreen.swift index 4bd22bd48e..22202abc74 100644 --- a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingScreen.swift +++ b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingScreen.swift @@ -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, 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 )) } diff --git a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingStyleSelectionComponent.swift b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingStyleSelectionComponent.swift index 926ca69b2b..4db0920e1b 100644 --- a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingStyleSelectionComponent.swift +++ b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingStyleSelectionComponent.swift @@ -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] = [:] private let createStyleItemView = ComponentView() 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)? { + 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? private let title = ComponentView() @@ -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, 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: {}, diff --git a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingTranslateContentComponent.swift b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingTranslateContentComponent.swift index e24f1ed861..609dbfc736 100644 --- a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingTranslateContentComponent.swift +++ b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingTranslateContentComponent.swift @@ -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: {}, diff --git a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextStyleEditScreen.swift b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextStyleEditScreen.swift index 5be2143797..5de58b7c19 100644 --- a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextStyleEditScreen.swift +++ b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextStyleEditScreen.swift @@ -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() - private let icon = ComponentView() + private let emptyIcon = ComponentView() + private var emojiIcon: ComponentView? private let titleSection = ComponentView() private let textSection = ComponentView() + private let linkOption = ComponentView() 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 + 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, 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(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, diff --git a/submodules/TelegramUI/Images.xcassets/TextProcessing/EditEmojiPlaceholder.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/TextProcessing/EditEmojiPlaceholder.imageset/Contents.json new file mode 100644 index 0000000000..9d000d735b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/TextProcessing/EditEmojiPlaceholder.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "setemoji.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/TextProcessing/EditEmojiPlaceholder.imageset/setemoji.pdf b/submodules/TelegramUI/Images.xcassets/TextProcessing/EditEmojiPlaceholder.imageset/setemoji.pdf new file mode 100644 index 0000000000..92143fac3b Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/TextProcessing/EditEmojiPlaceholder.imageset/setemoji.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/TextProcessing/NewStyle.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/TextProcessing/NewStyle.imageset/Contents.json new file mode 100644 index 0000000000..d313636cad --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/TextProcessing/NewStyle.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "addstyle.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/TextProcessing/NewStyle.imageset/addstyle.pdf b/submodules/TelegramUI/Images.xcassets/TextProcessing/NewStyle.imageset/addstyle.pdf new file mode 100644 index 0000000000..3565e78849 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/TextProcessing/NewStyle.imageset/addstyle.pdf differ diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 556eaa3b52..9e0c754208 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -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, 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)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, requestsContext: PeerInvitationImportersContext? = nil) -> ViewController? {