From a2d5c530a5d1e15aeb89de696e07ff137c2f675c Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Wed, 18 Mar 2026 00:27:13 +0100 Subject: [PATCH] Various improvements --- .../Sources/AttachmentPanel.swift | 1 + .../ChatPanelInterfaceInteraction.swift | 4 + .../Sources/ResizableSheetComponent.swift | 6 + submodules/Display/Source/TextNode.swift | 137 ++- .../Sources/TabBarContollerNode.swift | 2 +- submodules/TelegramApi/Sources/Api0.swift | 8 +- submodules/TelegramApi/Sources/Api16.swift | 111 +++ submodules/TelegramApi/Sources/Api32.swift | 16 +- submodules/TelegramApi/Sources/Api34.swift | 173 ++-- submodules/TelegramApi/Sources/Api35.swift | 200 +++-- submodules/TelegramApi/Sources/Api36.swift | 292 ++----- submodules/TelegramApi/Sources/Api37.swift | 413 +++++---- submodules/TelegramApi/Sources/Api38.swift | 422 ++++----- submodules/TelegramApi/Sources/Api39.swift | 210 +++++ submodules/TelegramApi/Sources/Api40.swift | 26 +- .../ApiUtils/StoreMessage_Telegram.swift | 2 + ...yncCore_TextEntitiesMessageAttribute.swift | 10 + .../Messages/TelegramEngineMessages.swift | 4 + .../TelegramEngine/Messages/Translate.swift | 125 +++ .../TelegramEngine/Peers/AddPeerMember.swift | 4 +- submodules/TelegramUI/BUILD | 1 + .../Sources/ChatRecentActionsController.swift | 1 + .../Sources/ChatTextInputPanelComponent.swift | 4 +- .../Sources/ChatTextInputPanelNode.swift | 54 ++ .../HeaderPanelContainerComponent.swift | 2 +- .../Sources/HorizontalTabsComponent.swift | 2 +- .../Sources/PeerInfoSelectionPanelNode.swift | 1 + .../Sources/PeerSelectionControllerNode.swift | 1 + .../Sources/TabBarComponent.swift | 434 +++++++--- .../Components/TextProcessingScreen/BUILD | 40 + ...ProcessingLanguageSelectionComponent.swift | 805 ++++++++++++++++++ .../Sources/TextProcessingScreen.swift | 722 ++++++++++++++++ ...extProcessingStyleSelectionComponent.swift | 273 ++++++ .../TextProcessingTextAreaComponent.swift | 532 ++++++++++++ ...tProcessingTranslateContentComponent.swift | 460 ++++++++++ .../Text/InputAIIcon.imageset/Contents.json | 12 + .../Input/Text/InputAIIcon.imageset/ai.pdf | Bin 0 -> 6317 bytes .../TextProcessing/Contents.json | 9 + .../TabFix.imageset/Contents.json | 12 + .../TextProcessing/TabFix.imageset/fix.pdf | Bin 0 -> 5854 bytes .../TabStylize.imageset/Contents.json | 12 + .../TabStylize.imageset/style.pdf | Bin 0 -> 7152 bytes .../TabTranslate.imageset/Contents.json | 12 + .../TabTranslate.imageset/translate.pdf | Bin 0 -> 6193 bytes .../Resources/Animations/SparklesEmoji.tgs | Bin 0 -> 8368 bytes .../Chat/ChatControllerLoadDisplayNode.swift | 5 + .../Sources/ChatControllerNode.swift | 55 +- .../ChatInterfaceStateInputPanels.swift | 1 + .../Sources/MusicListenTracker.swift | 4 +- 49 files changed, 4592 insertions(+), 1028 deletions(-) create mode 100644 submodules/TelegramUI/Components/TextProcessingScreen/BUILD create mode 100644 submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingLanguageSelectionComponent.swift create mode 100644 submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingScreen.swift create mode 100644 submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingStyleSelectionComponent.swift create mode 100644 submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingTextAreaComponent.swift create mode 100644 submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingTranslateContentComponent.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Text/InputAIIcon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Text/InputAIIcon.imageset/ai.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/TextProcessing/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/TextProcessing/TabFix.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/TextProcessing/TabFix.imageset/fix.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/TextProcessing/TabStylize.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/TextProcessing/TabStylize.imageset/style.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/TextProcessing/TabTranslate.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/TextProcessing/TabTranslate.imageset/translate.pdf create mode 100644 submodules/TelegramUI/Resources/Animations/SparklesEmoji.tgs diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index df96d89e41..bb392c27d3 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -1419,6 +1419,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog }, displayUndo: { _ in }, presentInputTextTranslation: { _, _ in }, sendEmoji: { _, _, _ in + }, openAICompose: { }, updateHistoryFilter: { _ in }, updateChatLocationThread: { _, _ in }, toggleChatSidebarMode: { diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift index b1c4d4f065..79933d161e 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift @@ -197,6 +197,7 @@ public final class ChatPanelInterfaceInteraction { public let displayUndo: (UndoOverlayContent) -> Void public let presentInputTextTranslation: (NSAttributedString, @escaping (NSAttributedString) -> Void) -> Void public let sendEmoji: (String, ChatTextInputTextCustomEmojiAttribute, Bool) -> Void + public let openAICompose: () -> Void public let requestLayout: (ContainedViewLayoutTransition) -> Void public let chatController: () -> ViewController? public let statuses: ChatPanelInterfaceInteractionStatuses? @@ -324,6 +325,7 @@ public final class ChatPanelInterfaceInteraction { displayUndo: @escaping (UndoOverlayContent) -> Void, presentInputTextTranslation: @escaping (NSAttributedString, @escaping (NSAttributedString) -> Void) -> Void, sendEmoji: @escaping (String, ChatTextInputTextCustomEmojiAttribute, Bool) -> Void, + openAICompose: @escaping () -> Void, updateHistoryFilter: @escaping ((ChatPresentationInterfaceState.HistoryFilter?) -> ChatPresentationInterfaceState.HistoryFilter?) -> Void, updateChatLocationThread: @escaping (Int64?, ChatControllerAnimateInnerChatSwitchDirection?) -> Void, toggleChatSidebarMode: @escaping () -> Void, @@ -454,6 +456,7 @@ public final class ChatPanelInterfaceInteraction { self.displayUndo = displayUndo self.presentInputTextTranslation = presentInputTextTranslation self.sendEmoji = sendEmoji + self.openAICompose = openAICompose self.updateHistoryFilter = updateHistoryFilter self.updateChatLocationThread = updateChatLocationThread self.toggleChatSidebarMode = toggleChatSidebarMode @@ -592,6 +595,7 @@ public final class ChatPanelInterfaceInteraction { }, displayUndo: { _ in }, presentInputTextTranslation: { _, _ in }, sendEmoji: { _, _, _ in + }, openAICompose: { }, updateHistoryFilter: { _ in }, updateChatLocationThread: { _, _ in }, toggleChatSidebarMode: { diff --git a/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift b/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift index 8ff5b544d2..22bed1cab9 100644 --- a/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift +++ b/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift @@ -460,6 +460,12 @@ public final class ResizableSheetComponent CGRect { @@ -1568,8 +1573,8 @@ open class TextNode: ASDisplayNode, TextNodeProtocol { size.height += line.frame.height + line.frame.height * lineSpacingFactor blockWidth = max(blockWidth, line.frame.origin.x + line.frame.width) - if let range = line.range { - attributedString.enumerateAttributes(in: range, options: []) { attributes, range, _ in + if let lineRange = line.range { + attributedString.enumerateAttributes(in: lineRange, options: []) { attributes, range, _ in if attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] != nil || attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] != nil { var ascent: CGFloat = 0.0 var descent: CGFloat = 0.0 @@ -1600,10 +1605,11 @@ open class TextNode: ASDisplayNode, TextNodeProtocol { addSpoiler(line: line, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) } else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] { + let clampedEnd = max(range.location, min(lineRange.location + lineRange.length, range.location + range.length)) let lowerX = floor(CTLineGetOffsetForStringIndex(line.line, range.location, nil)) - let upperX = ceil(CTLineGetOffsetForStringIndex(line.line, range.location + range.length, nil)) + let upperX = ceil(CTLineGetOffsetForStringIndex(line.line, clampedEnd, nil)) let x = lowerX < upperX ? lowerX : upperX - line.strikethroughs.append(TextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: line.frame.height))) + line.strikethroughs.append(TextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: line.frame.height), color: nil, style: .single)) } if let embeddedItem = (attributes[NSAttributedString.Key(rawValue: "TelegramEmbeddedItem")] as? AnyHashable ?? attributes[NSAttributedString.Key(rawValue: "Attribute__EmbeddedItem")] as? AnyHashable) { @@ -2041,44 +2047,47 @@ open class TextNode: ASDisplayNode, TextNodeProtocol { addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) } else if let _ = attributes[NSAttributedString.Key(rawValue: "TelegramBackground")] { + let clampedEnd = max(range.location, min(brokenLineRange.location + brokenLineRange.length, range.location + range.length)) let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil)) - let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil)) + let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, clampedEnd, nil)) let x = lowerX < upperX ? lowerX : upperX - backgrounds.append(TextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: fontLineHeight))) + backgrounds.append(TextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: fontLineHeight), color: nil, style: .single)) } else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] { + let clampedEnd = max(range.location, min(brokenLineRange.location + brokenLineRange.length, range.location + range.length)) let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil)) - let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil)) + let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, clampedEnd, nil)) let x = lowerX < upperX ? lowerX : upperX - strikethroughs.append(TextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: fontLineHeight))) - } else if let _ = attributes[NSAttributedString.Key.underlineStyle] { + strikethroughs.append(TextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: fontLineHeight), color: nil, style: .single)) + } else if let underlineStyle = attributes[NSAttributedString.Key.underlineStyle] as? Int { + let clampedEnd = max(range.location, min(brokenLineRange.location + brokenLineRange.length, range.location + range.length)) let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil)) - let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil)) + let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, clampedEnd, nil)) let x = lowerX < upperX ? lowerX : upperX - underlines.append(TextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: fontLineHeight))) + underlines.append(TextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: fontLineHeight), color: attributes[NSAttributedString.Key.underlineColor] as? UIColor, style: underlineStyle == NSUnderlineStyle.patternDot.rawValue ? .wavy : .single)) } else if let paragraphStyle = attributes[NSAttributedString.Key.paragraphStyle] as? NSParagraphStyle { headIndent = paragraphStyle.headIndent } - + if let embeddedItem = (attributes[NSAttributedString.Key(rawValue: "TelegramEmbeddedItem")] as? AnyHashable ?? attributes[NSAttributedString.Key(rawValue: "Attribute__EmbeddedItem")] as? AnyHashable) { if displayEmbeddedItemsUnderSpoilers || (attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] == nil && attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] == nil) { var ascent: CGFloat = 0.0 var descent: CGFloat = 0.0 CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) - + addEmbeddedItem(item: embeddedItem, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) } } - + if let attachment = attributes[NSAttributedString.Key.attachment] as? UIImage { var ascent: CGFloat = 0.0 var descent: CGFloat = 0.0 CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) - + addAttachment(attachment: attachment, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: max(range.location, min(lineRange.location + lineRange.length, range.location + range.length)), isAtEndOfTheLine: range.location + range.length >= lineRange.location + lineRange.length - 1) } } } - + var lineAscent: CGFloat = 0.0 var lineDescent: CGFloat = 0.0 let lineWidth = min(lineConstrainedSize.width, ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, &lineAscent, &lineDescent, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine)))) @@ -2168,39 +2177,42 @@ open class TextNode: ASDisplayNode, TextNodeProtocol { addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) } else if let _ = attributes[NSAttributedString.Key(rawValue: "TelegramBackground")] { + let clampedEnd = max(range.location, min(lineRange.location + lineRange.length, range.location + range.length)) let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil)) - let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil)) + let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, clampedEnd, nil)) let x = lowerX < upperX ? lowerX : upperX - backgrounds.append(TextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: fontLineHeight))) + backgrounds.append(TextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: fontLineHeight), color: nil, style: .single)) } else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] { + let clampedEnd = max(range.location, min(lineRange.location + lineRange.length, range.location + range.length)) let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil)) - let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil)) + let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, clampedEnd, nil)) let x = lowerX < upperX ? lowerX : upperX - strikethroughs.append(TextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: fontLineHeight))) - } else if let _ = attributes[NSAttributedString.Key.underlineStyle] { + strikethroughs.append(TextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: fontLineHeight), color: nil, style: .single)) + } else if let underlineStyle = attributes[NSAttributedString.Key.underlineStyle] as? Int { + let clampedEnd = max(range.location, min(lineRange.location + lineRange.length, range.location + range.length)) let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil)) - let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil)) + let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, clampedEnd, nil)) let x = lowerX < upperX ? lowerX : upperX - underlines.append(TextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: fontLineHeight))) + underlines.append(TextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: fontLineHeight), color: attributes[NSAttributedString.Key.underlineColor] as? UIColor, style: underlineStyle == NSUnderlineStyle.patternDot.rawValue ? .wavy : .single)) } else if let paragraphStyle = attributes[NSAttributedString.Key.paragraphStyle] as? NSParagraphStyle { headIndent = paragraphStyle.headIndent } - + if let embeddedItem = (attributes[NSAttributedString.Key(rawValue: "TelegramEmbeddedItem")] as? AnyHashable ?? attributes[NSAttributedString.Key(rawValue: "Attribute__EmbeddedItem")] as? AnyHashable) { if displayEmbeddedItemsUnderSpoilers || (attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] == nil && attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] == nil) { var ascent: CGFloat = 0.0 var descent: CGFloat = 0.0 CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) - + addEmbeddedItem(item: embeddedItem, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) } } - + if let attachment = attributes[NSAttributedString.Key.attachment] as? UIImage { var ascent: CGFloat = 0.0 var descent: CGFloat = 0.0 CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) - + addAttachment(attachment: attachment, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: max(range.location, min(lineRange.location + lineRange.length, range.location + range.length)), isAtEndOfTheLine: range.location + range.length >= lineRange.location + lineRange.length - 1) } } @@ -2605,22 +2617,59 @@ open class TextNode: ASDisplayNode, TextNodeProtocol { } if drawUnderlinesManually { - if !line.underlines.isEmpty { - for strikethrough in line.underlines { - guard let lineRange = line.range else { - continue + for strikethrough in line.underlines { + guard let lineRange = line.range else { + continue + } + var textColor: UIColor? + layout.attributedString?.enumerateAttributes(in: NSMakeRange(lineRange.location, lineRange.length), options: []) { attributes, range, _ in + if range == strikethrough.range, let color = attributes[NSAttributedString.Key.foregroundColor] as? UIColor { + textColor = color } - var textColor: UIColor? - layout.attributedString?.enumerateAttributes(in: NSMakeRange(lineRange.location, lineRange.length), options: []) { attributes, range, _ in - if range == strikethrough.range, let color = attributes[NSAttributedString.Key.foregroundColor] as? UIColor { - textColor = color - } - } - if let textColor = textColor { + } + switch strikethrough.style { + case .single: + if let color = strikethrough.color { + context.setFillColor(color.cgColor) + } else if let textColor { context.setFillColor(textColor.cgColor) } let frame = strikethrough.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY) context.fill(CGRect(x: frame.minX, y: frame.minY + 1.0, width: frame.width, height: 1.0)) + case .wavy: + if let color = strikethrough.color { + context.setStrokeColor(color.cgColor) + } else if let textColor { + context.setStrokeColor(textColor.cgColor) + } + context.setLineWidth(1.33) + context.setLineCap(.round) + context.setLineJoin(.round) + let frame = strikethrough.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY - 6.0) + + let amplitude: CGFloat = 1.2 + let period: CGFloat = 8.0 + let phase: CGFloat = -0.5 + let midY = frame.midY + let step: CGFloat = 1.0 + + context.saveGState() + context.clip(to: frame) + + var x = frame.minX + context.move(to: CGPoint(x: x, y: midY + amplitude * sin(phase))) + x += step + while x <= frame.maxX + step { + let y = midY + amplitude * sin((x - frame.minX) * 2.0 * .pi / period + phase) + context.addLine(to: CGPoint(x: x, y: y)) + x += step + } + context.strokePath() + context.restoreGState() + + /*context.setFillColor(UIColor.red.cgColor) + let frame1 = strikethrough.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY) + context.fill(CGRect(x: frame1.minX, y: frame1.minY + 1.0, width: frame1.width, height: 1.0))*/ } } } diff --git a/submodules/TabBarUI/Sources/TabBarContollerNode.swift b/submodules/TabBarUI/Sources/TabBarContollerNode.swift index a1df8ff032..a44d64ee04 100644 --- a/submodules/TabBarUI/Sources/TabBarContollerNode.swift +++ b/submodules/TabBarUI/Sources/TabBarContollerNode.swift @@ -238,7 +238,7 @@ final class TabBarControllerNode: ASDisplayNode { let index = self.tabBarItems.firstIndex(where: { AnyHashable(ObjectIdentifier($0.item)) == itemId }) ?? 0 return TabBarComponent.Item( - item: item.item, + content: .tabBarItem(item.item), action: { [weak self] isLongTap in guard let self else { return diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 2eb3282973..6ad3268d85 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -654,6 +654,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1280209983] = { return Api.MessageEntity.parse_messageEntityCashtag($0) } dict[681706865] = { return Api.MessageEntity.parse_messageEntityCode($0) } dict[-925956616] = { return Api.MessageEntity.parse_messageEntityCustomEmoji($0) } + dict[106086853] = { return Api.MessageEntity.parse_messageEntityDiffDelete($0) } + dict[1903653142] = { return Api.MessageEntity.parse_messageEntityDiffInsert($0) } + dict[-960371289] = { return Api.MessageEntity.parse_messageEntityDiffReplace($0) } dict[1692693954] = { return Api.MessageEntity.parse_messageEntityEmail($0) } dict[-1874147385] = { return Api.MessageEntity.parse_messageEntityFormattedDate($0) } dict[1868782349] = { return Api.MessageEntity.parse_messageEntityHashtag($0) } @@ -1357,7 +1360,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1012971041] = { return Api.bots.ExportedBotToken.parse_exportedBotToken($0) } dict[428978491] = { return Api.bots.PopularAppBots.parse_popularAppBots($0) } dict[212278628] = { return Api.bots.PreviewInfo.parse_previewInfo($0) } - dict[569994407] = { return Api.bots.RequestedButton.parse_requestedButton($0) } + dict[-247743273] = { return Api.bots.RequestedButton.parse_requestedButton($0) } dict[-309659827] = { return Api.channels.AdminLogResults.parse_adminLogResults($0) } dict[-541588713] = { return Api.channels.ChannelParticipant.parse_channelParticipant($0) } dict[-1699676497] = { return Api.channels.ChannelParticipants.parse_channelParticipants($0) } @@ -1436,6 +1439,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1694474197] = { return Api.messages.Chats.parse_chats($0) } dict[-1663561404] = { return Api.messages.Chats.parse_chatsSlice($0) } dict[-1571952873] = { return Api.messages.CheckedHistoryImportPeer.parse_checkedHistoryImportPeer($0) } + dict[-1864913414] = { return Api.messages.ComposedMessageWithAI.parse_composedMessageWithAI($0) } dict[740433629] = { return Api.messages.DhConfig.parse_dhConfig($0) } dict[-1058912715] = { return Api.messages.DhConfig.parse_dhConfigNotModified($0) } dict[718878489] = { return Api.messages.DialogFilters.parse_dialogFilters($0) } @@ -2601,6 +2605,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.messages.CheckedHistoryImportPeer: _1.serialize(buffer, boxed) + case let _1 as Api.messages.ComposedMessageWithAI: + _1.serialize(buffer, boxed) case let _1 as Api.messages.DhConfig: _1.serialize(buffer, boxed) case let _1 as Api.messages.DialogFilters: diff --git a/submodules/TelegramApi/Sources/Api16.swift b/submodules/TelegramApi/Sources/Api16.swift index a078ddc990..636e3bde9f 100644 --- a/submodules/TelegramApi/Sources/Api16.swift +++ b/submodules/TelegramApi/Sources/Api16.swift @@ -94,6 +94,41 @@ public extension Api { return ("messageEntityCustomEmoji", [("offset", self.offset as Any), ("length", self.length as Any), ("documentId", self.documentId as Any)]) } } + public class Cons_messageEntityDiffDelete: TypeConstructorDescription { + public var offset: Int32 + public var length: Int32 + public init(offset: Int32, length: Int32) { + self.offset = offset + self.length = length + } + public func descriptionFields() -> (String, [(String, Any)]) { + return ("messageEntityDiffDelete", [("offset", self.offset as Any), ("length", self.length as Any)]) + } + } + public class Cons_messageEntityDiffInsert: TypeConstructorDescription { + public var offset: Int32 + public var length: Int32 + public init(offset: Int32, length: Int32) { + self.offset = offset + self.length = length + } + public func descriptionFields() -> (String, [(String, Any)]) { + return ("messageEntityDiffInsert", [("offset", self.offset as Any), ("length", self.length as Any)]) + } + } + public class Cons_messageEntityDiffReplace: TypeConstructorDescription { + public var offset: Int32 + public var length: Int32 + public var oldText: String + public init(offset: Int32, length: Int32, oldText: String) { + self.offset = offset + self.length = length + self.oldText = oldText + } + public func descriptionFields() -> (String, [(String, Any)]) { + return ("messageEntityDiffReplace", [("offset", self.offset as Any), ("length", self.length as Any), ("oldText", self.oldText as Any)]) + } + } public class Cons_messageEntityEmail: TypeConstructorDescription { public var offset: Int32 public var length: Int32 @@ -266,6 +301,9 @@ public extension Api { case messageEntityCashtag(Cons_messageEntityCashtag) case messageEntityCode(Cons_messageEntityCode) case messageEntityCustomEmoji(Cons_messageEntityCustomEmoji) + case messageEntityDiffDelete(Cons_messageEntityDiffDelete) + case messageEntityDiffInsert(Cons_messageEntityDiffInsert) + case messageEntityDiffReplace(Cons_messageEntityDiffReplace) case messageEntityEmail(Cons_messageEntityEmail) case messageEntityFormattedDate(Cons_messageEntityFormattedDate) case messageEntityHashtag(Cons_messageEntityHashtag) @@ -342,6 +380,28 @@ public extension Api { serializeInt32(_data.length, buffer: buffer, boxed: false) serializeInt64(_data.documentId, buffer: buffer, boxed: false) break + case .messageEntityDiffDelete(let _data): + if boxed { + buffer.appendInt32(106086853) + } + serializeInt32(_data.offset, buffer: buffer, boxed: false) + serializeInt32(_data.length, buffer: buffer, boxed: false) + break + case .messageEntityDiffInsert(let _data): + if boxed { + buffer.appendInt32(1903653142) + } + serializeInt32(_data.offset, buffer: buffer, boxed: false) + serializeInt32(_data.length, buffer: buffer, boxed: false) + break + case .messageEntityDiffReplace(let _data): + if boxed { + buffer.appendInt32(-960371289) + } + serializeInt32(_data.offset, buffer: buffer, boxed: false) + serializeInt32(_data.length, buffer: buffer, boxed: false) + serializeString(_data.oldText, buffer: buffer, boxed: false) + break case .messageEntityEmail(let _data): if boxed { buffer.appendInt32(1692693954) @@ -466,6 +526,12 @@ public extension Api { return ("messageEntityCode", [("offset", _data.offset as Any), ("length", _data.length as Any)]) case .messageEntityCustomEmoji(let _data): return ("messageEntityCustomEmoji", [("offset", _data.offset as Any), ("length", _data.length as Any), ("documentId", _data.documentId as Any)]) + case .messageEntityDiffDelete(let _data): + return ("messageEntityDiffDelete", [("offset", _data.offset as Any), ("length", _data.length as Any)]) + case .messageEntityDiffInsert(let _data): + return ("messageEntityDiffInsert", [("offset", _data.offset as Any), ("length", _data.length as Any)]) + case .messageEntityDiffReplace(let _data): + return ("messageEntityDiffReplace", [("offset", _data.offset as Any), ("length", _data.length as Any), ("oldText", _data.oldText as Any)]) case .messageEntityEmail(let _data): return ("messageEntityEmail", [("offset", _data.offset as Any), ("length", _data.length as Any)]) case .messageEntityFormattedDate(let _data): @@ -620,6 +686,51 @@ public extension Api { return nil } } + public static func parse_messageEntityDiffDelete(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.MessageEntity.messageEntityDiffDelete(Cons_messageEntityDiffDelete(offset: _1!, length: _2!)) + } + else { + return nil + } + } + public static func parse_messageEntityDiffInsert(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.MessageEntity.messageEntityDiffInsert(Cons_messageEntityDiffInsert(offset: _1!, length: _2!)) + } + else { + return nil + } + } + public static func parse_messageEntityDiffReplace(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + _3 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.MessageEntity.messageEntityDiffReplace(Cons_messageEntityDiffReplace(offset: _1!, length: _2!, oldText: _3!)) + } + else { + return nil + } + } public static func parse_messageEntityEmail(_ reader: BufferReader) -> MessageEntity? { var _1: Int32? _1 = reader.readInt32() diff --git a/submodules/TelegramApi/Sources/Api32.swift b/submodules/TelegramApi/Sources/Api32.swift index f2621554cf..f073f86d84 100644 --- a/submodules/TelegramApi/Sources/Api32.swift +++ b/submodules/TelegramApi/Sources/Api32.swift @@ -229,12 +229,12 @@ public extension Api.bots { public extension Api.bots { enum RequestedButton: TypeConstructorDescription { public class Cons_requestedButton: TypeConstructorDescription { - public var requestId: String - public init(requestId: String) { - self.requestId = requestId + public var webappReqId: String + public init(webappReqId: String) { + self.webappReqId = webappReqId } public func descriptionFields() -> (String, [(String, Any)]) { - return ("requestedButton", [("requestId", self.requestId as Any)]) + return ("requestedButton", [("webappReqId", self.webappReqId as Any)]) } } case requestedButton(Cons_requestedButton) @@ -243,9 +243,9 @@ public extension Api.bots { switch self { case .requestedButton(let _data): if boxed { - buffer.appendInt32(569994407) + buffer.appendInt32(-247743273) } - serializeString(_data.requestId, buffer: buffer, boxed: false) + serializeString(_data.webappReqId, buffer: buffer, boxed: false) break } } @@ -253,7 +253,7 @@ public extension Api.bots { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .requestedButton(let _data): - return ("requestedButton", [("requestId", _data.requestId as Any)]) + return ("requestedButton", [("webappReqId", _data.webappReqId as Any)]) } } @@ -262,7 +262,7 @@ public extension Api.bots { _1 = parseString(reader) let _c1 = _1 != nil if _c1 { - return Api.bots.RequestedButton.requestedButton(Cons_requestedButton(requestId: _1!)) + return Api.bots.RequestedButton.requestedButton(Cons_requestedButton(webappReqId: _1!)) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api34.swift b/submodules/TelegramApi/Sources/Api34.swift index c59592deab..d0fdddfe78 100644 --- a/submodules/TelegramApi/Sources/Api34.swift +++ b/submodules/TelegramApi/Sources/Api34.swift @@ -994,6 +994,70 @@ public extension Api.messages { } } } +public extension Api.messages { + enum ComposedMessageWithAI: TypeConstructorDescription { + public class Cons_composedMessageWithAI: TypeConstructorDescription { + public var flags: Int32 + public var resultText: Api.TextWithEntities + public var diffText: Api.TextWithEntities? + public init(flags: Int32, resultText: Api.TextWithEntities, diffText: Api.TextWithEntities?) { + self.flags = flags + self.resultText = resultText + self.diffText = diffText + } + public func descriptionFields() -> (String, [(String, Any)]) { + return ("composedMessageWithAI", [("flags", self.flags as Any), ("resultText", self.resultText as Any), ("diffText", self.diffText as Any)]) + } + } + case composedMessageWithAI(Cons_composedMessageWithAI) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .composedMessageWithAI(let _data): + if boxed { + buffer.appendInt32(-1864913414) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + _data.resultText.serialize(buffer, true) + if Int(_data.flags) & Int(1 << 0) != 0 { + _data.diffText!.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .composedMessageWithAI(let _data): + return ("composedMessageWithAI", [("flags", _data.flags as Any), ("resultText", _data.resultText as Any), ("diffText", _data.diffText as Any)]) + } + } + + public static func parse_composedMessageWithAI(_ reader: BufferReader) -> ComposedMessageWithAI? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.TextWithEntities? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.TextWithEntities + } + var _3: Api.TextWithEntities? + if Int(_1!) & Int(1 << 0) != 0 { + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.TextWithEntities + } + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + if _c1 && _c2 && _c3 { + return Api.messages.ComposedMessageWithAI.composedMessageWithAI(Cons_composedMessageWithAI(flags: _1!, resultText: _2!, diffText: _3)) + } + else { + return nil + } + } + } +} public extension Api.messages { enum DhConfig: TypeConstructorDescription { public class Cons_dhConfig: TypeConstructorDescription { @@ -1667,112 +1731,3 @@ public extension Api.messages { } } } -public extension Api.messages { - enum ExportedChatInvite: TypeConstructorDescription { - public class Cons_exportedChatInvite: TypeConstructorDescription { - public var invite: Api.ExportedChatInvite - public var users: [Api.User] - public init(invite: Api.ExportedChatInvite, users: [Api.User]) { - self.invite = invite - self.users = users - } - public func descriptionFields() -> (String, [(String, Any)]) { - return ("exportedChatInvite", [("invite", self.invite as Any), ("users", self.users as Any)]) - } - } - public class Cons_exportedChatInviteReplaced: TypeConstructorDescription { - public var invite: Api.ExportedChatInvite - public var newInvite: Api.ExportedChatInvite - public var users: [Api.User] - public init(invite: Api.ExportedChatInvite, newInvite: Api.ExportedChatInvite, users: [Api.User]) { - self.invite = invite - self.newInvite = newInvite - self.users = users - } - public func descriptionFields() -> (String, [(String, Any)]) { - return ("exportedChatInviteReplaced", [("invite", self.invite as Any), ("newInvite", self.newInvite as Any), ("users", self.users as Any)]) - } - } - case exportedChatInvite(Cons_exportedChatInvite) - case exportedChatInviteReplaced(Cons_exportedChatInviteReplaced) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .exportedChatInvite(let _data): - if boxed { - buffer.appendInt32(410107472) - } - _data.invite.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users.count)) - for item in _data.users { - item.serialize(buffer, true) - } - break - case .exportedChatInviteReplaced(let _data): - if boxed { - buffer.appendInt32(572915951) - } - _data.invite.serialize(buffer, true) - _data.newInvite.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users.count)) - for item in _data.users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .exportedChatInvite(let _data): - return ("exportedChatInvite", [("invite", _data.invite as Any), ("users", _data.users as Any)]) - case .exportedChatInviteReplaced(let _data): - return ("exportedChatInviteReplaced", [("invite", _data.invite as Any), ("newInvite", _data.newInvite as Any), ("users", _data.users as Any)]) - } - } - - public static func parse_exportedChatInvite(_ reader: BufferReader) -> ExportedChatInvite? { - var _1: Api.ExportedChatInvite? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.ExportedChatInvite - } - var _2: [Api.User]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.ExportedChatInvite.exportedChatInvite(Cons_exportedChatInvite(invite: _1!, users: _2!)) - } - else { - return nil - } - } - public static func parse_exportedChatInviteReplaced(_ reader: BufferReader) -> ExportedChatInvite? { - var _1: Api.ExportedChatInvite? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.ExportedChatInvite - } - var _2: Api.ExportedChatInvite? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.ExportedChatInvite - } - var _3: [Api.User]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.messages.ExportedChatInvite.exportedChatInviteReplaced(Cons_exportedChatInviteReplaced(invite: _1!, newInvite: _2!, users: _3!)) - } - else { - return nil - } - } - } -} diff --git a/submodules/TelegramApi/Sources/Api35.swift b/submodules/TelegramApi/Sources/Api35.swift index e762a696ee..4c246713c5 100644 --- a/submodules/TelegramApi/Sources/Api35.swift +++ b/submodules/TelegramApi/Sources/Api35.swift @@ -1,3 +1,112 @@ +public extension Api.messages { + enum ExportedChatInvite: TypeConstructorDescription { + public class Cons_exportedChatInvite: TypeConstructorDescription { + public var invite: Api.ExportedChatInvite + public var users: [Api.User] + public init(invite: Api.ExportedChatInvite, users: [Api.User]) { + self.invite = invite + self.users = users + } + public func descriptionFields() -> (String, [(String, Any)]) { + return ("exportedChatInvite", [("invite", self.invite as Any), ("users", self.users as Any)]) + } + } + public class Cons_exportedChatInviteReplaced: TypeConstructorDescription { + public var invite: Api.ExportedChatInvite + public var newInvite: Api.ExportedChatInvite + public var users: [Api.User] + public init(invite: Api.ExportedChatInvite, newInvite: Api.ExportedChatInvite, users: [Api.User]) { + self.invite = invite + self.newInvite = newInvite + self.users = users + } + public func descriptionFields() -> (String, [(String, Any)]) { + return ("exportedChatInviteReplaced", [("invite", self.invite as Any), ("newInvite", self.newInvite as Any), ("users", self.users as Any)]) + } + } + case exportedChatInvite(Cons_exportedChatInvite) + case exportedChatInviteReplaced(Cons_exportedChatInviteReplaced) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .exportedChatInvite(let _data): + if boxed { + buffer.appendInt32(410107472) + } + _data.invite.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + break + case .exportedChatInviteReplaced(let _data): + if boxed { + buffer.appendInt32(572915951) + } + _data.invite.serialize(buffer, true) + _data.newInvite.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .exportedChatInvite(let _data): + return ("exportedChatInvite", [("invite", _data.invite as Any), ("users", _data.users as Any)]) + case .exportedChatInviteReplaced(let _data): + return ("exportedChatInviteReplaced", [("invite", _data.invite as Any), ("newInvite", _data.newInvite as Any), ("users", _data.users as Any)]) + } + } + + public static func parse_exportedChatInvite(_ reader: BufferReader) -> ExportedChatInvite? { + var _1: Api.ExportedChatInvite? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.ExportedChatInvite + } + var _2: [Api.User]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.ExportedChatInvite.exportedChatInvite(Cons_exportedChatInvite(invite: _1!, users: _2!)) + } + else { + return nil + } + } + public static func parse_exportedChatInviteReplaced(_ reader: BufferReader) -> ExportedChatInvite? { + var _1: Api.ExportedChatInvite? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.ExportedChatInvite + } + var _2: Api.ExportedChatInvite? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.ExportedChatInvite + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.messages.ExportedChatInvite.exportedChatInviteReplaced(Cons_exportedChatInviteReplaced(invite: _1!, newInvite: _2!, users: _3!)) + } + else { + return nil + } + } + } +} public extension Api.messages { enum ExportedChatInvites: TypeConstructorDescription { public class Cons_exportedChatInvites: TypeConstructorDescription { @@ -1837,94 +1946,3 @@ public extension Api.messages { } } } -public extension Api.messages { - enum RecentStickers: TypeConstructorDescription { - public class Cons_recentStickers: TypeConstructorDescription { - public var hash: Int64 - public var packs: [Api.StickerPack] - public var stickers: [Api.Document] - public var dates: [Int32] - public init(hash: Int64, packs: [Api.StickerPack], stickers: [Api.Document], dates: [Int32]) { - self.hash = hash - self.packs = packs - self.stickers = stickers - self.dates = dates - } - public func descriptionFields() -> (String, [(String, Any)]) { - return ("recentStickers", [("hash", self.hash as Any), ("packs", self.packs as Any), ("stickers", self.stickers as Any), ("dates", self.dates as Any)]) - } - } - case recentStickers(Cons_recentStickers) - case recentStickersNotModified - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .recentStickers(let _data): - if boxed { - buffer.appendInt32(-1999405994) - } - serializeInt64(_data.hash, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.packs.count)) - for item in _data.packs { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.stickers.count)) - for item in _data.stickers { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.dates.count)) - for item in _data.dates { - serializeInt32(item, buffer: buffer, boxed: false) - } - break - case .recentStickersNotModified: - if boxed { - buffer.appendInt32(186120336) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .recentStickers(let _data): - return ("recentStickers", [("hash", _data.hash as Any), ("packs", _data.packs as Any), ("stickers", _data.stickers as Any), ("dates", _data.dates as Any)]) - case .recentStickersNotModified: - return ("recentStickersNotModified", []) - } - } - - public static func parse_recentStickers(_ reader: BufferReader) -> RecentStickers? { - var _1: Int64? - _1 = reader.readInt64() - var _2: [Api.StickerPack]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerPack.self) - } - var _3: [Api.Document]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) - } - var _4: [Int32]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.messages.RecentStickers.recentStickers(Cons_recentStickers(hash: _1!, packs: _2!, stickers: _3!, dates: _4!)) - } - else { - return nil - } - } - public static func parse_recentStickersNotModified(_ reader: BufferReader) -> RecentStickers? { - return Api.messages.RecentStickers.recentStickersNotModified - } - } -} diff --git a/submodules/TelegramApi/Sources/Api36.swift b/submodules/TelegramApi/Sources/Api36.swift index c377bf69c2..34ce45fd5c 100644 --- a/submodules/TelegramApi/Sources/Api36.swift +++ b/submodules/TelegramApi/Sources/Api36.swift @@ -1,3 +1,94 @@ +public extension Api.messages { + enum RecentStickers: TypeConstructorDescription { + public class Cons_recentStickers: TypeConstructorDescription { + public var hash: Int64 + public var packs: [Api.StickerPack] + public var stickers: [Api.Document] + public var dates: [Int32] + public init(hash: Int64, packs: [Api.StickerPack], stickers: [Api.Document], dates: [Int32]) { + self.hash = hash + self.packs = packs + self.stickers = stickers + self.dates = dates + } + public func descriptionFields() -> (String, [(String, Any)]) { + return ("recentStickers", [("hash", self.hash as Any), ("packs", self.packs as Any), ("stickers", self.stickers as Any), ("dates", self.dates as Any)]) + } + } + case recentStickers(Cons_recentStickers) + case recentStickersNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .recentStickers(let _data): + if boxed { + buffer.appendInt32(-1999405994) + } + serializeInt64(_data.hash, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.packs.count)) + for item in _data.packs { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.stickers.count)) + for item in _data.stickers { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.dates.count)) + for item in _data.dates { + serializeInt32(item, buffer: buffer, boxed: false) + } + break + case .recentStickersNotModified: + if boxed { + buffer.appendInt32(186120336) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .recentStickers(let _data): + return ("recentStickers", [("hash", _data.hash as Any), ("packs", _data.packs as Any), ("stickers", _data.stickers as Any), ("dates", _data.dates as Any)]) + case .recentStickersNotModified: + return ("recentStickersNotModified", []) + } + } + + public static func parse_recentStickers(_ reader: BufferReader) -> RecentStickers? { + var _1: Int64? + _1 = reader.readInt64() + var _2: [Api.StickerPack]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerPack.self) + } + var _3: [Api.Document]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) + } + var _4: [Int32]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.messages.RecentStickers.recentStickers(Cons_recentStickers(hash: _1!, packs: _2!, stickers: _3!, dates: _4!)) + } + else { + return nil + } + } + public static func parse_recentStickersNotModified(_ reader: BufferReader) -> RecentStickers? { + return Api.messages.RecentStickers.recentStickersNotModified + } + } +} public extension Api.messages { enum SavedDialogs: TypeConstructorDescription { public class Cons_savedDialogs: TypeConstructorDescription { @@ -1646,204 +1737,3 @@ public extension Api.payments { } } } -public extension Api.payments { - enum ExportedInvoice: TypeConstructorDescription { - public class Cons_exportedInvoice: TypeConstructorDescription { - public var url: String - public init(url: String) { - self.url = url - } - public func descriptionFields() -> (String, [(String, Any)]) { - return ("exportedInvoice", [("url", self.url as Any)]) - } - } - case exportedInvoice(Cons_exportedInvoice) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .exportedInvoice(let _data): - if boxed { - buffer.appendInt32(-1362048039) - } - serializeString(_data.url, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .exportedInvoice(let _data): - return ("exportedInvoice", [("url", _data.url as Any)]) - } - } - - public static func parse_exportedInvoice(_ reader: BufferReader) -> ExportedInvoice? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return Api.payments.ExportedInvoice.exportedInvoice(Cons_exportedInvoice(url: _1!)) - } - else { - return nil - } - } - } -} -public extension Api.payments { - enum GiveawayInfo: TypeConstructorDescription { - public class Cons_giveawayInfo: TypeConstructorDescription { - public var flags: Int32 - public var startDate: Int32 - public var joinedTooEarlyDate: Int32? - public var adminDisallowedChatId: Int64? - public var disallowedCountry: String? - public init(flags: Int32, startDate: Int32, joinedTooEarlyDate: Int32?, adminDisallowedChatId: Int64?, disallowedCountry: String?) { - self.flags = flags - self.startDate = startDate - self.joinedTooEarlyDate = joinedTooEarlyDate - self.adminDisallowedChatId = adminDisallowedChatId - self.disallowedCountry = disallowedCountry - } - public func descriptionFields() -> (String, [(String, Any)]) { - return ("giveawayInfo", [("flags", self.flags as Any), ("startDate", self.startDate as Any), ("joinedTooEarlyDate", self.joinedTooEarlyDate as Any), ("adminDisallowedChatId", self.adminDisallowedChatId as Any), ("disallowedCountry", self.disallowedCountry as Any)]) - } - } - public class Cons_giveawayInfoResults: TypeConstructorDescription { - public var flags: Int32 - public var startDate: Int32 - public var giftCodeSlug: String? - public var starsPrize: Int64? - public var finishDate: Int32 - public var winnersCount: Int32 - public var activatedCount: Int32? - public init(flags: Int32, startDate: Int32, giftCodeSlug: String?, starsPrize: Int64?, finishDate: Int32, winnersCount: Int32, activatedCount: Int32?) { - self.flags = flags - self.startDate = startDate - self.giftCodeSlug = giftCodeSlug - self.starsPrize = starsPrize - self.finishDate = finishDate - self.winnersCount = winnersCount - self.activatedCount = activatedCount - } - public func descriptionFields() -> (String, [(String, Any)]) { - return ("giveawayInfoResults", [("flags", self.flags as Any), ("startDate", self.startDate as Any), ("giftCodeSlug", self.giftCodeSlug as Any), ("starsPrize", self.starsPrize as Any), ("finishDate", self.finishDate as Any), ("winnersCount", self.winnersCount as Any), ("activatedCount", self.activatedCount as Any)]) - } - } - case giveawayInfo(Cons_giveawayInfo) - case giveawayInfoResults(Cons_giveawayInfoResults) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .giveawayInfo(let _data): - if boxed { - buffer.appendInt32(1130879648) - } - serializeInt32(_data.flags, buffer: buffer, boxed: false) - serializeInt32(_data.startDate, buffer: buffer, boxed: false) - if Int(_data.flags) & Int(1 << 1) != 0 { - serializeInt32(_data.joinedTooEarlyDate!, buffer: buffer, boxed: false) - } - if Int(_data.flags) & Int(1 << 2) != 0 { - serializeInt64(_data.adminDisallowedChatId!, buffer: buffer, boxed: false) - } - if Int(_data.flags) & Int(1 << 4) != 0 { - serializeString(_data.disallowedCountry!, buffer: buffer, boxed: false) - } - break - case .giveawayInfoResults(let _data): - if boxed { - buffer.appendInt32(-512366993) - } - serializeInt32(_data.flags, buffer: buffer, boxed: false) - serializeInt32(_data.startDate, buffer: buffer, boxed: false) - if Int(_data.flags) & Int(1 << 3) != 0 { - serializeString(_data.giftCodeSlug!, buffer: buffer, boxed: false) - } - if Int(_data.flags) & Int(1 << 4) != 0 { - serializeInt64(_data.starsPrize!, buffer: buffer, boxed: false) - } - serializeInt32(_data.finishDate, buffer: buffer, boxed: false) - serializeInt32(_data.winnersCount, buffer: buffer, boxed: false) - if Int(_data.flags) & Int(1 << 2) != 0 { - serializeInt32(_data.activatedCount!, buffer: buffer, boxed: false) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .giveawayInfo(let _data): - return ("giveawayInfo", [("flags", _data.flags as Any), ("startDate", _data.startDate as Any), ("joinedTooEarlyDate", _data.joinedTooEarlyDate as Any), ("adminDisallowedChatId", _data.adminDisallowedChatId as Any), ("disallowedCountry", _data.disallowedCountry as Any)]) - case .giveawayInfoResults(let _data): - return ("giveawayInfoResults", [("flags", _data.flags as Any), ("startDate", _data.startDate as Any), ("giftCodeSlug", _data.giftCodeSlug as Any), ("starsPrize", _data.starsPrize as Any), ("finishDate", _data.finishDate as Any), ("winnersCount", _data.winnersCount as Any), ("activatedCount", _data.activatedCount as Any)]) - } - } - - public static func parse_giveawayInfo(_ reader: BufferReader) -> GiveawayInfo? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - if Int(_1!) & Int(1 << 1) != 0 { - _3 = reader.readInt32() - } - var _4: Int64? - if Int(_1!) & Int(1 << 2) != 0 { - _4 = reader.readInt64() - } - var _5: String? - if Int(_1!) & Int(1 << 4) != 0 { - _5 = parseString(reader) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 4) == 0) || _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.payments.GiveawayInfo.giveawayInfo(Cons_giveawayInfo(flags: _1!, startDate: _2!, joinedTooEarlyDate: _3, adminDisallowedChatId: _4, disallowedCountry: _5)) - } - else { - return nil - } - } - public static func parse_giveawayInfoResults(_ reader: BufferReader) -> GiveawayInfo? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: String? - if Int(_1!) & Int(1 << 3) != 0 { - _3 = parseString(reader) - } - var _4: Int64? - if Int(_1!) & Int(1 << 4) != 0 { - _4 = reader.readInt64() - } - var _5: Int32? - _5 = reader.readInt32() - var _6: Int32? - _6 = reader.readInt32() - var _7: Int32? - if Int(_1!) & Int(1 << 2) != 0 { - _7 = reader.readInt32() - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 3) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 4) == 0) || _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 2) == 0) || _7 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { - return Api.payments.GiveawayInfo.giveawayInfoResults(Cons_giveawayInfoResults(flags: _1!, startDate: _2!, giftCodeSlug: _3, starsPrize: _4, finishDate: _5!, winnersCount: _6!, activatedCount: _7)) - } - else { - return nil - } - } - } -} diff --git a/submodules/TelegramApi/Sources/Api37.swift b/submodules/TelegramApi/Sources/Api37.swift index 89cd11eeb8..e5be012948 100644 --- a/submodules/TelegramApi/Sources/Api37.swift +++ b/submodules/TelegramApi/Sources/Api37.swift @@ -1,3 +1,204 @@ +public extension Api.payments { + enum ExportedInvoice: TypeConstructorDescription { + public class Cons_exportedInvoice: TypeConstructorDescription { + public var url: String + public init(url: String) { + self.url = url + } + public func descriptionFields() -> (String, [(String, Any)]) { + return ("exportedInvoice", [("url", self.url as Any)]) + } + } + case exportedInvoice(Cons_exportedInvoice) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .exportedInvoice(let _data): + if boxed { + buffer.appendInt32(-1362048039) + } + serializeString(_data.url, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .exportedInvoice(let _data): + return ("exportedInvoice", [("url", _data.url as Any)]) + } + } + + public static func parse_exportedInvoice(_ reader: BufferReader) -> ExportedInvoice? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.payments.ExportedInvoice.exportedInvoice(Cons_exportedInvoice(url: _1!)) + } + else { + return nil + } + } + } +} +public extension Api.payments { + enum GiveawayInfo: TypeConstructorDescription { + public class Cons_giveawayInfo: TypeConstructorDescription { + public var flags: Int32 + public var startDate: Int32 + public var joinedTooEarlyDate: Int32? + public var adminDisallowedChatId: Int64? + public var disallowedCountry: String? + public init(flags: Int32, startDate: Int32, joinedTooEarlyDate: Int32?, adminDisallowedChatId: Int64?, disallowedCountry: String?) { + self.flags = flags + self.startDate = startDate + self.joinedTooEarlyDate = joinedTooEarlyDate + self.adminDisallowedChatId = adminDisallowedChatId + self.disallowedCountry = disallowedCountry + } + public func descriptionFields() -> (String, [(String, Any)]) { + return ("giveawayInfo", [("flags", self.flags as Any), ("startDate", self.startDate as Any), ("joinedTooEarlyDate", self.joinedTooEarlyDate as Any), ("adminDisallowedChatId", self.adminDisallowedChatId as Any), ("disallowedCountry", self.disallowedCountry as Any)]) + } + } + public class Cons_giveawayInfoResults: TypeConstructorDescription { + public var flags: Int32 + public var startDate: Int32 + public var giftCodeSlug: String? + public var starsPrize: Int64? + public var finishDate: Int32 + public var winnersCount: Int32 + public var activatedCount: Int32? + public init(flags: Int32, startDate: Int32, giftCodeSlug: String?, starsPrize: Int64?, finishDate: Int32, winnersCount: Int32, activatedCount: Int32?) { + self.flags = flags + self.startDate = startDate + self.giftCodeSlug = giftCodeSlug + self.starsPrize = starsPrize + self.finishDate = finishDate + self.winnersCount = winnersCount + self.activatedCount = activatedCount + } + public func descriptionFields() -> (String, [(String, Any)]) { + return ("giveawayInfoResults", [("flags", self.flags as Any), ("startDate", self.startDate as Any), ("giftCodeSlug", self.giftCodeSlug as Any), ("starsPrize", self.starsPrize as Any), ("finishDate", self.finishDate as Any), ("winnersCount", self.winnersCount as Any), ("activatedCount", self.activatedCount as Any)]) + } + } + case giveawayInfo(Cons_giveawayInfo) + case giveawayInfoResults(Cons_giveawayInfoResults) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .giveawayInfo(let _data): + if boxed { + buffer.appendInt32(1130879648) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + serializeInt32(_data.startDate, buffer: buffer, boxed: false) + if Int(_data.flags) & Int(1 << 1) != 0 { + serializeInt32(_data.joinedTooEarlyDate!, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 2) != 0 { + serializeInt64(_data.adminDisallowedChatId!, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 4) != 0 { + serializeString(_data.disallowedCountry!, buffer: buffer, boxed: false) + } + break + case .giveawayInfoResults(let _data): + if boxed { + buffer.appendInt32(-512366993) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + serializeInt32(_data.startDate, buffer: buffer, boxed: false) + if Int(_data.flags) & Int(1 << 3) != 0 { + serializeString(_data.giftCodeSlug!, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 4) != 0 { + serializeInt64(_data.starsPrize!, buffer: buffer, boxed: false) + } + serializeInt32(_data.finishDate, buffer: buffer, boxed: false) + serializeInt32(_data.winnersCount, buffer: buffer, boxed: false) + if Int(_data.flags) & Int(1 << 2) != 0 { + serializeInt32(_data.activatedCount!, buffer: buffer, boxed: false) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .giveawayInfo(let _data): + return ("giveawayInfo", [("flags", _data.flags as Any), ("startDate", _data.startDate as Any), ("joinedTooEarlyDate", _data.joinedTooEarlyDate as Any), ("adminDisallowedChatId", _data.adminDisallowedChatId as Any), ("disallowedCountry", _data.disallowedCountry as Any)]) + case .giveawayInfoResults(let _data): + return ("giveawayInfoResults", [("flags", _data.flags as Any), ("startDate", _data.startDate as Any), ("giftCodeSlug", _data.giftCodeSlug as Any), ("starsPrize", _data.starsPrize as Any), ("finishDate", _data.finishDate as Any), ("winnersCount", _data.winnersCount as Any), ("activatedCount", _data.activatedCount as Any)]) + } + } + + public static func parse_giveawayInfo(_ reader: BufferReader) -> GiveawayInfo? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + if Int(_1!) & Int(1 << 1) != 0 { + _3 = reader.readInt32() + } + var _4: Int64? + if Int(_1!) & Int(1 << 2) != 0 { + _4 = reader.readInt64() + } + var _5: String? + if Int(_1!) & Int(1 << 4) != 0 { + _5 = parseString(reader) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 4) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.payments.GiveawayInfo.giveawayInfo(Cons_giveawayInfo(flags: _1!, startDate: _2!, joinedTooEarlyDate: _3, adminDisallowedChatId: _4, disallowedCountry: _5)) + } + else { + return nil + } + } + public static func parse_giveawayInfoResults(_ reader: BufferReader) -> GiveawayInfo? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + if Int(_1!) & Int(1 << 3) != 0 { + _3 = parseString(reader) + } + var _4: Int64? + if Int(_1!) & Int(1 << 4) != 0 { + _4 = reader.readInt64() + } + var _5: Int32? + _5 = reader.readInt32() + var _6: Int32? + _6 = reader.readInt32() + var _7: Int32? + if Int(_1!) & Int(1 << 2) != 0 { + _7 = reader.readInt32() + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 3) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 4) == 0) || _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = (Int(_1!) & Int(1 << 2) == 0) || _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.payments.GiveawayInfo.giveawayInfoResults(Cons_giveawayInfoResults(flags: _1!, startDate: _2!, giftCodeSlug: _3, starsPrize: _4, finishDate: _5!, winnersCount: _6!, activatedCount: _7)) + } + else { + return nil + } + } + } +} public extension Api.payments { enum PaymentForm: TypeConstructorDescription { public class Cons_paymentForm: TypeConstructorDescription { @@ -2179,215 +2380,3 @@ public extension Api.payments { } } } -public extension Api.phone { - enum ExportedGroupCallInvite: TypeConstructorDescription { - public class Cons_exportedGroupCallInvite: TypeConstructorDescription { - public var link: String - public init(link: String) { - self.link = link - } - public func descriptionFields() -> (String, [(String, Any)]) { - return ("exportedGroupCallInvite", [("link", self.link as Any)]) - } - } - case exportedGroupCallInvite(Cons_exportedGroupCallInvite) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .exportedGroupCallInvite(let _data): - if boxed { - buffer.appendInt32(541839704) - } - serializeString(_data.link, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .exportedGroupCallInvite(let _data): - return ("exportedGroupCallInvite", [("link", _data.link as Any)]) - } - } - - public static func parse_exportedGroupCallInvite(_ reader: BufferReader) -> ExportedGroupCallInvite? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return Api.phone.ExportedGroupCallInvite.exportedGroupCallInvite(Cons_exportedGroupCallInvite(link: _1!)) - } - else { - return nil - } - } - } -} -public extension Api.phone { - enum GroupCall: TypeConstructorDescription { - public class Cons_groupCall: TypeConstructorDescription { - public var call: Api.GroupCall - public var participants: [Api.GroupCallParticipant] - public var participantsNextOffset: String - public var chats: [Api.Chat] - public var users: [Api.User] - public init(call: Api.GroupCall, participants: [Api.GroupCallParticipant], participantsNextOffset: String, chats: [Api.Chat], users: [Api.User]) { - self.call = call - self.participants = participants - self.participantsNextOffset = participantsNextOffset - self.chats = chats - self.users = users - } - public func descriptionFields() -> (String, [(String, Any)]) { - return ("groupCall", [("call", self.call as Any), ("participants", self.participants as Any), ("participantsNextOffset", self.participantsNextOffset as Any), ("chats", self.chats as Any), ("users", self.users as Any)]) - } - } - case groupCall(Cons_groupCall) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .groupCall(let _data): - if boxed { - buffer.appendInt32(-1636664659) - } - _data.call.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.participants.count)) - for item in _data.participants { - item.serialize(buffer, true) - } - serializeString(_data.participantsNextOffset, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.chats.count)) - for item in _data.chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users.count)) - for item in _data.users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .groupCall(let _data): - return ("groupCall", [("call", _data.call as Any), ("participants", _data.participants as Any), ("participantsNextOffset", _data.participantsNextOffset as Any), ("chats", _data.chats as Any), ("users", _data.users as Any)]) - } - } - - public static func parse_groupCall(_ reader: BufferReader) -> GroupCall? { - var _1: Api.GroupCall? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.GroupCall - } - var _2: [Api.GroupCallParticipant]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.GroupCallParticipant.self) - } - var _3: String? - _3 = parseString(reader) - var _4: [Api.Chat]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _5: [Api.User]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.phone.GroupCall.groupCall(Cons_groupCall(call: _1!, participants: _2!, participantsNextOffset: _3!, chats: _4!, users: _5!)) - } - else { - return nil - } - } - } -} -public extension Api.phone { - enum GroupCallStars: TypeConstructorDescription { - public class Cons_groupCallStars: TypeConstructorDescription { - public var totalStars: Int64 - public var topDonors: [Api.GroupCallDonor] - public var chats: [Api.Chat] - public var users: [Api.User] - public init(totalStars: Int64, topDonors: [Api.GroupCallDonor], chats: [Api.Chat], users: [Api.User]) { - self.totalStars = totalStars - self.topDonors = topDonors - self.chats = chats - self.users = users - } - public func descriptionFields() -> (String, [(String, Any)]) { - return ("groupCallStars", [("totalStars", self.totalStars as Any), ("topDonors", self.topDonors as Any), ("chats", self.chats as Any), ("users", self.users as Any)]) - } - } - case groupCallStars(Cons_groupCallStars) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .groupCallStars(let _data): - if boxed { - buffer.appendInt32(-1658995418) - } - serializeInt64(_data.totalStars, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.topDonors.count)) - for item in _data.topDonors { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.chats.count)) - for item in _data.chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users.count)) - for item in _data.users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .groupCallStars(let _data): - return ("groupCallStars", [("totalStars", _data.totalStars as Any), ("topDonors", _data.topDonors as Any), ("chats", _data.chats as Any), ("users", _data.users as Any)]) - } - } - - public static func parse_groupCallStars(_ reader: BufferReader) -> GroupCallStars? { - var _1: Int64? - _1 = reader.readInt64() - var _2: [Api.GroupCallDonor]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.GroupCallDonor.self) - } - var _3: [Api.Chat]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _4: [Api.User]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.phone.GroupCallStars.groupCallStars(Cons_groupCallStars(totalStars: _1!, topDonors: _2!, chats: _3!, users: _4!)) - } - else { - return nil - } - } - } -} diff --git a/submodules/TelegramApi/Sources/Api38.swift b/submodules/TelegramApi/Sources/Api38.swift index 2f70ec327a..9b3e6b0c7c 100644 --- a/submodules/TelegramApi/Sources/Api38.swift +++ b/submodules/TelegramApi/Sources/Api38.swift @@ -1,3 +1,215 @@ +public extension Api.phone { + enum ExportedGroupCallInvite: TypeConstructorDescription { + public class Cons_exportedGroupCallInvite: TypeConstructorDescription { + public var link: String + public init(link: String) { + self.link = link + } + public func descriptionFields() -> (String, [(String, Any)]) { + return ("exportedGroupCallInvite", [("link", self.link as Any)]) + } + } + case exportedGroupCallInvite(Cons_exportedGroupCallInvite) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .exportedGroupCallInvite(let _data): + if boxed { + buffer.appendInt32(541839704) + } + serializeString(_data.link, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .exportedGroupCallInvite(let _data): + return ("exportedGroupCallInvite", [("link", _data.link as Any)]) + } + } + + public static func parse_exportedGroupCallInvite(_ reader: BufferReader) -> ExportedGroupCallInvite? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.phone.ExportedGroupCallInvite.exportedGroupCallInvite(Cons_exportedGroupCallInvite(link: _1!)) + } + else { + return nil + } + } + } +} +public extension Api.phone { + enum GroupCall: TypeConstructorDescription { + public class Cons_groupCall: TypeConstructorDescription { + public var call: Api.GroupCall + public var participants: [Api.GroupCallParticipant] + public var participantsNextOffset: String + public var chats: [Api.Chat] + public var users: [Api.User] + public init(call: Api.GroupCall, participants: [Api.GroupCallParticipant], participantsNextOffset: String, chats: [Api.Chat], users: [Api.User]) { + self.call = call + self.participants = participants + self.participantsNextOffset = participantsNextOffset + self.chats = chats + self.users = users + } + public func descriptionFields() -> (String, [(String, Any)]) { + return ("groupCall", [("call", self.call as Any), ("participants", self.participants as Any), ("participantsNextOffset", self.participantsNextOffset as Any), ("chats", self.chats as Any), ("users", self.users as Any)]) + } + } + case groupCall(Cons_groupCall) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .groupCall(let _data): + if boxed { + buffer.appendInt32(-1636664659) + } + _data.call.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.participants.count)) + for item in _data.participants { + item.serialize(buffer, true) + } + serializeString(_data.participantsNextOffset, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.chats.count)) + for item in _data.chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .groupCall(let _data): + return ("groupCall", [("call", _data.call as Any), ("participants", _data.participants as Any), ("participantsNextOffset", _data.participantsNextOffset as Any), ("chats", _data.chats as Any), ("users", _data.users as Any)]) + } + } + + public static func parse_groupCall(_ reader: BufferReader) -> GroupCall? { + var _1: Api.GroupCall? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.GroupCall + } + var _2: [Api.GroupCallParticipant]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.GroupCallParticipant.self) + } + var _3: String? + _3 = parseString(reader) + var _4: [Api.Chat]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _5: [Api.User]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.phone.GroupCall.groupCall(Cons_groupCall(call: _1!, participants: _2!, participantsNextOffset: _3!, chats: _4!, users: _5!)) + } + else { + return nil + } + } + } +} +public extension Api.phone { + enum GroupCallStars: TypeConstructorDescription { + public class Cons_groupCallStars: TypeConstructorDescription { + public var totalStars: Int64 + public var topDonors: [Api.GroupCallDonor] + public var chats: [Api.Chat] + public var users: [Api.User] + public init(totalStars: Int64, topDonors: [Api.GroupCallDonor], chats: [Api.Chat], users: [Api.User]) { + self.totalStars = totalStars + self.topDonors = topDonors + self.chats = chats + self.users = users + } + public func descriptionFields() -> (String, [(String, Any)]) { + return ("groupCallStars", [("totalStars", self.totalStars as Any), ("topDonors", self.topDonors as Any), ("chats", self.chats as Any), ("users", self.users as Any)]) + } + } + case groupCallStars(Cons_groupCallStars) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .groupCallStars(let _data): + if boxed { + buffer.appendInt32(-1658995418) + } + serializeInt64(_data.totalStars, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.topDonors.count)) + for item in _data.topDonors { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.chats.count)) + for item in _data.chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .groupCallStars(let _data): + return ("groupCallStars", [("totalStars", _data.totalStars as Any), ("topDonors", _data.topDonors as Any), ("chats", _data.chats as Any), ("users", _data.users as Any)]) + } + } + + public static func parse_groupCallStars(_ reader: BufferReader) -> GroupCallStars? { + var _1: Int64? + _1 = reader.readInt64() + var _2: [Api.GroupCallDonor]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.GroupCallDonor.self) + } + var _3: [Api.Chat]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _4: [Api.User]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.phone.GroupCallStars.groupCallStars(Cons_groupCallStars(totalStars: _1!, topDonors: _2!, chats: _3!, users: _4!)) + } + else { + return nil + } + } + } +} public extension Api.phone { enum GroupCallStreamChannels: TypeConstructorDescription { public class Cons_groupCallStreamChannels: TypeConstructorDescription { @@ -1706,213 +1918,3 @@ public extension Api.storage { } } } -public extension Api.stories { - enum Albums: TypeConstructorDescription { - public class Cons_albums: TypeConstructorDescription { - public var hash: Int64 - public var albums: [Api.StoryAlbum] - public init(hash: Int64, albums: [Api.StoryAlbum]) { - self.hash = hash - self.albums = albums - } - public func descriptionFields() -> (String, [(String, Any)]) { - return ("albums", [("hash", self.hash as Any), ("albums", self.albums as Any)]) - } - } - case albums(Cons_albums) - case albumsNotModified - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .albums(let _data): - if boxed { - buffer.appendInt32(-1013417414) - } - serializeInt64(_data.hash, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.albums.count)) - for item in _data.albums { - item.serialize(buffer, true) - } - break - case .albumsNotModified: - if boxed { - buffer.appendInt32(1448008427) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .albums(let _data): - return ("albums", [("hash", _data.hash as Any), ("albums", _data.albums as Any)]) - case .albumsNotModified: - return ("albumsNotModified", []) - } - } - - public static func parse_albums(_ reader: BufferReader) -> Albums? { - var _1: Int64? - _1 = reader.readInt64() - var _2: [Api.StoryAlbum]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryAlbum.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.stories.Albums.albums(Cons_albums(hash: _1!, albums: _2!)) - } - else { - return nil - } - } - public static func parse_albumsNotModified(_ reader: BufferReader) -> Albums? { - return Api.stories.Albums.albumsNotModified - } - } -} -public extension Api.stories { - enum AllStories: TypeConstructorDescription { - public class Cons_allStories: TypeConstructorDescription { - public var flags: Int32 - public var count: Int32 - public var state: String - public var peerStories: [Api.PeerStories] - public var chats: [Api.Chat] - public var users: [Api.User] - public var stealthMode: Api.StoriesStealthMode - public init(flags: Int32, count: Int32, state: String, peerStories: [Api.PeerStories], chats: [Api.Chat], users: [Api.User], stealthMode: Api.StoriesStealthMode) { - self.flags = flags - self.count = count - self.state = state - self.peerStories = peerStories - self.chats = chats - self.users = users - self.stealthMode = stealthMode - } - public func descriptionFields() -> (String, [(String, Any)]) { - return ("allStories", [("flags", self.flags as Any), ("count", self.count as Any), ("state", self.state as Any), ("peerStories", self.peerStories as Any), ("chats", self.chats as Any), ("users", self.users as Any), ("stealthMode", self.stealthMode as Any)]) - } - } - public class Cons_allStoriesNotModified: TypeConstructorDescription { - public var flags: Int32 - public var state: String - public var stealthMode: Api.StoriesStealthMode - public init(flags: Int32, state: String, stealthMode: Api.StoriesStealthMode) { - self.flags = flags - self.state = state - self.stealthMode = stealthMode - } - public func descriptionFields() -> (String, [(String, Any)]) { - return ("allStoriesNotModified", [("flags", self.flags as Any), ("state", self.state as Any), ("stealthMode", self.stealthMode as Any)]) - } - } - case allStories(Cons_allStories) - case allStoriesNotModified(Cons_allStoriesNotModified) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .allStories(let _data): - if boxed { - buffer.appendInt32(1862033025) - } - serializeInt32(_data.flags, buffer: buffer, boxed: false) - serializeInt32(_data.count, buffer: buffer, boxed: false) - serializeString(_data.state, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.peerStories.count)) - for item in _data.peerStories { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.chats.count)) - for item in _data.chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users.count)) - for item in _data.users { - item.serialize(buffer, true) - } - _data.stealthMode.serialize(buffer, true) - break - case .allStoriesNotModified(let _data): - if boxed { - buffer.appendInt32(291044926) - } - serializeInt32(_data.flags, buffer: buffer, boxed: false) - serializeString(_data.state, buffer: buffer, boxed: false) - _data.stealthMode.serialize(buffer, true) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .allStories(let _data): - return ("allStories", [("flags", _data.flags as Any), ("count", _data.count as Any), ("state", _data.state as Any), ("peerStories", _data.peerStories as Any), ("chats", _data.chats as Any), ("users", _data.users as Any), ("stealthMode", _data.stealthMode as Any)]) - case .allStoriesNotModified(let _data): - return ("allStoriesNotModified", [("flags", _data.flags as Any), ("state", _data.state as Any), ("stealthMode", _data.stealthMode as Any)]) - } - } - - public static func parse_allStories(_ reader: BufferReader) -> AllStories? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: String? - _3 = parseString(reader) - var _4: [Api.PeerStories]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PeerStories.self) - } - var _5: [Api.Chat]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _6: [Api.User]? - if let _ = reader.readInt32() { - _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - var _7: Api.StoriesStealthMode? - if let signature = reader.readInt32() { - _7 = Api.parse(reader, signature: signature) as? Api.StoriesStealthMode - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - let _c7 = _7 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { - return Api.stories.AllStories.allStories(Cons_allStories(flags: _1!, count: _2!, state: _3!, peerStories: _4!, chats: _5!, users: _6!, stealthMode: _7!)) - } - else { - return nil - } - } - public static func parse_allStoriesNotModified(_ reader: BufferReader) -> AllStories? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) - var _3: Api.StoriesStealthMode? - if let signature = reader.readInt32() { - _3 = Api.parse(reader, signature: signature) as? Api.StoriesStealthMode - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.stories.AllStories.allStoriesNotModified(Cons_allStoriesNotModified(flags: _1!, state: _2!, stealthMode: _3!)) - } - else { - return nil - } - } - } -} diff --git a/submodules/TelegramApi/Sources/Api39.swift b/submodules/TelegramApi/Sources/Api39.swift index ad59c84c8d..d613584bff 100644 --- a/submodules/TelegramApi/Sources/Api39.swift +++ b/submodules/TelegramApi/Sources/Api39.swift @@ -1,3 +1,213 @@ +public extension Api.stories { + enum Albums: TypeConstructorDescription { + public class Cons_albums: TypeConstructorDescription { + public var hash: Int64 + public var albums: [Api.StoryAlbum] + public init(hash: Int64, albums: [Api.StoryAlbum]) { + self.hash = hash + self.albums = albums + } + public func descriptionFields() -> (String, [(String, Any)]) { + return ("albums", [("hash", self.hash as Any), ("albums", self.albums as Any)]) + } + } + case albums(Cons_albums) + case albumsNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .albums(let _data): + if boxed { + buffer.appendInt32(-1013417414) + } + serializeInt64(_data.hash, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.albums.count)) + for item in _data.albums { + item.serialize(buffer, true) + } + break + case .albumsNotModified: + if boxed { + buffer.appendInt32(1448008427) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .albums(let _data): + return ("albums", [("hash", _data.hash as Any), ("albums", _data.albums as Any)]) + case .albumsNotModified: + return ("albumsNotModified", []) + } + } + + public static func parse_albums(_ reader: BufferReader) -> Albums? { + var _1: Int64? + _1 = reader.readInt64() + var _2: [Api.StoryAlbum]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryAlbum.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.stories.Albums.albums(Cons_albums(hash: _1!, albums: _2!)) + } + else { + return nil + } + } + public static func parse_albumsNotModified(_ reader: BufferReader) -> Albums? { + return Api.stories.Albums.albumsNotModified + } + } +} +public extension Api.stories { + enum AllStories: TypeConstructorDescription { + public class Cons_allStories: TypeConstructorDescription { + public var flags: Int32 + public var count: Int32 + public var state: String + public var peerStories: [Api.PeerStories] + public var chats: [Api.Chat] + public var users: [Api.User] + public var stealthMode: Api.StoriesStealthMode + public init(flags: Int32, count: Int32, state: String, peerStories: [Api.PeerStories], chats: [Api.Chat], users: [Api.User], stealthMode: Api.StoriesStealthMode) { + self.flags = flags + self.count = count + self.state = state + self.peerStories = peerStories + self.chats = chats + self.users = users + self.stealthMode = stealthMode + } + public func descriptionFields() -> (String, [(String, Any)]) { + return ("allStories", [("flags", self.flags as Any), ("count", self.count as Any), ("state", self.state as Any), ("peerStories", self.peerStories as Any), ("chats", self.chats as Any), ("users", self.users as Any), ("stealthMode", self.stealthMode as Any)]) + } + } + public class Cons_allStoriesNotModified: TypeConstructorDescription { + public var flags: Int32 + public var state: String + public var stealthMode: Api.StoriesStealthMode + public init(flags: Int32, state: String, stealthMode: Api.StoriesStealthMode) { + self.flags = flags + self.state = state + self.stealthMode = stealthMode + } + public func descriptionFields() -> (String, [(String, Any)]) { + return ("allStoriesNotModified", [("flags", self.flags as Any), ("state", self.state as Any), ("stealthMode", self.stealthMode as Any)]) + } + } + case allStories(Cons_allStories) + case allStoriesNotModified(Cons_allStoriesNotModified) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .allStories(let _data): + if boxed { + buffer.appendInt32(1862033025) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + serializeInt32(_data.count, buffer: buffer, boxed: false) + serializeString(_data.state, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.peerStories.count)) + for item in _data.peerStories { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.chats.count)) + for item in _data.chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + _data.stealthMode.serialize(buffer, true) + break + case .allStoriesNotModified(let _data): + if boxed { + buffer.appendInt32(291044926) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + serializeString(_data.state, buffer: buffer, boxed: false) + _data.stealthMode.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .allStories(let _data): + return ("allStories", [("flags", _data.flags as Any), ("count", _data.count as Any), ("state", _data.state as Any), ("peerStories", _data.peerStories as Any), ("chats", _data.chats as Any), ("users", _data.users as Any), ("stealthMode", _data.stealthMode as Any)]) + case .allStoriesNotModified(let _data): + return ("allStoriesNotModified", [("flags", _data.flags as Any), ("state", _data.state as Any), ("stealthMode", _data.stealthMode as Any)]) + } + } + + public static func parse_allStories(_ reader: BufferReader) -> AllStories? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + _3 = parseString(reader) + var _4: [Api.PeerStories]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PeerStories.self) + } + var _5: [Api.Chat]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _6: [Api.User]? + if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + var _7: Api.StoriesStealthMode? + if let signature = reader.readInt32() { + _7 = Api.parse(reader, signature: signature) as? Api.StoriesStealthMode + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.stories.AllStories.allStories(Cons_allStories(flags: _1!, count: _2!, state: _3!, peerStories: _4!, chats: _5!, users: _6!, stealthMode: _7!)) + } + else { + return nil + } + } + public static func parse_allStoriesNotModified(_ reader: BufferReader) -> AllStories? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Api.StoriesStealthMode? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.StoriesStealthMode + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.stories.AllStories.allStoriesNotModified(Cons_allStoriesNotModified(flags: _1!, state: _2!, stealthMode: _3!)) + } + else { + return nil + } + } + } +} public extension Api.stories { enum CanSendStoryCount: TypeConstructorDescription { public class Cons_canSendStoryCount: TypeConstructorDescription { diff --git a/submodules/TelegramApi/Sources/Api40.swift b/submodules/TelegramApi/Sources/Api40.swift index 8ca6b9b40a..fbd43aae64 100644 --- a/submodules/TelegramApi/Sources/Api40.swift +++ b/submodules/TelegramApi/Sources/Api40.swift @@ -2772,12 +2772,12 @@ public extension Api.functions.bots { } } public extension Api.functions.bots { - static func getRequestedWebViewButton(bot: Api.InputUser, requestId: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func getRequestedWebViewButton(bot: Api.InputUser, webappReqId: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-1295431495) + buffer.appendInt32(-1088047117) bot.serialize(buffer, true) - serializeString(requestId, buffer: buffer, boxed: false) - return (FunctionDescription(name: "bots.getRequestedWebViewButton", parameters: [("bot", String(describing: bot)), ("requestId", String(describing: requestId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.KeyboardButton? in + serializeString(webappReqId, buffer: buffer, boxed: false) + return (FunctionDescription(name: "bots.getRequestedWebViewButton", parameters: [("bot", String(describing: bot)), ("webappReqId", String(describing: webappReqId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.KeyboardButton? in let reader = BufferReader(buffer) var result: Api.KeyboardButton? if let signature = reader.readInt32() { @@ -5496,9 +5496,9 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func composeMessageWithAI(flags: Int32, text: Api.TextWithEntities, translateToLang: String?, changeTone: String?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func composeMessageWithAI(flags: Int32, text: Api.TextWithEntities, translateToLang: String?, changeTone: String?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-1080571914) + buffer.appendInt32(-45978882) serializeInt32(flags, buffer: buffer, boxed: false) text.serialize(buffer, true) if Int(flags) & Int(1 << 1) != 0 { @@ -5507,11 +5507,11 @@ public extension Api.functions.messages { if Int(flags) & Int(1 << 2) != 0 { serializeString(changeTone!, buffer: buffer, boxed: false) } - return (FunctionDescription(name: "messages.composeMessageWithAI", parameters: [("flags", String(describing: flags)), ("text", String(describing: text)), ("translateToLang", String(describing: translateToLang)), ("changeTone", String(describing: changeTone))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.TextWithEntities? in + return (FunctionDescription(name: "messages.composeMessageWithAI", parameters: [("flags", String(describing: flags)), ("text", String(describing: text)), ("translateToLang", String(describing: translateToLang)), ("changeTone", String(describing: changeTone))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.ComposedMessageWithAI? in let reader = BufferReader(buffer) - var result: Api.TextWithEntities? + var result: Api.messages.ComposedMessageWithAI? if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.TextWithEntities + result = Api.parse(reader, signature: signature) as? Api.messages.ComposedMessageWithAI } return result }) @@ -8887,16 +8887,16 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func sendBotRequestedPeer(flags: Int32, peer: Api.InputPeer, msgId: Int32?, requestId: String?, buttonId: Int32, requestedPeers: [Api.InputPeer]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func sendBotRequestedPeer(flags: Int32, peer: Api.InputPeer, msgId: Int32?, webappReqId: String?, buttonId: Int32, requestedPeers: [Api.InputPeer]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-1662773304) + buffer.appendInt32(1818030759) serializeInt32(flags, buffer: buffer, boxed: false) peer.serialize(buffer, true) if Int(flags) & Int(1 << 0) != 0 { serializeInt32(msgId!, buffer: buffer, boxed: false) } if Int(flags) & Int(1 << 1) != 0 { - serializeString(requestId!, buffer: buffer, boxed: false) + serializeString(webappReqId!, buffer: buffer, boxed: false) } serializeInt32(buttonId, buffer: buffer, boxed: false) buffer.appendInt32(481674261) @@ -8904,7 +8904,7 @@ public extension Api.functions.messages { for item in requestedPeers { item.serialize(buffer, true) } - return (FunctionDescription(name: "messages.sendBotRequestedPeer", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("msgId", String(describing: msgId)), ("requestId", String(describing: requestId)), ("buttonId", String(describing: buttonId)), ("requestedPeers", String(describing: requestedPeers))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + return (FunctionDescription(name: "messages.sendBotRequestedPeer", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("msgId", String(describing: msgId)), ("webappReqId", String(describing: webappReqId)), ("buttonId", String(describing: buttonId)), ("requestedPeers", String(describing: requestedPeers))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? if let signature = reader.readInt32() { diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 6cba95495e..86c792493a 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -824,6 +824,8 @@ func messageTextEntitiesFromApiEntities(_ entities: [Api.MessageEntity]) -> [Mes format = .full(timeFormat: timeFormat, dateFormat: dateFormat, dayOfWeek: (flags & (1 << 5)) != 0) } result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .FormattedDate(format: format, date: date))) + case .messageEntityDiffDelete, .messageEntityDiffInsert, .messageEntityDiffReplace: + break } } return result diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TextEntitiesMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TextEntitiesMessageAttribute.swift index 89feec5ed7..d98be65f97 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TextEntitiesMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TextEntitiesMessageAttribute.swift @@ -467,3 +467,13 @@ public func trimStringWithEntities(string: String, entities: [MessageTextEntity] let nsRange = NSRange(location: range.lowerBound, length: range.upperBound - range.lowerBound) return (nsString.substring(with: nsRange), messageTextEntitiesInRange(entities: entities, range: nsRange, onlyQuoteable: false)) } + +public struct TextWithEntities: Codable, Equatable { + public let text: String + public let entities: [MessageTextEntity] + + public init(text: String, entities: [MessageTextEntity]) { + self.text = text + self.entities = entities + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 38d7db955d..f51e59d575 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -1762,6 +1762,10 @@ public extension TelegramEngine { } |> ignoreValues } + + public func composeAIMessage(text: TextWithEntities, mode: TelegramComposeAIMessageMode) -> Signal { + return _internal_composeAIMessage(account: self.account, text: text, mode: mode) + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift index 04f1a29f62..d24fb1185c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift @@ -365,3 +365,128 @@ func _internal_togglePeerMessagesTranslationHidden(account: Account, peerId: Eng |> ignoreValues } } + +public enum TelegramComposeAIMessageMode { + public enum Style { + case neutral + case formal + case short + case savage + case biblical + case posh + } + + case translate(toLanguage: String, emojify: Bool, style: Style) + case stylize(emojify: Bool, style: Style) + case proofread +} + +extension TelegramComposeAIMessageMode.Style { + var apiStyle: String { + switch self { + case .neutral: + return "" + case .formal: + return "formal" + case .short: + return "short" + case .savage: + return "savage" + case .biblical: + return "biblical" + case .posh: + return "posh" + } + } +} + +public final class TelegramAIComposeMessageResult { + public let text: TextWithEntities + public let diffRanges: [Range] + + public init(text: TextWithEntities, diffRanges: [Range]) { + self.text = text + self.diffRanges = diffRanges + } +} + +extension TextWithEntities { + init(apiValue: Api.TextWithEntities) { + switch apiValue { + case let .textWithEntities(textWithEntities): + self.init(text: textWithEntities.text, entities: messageTextEntitiesFromApiEntities(textWithEntities.entities)) + } + } +} + +func _internal_composeAIMessage(account: Account, text: TextWithEntities, mode: TelegramComposeAIMessageMode) -> Signal { + var flags: Int32 = 0 + var translateToLang: String? + var changeTone: String? + switch mode { + case let .translate(toLanguage, emojify, style): + translateToLang = toLanguage + flags |= (1 << 1) + + if emojify { + flags |= (1 << 3) + } + + if style != .neutral { + changeTone = style.apiStyle + flags |= (1 << 2) + } + case let .stylize(emojify, style): + if emojify { + flags |= (1 << 3) + } + + if style != .neutral { + changeTone = style.apiStyle + flags |= (1 << 2) + } + case .proofread: + flags |= (1 << 0) + } + + let inputText: Api.TextWithEntities = .textWithEntities(Api.TextWithEntities.Cons_textWithEntities(text: text.text, entities: apiEntitiesFromMessageTextEntities(text.entities, associatedPeers: SimpleDictionary()))) + + return account.network.request(Api.functions.messages.composeMessageWithAI(flags: flags, text: inputText, translateToLang: translateToLang, changeTone: changeTone)) + |> delay(0.4, queue: .mainQueue()) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + guard let result else { + return .single(nil) + } + switch result { + case let .composedMessageWithAI(composedMessageWithAI): + var diffRanges: [Range] = [] + if let diffText = composedMessageWithAI.diffText { + switch diffText { + case let .textWithEntities(textWithEntities): + for entity in textWithEntities.entities { + switch entity { + case let .messageEntityDiffReplace(messageEntityDiffReplace): + if messageEntityDiffReplace.length >= 0 { + diffRanges.append(Int(messageEntityDiffReplace.offset) ..< Int(messageEntityDiffReplace.offset + messageEntityDiffReplace.length)) + } + case let .messageEntityDiffInsert(messageEntityDiffInsert): + if messageEntityDiffInsert.length >= 0 { + diffRanges.append(Int(messageEntityDiffInsert.offset) ..< Int(messageEntityDiffInsert.offset + messageEntityDiffInsert.length)) + } + default: + break + } + } + } + } + return .single(TelegramAIComposeMessageResult( + text: TextWithEntities(apiValue: composedMessageWithAI.resultText), + diffRanges: diffRanges + )) + } + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift index 691c6f25cd..488744fb00 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift @@ -369,7 +369,7 @@ func _internal_sendBotRequestedPeer(account: Account, peerId: PeerId, messageId: if let _ = requestId { flags |= (1 << 1) } - let signal = account.network.request(Api.functions.messages.sendBotRequestedPeer(flags: flags, peer: inputPeer, msgId: msgId, requestId: requestId, buttonId: buttonId, requestedPeers: inputRequestedPeers)) + let signal = account.network.request(Api.functions.messages.sendBotRequestedPeer(flags: flags, peer: inputPeer, msgId: msgId, webappReqId: requestId, buttonId: buttonId, requestedPeers: inputRequestedPeers)) |> mapError { error -> SendBotRequestedPeerError in return .generic } @@ -436,7 +436,7 @@ func _internal_getRequestedWebViewButton(account: Account, botId: PeerId, reques guard let inputUser = inputUser else { return .fail(.generic) } - return account.network.request(Api.functions.bots.getRequestedWebViewButton(bot: inputUser, requestId: requestId)) + return account.network.request(Api.functions.bots.getRequestedWebViewButton(bot: inputUser, webappReqId: requestId)) |> mapError { _ -> GetRequestedWebViewButtonError in return .generic } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 9270210f27..6850dd8c5e 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -521,6 +521,7 @@ swift_library( "//submodules/TelegramUI/Components/PeerInfo/PeerCopyProtectionInfoScreen", "//submodules/TelegramUI/Components/Chat/ChatRankInfoScreen", "//submodules/TelegramUI/Components/RankChatPreviewItem", + "//submodules/TelegramUI/Components/TextProcessingScreen", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift index 5171c2a152..df2b0c6d8c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift @@ -181,6 +181,7 @@ public final class ChatRecentActionsController: TelegramBaseController { }, displayUndo: { _ in }, presentInputTextTranslation: { _, _ in }, sendEmoji: { _, _, _ in + }, openAICompose: { }, updateHistoryFilter: { _ in }, updateChatLocationThread: { _, _ in }, toggleChatSidebarMode: { diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift index 2380c96b79..e69ecc3248 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift @@ -760,7 +760,9 @@ public final class ChatTextInputPanelComponent: Component { }, sendEmoji: { _, _, _ in }, - updateHistoryFilter: { _ in + openAICompose: { + } + ,updateHistoryFilter: { _ in }, updateChatLocationThread: { _, _ in }, diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift index 4c75039caa..a319ccccc6 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift @@ -246,6 +246,8 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg public var mediaRecordingAccessibilityArea: AccessibilityAreaNode? private let counterTextNode: ImmediateTextNode + private var aiButton: (button: HighlightTrackingButton, icon: UIImageView)? + public let menuButton: HighlightTrackingButtonNode private let menuButtonBackgroundView: GlassBackgroundView private let menuButtonClippingNode: ASDisplayNode @@ -333,6 +335,8 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg private let hapticFeedback = HapticFeedback() + public var isAIEnabled: Bool = false + public var inputTextState: ChatTextInputState { if let textInputNode = self.textInputNode { let selectionRange: Range = textInputNode.selectedRange.location ..< (textInputNode.selectedRange.location + textInputNode.selectedRange.length) @@ -3523,6 +3527,52 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg ) } + if self.isAIEnabled { + let aiButton: (button: HighlightTrackingButton, icon: UIImageView) + if let current = self.aiButton { + aiButton = current + } else { + aiButton = (HighlightTrackingButton(), GlassBackgroundView.ContentImageView()) + self.aiButton = aiButton + aiButton.button.highligthedChanged = { [weak self] highlighted in + guard let self, let aiButton = self.aiButton else { + return + } + if highlighted { + aiButton.icon.alpha = 0.6 + } else { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) + transition.updateAlpha(layer: aiButton.icon.layer, alpha: 1.0) + } + } + aiButton.button.addTarget(self, action: #selector(self.aiButtonPressed), for: .touchUpInside) + aiButton.button.addSubview(aiButton.icon) + aiButton.icon.image = UIImage(bundleImageName: "Chat/Input/Text/InputAIIcon")?.withRenderingMode(.alwaysTemplate) + self.textInputContainerBackgroundView.contentView.addSubview(aiButton.icon) + self.textInputContainerBackgroundView.contentView.addSubview(aiButton.button) + } + aiButton.icon.tintColor = interfaceState.theme.chat.inputPanel.inputControlColor + if let image = aiButton.icon.image { + let aiButtonSize = CGSize(width: 40.0, height: 40.0) + let aiButtonFrame = CGRect(origin: CGPoint(x: textInputContainerBackgroundFrame.width - aiButtonSize.width - 3.0, y: 0.0), size: aiButtonSize) + transition.updateFrame(view: aiButton.button, frame: aiButtonFrame) + transition.updateFrame(view: aiButton.icon, frame: image.size.centered(in: aiButtonFrame)) + } + let aiButtonAlpha: CGFloat = textInputContainerBackgroundFrame.height >= 78.0 ? 1.0 : 0.0 + transition.updateAlpha(layer: aiButton.button.layer, alpha: aiButtonAlpha) + transition.updateAlpha(layer: aiButton.icon.layer, alpha: aiButtonAlpha) + } else if let aiButton = self.aiButton { + self.aiButton = nil + let aiButtonView = aiButton.button + let aiButtonIconView = aiButton.button + transition.updateAlpha(layer: aiButton.button.layer, alpha: 0.0, completion: { [weak aiButtonView] _ in + aiButtonView?.removeFromSuperview() + }) + transition.updateAlpha(layer: aiButton.icon.layer, alpha: 0.0, completion: { [weak aiButtonIconView] _ in + aiButtonIconView?.removeFromSuperview() + }) + } + let containerFrame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: contentHeight + 64.0)) transition.updateFrame(view: self.glassBackgroundContainer, frame: containerFrame) self.glassBackgroundContainer.update(size: containerFrame.size, isDark: interfaceState.theme.overallDarkAppearance, transition: ComponentTransition(transition)) @@ -3555,6 +3605,10 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg self.interfaceInteraction?.resumeMediaRecording() } + @objc private func aiButtonPressed() { + self.interfaceInteraction?.openAICompose() + } + private func displayViewOnceTooltip(text: String) { guard let context = self.context, let parentController = self.interfaceInteraction?.chatController() else { return diff --git a/submodules/TelegramUI/Components/HeaderPanelContainerComponent/Sources/HeaderPanelContainerComponent.swift b/submodules/TelegramUI/Components/HeaderPanelContainerComponent/Sources/HeaderPanelContainerComponent.swift index d4e26a4f58..4d60d6cc71 100644 --- a/submodules/TelegramUI/Components/HeaderPanelContainerComponent/Sources/HeaderPanelContainerComponent.swift +++ b/submodules/TelegramUI/Components/HeaderPanelContainerComponent/Sources/HeaderPanelContainerComponent.swift @@ -258,7 +258,7 @@ public final class HeaderPanelContainerComponent: Component { } panelCount += component.panels.count var cornerRadius: CGFloat = 0.0 - if panelCount == 1 && backgroundFrame.height <= 50.0 * 2.0 { + if panelCount == 1 && backgroundFrame.height <= 50.0 { cornerRadius = backgroundFrame.height * 0.5 } else { cornerRadius = 20.0 diff --git a/submodules/TelegramUI/Components/HorizontalTabsComponent/Sources/HorizontalTabsComponent.swift b/submodules/TelegramUI/Components/HorizontalTabsComponent/Sources/HorizontalTabsComponent.swift index e9a4727b25..1528573a08 100644 --- a/submodules/TelegramUI/Components/HorizontalTabsComponent/Sources/HorizontalTabsComponent.swift +++ b/submodules/TelegramUI/Components/HorizontalTabsComponent/Sources/HorizontalTabsComponent.swift @@ -183,7 +183,7 @@ public final class HorizontalTabsComponent: Component { public let contextAction: ((ContextExtractedContentContainingView, ContextGesture?) -> Void)? public let deleteAction: (() -> Void)? - public init(id: AnyHashable, content: Content, badge: Badge?, action: @escaping () -> Void, contextAction: ((ContextExtractedContentContainingView, ContextGesture?) -> Void)?, deleteAction: (() -> Void)?) { + public init(id: AnyHashable, content: Content, badge: Badge?, action: @escaping () -> Void, contextAction: ((ContextExtractedContentContainingView, ContextGesture?) -> Void)? = nil, deleteAction: (() -> Void)? = nil) { self.id = id self.content = content self.badge = badge diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSelectionPanelNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSelectionPanelNode.swift index 48aa418161..5336fc7837 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSelectionPanelNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSelectionPanelNode.swift @@ -174,6 +174,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { }, displayUndo: { _ in }, presentInputTextTranslation: { _, _ in }, sendEmoji: { _, _, _ in + }, openAICompose: { }, updateHistoryFilter: { _ in }, updateChatLocationThread: { _, _ in }, toggleChatSidebarMode: { diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index 2eadeee068..1eef311353 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -835,6 +835,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { }, displayUndo: { _ in }, presentInputTextTranslation: { _, _ in }, sendEmoji: { _, _, _ in + }, openAICompose: { }, updateHistoryFilter: { _ in }, updateChatLocationThread: { _, _ in }, toggleChatSidebarMode: { diff --git a/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift b/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift index cfce67e3be..37582cb025 100644 --- a/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift +++ b/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift @@ -248,27 +248,73 @@ public final class NavigationSearchView: UIView { public final class TabBarComponent: Component { public final class Item: Equatable { - public let item: UITabBarItem + public enum Content: Equatable { + public struct CustomItem: Equatable { + public enum Icon: Equatable { + case bundleIcon(name: String) + case animation(name: String, offset: CGPoint) + } + + public var id: AnyHashable + public var title: String + public var icon: Icon + public var badge: String? + + public init(id: AnyHashable, title: String, icon: Icon, badge: String? = nil) { + self.id = id + self.title = title + self.icon = icon + self.badge = badge + } + } + + case tabBarItem(UITabBarItem) + case customItem(CustomItem) + + public static func ==(lhs: Content, rhs: Content) -> Bool { + switch lhs { + case let .tabBarItem(lhsTabBarItem): + if case let .tabBarItem(rhsTabBarItem) = rhs { + return lhsTabBarItem === rhsTabBarItem + } else { + return false + } + case let .customItem(lhsCustomItem): + if case let .customItem(rhsCustomItem) = rhs { + return lhsCustomItem == rhsCustomItem + } else { + return false + } + } + } + } + + public let content: Content public let action: (Bool) -> Void public let doubleTapAction: (() -> Void)? public let contextAction: ((ContextGesture, ContextExtractedContentContainingView) -> Void)? - + fileprivate var id: AnyHashable { - return AnyHashable(ObjectIdentifier(self.item)) + switch self.content { + case let .tabBarItem(tabBarItem): + return AnyHashable(ObjectIdentifier(tabBarItem)) + case let .customItem(customItem): + return customItem.id + } } - - public init(item: UITabBarItem, action: @escaping (Bool) -> Void, doubleTapAction: (() -> Void)?, contextAction: ((ContextGesture, ContextExtractedContentContainingView) -> Void)?) { - self.item = item + + public init(content: Content, action: @escaping (Bool) -> Void, doubleTapAction: (() -> Void)?, contextAction: ((ContextGesture, ContextExtractedContentContainingView) -> Void)?) { + self.content = content self.action = action self.doubleTapAction = doubleTapAction self.contextAction = contextAction } - + public static func ==(lhs: Item, rhs: Item) -> Bool { if lhs === rhs { return true } - if lhs.item !== rhs.item { + if lhs.content != rhs.content { return false } if (lhs.doubleTapAction == nil) != (rhs.doubleTapAction == nil) { @@ -301,6 +347,7 @@ public final class TabBarComponent: Component { } public let theme: PresentationTheme + public let tintSelectedItem: Bool public let strings: PresentationStrings public let items: [Item] public let search: Search? @@ -309,6 +356,7 @@ public final class TabBarComponent: Component { public init( theme: PresentationTheme, + tintSelectedItem: Bool = true, strings: PresentationStrings, items: [Item], search: Search?, @@ -316,6 +364,7 @@ public final class TabBarComponent: Component { outerInsets: UIEdgeInsets ) { self.theme = theme + self.tintSelectedItem = tintSelectedItem self.strings = strings self.items = items self.search = search @@ -327,6 +376,9 @@ public final class TabBarComponent: Component { if lhs.theme !== rhs.theme { return false } + if lhs.tintSelectedItem != rhs.tintSelectedItem { + return false + } if lhs.strings !== rhs.strings { return false } @@ -633,6 +685,7 @@ public final class TabBarComponent: Component { theme: component.theme, isCompact: false, isSelected: false, + tintSelectedItem: true, isUnconstrained: true )), environment: {}, @@ -709,6 +762,7 @@ public final class TabBarComponent: Component { theme: component.theme, isCompact: component.search?.isActive == true, isSelected: false, + tintSelectedItem: component.tintSelectedItem, isUnconstrained: false )), environment: {}, @@ -721,6 +775,7 @@ public final class TabBarComponent: Component { theme: component.theme, isCompact: component.search?.isActive == true, isSelected: true, + tintSelectedItem: component.tintSelectedItem, isUnconstrained: false )), environment: {}, @@ -895,13 +950,15 @@ private final class ItemComponent: Component { let theme: PresentationTheme let isCompact: Bool let isSelected: Bool + let tintSelectedItem: Bool let isUnconstrained: Bool - init(item: TabBarComponent.Item, theme: PresentationTheme, isCompact: Bool, isSelected: Bool, isUnconstrained: Bool) { + init(item: TabBarComponent.Item, theme: PresentationTheme, isCompact: Bool, isSelected: Bool, tintSelectedItem: Bool, isUnconstrained: Bool) { self.item = item self.theme = theme self.isCompact = isCompact self.isSelected = isSelected + self.tintSelectedItem = tintSelectedItem self.isUnconstrained = isUnconstrained } @@ -918,6 +975,9 @@ private final class ItemComponent: Component { if lhs.isSelected != rhs.isSelected { return false } + if lhs.tintSelectedItem != rhs.tintSelectedItem { + return false + } if lhs.isUnconstrained != rhs.isUnconstrained { return false } @@ -934,37 +994,37 @@ private final class ItemComponent: Component { private var component: ItemComponent? private weak var state: EmptyComponentState? - + private var setImageListener: Int? private var setSelectedImageListener: Int? private var setBadgeListener: Int? - + override init(frame: CGRect) { self.contextContainerView = ContextExtractedContentContainingView() - + super.init(frame: frame) - + self.addSubview(self.contextContainerView) } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + deinit { - if let component = self.component { + if let component = self.component, case let .tabBarItem(tabBarItem) = component.item.content { if let setImageListener = self.setImageListener { - component.item.item.removeSetImageListener(setImageListener) + tabBarItem.removeSetImageListener(setImageListener) } if let setSelectedImageListener = self.setSelectedImageListener { - component.item.item.removeSetSelectedImageListener(setSelectedImageListener) + tabBarItem.removeSetSelectedImageListener(setSelectedImageListener) } if let setBadgeListener = self.setBadgeListener { - component.item.item.removeSetBadgeListener(setBadgeListener) + tabBarItem.removeSetBadgeListener(setBadgeListener) } } } - + func playSelectionAnimation() { if let animationIconView = self.animationIcon?.view as? LottieComponent.View { animationIconView.playOnce() @@ -975,125 +1035,241 @@ private final class ItemComponent: Component { let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.25) let previousComponent = self.component - - if previousComponent?.item.item !== component.item.item { - if let setImageListener = self.setImageListener { - self.component?.item.item.removeSetImageListener(setImageListener) - } - if let setSelectedImageListener = self.setSelectedImageListener { - self.component?.item.item.removeSetSelectedImageListener(setSelectedImageListener) - } - if let setBadgeListener = self.setBadgeListener { - self.component?.item.item.removeSetBadgeListener(setBadgeListener) - } - self.setImageListener = component.item.item.addSetImageListener { [weak self] _ in - guard let self else { - return + + let previousTabBarItem: UITabBarItem? + if let previousComponent, case let .tabBarItem(tabBarItem) = previousComponent.item.content { + previousTabBarItem = tabBarItem + } else { + previousTabBarItem = nil + } + + let currentTabBarItem: UITabBarItem? + if case let .tabBarItem(tabBarItem) = component.item.content { + currentTabBarItem = tabBarItem + } else { + currentTabBarItem = nil + } + + if previousTabBarItem !== currentTabBarItem { + if let previousTabBarItem { + if let setImageListener = self.setImageListener { + previousTabBarItem.removeSetImageListener(setImageListener) } - self.state?.updated(transition: .immediate, isLocal: true) - } - self.setSelectedImageListener = component.item.item.addSetSelectedImageListener { [weak self] _ in - guard let self else { - return + if let setSelectedImageListener = self.setSelectedImageListener { + previousTabBarItem.removeSetSelectedImageListener(setSelectedImageListener) } - self.state?.updated(transition: .immediate, isLocal: true) - } - self.setBadgeListener = UITabBarItem_addSetBadgeListener(component.item.item) { [weak self] _ in - guard let self else { - return + if let setBadgeListener = self.setBadgeListener { + previousTabBarItem.removeSetBadgeListener(setBadgeListener) + } + self.setImageListener = nil + self.setSelectedImageListener = nil + self.setBadgeListener = nil + } + if let currentTabBarItem { + self.setImageListener = currentTabBarItem.addSetImageListener { [weak self] _ in + guard let self else { + return + } + self.state?.updated(transition: .immediate, isLocal: true) + } + self.setSelectedImageListener = currentTabBarItem.addSetSelectedImageListener { [weak self] _ in + guard let self else { + return + } + self.state?.updated(transition: .immediate, isLocal: true) + } + self.setBadgeListener = UITabBarItem_addSetBadgeListener(currentTabBarItem) { [weak self] _ in + guard let self else { + return + } + self.state?.updated(transition: .immediate, isLocal: true) } - self.state?.updated(transition: .immediate, isLocal: true) } } - + self.component = component self.state = state - - if let animationName = component.item.item.animationName { - if let imageIcon = self.imageIcon { - self.imageIcon = nil - imageIcon.view?.removeFromSuperview() - } - - let animationIcon: ComponentView - var iconTransition = transition - if let current = self.animationIcon { - animationIcon = current - } else { - iconTransition = iconTransition.withAnimation(.none) - animationIcon = ComponentView() - self.animationIcon = animationIcon - } - - let iconSize = animationIcon.update( - transition: iconTransition, - component: AnyComponent(LottieComponent( - content: LottieComponent.AppBundleContent( - name: animationName - ), - color: (component.isSelected && !component.isCompact) ? component.theme.rootController.tabBar.selectedTextColor : component.theme.rootController.tabBar.textColor, - placeholderColor: nil, - startingPosition: .end, - size: CGSize(width: 48.0, height: 48.0), - loop: false - )), - environment: {}, - containerSize: CGSize(width: 48.0, height: 48.0) - ) - let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: -4.0), size: iconSize).offsetBy(dx: component.item.item.animationOffset.x, dy: component.item.item.animationOffset.y) - if let animationIconView = animationIcon.view { - if animationIconView.superview == nil { - if let badgeView = self.badge?.view { - self.contextContainerView.contentView.insertSubview(animationIconView, belowSubview: badgeView) - } else { - self.contextContainerView.contentView.addSubview(animationIconView) - } + + let iconTintColor = (component.isSelected && component.tintSelectedItem && !component.isCompact) ? component.theme.rootController.tabBar.selectedTextColor : component.theme.rootController.tabBar.textColor + + let title: String + let badgeValue: String? + + switch component.item.content { + case let .tabBarItem(tabBarItem): + title = tabBarItem.title ?? " " + badgeValue = tabBarItem.badgeValue + + if let animationName = tabBarItem.animationName { + if let imageIcon = self.imageIcon { + self.imageIcon = nil + imageIcon.view?.removeFromSuperview() } - iconTransition.setFrame(view: animationIconView, frame: iconFrame) - } - } else { - if let animationIcon = self.animationIcon { - self.animationIcon = nil - animationIcon.view?.removeFromSuperview() - } - - let imageIcon: ComponentView - var iconTransition = transition - if let current = self.imageIcon { - imageIcon = current - } else { - iconTransition = iconTransition.withAnimation(.none) - imageIcon = ComponentView() - self.imageIcon = imageIcon - } - - let iconSize = imageIcon.update( - transition: iconTransition, - component: AnyComponent(Image( - image: component.isSelected ? component.item.item.selectedImage : component.item.item.image, - tintColor: nil, - contentMode: .center - )), - environment: {}, - containerSize: CGSize(width: 100.0, height: 100.0) - ) - let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: 3.0), size: iconSize) - if let imageIconView = imageIcon.view { - if imageIconView.superview == nil { - if let badgeView = self.badge?.view { - self.contextContainerView.contentView.insertSubview(imageIconView, belowSubview: badgeView) - } else { - self.contextContainerView.contentView.addSubview(imageIconView) - } + + let animationIcon: ComponentView + var iconTransition = transition + if let current = self.animationIcon { + animationIcon = current + } else { + iconTransition = iconTransition.withAnimation(.none) + animationIcon = ComponentView() + self.animationIcon = animationIcon + } + + let iconSize = animationIcon.update( + transition: iconTransition, + component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent( + name: animationName + ), + color: iconTintColor, + placeholderColor: nil, + startingPosition: .end, + size: CGSize(width: 48.0, height: 48.0), + loop: false + )), + environment: {}, + containerSize: CGSize(width: 48.0, height: 48.0) + ) + let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: -4.0), size: iconSize).offsetBy(dx: tabBarItem.animationOffset.x, dy: tabBarItem.animationOffset.y) + if let animationIconView = animationIcon.view { + if animationIconView.superview == nil { + if let badgeView = self.badge?.view { + self.contextContainerView.contentView.insertSubview(animationIconView, belowSubview: badgeView) + } else { + self.contextContainerView.contentView.addSubview(animationIconView) + } + } + iconTransition.setFrame(view: animationIconView, frame: iconFrame) + } + } else { + if let animationIcon = self.animationIcon { + self.animationIcon = nil + animationIcon.view?.removeFromSuperview() + } + + let imageIcon: ComponentView + var iconTransition = transition + if let current = self.imageIcon { + imageIcon = current + } else { + iconTransition = iconTransition.withAnimation(.none) + imageIcon = ComponentView() + self.imageIcon = imageIcon + } + + let iconSize = imageIcon.update( + transition: iconTransition, + component: AnyComponent(Image( + image: component.isSelected ? tabBarItem.selectedImage : tabBarItem.image, + tintColor: nil, + contentMode: .center + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: 3.0), size: iconSize) + if let imageIconView = imageIcon.view { + if imageIconView.superview == nil { + if let badgeView = self.badge?.view { + self.contextContainerView.contentView.insertSubview(imageIconView, belowSubview: badgeView) + } else { + self.contextContainerView.contentView.addSubview(imageIconView) + } + } + iconTransition.setFrame(view: imageIconView, frame: iconFrame) + } + } + case let .customItem(customItem): + title = customItem.title + badgeValue = customItem.badge + + switch customItem.icon { + case let .animation(name, offset): + if let imageIcon = self.imageIcon { + self.imageIcon = nil + imageIcon.view?.removeFromSuperview() + } + + let animationIcon: ComponentView + var iconTransition = transition + if let current = self.animationIcon { + animationIcon = current + } else { + iconTransition = iconTransition.withAnimation(.none) + animationIcon = ComponentView() + self.animationIcon = animationIcon + } + + let iconSize = animationIcon.update( + transition: iconTransition, + component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent( + name: name + ), + color: iconTintColor, + placeholderColor: nil, + startingPosition: .end, + size: CGSize(width: 48.0, height: 48.0), + loop: false + )), + environment: {}, + containerSize: CGSize(width: 48.0, height: 48.0) + ) + let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: -4.0), size: iconSize).offsetBy(dx: offset.x, dy: offset.y) + if let animationIconView = animationIcon.view { + if animationIconView.superview == nil { + if let badgeView = self.badge?.view { + self.contextContainerView.contentView.insertSubview(animationIconView, belowSubview: badgeView) + } else { + self.contextContainerView.contentView.addSubview(animationIconView) + } + } + iconTransition.setFrame(view: animationIconView, frame: iconFrame) + } + case let .bundleIcon(name): + if let animationIcon = self.animationIcon { + self.animationIcon = nil + animationIcon.view?.removeFromSuperview() + } + + let imageIcon: ComponentView + var iconTransition = transition + if let current = self.imageIcon { + imageIcon = current + } else { + iconTransition = iconTransition.withAnimation(.none) + imageIcon = ComponentView() + self.imageIcon = imageIcon + } + + let iconSize = imageIcon.update( + transition: iconTransition, + component: AnyComponent(BundleIconComponent( + name: name, + tintColor: iconTintColor + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: 8.0), size: iconSize) + if let imageIconView = imageIcon.view { + if imageIconView.superview == nil { + if let badgeView = self.badge?.view { + self.contextContainerView.contentView.insertSubview(imageIconView, belowSubview: badgeView) + } else { + self.contextContainerView.contentView.addSubview(imageIconView) + } + } + iconTransition.setFrame(view: imageIconView, frame: iconFrame) } - iconTransition.setFrame(view: imageIconView, frame: iconFrame) } } - + let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: component.item.item.title ?? " ", font: Font.semibold(10.0), textColor: component.isSelected ? component.theme.rootController.tabBar.selectedTextColor : component.theme.rootController.tabBar.textColor)) + text: .plain(NSAttributedString(string: title, font: Font.semibold(10.0), textColor: (component.isSelected && component.tintSelectedItem) ? component.theme.rootController.tabBar.selectedTextColor : component.theme.rootController.tabBar.textColor)) )), environment: {}, containerSize: CGSize(width: availableSize.width, height: 100.0) @@ -1106,8 +1282,8 @@ private final class ItemComponent: Component { titleView.frame = titleFrame alphaTransition.setAlpha(view: titleView, alpha: component.isCompact ? 0.0 : 1.0) } - - if let badgeText = component.item.item.badgeValue, !badgeText.isEmpty { + + if let badgeText = badgeValue, !badgeText.isEmpty { let badge: ComponentView var badgeTransition = transition if let current = self.badge { @@ -1142,11 +1318,11 @@ private final class ItemComponent: Component { self.badge = nil badge.view?.removeFromSuperview() } - + transition.setFrame(view: self.contextContainerView, frame: CGRect(origin: CGPoint(), size: availableSize)) transition.setFrame(view: self.contextContainerView.contentView, frame: CGRect(origin: CGPoint(), size: availableSize)) self.contextContainerView.contentRect = CGRect(origin: CGPoint(), size: availableSize) - + if component.isUnconstrained { return CGSize(width: titleSize.width + 10.0 * 2.0, height: availableSize.height) } else { diff --git a/submodules/TelegramUI/Components/TextProcessingScreen/BUILD b/submodules/TelegramUI/Components/TextProcessingScreen/BUILD new file mode 100644 index 0000000000..7109331c89 --- /dev/null +++ b/submodules/TelegramUI/Components/TextProcessingScreen/BUILD @@ -0,0 +1,40 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "TextProcessingScreen", + module_name = "TextProcessingScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/TelegramPresentationData", + "//submodules/ComponentFlow", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/AccountContext", + "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/PresentationDataUtils", + "//submodules/Components/ResizableSheetComponent", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/TelegramUI/Components/TabBarComponent", + "//submodules/TranslateUI", + "//submodules/Components/MultilineTextWithEntitiesComponent", + "//submodules/TextFormat", + "//submodules/TelegramUI/Components/PlainButtonComponent", + "//submodules/TelegramUI/Components/CheckComponent", + "//submodules/ShimmerEffect", + "//submodules/TelegramUI/Components/LottieComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingLanguageSelectionComponent.swift b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingLanguageSelectionComponent.swift new file mode 100644 index 0000000000..fb999ec9ae --- /dev/null +++ b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingLanguageSelectionComponent.swift @@ -0,0 +1,805 @@ +import Foundation +import UIKit +import Display +import TelegramPresentationData +import ComponentFlow +import GlassBackgroundComponent +import MultilineTextComponent +import BundleIconComponent +import TelegramCore +import TranslateUI + +final class TextProcessingLanguageSelectionComponent: Component { + public struct Language: Equatable { + public let id: String + public let languageCode: String + public let name: String + + public init(id: String, languageCode: String, name: String) { + self.id = id + self.languageCode = languageCode + self.name = name + } + } + + let theme: PresentationTheme + let strings: PresentationStrings + let sourceView: UIView + let topLanguages: [Language] + let selectedLanguageCode: String + let currentStyle: TelegramComposeAIMessageMode.Style + let displayStyles: Bool + let completion: (String, TelegramComposeAIMessageMode.Style) -> Void + let dismissed: () -> Void + + init( + theme: PresentationTheme, + strings: PresentationStrings, + sourceView: UIView, + topLanguages: [Language], + selectedLanguageCode: String, + currentStyle: TelegramComposeAIMessageMode.Style, + displayStyles: Bool, + completion: @escaping (String, TelegramComposeAIMessageMode.Style) -> Void, + dismissed: @escaping () -> Void + ) { + self.theme = theme + self.strings = strings + self.sourceView = sourceView + self.topLanguages = topLanguages + self.selectedLanguageCode = selectedLanguageCode + self.currentStyle = currentStyle + self.displayStyles = displayStyles + self.completion = completion + self.dismissed = dismissed + } + + static func ==(lhs: TextProcessingLanguageSelectionComponent, rhs: TextProcessingLanguageSelectionComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.topLanguages != rhs.topLanguages { + return false + } + if lhs.selectedLanguageCode != rhs.selectedLanguageCode { + return false + } + if lhs.currentStyle != rhs.currentStyle { + return false + } + if lhs.displayStyles != rhs.displayStyles { + return false + } + return true + } + + private final class ScrollView: UIScrollView { + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + } + + private struct ItemLayout: Equatable { + let size: CGSize + let itemHeight: CGFloat + let itemCount: Int + let topSeparatedItemCount: Int + let topSeparatorHeight: CGFloat + let verticalInset: CGFloat + let contentHeight: CGFloat + + init(size: CGSize, itemHeight: CGFloat, itemCount: Int, topSeparatedItemCount: Int, verticalInset: CGFloat) { + self.size = size + self.itemHeight = itemHeight + self.itemCount = itemCount + self.topSeparatedItemCount = topSeparatedItemCount + self.topSeparatorHeight = 20.0 + self.verticalInset = verticalInset + var contentHeight = verticalInset * 2.0 + CGFloat(itemCount) * itemHeight + self.contentHeight = contentHeight + if self.topSeparatedItemCount != 0 { + contentHeight += self.topSeparatorHeight + } + } + + func indexRange(minY: CGFloat, maxY: CGFloat) -> Range? { + var firstIndex = Int(floor((minY - self.verticalInset - self.topSeparatorHeight) / self.itemHeight)) + firstIndex = max(0, firstIndex) + + var lastIndex = Int(ceil((maxY - self.verticalInset + self.topSeparatorHeight) / self.itemHeight)) + lastIndex = min(self.itemCount - 1, lastIndex) + + if firstIndex < lastIndex { + return firstIndex ..< (lastIndex + 1) + } else { + return nil + } + } + + func frame(forItemAt index: Int) -> CGRect { + var rect = CGRect(origin: CGPoint(x: 0.0, y: self.verticalInset + CGFloat(index) * self.itemHeight), size: CGSize(width: self.size.width, height: self.itemHeight)) + if index >= self.topSeparatedItemCount && self.topSeparatedItemCount != 0 { + rect.origin.y += self.topSeparatorHeight + } + return rect + } + } + + final class View: UIView, UIScrollViewDelegate { + private let dimView: UIView + + private let backgroundContainer: GlassBackgroundContainerView + private let mainBackground: GlassBackgroundView + private let mainScrollView: ScrollView + private var mainItemViews: [String: ComponentView] = [:] + private let mainMeasureItem = ComponentView() + + private let mainTopSeparator: SimpleLayer + + private let stylesBackground: GlassBackgroundView + private let stylesScrollView: ScrollView + private let stylesSelectionView: UIImageView + private var stylesItemViews: [TelegramComposeAIMessageMode.Style: ComponentView] = [:] + + private var mainItems: [Language] = [] + private var mainTopItemCount: Int = 0 + + private var mainItemLayout: ItemLayout? + + private var component: TextProcessingLanguageSelectionComponent? + private weak var state: EmptyComponentState? + private var isUpdating: Bool = false + private var ignoreScrolling: Bool = false + + private var updatedLanguage: String? + private var updatedStyle: TelegramComposeAIMessageMode.Style? + + override init(frame: CGRect) { + self.dimView = UIView() + + self.backgroundContainer = GlassBackgroundContainerView() + + self.mainBackground = GlassBackgroundView() + self.backgroundContainer.contentView.addSubview(self.mainBackground) + + self.stylesBackground = GlassBackgroundView() + + self.mainScrollView = ScrollView() + self.stylesScrollView = ScrollView() + + self.mainTopSeparator = SimpleLayer() + self.mainScrollView.layer.addSublayer(self.mainTopSeparator) + + self.stylesSelectionView = UIImageView() + self.stylesScrollView.addSubview(self.stylesSelectionView) + + super.init(frame: frame) + + self.addSubview(self.dimView) + self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onDimTapGesture(_:)))) + + self.addSubview(self.backgroundContainer) + + self.mainScrollView.delaysContentTouches = false + self.mainScrollView.canCancelContentTouches = true + self.mainScrollView.contentInsetAdjustmentBehavior = .never + self.mainScrollView.automaticallyAdjustsScrollIndicatorInsets = false + self.mainScrollView.showsVerticalScrollIndicator = false + self.mainScrollView.showsHorizontalScrollIndicator = false + self.mainScrollView.alwaysBounceHorizontal = false + self.mainScrollView.alwaysBounceVertical = true + self.mainScrollView.scrollsToTop = false + self.mainScrollView.delegate = self + self.mainScrollView.clipsToBounds = true + self.mainBackground.contentView.addSubview(self.mainScrollView) + + self.stylesScrollView.delaysContentTouches = false + self.stylesScrollView.canCancelContentTouches = true + self.stylesScrollView.contentInsetAdjustmentBehavior = .never + self.stylesScrollView.automaticallyAdjustsScrollIndicatorInsets = false + self.stylesScrollView.showsVerticalScrollIndicator = false + self.stylesScrollView.showsHorizontalScrollIndicator = false + self.stylesScrollView.alwaysBounceHorizontal = false + self.stylesScrollView.alwaysBounceVertical = false + self.stylesScrollView.scrollsToTop = false + self.stylesScrollView.delegate = self + self.stylesScrollView.clipsToBounds = true + self.stylesBackground.contentView.addSubview(self.stylesScrollView) + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + @objc private func onDimTapGesture(_ recognizer: UITapGestureRecognizer) { + guard let component = self.component else { + return + } + if case .ended = recognizer.state { + if component.displayStyles, let updatedStyle = self.updatedStyle { + component.completion(component.selectedLanguageCode, updatedStyle) + } + self.animateOut() + } + } + + private func animateIn() { + self.mainBackground.layer.animateSpring(from: 0.001, to: 1.0, keyPath: "transform.scale", duration: 0.5) + self.stylesBackground.layer.animateSpring(from: 0.001, to: 1.0, keyPath: "transform.scale", duration: 0.5) + self.backgroundContainer.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + private func animateOut() { + self.mainBackground.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false) + self.stylesBackground.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false) + self.backgroundContainer.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak self] _ in + guard let self, let component = self.component else { + return + } + component.dismissed() + }) + } + + private func completeIfPossible() { + guard let component = self.component else { + return + } + if self.updatedLanguage != nil || self.updatedStyle != nil { + component.completion(self.updatedLanguage ?? component.selectedLanguageCode, self.updatedStyle ?? component.currentStyle) + } + self.animateOut() + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.updateScrolling(scrollView: scrollView, transition: .immediate) + } + } + + private func updateScrolling(scrollView: UIScrollView, transition: ComponentTransition) { + guard let component = self.component else { + return + } + if scrollView == self.mainScrollView { + guard let itemLayout = self.mainItemLayout else { + return + } + let visibleBounds = scrollView.bounds + + var validIds: [String] = [] + if let indexRange = itemLayout.indexRange(minY: visibleBounds.minY, maxY: visibleBounds.maxY) { + for index in indexRange.lowerBound ..< indexRange.upperBound { + if index >= self.mainItems.count { + break + } + let item = self.mainItems[index] + validIds.append(item.id) + + let itemView: ComponentView + var itemTransition = transition + if let current = self.mainItemViews[item.id] { + itemView = current + } else { + itemTransition = itemTransition.withAnimation(.none) + itemView = ComponentView() + self.mainItemViews[item.id] = itemView + } + + let itemFrame = itemLayout.frame(forItemAt: index) + let _ = itemView.update( + transition: itemTransition, + component: AnyComponent(LanguageItemComponent( + theme: component.theme, + title: item.name, + isSelected: item.languageCode == component.selectedLanguageCode, + action: { [weak self] in + guard let self else { + return + } + self.updatedLanguage = item.languageCode + self.completeIfPossible() + } + )), + environment: {}, + containerSize: itemFrame.size + ) + if let itemComponentView = itemView.view { + if itemComponentView.superview == nil { + self.mainScrollView.addSubview(itemComponentView) + } + itemTransition.setFrame(view: itemComponentView, frame: itemFrame) + } + } + } + + var removedIds: [String] = [] + for (id, itemView) in self.mainItemViews { + if !validIds.contains(id) { + removedIds.append(id) + itemView.view?.removeFromSuperview() + } + } + for id in removedIds { + self.mainItemViews.removeValue(forKey: id) + } + } + } + + func update(component: TextProcessingLanguageSelectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let containerSideInset: CGFloat = 16.0 + + var shouldAnimateIn = false + if self.component == nil { + shouldAnimateIn = true + } + + self.component = component + self.state = state + + if self.mainItems.isEmpty { + self.mainItems = supportedTranslationLanguages.compactMap { item in + return Language(id: item, languageCode: item, name: localizedLanguageName(strings: component.strings, language: item)) + } + var topIds: [String] = [] + if !topIds.contains(component.selectedLanguageCode), let item = self.mainItems.first(where: { $0.languageCode == component.selectedLanguageCode }) { + self.mainItems.insert(TextProcessingLanguageSelectionComponent.Language( + id: "top-" + item.id, + languageCode: item.languageCode, + name: item.name + ), at: 0) + topIds.append(item.languageCode) + } + if !topIds.contains("en"), let item = self.mainItems.first(where: { $0.languageCode == "en" }) { + self.mainItems.insert(TextProcessingLanguageSelectionComponent.Language( + id: "top-" + item.id, + languageCode: item.languageCode, + name: item.name + ), at: 0) + topIds.append(item.languageCode) + } + + var languageCode = component.strings.baseLanguageCode + let rawSuffix = "-raw" + if languageCode.hasSuffix(rawSuffix) { + languageCode = String(languageCode.dropLast(rawSuffix.count)) + } + + if !topIds.contains(languageCode), let item = self.mainItems.first(where: { $0.languageCode == languageCode }) { + self.mainItems.insert(TextProcessingLanguageSelectionComponent.Language( + id: "top-" + item.id, + languageCode: item.languageCode, + name: item.name + ), at: 0) + topIds.append(item.languageCode) + } + self.mainTopItemCount = topIds.count + } + + let mainWidth: CGFloat = 220.0 + let mainContainerInset: CGFloat = 11.0 + let mainItemSize = self.mainMeasureItem.update( + transition: .immediate, + component: AnyComponent(LanguageItemComponent( + theme: component.theme, + title: "A", + isSelected: false, + action: { + } + )), + environment: {}, + containerSize: CGSize(width: mainWidth, height: 1000.0) + ) + let mainContentHeight = mainContainerInset * 2.0 + CGFloat(self.mainItems.count) * mainItemSize.height + + var mainSize = CGSize(width: mainWidth, height: min(370.0, mainContentHeight)) + + var stylesSize: CGSize? + let stylesSpacing: CGFloat = 8.0 + if component.displayStyles { + var styleData: [(id: TelegramComposeAIMessageMode.Style, icon: String, title: String)] = [] + styleData.append((.neutral, "🏳️", "Neutral")) + styleData.append((.formal, "🤝", "Formal")) + styleData.append((.short, "🎯", "Short")) + styleData.append((.savage, "🍖", "Savage")) + styleData.append((.biblical, "🕯", "Biblical")) + styleData.append((.posh, "🍷", "Posh")) + + let stylesItemSize = CGSize(width: 82.0, height: 60.0) + var selectedItemFrame: CGRect? + stylesSize = CGSize(width: stylesItemSize.width, height: CGFloat(styleData.count) * stylesItemSize.height) + for index in 0 ..< styleData.count { + let item = styleData[index] + let itemView: ComponentView + var itemViewTransition = transition + if let current = self.stylesItemViews[item.id] { + itemView = current + } else { + itemViewTransition = itemViewTransition.withAnimation(.none) + itemView = ComponentView() + self.stylesItemViews[item.id] = itemView + } + let _ = itemView.update( + transition: itemViewTransition, + component: AnyComponent(StyleItemComponent( + theme: component.theme, + icon: item.icon, + title: item.title, + action: { [weak self] in + guard let self else { + return + } + self.updatedStyle = item.id + self.completeIfPossible() + } + )), + environment: {}, + containerSize: stylesItemSize + ) + let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: CGFloat(index) * stylesItemSize.height), size: stylesItemSize) + if let itemComponentView = itemView.view { + if itemComponentView.superview == nil { + self.stylesScrollView.addSubview(itemComponentView) + } + itemViewTransition.setFrame(view: itemComponentView, frame: itemFrame) + } + if item.id == self.updatedStyle ?? component.currentStyle { + selectedItemFrame = itemFrame + } + } + + if self.stylesSelectionView.image == nil { + self.stylesSelectionView.image = generateStretchableFilledCircleImage(diameter: (30.0 - 4.0) * 2.0, color: .white)?.withRenderingMode(.alwaysTemplate) + } + self.stylesSelectionView.tintColor = component.theme.list.itemHighlightedBackgroundColor.withMultipliedAlpha(0.6) + + if let selectedItemFrame { + var selectedBackgroundTransition = transition + if self.stylesSelectionView.isHidden { + self.stylesSelectionView.isHidden = false + selectedBackgroundTransition = selectedBackgroundTransition.withAnimation(.none) + } + selectedBackgroundTransition.setFrame(view: self.stylesSelectionView, frame: selectedItemFrame.insetBy(dx: 4.0, dy: 4.0)) + transition.setAlpha(view: self.stylesSelectionView, alpha: 1.0) + } else { + if !self.stylesSelectionView.isHidden { + transition.setAlpha(view: self.stylesSelectionView, alpha: 0.0, completion: { [weak self] flag in + guard let self, flag else { + return + } + self.stylesSelectionView.isHidden = true + }) + } + } + } + if let stylesSize { + mainSize.height = stylesSize.height + } + + let mainItemLayout = ItemLayout(size: mainSize, itemHeight: mainItemSize.height, itemCount: self.mainItems.count, topSeparatedItemCount: self.mainTopItemCount, verticalInset: mainContainerInset) + self.mainItemLayout = mainItemLayout + + if mainItemLayout.topSeparatedItemCount != 0 { + self.mainTopSeparator.backgroundColor = component.theme.contextMenu.itemSeparatorColor.cgColor + self.mainTopSeparator.isHidden = false + var topSeparatorFrame = CGRect(origin: CGPoint(x: 18.0, y: mainItemLayout.verticalInset + CGFloat(mainItemLayout.topSeparatedItemCount) * mainItemLayout.itemHeight), size: CGSize(width: mainItemLayout.size.width - 18.0 - 18.0, height: UIScreenPixel)) + topSeparatorFrame.origin.y += floorToScreenPixels((mainItemLayout.topSeparatorHeight - topSeparatorFrame.height) * 0.5) + transition.setFrame(layer: self.mainTopSeparator, frame: topSeparatorFrame) + } else { + self.mainTopSeparator.isHidden = true + } + + self.ignoreScrolling = true + if self.mainScrollView.bounds.size != mainItemLayout.size || self.mainScrollView.contentSize.height != mainItemLayout.contentHeight { + self.mainScrollView.frame = CGRect(origin: CGPoint(), size: mainItemLayout.size) + self.mainScrollView.contentSize = CGSize(width: mainItemLayout.size.width, height: mainItemLayout.contentHeight) + } + self.ignoreScrolling = false + self.updateScrolling(scrollView: self.mainScrollView, transition: transition) + + transition.setFrame(view: self.backgroundContainer, frame: CGRect(origin: CGPoint(), size: availableSize)) + self.backgroundContainer.update(size: availableSize, isDark: component.theme.overallDarkAppearance, transition: transition) + + let sourceLocation = component.sourceView.convert(component.sourceView.bounds.center, to: self) + var mainFrame = CGRect(origin: CGPoint(x: floor(sourceLocation.x - mainItemLayout.size.width * 0.5), y: floor(sourceLocation.y - mainItemLayout.size.height * 0.5)), size: mainItemLayout.size) + if mainFrame.origin.x + mainFrame.size.width > availableSize.width - containerSideInset { + mainFrame.origin.x = availableSize.width - containerSideInset - mainFrame.size.width + } + if mainFrame.origin.y + mainFrame.size.height > availableSize.height - containerSideInset { + mainFrame.origin.y = availableSize.height - containerSideInset - mainFrame.size.height + } + if mainFrame.origin.x < containerSideInset { + mainFrame.origin.x = containerSideInset + } + if mainFrame.origin.y < containerSideInset { + mainFrame.origin.y = containerSideInset + } + + transition.setFrame(view: self.mainBackground, frame: mainFrame) + self.mainBackground.update(size: mainFrame.size, cornerRadius: 30.0, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: true, transition: transition) + + if let stylesSize { + let stylesFrame = CGRect(origin: CGPoint(x: mainFrame.maxX + stylesSpacing, y: mainFrame.minY), size: stylesSize) + if self.stylesBackground.superview == nil { + self.backgroundContainer.contentView.addSubview(self.stylesBackground) + } + transition.setFrame(view: self.stylesBackground, frame: stylesFrame) + self.stylesBackground.update(size: stylesFrame.size, cornerRadius: 30.0, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: true, transition: transition) + + transition.setFrame(view: self.stylesScrollView, frame: CGRect(origin: CGPoint(), size: stylesFrame.size)) + self.stylesScrollView.contentSize = stylesFrame.size + } + + transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) + + if shouldAnimateIn { + self.animateIn() + } + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class LanguageItemComponent: Component { + let theme: PresentationTheme + let title: String + let isSelected: Bool + let action: () -> Void + + init( + theme: PresentationTheme, + title: String, + isSelected: Bool, + action: @escaping () -> Void + ) { + self.theme = theme + self.title = title + self.isSelected = isSelected + self.action = action + } + + static func ==(lhs: LanguageItemComponent, rhs: LanguageItemComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.isSelected != rhs.isSelected { + return false + } + return true + } + + final class View: UIView { + private var imageIcon: ComponentView? + private let title = ComponentView() + + private var component: LanguageItemComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func onTapGesture(_ recognizer: UITapGestureRecognizer) { + guard let component = self.component else { + return + } + if case .ended = recognizer.state { + component.action() + } + } + + func update(component: LanguageItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let size = CGSize(width: availableSize.width, height: 42.0) + + let leftTitleInset: CGFloat = 60.0 + let rightTitleInset: CGFloat = 8.0 + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.regular(17.0), textColor: component.theme.contextMenu.primaryColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftTitleInset - rightTitleInset, height: 100.0) + ) + let titleFrame = CGRect(origin: CGPoint(x: leftTitleInset, y: floorToScreenPixels((size.height - titleSize.height) * 0.5)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.addSubview(titleView) + } + titleView.frame = titleFrame + } + + if component.isSelected { + let imageIcon: ComponentView + var imageIconTransition = transition + if let current = self.imageIcon { + imageIcon = current + } else { + imageIconTransition = imageIconTransition.withAnimation(.none) + imageIcon = ComponentView() + self.imageIcon = imageIcon + } + let imageIconSize = imageIcon.update( + transition: imageIconTransition, + component: AnyComponent(BundleIconComponent( + name: "Chat/Context Menu/Check", + tintColor: component.theme.contextMenu.primaryColor + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + let imageIconFrame = CGRect(origin: CGPoint(x: 23.0, y: floorToScreenPixels((size.height - imageIconSize.height) * 0.5)), size: imageIconSize) + if let imageIconView = imageIcon.view { + if imageIconView.superview == nil { + imageIconView.isUserInteractionEnabled = false + self.addSubview(imageIconView) + } + imageIconTransition.setFrame(view: imageIconView, frame: imageIconFrame) + } + } else { + if let imageIcon = self.imageIcon { + self.imageIcon = nil + imageIcon.view?.removeFromSuperview() + } + } + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class StyleItemComponent: Component { + let theme: PresentationTheme + let icon: String + let title: String + let action: () -> Void + + init( + theme: PresentationTheme, + icon: String, + title: String, + action: @escaping () -> Void + ) { + self.theme = theme + self.icon = icon + self.title = title + self.action = action + } + + static func ==(lhs: StyleItemComponent, rhs: StyleItemComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.icon != rhs.icon { + return false + } + if lhs.title != rhs.title { + return false + } + return true + } + + final class View: UIView { + private var imageIcon: ComponentView? + private let title = ComponentView() + + private var component: StyleItemComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func onTapGesture(_ recognizer: UITapGestureRecognizer) { + guard let component = self.component else { + return + } + if case .ended = recognizer.state { + component.action() + } + } + + func update(component: StyleItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let iconTintColor = component.theme.list.itemPrimaryTextColor + + let imageIcon: ComponentView + var iconTransition = transition + if let current = self.imageIcon { + imageIcon = current + } else { + iconTransition = iconTransition.withAnimation(.none) + imageIcon = ComponentView() + self.imageIcon = imageIcon + } + + let iconSize = imageIcon.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.icon, font: Font.regular(25.0), textColor: .black)) + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: 8.0), size: iconSize) + if let imageIconView = imageIcon.view { + if imageIconView.superview == nil { + self.addSubview(imageIconView) + } + iconTransition.setFrame(view: imageIconView, frame: iconFrame) + } + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.semibold(10.0), textColor: iconTintColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: availableSize.height - 9.0 - titleSize.height), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.frame = titleFrame + } + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingScreen.swift b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingScreen.swift new file mode 100644 index 0000000000..7da23c7dc4 --- /dev/null +++ b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingScreen.swift @@ -0,0 +1,722 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Display +import TelegramPresentationData +import ComponentFlow +import ComponentDisplayAdapters +import AccountContext +import ViewControllerComponent +import MultilineTextComponent +import ButtonComponent +import BundleIconComponent +import TelegramCore +import PresentationDataUtils +import ResizableSheetComponent +import GlassBarButtonComponent +import TabBarComponent +import TranslateUI +import LottieComponent + +final class TextProcessingContentComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + final class ExternalState { + fileprivate(set) var isProcessing: Bool = false + fileprivate(set) var result: TextWithEntities? + + init() { + } + } + + let externalState: ExternalState + let context: AccountContext + let inputText: TextWithEntities + let copyCurrentResult: () -> Void + let displayLanguageSelectionMenu: (UIView, String, TelegramComposeAIMessageMode.Style, Bool, @escaping (String, TelegramComposeAIMessageMode.Style) -> Void) -> Void + + init( + externalState: ExternalState, + context: AccountContext, + inputText: TextWithEntities, + copyCurrentResult: @escaping () -> Void, + displayLanguageSelectionMenu: @escaping (UIView, String, TelegramComposeAIMessageMode.Style, Bool, @escaping (String, TelegramComposeAIMessageMode.Style) -> Void) -> Void + ) { + self.externalState = externalState + self.context = context + self.inputText = inputText + self.copyCurrentResult = copyCurrentResult + self.displayLanguageSelectionMenu = displayLanguageSelectionMenu + } + + static func ==(lhs: TextProcessingContentComponent, rhs: TextProcessingContentComponent) -> Bool { + return true + } + + private enum Mode { + case translate + case stylize + case fix + } + + final class View: UIView { + private var component: TextProcessingContentComponent? + private weak var state: EmptyComponentState? + private var isUpdating: Bool = false + + private let modeTabs = ComponentView() + + private let currentContentBackground: UIImageView + private let currentContentContainer: UIView + + private let translateState = TextProcessingTranslateContentComponent.ExternalState() + private let stylizeState = TextProcessingTranslateContentComponent.ExternalState() + private let fixState = TextProcessingTranslateContentComponent.ExternalState() + + private var currentContent: (mode: Mode, view: ComponentView)? + + private var currentMode: Mode = .translate + + override init(frame: CGRect) { + self.currentContentBackground = UIImageView() + self.currentContentContainer = UIView() + self.currentContentContainer.clipsToBounds = true + + super.init(frame: frame) + + self.addSubview(self.currentContentBackground) + self.addSubview(self.currentContentContainer) + + self.translateState.resultUpdated = { [weak self] result in + guard let self, let component = self.component else { + return + } + if case .translate = self.currentMode { + component.externalState.result = result?.text + } + } + self.translateState.isProcessingUpdated = { [weak self] isProcessing in + guard let self, let component = self.component else { + return + } + if case .translate = self.currentMode { + component.externalState.isProcessing = isProcessing + } + } + self.stylizeState.resultUpdated = { [weak self] result in + guard let self, let component = self.component else { + return + } + if case .stylize = self.currentMode { + component.externalState.result = result?.text + } + } + self.stylizeState.isProcessingUpdated = { [weak self] isProcessing in + guard let self, let component = self.component else { + return + } + if case .stylize = self.currentMode { + component.externalState.isProcessing = isProcessing + } + } + self.fixState.resultUpdated = { [weak self] result in + guard let self, let component = self.component else { + return + } + if case .fix = self.currentMode { + component.externalState.result = result?.text + } + } + self.fixState.isProcessingUpdated = { [weak self] isProcessing in + guard let self, let component = self.component else { + return + } + if case .fix = self.currentMode { + component.externalState.isProcessing = isProcessing + } + } + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + func update(component: TextProcessingContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.2) + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + + self.component = component + self.state = state + + let sideInset: CGFloat = 16.0 + + var contentHeight: CGFloat = 0.0 + contentHeight += 85.0 + + var tabs: [TabBarComponent.Item] = [] + tabs.append(TabBarComponent.Item( + content: .customItem(TabBarComponent.Item.Content.CustomItem( + id: "translate", + title: "Translate", + icon: .bundleIcon(name: "TextProcessing/TabTranslate") + )), + action: { [weak self] _ in + guard let self else { + return + } + if self.currentMode != .translate { + self.currentMode = .translate + } + if !self.isUpdating { + self.state?.updated(transition: .spring(duration: 0.4)) + } + }, + doubleTapAction: nil, + contextAction: nil + )) + tabs.append(TabBarComponent.Item( + content: .customItem(TabBarComponent.Item.Content.CustomItem( + id: "stylize", + title: "Style", + icon: .bundleIcon(name: "TextProcessing/TabStylize") + )), + action: { [weak self] _ in + guard let self else { + return + } + if self.currentMode != .stylize { + self.currentMode = .stylize + } + if !self.isUpdating { + self.state?.updated(transition: .spring(duration: 0.4)) + } + }, + doubleTapAction: nil, + contextAction: nil + )) + tabs.append(TabBarComponent.Item( + content: .customItem(TabBarComponent.Item.Content.CustomItem( + id: "fix", + title: "Fix", + icon: .bundleIcon(name: "TextProcessing/TabFix") + )), + action: { [weak self] _ in + guard let self else { + return + } + if self.currentMode != .fix { + self.currentMode = .fix + } + if !self.isUpdating { + self.state?.updated(transition: .spring(duration: 0.4)) + } + }, + doubleTapAction: nil, + contextAction: nil + )) + + let currentModeId: String + switch self.currentMode { + case .translate: + currentModeId = "translate" + case .stylize: + currentModeId = "stylize" + case .fix: + currentModeId = "fix" + } + let modeTabsSize = self.modeTabs.update( + transition: transition, + component: AnyComponent(TabBarComponent( + theme: environment.theme, + tintSelectedItem: false, + strings: environment.strings, + items: tabs, + search: nil, + selectedId: currentModeId, + outerInsets: UIEdgeInsets() + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 62.0) + ) + let modeTabsFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: modeTabsSize) + if let modeTabsView = self.modeTabs.view { + if modeTabsView.superview == nil { + self.modeTabs.parentState = state + self.addSubview(modeTabsView) + } + transition.setFrame(view: modeTabsView, frame: modeTabsFrame) + } + contentHeight += modeTabsSize.height + contentHeight += 24.0 + + if let currentContent = self.currentContent, currentContent.mode != self.currentMode { + if let currentContentView = currentContent.view.view { + transition.setAlpha(view: currentContentView, alpha: 0.0, completion: { [weak currentContentView] _ in + currentContentView?.removeFromSuperview() + }) + } + self.currentContent = nil + } + + let contentComponent: AnyComponent + switch self.currentMode { + case .translate: + contentComponent = AnyComponent(TextProcessingTranslateContentComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + externalState: self.translateState, + inputText: component.inputText, + mode: .translate, + copyAction: component.copyCurrentResult, + displayLanguageSelectionMenu: component.displayLanguageSelectionMenu + )) + case .stylize: + contentComponent = AnyComponent(TextProcessingTranslateContentComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + externalState: self.stylizeState, + inputText: component.inputText, + mode: .stylize, + copyAction: component.copyCurrentResult, + displayLanguageSelectionMenu: component.displayLanguageSelectionMenu + )) + case .fix: + contentComponent = AnyComponent(TextProcessingTranslateContentComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + externalState: self.fixState, + inputText: component.inputText, + mode: .fix, + copyAction: component.copyCurrentResult, + displayLanguageSelectionMenu: component.displayLanguageSelectionMenu + )) + } + + let content: ComponentView + var contentTransition = transition + if let current = self.currentContent { + content = current.view + } else { + content = ComponentView() + self.currentContent = (self.currentMode, content) + contentTransition = contentTransition.withAnimation(.none) + } + let contentSize = content.update( + transition: contentTransition, + component: contentComponent, + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000000.0) + ) + if let contentView = content.view { + if contentView.superview == nil { + content.parentState = state + self.currentContentContainer.addSubview(contentView) + contentView.layer.allowsGroupOpacity = true + contentView.alpha = 0.0 + } + alphaTransition.setAlpha(view: contentView, alpha: 1.0) + contentTransition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(), size: contentSize)) + } + let contentFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: contentSize) + transition.setFrame(view: self.currentContentContainer, frame: contentFrame) + + if self.currentContentBackground.image == nil { + self.currentContentBackground.image = generateStretchableFilledCircleImage(diameter: 60.0, color: .white)?.withRenderingMode(.alwaysTemplate) + } + self.currentContentBackground.tintColor = environment.theme.list.itemBlocksBackgroundColor + transition.setFrame(view: self.currentContentBackground, frame: contentFrame) + + contentHeight += contentSize.height + + contentHeight += 106.0 + + return CGSize(width: availableSize.width, height: contentHeight) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class TextProcessingSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let inputText: TextWithEntities + let copyCurrentResult: (TextWithEntities) -> Void + let completion: (TextWithEntities) -> Void + + init( + context: AccountContext, + inputText: TextWithEntities, + copyCurrentResult: @escaping (TextWithEntities) -> Void, + completion: @escaping (TextWithEntities) -> Void + ) { + self.context = context + self.inputText = inputText + self.copyCurrentResult = copyCurrentResult + self.completion = completion + } + + static func ==(lhs: TextProcessingSheetComponent, rhs: TextProcessingSheetComponent) -> Bool { + return true + } + + static var body: Body { + let sheet = Child(ResizableSheetComponent.self) + let languageSelectionMenu = Child(TextProcessingLanguageSelectionComponent.self) + let animateOut = StoredActionSlot(Action.self) + let contentExternalState = TextProcessingContentComponent.ExternalState() + + class LanguageSelectionMenuData { + let sourceView: UIView + let currentLanguage: String + let currentStyle: TelegramComposeAIMessageMode.Style + let displayStyle: Bool + let completion: (String, TelegramComposeAIMessageMode.Style) -> Void + + init(sourceView: UIView, currentLanguage: String, currentStyle: TelegramComposeAIMessageMode.Style, displayStyle: Bool, completion: @escaping (String, TelegramComposeAIMessageMode.Style) -> Void) { + self.sourceView = sourceView + self.currentLanguage = currentLanguage + self.currentStyle = currentStyle + self.displayStyle = displayStyle + self.completion = completion + } + } + var languageSelectionMenuData: LanguageSelectionMenuData? + + return { context in + let environment = context.environment[EnvironmentType.self] + let controller = environment.controller + let theme = environment.theme + let state = context.state + + let dismiss: (Bool) -> Void = { animated in + if animated { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else { + if let controller = controller() { + controller.dismiss(completion: nil) + } + } + } + + let completion = context.component.completion + let performMainAction: () -> Void = { + if let result = contentExternalState.result { + completion(result) + } + dismiss(true) + } + let copyCurrentResult = context.component.copyCurrentResult + let copyCurrentResultImpl: () -> Void = { + if let result = contentExternalState.result { + copyCurrentResult(result) + } + } + + //TODO:localize (for AI: don't remove this line) + let titleString: String = "AI Editor" + //TODO:localize (for AI: don't remove this line) + let actionButtonTitle: String = "Apply" + + let sheet = sheet.update( + component: ResizableSheetComponent( + content: AnyComponent(TextProcessingContentComponent( + externalState: contentExternalState, + context: context.component.context, + inputText: context.component.inputText, + copyCurrentResult: { + copyCurrentResultImpl() + }, + displayLanguageSelectionMenu: { [weak state] sourceView, currentLanguage, currentStyle, displayStyle, completion in + languageSelectionMenuData = LanguageSelectionMenuData(sourceView: sourceView, currentLanguage: currentLanguage, currentStyle: currentStyle, displayStyle: displayStyle, completion: completion) + state?.updated(transition: .immediate) + } + )), + titleItem: AnyComponent(TitleComponent( + theme: theme, + title: titleString, + isProcessing: contentExternalState.isProcessing + )), + leftItem: AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: theme.chat.inputPanel.panelControlColor + ) + )), + action: { _ in + dismiss(true) + } + ) + ), + bottomItem: AnyComponent( + ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: theme.list.itemCheckColors.fillColor, + foreground: theme.list.itemCheckColors.foregroundColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(ButtonTextContentComponent( + text: actionButtonTitle, + badge: 0, + textColor: theme.list.itemCheckColors.foregroundColor, + badgeBackground: theme.list.itemCheckColors.foregroundColor, + badgeForeground: theme.list.itemCheckColors.fillColor + )) + ), + isEnabled: !contentExternalState.isProcessing, + displaysProgress: false, + action: { + performMainAction() + } + ) + ), + backgroundColor: .color(theme.list.blocksBackgroundColor), + animateOut: animateOut + ), + environment: { + environment + ResizableSheetComponentEnvironment( + theme: theme, + statusBarHeight: environment.statusBarHeight, + safeInsets: environment.safeInsets, + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + screenSize: context.availableSize, + regularMetricsSize: nil, + dismiss: { animated in + dismiss(animated) + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + if let languageSelectionMenuDataValue = languageSelectionMenuData { + let languageSelectionMenu = languageSelectionMenu.update( + component: TextProcessingLanguageSelectionComponent( + theme: environment.theme, + strings: environment.strings, + sourceView: languageSelectionMenuDataValue.sourceView, + topLanguages: [], + selectedLanguageCode: languageSelectionMenuDataValue.currentLanguage, + currentStyle: languageSelectionMenuDataValue.currentStyle, + displayStyles: languageSelectionMenuDataValue.displayStyle, + completion: languageSelectionMenuDataValue.completion, + dismissed: { [weak state] in + languageSelectionMenuData = nil + state?.updated(transition: .immediate) + } + ), + availableSize: context.availableSize, + transition: context.transition + ) + context.add(languageSelectionMenu + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + } + + return context.availableSize + } + } +} + +public class TextProcessingScreen: ViewControllerComponentContainer { + private let context: AccountContext + + public init( + context: AccountContext, + inputText: TextWithEntities, + copyResult: @escaping (TextWithEntities) -> Void, + completion: @escaping (TextWithEntities) -> Void + ) { + self.context = context + + super.init( + context: context, + component: TextProcessingSheetComponent( + context: context, + inputText: inputText, + copyCurrentResult: copyResult, + completion: completion + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: .default + ) + + self.statusBar.statusBarStyle = .Ignore + self.navigationPresentation = .flatModal + self.blocksBackgroundWhenInOverlay = true + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent.View.Tag()) as? ResizableSheetComponent.View { + view.dismissAnimated() + } + } +} + +private final class TitleComponent: Component { + let theme: PresentationTheme + let title: String + let isProcessing: Bool + + init( + theme: PresentationTheme, + title: String, + isProcessing: Bool + ) { + self.theme = theme + self.title = title + self.isProcessing = isProcessing + } + + static func ==(lhs: TitleComponent, rhs: TitleComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.isProcessing != rhs.isProcessing { + return false + } + return true + } + + final class View: UIView { + private var animationIcon: ComponentView? + private let title = ComponentView() + + private var component: TitleComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: TitleComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + let titleFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.addSubview(titleView) + } + titleView.frame = titleFrame + } + + if component.isProcessing { + let animationIcon: ComponentView + var animationIconTransition = transition + if let current = self.animationIcon { + animationIcon = current + } else { + animationIconTransition = animationIconTransition.withAnimation(.none) + animationIcon = ComponentView() + self.animationIcon = animationIcon + } + + let animationIconSize = animationIcon.update( + transition: animationIconTransition, + component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent( + name: "SparklesEmoji" + ), + placeholderColor: nil, + startingPosition: .begin, + size: CGSize(width: 30.0, height: 30.0), + loop: true + )), + environment: {}, + containerSize: CGSize(width: 30.0, height: 30.0) + ) + let animationIconFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: titleFrame.minY + floorToScreenPixels((titleFrame.height - animationIconSize.height) * 0.5) - 2.0), size: animationIconSize) + if let animationIconView = animationIcon.view { + if animationIconView.superview == nil { + self.addSubview(animationIconView) + animationIconView.alpha = 0.0 + } + animationIconTransition.setFrame(view: animationIconView, frame: animationIconFrame) + transition.setAlpha(view: animationIconView, alpha: 1.0) + } + } else { + if let animationIcon = self.animationIcon { + self.animationIcon = nil + if let animationIconView = animationIcon.view { + transition.setAlpha(view: animationIconView, alpha: 0.0, completion: { [weak animationIconView] _ in + animationIconView?.removeFromSuperview() + }) + } + } + } + + return titleSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingStyleSelectionComponent.swift b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingStyleSelectionComponent.swift new file mode 100644 index 0000000000..f740f590ef --- /dev/null +++ b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingStyleSelectionComponent.swift @@ -0,0 +1,273 @@ +import Foundation +import UIKit +import Display +import TelegramPresentationData +import ComponentFlow +import MultilineTextComponent +import TelegramCore + +final class TextProcessingStyleSelectionComponent: Component { + let theme: PresentationTheme + let strings: PresentationStrings + let selectedStyle: TelegramComposeAIMessageMode.Style + let updateStyle: (TelegramComposeAIMessageMode.Style) -> Void + + init( + theme: PresentationTheme, + strings: PresentationStrings, + selectedStyle: TelegramComposeAIMessageMode.Style, + updateStyle: @escaping (TelegramComposeAIMessageMode.Style) -> Void + ) { + self.theme = theme + self.strings = strings + self.selectedStyle = selectedStyle + self.updateStyle = updateStyle + } + + static func ==(lhs: TextProcessingStyleSelectionComponent, rhs: TextProcessingStyleSelectionComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.selectedStyle != rhs.selectedStyle { + return false + } + return true + } + + final class View: UIView { + private var component: TextProcessingStyleSelectionComponent? + private weak var state: EmptyComponentState? + private var isUpdating: Bool = false + + private var itemViews: [TelegramComposeAIMessageMode.Style: ComponentView] = [:] + private let selectedBackgroundView: UIImageView + + override init(frame: CGRect) { + self.selectedBackgroundView = UIImageView() + self.selectedBackgroundView.isHidden = true + self.selectedBackgroundView.alpha = 0.0 + + super.init(frame: frame) + + self.addSubview(self.selectedBackgroundView) + + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:)))) + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + @objc private func onTapGesture(_ recognizer: UITapGestureRecognizer) { + guard let component = self.component else { + return + } + if case .ended = recognizer.state { + for (id, itemView) in self.itemViews { + if let itemComponentView = itemView.view { + if itemComponentView.bounds.contains(self.convert(recognizer.location(in: self), to: itemComponentView)) { + component.updateStyle(id) + break + } + } + } + } + } + + func update(component: TextProcessingStyleSelectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.2) + + self.component = component + self.state = state + + var styleData: [(id: TelegramComposeAIMessageMode.Style, icon: String, title: String)] = [] + styleData.append((.neutral, "🏳️", "Neutral")) + styleData.append((.formal, "🤝", "Formal")) + styleData.append((.short, "🎯", "Short")) + styleData.append((.savage, "🍖", "Savage")) + styleData.append((.biblical, "🕯", "Biblical")) + styleData.append((.posh, "🍷", "Posh")) + + let itemSize = CGSize(width: floor(availableSize.width / CGFloat(styleData.count)), height: availableSize.height) + var selectedItemFrame: CGRect? + for i in 0 ..< styleData.count { + let style = styleData[i] + let itemView: ComponentView + var itemTransition = transition + if let current = self.itemViews[style.id] { + itemView = current + } else { + itemTransition = itemTransition.withAnimation(.none) + itemView = ComponentView() + self.itemViews[style.id] = itemView + } + let itemFrame = CGRect(origin: CGPoint(x: CGFloat(i) * itemSize.width, y: 0.0), size: itemSize) + let _ = itemView.update( + transition: itemTransition, + component: AnyComponent(ItemComponent( + theme: component.theme, + icon: style.icon, + title: style.title + )), + environment: {}, + containerSize: itemSize + ) + if let itemComponentView = itemView.view { + if itemComponentView.superview == nil { + self.addSubview(itemComponentView) + } + itemTransition.setFrame(view: itemComponentView, frame: itemFrame) + } + if style.id == component.selectedStyle { + selectedItemFrame = CGRect(origin: CGPoint(x: itemFrame.minX, y: itemFrame.minY - 5.0), size: CGSize(width: itemFrame.width, height: itemFrame.height + 5.0 + 3.0)) + } + } + + if self.selectedBackgroundView.image == nil { + self.selectedBackgroundView.image = generateStretchableFilledCircleImage(diameter: 16.0 * 2.0, color: .white)?.withRenderingMode(.alwaysTemplate) + } + self.selectedBackgroundView.tintColor = component.theme.list.itemHighlightedBackgroundColor.withMultipliedAlpha(0.6) + + if let selectedItemFrame { + var selectedBackgroundTransition = transition + if self.selectedBackgroundView.isHidden { + self.selectedBackgroundView.isHidden = false + selectedBackgroundTransition = selectedBackgroundTransition.withAnimation(.none) + } + selectedBackgroundTransition.setFrame(view: self.selectedBackgroundView, frame: selectedItemFrame) + alphaTransition.setAlpha(view: self.selectedBackgroundView, alpha: 1.0) + } else { + if !self.selectedBackgroundView.isHidden { + alphaTransition.setAlpha(view: self.selectedBackgroundView, alpha: 0.0, completion: { [weak self] flag in + guard let self, flag else { + return + } + self.selectedBackgroundView.isHidden = true + }) + } + } + + return CGSize(width: availableSize.width, height: availableSize.height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class ItemComponent: Component { + let theme: PresentationTheme + let icon: String + let title: String + + init( + theme: PresentationTheme, + icon: String, + title: String + ) { + self.theme = theme + self.icon = icon + self.title = title + } + + static func ==(lhs: ItemComponent, rhs: ItemComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.icon != rhs.icon { + return false + } + if lhs.title != rhs.title { + return false + } + return true + } + + final class View: UIView { + private var imageIcon: ComponentView? + private let title = ComponentView() + + private var component: ItemComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: ItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let iconTintColor = component.theme.list.itemPrimaryTextColor + + let imageIcon: ComponentView + var iconTransition = transition + if let current = self.imageIcon { + imageIcon = current + } else { + iconTransition = iconTransition.withAnimation(.none) + imageIcon = ComponentView() + self.imageIcon = imageIcon + } + + let iconSize = imageIcon.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.icon, font: Font.regular(25.0), textColor: .black)) + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: -1.0), size: iconSize) + if let imageIconView = imageIcon.view { + if imageIconView.superview == nil { + self.addSubview(imageIconView) + } + iconTransition.setFrame(view: imageIconView, frame: iconFrame) + } + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.semibold(10.0), textColor: iconTintColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - 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) + } + titleView.frame = titleFrame + } + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingTextAreaComponent.swift b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingTextAreaComponent.swift new file mode 100644 index 0000000000..8c28d97be2 --- /dev/null +++ b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingTextAreaComponent.swift @@ -0,0 +1,532 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Display +import TelegramPresentationData +import ComponentFlow +import AccountContext +import MultilineTextComponent +import BundleIconComponent +import TelegramCore +import MultilineTextWithEntitiesComponent +import TextFormat +import PlainButtonComponent +import CheckComponent +import ShimmerEffect + +final class TextProcessingTextAreaComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let titlePrefix: String + let title: String + let titleAction: ((UIView) -> Void)? + let isExpanded: (value: Bool, toggle: () -> Void)? + let copyAction: (() -> Void)? + let emojify: (value: Bool, toggle: () -> Void)? + let text: TextWithEntities? + let textCorrectionRanges: [Range] + + init( + context: AccountContext, + theme: PresentationTheme, + titlePrefix: String, + title: String, + titleAction: ((UIView) -> Void)?, + isExpanded: (value: Bool, toggle: () -> Void)?, + copyAction: (() -> Void)?, + emojify: (value: Bool, toggle: () -> Void)?, + text: TextWithEntities?, + textCorrectionRanges: [Range] + ) { + self.context = context + self.theme = theme + self.titlePrefix = titlePrefix + self.isExpanded = isExpanded + self.copyAction = copyAction + self.title = title + self.titleAction = titleAction + self.emojify = emojify + self.text = text + self.textCorrectionRanges = textCorrectionRanges + } + + static func ==(lhs: TextProcessingTextAreaComponent, rhs: TextProcessingTextAreaComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.titlePrefix != rhs.titlePrefix { + return false + } + if lhs.title != rhs.title { + return false + } + if (lhs.titleAction == nil) != (rhs.titleAction == nil) { + return false + } + if lhs.isExpanded?.value != rhs.isExpanded?.value { + return false + } + if (lhs.copyAction == nil) != (rhs.copyAction == nil) { + return false + } + if lhs.emojify?.value != rhs.emojify?.value { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.textCorrectionRanges != rhs.textCorrectionRanges { + return false + } + return true + } + + final class View: UIView { + private var component: TextProcessingTextAreaComponent? + private weak var state: EmptyComponentState? + private var isUpdating: Bool = false + + private let titlePrefix = ComponentView() + private let title = ComponentView() + private var titleArrow: ComponentView? + private var emojify: ComponentView? + private let titleButton: HighlightTrackingButton + + private let textState = MultilineTextWithEntitiesComponent.External() + private let textContainer: UIView + private let text = ComponentView() + private var expandShadow: UIImageView? + private var expandButton: ComponentView? + + private let copyButton = ComponentView() + + private let measureLoadingTextState = MultilineTextWithEntitiesComponent.External() + private let measureLoadingText = ComponentView() + private var shimmerEffectNode: ShimmerEffectNode? + + override init(frame: CGRect) { + self.textContainer = UIView() + self.textContainer.clipsToBounds = true + + self.titleButton = HighlightTrackingButton() + + super.init(frame: frame) + + self.addSubview(self.textContainer) + self.addSubview(self.titleButton) + + self.titleButton.highligthedChanged = { [weak self] highighed in + guard let self, let titleView = self.title.view, let titleArrowView = self.titleArrow?.view else { + return + } + if highighed { + titleView.alpha = 0.6 + titleArrowView.alpha = 0.6 + } else { + let transition: ComponentTransition = .easeInOut(duration: 0.25) + transition.setAlpha(view: titleView, alpha: 1.0) + transition.setAlpha(view: titleArrowView, alpha: 1.0) + } + } + self.titleButton.addTarget(self, action: #selector(self.titleButtonPressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + @objc private func titleButtonPressed() { + guard let component = self.component, let titleView = self.title.view else { + return + } + component.titleAction?(titleView) + } + + func update(component: TextProcessingTextAreaComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.2) + + self.component = component + self.state = state + + self.titleButton.isUserInteractionEnabled = component.titleAction != nil + + let topInset: CGFloat = 0.0 + let bottomInset: CGFloat = 0.0 + let sideInset: CGFloat = 0.0 + + var contentHeight: CGFloat = 0.0 + contentHeight += topInset + + let titlePrefixSize = self.titlePrefix.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.titlePrefix, font: Font.semibold(13.0), textColor: component.theme.list.itemSecondaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 10.0, height: 100.0) + ) + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.semibold(13.0), textColor: component.theme.list.itemAccentColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 10.0, height: 100.0) + ) + + let titlePrefixFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: titlePrefixSize) + var titleFrame = CGRect(origin: CGPoint(x: titlePrefixFrame.maxX, y: titlePrefixFrame.minY), size: titleSize) + if !component.titlePrefix.isEmpty { + titleFrame.origin.x += 3.0 + } + + transition.setFrame(view: self.titleButton, frame: titleFrame.insetBy(dx: -10.0, dy: -10.0)) + + if let titlePrefixView = self.titlePrefix.view { + if titlePrefixView.superview == nil { + titlePrefixView.layer.anchorPoint = CGPoint() + titlePrefixView.isUserInteractionEnabled = false + self.addSubview(titlePrefixView) + } + titlePrefixView.bounds = CGRect(origin: CGPoint(), size: titlePrefixFrame.size) + transition.setPosition(view: titlePrefixView, position: titlePrefixFrame.origin) + } + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.layer.anchorPoint = CGPoint() + titleView.isUserInteractionEnabled = false + self.addSubview(titleView) + } + titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + transition.setPosition(view: titleView, position: titleFrame.origin) + } + + if component.titleAction != nil { + let titleArrow: ComponentView + var titleArrowTransition = transition + if let current = self.titleArrow { + titleArrow = current + } else { + titleArrowTransition = titleArrowTransition.withAnimation(.none) + titleArrow = ComponentView() + self.titleArrow = titleArrow + } + let titleArrowSize = titleArrow.update( + transition: titleArrowTransition, + component: AnyComponent(BundleIconComponent( + name: "Item List/ExpandableSelectorArrows", tintColor: component.theme.list.itemAccentColor.withMultipliedAlpha(0.8))), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + let titleArrowFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 2.0, y: titleFrame.minY + floorToScreenPixels((titleFrame.height - titleArrowSize.height) * 0.5)), size: titleArrowSize) + if let titleArrowView = titleArrow.view { + if titleArrowView.superview == nil { + titleArrowView.isUserInteractionEnabled = false + self.addSubview(titleArrowView) + } + transition.setFrame(view: titleArrowView, frame: titleArrowFrame) + } + } else { + if let titleArrow = self.titleArrow { + self.titleArrow = nil + titleArrow.view?.removeFromSuperview() + } + } + + if let emojifyValue = component.emojify { + let emojify: ComponentView + var emojifyTransition = transition + if let current = self.emojify { + emojify = current + } else { + emojify = ComponentView() + self.emojify = emojify + emojifyTransition = emojifyTransition.withAnimation(.none) + } + let checkTheme = CheckComponent.Theme( + backgroundColor: component.theme.list.itemCheckColors.fillColor, + strokeColor: component.theme.list.itemCheckColors.foregroundColor, + borderColor: component.theme.list.itemCheckColors.strokeColor, + overlayBorder: false, + hasInset: false, + hasShadow: false + ) + let emojifySize = emojify.update( + transition: emojifyTransition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(HStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(CheckComponent( + theme: checkTheme, + size: CGSize(width: 16.0, height: 16.0), + selected: emojifyValue.value + ))), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "Emojify", font: Font.semibold(13.0), textColor: component.theme.list.itemSecondaryTextColor)) + ))) + ], spacing: 7.0)), + effectAlignment: .center, + action: { + emojifyValue.toggle() + }, + animateAlpha: false, + animateScale: false + )), + environment: { + }, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let emojifyFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - emojifySize.width, y: contentHeight), size: emojifySize) + if let emojifyView = emojify.view { + if emojifyView.superview == nil { + self.addSubview(emojifyView) + } + emojifyTransition.setFrame(view: emojifyView, frame: emojifyFrame) + } + } else { + if let emojify = self.emojify { + self.emojify = nil + emojify.view?.removeFromSuperview() + } + } + + contentHeight += 25.0 + + let fontSize: CGFloat = 17.0 + let textValue = NSMutableAttributedString(attributedString: stringWithAppliedEntities( + component.text?.text ?? "", + entities: component.text?.entities ?? [], + baseColor: component.theme.list.itemPrimaryTextColor, + linkColor: component.theme.list.itemAccentColor, + baseFont: Font.regular(fontSize), + linkFont: Font.regular(fontSize), + boldFont: Font.semibold(fontSize), + italicFont: Font.italic(fontSize), + boldItalicFont: Font.semiboldItalic(fontSize), + fixedFont: Font.monospace(fontSize), + blockQuoteFont: Font.monospace(fontSize), + message: nil + )) + for range in component.textCorrectionRanges { + if range.lowerBound >= 0 && range.upperBound < textValue.length { + textValue.addAttributes([ + .underlineColor: component.theme.list.itemAccentColor, + .underlineStyle: NSUnderlineStyle.patternDot.rawValue + ], range: NSRange(location: range.lowerBound, length: range.upperBound - range.lowerBound)) + } + } + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent(MultilineTextWithEntitiesComponent( + external: self.textState, + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: component.theme.list.mediaPlaceholderColor, + text: .plain(textValue), + maximumNumberOfLines: 0, + lineSpacing: 0.12, + cutout: nil, + insets: UIEdgeInsets(), + spoilerColor: component.theme.list.itemPrimaryTextColor, + enableLooping: true, + displaysAsynchronously: false, + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) + ) + let textFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: textSize) + if let textView = self.text.view { + if textView.superview == nil { + textView.layer.anchorPoint = CGPoint() + self.textContainer.addSubview(textView) + } + textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size) + } + + var textContainerFrame = textFrame + if let isExpanded = component.isExpanded, let textLayout = self.textState.layout, textLayout.numberOfLines > 1 { + if !isExpanded.value, let firstLineRect = textLayout.linesRects().first { + textContainerFrame.size.height = firstLineRect.maxY - 14.0 + } + + let expandButton: ComponentView + var expandButtonTransition = transition + if let current = self.expandButton { + expandButton = current + } else { + expandButtonTransition = expandButtonTransition.withAnimation(.none) + expandButton = ComponentView() + self.expandButton = expandButton + } + let expandShadow: UIImageView + if let current = self.expandShadow { + expandShadow = current + } else { + expandShadow = UIImageView() + self.expandShadow = expandShadow + self.addSubview(expandShadow) + } + let expandShadowExtent: CGFloat = 20.0 + if expandShadow.image == nil { + let baseSize: CGFloat = 20.0 + expandShadow.image = generateImage(CGSize(width: baseSize + expandShadowExtent * 2.0, height: baseSize + expandShadowExtent * 2.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + let colors: [CGColor] = [UIColor.white.cgColor, UIColor.white.withAlphaComponent(0.0).cgColor] + let locations: [CGFloat] = [0.0, 1.0] + if let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: colors as CFArray, locations: locations) { + let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + context.drawRadialGradient(gradient, startCenter: center, startRadius: baseSize / 2.0, endCenter: center, endRadius: size.width / 2.0, options: [.drawsBeforeStartLocation, .drawsAfterEndLocation]) + } + })?.withRenderingMode(.alwaysTemplate).stretchableImage(withLeftCapWidth: Int(baseSize / 2.0 + expandShadowExtent), topCapHeight: Int(baseSize / 2.0 + expandShadowExtent)) + } + expandShadow.tintColor = component.theme.list.itemBlocksBackgroundColor + + let expandButtonSize = expandButton.update( + transition: expandButtonTransition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: isExpanded.value ? "less" : "more", font: Font.regular(17.0), textColor: component.theme.list.itemAccentColor)) + )), + effectAlignment: .right, + action: { + isExpanded.toggle() + }, + animateAlpha: false, + animateScale: false, + animateContents: false + )), + environment: {}, + containerSize: CGSize(width: 200.0, height: 100.0) + ) + let expandButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - expandButtonSize.width, y: textContainerFrame.maxY - expandButtonSize.height - 2.0), size: expandButtonSize) + if let expandButtonView = expandButton.view { + if expandButtonView.superview == nil { + expandButtonView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.0) + self.addSubview(expandButtonView) + } + expandButtonTransition.setPosition(view: expandButtonView, position: CGPoint(x: expandButtonFrame.maxX, y: expandButtonFrame.minY)) + expandButtonTransition.setBounds(view: expandButtonView, bounds: CGRect(origin: CGPoint(), size: expandButtonFrame.size)) + + expandButtonTransition.setFrame(view: expandShadow, frame: expandButtonFrame.insetBy(dx: -expandShadowExtent, dy: -expandShadowExtent)) + } + } else { + if let expandButton = self.expandButton { + self.expandButton = nil + expandButton.view?.removeFromSuperview() + } + if let expandShadow = self.expandShadow { + self.expandShadow = nil + expandShadow.removeFromSuperview() + } + } + + if component.text == nil { + let shimmerEffectNode: ShimmerEffectNode + if let current = self.shimmerEffectNode { + shimmerEffectNode = current + } else { + shimmerEffectNode = ShimmerEffectNode() + self.shimmerEffectNode = shimmerEffectNode + self.addSubview(shimmerEffectNode.view) + } + + let measureLoadingTextSize = self.measureLoadingText.update( + transition: .immediate, + component: AnyComponent(MultilineTextWithEntitiesComponent( + external: self.measureLoadingTextState, + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: component.theme.list.mediaPlaceholderColor, + text: .plain(NSAttributedString(string: "a\na\na\na", font: Font.regular(fontSize), textColor: .black)), + maximumNumberOfLines: 0, + lineSpacing: 0.12, + cutout: nil, + insets: UIEdgeInsets(), + spoilerColor: component.theme.list.itemPrimaryTextColor, + enableLooping: true, + displaysAsynchronously: false, + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) + ) + textContainerFrame.size = CGSize(width: availableSize.width, height: measureLoadingTextSize.height) + + shimmerEffectNode.frame = textContainerFrame + + var shapes: [ShimmerEffectNode.Shape] = [] + if let textLayout = self.measureLoadingTextState.layout { + let lineWidths: [CGFloat] = [1.0, 0.9, 1.0, 0.8] + var index = 0 + for lineRect in textLayout.linesRects() { + shapes.append(.roundedRectLine(startPoint: CGPoint(x: 0.0, y: lineRect.midY - 18.0), width: floor(textContainerFrame.width * lineWidths[index % lineWidths.count]), diameter: 6.0)) + index += 1 + } + } + shimmerEffectNode.updateAbsoluteRect(shimmerEffectNode.bounds, within: shimmerEffectNode.bounds.size) + shimmerEffectNode.update(backgroundColor: component.theme.list.plainBackgroundColor, foregroundColor: component.theme.list.mediaPlaceholderColor, shimmeringColor: component.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: shimmerEffectNode.bounds.size) + } else { + if let shimmerEffectNode = self.shimmerEffectNode { + self.shimmerEffectNode = nil + shimmerEffectNode.view.removeFromSuperview() + } + } + + if let copyAction = component.copyAction { + let copyButtonSize = self.copyButton.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(BundleIconComponent( + name: "Chat/Context Menu/Copy", + tintColor: component.theme.list.itemAccentColor + )), + effectAlignment: .right, + action: { + copyAction() + }, + animateAlpha: true, + animateScale: false, + animateContents: false + )), + environment: {}, + containerSize: CGSize(width: 200.0, height: 100.0) + ) + let copyButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - copyButtonSize.width, y: textContainerFrame.maxY - copyButtonSize.height - 2.0), size: copyButtonSize) + if let copyButtonView = self.copyButton.view { + if copyButtonView.superview == nil { + copyButtonView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.0) + self.addSubview(copyButtonView) + } + transition.setPosition(view: copyButtonView, position: CGPoint(x: copyButtonFrame.maxX, y: copyButtonFrame.minY)) + transition.setBounds(view: copyButtonView, bounds: CGRect(origin: CGPoint(), size: copyButtonFrame.size)) + alphaTransition.setAlpha(view: copyButtonView, alpha: component.text != nil ? 1.0 : 0.0) + } + } + + transition.setFrame(view: self.textContainer, frame: textContainerFrame) + contentHeight += textContainerFrame.height + + contentHeight += bottomInset + + return CGSize(width: availableSize.width, height: contentHeight) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + diff --git a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingTranslateContentComponent.swift b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingTranslateContentComponent.swift new file mode 100644 index 0000000000..e958157ed4 --- /dev/null +++ b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingTranslateContentComponent.swift @@ -0,0 +1,460 @@ +import Foundation +import NaturalLanguage +import UIKit +import SwiftSignalKit +import Display +import TelegramPresentationData +import ComponentFlow +import AccountContext +import MultilineTextComponent +import BundleIconComponent +import TelegramCore +import TranslateUI + +private let languageRecognizer = NLLanguageRecognizer() + +func localizedLanguageName(strings: PresentationStrings, language: String) -> String { + let toLang = language + let key = "Translation.Language.\(toLang)" + let translateTitle: String + if let string = strings.primaryComponent.dict[key] { + translateTitle = string + } else { + let languageLocale = Locale(identifier: language) + let toLanguage = languageLocale.localizedString(forLanguageCode: toLang) ?? "" + return toLanguage + } + return translateTitle +} + +final class TextProcessingTranslateContentComponent: Component { + enum Mode { + case translate + case stylize + case fix + } + + final class ExternalState { + fileprivate(set) var sourceLanguage: String? + + fileprivate(set) var result: (language: String, text: TextWithEntities?, textCorrectionRanges: [Range])? = nil { + didSet { + if self.result?.language != oldValue?.language || self.result?.text != oldValue?.text { + self.resultUpdated?(self.result) + } + } + } + var resultUpdated: (((language: String, text: TextWithEntities?, textCorrectionRanges: [Range])?) -> Void)? + + fileprivate(set) var emojify: Bool = false + fileprivate(set) var isSourceTextExpanded: Bool = false + fileprivate(set) var style: TelegramComposeAIMessageMode.Style = .neutral + + fileprivate(set) var isProcessing: Bool = false { + didSet { + if self.isProcessing != oldValue { + self.isProcessingUpdated?(self.isProcessing) + } + } + } + var isProcessingUpdated: ((Bool) -> Void)? + + init() { + } + } + + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let inputText: TextWithEntities + let externalState: ExternalState + let mode: Mode + let copyAction: () -> Void + let displayLanguageSelectionMenu: (UIView, String, TelegramComposeAIMessageMode.Style, Bool, @escaping (String, TelegramComposeAIMessageMode.Style) -> Void) -> Void + + init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + externalState: ExternalState, + inputText: TextWithEntities, + mode: Mode, + copyAction: @escaping () -> Void, + displayLanguageSelectionMenu: @escaping (UIView, String, TelegramComposeAIMessageMode.Style, Bool, @escaping (String, TelegramComposeAIMessageMode.Style) -> Void) -> Void + ) { + self.context = context + self.theme = theme + self.strings = strings + self.externalState = externalState + self.inputText = inputText + self.mode = mode + self.copyAction = copyAction + self.displayLanguageSelectionMenu = displayLanguageSelectionMenu + } + + static func ==(lhs: TextProcessingTranslateContentComponent, rhs: TextProcessingTranslateContentComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.externalState !== rhs.externalState { + return false + } + if lhs.inputText != rhs.inputText { + return false + } + if lhs.mode != rhs.mode { + return false + } + return true + } + + final class View: UIView { + private let sourceText = ComponentView() + private let targetText = ComponentView() + private let separatorLayer: SimpleLayer + + private var component: TextProcessingTranslateContentComponent? + private weak var state: EmptyComponentState? + private var isUpdating: Bool = false + + private var processDisposable: Disposable? + + override init(frame: CGRect) { + self.separatorLayer = SimpleLayer() + + super.init(frame: frame) + + self.layer.addSublayer(self.separatorLayer) + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + deinit { + self.processDisposable?.dispose() + } + + private func beginTranslationIfNecessary(reset: Bool) { + guard let component = self.component else { + return + } + + if reset { + self.processDisposable?.dispose() + self.processDisposable = nil + if let result = component.externalState.result { + component.externalState.result = (result.language, nil, []) + } + } + + if let result = component.externalState.result, result.text == nil, self.processDisposable == nil { + switch component.mode { + case .translate: + component.externalState.isProcessing = true + self.processDisposable = (component.context.engine.messages.composeAIMessage( + text: component.inputText, + mode: .translate(toLanguage: result.language, emojify: component.externalState.emojify, style: component.externalState.style) + ) |> deliverOnMainQueue).startStrict(next: { [weak self] processedText in + guard let self, let component = self.component, let processedText else { + return + } + component.externalState.isProcessing = false + component.externalState.result = (result.language, processedText.text, processedText.diffRanges) + if !self.isUpdating { + self.state?.updated(transition: .spring(duration: 0.4)) + } + }) + case .stylize: + if !component.externalState.emojify && component.externalState.style == .neutral { + component.externalState.isProcessing = false + component.externalState.result = (result.language, component.inputText, []) + if !self.isUpdating { + self.state?.updated(transition: .spring(duration: 0.4)) + } + } else { + component.externalState.isProcessing = true + self.processDisposable = (component.context.engine.messages.composeAIMessage( + text: component.inputText, + mode: .stylize(emojify: component.externalState.emojify, style: component.externalState.style) + ) |> deliverOnMainQueue).startStrict(next: { [weak self] processedText in + guard let self, let component = self.component, let processedText else { + return + } + component.externalState.isProcessing = false + component.externalState.result = (result.language, processedText.text, processedText.diffRanges) + if !self.isUpdating { + self.state?.updated(transition: .spring(duration: 0.4)) + } + }) + } + case .fix: + component.externalState.isProcessing = true + self.processDisposable = (component.context.engine.messages.composeAIMessage( + text: component.inputText, + mode: .proofread + ) |> deliverOnMainQueue).startStrict(next: { [weak self] processedText in + guard let self, let component = self.component, let processedText else { + return + } + component.externalState.isProcessing = false + component.externalState.result = (result.language, processedText.text, processedText.diffRanges) + if !self.isUpdating { + self.state?.updated(transition: .spring(duration: 0.4)) + } + }) + } + } + } + + func update(component: TextProcessingTranslateContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + if component.externalState.sourceLanguage == nil { + languageRecognizer.processString(component.inputText.text) + let hypotheses = languageRecognizer.languageHypotheses(withMaximum: 3) + languageRecognizer.reset() + + let filteredLanguages = hypotheses.sorted(by: { $0.value > $1.value }) + if let first = filteredLanguages.first { + component.externalState.sourceLanguage = normalizeTranslationLanguage(first.key.rawValue) + } else { + component.externalState.sourceLanguage = "en" + } + } + + self.component = component + self.state = state + + if component.externalState.result == nil { + switch component.mode { + case .translate: + var languageCode = component.strings.baseLanguageCode + let rawSuffix = "-raw" + if languageCode.hasSuffix(rawSuffix) { + languageCode = String(languageCode.dropLast(rawSuffix.count)) + } + component.externalState.result = (languageCode, nil, []) + self.beginTranslationIfNecessary(reset: false) + case .stylize: + component.externalState.result = ("", component.inputText, []) + case .fix: + component.externalState.result = ("", nil, []) + self.beginTranslationIfNecessary(reset: false) + } + } + + var contentHeight: CGFloat = 0.0 + + let sideInset: CGFloat = 16.0 + let topInset: CGFloat = 17.0 + let bottomInset: CGFloat = 14.0 + let blockSpacing: CGFloat = 30.0 + + let fromPrefix: String + let toPrefix: String + var toTitle: String + switch component.mode { + case .translate: + fromPrefix = "From" + toPrefix = "To" + toTitle = localizedLanguageName(strings: component.strings, language: component.externalState.result?.language ?? "") + if component.externalState.style != .neutral { + toTitle.append(" (") + let styleName: String + switch component.externalState.style { + case .neutral: + styleName = "" + case .formal: + styleName = "Formal" + case .short: + styleName = "Short" + case .savage: + styleName = "Savage" + case .biblical: + styleName = "Biblical" + case .posh: + styleName = "Posh" + } + toTitle.append(styleName) + toTitle.append(")") + } + case .stylize, .fix: + fromPrefix = "Original:" + toPrefix = "Result" + toTitle = "" + } + + contentHeight += topInset + if case .stylize = component.mode { + let sourceTextSize = self.sourceText.update( + transition: transition, + component: AnyComponent(TextProcessingStyleSelectionComponent( + theme: component.theme, + strings: component.strings, + selectedStyle: component.externalState.style, + updateStyle: { [weak self] style in + guard let self, let component = self.component else { + return + } + if component.externalState.style != style { + component.externalState.style = style + + if let result = component.externalState.result { + component.externalState.result = (result.language, nil, []) + if !self.isUpdating { + self.state?.updated(transition: .spring(duration: 0.4)) + } + self.beginTranslationIfNecessary(reset: true) + } + } + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 46.0) + ) + let sourceTextFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: sourceTextSize) + contentHeight += sourceTextSize.height + + if let sourceTextView = self.sourceText.view { + if sourceTextView.superview == nil { + self.sourceText.parentState = state + self.addSubview(sourceTextView) + } + transition.setFrame(view: sourceTextView, frame: sourceTextFrame) + } + } else { + let sourceTextSize = self.sourceText.update( + transition: transition, + component: AnyComponent(TextProcessingTextAreaComponent( + context: component.context, + theme: component.theme, + titlePrefix: fromPrefix, + title: localizedLanguageName(strings: component.strings, language: component.externalState.sourceLanguage ?? ""), + titleAction: nil, + isExpanded: ( + component.externalState.isSourceTextExpanded, + { [weak self] in + guard let self, let component = self.component else { + return + } + component.externalState.isSourceTextExpanded = !component.externalState.isSourceTextExpanded + if !self.isUpdating { + self.state?.updated(transition: .spring(duration: 0.4)) + } + } + ), + copyAction: nil, + emojify: nil, + text: component.inputText, + textCorrectionRanges: [] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) + ) + + let sourceTextFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: sourceTextSize) + contentHeight += sourceTextSize.height + + if let sourceTextView = self.sourceText.view { + if sourceTextView.superview == nil { + self.sourceText.parentState = state + self.addSubview(sourceTextView) + } + transition.setFrame(view: sourceTextView, frame: sourceTextFrame) + } + } + + let targetTextSize = self.targetText.update( + transition: transition, + component: AnyComponent(TextProcessingTextAreaComponent( + context: component.context, + theme: component.theme, + titlePrefix: toPrefix, + title: toTitle, + titleAction: component.mode == .translate ? { [weak self] sourceView in + guard let self, let component = self.component, let result = component.externalState.result else { + return + } + component.displayLanguageSelectionMenu(sourceView, result.language, component.externalState.style, true, { [weak self] language, style in + guard let self, let component = self.component else { + return + } + + if component.externalState.result?.language != language || component.externalState.style != style { + component.externalState.result = (language, nil, []) + component.externalState.style = style + + if !self.isUpdating { + self.state?.updated(transition: .spring(duration: 0.4)) + } + self.beginTranslationIfNecessary(reset: true) + } + }) + } : nil, + isExpanded: nil, + copyAction: { [weak self] in + guard let self, let component = self.component else { + return + } + component.copyAction() + }, + emojify: (component.mode == .translate || component.mode == .stylize) ? ( + component.externalState.emojify, + { [weak self] in + guard let self, let component = self.component else { + return + } + component.externalState.emojify = !component.externalState.emojify + + self.beginTranslationIfNecessary(reset: true) + if !self.isUpdating { + self.state?.updated(transition: .spring(duration: 0.4)) + } + } + ) : nil, + text: component.externalState.result?.text, + textCorrectionRanges: component.externalState.result?.textCorrectionRanges ?? [] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) + ) + + transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight + floorToScreenPixels((blockSpacing - UIScreenPixel) * 0.5) - 1.0), size: CGSize(width: availableSize.width - sideInset * 2.0, height: UIScreenPixel))) + self.separatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor + + contentHeight += blockSpacing + let targetTextFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: targetTextSize) + contentHeight += targetTextSize.height + + if let targetTextView = self.targetText.view { + if targetTextView.superview == nil { + self.targetText.parentState = state + self.addSubview(targetTextView) + } + transition.setFrame(view: targetTextView, frame: targetTextFrame) + } + + contentHeight += bottomInset + + return CGSize(width: availableSize.width, height: contentHeight) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Text/InputAIIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Text/InputAIIcon.imageset/Contents.json new file mode 100644 index 0000000000..ab14f3ed29 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Text/InputAIIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ai.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Text/InputAIIcon.imageset/ai.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Text/InputAIIcon.imageset/ai.pdf new file mode 100644 index 0000000000000000000000000000000000000000..dbb7ef7e49f42e1c55caba4c70bfb1649736b773 GIT binary patch literal 6317 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 k6$W{ik-u)%d!g#YaQ4AAB67`d;kCd literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/TextProcessing/TabStylize.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/TextProcessing/TabStylize.imageset/Contents.json new file mode 100644 index 0000000000..e8c9e43476 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/TextProcessing/TabStylize.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "style.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/TextProcessing/TabStylize.imageset/style.pdf b/submodules/TelegramUI/Images.xcassets/TextProcessing/TabStylize.imageset/style.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ab48ad8a28df78fbdceefbaa7cd4f384b57e8f13 GIT binary patch literal 7152 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 H+~9uzMpvp5 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Resources/Animations/SparklesEmoji.tgs b/submodules/TelegramUI/Resources/Animations/SparklesEmoji.tgs new file mode 100644 index 0000000000000000000000000000000000000000..9a97d10b76436759791b7024d34b1d90d741f7bf GIT binary patch literal 8368 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 GpaB3Y(hpex literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index 6ff78cc0b7..e5499a2cfc 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -4501,6 +4501,11 @@ extension ChatControllerImpl { return } self.controllerInteraction?.sendEmoji(text, attribute, immediately) + }, openAICompose: { [weak self] in + guard let self else { + return + } + self.chatDisplayNode.openAICompose() }, updateHistoryFilter: { [weak self] update in guard let self else { return diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index c751156f35..32713019b2 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -61,6 +61,8 @@ import LegacyChatHeaderPanelComponent import ChatSearchNavigationContentNode import GroupCallHeaderPanelComponent import PresentationDataUtils +import TextProcessingScreen +import Pasteboard final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem { let itemNode: OverlayMediaItemNode @@ -878,6 +880,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.textInputPanelNode = ChatTextInputPanelNode(context: context, presentationInterfaceState: chatPresentationInterfaceState, presentationContext: ChatPresentationContext(context: context, backgroundNode: backgroundNode), presentController: { [weak self] controller in self?.interfaceInteraction?.presentController(controller, nil) }) + self.textInputPanelNode?.isAIEnabled = true self.textInputPanelNode?.textInputAccessoryPanel = textInputAccessoryPanel self.textInputPanelNode?.textInputContextPanel = textInputContextPanel self.textInputPanelNode?.storedInputLanguage = chatPresentationInterfaceState.interfaceState.inputLanguage @@ -4386,8 +4389,58 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } } - func sendCurrentMessage(silentPosting: Bool? = nil, scheduleTime: Int32? = nil, repeatPeriod: Int32? = nil, postpone: Bool = false, messageEffect: ChatSendMessageEffect? = nil, completion: @escaping () -> Void = {}) { + func openAICompose() { + var effectivePresentationInterfaceState = self.chatPresentationInterfaceState + if let textInputPanelNode = self.textInputPanelNode { + effectivePresentationInterfaceState = effectivePresentationInterfaceState.updatedInterfaceState { $0.withUpdatedEffectiveInputState(textInputPanelNode.inputTextState) } + } + + let effectiveInputText: NSAttributedString + if effectivePresentationInterfaceState.interfaceState.editMessage != nil && effectivePresentationInterfaceState.interfaceState.postSuggestionState != nil { + effectiveInputText = expandedInputStateAttributedString(effectivePresentationInterfaceState.interfaceState.effectiveInputState.inputText) + } else { + effectiveInputText = expandedInputStateAttributedString(effectivePresentationInterfaceState.interfaceState.composeInputState.inputText) + } + + if effectiveInputText.length == 0 { + return + } + + let inputText = trimChatInputText(effectiveInputText) + var entities: [MessageTextEntity] = [] + if inputText.length != 0 { + if case let .customChatContents(customChatContents) = self.chatPresentationInterfaceState.subject, case .businessLinkSetup = customChatContents.kind { + entities = generateChatInputTextEntities(inputText, generateLinks: false) + } else { + entities = generateTextEntities(inputText.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(inputText, maxAnimatedEmojisInText: 0)) + } + } + + self.controller?.push(TextProcessingScreen( + context: self.context, + inputText: TextWithEntities(text: inputText.string, entities: entities), + copyResult: { [weak self] text in + guard let self else { + return + } + let _ = self + storeMessageTextInPasteboard(text.text, entities: text.entities) + }, + completion: { [weak self] text in + guard let self, let controller = self.controller else { + return + } + controller.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in + return state.updatedInterfaceState { interfaceState in + return interfaceState.withUpdatedEffectiveInputState(ChatTextInputState(inputText: chatInputStateStringWithAppliedEntities(text.text, entities: text.entities))) + } + }) + } + )) + } + + func sendCurrentMessage(silentPosting: Bool? = nil, scheduleTime: Int32? = nil, repeatPeriod: Int32? = nil, postpone: Bool = false, messageEffect: ChatSendMessageEffect? = nil, completion: @escaping () -> Void = {}) { guard let textInputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode else { return } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift index b2302adb43..9783d7befa 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift @@ -460,6 +460,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState let panel = ChatTextInputPanelNode(context: context, presentationInterfaceState: chatPresentationInterfaceState, presentationContext: nil, presentController: { [weak interfaceInteraction] controller in interfaceInteraction?.presentController(controller, nil) }) + panel.isAIEnabled = true panel.textInputAccessoryPanel = textInputAccessoryPanel panel.textInputContextPanel = textInputContextPanel panel.chatControllerInteraction = chatControllerInteraction diff --git a/submodules/TelegramUI/Sources/MusicListenTracker.swift b/submodules/TelegramUI/Sources/MusicListenTracker.swift index 0b9d0de397..e1786cec62 100644 --- a/submodules/TelegramUI/Sources/MusicListenTracker.swift +++ b/submodules/TelegramUI/Sources/MusicListenTracker.swift @@ -172,9 +172,7 @@ final class MusicListenTracker { let duration = self.accumulatedDuration let trackDuration = self.trackDuration - // Check minimum threshold: min(30s, 90% of track) - let threshold = min(30.0, trackDuration * 0.9) - if duration >= threshold && duration > 0 && trackDuration > 0 { + if duration >= 3.0 && trackDuration > 0 { let reportedDuration = Int(duration) self.reportDisposable.set( self.engine.messages.reportMusicListened(file: fileReference, duration: reportedDuration).startStrict()