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 0000000000..dbb7ef7e49 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Input/Text/InputAIIcon.imageset/ai.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/TextProcessing/Contents.json b/submodules/TelegramUI/Images.xcassets/TextProcessing/Contents.json new file mode 100644 index 0000000000..6e965652df --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/TextProcessing/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/submodules/TelegramUI/Images.xcassets/TextProcessing/TabFix.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/TextProcessing/TabFix.imageset/Contents.json new file mode 100644 index 0000000000..5a6ec8204f --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/TextProcessing/TabFix.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "fix.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/TextProcessing/TabFix.imageset/fix.pdf b/submodules/TelegramUI/Images.xcassets/TextProcessing/TabFix.imageset/fix.pdf new file mode 100644 index 0000000000..1bb017c86b Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/TextProcessing/TabFix.imageset/fix.pdf differ 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 0000000000..ab48ad8a28 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/TextProcessing/TabStylize.imageset/style.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/TextProcessing/TabTranslate.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/TextProcessing/TabTranslate.imageset/Contents.json new file mode 100644 index 0000000000..b0911cb948 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/TextProcessing/TabTranslate.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "translate.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/TextProcessing/TabTranslate.imageset/translate.pdf b/submodules/TelegramUI/Images.xcassets/TextProcessing/TabTranslate.imageset/translate.pdf new file mode 100644 index 0000000000..508e0f3886 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/TextProcessing/TabTranslate.imageset/translate.pdf differ diff --git a/submodules/TelegramUI/Resources/Animations/SparklesEmoji.tgs b/submodules/TelegramUI/Resources/Animations/SparklesEmoji.tgs new file mode 100644 index 0000000000..9a97d10b76 Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/SparklesEmoji.tgs differ 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()