From 997bcf7edf528e35bc59227bef67337520d9ba67 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 30 May 2025 12:37:57 +0200 Subject: [PATCH 1/6] Update API --- submodules/TelegramApi/Sources/Api0.swift | 12 ++ submodules/TelegramApi/Sources/Api10.swift | 22 +++ submodules/TelegramApi/Sources/Api15.swift | 36 +++++ submodules/TelegramApi/Sources/Api16.swift | 36 +++++ submodules/TelegramApi/Sources/Api26.swift | 138 ++++++++++++++++++ submodules/TelegramApi/Sources/Api38.swift | 47 ++++++ .../Sources/Account/AccountManager.swift | 3 + .../ApiUtils/StoreMessage_Telegram.swift | 26 +++- .../ApiUtils/TelegramMediaAction.swift | 2 + .../Sources/ApiUtils/TelegramMediaTodo.swift | 33 +++++ .../PendingMessageUploadedContent.swift | 17 ++- .../Sources/State/Serialization.swift | 2 +- .../SyncCore_TelegramMediaAction.swift | 10 ++ .../SyncCore/SyncCore_TelegramMediaTodo.swift | 134 +++++++++++++++++ .../TelegramEngine/Messages/Media.swift | 7 + .../Messages/TelegramEngineMessages.swift | 8 + .../TelegramEngine/Messages/Todo.swift | 73 +++++++++ 17 files changed, 601 insertions(+), 5 deletions(-) create mode 100644 submodules/TelegramCore/Sources/ApiUtils/TelegramMediaTodo.swift create mode 100644 submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaTodo.swift create mode 100644 submodules/TelegramCore/Sources/TelegramEngine/Messages/Todo.swift diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 882d494eb8..be04611d4f 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -410,6 +410,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-440664550] = { return Api.InputMedia.parse_inputMediaPhotoExternal($0) } dict[261416433] = { return Api.InputMedia.parse_inputMediaPoll($0) } dict[-1979852936] = { return Api.InputMedia.parse_inputMediaStory($0) } + dict[-1614454818] = { return Api.InputMedia.parse_inputMediaTodo($0) } dict[58495792] = { return Api.InputMedia.parse_inputMediaUploadedDocument($0) } dict[505969924] = { return Api.InputMedia.parse_inputMediaUploadedPhoto($0) } dict[-1052959727] = { return Api.InputMedia.parse_inputMediaVenue($0) } @@ -606,6 +607,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1192749220] = { return Api.MessageAction.parse_messageActionStarGift($0) } dict[775611918] = { return Api.MessageAction.parse_messageActionStarGiftUnique($0) } dict[1474192222] = { return Api.MessageAction.parse_messageActionSuggestProfilePhoto($0) } + dict[-864265079] = { return Api.MessageAction.parse_messageActionTodoCompletions($0) } dict[228168278] = { return Api.MessageAction.parse_messageActionTopicCreate($0) } dict[-1064024032] = { return Api.MessageAction.parse_messageActionTopicEdit($0) } dict[-1262252875] = { return Api.MessageAction.parse_messageActionWebViewDataSent($0) } @@ -648,6 +650,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1766936791] = { return Api.MessageMedia.parse_messageMediaPhoto($0) } dict[1272375192] = { return Api.MessageMedia.parse_messageMediaPoll($0) } dict[1758159491] = { return Api.MessageMedia.parse_messageMediaStory($0) } + dict[-1974226924] = { return Api.MessageMedia.parse_messageMediaToDo($0) } dict[-1618676578] = { return Api.MessageMedia.parse_messageMediaUnsupported($0) } dict[784356159] = { return Api.MessageMedia.parse_messageMediaVenue($0) } dict[-571405253] = { return Api.MessageMedia.parse_messageMediaWebPage($0) } @@ -999,6 +1002,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1609668650] = { return Api.Theme.parse_theme($0) } dict[-94849324] = { return Api.ThemeSettings.parse_themeSettings($0) } dict[-7173643] = { return Api.Timezone.parse_timezone($0) } + dict[1287725239] = { return Api.TodoCompletion.parse_todoCompletion($0) } + dict[-878074577] = { return Api.TodoItem.parse_todoItem($0) } + dict[1236871718] = { return Api.TodoList.parse_todoList($0) } dict[-305282981] = { return Api.TopPeer.parse_topPeer($0) } dict[-39945236] = { return Api.TopPeerCategory.parse_topPeerCategoryBotsApp($0) } dict[344356834] = { return Api.TopPeerCategory.parse_topPeerCategoryBotsInline($0) } @@ -2203,6 +2209,12 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.Timezone: _1.serialize(buffer, boxed) + case let _1 as Api.TodoCompletion: + _1.serialize(buffer, boxed) + case let _1 as Api.TodoItem: + _1.serialize(buffer, boxed) + case let _1 as Api.TodoList: + _1.serialize(buffer, boxed) case let _1 as Api.TopPeer: _1.serialize(buffer, boxed) case let _1 as Api.TopPeerCategory: diff --git a/submodules/TelegramApi/Sources/Api10.swift b/submodules/TelegramApi/Sources/Api10.swift index fb6c088eef..95b7d1d12a 100644 --- a/submodules/TelegramApi/Sources/Api10.swift +++ b/submodules/TelegramApi/Sources/Api10.swift @@ -572,6 +572,7 @@ public extension Api { case inputMediaPhotoExternal(flags: Int32, url: String, ttlSeconds: Int32?) case inputMediaPoll(flags: Int32, poll: Api.Poll, correctAnswers: [Buffer]?, solution: String?, solutionEntities: [Api.MessageEntity]?) case inputMediaStory(peer: Api.InputPeer, id: Int32) + case inputMediaTodo(todo: Api.TodoList) case inputMediaUploadedDocument(flags: Int32, file: Api.InputFile, thumb: Api.InputFile?, mimeType: String, attributes: [Api.DocumentAttribute], stickers: [Api.InputDocument]?, videoCover: Api.InputPhoto?, videoTimestamp: Int32?, ttlSeconds: Int32?) case inputMediaUploadedPhoto(flags: Int32, file: Api.InputFile, stickers: [Api.InputDocument]?, ttlSeconds: Int32?) case inputMediaVenue(geoPoint: Api.InputGeoPoint, title: String, address: String, provider: String, venueId: String, venueType: String) @@ -712,6 +713,12 @@ public extension Api { peer.serialize(buffer, true) serializeInt32(id, buffer: buffer, boxed: false) break + case .inputMediaTodo(let todo): + if boxed { + buffer.appendInt32(-1614454818) + } + todo.serialize(buffer, true) + break case .inputMediaUploadedDocument(let flags, let file, let thumb, let mimeType, let attributes, let stickers, let videoCover, let videoTimestamp, let ttlSeconds): if boxed { buffer.appendInt32(58495792) @@ -798,6 +805,8 @@ public extension Api { return ("inputMediaPoll", [("flags", flags as Any), ("poll", poll as Any), ("correctAnswers", correctAnswers as Any), ("solution", solution as Any), ("solutionEntities", solutionEntities as Any)]) case .inputMediaStory(let peer, let id): return ("inputMediaStory", [("peer", peer as Any), ("id", id as Any)]) + case .inputMediaTodo(let todo): + return ("inputMediaTodo", [("todo", todo as Any)]) case .inputMediaUploadedDocument(let flags, let file, let thumb, let mimeType, let attributes, let stickers, let videoCover, let videoTimestamp, let ttlSeconds): return ("inputMediaUploadedDocument", [("flags", flags as Any), ("file", file as Any), ("thumb", thumb as Any), ("mimeType", mimeType as Any), ("attributes", attributes as Any), ("stickers", stickers as Any), ("videoCover", videoCover as Any), ("videoTimestamp", videoTimestamp as Any), ("ttlSeconds", ttlSeconds as Any)]) case .inputMediaUploadedPhoto(let flags, let file, let stickers, let ttlSeconds): @@ -1098,6 +1107,19 @@ public extension Api { return nil } } + public static func parse_inputMediaTodo(_ reader: BufferReader) -> InputMedia? { + var _1: Api.TodoList? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.TodoList + } + let _c1 = _1 != nil + if _c1 { + return Api.InputMedia.inputMediaTodo(todo: _1!) + } + else { + return nil + } + } public static func parse_inputMediaUploadedDocument(_ reader: BufferReader) -> InputMedia? { var _1: Int32? _1 = reader.readInt32() diff --git a/submodules/TelegramApi/Sources/Api15.swift b/submodules/TelegramApi/Sources/Api15.swift index ff3f9b70c2..8df4f48c65 100644 --- a/submodules/TelegramApi/Sources/Api15.swift +++ b/submodules/TelegramApi/Sources/Api15.swift @@ -383,6 +383,7 @@ public extension Api { case messageActionStarGift(flags: Int32, gift: Api.StarGift, message: Api.TextWithEntities?, convertStars: Int64?, upgradeMsgId: Int32?, upgradeStars: Int64?, fromId: Api.Peer?, peer: Api.Peer?, savedId: Int64?) case messageActionStarGiftUnique(flags: Int32, gift: Api.StarGift, canExportAt: Int32?, transferStars: Int64?, fromId: Api.Peer?, peer: Api.Peer?, savedId: Int64?, resaleStars: Int64?, canTransferAt: Int32?, canResellAt: Int32?) case messageActionSuggestProfilePhoto(photo: Api.Photo) + case messageActionTodoCompletions(completed: [Int32], incompleted: [Int32]) case messageActionTopicCreate(flags: Int32, title: String, iconColor: Int32, iconEmojiId: Int64?) case messageActionTopicEdit(flags: Int32, title: String?, iconEmojiId: Int64?, closed: Api.Bool?, hidden: Api.Bool?) case messageActionWebViewDataSent(text: String) @@ -789,6 +790,21 @@ public extension Api { } photo.serialize(buffer, true) break + case .messageActionTodoCompletions(let completed, let incompleted): + if boxed { + buffer.appendInt32(-864265079) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(completed.count)) + for item in completed { + serializeInt32(item, buffer: buffer, boxed: false) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(incompleted.count)) + for item in incompleted { + serializeInt32(item, buffer: buffer, boxed: false) + } + break case .messageActionTopicCreate(let flags, let title, let iconColor, let iconEmojiId): if boxed { buffer.appendInt32(228168278) @@ -920,6 +936,8 @@ public extension Api { return ("messageActionStarGiftUnique", [("flags", flags as Any), ("gift", gift as Any), ("canExportAt", canExportAt as Any), ("transferStars", transferStars as Any), ("fromId", fromId as Any), ("peer", peer as Any), ("savedId", savedId as Any), ("resaleStars", resaleStars as Any), ("canTransferAt", canTransferAt as Any), ("canResellAt", canResellAt as Any)]) case .messageActionSuggestProfilePhoto(let photo): return ("messageActionSuggestProfilePhoto", [("photo", photo as Any)]) + case .messageActionTodoCompletions(let completed, let incompleted): + return ("messageActionTodoCompletions", [("completed", completed as Any), ("incompleted", incompleted as Any)]) case .messageActionTopicCreate(let flags, let title, let iconColor, let iconEmojiId): return ("messageActionTopicCreate", [("flags", flags as Any), ("title", title as Any), ("iconColor", iconColor as Any), ("iconEmojiId", iconEmojiId as Any)]) case .messageActionTopicEdit(let flags, let title, let iconEmojiId, let closed, let hidden): @@ -1715,6 +1733,24 @@ public extension Api { return nil } } + public static func parse_messageActionTodoCompletions(_ reader: BufferReader) -> MessageAction? { + var _1: [Int32]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } + var _2: [Int32]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.MessageAction.messageActionTodoCompletions(completed: _1!, incompleted: _2!) + } + else { + return nil + } + } public static func parse_messageActionTopicCreate(_ reader: BufferReader) -> MessageAction? { var _1: Int32? _1 = reader.readInt32() diff --git a/submodules/TelegramApi/Sources/Api16.swift b/submodules/TelegramApi/Sources/Api16.swift index c52bd5fd42..8638c17fac 100644 --- a/submodules/TelegramApi/Sources/Api16.swift +++ b/submodules/TelegramApi/Sources/Api16.swift @@ -722,6 +722,7 @@ public extension Api { case messageMediaPhoto(flags: Int32, photo: Api.Photo?, ttlSeconds: Int32?) case messageMediaPoll(poll: Api.Poll, results: Api.PollResults) case messageMediaStory(flags: Int32, peer: Api.Peer, id: Int32, story: Api.StoryItem?) + case messageMediaToDo(flags: Int32, todo: Api.TodoList, completions: [Api.TodoCompletion]?) case messageMediaUnsupported case messageMediaVenue(geo: Api.GeoPoint, title: String, address: String, provider: String, venueId: String, venueType: String) case messageMediaWebPage(flags: Int32, webpage: Api.WebPage) @@ -878,6 +879,18 @@ public extension Api { serializeInt32(id, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {story!.serialize(buffer, true)} break + case .messageMediaToDo(let flags, let todo, let completions): + if boxed { + buffer.appendInt32(-1974226924) + } + serializeInt32(flags, buffer: buffer, boxed: false) + todo.serialize(buffer, true) + if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(completions!.count)) + for item in completions! { + item.serialize(buffer, true) + }} + break case .messageMediaUnsupported: if boxed { buffer.appendInt32(-1618676578) @@ -935,6 +948,8 @@ public extension Api { return ("messageMediaPoll", [("poll", poll as Any), ("results", results as Any)]) case .messageMediaStory(let flags, let peer, let id, let story): return ("messageMediaStory", [("flags", flags as Any), ("peer", peer as Any), ("id", id as Any), ("story", story as Any)]) + case .messageMediaToDo(let flags, let todo, let completions): + return ("messageMediaToDo", [("flags", flags as Any), ("todo", todo as Any), ("completions", completions as Any)]) case .messageMediaUnsupported: return ("messageMediaUnsupported", []) case .messageMediaVenue(let geo, let title, let address, let provider, let venueId, let venueType): @@ -1262,6 +1277,27 @@ public extension Api { return nil } } + public static func parse_messageMediaToDo(_ reader: BufferReader) -> MessageMedia? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.TodoList? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.TodoList + } + var _3: [Api.TodoCompletion]? + if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.TodoCompletion.self) + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + if _c1 && _c2 && _c3 { + return Api.MessageMedia.messageMediaToDo(flags: _1!, todo: _2!, completions: _3) + } + else { + return nil + } + } public static func parse_messageMediaUnsupported(_ reader: BufferReader) -> MessageMedia? { return Api.MessageMedia.messageMediaUnsupported } diff --git a/submodules/TelegramApi/Sources/Api26.swift b/submodules/TelegramApi/Sources/Api26.swift index c001972a2d..a1aa3902be 100644 --- a/submodules/TelegramApi/Sources/Api26.swift +++ b/submodules/TelegramApi/Sources/Api26.swift @@ -762,6 +762,144 @@ public extension Api { } } +public extension Api { + enum TodoCompletion: TypeConstructorDescription { + case todoCompletion(id: Int32, completedBy: Int64, date: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .todoCompletion(let id, let completedBy, let date): + if boxed { + buffer.appendInt32(1287725239) + } + serializeInt32(id, buffer: buffer, boxed: false) + serializeInt64(completedBy, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .todoCompletion(let id, let completedBy, let date): + return ("todoCompletion", [("id", id as Any), ("completedBy", completedBy as Any), ("date", date as Any)]) + } + } + + public static func parse_todoCompletion(_ reader: BufferReader) -> TodoCompletion? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int32? + _3 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.TodoCompletion.todoCompletion(id: _1!, completedBy: _2!, date: _3!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum TodoItem: TypeConstructorDescription { + case todoItem(id: Int32, title: Api.TextWithEntities) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .todoItem(let id, let title): + if boxed { + buffer.appendInt32(-878074577) + } + serializeInt32(id, buffer: buffer, boxed: false) + title.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .todoItem(let id, let title): + return ("todoItem", [("id", id as Any), ("title", title as Any)]) + } + } + + public static func parse_todoItem(_ reader: BufferReader) -> TodoItem? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.TextWithEntities? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.TextWithEntities + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.TodoItem.todoItem(id: _1!, title: _2!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum TodoList: TypeConstructorDescription { + case todoList(flags: Int32, title: Api.TextWithEntities, list: [Api.TodoItem]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .todoList(let flags, let title, let list): + if boxed { + buffer.appendInt32(1236871718) + } + serializeInt32(flags, buffer: buffer, boxed: false) + title.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(list.count)) + for item in list { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .todoList(let flags, let title, let list): + return ("todoList", [("flags", flags as Any), ("title", title as Any), ("list", list as Any)]) + } + } + + public static func parse_todoList(_ reader: BufferReader) -> TodoList? { + 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.TodoItem]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.TodoItem.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.TodoList.todoList(flags: _1!, title: _2!, list: _3!) + } + else { + return nil + } + } + + } +} public extension Api { enum TopPeer: TypeConstructorDescription { case topPeer(peer: Api.Peer, rating: Double) diff --git a/submodules/TelegramApi/Sources/Api38.swift b/submodules/TelegramApi/Sources/Api38.swift index dfd17d37da..f78a34a36e 100644 --- a/submodules/TelegramApi/Sources/Api38.swift +++ b/submodules/TelegramApi/Sources/Api38.swift @@ -5067,6 +5067,27 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func appendTodoList(peer: Api.InputPeer, msgId: Int32, list: [Api.TodoItem]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(564531287) + peer.serialize(buffer, true) + serializeInt32(msgId, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(list.count)) + for item in list { + item.serialize(buffer, true) + } + return (FunctionDescription(name: "messages.appendTodoList", parameters: [("peer", String(describing: peer)), ("msgId", String(describing: msgId)), ("list", String(describing: list))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } +} public extension Api.functions.messages { static func checkChatInvite(hash: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -8880,6 +8901,32 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func toggleTodoCompleted(peer: Api.InputPeer, msgId: Int32, completed: [Int32], incompleted: [Int32]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-740282076) + peer.serialize(buffer, true) + serializeInt32(msgId, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(completed.count)) + for item in completed { + serializeInt32(item, buffer: buffer, boxed: false) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(incompleted.count)) + for item in incompleted { + serializeInt32(item, buffer: buffer, boxed: false) + } + return (FunctionDescription(name: "messages.toggleTodoCompleted", parameters: [("peer", String(describing: peer)), ("msgId", String(describing: msgId)), ("completed", String(describing: completed)), ("incompleted", String(describing: incompleted))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } +} public extension Api.functions.messages { static func transcribeAudio(peer: Api.InputPeer, msgId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramCore/Sources/Account/AccountManager.swift b/submodules/TelegramCore/Sources/Account/AccountManager.swift index ef367cdca9..cd25ec6b63 100644 --- a/submodules/TelegramCore/Sources/Account/AccountManager.swift +++ b/submodules/TelegramCore/Sources/Account/AccountManager.swift @@ -233,6 +233,9 @@ private var declaredEncodables: Void = { declareEncodable(TelegramMediaPaidContent.self, f: { TelegramMediaPaidContent(decoder: $0) }) declareEncodable(ReportDeliveryMessageAttribute.self, f: { ReportDeliveryMessageAttribute(decoder: $0) }) declareEncodable(PaidStarsMessageAttribute.self, f: { PaidStarsMessageAttribute(decoder: $0) }) + declareEncodable(TelegramMediaTodo.self, f: { TelegramMediaTodo(decoder: $0) }) + declareEncodable(TelegramMediaTodo.Item.self, f: { TelegramMediaTodo.Item(decoder: $0) }) + declareEncodable(TelegramMediaTodo.Completion.self, f: { TelegramMediaTodo.Completion(decoder: $0) }) return }() diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 0726313806..e540775579 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -229,7 +229,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { } switch action { - case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionGiftStars, .messageActionTopicCreate, .messageActionTopicEdit, .messageActionSuggestProfilePhoto, .messageActionSetChatWallPaper, .messageActionGiveawayLaunch, .messageActionGiveawayResults, .messageActionBoostApply, .messageActionRequestedPeerSentMe, .messageActionStarGift, .messageActionStarGiftUnique, .messageActionPaidMessagesRefunded, .messageActionPaidMessagesPrice: + case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionGiftStars, .messageActionTopicCreate, .messageActionTopicEdit, .messageActionSuggestProfilePhoto, .messageActionSetChatWallPaper, .messageActionGiveawayLaunch, .messageActionGiveawayResults, .messageActionBoostApply, .messageActionRequestedPeerSentMe, .messageActionStarGift, .messageActionStarGiftUnique, .messageActionPaidMessagesRefunded, .messageActionPaidMessagesPrice, .messageActionTodoCompletions: break case let .messageActionChannelMigrateFrom(_, chatId): result.append(PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId))) @@ -428,6 +428,30 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI return (TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: questionText, textEntities: questionEntities, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod), nil, nil, nil, nil, nil) } + case let .messageMediaToDo(_, todo, completions): + switch todo { + case let .todoList(apiFlags, title, list): + var flags: TelegramMediaTodo.Flags = [] + if (apiFlags & (1 << 0)) != 0 { + flags.insert(.othersCanAppend) + } + if (apiFlags & (1 << 1)) != 0 { + flags.insert(.othersCanComplete) + } + + let todoText: String + let todoEntities: [MessageTextEntity] + switch title { + case let .textWithEntities(text, entities): + todoText = text + todoEntities = messageTextEntitiesFromApiEntities(entities) + } + var todoCompletions: [TelegramMediaTodo.Completion] = [] + if let completions { + todoCompletions = completions.map(TelegramMediaTodo.Completion.init(apiCompletion:)) + } + return (TelegramMediaTodo(flags: flags, text: todoText, textEntities: todoEntities, items: list.map(TelegramMediaTodo.Item.init(apiItem:)), completions: todoCompletions), nil, nil, nil, nil, nil) + } case let .messageMediaDice(value, emoticon): return (TelegramMediaDice(emoji: emoticon, value: value), nil, nil, nil, nil, nil) case let .messageMediaStory(flags, peerId, id, _): diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index f6ff6875d4..b10c3671c1 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -224,6 +224,8 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe flags: mappedFlags, otherParticipants: otherParticipants.flatMap({ return $0.map(\.peerId) }) ?? [] ))) + case let .messageActionTodoCompletions(completed, incompleted): + return TelegramMediaAction(action: .todoCompletions(completed: completed, incompleted: incompleted)) } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaTodo.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaTodo.swift new file mode 100644 index 0000000000..d98d35e499 --- /dev/null +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaTodo.swift @@ -0,0 +1,33 @@ +import Foundation +import Postbox +import TelegramApi + + +extension TelegramMediaTodo.Item { + init(apiItem: Api.TodoItem) { + switch apiItem { + case let .todoItem(id, title): + let itemText: String + let itemEntities: [MessageTextEntity] + switch title { + case let .textWithEntities(text, entities): + itemText = text + itemEntities = messageTextEntitiesFromApiEntities(entities) + } + self.init(text: itemText, entities: itemEntities, id: id) + } + } + + var apiItem: Api.TodoItem { + return .todoItem(id: self.id, title: .textWithEntities(text: self.text, entities: apiEntitiesFromMessageTextEntities(self.entities, associatedPeers: SimpleDictionary()))) + } +} + +extension TelegramMediaTodo.Completion { + init(apiCompletion: Api.TodoCompletion) { + switch apiCompletion { + case let .todoCompletion(id, completedBy, date): + self.init(id: id, date: date, completedBy: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(completedBy))) + } + } +} diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift index 8522225221..5ca32413ab 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift @@ -319,6 +319,7 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post if poll.deadlineTimeout != nil { pollFlags |= 1 << 4 } + var mappedSolution: String? var mappedSolutionEntities: [Api.MessageEntity]? if let solution = poll.results.solution { @@ -328,10 +329,20 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post } let inputPoll = Api.InputMedia.inputMediaPoll(flags: pollMediaFlags, poll: Api.Poll.poll(id: 0, flags: pollFlags, question: .textWithEntities(text: poll.text, entities: apiEntitiesFromMessageTextEntities(poll.textEntities, associatedPeers: SimpleDictionary())), answers: poll.options.map({ $0.apiOption }), closePeriod: poll.deadlineTimeout, closeDate: nil), correctAnswers: correctAnswers, solution: mappedSolution, solutionEntities: mappedSolutionEntities) return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(inputPoll, text), reuploadInfo: nil, cacheReferenceKey: nil))) - } else if let media = media as? TelegramMediaDice { - let inputDice = Api.InputMedia.inputMediaDice(emoticon: media.emoji) + } else if let todo = media as? TelegramMediaTodo { + var flags: Int32 = 0 + if todo.flags.contains(.othersCanAppend) { + flags |= 1 << 0 + } + if todo.flags.contains(.othersCanComplete) { + flags |= 1 << 1 + } + let inputTodo = Api.InputMedia.inputMediaTodo(todo: .todoList(flags: flags, title: .textWithEntities(text: todo.text, entities: apiEntitiesFromMessageTextEntities(todo.textEntities, associatedPeers: SimpleDictionary())), list: todo.items.map { $0.apiItem })) + return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(inputTodo, text), reuploadInfo: nil, cacheReferenceKey: nil))) + } else if let dice = media as? TelegramMediaDice { + let inputDice = Api.InputMedia.inputMediaDice(emoticon: dice.emoji) return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(inputDice, text), reuploadInfo: nil, cacheReferenceKey: nil))) - } else if let media = media as? TelegramMediaWebpage, case let .Loaded(content) = media.content { + } else if let webPage = media as? TelegramMediaWebpage, case let .Loaded(content) = webPage.content { var flags: Int32 = 0 flags |= 1 << 2 if let attribute = attributes.first(where: { $0 is WebpagePreviewMessageAttribute }) as? WebpagePreviewMessageAttribute { diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index 78fefb6bcb..a14e1baec0 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 204 + return 205 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index 6e54d7af63..4ab7855bcb 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -160,6 +160,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case paidMessagesRefunded(count: Int32, stars: Int64) case paidMessagesPriceEdited(stars: Int64, broadcastMessagesAllowed: Bool) case conferenceCall(ConferenceCall) + case todoCompletions(completed: [Int32], incompleted: [Int32]) public init(decoder: PostboxDecoder) { let rawValue: Int32 = decoder.decodeInt32ForKey("_rawValue", orElse: 0) @@ -295,6 +296,11 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { flags: ConferenceCall.Flags(rawValue: decoder.decodeInt32ForKey("flags", orElse: 0)), otherParticipants: decoder.decodeInt64ArrayForKey("part").map(PeerId.init) )) + case 49: + self = .todoCompletions( + completed: decoder.decodeInt32ArrayForKey("completed"), + incompleted: decoder.decodeInt32ArrayForKey("incompleted") + ) default: self = .unknown } @@ -698,6 +704,10 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } encoder.encodeInt32(conferenceCall.flags.rawValue, forKey: "flags") encoder.encodeInt64Array(conferenceCall.otherParticipants.map({ $0.toInt64() }), forKey: "part") + case let .todoCompletions(completed, incompleted): + encoder.encodeInt32(49, forKey: "_rawValue") + encoder.encodeInt32Array(completed, forKey: "completed") + encoder.encodeInt32Array(incompleted, forKey: "incompleted") } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaTodo.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaTodo.swift new file mode 100644 index 0000000000..7c7109f6d1 --- /dev/null +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaTodo.swift @@ -0,0 +1,134 @@ +import Foundation +import Postbox + +public final class TelegramMediaTodo: Media, Equatable { + public struct Flags: OptionSet { + public var rawValue: Int32 + + public init() { + self.rawValue = 0 + } + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public static let othersCanAppend = Flags(rawValue: 1 << 0) + public static let othersCanComplete = Flags(rawValue: 1 << 1) + } + + public struct Item: Equatable, PostboxCoding { + public let text: String + public let entities: [MessageTextEntity] + public let id: Int32 + + public init(text: String, entities: [MessageTextEntity], id: Int32) { + self.text = text + self.entities = entities + self.id = id + } + + public init(decoder: PostboxDecoder) { + self.text = decoder.decodeStringForKey("t", orElse: "") + self.entities = decoder.decodeObjectArrayWithDecoderForKey("et") + self.id = decoder.decodeInt32ForKey("i", orElse: 0) + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeString(self.text, forKey: "t") + encoder.encodeObjectArray(self.entities, forKey: "et") + encoder.encodeInt32(self.id, forKey: "i") + } + } + + public struct Completion: Equatable, PostboxCoding { + public let id: Int32 + public let date: Int32 + public let completedBy: EnginePeer.Id + + public init(id: Int32, date: Int32, completedBy: EnginePeer.Id) { + self.id = id + self.date = date + self.completedBy = completedBy + } + + public init(decoder: PostboxDecoder) { + self.id = decoder.decodeInt32ForKey("i", orElse: 0) + self.date = decoder.decodeInt32ForKey("d", orElse: 0) + self.completedBy = PeerId(decoder.decodeInt64ForKey("p", orElse: 0)) + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.id, forKey: "i") + encoder.encodeInt32(self.date, forKey: "d") + encoder.encodeInt64(self.completedBy.toInt64(), forKey: "p") + } + } + + public var id: MediaId? { + return nil + } + public var peerIds: [PeerId] { + return [] + } + + public let flags: Flags + public let text: String + public let textEntities: [MessageTextEntity] + public let items: [Item] + public let completions: [Completion] + + public init(flags: Flags, text: String, textEntities: [MessageTextEntity], items: [Item], completions: [Completion] = []) { + self.flags = flags + self.text = text + self.textEntities = textEntities + self.items = items + self.completions = completions + } + + public init(decoder: PostboxDecoder) { + self.flags = Flags(rawValue: decoder.decodeInt32ForKey("f", orElse: 0)) + self.text = decoder.decodeStringForKey("t", orElse: "") + self.textEntities = decoder.decodeObjectArrayWithDecoderForKey("te") + self.items = decoder.decodeObjectArrayWithDecoderForKey("is") + self.completions = decoder.decodeObjectArrayWithDecoderForKey("cs") + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.flags.rawValue, forKey: "f") + encoder.encodeString(self.text, forKey: "t") + encoder.encodeObjectArray(self.textEntities, forKey: "te") + encoder.encodeObjectArray(self.items, forKey: "is") + encoder.encodeObjectArray(self.completions, forKey: "cs") + } + + public func isEqual(to other: Media) -> Bool { + guard let other = other as? TelegramMediaTodo else { + return false + } + return self == other + } + + public func isSemanticallyEqual(to other: Media) -> Bool { + return self.isEqual(to: other) + } + + public static func ==(lhs: TelegramMediaTodo, rhs: TelegramMediaTodo) -> Bool { + if lhs.flags != rhs.flags { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.textEntities != rhs.textEntities { + return false + } + if lhs.items != rhs.items { + return false + } + if lhs.completions != rhs.completions { + return false + } + return true + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Media.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Media.swift index e841185a88..5e389a6ad6 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Media.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Media.swift @@ -20,6 +20,7 @@ public enum EngineMedia: Equatable { case giveaway(TelegramMediaGiveaway) case giveawayResults(TelegramMediaGiveawayResults) case paidContent(TelegramMediaPaidContent) + case todo(TelegramMediaTodo) } public extension EngineMedia { @@ -59,6 +60,8 @@ public extension EngineMedia { return giveawayResults.id case let .paidContent(paidContent): return paidContent.id + case .todo: + return nil } } } @@ -100,6 +103,8 @@ public extension EngineMedia { self = .giveawayResults(giveawayResults) case let paidContent as TelegramMediaPaidContent: self = .paidContent(paidContent) + case let todo as TelegramMediaTodo: + self = .todo(todo) default: preconditionFailure() } @@ -141,6 +146,8 @@ public extension EngineMedia { return giveawayResults case let .paidContent(paidContent): return paidContent + case let .todo(todo): + return todo } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 36c71c6413..ee6bbb5df9 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -204,6 +204,14 @@ public extension TelegramEngine { return PollResultsContext(account: self.account, messageId: messageId, poll: poll) } + public func requestUpdateTodoMessageItems(messageId: MessageId, completedIds: [Int32], incompletedIds: [Int32]) -> Signal { + return _internal_requestUpdateTodoMessageItems(account: self.account, messageId: messageId, completedIds: completedIds, incompletedIds: incompletedIds) + } + + public func appendTodoMessageItems(messageId: MessageId, items: [TelegramMediaTodo.Item]) -> Signal { + return _internal_appendTodoMessageItems(account: self.account, messageId: messageId, items: items) + } + public func earliestUnseenPersonalMentionMessage(peerId: PeerId, threadId: Int64?) -> Signal { let account = self.account return _internal_earliestUnseenPersonalMentionMessage(account: self.account, peerId: peerId, threadId: threadId) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Todo.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Todo.swift new file mode 100644 index 0000000000..746bc39800 --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Todo.swift @@ -0,0 +1,73 @@ +import Foundation +import TelegramApi +import Postbox +import SwiftSignalKit +import MtProtoKit + + +public enum RequestUpdateTodoMessageError { + case generic +} + +func _internal_requestUpdateTodoMessageItems(account: Account, messageId: MessageId, completedIds: [Int32], incompletedIds: [Int32]) -> Signal { + return account.postbox.loadedPeerWithId(messageId.peerId) + |> take(1) + |> castError(RequestUpdateTodoMessageError.self) + |> mapToSignal { peer in + if let inputPeer = apiInputPeer(peer) { + return account.network.request(Api.functions.messages.toggleTodoCompleted(peer: inputPeer, msgId: messageId.id, completed: completedIds, incompleted: incompletedIds)) + |> mapError { _ -> RequestUpdateTodoMessageError in + return .generic + } + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction -> TelegramMediaTodo? in + switch result { + case let .updates(updates, _, _, _, _): + let _ = updates + default: + break + } + account.stateManager.addUpdates(result) + return nil + } + |> castError(RequestUpdateTodoMessageError.self) + } + } else { + return .single(nil) + } + } + |> ignoreValues +} + +public enum AppendTodoMessageError { + case generic +} + +func _internal_appendTodoMessageItems(account: Account, messageId: MessageId, items: [TelegramMediaTodo.Item]) -> Signal { + return account.postbox.loadedPeerWithId(messageId.peerId) + |> take(1) + |> castError(AppendTodoMessageError.self) + |> mapToSignal { peer -> Signal in + guard let inputPeer = apiInputPeer(peer) else { + return .single(nil) + } + return account.network.request(Api.functions.messages.appendTodoList(peer: inputPeer, msgId: messageId.id, list: items.map { $0.apiItem })) + |> mapError { _ -> AppendTodoMessageError in + return .generic + } + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction -> TelegramMediaTodo? in + switch result { + case let .updates(updates, _, _, _, _): + let _ = updates + default: + break + } + account.stateManager.addUpdates(result) + return nil + } + |> castError(AppendTodoMessageError.self) + } + } + |> ignoreValues +} From 5b2a54163b5f223c8b6362edb8a3436342bd29e7 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Tue, 3 Jun 2025 11:55:11 +0100 Subject: [PATCH 2/6] todo peerIds --- .../Sources/SyncCore/SyncCore_TelegramMediaTodo.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaTodo.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaTodo.swift index 7c7109f6d1..e7a98d613e 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaTodo.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaTodo.swift @@ -69,7 +69,7 @@ public final class TelegramMediaTodo: Media, Equatable { return nil } public var peerIds: [PeerId] { - return [] + return completions.map(\.completedBy) } public let flags: Flags From 1dd07df394592ca56be43b49b25810dd2487d7a9 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 6 Jun 2025 00:51:05 +0800 Subject: [PATCH 3/6] Update API --- submodules/TelegramApi/Sources/Api0.swift | 7 +- submodules/TelegramApi/Sources/Api15.swift | 74 +++++++++++++------ submodules/TelegramApi/Sources/Api24.swift | 22 ++++-- submodules/TelegramApi/Sources/Api34.swift | 38 ++++++---- submodules/TelegramApi/Sources/Api38.swift | 49 ++++++------ .../ApiUtils/StoreMessage_Telegram.swift | 17 +++-- .../ApiUtils/TelegramMediaAction.swift | 3 + .../Sources/State/ApplyUpdateMessage.swift | 2 +- .../Sources/State/PaidMessages.swift | 5 +- .../Sources/State/UpdatesApiUtils.swift | 8 +- .../TelegramEngine/Messages/AdMessages.swift | 13 +++- .../Sources/ServiceMessageStrings.swift | 5 ++ 12 files changed, 159 insertions(+), 84 deletions(-) diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index be04611d4f..644cb83ce5 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -559,7 +559,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-808853502] = { return Api.MediaAreaCoordinates.parse_mediaAreaCoordinates($0) } dict[-356721331] = { return Api.Message.parse_message($0) } dict[-1868117372] = { return Api.Message.parse_messageEmpty($0) } - dict[-741178048] = { return Api.Message.parse_messageService($0) } + dict[2055212554] = { return Api.Message.parse_messageService($0) } dict[-872240531] = { return Api.MessageAction.parse_messageActionBoostApply($0) } dict[-988359047] = { return Api.MessageAction.parse_messageActionBotAllowed($0) } dict[-1781355374] = { return Api.MessageAction.parse_messageActionChannelCreate($0) } @@ -607,6 +607,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1192749220] = { return Api.MessageAction.parse_messageActionStarGift($0) } dict[775611918] = { return Api.MessageAction.parse_messageActionStarGiftUnique($0) } dict[1474192222] = { return Api.MessageAction.parse_messageActionSuggestProfilePhoto($0) } + dict[-940721021] = { return Api.MessageAction.parse_messageActionTodoAppendTasks($0) } dict[-864265079] = { return Api.MessageAction.parse_messageActionTodoCompletions($0) } dict[228168278] = { return Api.MessageAction.parse_messageActionTopicCreate($0) } dict[-1064024032] = { return Api.MessageAction.parse_messageActionTopicEdit($0) } @@ -938,7 +939,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-651419003] = { return Api.SendMessageAction.parse_speakingInGroupCallAction($0) } dict[-1239335713] = { return Api.ShippingOption.parse_shippingOption($0) } dict[-425595208] = { return Api.SmsJob.parse_smsJob($0) } - dict[1301522832] = { return Api.SponsoredMessage.parse_sponsoredMessage($0) } + dict[2109703795] = { return Api.SponsoredMessage.parse_sponsoredMessage($0) } dict[1124938064] = { return Api.SponsoredMessageReportOption.parse_sponsoredMessageReportOption($0) } dict[-963180333] = { return Api.SponsoredPeer.parse_sponsoredPeer($0) } dict[-970274264] = { return Api.StarGift.parse_starGift($0) } @@ -1397,7 +1398,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1404185519] = { return Api.messages.SearchResultsPositions.parse_searchResultsPositions($0) } dict[-1802240206] = { return Api.messages.SentEncryptedMessage.parse_sentEncryptedFile($0) } dict[1443858741] = { return Api.messages.SentEncryptedMessage.parse_sentEncryptedMessage($0) } - dict[-907141753] = { return Api.messages.SponsoredMessages.parse_sponsoredMessages($0) } + dict[-2464403] = { return Api.messages.SponsoredMessages.parse_sponsoredMessages($0) } dict[406407439] = { return Api.messages.SponsoredMessages.parse_sponsoredMessagesEmpty($0) } dict[1846886166] = { return Api.messages.StickerSet.parse_stickerSet($0) } dict[-738646805] = { return Api.messages.StickerSet.parse_stickerSetNotModified($0) } diff --git a/submodules/TelegramApi/Sources/Api15.swift b/submodules/TelegramApi/Sources/Api15.swift index 8df4f48c65..237813eda5 100644 --- a/submodules/TelegramApi/Sources/Api15.swift +++ b/submodules/TelegramApi/Sources/Api15.swift @@ -62,7 +62,7 @@ public extension Api { indirect enum Message: TypeConstructorDescription { case message(flags: Int32, flags2: Int32, id: Int32, fromId: Api.Peer?, fromBoostsApplied: Int32?, peerId: Api.Peer, savedPeerId: Api.Peer?, fwdFrom: Api.MessageFwdHeader?, viaBotId: Int64?, viaBusinessBotId: Int64?, replyTo: Api.MessageReplyHeader?, date: Int32, message: String, media: Api.MessageMedia?, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?, views: Int32?, forwards: Int32?, replies: Api.MessageReplies?, editDate: Int32?, postAuthor: String?, groupedId: Int64?, reactions: Api.MessageReactions?, restrictionReason: [Api.RestrictionReason]?, ttlPeriod: Int32?, quickReplyShortcutId: Int32?, effect: Int64?, factcheck: Api.FactCheck?, reportDeliveryUntilDate: Int32?, paidMessageStars: Int64?) case messageEmpty(flags: Int32, id: Int32, peerId: Api.Peer?) - case messageService(flags: Int32, id: Int32, fromId: Api.Peer?, peerId: Api.Peer, replyTo: Api.MessageReplyHeader?, date: Int32, action: Api.MessageAction, reactions: Api.MessageReactions?, ttlPeriod: Int32?) + case messageService(flags: Int32, id: Int32, fromId: Api.Peer?, peerId: Api.Peer, savedPeerId: Api.Peer?, replyTo: Api.MessageReplyHeader?, date: Int32, action: Api.MessageAction, reactions: Api.MessageReactions?, ttlPeriod: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -117,14 +117,15 @@ public extension Api { serializeInt32(id, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {peerId!.serialize(buffer, true)} break - case .messageService(let flags, let id, let fromId, let peerId, let replyTo, let date, let action, let reactions, let ttlPeriod): + case .messageService(let flags, let id, let fromId, let peerId, let savedPeerId, let replyTo, let date, let action, let reactions, let ttlPeriod): if boxed { - buffer.appendInt32(-741178048) + buffer.appendInt32(2055212554) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(id, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 8) != 0 {fromId!.serialize(buffer, true)} peerId.serialize(buffer, true) + if Int(flags) & Int(1 << 28) != 0 {savedPeerId!.serialize(buffer, true)} if Int(flags) & Int(1 << 3) != 0 {replyTo!.serialize(buffer, true)} serializeInt32(date, buffer: buffer, boxed: false) action.serialize(buffer, true) @@ -140,8 +141,8 @@ public extension Api { return ("message", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("fromId", fromId as Any), ("fromBoostsApplied", fromBoostsApplied as Any), ("peerId", peerId as Any), ("savedPeerId", savedPeerId as Any), ("fwdFrom", fwdFrom as Any), ("viaBotId", viaBotId as Any), ("viaBusinessBotId", viaBusinessBotId as Any), ("replyTo", replyTo as Any), ("date", date as Any), ("message", message as Any), ("media", media as Any), ("replyMarkup", replyMarkup as Any), ("entities", entities as Any), ("views", views as Any), ("forwards", forwards as Any), ("replies", replies as Any), ("editDate", editDate as Any), ("postAuthor", postAuthor as Any), ("groupedId", groupedId as Any), ("reactions", reactions as Any), ("restrictionReason", restrictionReason as Any), ("ttlPeriod", ttlPeriod as Any), ("quickReplyShortcutId", quickReplyShortcutId as Any), ("effect", effect as Any), ("factcheck", factcheck as Any), ("reportDeliveryUntilDate", reportDeliveryUntilDate as Any), ("paidMessageStars", paidMessageStars as Any)]) case .messageEmpty(let flags, let id, let peerId): return ("messageEmpty", [("flags", flags as Any), ("id", id as Any), ("peerId", peerId as Any)]) - case .messageService(let flags, let id, let fromId, let peerId, let replyTo, let date, let action, let reactions, let ttlPeriod): - return ("messageService", [("flags", flags as Any), ("id", id as Any), ("fromId", fromId as Any), ("peerId", peerId as Any), ("replyTo", replyTo as Any), ("date", date as Any), ("action", action as Any), ("reactions", reactions as Any), ("ttlPeriod", ttlPeriod as Any)]) + case .messageService(let flags, let id, let fromId, let peerId, let savedPeerId, let replyTo, let date, let action, let reactions, let ttlPeriod): + return ("messageService", [("flags", flags as Any), ("id", id as Any), ("fromId", fromId as Any), ("peerId", peerId as Any), ("savedPeerId", savedPeerId as Any), ("replyTo", replyTo as Any), ("date", date as Any), ("action", action as Any), ("reactions", reactions as Any), ("ttlPeriod", ttlPeriod as Any)]) } } @@ -299,33 +300,38 @@ public extension Api { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.Peer } - var _5: Api.MessageReplyHeader? + var _5: Api.Peer? + if Int(_1!) & Int(1 << 28) != 0 {if let signature = reader.readInt32() { + _5 = Api.parse(reader, signature: signature) as? Api.Peer + } } + var _6: Api.MessageReplyHeader? if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() { - _5 = Api.parse(reader, signature: signature) as? Api.MessageReplyHeader + _6 = Api.parse(reader, signature: signature) as? Api.MessageReplyHeader } } - var _6: Int32? - _6 = reader.readInt32() - var _7: Api.MessageAction? + var _7: Int32? + _7 = reader.readInt32() + var _8: Api.MessageAction? if let signature = reader.readInt32() { - _7 = Api.parse(reader, signature: signature) as? Api.MessageAction + _8 = Api.parse(reader, signature: signature) as? Api.MessageAction } - var _8: Api.MessageReactions? + var _9: Api.MessageReactions? if Int(_1!) & Int(1 << 20) != 0 {if let signature = reader.readInt32() { - _8 = Api.parse(reader, signature: signature) as? Api.MessageReactions + _9 = Api.parse(reader, signature: signature) as? Api.MessageReactions } } - var _9: Int32? - if Int(_1!) & Int(1 << 25) != 0 {_9 = reader.readInt32() } + var _10: Int32? + if Int(_1!) & Int(1 << 25) != 0 {_10 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = (Int(_1!) & Int(1 << 8) == 0) || _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil - let _c6 = _6 != nil + let _c5 = (Int(_1!) & Int(1 << 28) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil let _c7 = _7 != nil - let _c8 = (Int(_1!) & Int(1 << 20) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 25) == 0) || _9 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { - return Api.Message.messageService(flags: _1!, id: _2!, fromId: _3, peerId: _4!, replyTo: _5, date: _6!, action: _7!, reactions: _8, ttlPeriod: _9) + let _c8 = _8 != nil + let _c9 = (Int(_1!) & Int(1 << 20) == 0) || _9 != nil + let _c10 = (Int(_1!) & Int(1 << 25) == 0) || _10 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { + return Api.Message.messageService(flags: _1!, id: _2!, fromId: _3, peerId: _4!, savedPeerId: _5, replyTo: _6, date: _7!, action: _8!, reactions: _9, ttlPeriod: _10) } else { return nil @@ -383,6 +389,7 @@ public extension Api { case messageActionStarGift(flags: Int32, gift: Api.StarGift, message: Api.TextWithEntities?, convertStars: Int64?, upgradeMsgId: Int32?, upgradeStars: Int64?, fromId: Api.Peer?, peer: Api.Peer?, savedId: Int64?) case messageActionStarGiftUnique(flags: Int32, gift: Api.StarGift, canExportAt: Int32?, transferStars: Int64?, fromId: Api.Peer?, peer: Api.Peer?, savedId: Int64?, resaleStars: Int64?, canTransferAt: Int32?, canResellAt: Int32?) case messageActionSuggestProfilePhoto(photo: Api.Photo) + case messageActionTodoAppendTasks(list: [Api.TodoItem]) case messageActionTodoCompletions(completed: [Int32], incompleted: [Int32]) case messageActionTopicCreate(flags: Int32, title: String, iconColor: Int32, iconEmojiId: Int64?) case messageActionTopicEdit(flags: Int32, title: String?, iconEmojiId: Int64?, closed: Api.Bool?, hidden: Api.Bool?) @@ -790,6 +797,16 @@ public extension Api { } photo.serialize(buffer, true) break + case .messageActionTodoAppendTasks(let list): + if boxed { + buffer.appendInt32(-940721021) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(list.count)) + for item in list { + item.serialize(buffer, true) + } + break case .messageActionTodoCompletions(let completed, let incompleted): if boxed { buffer.appendInt32(-864265079) @@ -936,6 +953,8 @@ public extension Api { return ("messageActionStarGiftUnique", [("flags", flags as Any), ("gift", gift as Any), ("canExportAt", canExportAt as Any), ("transferStars", transferStars as Any), ("fromId", fromId as Any), ("peer", peer as Any), ("savedId", savedId as Any), ("resaleStars", resaleStars as Any), ("canTransferAt", canTransferAt as Any), ("canResellAt", canResellAt as Any)]) case .messageActionSuggestProfilePhoto(let photo): return ("messageActionSuggestProfilePhoto", [("photo", photo as Any)]) + case .messageActionTodoAppendTasks(let list): + return ("messageActionTodoAppendTasks", [("list", list as Any)]) case .messageActionTodoCompletions(let completed, let incompleted): return ("messageActionTodoCompletions", [("completed", completed as Any), ("incompleted", incompleted as Any)]) case .messageActionTopicCreate(let flags, let title, let iconColor, let iconEmojiId): @@ -1733,6 +1752,19 @@ public extension Api { return nil } } + public static func parse_messageActionTodoAppendTasks(_ reader: BufferReader) -> MessageAction? { + var _1: [Api.TodoItem]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.TodoItem.self) + } + let _c1 = _1 != nil + if _c1 { + return Api.MessageAction.messageActionTodoAppendTasks(list: _1!) + } + else { + return nil + } + } public static func parse_messageActionTodoCompletions(_ reader: BufferReader) -> MessageAction? { var _1: [Int32]? if let _ = reader.readInt32() { diff --git a/submodules/TelegramApi/Sources/Api24.swift b/submodules/TelegramApi/Sources/Api24.swift index deb0fe67e3..ecb0e13970 100644 --- a/submodules/TelegramApi/Sources/Api24.swift +++ b/submodules/TelegramApi/Sources/Api24.swift @@ -442,13 +442,13 @@ public extension Api { } public extension Api { indirect enum SponsoredMessage: TypeConstructorDescription { - case sponsoredMessage(flags: Int32, randomId: Buffer, url: String, title: String, message: String, entities: [Api.MessageEntity]?, photo: Api.Photo?, media: Api.MessageMedia?, color: Api.PeerColor?, buttonText: String, sponsorInfo: String?, additionalInfo: String?) + case sponsoredMessage(flags: Int32, randomId: Buffer, url: String, title: String, message: String, entities: [Api.MessageEntity]?, photo: Api.Photo?, media: Api.MessageMedia?, color: Api.PeerColor?, buttonText: String, sponsorInfo: String?, additionalInfo: String?, minDisplayDuration: Int32?, maxDisplayDuration: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .sponsoredMessage(let flags, let randomId, let url, let title, let message, let entities, let photo, let media, let color, let buttonText, let sponsorInfo, let additionalInfo): + case .sponsoredMessage(let flags, let randomId, let url, let title, let message, let entities, let photo, let media, let color, let buttonText, let sponsorInfo, let additionalInfo, let minDisplayDuration, let maxDisplayDuration): if boxed { - buffer.appendInt32(1301522832) + buffer.appendInt32(2109703795) } serializeInt32(flags, buffer: buffer, boxed: false) serializeBytes(randomId, buffer: buffer, boxed: false) @@ -466,14 +466,16 @@ public extension Api { serializeString(buttonText, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 7) != 0 {serializeString(sponsorInfo!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 8) != 0 {serializeString(additionalInfo!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 15) != 0 {serializeInt32(minDisplayDuration!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 15) != 0 {serializeInt32(maxDisplayDuration!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .sponsoredMessage(let flags, let randomId, let url, let title, let message, let entities, let photo, let media, let color, let buttonText, let sponsorInfo, let additionalInfo): - return ("sponsoredMessage", [("flags", flags as Any), ("randomId", randomId as Any), ("url", url as Any), ("title", title as Any), ("message", message as Any), ("entities", entities as Any), ("photo", photo as Any), ("media", media as Any), ("color", color as Any), ("buttonText", buttonText as Any), ("sponsorInfo", sponsorInfo as Any), ("additionalInfo", additionalInfo as Any)]) + case .sponsoredMessage(let flags, let randomId, let url, let title, let message, let entities, let photo, let media, let color, let buttonText, let sponsorInfo, let additionalInfo, let minDisplayDuration, let maxDisplayDuration): + return ("sponsoredMessage", [("flags", flags as Any), ("randomId", randomId as Any), ("url", url as Any), ("title", title as Any), ("message", message as Any), ("entities", entities as Any), ("photo", photo as Any), ("media", media as Any), ("color", color as Any), ("buttonText", buttonText as Any), ("sponsorInfo", sponsorInfo as Any), ("additionalInfo", additionalInfo as Any), ("minDisplayDuration", minDisplayDuration as Any), ("maxDisplayDuration", maxDisplayDuration as Any)]) } } @@ -510,6 +512,10 @@ public extension Api { if Int(_1!) & Int(1 << 7) != 0 {_11 = parseString(reader) } var _12: String? if Int(_1!) & Int(1 << 8) != 0 {_12 = parseString(reader) } + var _13: Int32? + if Int(_1!) & Int(1 << 15) != 0 {_13 = reader.readInt32() } + var _14: Int32? + if Int(_1!) & Int(1 << 15) != 0 {_14 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -522,8 +528,10 @@ public extension Api { let _c10 = _10 != nil let _c11 = (Int(_1!) & Int(1 << 7) == 0) || _11 != nil let _c12 = (Int(_1!) & Int(1 << 8) == 0) || _12 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 { - return Api.SponsoredMessage.sponsoredMessage(flags: _1!, randomId: _2!, url: _3!, title: _4!, message: _5!, entities: _6, photo: _7, media: _8, color: _9, buttonText: _10!, sponsorInfo: _11, additionalInfo: _12) + let _c13 = (Int(_1!) & Int(1 << 15) == 0) || _13 != nil + let _c14 = (Int(_1!) & Int(1 << 15) == 0) || _14 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 { + return Api.SponsoredMessage.sponsoredMessage(flags: _1!, randomId: _2!, url: _3!, title: _4!, message: _5!, entities: _6, photo: _7, media: _8, color: _9, buttonText: _10!, sponsorInfo: _11, additionalInfo: _12, minDisplayDuration: _13, maxDisplayDuration: _14) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api34.swift b/submodules/TelegramApi/Sources/Api34.swift index b2ea498c9f..1c8af79156 100644 --- a/submodules/TelegramApi/Sources/Api34.swift +++ b/submodules/TelegramApi/Sources/Api34.swift @@ -304,17 +304,19 @@ public extension Api.messages { } public extension Api.messages { enum SponsoredMessages: TypeConstructorDescription { - case sponsoredMessages(flags: Int32, postsBetween: Int32?, messages: [Api.SponsoredMessage], chats: [Api.Chat], users: [Api.User]) + case sponsoredMessages(flags: Int32, postsBetween: Int32?, startDelay: Int32?, betweenDelay: Int32?, messages: [Api.SponsoredMessage], chats: [Api.Chat], users: [Api.User]) case sponsoredMessagesEmpty public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .sponsoredMessages(let flags, let postsBetween, let messages, let chats, let users): + case .sponsoredMessages(let flags, let postsBetween, let startDelay, let betweenDelay, let messages, let chats, let users): if boxed { - buffer.appendInt32(-907141753) + buffer.appendInt32(-2464403) } serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {serializeInt32(postsBetween!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {serializeInt32(startDelay!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 2) != 0 {serializeInt32(betweenDelay!, buffer: buffer, boxed: false)} buffer.appendInt32(481674261) buffer.appendInt32(Int32(messages.count)) for item in messages { @@ -342,8 +344,8 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .sponsoredMessages(let flags, let postsBetween, let messages, let chats, let users): - return ("sponsoredMessages", [("flags", flags as Any), ("postsBetween", postsBetween as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) + case .sponsoredMessages(let flags, let postsBetween, let startDelay, let betweenDelay, let messages, let chats, let users): + return ("sponsoredMessages", [("flags", flags as Any), ("postsBetween", postsBetween as Any), ("startDelay", startDelay as Any), ("betweenDelay", betweenDelay as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) case .sponsoredMessagesEmpty: return ("sponsoredMessagesEmpty", []) } @@ -354,25 +356,31 @@ public extension Api.messages { _1 = reader.readInt32() var _2: Int32? if Int(_1!) & Int(1 << 0) != 0 {_2 = reader.readInt32() } - var _3: [Api.SponsoredMessage]? + var _3: Int32? + if Int(_1!) & Int(1 << 1) != 0 {_3 = reader.readInt32() } + var _4: Int32? + if Int(_1!) & Int(1 << 2) != 0 {_4 = reader.readInt32() } + var _5: [Api.SponsoredMessage]? if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.SponsoredMessage.self) + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.SponsoredMessage.self) } - var _4: [Api.Chat]? + var _6: [Api.Chat]? if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) } - var _5: [Api.User]? + var _7: [Api.User]? if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } let _c1 = _1 != nil let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.messages.SponsoredMessages.sponsoredMessages(flags: _1!, postsBetween: _2, messages: _3!, chats: _4!, users: _5!) + let _c6 = _6 != nil + let _c7 = _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.messages.SponsoredMessages.sponsoredMessages(flags: _1!, postsBetween: _2, startDelay: _3, betweenDelay: _4, messages: _5!, chats: _6!, users: _7!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api38.swift b/submodules/TelegramApi/Sources/Api38.swift index f78a34a36e..1c436d111e 100644 --- a/submodules/TelegramApi/Sources/Api38.swift +++ b/submodules/TelegramApi/Sources/Api38.swift @@ -21,22 +21,6 @@ public extension Api.functions.account { }) } } -public extension Api.functions.account { - static func addNoPaidMessagesException(flags: Int32, userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { - let buffer = Buffer() - buffer.appendInt32(1869122215) - serializeInt32(flags, buffer: buffer, boxed: false) - userId.serialize(buffer, true) - return (FunctionDescription(name: "account.addNoPaidMessagesException", parameters: [("flags", String(describing: flags)), ("userId", String(describing: userId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in - let reader = BufferReader(buffer) - var result: Api.Bool? - if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.Bool - } - return result - }) - } -} public extension Api.functions.account { static func cancelPasswordEmail() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -666,11 +650,13 @@ public extension Api.functions.account { } } public extension Api.functions.account { - static func getPaidMessagesRevenue(userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func getPaidMessagesRevenue(flags: Int32, parentPeer: Api.InputPeer?, userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-249139400) + buffer.appendInt32(431639143) + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {parentPeer!.serialize(buffer, true)} userId.serialize(buffer, true) - return (FunctionDescription(name: "account.getPaidMessagesRevenue", parameters: [("userId", String(describing: userId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.account.PaidMessagesRevenue? in + return (FunctionDescription(name: "account.getPaidMessagesRevenue", parameters: [("flags", String(describing: flags)), ("parentPeer", String(describing: parentPeer)), ("userId", String(describing: userId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.account.PaidMessagesRevenue? in let reader = BufferReader(buffer) var result: Api.account.PaidMessagesRevenue? if let signature = reader.readInt32() { @@ -1437,6 +1423,23 @@ public extension Api.functions.account { }) } } +public extension Api.functions.account { + static func toggleNoPaidMessagesException(flags: Int32, parentPeer: Api.InputPeer?, userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-30483850) + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {parentPeer!.serialize(buffer, true)} + userId.serialize(buffer, true) + return (FunctionDescription(name: "account.toggleNoPaidMessagesException", parameters: [("flags", String(describing: flags)), ("parentPeer", String(describing: parentPeer)), ("userId", String(describing: userId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.account { static func toggleSponsoredMessages(enabled: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -7086,11 +7089,13 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func getSponsoredMessages(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func getSponsoredMessages(flags: Int32, peer: Api.InputPeer, msgId: Int32?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-1680673735) + buffer.appendInt32(1030547536) + serializeInt32(flags, buffer: buffer, boxed: false) peer.serialize(buffer, true) - return (FunctionDescription(name: "messages.getSponsoredMessages", parameters: [("peer", String(describing: peer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.SponsoredMessages? in + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(msgId!, buffer: buffer, boxed: false)} + return (FunctionDescription(name: "messages.getSponsoredMessages", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("msgId", String(describing: msgId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.SponsoredMessages? in let reader = BufferReader(buffer) var result: Api.messages.SponsoredMessages? if let signature = reader.readInt32() { diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index e540775579..32f998d09c 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -137,7 +137,7 @@ func apiMessagePeerId(_ messsage: Api.Message) -> PeerId? { } else { return nil } - case let .messageService(_, _, _, chatPeerId, _, _, _, _, _): + case let .messageService(_, _, _, chatPeerId, _, _, _, _, _, _): return chatPeerId.peerId } } @@ -218,7 +218,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { return result case .messageEmpty: return [] - case let .messageService(_, _, fromId, chatPeerId, _, _, action, _, _): + case let .messageService(_, _, fromId, chatPeerId, savedPeerId, _, _, action, _, _): let peerId: PeerId = chatPeerId.peerId var result = [peerId] @@ -227,9 +227,12 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { if resolvedFromId != peerId { result.append(resolvedFromId) } + if let savedPeerId, resolvedFromId != savedPeerId.peerId { + result.append(savedPeerId.peerId) + } switch action { - case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionGiftStars, .messageActionTopicCreate, .messageActionTopicEdit, .messageActionSuggestProfilePhoto, .messageActionSetChatWallPaper, .messageActionGiveawayLaunch, .messageActionGiveawayResults, .messageActionBoostApply, .messageActionRequestedPeerSentMe, .messageActionStarGift, .messageActionStarGiftUnique, .messageActionPaidMessagesRefunded, .messageActionPaidMessagesPrice, .messageActionTodoCompletions: + case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionGiftStars, .messageActionTopicCreate, .messageActionTopicEdit, .messageActionSuggestProfilePhoto, .messageActionSetChatWallPaper, .messageActionGiveawayLaunch, .messageActionGiveawayResults, .messageActionBoostApply, .messageActionRequestedPeerSentMe, .messageActionStarGift, .messageActionStarGiftUnique, .messageActionPaidMessagesRefunded, .messageActionPaidMessagesPrice, .messageActionTodoCompletions, .messageActionTodoAppendTasks: break case let .messageActionChannelMigrateFrom(_, chatId): result.append(PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId))) @@ -301,7 +304,7 @@ func apiMessageAssociatedMessageIds(_ message: Api.Message) -> (replyIds: Refere } case .messageEmpty: break - case let .messageService(_, id, _, chatPeerId, replyHeader, _, _, _, _): + case let .messageService(_, id, _, chatPeerId, _, replyHeader, _, _, _, _): if let replyHeader = replyHeader { switch replyHeader { case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, replyMedia, replyToTopId, quoteText, quoteEntities, quoteOffset): @@ -1061,14 +1064,16 @@ extension StoreMessage { self.init(id: MessageId(peerId: peerId, namespace: namespace, id: id), globallyUniqueId: nil, groupingKey: groupingId, threadId: threadId, timestamp: date, flags: storeFlags, tags: tags, globalTags: globalTags, localTags: [], forwardInfo: forwardInfo, authorId: authorId, text: messageText, attributes: attributes, media: medias) case .messageEmpty: return nil - case let .messageService(flags, id, fromId, chatPeerId, replyTo, date, action, reactions, ttlPeriod): + case let .messageService(flags, id, fromId, chatPeerId, savedPeerId, replyTo, date, action, reactions, ttlPeriod): let peerId: PeerId = chatPeerId.peerId let authorId: PeerId? = fromId?.peerId ?? chatPeerId.peerId var attributes: [MessageAttribute] = [] var threadId: Int64? - if let replyTo = replyTo { + if let savedPeerId { + threadId = savedPeerId.peerId.toInt64() + } else if let replyTo = replyTo { var threadMessageId: MessageId? switch replyTo { case let .messageReplyHeader(innerFlags, replyToMsgId, replyToPeerId, replyHeader, replyMedia, replyToTopId, quoteText, quoteEntities, quoteOffset): diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index b10c3671c1..8edf2aff8a 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -226,6 +226,9 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe ))) case let .messageActionTodoCompletions(completed, incompleted): return TelegramMediaAction(action: .todoCompletions(completed: completed, incompleted: incompleted)) + case .messageActionTodoAppendTasks: + //TODO:release + return nil } } diff --git a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift index 3507b5eda5..77627e29b3 100644 --- a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift +++ b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift @@ -108,7 +108,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes updatedTimestamp = date case .messageEmpty: break - case let .messageService(_, _, _, _, _, date, _, _, _): + case let .messageService(_, _, _, _, _, _, date, _, _, _): updatedTimestamp = date } } else { diff --git a/submodules/TelegramCore/Sources/State/PaidMessages.swift b/submodules/TelegramCore/Sources/State/PaidMessages.swift index d72997b581..02b40e1907 100644 --- a/submodules/TelegramCore/Sources/State/PaidMessages.swift +++ b/submodules/TelegramCore/Sources/State/PaidMessages.swift @@ -11,7 +11,7 @@ func _internal_getPaidMessagesRevenue(account: Account, peerId: PeerId) -> Signa guard let inputUser else { return .single(nil) } - return account.network.request(Api.functions.account.getPaidMessagesRevenue(userId: inputUser)) + return account.network.request(Api.functions.account.getPaidMessagesRevenue(flags: 0, parentPeer: nil, userId: inputUser)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) @@ -40,7 +40,8 @@ func _internal_addNoPaidMessagesException(account: Account, peerId: PeerId, refu if refundCharged { flags |= (1 << 0) } - return account.network.request(Api.functions.account.addNoPaidMessagesException(flags: flags, userId: inputUser)) + + return account.network.request(Api.functions.account.toggleNoPaidMessagesException(flags: flags, parentPeer: nil, userId: inputUser)) |> `catch` { _ -> Signal in return .single(.boolFalse) } |> mapToSignal { _ in diff --git a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift index b3a7ef1dfb..f2291524a3 100644 --- a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift +++ b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift @@ -108,7 +108,7 @@ extension Api.Message { return id case let .messageEmpty(_, id, _): return id - case let .messageService(_, id, _, _, _, _, _, _, _): + case let .messageService(_, id, _, _, _, _, _, _, _, _): return id } } @@ -128,7 +128,7 @@ extension Api.Message { } else { return nil } - case let .messageService(_, id, _, chatPeerId, _, _, _, _, _): + case let .messageService(_, id, _, chatPeerId, _, _, _, _, _, _): let peerId: PeerId = chatPeerId.peerId return MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: id) } @@ -141,7 +141,7 @@ extension Api.Message { return peerId case let .messageEmpty(_, _, peerId): return peerId?.peerId - case let .messageService(_, _, _, chatPeerId, _, _, _, _, _): + case let .messageService(_, _, _, chatPeerId, _, _, _, _, _, _): let peerId: PeerId = chatPeerId.peerId return peerId } @@ -151,7 +151,7 @@ extension Api.Message { switch self { case let .message(_, _, _, _, _, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): return date - case let .messageService(_, _, _, _, _, date, _, _, _): + case let .messageService(_, _, _, _, _, _, date, _, _, _): return date case .messageEmpty: return nil diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift index 05a1a8dba1..de9516cd91 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift @@ -430,7 +430,7 @@ private class AdMessagesHistoryContextImpl { guard let inputPeer else { return .single((nil, [])) } - return account.network.request(Api.functions.messages.getSponsoredMessages(peer: inputPeer)) + return account.network.request(Api.functions.messages.getSponsoredMessages(flags: 0, peer: inputPeer, msgId: nil)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) @@ -442,7 +442,10 @@ private class AdMessagesHistoryContextImpl { return account.postbox.transaction { transaction -> (interPostInterval: Int32?, messages: [Message]) in switch result { - case let .sponsoredMessages(_, postsBetween, messages, chats, users): + case let .sponsoredMessages(_, postsBetween, startDelay, betweenDelay, messages, chats, users): + //TODO:release + let _ = startDelay + let _ = betweenDelay let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users) updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) @@ -450,7 +453,11 @@ private class AdMessagesHistoryContextImpl { for message in messages { switch message { - case let .sponsoredMessage(flags, randomId, url, title, message, entities, photo, media, color, buttonText, sponsorInfo, additionalInfo): + case let .sponsoredMessage(flags, randomId, url, title, message, entities, photo, media, color, buttonText, sponsorInfo, additionalInfo, minDisplayDuration, maxDisplayDuration): + //TODO:release + let _ = minDisplayDuration + let _ = maxDisplayDuration + var parsedEntities: [MessageTextEntity] = [] if let entities = entities { parsedEntities = messageTextEntitiesFromApiEntities(entities) diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 17874db695..0b500598aa 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -1288,6 +1288,11 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, } case .unknown: attributedString = nil + case let .todoCompletions(completed, incompleted): + //TODO:release + let _ = completed + let _ = incompleted + attributedString = nil } break } else if let expiredMedia = media as? TelegramMediaExpiredContent { From 8709581d6a19821ea31803019bef3906bb6ce07a Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 6 Jun 2025 09:52:11 +0200 Subject: [PATCH 4/6] Update API --- submodules/TelegramApi/Sources/Api0.swift | 5 +- submodules/TelegramApi/Sources/Api15.swift | 26 ++++ submodules/TelegramApi/Sources/Api24.swift | 22 ++- submodules/TelegramApi/Sources/Api34.swift | 38 +++--- submodules/TelegramApi/Sources/Api38.swift | 49 ++++--- .../Sources/ApiUtils/AdMessageAttribute.swift | 6 +- .../ApiUtils/StoreMessage_Telegram.swift | 2 +- .../ApiUtils/TelegramMediaAction.swift | 2 + .../Sources/Network/Network.swift | 2 +- .../PendingMessages/RequestEditMessage.swift | 15 ++- .../Sources/State/PaidMessages.swift | 4 +- .../SyncCore_TelegramMediaAction.swift | 8 ++ .../SyncCore/SyncCore_TelegramMediaTodo.swift | 2 +- .../TelegramEngine/Messages/AdMessages.swift | 127 +++++++++++++----- .../Messages/TelegramEngineMessages.swift | 4 +- .../TelegramEngine/Messages/Todo.swift | 17 +-- 16 files changed, 225 insertions(+), 104 deletions(-) diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index be04611d4f..95e5be0d1a 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -607,6 +607,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1192749220] = { return Api.MessageAction.parse_messageActionStarGift($0) } dict[775611918] = { return Api.MessageAction.parse_messageActionStarGiftUnique($0) } dict[1474192222] = { return Api.MessageAction.parse_messageActionSuggestProfilePhoto($0) } + dict[-940721021] = { return Api.MessageAction.parse_messageActionTodoAppendTasks($0) } dict[-864265079] = { return Api.MessageAction.parse_messageActionTodoCompletions($0) } dict[228168278] = { return Api.MessageAction.parse_messageActionTopicCreate($0) } dict[-1064024032] = { return Api.MessageAction.parse_messageActionTopicEdit($0) } @@ -938,7 +939,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-651419003] = { return Api.SendMessageAction.parse_speakingInGroupCallAction($0) } dict[-1239335713] = { return Api.ShippingOption.parse_shippingOption($0) } dict[-425595208] = { return Api.SmsJob.parse_smsJob($0) } - dict[1301522832] = { return Api.SponsoredMessage.parse_sponsoredMessage($0) } + dict[2109703795] = { return Api.SponsoredMessage.parse_sponsoredMessage($0) } dict[1124938064] = { return Api.SponsoredMessageReportOption.parse_sponsoredMessageReportOption($0) } dict[-963180333] = { return Api.SponsoredPeer.parse_sponsoredPeer($0) } dict[-970274264] = { return Api.StarGift.parse_starGift($0) } @@ -1397,7 +1398,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1404185519] = { return Api.messages.SearchResultsPositions.parse_searchResultsPositions($0) } dict[-1802240206] = { return Api.messages.SentEncryptedMessage.parse_sentEncryptedFile($0) } dict[1443858741] = { return Api.messages.SentEncryptedMessage.parse_sentEncryptedMessage($0) } - dict[-907141753] = { return Api.messages.SponsoredMessages.parse_sponsoredMessages($0) } + dict[-2464403] = { return Api.messages.SponsoredMessages.parse_sponsoredMessages($0) } dict[406407439] = { return Api.messages.SponsoredMessages.parse_sponsoredMessagesEmpty($0) } dict[1846886166] = { return Api.messages.StickerSet.parse_stickerSet($0) } dict[-738646805] = { return Api.messages.StickerSet.parse_stickerSetNotModified($0) } diff --git a/submodules/TelegramApi/Sources/Api15.swift b/submodules/TelegramApi/Sources/Api15.swift index 8df4f48c65..6ea773d8b5 100644 --- a/submodules/TelegramApi/Sources/Api15.swift +++ b/submodules/TelegramApi/Sources/Api15.swift @@ -383,6 +383,7 @@ public extension Api { case messageActionStarGift(flags: Int32, gift: Api.StarGift, message: Api.TextWithEntities?, convertStars: Int64?, upgradeMsgId: Int32?, upgradeStars: Int64?, fromId: Api.Peer?, peer: Api.Peer?, savedId: Int64?) case messageActionStarGiftUnique(flags: Int32, gift: Api.StarGift, canExportAt: Int32?, transferStars: Int64?, fromId: Api.Peer?, peer: Api.Peer?, savedId: Int64?, resaleStars: Int64?, canTransferAt: Int32?, canResellAt: Int32?) case messageActionSuggestProfilePhoto(photo: Api.Photo) + case messageActionTodoAppendTasks(list: [Api.TodoItem]) case messageActionTodoCompletions(completed: [Int32], incompleted: [Int32]) case messageActionTopicCreate(flags: Int32, title: String, iconColor: Int32, iconEmojiId: Int64?) case messageActionTopicEdit(flags: Int32, title: String?, iconEmojiId: Int64?, closed: Api.Bool?, hidden: Api.Bool?) @@ -790,6 +791,16 @@ public extension Api { } photo.serialize(buffer, true) break + case .messageActionTodoAppendTasks(let list): + if boxed { + buffer.appendInt32(-940721021) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(list.count)) + for item in list { + item.serialize(buffer, true) + } + break case .messageActionTodoCompletions(let completed, let incompleted): if boxed { buffer.appendInt32(-864265079) @@ -936,6 +947,8 @@ public extension Api { return ("messageActionStarGiftUnique", [("flags", flags as Any), ("gift", gift as Any), ("canExportAt", canExportAt as Any), ("transferStars", transferStars as Any), ("fromId", fromId as Any), ("peer", peer as Any), ("savedId", savedId as Any), ("resaleStars", resaleStars as Any), ("canTransferAt", canTransferAt as Any), ("canResellAt", canResellAt as Any)]) case .messageActionSuggestProfilePhoto(let photo): return ("messageActionSuggestProfilePhoto", [("photo", photo as Any)]) + case .messageActionTodoAppendTasks(let list): + return ("messageActionTodoAppendTasks", [("list", list as Any)]) case .messageActionTodoCompletions(let completed, let incompleted): return ("messageActionTodoCompletions", [("completed", completed as Any), ("incompleted", incompleted as Any)]) case .messageActionTopicCreate(let flags, let title, let iconColor, let iconEmojiId): @@ -1733,6 +1746,19 @@ public extension Api { return nil } } + public static func parse_messageActionTodoAppendTasks(_ reader: BufferReader) -> MessageAction? { + var _1: [Api.TodoItem]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.TodoItem.self) + } + let _c1 = _1 != nil + if _c1 { + return Api.MessageAction.messageActionTodoAppendTasks(list: _1!) + } + else { + return nil + } + } public static func parse_messageActionTodoCompletions(_ reader: BufferReader) -> MessageAction? { var _1: [Int32]? if let _ = reader.readInt32() { diff --git a/submodules/TelegramApi/Sources/Api24.swift b/submodules/TelegramApi/Sources/Api24.swift index deb0fe67e3..ecb0e13970 100644 --- a/submodules/TelegramApi/Sources/Api24.swift +++ b/submodules/TelegramApi/Sources/Api24.swift @@ -442,13 +442,13 @@ public extension Api { } public extension Api { indirect enum SponsoredMessage: TypeConstructorDescription { - case sponsoredMessage(flags: Int32, randomId: Buffer, url: String, title: String, message: String, entities: [Api.MessageEntity]?, photo: Api.Photo?, media: Api.MessageMedia?, color: Api.PeerColor?, buttonText: String, sponsorInfo: String?, additionalInfo: String?) + case sponsoredMessage(flags: Int32, randomId: Buffer, url: String, title: String, message: String, entities: [Api.MessageEntity]?, photo: Api.Photo?, media: Api.MessageMedia?, color: Api.PeerColor?, buttonText: String, sponsorInfo: String?, additionalInfo: String?, minDisplayDuration: Int32?, maxDisplayDuration: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .sponsoredMessage(let flags, let randomId, let url, let title, let message, let entities, let photo, let media, let color, let buttonText, let sponsorInfo, let additionalInfo): + case .sponsoredMessage(let flags, let randomId, let url, let title, let message, let entities, let photo, let media, let color, let buttonText, let sponsorInfo, let additionalInfo, let minDisplayDuration, let maxDisplayDuration): if boxed { - buffer.appendInt32(1301522832) + buffer.appendInt32(2109703795) } serializeInt32(flags, buffer: buffer, boxed: false) serializeBytes(randomId, buffer: buffer, boxed: false) @@ -466,14 +466,16 @@ public extension Api { serializeString(buttonText, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 7) != 0 {serializeString(sponsorInfo!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 8) != 0 {serializeString(additionalInfo!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 15) != 0 {serializeInt32(minDisplayDuration!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 15) != 0 {serializeInt32(maxDisplayDuration!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .sponsoredMessage(let flags, let randomId, let url, let title, let message, let entities, let photo, let media, let color, let buttonText, let sponsorInfo, let additionalInfo): - return ("sponsoredMessage", [("flags", flags as Any), ("randomId", randomId as Any), ("url", url as Any), ("title", title as Any), ("message", message as Any), ("entities", entities as Any), ("photo", photo as Any), ("media", media as Any), ("color", color as Any), ("buttonText", buttonText as Any), ("sponsorInfo", sponsorInfo as Any), ("additionalInfo", additionalInfo as Any)]) + case .sponsoredMessage(let flags, let randomId, let url, let title, let message, let entities, let photo, let media, let color, let buttonText, let sponsorInfo, let additionalInfo, let minDisplayDuration, let maxDisplayDuration): + return ("sponsoredMessage", [("flags", flags as Any), ("randomId", randomId as Any), ("url", url as Any), ("title", title as Any), ("message", message as Any), ("entities", entities as Any), ("photo", photo as Any), ("media", media as Any), ("color", color as Any), ("buttonText", buttonText as Any), ("sponsorInfo", sponsorInfo as Any), ("additionalInfo", additionalInfo as Any), ("minDisplayDuration", minDisplayDuration as Any), ("maxDisplayDuration", maxDisplayDuration as Any)]) } } @@ -510,6 +512,10 @@ public extension Api { if Int(_1!) & Int(1 << 7) != 0 {_11 = parseString(reader) } var _12: String? if Int(_1!) & Int(1 << 8) != 0 {_12 = parseString(reader) } + var _13: Int32? + if Int(_1!) & Int(1 << 15) != 0 {_13 = reader.readInt32() } + var _14: Int32? + if Int(_1!) & Int(1 << 15) != 0 {_14 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -522,8 +528,10 @@ public extension Api { let _c10 = _10 != nil let _c11 = (Int(_1!) & Int(1 << 7) == 0) || _11 != nil let _c12 = (Int(_1!) & Int(1 << 8) == 0) || _12 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 { - return Api.SponsoredMessage.sponsoredMessage(flags: _1!, randomId: _2!, url: _3!, title: _4!, message: _5!, entities: _6, photo: _7, media: _8, color: _9, buttonText: _10!, sponsorInfo: _11, additionalInfo: _12) + let _c13 = (Int(_1!) & Int(1 << 15) == 0) || _13 != nil + let _c14 = (Int(_1!) & Int(1 << 15) == 0) || _14 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 { + return Api.SponsoredMessage.sponsoredMessage(flags: _1!, randomId: _2!, url: _3!, title: _4!, message: _5!, entities: _6, photo: _7, media: _8, color: _9, buttonText: _10!, sponsorInfo: _11, additionalInfo: _12, minDisplayDuration: _13, maxDisplayDuration: _14) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api34.swift b/submodules/TelegramApi/Sources/Api34.swift index b2ea498c9f..1c8af79156 100644 --- a/submodules/TelegramApi/Sources/Api34.swift +++ b/submodules/TelegramApi/Sources/Api34.swift @@ -304,17 +304,19 @@ public extension Api.messages { } public extension Api.messages { enum SponsoredMessages: TypeConstructorDescription { - case sponsoredMessages(flags: Int32, postsBetween: Int32?, messages: [Api.SponsoredMessage], chats: [Api.Chat], users: [Api.User]) + case sponsoredMessages(flags: Int32, postsBetween: Int32?, startDelay: Int32?, betweenDelay: Int32?, messages: [Api.SponsoredMessage], chats: [Api.Chat], users: [Api.User]) case sponsoredMessagesEmpty public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .sponsoredMessages(let flags, let postsBetween, let messages, let chats, let users): + case .sponsoredMessages(let flags, let postsBetween, let startDelay, let betweenDelay, let messages, let chats, let users): if boxed { - buffer.appendInt32(-907141753) + buffer.appendInt32(-2464403) } serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {serializeInt32(postsBetween!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {serializeInt32(startDelay!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 2) != 0 {serializeInt32(betweenDelay!, buffer: buffer, boxed: false)} buffer.appendInt32(481674261) buffer.appendInt32(Int32(messages.count)) for item in messages { @@ -342,8 +344,8 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .sponsoredMessages(let flags, let postsBetween, let messages, let chats, let users): - return ("sponsoredMessages", [("flags", flags as Any), ("postsBetween", postsBetween as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) + case .sponsoredMessages(let flags, let postsBetween, let startDelay, let betweenDelay, let messages, let chats, let users): + return ("sponsoredMessages", [("flags", flags as Any), ("postsBetween", postsBetween as Any), ("startDelay", startDelay as Any), ("betweenDelay", betweenDelay as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) case .sponsoredMessagesEmpty: return ("sponsoredMessagesEmpty", []) } @@ -354,25 +356,31 @@ public extension Api.messages { _1 = reader.readInt32() var _2: Int32? if Int(_1!) & Int(1 << 0) != 0 {_2 = reader.readInt32() } - var _3: [Api.SponsoredMessage]? + var _3: Int32? + if Int(_1!) & Int(1 << 1) != 0 {_3 = reader.readInt32() } + var _4: Int32? + if Int(_1!) & Int(1 << 2) != 0 {_4 = reader.readInt32() } + var _5: [Api.SponsoredMessage]? if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.SponsoredMessage.self) + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.SponsoredMessage.self) } - var _4: [Api.Chat]? + var _6: [Api.Chat]? if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) } - var _5: [Api.User]? + var _7: [Api.User]? if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } let _c1 = _1 != nil let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.messages.SponsoredMessages.sponsoredMessages(flags: _1!, postsBetween: _2, messages: _3!, chats: _4!, users: _5!) + let _c6 = _6 != nil + let _c7 = _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.messages.SponsoredMessages.sponsoredMessages(flags: _1!, postsBetween: _2, startDelay: _3, betweenDelay: _4, messages: _5!, chats: _6!, users: _7!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api38.swift b/submodules/TelegramApi/Sources/Api38.swift index f78a34a36e..1c436d111e 100644 --- a/submodules/TelegramApi/Sources/Api38.swift +++ b/submodules/TelegramApi/Sources/Api38.swift @@ -21,22 +21,6 @@ public extension Api.functions.account { }) } } -public extension Api.functions.account { - static func addNoPaidMessagesException(flags: Int32, userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { - let buffer = Buffer() - buffer.appendInt32(1869122215) - serializeInt32(flags, buffer: buffer, boxed: false) - userId.serialize(buffer, true) - return (FunctionDescription(name: "account.addNoPaidMessagesException", parameters: [("flags", String(describing: flags)), ("userId", String(describing: userId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in - let reader = BufferReader(buffer) - var result: Api.Bool? - if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.Bool - } - return result - }) - } -} public extension Api.functions.account { static func cancelPasswordEmail() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -666,11 +650,13 @@ public extension Api.functions.account { } } public extension Api.functions.account { - static func getPaidMessagesRevenue(userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func getPaidMessagesRevenue(flags: Int32, parentPeer: Api.InputPeer?, userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-249139400) + buffer.appendInt32(431639143) + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {parentPeer!.serialize(buffer, true)} userId.serialize(buffer, true) - return (FunctionDescription(name: "account.getPaidMessagesRevenue", parameters: [("userId", String(describing: userId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.account.PaidMessagesRevenue? in + return (FunctionDescription(name: "account.getPaidMessagesRevenue", parameters: [("flags", String(describing: flags)), ("parentPeer", String(describing: parentPeer)), ("userId", String(describing: userId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.account.PaidMessagesRevenue? in let reader = BufferReader(buffer) var result: Api.account.PaidMessagesRevenue? if let signature = reader.readInt32() { @@ -1437,6 +1423,23 @@ public extension Api.functions.account { }) } } +public extension Api.functions.account { + static func toggleNoPaidMessagesException(flags: Int32, parentPeer: Api.InputPeer?, userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-30483850) + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {parentPeer!.serialize(buffer, true)} + userId.serialize(buffer, true) + return (FunctionDescription(name: "account.toggleNoPaidMessagesException", parameters: [("flags", String(describing: flags)), ("parentPeer", String(describing: parentPeer)), ("userId", String(describing: userId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.account { static func toggleSponsoredMessages(enabled: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -7086,11 +7089,13 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func getSponsoredMessages(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func getSponsoredMessages(flags: Int32, peer: Api.InputPeer, msgId: Int32?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-1680673735) + buffer.appendInt32(1030547536) + serializeInt32(flags, buffer: buffer, boxed: false) peer.serialize(buffer, true) - return (FunctionDescription(name: "messages.getSponsoredMessages", parameters: [("peer", String(describing: peer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.SponsoredMessages? in + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(msgId!, buffer: buffer, boxed: false)} + return (FunctionDescription(name: "messages.getSponsoredMessages", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("msgId", String(describing: msgId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.SponsoredMessages? in let reader = BufferReader(buffer) var result: Api.messages.SponsoredMessages? if let signature = reader.readInt32() { diff --git a/submodules/TelegramCore/Sources/ApiUtils/AdMessageAttribute.swift b/submodules/TelegramCore/Sources/ApiUtils/AdMessageAttribute.swift index ad5dae0a83..5e81b337c9 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/AdMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/AdMessageAttribute.swift @@ -15,8 +15,10 @@ public final class AdMessageAttribute: MessageAttribute { public let additionalInfo: String? public let canReport: Bool public let hasContentMedia: Bool + public let minDisplayDuration: Int32? + public let maxDisplayDuration: Int32? - public init(opaqueId: Data, messageType: MessageType, url: String, buttonText: String, sponsorInfo: String?, additionalInfo: String?, canReport: Bool, hasContentMedia: Bool) { + public init(opaqueId: Data, messageType: MessageType, url: String, buttonText: String, sponsorInfo: String?, additionalInfo: String?, canReport: Bool, hasContentMedia: Bool, minDisplayDuration: Int32?, maxDisplayDuration: Int32?) { self.opaqueId = opaqueId self.messageType = messageType self.url = url @@ -25,6 +27,8 @@ public final class AdMessageAttribute: MessageAttribute { self.additionalInfo = additionalInfo self.canReport = canReport self.hasContentMedia = hasContentMedia + self.minDisplayDuration = minDisplayDuration + self.maxDisplayDuration = maxDisplayDuration } public init(decoder: PostboxDecoder) { diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index e540775579..012bde151d 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -229,7 +229,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { } switch action { - case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionGiftStars, .messageActionTopicCreate, .messageActionTopicEdit, .messageActionSuggestProfilePhoto, .messageActionSetChatWallPaper, .messageActionGiveawayLaunch, .messageActionGiveawayResults, .messageActionBoostApply, .messageActionRequestedPeerSentMe, .messageActionStarGift, .messageActionStarGiftUnique, .messageActionPaidMessagesRefunded, .messageActionPaidMessagesPrice, .messageActionTodoCompletions: + case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionGiftStars, .messageActionTopicCreate, .messageActionTopicEdit, .messageActionSuggestProfilePhoto, .messageActionSetChatWallPaper, .messageActionGiveawayLaunch, .messageActionGiveawayResults, .messageActionBoostApply, .messageActionRequestedPeerSentMe, .messageActionStarGift, .messageActionStarGiftUnique, .messageActionPaidMessagesRefunded, .messageActionPaidMessagesPrice, .messageActionTodoCompletions, .messageActionTodoAppendTasks: break case let .messageActionChannelMigrateFrom(_, chatId): result.append(PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId))) diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index b10c3671c1..9956dc94b8 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -226,6 +226,8 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe ))) case let .messageActionTodoCompletions(completed, incompleted): return TelegramMediaAction(action: .todoCompletions(completed: completed, incompleted: incompleted)) + case let .messageActionTodoAppendTasks(list): + return TelegramMediaAction(action: .todoAppendTasks(list.map { TelegramMediaTodo.Item(apiItem: $0) })) } } diff --git a/submodules/TelegramCore/Sources/Network/Network.swift b/submodules/TelegramCore/Sources/Network/Network.swift index 525a743eb7..f9dd1d6c1e 100644 --- a/submodules/TelegramCore/Sources/Network/Network.swift +++ b/submodules/TelegramCore/Sources/Network/Network.swift @@ -438,7 +438,7 @@ public struct NetworkInitializationArguments { public let externalRecaptchaRequestVerification: (String, String) -> Signal public let autolockDeadine: Signal public let encryptionProvider: EncryptionProvider - public let deviceModelName:String? + public let deviceModelName: String? public let useBetaFeatures: Bool public let isICloudEnabled: Bool diff --git a/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift index 398ddb9e9c..6b993d8911 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift @@ -68,7 +68,18 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox, } return mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: stateManager.auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: mediaReferenceRevalidationContext, forceReupload: forceReupload, isGrouped: false, passFetchProgress: false, forceNoBigParts: false, peerId: messageId.peerId, media: augmentedMedia, text: "", autoremoveMessageAttribute: nil, autoclearMessageAttribute: nil, messageId: nil, attributes: attributes, mediaReference: nil) } - if let uploadSignal = generateUploadSignal(forceReupload) { + if let todo = media.media as? TelegramMediaTodo { + var flags: Int32 = 0 + if todo.flags.contains(.othersCanAppend) { + flags |= 1 << 0 + } + if todo.flags.contains(.othersCanComplete) { + flags |= 1 << 1 + } + let inputTodo = Api.InputMedia.inputMediaTodo(todo: .todoList(flags: flags, title: .textWithEntities(text: todo.text, entities: apiEntitiesFromMessageTextEntities(todo.textEntities, associatedPeers: SimpleDictionary())), list: todo.items.map { $0.apiItem })) + uploadedMedia = .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(inputTodo, text), reuploadInfo: nil, cacheReferenceKey: nil))) + } + else if let uploadSignal = generateUploadSignal(forceReupload) { uploadedMedia = .single(.progress(PendingMessageUploadedContentProgress(progress: 0.027))) |> then(uploadSignal) |> map { result -> PendingMessageUploadedContentResult? in @@ -110,7 +121,7 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox, if text.isEmpty { for media in message.media { switch media { - case _ as TelegramMediaImage, _ as TelegramMediaFile: + case _ as TelegramMediaImage, _ as TelegramMediaFile, _ as TelegramMediaTodo: break default: if let _ = scheduleTime { diff --git a/submodules/TelegramCore/Sources/State/PaidMessages.swift b/submodules/TelegramCore/Sources/State/PaidMessages.swift index d72997b581..57930c939e 100644 --- a/submodules/TelegramCore/Sources/State/PaidMessages.swift +++ b/submodules/TelegramCore/Sources/State/PaidMessages.swift @@ -11,7 +11,7 @@ func _internal_getPaidMessagesRevenue(account: Account, peerId: PeerId) -> Signa guard let inputUser else { return .single(nil) } - return account.network.request(Api.functions.account.getPaidMessagesRevenue(userId: inputUser)) + return account.network.request(Api.functions.account.getPaidMessagesRevenue(flags: 0, parentPeer: nil, userId: inputUser)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) @@ -40,7 +40,7 @@ func _internal_addNoPaidMessagesException(account: Account, peerId: PeerId, refu if refundCharged { flags |= (1 << 0) } - return account.network.request(Api.functions.account.addNoPaidMessagesException(flags: flags, userId: inputUser)) + return account.network.request(Api.functions.account.toggleNoPaidMessagesException(flags: flags, parentPeer: nil, userId: inputUser)) |> `catch` { _ -> Signal in return .single(.boolFalse) } |> mapToSignal { _ in diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index 4ab7855bcb..205308ab5a 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -161,6 +161,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case paidMessagesPriceEdited(stars: Int64, broadcastMessagesAllowed: Bool) case conferenceCall(ConferenceCall) case todoCompletions(completed: [Int32], incompleted: [Int32]) + case todoAppendTasks([TelegramMediaTodo.Item]) public init(decoder: PostboxDecoder) { let rawValue: Int32 = decoder.decodeInt32ForKey("_rawValue", orElse: 0) @@ -301,6 +302,10 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { completed: decoder.decodeInt32ArrayForKey("completed"), incompleted: decoder.decodeInt32ArrayForKey("incompleted") ) + case 50: + self = .todoAppendTasks( + decoder.decodeObjectArrayWithDecoderForKey("tasks") + ) default: self = .unknown } @@ -708,6 +713,9 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { encoder.encodeInt32(49, forKey: "_rawValue") encoder.encodeInt32Array(completed, forKey: "completed") encoder.encodeInt32Array(incompleted, forKey: "incompleted") + case let .todoAppendTasks(tasks): + encoder.encodeInt32(50, forKey: "_rawValue") + encoder.encodeObjectArray(tasks, forKey: "tasks") } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaTodo.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaTodo.swift index 7c7109f6d1..ef033d8581 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaTodo.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaTodo.swift @@ -69,7 +69,7 @@ public final class TelegramMediaTodo: Media, Equatable { return nil } public var peerIds: [PeerId] { - return [] + return self.completions.map { $0.completedBy } } public let flags: Flags diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift index 05a1a8dba1..87a848158a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift @@ -20,6 +20,8 @@ private class AdMessagesHistoryContextImpl { case sponsorInfo case additionalInfo case canReport + case minDisplayDuration + case maxDisplayDuration } enum MessageType: Int32, Codable { @@ -41,7 +43,9 @@ private class AdMessagesHistoryContextImpl { public let sponsorInfo: String? public let additionalInfo: String? public let canReport: Bool - + public let minDisplayDuration: Int32? + public let maxDisplayDuration: Int32? + public init( opaqueId: Data, messageType: MessageType, @@ -56,7 +60,9 @@ private class AdMessagesHistoryContextImpl { buttonText: String, sponsorInfo: String?, additionalInfo: String?, - canReport: Bool + canReport: Bool, + minDisplayDuration: Int32?, + maxDisplayDuration: Int32? ) { self.opaqueId = opaqueId self.messageType = messageType @@ -72,6 +78,8 @@ private class AdMessagesHistoryContextImpl { self.sponsorInfo = sponsorInfo self.additionalInfo = additionalInfo self.canReport = canReport + self.minDisplayDuration = minDisplayDuration + self.maxDisplayDuration = maxDisplayDuration } public init(from decoder: Decoder) throws { @@ -109,6 +117,9 @@ private class AdMessagesHistoryContextImpl { self.additionalInfo = try container.decodeIfPresent(String.self, forKey: .additionalInfo) self.canReport = try container.decodeIfPresent(Bool.self, forKey: .canReport) ?? false + + self.minDisplayDuration = try container.decodeIfPresent(Int32.self, forKey: .minDisplayDuration) + self.maxDisplayDuration = try container.decodeIfPresent(Int32.self, forKey: .maxDisplayDuration) } public func encode(to encoder: Encoder) throws { @@ -144,6 +155,9 @@ private class AdMessagesHistoryContextImpl { try container.encodeIfPresent(self.additionalInfo, forKey: .additionalInfo) try container.encode(self.canReport, forKey: .canReport) + + try container.encodeIfPresent(self.minDisplayDuration, forKey: .minDisplayDuration) + try container.encodeIfPresent(self.maxDisplayDuration, forKey: .maxDisplayDuration) } public static func ==(lhs: CachedMessage, rhs: CachedMessage) -> Bool { @@ -193,6 +207,12 @@ private class AdMessagesHistoryContextImpl { if lhs.canReport != rhs.canReport { return false } + if lhs.minDisplayDuration != rhs.minDisplayDuration { + return false + } + if lhs.maxDisplayDuration != rhs.maxDisplayDuration { + return false + } return true } @@ -206,10 +226,22 @@ private class AdMessagesHistoryContextImpl { case .recommended: mappedMessageType = .recommended } - attributes.append(AdMessageAttribute(opaqueId: self.opaqueId, messageType: mappedMessageType, url: self.url, buttonText: self.buttonText, sponsorInfo: self.sponsorInfo, additionalInfo: self.additionalInfo, canReport: self.canReport, hasContentMedia: !self.contentMedia.isEmpty)) + let adAttribute = AdMessageAttribute( + opaqueId: self.opaqueId, + messageType: mappedMessageType, + url: self.url, + buttonText: self.buttonText, + sponsorInfo: self.sponsorInfo, + additionalInfo: self.additionalInfo, + canReport: self.canReport, + hasContentMedia: !self.contentMedia.isEmpty, + minDisplayDuration: self.minDisplayDuration, + maxDisplayDuration: self.maxDisplayDuration + ) + attributes.append(adAttribute) if !self.textEntities.isEmpty { - let attribute = TextEntitiesMessageAttribute(entities: self.textEntities) - attributes.append(attribute) + let entitiesAttribute = TextEntitiesMessageAttribute(entities: self.textEntities) + attributes.append(entitiesAttribute) } var messagePeers = SimpleDictionary() @@ -282,7 +314,8 @@ private class AdMessagesHistoryContextImpl { private let queue: Queue private let account: Account - private let peerId: PeerId + private let peerId: EnginePeer.Id + private let messageId: EngineMessage.Id? private let maskAsSeenDisposables = DisposableDict() @@ -369,12 +402,20 @@ private class AdMessagesHistoryContextImpl { struct State: Equatable { var interPostInterval: Int32? + var startDelay: Int32? + var betweenDelay: Int32? var messages: [Message] static func ==(lhs: State, rhs: State) -> Bool { if lhs.interPostInterval != rhs.interPostInterval { return false } + if lhs.startDelay != rhs.startDelay { + return false + } + if lhs.betweenDelay != rhs.betweenDelay { + return false + } if lhs.messages.count != rhs.messages.count { return false } @@ -401,48 +442,55 @@ private class AdMessagesHistoryContextImpl { private let disposable = MetaDisposable() - init(queue: Queue, account: Account, peerId: PeerId) { + init(queue: Queue, account: Account, peerId: EnginePeer.Id, messageId: EngineMessage.Id?) { self.queue = queue self.account = account self.peerId = peerId + self.messageId = messageId let accountPeerId = account.peerId self.stateValue = State(interPostInterval: nil, messages: []) - self.state.set(CachedState.getCached(postbox: account.postbox, peerId: peerId) - |> mapToSignal { cachedState -> Signal in - if let cachedState = cachedState, cachedState.timestamp >= Int32(Date().timeIntervalSince1970) - 5 * 60 { - return account.postbox.transaction { transaction -> State in - return State(interPostInterval: cachedState.interPostInterval, messages: cachedState.messages.compactMap { message -> Message? in - return message.toMessage(peerId: peerId, transaction: transaction) - }) + if messageId == nil { + self.state.set(CachedState.getCached(postbox: account.postbox, peerId: peerId) + |> mapToSignal { cachedState -> Signal in + if let cachedState = cachedState, cachedState.timestamp >= Int32(Date().timeIntervalSince1970) - 5 * 60 { + return account.postbox.transaction { transaction -> State in + return State(interPostInterval: cachedState.interPostInterval, messages: cachedState.messages.compactMap { message -> Message? in + return message.toMessage(peerId: peerId, transaction: transaction) + }) + } + } else { + return .single(State(interPostInterval: nil, messages: [])) } - } else { - return .single(State(interPostInterval: nil, messages: [])) - } - }) + }) + } - let signal: Signal<(interPostInterval: Int32?, messages: [Message]), NoError> = account.postbox.transaction { transaction -> Api.InputPeer? in + let signal: Signal<(interPostInterval: Int32?, startDelay: Int32?, betweenDelay: Int32?, messages: [Message]), NoError> = account.postbox.transaction { transaction -> Api.InputPeer? in return transaction.getPeer(peerId).flatMap(apiInputPeer) } - |> mapToSignal { inputPeer -> Signal<(interPostInterval: Int32?, messages: [Message]), NoError> in + |> mapToSignal { inputPeer -> Signal<(interPostInterval: Int32?, startDelay: Int32?, betweenDelay: Int32?, messages: [Message]), NoError> in guard let inputPeer else { - return .single((nil, [])) + return .single((nil, nil, nil, [])) } - return account.network.request(Api.functions.messages.getSponsoredMessages(peer: inputPeer)) + var flags: Int32 = 0 + if let _ = messageId { + flags |= (1 << 0) + } + return account.network.request(Api.functions.messages.getSponsoredMessages(flags: flags, peer: inputPeer, msgId: messageId?.id)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } - |> mapToSignal { result -> Signal<(interPostInterval: Int32?, messages: [Message]), NoError> in + |> mapToSignal { result -> Signal<(interPostInterval: Int32?, startDelay: Int32?, betweenDelay: Int32?, messages: [Message]), NoError> in guard let result = result else { - return .single((nil, [])) + return .single((nil, nil, nil, [])) } - return account.postbox.transaction { transaction -> (interPostInterval: Int32?, messages: [Message]) in + return account.postbox.transaction { transaction -> (interPostInterval: Int32?, startDelay: Int32?, betweenDelay: Int32?, messages: [Message]) in switch result { - case let .sponsoredMessages(_, postsBetween, messages, chats, users): + case let .sponsoredMessages(_, postsBetween, startDelay, betweenDelay, messages, chats, users): let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users) updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) @@ -450,7 +498,7 @@ private class AdMessagesHistoryContextImpl { for message in messages { switch message { - case let .sponsoredMessage(flags, randomId, url, title, message, entities, photo, media, color, buttonText, sponsorInfo, additionalInfo): + case let .sponsoredMessage(flags, randomId, url, title, message, entities, photo, media, color, buttonText, sponsorInfo, additionalInfo, minDisplayDuration, maxDisplayDuration): var parsedEntities: [MessageTextEntity] = [] if let entities = entities { parsedEntities = messageTextEntitiesFromApiEntities(entities) @@ -486,29 +534,33 @@ private class AdMessagesHistoryContextImpl { buttonText: buttonText, sponsorInfo: sponsorInfo, additionalInfo: additionalInfo, - canReport: canReport + canReport: canReport, + minDisplayDuration: minDisplayDuration, + maxDisplayDuration: maxDisplayDuration )) } } - CachedState.setCached(transaction: transaction, peerId: peerId, state: CachedState(timestamp: Int32(Date().timeIntervalSince1970), interPostInterval: postsBetween, messages: parsedMessages)) - - return (postsBetween, parsedMessages.compactMap { message -> Message? in + if messageId == nil { + CachedState.setCached(transaction: transaction, peerId: peerId, state: CachedState(timestamp: Int32(Date().timeIntervalSince1970), interPostInterval: postsBetween, messages: parsedMessages)) + } + + return (postsBetween, startDelay, betweenDelay, parsedMessages.compactMap { message -> Message? in return message.toMessage(peerId: peerId, transaction: transaction) }) case .sponsoredMessagesEmpty: - return (nil, []) + return (nil, nil, nil, []) } } } } self.disposable.set((signal - |> deliverOn(self.queue)).start(next: { [weak self] interPostInterval, messages in + |> deliverOn(self.queue)).start(next: { [weak self] interPostInterval, startDelay, betweenDelay, messages in guard let strongSelf = self else { return } - strongSelf.stateValue = State(interPostInterval: interPostInterval, messages: messages) + strongSelf.stateValue = State(interPostInterval: interPostInterval, startDelay: startDelay, betweenDelay: betweenDelay, messages: messages) })) } @@ -560,6 +612,7 @@ public class AdMessagesHistoryContext { private let queue = Queue() private let impl: QueueLocalObject public let peerId: EnginePeer.Id + public let messageId: EngineMessage.Id? public var state: Signal<(interPostInterval: Int32?, messages: [Message]), NoError> { return Signal { subscriber in @@ -576,11 +629,13 @@ public class AdMessagesHistoryContext { } } - public init(account: Account, peerId: PeerId) { + public init(account: Account, peerId: EnginePeer.Id, messageId: EngineMessage.Id? = nil) { self.peerId = peerId + self.messageId = messageId + let queue = self.queue self.impl = QueueLocalObject(queue: queue, generate: { - return AdMessagesHistoryContextImpl(queue: queue, account: account, peerId: peerId) + return AdMessagesHistoryContextImpl(queue: queue, account: account, peerId: peerId, messageId: messageId) }) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index ee6bbb5df9..3f878d31d1 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -415,8 +415,8 @@ public extension TelegramEngine { } } - public func adMessages(peerId: PeerId) -> AdMessagesHistoryContext { - return AdMessagesHistoryContext(account: self.account, peerId: peerId) + public func adMessages(peerId: PeerId, messageId: EngineMessage.Id? = nil) -> AdMessagesHistoryContext { + return AdMessagesHistoryContext(account: self.account, peerId: peerId, messageId: messageId) } public func messageReadStats(id: MessageId) -> Signal { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Todo.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Todo.swift index 746bc39800..c976c8cabd 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Todo.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Todo.swift @@ -13,30 +13,23 @@ func _internal_requestUpdateTodoMessageItems(account: Account, messageId: Messag return account.postbox.loadedPeerWithId(messageId.peerId) |> take(1) |> castError(RequestUpdateTodoMessageError.self) - |> mapToSignal { peer in + |> mapToSignal { peer -> Signal in if let inputPeer = apiInputPeer(peer) { return account.network.request(Api.functions.messages.toggleTodoCompleted(peer: inputPeer, msgId: messageId.id, completed: completedIds, incompleted: incompletedIds)) |> mapError { _ -> RequestUpdateTodoMessageError in return .generic } - |> mapToSignal { result -> Signal in - return account.postbox.transaction { transaction -> TelegramMediaTodo? in - switch result { - case let .updates(updates, _, _, _, _): - let _ = updates - default: - break - } + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction in account.stateManager.addUpdates(result) - return nil } |> castError(RequestUpdateTodoMessageError.self) } + |> ignoreValues } else { - return .single(nil) + return .complete() } } - |> ignoreValues } public enum AppendTodoMessageError { From bc04d3c8e919b890d0ddbf63ea939ffba34660b3 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Sat, 7 Jun 2025 18:30:09 +0100 Subject: [PATCH 5/6] ads --- .../Sources/TelegramEngine/Messages/AdMessages.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift index 87a848158a..f4f8f084b2 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift @@ -614,13 +614,13 @@ public class AdMessagesHistoryContext { public let peerId: EnginePeer.Id public let messageId: EngineMessage.Id? - public var state: Signal<(interPostInterval: Int32?, messages: [Message]), NoError> { + public var state: Signal<(interPostInterval: Int32?, messages: [Message], startDelay: Int32?, betweenDelay: Int32?), NoError> { return Signal { subscriber in let disposable = MetaDisposable() self.impl.with { impl in let stateDisposable = impl.state.get().start(next: { state in - subscriber.putNext((state.interPostInterval, state.messages)) + subscriber.putNext((state.interPostInterval, state.messages, state.startDelay, state.betweenDelay)) }) disposable.set(stateDisposable) } From 41ae91610678cf3889ef9f120841bbc63a44d59e Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sun, 8 Jun 2025 18:28:49 +0200 Subject: [PATCH 6/6] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 16 + .../AccountContext/Sources/Premium.swift | 2 + .../Sources/AttachmentController.swift | 9 + .../Sources/AttachmentPanel.swift | 7 + .../AvatarNode/Sources/PeerAvatar.swift | 3 +- .../Sources/BrowserBookmarksScreen.swift | 2 + submodules/ChatListUI/BUILD | 2 +- .../ChatListFilterPresetController.swift | 2 +- .../ItemListFilterTitleInputItem.swift | 2 +- .../Sources/Node/ChatListItemStrings.swift | 14 + .../ChatPanelInterfaceInteraction.swift | 4 + submodules/ComposePollUI/BUILD | 1 + .../Sources/ComposePollScreen.swift | 45 +- .../Sources/CreatePollController.swift | 1245 -------------- .../Sources/CreatePollOptionActionItem.swift | 274 --- .../Sources/CreatePollOptionItem.swift | 539 ------ .../Sources/CreatePollTextInputItem.swift | 683 -------- submodules/GalleryUI/BUILD | 3 + .../Items/UniversalVideoGalleryItem.swift | 89 + .../PremiumUI/Sources/PremiumDemoScreen.swift | 28 + .../Sources/PremiumIntroScreen.swift | 20 + .../SyncCore/SyncCore_TelegramMediaTodo.swift | 20 + .../TelegramEngine/Messages/Todo.swift | 80 +- .../Resources/PresentationResourceKey.swift | 5 + .../Resources/PresentationResourcesChat.swift | 24 + .../Sources/ServiceMessageStrings.swift | 79 +- submodules/TelegramUI/BUILD | 1 + .../Chat/ChatMessageBubbleItemNode/BUILD | 1 + .../Sources/ChatMessageBubbleItemNode.swift | 4 + .../ChatMessagePollBubbleContentNode.swift | 4 +- .../ChatMessageTodoBubbleContentNode/BUILD | 35 + .../ChatMessageTodoBubbleContentNode.swift | 1244 ++++++++++++++ .../Sources/ChatRecentActionsController.swift | 1 + .../ChatRecentActionsControllerNode.swift | 2 + .../ChatSendAudioMessageContextPreview.swift | 2 + .../Sources/ChatControllerInteraction.swift | 7 +- .../Components/ComposeTodoScreen/BUILD | 51 + .../Sources/ComposeTodoScreen.swift | 1531 +++++++++++++++++ .../ListComposePollOptionComponent/BUILD | 30 + .../ListComposePollOptionComponent.swift | 9 + .../Sources/ListSectionComponent.swift | 5 +- .../Sources/PeerInfoScreen.swift | 3 + .../Sources/PeerSelectionControllerNode.swift | 1 + .../Sources/StarsTransactionScreen.swift | 12 +- .../StarsTransactionsListPanelComponent.swift | 10 +- .../Sources/ToastContentComponent.swift | 19 +- .../Attach Menu/Todo.imageset/Contents.json | 12 + .../Attach Menu/Todo.imageset/todolist_30.pdf | Bin 0 -> 4046 bytes .../Message/TodoCheck.imageset/Contents.json | 12 + .../Message/TodoCheck.imageset/todo_check.pdf | Bin 0 -> 3906 bytes .../Message/TodoDot.imageset/Contents.json | 12 + .../Message/TodoDot.imageset/todo_dot.pdf | Bin 0 -> 3832 bytes .../Chat/ChatControllerLoadDisplayNode.swift | 6 +- .../Chat/ChatControllerMediaRecording.swift | 12 +- .../TelegramUI/Sources/ChatController.swift | 118 +- .../ChatControllerOpenAttachmentMenu.swift | 171 +- .../Sources/ChatHistoryListNode.swift | 16 +- .../ChatInterfaceStateContextMenus.swift | 43 +- .../OverlayAudioPlayerControllerNode.swift | 2 + .../Sources/SharedAccountContext.swift | 6 + 60 files changed, 3715 insertions(+), 2865 deletions(-) delete mode 100644 submodules/ComposePollUI/Sources/CreatePollController.swift delete mode 100644 submodules/ComposePollUI/Sources/CreatePollOptionActionItem.swift delete mode 100644 submodules/ComposePollUI/Sources/CreatePollOptionItem.swift delete mode 100644 submodules/ComposePollUI/Sources/CreatePollTextInputItem.swift create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/BUILD create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift create mode 100644 submodules/TelegramUI/Components/ComposeTodoScreen/BUILD create mode 100644 submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift create mode 100644 submodules/TelegramUI/Components/ListComposePollOptionComponent/BUILD rename submodules/{ComposePollUI => TelegramUI/Components/ListComposePollOptionComponent}/Sources/ListComposePollOptionComponent.swift (98%) create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Todo.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Todo.imageset/todolist_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/TodoCheck.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/TodoCheck.imageset/todo_check.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/TodoDot.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/TodoDot.imageset/todo_dot.pdf diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 90d84f161d..544a204872 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -14412,3 +14412,19 @@ Sorry for the inconvenience."; "Stars.SendMessage.AdjustmentAction" = "OK"; "Stars.SendMessage.PriceFree" = "Free"; + +"Notification.TodoTasks_1" = "%@ task"; +"Notification.TodoTasks_any" = "%@ tasks"; + +"Notification.TodoCompleted" = "%1$@ marked \"%2$@\" as done."; +"Notification.TodoIncompleted" = "%1$@ marked \"%2$@\" as undone."; +"Notification.TodoAddedTask" = "%1$@ added a new task \"%2$@\" to \"%3$@\"."; +"Notification.TodoAddedMultipleTasks" = "%1$@ added %2$@ to \"%3$@\"."; + +"Notification.TodoCompletedYou" = "You marked \"%1$@\" as done."; +"Notification.TodoIncompletedYou" = "You marked \"%1$@\" as not done."; +"Notification.TodoAddedTaskYou" = "You added a new task \"%1$@\" to \"%2$@\"."; +"Notification.TodoAddedMultipleTasksYou" = "You added %1$@ to \"%2$@\"."; + +"Stars.Transaction.GiftTransfer" = "Gift Transfer"; +"Stars.Intro.Transaction.GiftTransfer" = "Gift Transfer"; diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index e1b5e521bb..360282a1fe 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -43,6 +43,7 @@ public enum PremiumIntroSource { case animatedEmoji case messageEffects case paidMessages + case todo case auth(String) } @@ -82,6 +83,7 @@ public enum PremiumDemoSubject { case business case messageEffects case paidMessages + case todo case businessLocation case businessHours diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index 9f4a1610df..64072bfc41 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -21,6 +21,7 @@ public enum AttachmentButtonType: Equatable { case gallery case file case location + case todo case quickReply case contact case poll @@ -36,6 +37,8 @@ public enum AttachmentButtonType: Equatable { return "file" case .location: return "location" + case .todo: + return "todo" case .quickReply: return "quickReply" case .contact: @@ -71,6 +74,12 @@ public enum AttachmentButtonType: Equatable { } else { return false } + case .todo: + if case .todo = rhs { + return true + } else { + return false + } case .quickReply: if case .quickReply = rhs { return true diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index c030d277eb..a731bf4f96 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -195,6 +195,9 @@ private final class AttachButtonComponent: CombinedComponent { case .location: name = strings.Attachment_Location imageName = "Chat/Attach Menu/Location" + case .todo: + name = "To Do List" + imageName = "Chat/Attach Menu/Todo" case .contact: name = strings.Attachment_Contact imageName = "Chat/Attach Menu/Contact" @@ -1267,6 +1270,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { }, openBoostToUnrestrict: { }, updateRecordingTrimRange: { _, _, _, _ in }, dismissAllTooltips: { + }, editTodoMessage: { _, _ in }, updateHistoryFilter: { _ in }, updateChatLocationThread: { _, _ in }, toggleChatSidebarMode: { @@ -1490,6 +1494,9 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { accessibilityTitle = self.presentationData.strings.Attachment_File case .location: accessibilityTitle = self.presentationData.strings.Attachment_Location + case .todo: + //TODO:localize + accessibilityTitle = "To Do List" case .contact: accessibilityTitle = self.presentationData.strings.Attachment_Contact case .poll: diff --git a/submodules/AvatarNode/Sources/PeerAvatar.swift b/submodules/AvatarNode/Sources/PeerAvatar.swift index e4c3bf1fc4..7d1a9c8009 100644 --- a/submodules/AvatarNode/Sources/PeerAvatar.swift +++ b/submodules/AvatarNode/Sources/PeerAvatar.swift @@ -333,7 +333,8 @@ public func peerAvatarImage(postbox: Postbox, network: Network, peerReference: P if let cutoutRect { context.setBlendMode(.copy) context.setFillColor(UIColor.clear.cgColor) - context.fillEllipse(in: cutoutRect.offsetBy(dx: 0.0, dy: size.height - cutoutRect.maxY - cutoutRect.height)) + //TODO:fix + context.fillEllipse(in: cutoutRect) } }) let unroundedImage: UIImage? diff --git a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift index d7e5cc0247..acf349f8f9 100644 --- a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift @@ -176,6 +176,8 @@ public final class BrowserBookmarksScreen: ViewController { }, playShakeAnimation: { }, displayQuickShare: { _, _ ,_ in }, updateChatLocationThread: { _, _ in + }, requestToggleTodoMessageItem: { _, _, _ in + }, displayTodoToggleUnavailable: { _ in }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index b1491734a3..808dfa9b0d 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -108,7 +108,7 @@ swift_library( "//submodules/TelegramUI/Components/PeerManagement/OldChannelsController", "//submodules/TelegramUI/Components/TextFieldComponent", "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode", - "//submodules/ComposePollUI", + "//submodules/TelegramUI/Components/ListComposePollOptionComponent", "//submodules/ChatPresentationInterfaceState", "//submodules/ShimmerEffect:ShimmerEffect", "//submodules/TelegramUI/Components/LottieComponent", diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index 5c0493ef89..93d9d7bcdc 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -20,7 +20,7 @@ import AsyncDisplayKit import UndoUI import PeerNameColorItem import EntityKeyboard -import ComposePollUI +import ListComposePollOptionComponent import ChatEntityKeyboardInputNode import ComponentFlow import ChatPresentationInterfaceState diff --git a/submodules/ChatListUI/Sources/ItemListFilterTitleInputItem.swift b/submodules/ChatListUI/Sources/ItemListFilterTitleInputItem.swift index b511eaced3..ea0101a66e 100644 --- a/submodules/ChatListUI/Sources/ItemListFilterTitleInputItem.swift +++ b/submodules/ChatListUI/Sources/ItemListFilterTitleInputItem.swift @@ -8,7 +8,7 @@ import TextNodeWithEntities import AccountContext import ItemListUI import ComponentFlow -import ComposePollUI +import ListComposePollOptionComponent import TextFieldComponent public class ItemListFilterTitleInputItem: ListViewItem, ItemListItem { diff --git a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift index 1baf8ed6a5..ba82ca6fae 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift @@ -423,6 +423,20 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: if messageText.isEmpty, case let .Loaded(content) = webpage.content { messageText = content.displayUrl } + case let todo as TelegramMediaTodo: + let pollPrefix = "☑️ " + let entityOffset = (pollPrefix as NSString).length + messageText = "\(pollPrefix)\(todo.text)" + for entity in todo.textEntities { + if case let .CustomEmoji(_, fileId) = entity.type { + if customEmojiRanges == nil { + customEmojiRanges = [] + } + let range = NSRange(location: entityOffset + entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound) + let attribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: message.associatedMedia[EngineMedia.Id(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile) + customEmojiRanges?.append((range, attribute)) + } + } default: break } diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift index f0566406fa..de76345dfc 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift @@ -177,6 +177,7 @@ public final class ChatPanelInterfaceInteraction { public let openBoostToUnrestrict: () -> Void public let updateRecordingTrimRange: (Double, Double, Bool, Bool) -> Void public let dismissAllTooltips: () -> Void + public let editTodoMessage: (MessageId, Bool) -> Void public let requestLayout: (ContainedViewLayoutTransition) -> Void public let chatController: () -> ViewController? public let statuses: ChatPanelInterfaceInteractionStatuses? @@ -293,6 +294,7 @@ public final class ChatPanelInterfaceInteraction { openBoostToUnrestrict: @escaping () -> Void, updateRecordingTrimRange: @escaping (Double, Double, Bool, Bool) -> Void, dismissAllTooltips: @escaping () -> Void, + editTodoMessage: @escaping (MessageId, Bool) -> Void, updateHistoryFilter: @escaping ((ChatPresentationInterfaceState.HistoryFilter?) -> ChatPresentationInterfaceState.HistoryFilter?) -> Void, updateChatLocationThread: @escaping (Int64?, ChatControllerAnimateInnerChatSwitchDirection?) -> Void, toggleChatSidebarMode: @escaping () -> Void, @@ -412,6 +414,7 @@ public final class ChatPanelInterfaceInteraction { self.openBoostToUnrestrict = openBoostToUnrestrict self.updateRecordingTrimRange = updateRecordingTrimRange self.dismissAllTooltips = dismissAllTooltips + self.editTodoMessage = editTodoMessage self.updateHistoryFilter = updateHistoryFilter self.updateChatLocationThread = updateChatLocationThread self.toggleChatSidebarMode = toggleChatSidebarMode @@ -540,6 +543,7 @@ public final class ChatPanelInterfaceInteraction { }, openBoostToUnrestrict: { }, updateRecordingTrimRange: { _, _, _, _ in }, dismissAllTooltips: { + }, editTodoMessage: { _, _ in }, updateHistoryFilter: { _ in }, updateChatLocationThread: { _, _ in }, toggleChatSidebarMode: { diff --git a/submodules/ComposePollUI/BUILD b/submodules/ComposePollUI/BUILD index 209d04ead7..4b1cb85132 100644 --- a/submodules/ComposePollUI/BUILD +++ b/submodules/ComposePollUI/BUILD @@ -41,6 +41,7 @@ swift_library( "//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/ChatPresentationInterfaceState", "//submodules/TelegramUI/Components/EmojiSuggestionsComponent", + "//submodules/TelegramUI/Components/ListComposePollOptionComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/ComposePollUI/Sources/ComposePollScreen.swift b/submodules/ComposePollUI/Sources/ComposePollScreen.swift index c96f0dd597..92636c4568 100644 --- a/submodules/ComposePollUI/Sources/ComposePollScreen.swift +++ b/submodules/ComposePollUI/Sources/ComposePollScreen.swift @@ -26,6 +26,49 @@ import ChatPresentationInterfaceState import EmojiSuggestionsComponent import TextFormat import TextFieldComponent +import ListComposePollOptionComponent + +public final class ComposedPoll { + public struct Text { + public let string: String + public let entities: [MessageTextEntity] + + public init(string: String, entities: [MessageTextEntity]) { + self.string = string + self.entities = entities + } + } + + public let publicity: TelegramMediaPollPublicity + public let kind: TelegramMediaPollKind + + public let text: Text + public let options: [TelegramMediaPollOption] + public let correctAnswers: [Data]? + public let results: TelegramMediaPollResults + public let deadlineTimeout: Int32? + public let usedCustomEmojiFiles: [Int64: TelegramMediaFile] + + public init( + publicity: TelegramMediaPollPublicity, + kind: TelegramMediaPollKind, + text: Text, + options: [TelegramMediaPollOption], + correctAnswers: [Data]?, + results: TelegramMediaPollResults, + deadlineTimeout: Int32?, + usedCustomEmojiFiles: [Int64: TelegramMediaFile] + ) { + self.publicity = publicity + self.kind = kind + self.text = text + self.options = options + self.correctAnswers = correctAnswers + self.results = results + self.deadlineTimeout = deadlineTimeout + self.usedCustomEmojiFiles = usedCustomEmojiFiles + } +} final class ComposePollScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -1585,7 +1628,7 @@ public class ComposePollScreen: ViewControllerComponentContainer, AttachmentCont maxPollAnwsersCount = Int(value) } return InitialData( - maxPollTextLength: Int(200), + maxPollTextLength: 200, maxPollOptionLength: 100, maxPollAnwsersCount: maxPollAnwsersCount ) diff --git a/submodules/ComposePollUI/Sources/CreatePollController.swift b/submodules/ComposePollUI/Sources/CreatePollController.swift deleted file mode 100644 index 86fb01227b..0000000000 --- a/submodules/ComposePollUI/Sources/CreatePollController.swift +++ /dev/null @@ -1,1245 +0,0 @@ -import Foundation -import UIKit -import Display -import SwiftSignalKit -import TelegramCore -import TelegramPresentationData -import ItemListUI -import PresentationDataUtils -import AccountContext -import AlertUI -import PresentationDataUtils -import TextFormat -import AttachmentUI - -private struct OrderedLinkedListItemOrderingId: RawRepresentable, Hashable { - var rawValue: Int -} - -private struct OrderedLinkedListItemOrdering: Comparable { - var id: OrderedLinkedListItemOrderingId - var lowerItemIds: Set - var higherItemIds: Set - - static func <(lhs: OrderedLinkedListItemOrdering, rhs: OrderedLinkedListItemOrdering) -> Bool { - if rhs.lowerItemIds.contains(lhs.id) { - return true - } - if rhs.higherItemIds.contains(lhs.id) { - return false - } - if lhs.lowerItemIds.contains(rhs.id) { - return false - } - if lhs.higherItemIds.contains(rhs.id) { - return true - } - assertionFailure() - return false - } -} - -private struct OrderedLinkedListItem { - var item: T - var ordering: OrderedLinkedListItemOrdering -} - -private struct OrderedLinkedList: Sequence, Equatable { - private var items: [OrderedLinkedListItem] = [] - private var nextId: Int = 0 - - init(items: [T]) { - for i in 0 ..< items.count { - self.insert(items[i], at: i, id: nil) - } - } - - static func ==(lhs: OrderedLinkedList, rhs: OrderedLinkedList) -> Bool { - if lhs.items.count != rhs.items.count { - return false - } - for i in 0 ..< lhs.items.count { - if lhs.items[i].item != rhs.items[i].item { - return false - } - } - return true - } - - func makeIterator() -> AnyIterator> { - var index = 0 - return AnyIterator { () -> OrderedLinkedListItem? in - if index < self.items.count { - let currentIndex = index - index += 1 - return self.items[currentIndex] - } - return nil - } - } - - subscript(index: Int) -> OrderedLinkedListItem { - return self.items[index] - } - - mutating func update(at index: Int, _ f: (inout T) -> Void) { - f(&self.items[index].item) - } - - var count: Int { - return self.items.count - } - - var isEmpty: Bool { - return self.items.isEmpty - } - - var last: OrderedLinkedListItem? { - return self.items.last - } - - mutating func append(_ item: T, id: OrderedLinkedListItemOrderingId?) { - self.insert(item, at: self.items.count, id: id) - } - - mutating func insert(_ item: T, at index: Int, id: OrderedLinkedListItemOrderingId?) { - let previousId = id - let id = previousId ?? OrderedLinkedListItemOrderingId(rawValue: self.nextId) - self.nextId += 1 - - if let previousId = previousId { - for i in 0 ..< self.items.count { - self.items[i].ordering.higherItemIds.remove(previousId) - self.items[i].ordering.lowerItemIds.remove(previousId) - } - } - - var lowerItemIds = Set() - var higherItemIds = Set() - for i in 0 ..< self.items.count { - if i < index { - lowerItemIds.insert(self.items[i].ordering.id) - self.items[i].ordering.higherItemIds.insert(id) - } else { - higherItemIds.insert(self.items[i].ordering.id) - self.items[i].ordering.lowerItemIds.insert(id) - } - } - - self.items.insert(OrderedLinkedListItem(item: item, ordering: OrderedLinkedListItemOrdering(id: id, lowerItemIds: lowerItemIds, higherItemIds: higherItemIds)), at: index) - } - - mutating func remove(at index: Int) { - self.items.remove(at: index) - } -} - -private let maxTextLength = 200 -private let maxOptionLength = 100 -private let maxOptionCount = 10 - -private func processPollText(_ text: String) -> String { - var text = text.trimmingCharacters(in: .whitespacesAndNewlines) - while text.contains("\n\n\n") { - text = text.replacingOccurrences(of: "\n\n\n", with: "\n\n") - } - return text -} - -private final class CreatePollControllerArguments { - let context: AccountContext - let updatePollText: (String) -> Void - let updateOptionText: (Int, String, Bool) -> Void - let moveToNextOption: (Int) -> Void - let moveToPreviousOption: (Int) -> Void - let removeOption: (Int, Bool) -> Void - let optionFocused: (Int, Bool) -> Void - let setItemIdWithRevealedOptions: (Int?, Int?) -> Void - let toggleOptionSelected: (Int) -> Void - let updateAnonymous: (Bool) -> Void - let updateMultipleChoice: (Bool) -> Void - let displayMultipleChoiceDisabled: () -> Void - let updateQuiz: (Bool) -> Void - let updateSolutionText: (NSAttributedString) -> Void - let solutionTextFocused: (Bool) -> Void - let questionTextFocused: (Bool) -> Void - - init(context: AccountContext, updatePollText: @escaping (String) -> Void, updateOptionText: @escaping (Int, String, Bool) -> Void, moveToNextOption: @escaping (Int) -> Void, moveToPreviousOption: @escaping (Int) -> Void, removeOption: @escaping (Int, Bool) -> Void, optionFocused: @escaping (Int, Bool) -> Void, setItemIdWithRevealedOptions: @escaping (Int?, Int?) -> Void, toggleOptionSelected: @escaping (Int) -> Void, updateAnonymous: @escaping (Bool) -> Void, updateMultipleChoice: @escaping (Bool) -> Void, displayMultipleChoiceDisabled: @escaping () -> Void, updateQuiz: @escaping (Bool) -> Void, updateSolutionText: @escaping (NSAttributedString) -> Void, solutionTextFocused: @escaping (Bool) -> Void, questionTextFocused: @escaping (Bool) -> Void) { - self.context = context - self.updatePollText = updatePollText - self.updateOptionText = updateOptionText - self.moveToNextOption = moveToNextOption - self.moveToPreviousOption = moveToPreviousOption - self.removeOption = removeOption - self.optionFocused = optionFocused - self.setItemIdWithRevealedOptions = setItemIdWithRevealedOptions - self.toggleOptionSelected = toggleOptionSelected - self.updateAnonymous = updateAnonymous - self.updateMultipleChoice = updateMultipleChoice - self.displayMultipleChoiceDisabled = displayMultipleChoiceDisabled - self.updateQuiz = updateQuiz - self.updateSolutionText = updateSolutionText - self.solutionTextFocused = solutionTextFocused - self.questionTextFocused = questionTextFocused - } -} - -private enum CreatePollSection: Int32 { - case text - case options - case settings - case quizSolution -} - -private enum CreatePollEntryId: Hashable { - case textHeader - case text - case optionsHeader - case option(Int) - case optionsInfo - case anonymousVotes - case multipleChoice - case quiz - case quizInfo - case quizSolutionHeader - case quizSolutionText - case quizSolutionInfo -} - -private enum CreatePollEntryTag: Equatable, ItemListItemTag { - case text - case option(Int) - case optionsInfo - case solution - - func isEqual(to other: ItemListItemTag) -> Bool { - if let other = other as? CreatePollEntryTag { - return self == other - } else { - return false - } - } -} - -private struct SolutionText: Equatable { - var value: NSAttributedString - - init(value: NSAttributedString) { - self.value = value - } - - static func ==(lhs: SolutionText, rhs: SolutionText) -> Bool { - return lhs.value.isEqual(to: rhs.value) - } -} - -private enum CreatePollEntry: ItemListNodeEntry { - case textHeader(String, ItemListSectionHeaderAccessoryText) - case text(String, String, Int) - case optionsHeader(String) - case option(id: Int, ordering: OrderedLinkedListItemOrdering, placeholder: String, text: String, revealed: Bool, hasNext: Bool, isLast: Bool, canMove: Bool, isSelected: Bool?) - case optionsInfo(String) - case anonymousVotes(String, Bool) - case multipleChoice(String, Bool, Bool) - case quiz(String, Bool) - case quizInfo(String) - case quizSolutionHeader(String) - case quizSolutionText(placeholder: String, text: SolutionText) - case quizSolutionInfo(String) - - var section: ItemListSectionId { - switch self { - case .textHeader, .text: - return CreatePollSection.text.rawValue - case .optionsHeader, .option, .optionsInfo: - return CreatePollSection.options.rawValue - case .anonymousVotes, .multipleChoice, .quiz, .quizInfo: - return CreatePollSection.settings.rawValue - case .quizSolutionHeader, .quizSolutionText, .quizSolutionInfo: - return CreatePollSection.quizSolution.rawValue - } - } - - var tag: ItemListItemTag? { - switch self { - case .text: - return CreatePollEntryTag.text - case let .option(id, _, _, _, _, _, _, _, _): - return CreatePollEntryTag.option(id) - default: - break - } - return nil - } - - var stableId: CreatePollEntryId { - switch self { - case .textHeader: - return .textHeader - case .text: - return .text - case .optionsHeader: - return .optionsHeader - case let .option(id, _, _, _, _, _, _, _, _): - return .option(id) - case .optionsInfo: - return .optionsInfo - case .anonymousVotes: - return .anonymousVotes - case .multipleChoice: - return .multipleChoice - case .quiz: - return .quiz - case .quizInfo: - return .quizInfo - case .quizSolutionHeader: - return .quizSolutionHeader - case .quizSolutionText: - return .quizSolutionText - case .quizSolutionInfo: - return .quizSolutionInfo - } - } - - private var sortId: Int { - switch self { - case .textHeader: - return 0 - case .text: - return 1 - case .optionsHeader: - return 2 - case .option: - return 3 - case .optionsInfo: - return 1001 - case .anonymousVotes: - return 1002 - case .multipleChoice: - return 1003 - case .quiz: - return 1004 - case .quizInfo: - return 1005 - case .quizSolutionHeader: - return 1006 - case .quizSolutionText: - return 1007 - case .quizSolutionInfo: - return 1008 - } - } - - static func <(lhs: CreatePollEntry, rhs: CreatePollEntry) -> Bool { - switch lhs { - case let .option(_, lhsOrdering, _, _, _, _, _, _, _): - switch rhs { - case let .option(_, rhsOrdering, _, _, _, _, _, _, _): - return lhsOrdering < rhsOrdering - default: - break - } - default: - break - } - return lhs.sortId < rhs.sortId - } - - func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { - let arguments = arguments as! CreatePollControllerArguments - switch self { - case let .textHeader(text, accessoryText): - return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: accessoryText, sectionId: self.section) - case let .text(placeholder, text, maxLength): - return ItemListMultilineInputItem(presentationData: presentationData, text: text, placeholder: placeholder, maxLength: ItemListMultilineInputItemTextLimit(value: maxLength, display: false), sectionId: self.section, style: .blocks, textUpdated: { value in - arguments.updatePollText(value) - }, updatedFocus: { value in - arguments.questionTextFocused(value) - }, tag: CreatePollEntryTag.text) - case let .optionsHeader(text): - return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .option(id, _, placeholder, text, revealed, hasNext, isLast, canMove, isSelected): - return CreatePollOptionItem(presentationData: presentationData, id: id, placeholder: placeholder, value: text, isSelected: isSelected, maxLength: maxOptionLength, editing: CreatePollOptionItemEditing(editable: true, hasActiveRevealControls: revealed), sectionId: self.section, setItemIdWithRevealedOptions: { id, fromId in - arguments.setItemIdWithRevealedOptions(id, fromId) - }, updated: { value, isFocused in - arguments.updateOptionText(id, value, isFocused) - }, next: hasNext ? { - arguments.moveToNextOption(id) - } : nil, delete: { focused in - if !isLast { - arguments.removeOption(id, focused) - } else { - arguments.moveToPreviousOption(id) - } - }, canDelete: !isLast, - canMove: canMove, - focused: { isFocused in - arguments.optionFocused(id, isFocused) - }, toggleSelected: { - arguments.toggleOptionSelected(id) - }, tag: CreatePollEntryTag.option(id)) - case let .optionsInfo(text): - return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section, tag: CreatePollEntryTag.optionsInfo) - case let .anonymousVotes(text, value): - return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in - arguments.updateAnonymous(value) - }) - case let .multipleChoice(text, value, enabled): - return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in - arguments.updateMultipleChoice(value) - }, activatedWhileDisabled: { - arguments.displayMultipleChoiceDisabled() - }) - case let .quiz(text, value): - return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in - arguments.updateQuiz(value) - }) - case let .quizInfo(text): - return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) - case let .quizSolutionHeader(text): - return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .quizSolutionText(placeholder, text): - return CreatePollTextInputItem(context: arguments.context, presentationData: presentationData, text: text.value, placeholder: placeholder, maxLength: CreatePollTextInputItemTextLimit(value: 200, display: true), sectionId: self.section, style: .blocks, textUpdated: { text in - arguments.updateSolutionText(text) - }, updatedFocus: { value in - arguments.solutionTextFocused(value) - }, tag: CreatePollEntryTag.solution) - case let .quizSolutionInfo(text): - return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) - } - } -} - -private struct CreatePollControllerOption: Equatable { - var text: String - let id: Int - var isSelected: Bool -} - -private struct CreatePollControllerState: Equatable { - var text: String = "" - var options = OrderedLinkedList(items: [CreatePollControllerOption(text: "", id: 0, isSelected: false), CreatePollControllerOption(text: "", id: 1, isSelected: false)]) - var nextOptionId: Int = 2 - var focusOptionId: Int? - var optionIdWithRevealControls: Int? - var isAnonymous: Bool = true - var isMultipleChoice: Bool = false - var isQuiz: Bool = false - var solutionText: SolutionText = SolutionText(value: NSAttributedString(string: "")) - var isEditingSolution: Bool = false -} - -private func createPollControllerEntries(presentationData: PresentationData, peer: EnginePeer, state: CreatePollControllerState, limitsConfiguration: EngineConfiguration.UserLimits, defaultIsQuiz: Bool?) -> [CreatePollEntry] { - var entries: [CreatePollEntry] = [] - - var textLimitText = ItemListSectionHeaderAccessoryText(value: "", color: .generic) - if state.text.count >= Int(maxTextLength) * 70 / 100 { - let remainingCount = Int(maxTextLength) - state.text.count - textLimitText = ItemListSectionHeaderAccessoryText(value: "\(remainingCount)", color: remainingCount < 0 ? .destructive : .generic) - } - entries.append(.textHeader(presentationData.strings.CreatePoll_TextHeader, textLimitText)) - entries.append(.text(presentationData.strings.CreatePoll_TextPlaceholder, state.text, Int(limitsConfiguration.maxCaptionLength))) - let optionsHeaderTitle: String - if let defaultIsQuiz = defaultIsQuiz, defaultIsQuiz { - optionsHeaderTitle = presentationData.strings.CreatePoll_QuizOptionsHeader - } else { - optionsHeaderTitle = presentationData.strings.CreatePoll_OptionsHeader - } - entries.append(.optionsHeader(optionsHeaderTitle)) - for i in 0 ..< state.options.count { - let isSecondLast = state.options.count == 2 && i == 0 - let isLast = i == state.options.count - 1 - let option = state.options[i].item - entries.append(.option(id: option.id, ordering: state.options[i].ordering, placeholder: isLast ? presentationData.strings.CreatePoll_AddOption : presentationData.strings.CreatePoll_OptionPlaceholder, text: option.text, revealed: state.optionIdWithRevealControls == option.id, hasNext: i != 9, isLast: isLast || isSecondLast, canMove: !isLast || state.options.count == 10, isSelected: state.isQuiz ? option.isSelected : nil)) - } - if state.options.count < maxOptionCount { - entries.append(.optionsInfo(presentationData.strings.CreatePoll_AddMoreOptions(Int32(maxOptionCount - state.options.count)))) - } else { - entries.append(.optionsInfo(presentationData.strings.CreatePoll_AllOptionsAdded)) - } - - var canBePublic = true - if case let .channel(channel) = peer, case .broadcast = channel.info { - canBePublic = false - } - - if canBePublic { - entries.append(.anonymousVotes(presentationData.strings.CreatePoll_Anonymous, state.isAnonymous)) - } - var isQuiz = false - if let defaultIsQuiz = defaultIsQuiz { - if !defaultIsQuiz { - entries.append(.multipleChoice(presentationData.strings.CreatePoll_MultipleChoice, state.isMultipleChoice && !state.isQuiz, !state.isQuiz)) - } else { - isQuiz = true - } - } else { - entries.append(.multipleChoice(presentationData.strings.CreatePoll_MultipleChoice, state.isMultipleChoice && !state.isQuiz, !state.isQuiz)) - entries.append(.quiz(presentationData.strings.CreatePoll_Quiz, state.isQuiz)) - entries.append(.quizInfo(presentationData.strings.CreatePoll_QuizInfo)) - isQuiz = state.isQuiz - } - - if isQuiz { - entries.append(.quizSolutionHeader(presentationData.strings.CreatePoll_ExplanationHeader)) - entries.append(.quizSolutionText(placeholder: presentationData.strings.CreatePoll_Explanation, text: state.solutionText)) - entries.append(.quizSolutionInfo(presentationData.strings.CreatePoll_ExplanationInfo)) - } - - return entries -} - -public final class ComposedPoll { - public struct Text { - public let string: String - public let entities: [MessageTextEntity] - - public init(string: String, entities: [MessageTextEntity]) { - self.string = string - self.entities = entities - } - } - - public let publicity: TelegramMediaPollPublicity - public let kind: TelegramMediaPollKind - - public let text: Text - public let options: [TelegramMediaPollOption] - public let correctAnswers: [Data]? - public let results: TelegramMediaPollResults - public let deadlineTimeout: Int32? - public let usedCustomEmojiFiles: [Int64: TelegramMediaFile] - - public init( - publicity: TelegramMediaPollPublicity, - kind: TelegramMediaPollKind, - text: Text, - options: [TelegramMediaPollOption], - correctAnswers: [Data]?, - results: TelegramMediaPollResults, - deadlineTimeout: Int32?, - usedCustomEmojiFiles: [Int64: TelegramMediaFile] - ) { - self.publicity = publicity - self.kind = kind - self.text = text - self.options = options - self.correctAnswers = correctAnswers - self.results = results - self.deadlineTimeout = deadlineTimeout - self.usedCustomEmojiFiles = usedCustomEmojiFiles - } -} - -private final class CreatePollContext: AttachmentMediaPickerContext { - -} - - -public class CreatePollControllerImpl: ItemListController, AttachmentContainable { - public var requestAttachmentMenuExpansion: () -> Void = {} - public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } - public var parentController: () -> ViewController? = { - return nil - } - public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in } - public var updateTabBarVisibility: (Bool, ContainedViewLayoutTransition) -> Void = { _, _ in } - public var cancelPanGesture: () -> Void = { } - public var isContainerPanning: () -> Bool = { return false } - public var isContainerExpanded: () -> Bool = { return false } - public var isMinimized: Bool = false - - public var mediaPickerContext: AttachmentMediaPickerContext? { - return CreatePollContext() - } - - fileprivate var stateValue: Atomic? - - private var hasContent: Bool { - if let stateValue { - let state = stateValue.with { $0 } - var hasNonEmptyOptions = false - for i in 0 ..< state.options.count { - let optionText = state.options[i].item.text.trimmingCharacters(in: .whitespacesAndNewlines) - if !optionText.isEmpty { - hasNonEmptyOptions = true - } - } - if hasNonEmptyOptions || !state.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - return true - } else { - return false - } - } else { - return false - } - } - - var context: AccountContext? - public func requestDismiss(completion: @escaping () -> Void) { - if self.hasContent, let context = self.context { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - self.present(textAlertController(context: context, updatedPresentationData: nil, title: nil, text: presentationData.strings.CreatePoll_CancelConfirmation, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_No, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Yes, action: { - completion() - })]), in: .window(.root)) - } else { - completion() - } - } - - public func shouldDismissImmediately() -> Bool { - return !self.hasContent - } -} - -public func createPollController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peer: EnginePeer, isQuiz: Bool? = nil, completion: @escaping (ComposedPoll) -> Void) -> ViewController { - return ComposePollScreen( - context: context, - initialData: ComposePollScreen.initialData(context: context), - peer: peer, - isQuiz: isQuiz, - completion: completion - ) -} - -private func legacyCreatePollController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peer: EnginePeer, isQuiz: Bool?, completion: @escaping (ComposedPoll) -> Void) -> ViewController { - var initialState = CreatePollControllerState() - if let isQuiz = isQuiz { - initialState.isQuiz = isQuiz - } - let statePromise = ValuePromise(initialState, ignoreRepeated: true) - let stateValue = Atomic(value: initialState) - let updateState: ((CreatePollControllerState) -> CreatePollControllerState) -> Void = { f in - statePromise.set(stateValue.modify { f($0) }) - } - - var presentControllerImpl: ((ViewController, Any?) -> Void)? - var dismissImpl: (() -> Void)? - var dismissInputImpl: (() -> Void)? - var ensureTextVisibleImpl: (() -> Void)? - var ensureOptionVisibleImpl: ((Int) -> Void)? - var ensureSolutionVisibleImpl: (() -> Void)? - var ensureQuestionVisibleImpl: (() -> Void)? - var displayQuizTooltipImpl: ((Bool) -> Void)? - - let actionsDisposable = DisposableSet() - - let checkAddressNameDisposable = MetaDisposable() - actionsDisposable.add(checkAddressNameDisposable) - - let updateAddressNameDisposable = MetaDisposable() - actionsDisposable.add(updateAddressNameDisposable) - - let arguments = CreatePollControllerArguments(context: context, updatePollText: { value in - updateState { state in - var state = state - state.focusOptionId = nil - state.text = value - state.isEditingSolution = false - return state - } - ensureTextVisibleImpl?() - }, updateOptionText: { id, value, isFocused in - var ensureVisibleId = id - updateState { state in - var state = state - for i in 0 ..< state.options.count { - if state.options[i].item.id == id { - if isFocused { - state.focusOptionId = id - } - state.options.update(at: i, { option in - option.text = value - }) - if !value.isEmpty && i == state.options.count - 1 && state.options.count < maxOptionCount { - state.options.append(CreatePollControllerOption(text: "", id: state.nextOptionId, isSelected: false), id: nil) - state.nextOptionId += 1 - } - if i != state.options.count - 1 { - ensureVisibleId = state.options[i + 1].item.id - } - break - } - } - return state - } - if isFocused { - ensureOptionVisibleImpl?(ensureVisibleId) - } - }, moveToNextOption: { id in - var resetFocusOptionId: Int? - updateState { state in - var state = state - for i in 0 ..< state.options.count { - if state.options[i].item.id == id { - if i == state.options.count - 1 { - /*state.options.append(CreatePollControllerOption(text: "", id: state.nextOptionId, isSelected: false)) - state.focusOptionId = state.nextOptionId - state.nextOptionId += 1*/ - } else { - if state.focusOptionId == state.options[i + 1].item.id { - resetFocusOptionId = state.options[i + 1].item.id - state.focusOptionId = -1 - } else { - state.focusOptionId = state.options[i + 1].item.id - } - } - break - } - } - return state - } - if let resetFocusOptionId = resetFocusOptionId { - updateState { state in - var state = state - state.focusOptionId = resetFocusOptionId - return state - } - } - }, moveToPreviousOption: { id in - var resetFocusOptionId: Int? - updateState { state in - var state = state - for i in 0 ..< state.options.count { - if state.options[i].item.id == id { - if i != 0 { - if state.focusOptionId == state.options[i - 1].item.id { - resetFocusOptionId = state.options[i - 1].item.id - state.focusOptionId = -1 - } else { - state.focusOptionId = state.options[i - 1].item.id - } - } - break - } - } - return state - } - if let resetFocusOptionId = resetFocusOptionId { - updateState { state in - var state = state - state.focusOptionId = resetFocusOptionId - return state - } - } - }, removeOption: { id, focused in - updateState { state in - var state = state - for i in 0 ..< state.options.count { - if state.options[i].item.id == id { - state.options.remove(at: i) - if focused && i != 0 { - state.focusOptionId = state.options[i - 1].item.id - } - break - } - } - let focusOnFirst = state.options.isEmpty - if state.options.count < 2 { - for i in 0 ..< (2 - state.options.count) { - if i == 0 && focusOnFirst { - state.options.append(CreatePollControllerOption(text: "", id: state.nextOptionId, isSelected: false), id: nil) - state.focusOptionId = state.nextOptionId - state.nextOptionId += 1 - } else { - state.options.append(CreatePollControllerOption(text: "", id: state.nextOptionId, isSelected: false), id: nil) - state.nextOptionId += 1 - } - } - } - return state - } - }, optionFocused: { id, isFocused in - if isFocused { - ensureOptionVisibleImpl?(id) - } else { - updateState { state in - var state = state - if state.options.count > 2 { - for i in 0 ..< state.options.count { - if state.options[i].item.id == id { - if state.options[i].item.text.isEmpty && i != state.options.count - 1 { - state.options.remove(at: i) - } - break - } - } - } - return state - } - } - }, setItemIdWithRevealedOptions: { id, fromId in - updateState { state in - var state = state - if (id == nil && fromId == state.optionIdWithRevealControls) || (id != nil && fromId == nil) { - state.optionIdWithRevealControls = id - return state - } else { - return state - } - } - }, toggleOptionSelected: { id in - updateState { state in - var state = state - for i in 0 ..< state.options.count { - if state.options[i].item.id == id { - state.options.update(at: i, { option in - option.isSelected = !option.isSelected - }) - if state.options[i].item.isSelected && state.isQuiz { - for j in 0 ..< state.options.count { - if i != j { - state.options.update(at: j, { option in - option.isSelected = false - }) - } - } - } - break - } - } - return state - } - }, updateAnonymous: { value in - updateState { state in - var state = state - state.focusOptionId = -1 - state.isAnonymous = value - return state - } - }, updateMultipleChoice: { value in - updateState { state in - var state = state - state.focusOptionId = -1 - state.isMultipleChoice = value - return state - } - }, displayMultipleChoiceDisabled: { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.CreatePoll_MultipleChoiceQuizAlert, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), nil) - }, updateQuiz: { value in - if !value { - displayQuizTooltipImpl?(value) - } - updateState { state in - var state = state - state.focusOptionId = -1 - state.isQuiz = value - if value { - state.isMultipleChoice = false - var foundSelectedOption = false - for i in 0 ..< state.options.count { - if state.options[i].item.isSelected { - if !foundSelectedOption { - foundSelectedOption = true - } else { - state.options.update(at: i, { option in - option.isSelected = false - }) - } - } - } - } - return state - } - if value { - displayQuizTooltipImpl?(value) - } - }, updateSolutionText: { text in - updateState { state in - var state = state - state.solutionText = SolutionText(value: text) - state.focusOptionId = nil - state.isEditingSolution = true - return state - } - ensureSolutionVisibleImpl?() - }, solutionTextFocused: { isFocused in - if isFocused { - ensureSolutionVisibleImpl?() - } - }, questionTextFocused: { isFocused in - if isFocused { - ensureQuestionVisibleImpl?() - } - }) - - let previousOptionIds = Atomic<[Int]?>(value: nil) - - let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData - let signal = combineLatest(queue: .mainQueue(), - presentationData, - statePromise.get(), - context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false)) - ) - |> map { presentationData, state, limitsConfiguration -> (ItemListControllerState, (ItemListNodeState, Any)) in - var presentationData = presentationData - - let updatedTheme = presentationData.theme.withModalBlocksBackground() - presentationData = presentationData.withUpdated(theme: updatedTheme) - - var enabled = true - if processPollText(state.text).isEmpty { - enabled = false - } - if state.text.count > maxTextLength { - enabled = false - } - var nonEmptyOptionCount = 0 - var hasSelectedOptions = false - for option in state.options { - if !option.item.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - nonEmptyOptionCount += 1 - } - if option.item.text.count > maxOptionLength { - enabled = false - } - if option.item.isSelected { - hasSelectedOptions = true - } - if state.isQuiz { - if option.item.text.isEmpty && option.item.isSelected { - enabled = false - } - } - } - if state.isQuiz { - if !hasSelectedOptions { - enabled = false - } - if state.solutionText.value.string.count > 200 { - enabled = false - } - } - if nonEmptyOptionCount < 2 { - enabled = false - } - var rightNavigationButton: ItemListNavigationButton? - rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.CreatePoll_Create), style: .bold, enabled: enabled, action: { - let state = stateValue.with { $0 } - var options: [TelegramMediaPollOption] = [] - var correctAnswers: [Data]? - for i in 0 ..< state.options.count { - let optionText = state.options[i].item.text.trimmingCharacters(in: .whitespacesAndNewlines) - if !optionText.isEmpty { - let optionData = "\(i)".data(using: .utf8)! - options.append(TelegramMediaPollOption(text: optionText, entities: [], opaqueIdentifier: optionData)) - if state.isQuiz && state.options[i].item.isSelected { - correctAnswers = [optionData] - } - } - } - let publicity: TelegramMediaPollPublicity - if state.isAnonymous { - publicity = .anonymous - } else { - publicity = .public - } - var resolvedSolution: TelegramMediaPollResults.Solution? - let kind: TelegramMediaPollKind - if state.isQuiz { - kind = .quiz - if !state.solutionText.value.string.isEmpty { - let entities = generateTextEntities(state.solutionText.value.string, enabledTypes: .allUrl, currentEntities: generateChatInputTextEntities(state.solutionText.value)) - resolvedSolution = TelegramMediaPollResults.Solution(text: state.solutionText.value.string, entities: entities) - } - } else { - kind = .poll(multipleAnswers: state.isMultipleChoice) - } - - let deadlineTimeout: Int32? = nil - - dismissImpl?() - - completion(ComposedPoll( - publicity: publicity, - kind: kind, - text: ComposedPoll.Text(string: processPollText(state.text), entities: []), - options: options, - correctAnswers: correctAnswers, - results: TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: [], solution: resolvedSolution), - deadlineTimeout: deadlineTimeout, - usedCustomEmojiFiles: [:] - )) - }) - - let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { - dismissImpl?() - }) - - let optionIds = state.options.map { $0.item.id } - let previousIds = previousOptionIds.swap(optionIds) - - var focusItemTag: ItemListItemTag? - var ensureVisibleItemTag: ItemListItemTag? - if let focusOptionId = state.focusOptionId { - focusItemTag = CreatePollEntryTag.option(focusOptionId) - if focusOptionId == state.options.last?.item.id { - ensureVisibleItemTag = nil - } else { - ensureVisibleItemTag = focusItemTag - } - } else if state.isEditingSolution { - focusItemTag = CreatePollEntryTag.solution - ensureVisibleItemTag = focusItemTag - } else { -// focusItemTag = CreatePollEntryTag.text -// ensureVisibleItemTag = focusItemTag - } - - let title: String - if let isQuiz = isQuiz, isQuiz { - title = presentationData.strings.CreatePoll_QuizTitle - } else { - title = presentationData.strings.CreatePoll_Title - } - - let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: createPollControllerEntries(presentationData: presentationData, peer: peer, state: state, limitsConfiguration: limitsConfiguration, defaultIsQuiz: isQuiz), style: .blocks, focusItemTag: focusItemTag, ensureVisibleItemTag: ensureVisibleItemTag, animateChanges: previousIds != nil) - - return (controllerState, (listState, arguments)) - } - |> afterDisposed { - actionsDisposable.dispose() - } - - weak var currentTooltipController: TooltipController? - let controller = CreatePollControllerImpl(context: context, state: signal) - controller.context = context - controller.stateValue = stateValue - controller.navigationPresentation = .modal - controller.visibleBottomContentOffsetChanged = { [weak controller] _ in - controller?.updateTabBarAlpha(1.0, .immediate) - } - presentControllerImpl = { [weak controller] c, a in - controller?.present(c, in: .window(.root), with: a) - } - dismissImpl = { [weak controller] in - controller?.dismiss() - } - ensureTextVisibleImpl = { [weak controller] in - controller?.afterLayout({ - guard let controller = controller else { - return - } - - var resultItemNode: ListViewItemNode? - let _ = controller.frameForItemNode({ itemNode in - if let itemNode = itemNode as? ItemListItemNode { - if let tag = itemNode.tag, tag.isEqual(to: CreatePollEntryTag.text) { - resultItemNode = itemNode as? ListViewItemNode - return true - } - } - return false - }) - if let resultItemNode = resultItemNode { - controller.ensureItemNodeVisible(resultItemNode) - } - }) - } - ensureSolutionVisibleImpl = { [weak controller] in - controller?.afterLayout({ - guard let controller = controller else { - return - } - - controller.requestAttachmentMenuExpansion() - - var resultItemNode: ListViewItemNode? - let _ = controller.frameForItemNode({ itemNode in - if let itemNode = itemNode as? ItemListItemNode { - if let tag = itemNode.tag, tag.isEqual(to: CreatePollEntryTag.solution) { - resultItemNode = itemNode as? ListViewItemNode - return true - } - } - return false - }) - if let resultItemNode = resultItemNode { - controller.ensureItemNodeVisible(resultItemNode) - } - }) - } - ensureQuestionVisibleImpl = { [weak controller] in - controller?.afterLayout({ - guard let controller = controller else { - return - } - controller.requestAttachmentMenuExpansion() - }) - } - ensureOptionVisibleImpl = { [weak controller] id in - controller?.afterLayout({ - guard let controller = controller else { - return - } - - controller.requestAttachmentMenuExpansion() - - var resultItemNode: ListViewItemNode? - let state = stateValue.with({ $0 }) - var isLast = false - if state.options.last?.item.id == id { - isLast = true - } - if resultItemNode == nil { - let _ = controller.frameForItemNode({ itemNode in - if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag { - if isLast { - if tag.isEqual(to: CreatePollEntryTag.optionsInfo) { - resultItemNode = itemNode as? ListViewItemNode - return true - } - } else { - if tag.isEqual(to: CreatePollEntryTag.option(id)) { - resultItemNode = itemNode as? ListViewItemNode - return true - } - } - } - return false - }) - } - - if let resultItemNode = resultItemNode { - controller.ensureItemNodeVisible(resultItemNode) - } - }) - } - displayQuizTooltipImpl = { [weak controller] display in - guard let controller = controller else { - return - } - var resultItemNode: CreatePollOptionItemNode? - let insets = controller.listInsets - let _ = controller.frameForItemNode({ itemNode in - if resultItemNode == nil, let itemNode = itemNode as? CreatePollOptionItemNode { - if itemNode.frame.minY >= insets.top { - resultItemNode = itemNode - return true - } - } - return false - }) - if let resultItemNode = resultItemNode, let localCheckNodeFrame = resultItemNode.checkNodeFrame { - let checkNodeFrame = resultItemNode.view.convert(localCheckNodeFrame, to: controller.view) - if display { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let tooltipController = TooltipController(content: .text(presentationData.strings.CreatePoll_QuizTip), baseFontSize: presentationData.listsFontSize.baseDisplaySize, dismissByTapOutside: true) - controller.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceViewAndRect: { [weak controller] in - if let controller = controller { - return (controller.view, checkNodeFrame.insetBy(dx: 0.0, dy: 0.0)) - } - return nil - })) - tooltipController.displayNode.layer.animatePosition(from: CGPoint(x: -checkNodeFrame.maxX, y: 0.0), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - currentTooltipController = tooltipController - } else if let tooltipController = currentTooltipController{ - currentTooltipController = nil - tooltipController.displayNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -checkNodeFrame.maxX, y: 0.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) - } - } - } - controller.setReorderEntry({ (fromIndex: Int, toIndex: Int, entries: [CreatePollEntry]) -> Signal in - let fromEntry = entries[fromIndex] - guard case let .option(id, _, _, _, _, _, _, _, _) = fromEntry else { - return .single(false) - } - var referenceId: Int? - var beforeAll = false - var afterAll = false - if toIndex < entries.count { - switch entries[toIndex] { - case let .option(toId, _, _, _, _, _, _, _, _): - referenceId = toId - default: - if entries[toIndex] < fromEntry { - beforeAll = true - } else { - afterAll = true - } - } - } else { - afterAll = true - } - - var didReorder = false - - updateState { state in - var state = state - var options = state.options - var reorderOption: OrderedLinkedListItem? - var previousIndex: Int? - for i in 0 ..< options.count { - if options[i].item.id == id { - reorderOption = options[i] - previousIndex = i - options.remove(at: i) - break - } - } - if let reorderOption = reorderOption { - if let referenceId = referenceId { - var inserted = false - for i in 0 ..< options.count - 1 { - if options[i].item.id == referenceId { - if fromIndex < toIndex { - didReorder = previousIndex != i + 1 - options.insert(reorderOption.item, at: i + 1, id: reorderOption.ordering.id) - } else { - didReorder = previousIndex != i - options.insert(reorderOption.item, at: i, id: reorderOption.ordering.id) - } - inserted = true - break - } - } - if !inserted { - if options.count >= 2 { - didReorder = previousIndex != options.count - 1 - options.insert(reorderOption.item, at: options.count - 1, id: reorderOption.ordering.id) - } else { - didReorder = previousIndex != options.count - options.append(reorderOption.item, id: reorderOption.ordering.id) - - if options.count < maxOptionCount { - options.append(CreatePollControllerOption(text: "", id: state.nextOptionId, isSelected: false), id: nil) - state.nextOptionId += 1 - } - } - } - } else if beforeAll { - didReorder = previousIndex != 0 - options.insert(reorderOption.item, at: 0, id: reorderOption.ordering.id) - } else if afterAll { - if options.count >= 2 { - didReorder = previousIndex != options.count - 1 - options.insert(reorderOption.item, at: options.count - 1, id: reorderOption.ordering.id) - } else { - didReorder = previousIndex != options.count - options.append(reorderOption.item, id: reorderOption.ordering.id) - } - } - state.options = options - } - return state - } - - if didReorder { - DispatchQueue.main.async { - dismissInputImpl?() - } - } - - return .single(didReorder) - }) - dismissInputImpl = { [weak controller] in - controller?.view.endEditing(true) - } - controller.acceptsFocusWhenInOverlay = true - controller.experimentalSnapScrollToItem = true - controller.alwaysSynchronous = true - - return controller -} diff --git a/submodules/ComposePollUI/Sources/CreatePollOptionActionItem.swift b/submodules/ComposePollUI/Sources/CreatePollOptionActionItem.swift deleted file mode 100644 index 35f7a14604..0000000000 --- a/submodules/ComposePollUI/Sources/CreatePollOptionActionItem.swift +++ /dev/null @@ -1,274 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import SwiftSignalKit -import TelegramPresentationData -import ItemListUI -import PresentationDataUtils - -class CreatePollOptionActionItem: ListViewItem, ItemListItem { - let theme: PresentationTheme - let title: String - let enabled: Bool - let tag: ItemListItemTag? - let sectionId: ItemListSectionId - let action: () -> Void - - init(theme: PresentationTheme, title: String, enabled: Bool, tag: ItemListItemTag?, sectionId: ItemListSectionId, action: @escaping () -> Void) { - self.theme = theme - self.title = title - self.enabled = enabled - self.tag = tag - self.sectionId = sectionId - self.action = action - } - - func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { - async { - let node = CreatePollOptionActionItemNode() - let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) - - node.contentSize = layout.contentSize - node.insets = layout.insets - - Queue.mainQueue().async { - completion(node, { - return (nil, { _ in apply(false) }) - }) - } - } - } - - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { - Queue.mainQueue().async { - if let nodeValue = node() as? CreatePollOptionActionItemNode { - let makeLayout = nodeValue.asyncLayout() - - var animated = true - if case .None = animation { - animated = false - } - - async { - let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) - Queue.mainQueue().async { - completion(layout, { _ in - apply(animated) - }) - } - } - } - } - } - - var selectable: Bool { - return self.enabled - } - - func selected(listView: ListView){ - listView.clearHighlightAnimated(true) - self.action() - } -} - -private let titleFont = Font.regular(17.0) - -class CreatePollOptionActionItemNode: ListViewItemNode, ItemListItemNode { - private let backgroundNode: ASDisplayNode - private let topStripeNode: ASDisplayNode - private let bottomStripeNode: ASDisplayNode - private let highlightedBackgroundNode: ASDisplayNode - private let maskNode: ASImageNode - - private let iconNode: ASImageNode - private let titleNode: TextNode - - private var item: CreatePollOptionActionItem? - - var tag: ItemListItemTag? { - return self.item?.tag - } - - init() { - self.backgroundNode = ASDisplayNode() - self.backgroundNode.isLayerBacked = true - - self.topStripeNode = ASDisplayNode() - self.topStripeNode.isLayerBacked = true - - self.bottomStripeNode = ASDisplayNode() - self.bottomStripeNode.isLayerBacked = true - - self.maskNode = ASImageNode() - - self.iconNode = ASImageNode() - self.iconNode.isLayerBacked = true - self.iconNode.displayWithoutProcessing = true - self.iconNode.displaysAsynchronously = false - - self.titleNode = TextNode() - self.titleNode.isUserInteractionEnabled = false - self.titleNode.contentMode = .left - self.titleNode.contentsScale = UIScreen.main.scale - - self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.isLayerBacked = true - - super.init(layerBacked: false, dynamicBounce: false) - - self.addSubnode(self.iconNode) - self.addSubnode(self.titleNode) - } - - func asyncLayout() -> (_ item: CreatePollOptionActionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { - let makeTitleLayout = TextNode.asyncLayout(self.titleNode) - - let currentItem = self.item - - return { item, params, neighbors in - var updatedTheme: PresentationTheme? - var updatedIcon: UIImage? - - if currentItem?.theme !== item.theme { - updatedTheme = item.theme - updatedIcon = PresentationResourcesItemList.addPhoneIcon(item.theme) - } - let leftInset: CGFloat = 60.0 + params.leftInset - - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.enabled ? item.theme.list.itemAccentColor : item.theme.list.itemDisabledTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - - let separatorHeight = UIScreenPixel - - let insets = itemListNeighborsGroupedInsets(neighbors, params) - let contentSize = CGSize(width: params.width, height: 44.0) - - let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) - let layoutSize = layout.size - - return (layout, { [weak self] animated in - if let strongSelf = self { - strongSelf.item = item - - if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor - } - - let _ = titleApply() - - let transition: ContainedViewLayoutTransition - if animated { - transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) - } else { - transition = .immediate - } - - if let updatedIcon = updatedIcon { - strongSelf.iconNode.image = updatedIcon - } - if let image = strongSelf.iconNode.image { - transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - image.size.width) / 2.0 - 3.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size)) - } - - if strongSelf.backgroundNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) - } - if strongSelf.topStripeNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) - } - if strongSelf.bottomStripeNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) - } - if strongSelf.maskNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.maskNode, at: 3) - } - - let hasCorners = itemListHasRoundedBlockLayout(params) - var hasTopCorners = false - var hasBottomCorners = false - switch neighbors.top { - case .sameSection(false): - strongSelf.topStripeNode.isHidden = true - default: - hasTopCorners = true - strongSelf.topStripeNode.isHidden = hasCorners - } - - let bottomStripeInset: CGFloat - let bottomStripeOffset: CGFloat - switch neighbors.bottom { - case .sameSection(false): - bottomStripeInset = leftInset - bottomStripeOffset = -separatorHeight - strongSelf.bottomStripeNode.isHidden = false - default: - bottomStripeInset = 0.0 - bottomStripeOffset = 0.0 - hasBottomCorners = true - strongSelf.bottomStripeNode.isHidden = hasCorners - } - - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil - - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) - strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) - strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) - transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))) - - transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size)) - - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: strongSelf.backgroundNode.frame.height + UIScreenPixel + UIScreenPixel)) - } - }) - } - } - - override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { - super.setHighlighted(highlighted, at: point, animated: animated) - - if highlighted { - self.highlightedBackgroundNode.alpha = 1.0 - if self.highlightedBackgroundNode.supernode == nil { - var anchorNode: ASDisplayNode? - if self.bottomStripeNode.supernode != nil { - anchorNode = self.bottomStripeNode - } else if self.topStripeNode.supernode != nil { - anchorNode = self.topStripeNode - } else if self.backgroundNode.supernode != nil { - anchorNode = self.backgroundNode - } - if let anchorNode = anchorNode { - self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) - } else { - self.addSubnode(self.highlightedBackgroundNode) - } - } - } else { - if self.highlightedBackgroundNode.supernode != nil { - if animated { - self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in - if let strongSelf = self { - if completed { - strongSelf.highlightedBackgroundNode.removeFromSupernode() - } - } - }) - self.highlightedBackgroundNode.alpha = 0.0 - } else { - self.highlightedBackgroundNode.removeFromSupernode() - } - } - } - } - - override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { - self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) - } - - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { - self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) - } -} diff --git a/submodules/ComposePollUI/Sources/CreatePollOptionItem.swift b/submodules/ComposePollUI/Sources/CreatePollOptionItem.swift deleted file mode 100644 index 891510a807..0000000000 --- a/submodules/ComposePollUI/Sources/CreatePollOptionItem.swift +++ /dev/null @@ -1,539 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import SwiftSignalKit -import TelegramPresentationData -import ItemListUI -import PresentationDataUtils -import CheckNode - -struct CreatePollOptionItemEditing { - let editable: Bool - let hasActiveRevealControls: Bool -} - -class CreatePollOptionItem: ListViewItem, ItemListItem { - let presentationData: ItemListPresentationData - let id: Int - let placeholder: String - let value: String - let isSelected: Bool? - let maxLength: Int - let editing: CreatePollOptionItemEditing - let sectionId: ItemListSectionId - let setItemIdWithRevealedOptions: (Int?, Int?) -> Void - let updated: (String, Bool) -> Void - let next: (() -> Void)? - let delete: (Bool) -> Void - let canDelete: Bool - let canMove: Bool - let focused: (Bool) -> Void - let toggleSelected: () -> Void - let tag: ItemListItemTag? - - init(presentationData: ItemListPresentationData, id: Int, placeholder: String, value: String, isSelected: Bool?, maxLength: Int, editing: CreatePollOptionItemEditing, sectionId: ItemListSectionId, setItemIdWithRevealedOptions: @escaping (Int?, Int?) -> Void, updated: @escaping (String, Bool) -> Void, next: (() -> Void)?, delete: @escaping (Bool) -> Void, canDelete: Bool, canMove: Bool, focused: @escaping (Bool) -> Void, toggleSelected: @escaping () -> Void, tag: ItemListItemTag?) { - self.presentationData = presentationData - self.id = id - self.placeholder = placeholder - self.value = value - self.isSelected = isSelected - self.maxLength = maxLength - self.editing = editing - self.sectionId = sectionId - self.setItemIdWithRevealedOptions = setItemIdWithRevealedOptions - self.updated = updated - self.next = next - self.delete = delete - self.canDelete = canDelete - self.canMove = canMove - self.focused = focused - self.toggleSelected = toggleSelected - self.tag = tag - } - - func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { - async { - let node = CreatePollOptionItemNode() - let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) - - node.contentSize = layout.contentSize - node.insets = layout.insets - - Queue.mainQueue().async { - completion(node, { - return (nil, { _ in apply(.None) }) - }) - } - } - } - - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { - Queue.mainQueue().async { - if let nodeValue = node() as? CreatePollOptionItemNode { - let makeLayout = nodeValue.asyncLayout() - - async { - let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) - Queue.mainQueue().async { - completion(layout, { _ in - apply(animation) - }) - } - } - } - } - } - - var selectable: Bool = false -} - -private let titleFont = Font.regular(15.0) - -class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode, ItemListItemFocusableNode, ASEditableTextNodeDelegate { - private let containerNode: ASDisplayNode - private let backgroundNode: ASDisplayNode - private let topStripeNode: ASDisplayNode - private let bottomStripeNode: ASDisplayNode - private let maskNode: ASImageNode - - private var checkNode: InteractiveCheckNode? - - private let textClippingNode: ASDisplayNode - private let textNode: EditableTextNode - private let measureTextNode: TextNode - - private let textLimitNode: TextNode - private let reorderControlNode: ItemListEditableReorderControlNode - - private var item: CreatePollOptionItem? - private var layoutParams: ListViewItemLayoutParams? - - var tag: ItemListItemTag? { - return self.item?.tag - } - - override var controlsContainer: ASDisplayNode { - return self.containerNode - } - - var checkNodeFrame: CGRect? { - guard let _ = self.layoutParams, let checkNode = self.checkNode else { - return nil - } - return checkNode.frame - } - - init() { - self.containerNode = ASDisplayNode() - self.containerNode.clipsToBounds = true - - self.backgroundNode = ASDisplayNode() - self.backgroundNode.isLayerBacked = true - self.backgroundNode.backgroundColor = .white - - self.topStripeNode = ASDisplayNode() - self.topStripeNode.isLayerBacked = true - - self.bottomStripeNode = ASDisplayNode() - self.bottomStripeNode.isLayerBacked = true - - self.maskNode = ASImageNode() - - self.reorderControlNode = ItemListEditableReorderControlNode() - - self.textClippingNode = ASDisplayNode() - self.textClippingNode.clipsToBounds = true - - self.textNode = EditableTextNode() - self.measureTextNode = TextNode() - - self.textLimitNode = TextNode() - self.textLimitNode.isUserInteractionEnabled = false - - super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) - - self.addSubnode(self.containerNode) - - self.textClippingNode.addSubnode(self.textNode) - self.containerNode.addSubnode(self.textClippingNode) - - self.containerNode.addSubnode(self.reorderControlNode) - self.containerNode.addSubnode(self.textLimitNode) - } - - override func didLoad() { - super.didLoad() - - var textColor: UIColor = .black - if let item = self.item { - textColor = item.presentationData.theme.list.itemPrimaryTextColor - } - self.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: textColor] - self.textNode.clipsToBounds = true - self.textNode.delegate = self - self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) - } - - func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { - self.item?.focused(true) - } - - func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { - self.internalEditableTextNodeDidUpdateText(editableTextNode, isLosingFocus: true) - self.item?.focused(false) - } - - func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { - guard let item = self.item else { - return false - } - if text.firstIndex(of: "\n") != nil { - if text != "\n" { - let currentText = editableTextNode.attributedText?.string ?? "" - var updatedText = (currentText as NSString).replacingCharacters(in: range, with: text) - updatedText = updatedText.replacingOccurrences(of: "\n", with: " ") - if updatedText.count == 1 { - updatedText = "" - } - let updatedAttributedText = NSAttributedString(string: updatedText, font: Font.regular(17.0), textColor: item.presentationData.theme.list.itemPrimaryTextColor) - self.textNode.attributedText = updatedAttributedText - self.editableTextNodeDidUpdateText(editableTextNode) - } - if let next = item.next { - next() - } else { - editableTextNode.resignFirstResponder() - } - return false - } - return true - } - - func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { - self.internalEditableTextNodeDidUpdateText(editableTextNode, isLosingFocus: false) - } - - private func internalEditableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode, isLosingFocus: Bool) { - if let item = self.item { - let text = self.textNode.attributedText ?? NSAttributedString() - - var updatedText = text.string - var hadReturn = false - if updatedText.firstIndex(of: "\n") != nil { - hadReturn = true - updatedText = updatedText.replacingOccurrences(of: "\n", with: " ") - } - let updatedAttributedText = NSAttributedString(string: updatedText, font: Font.regular(17.0), textColor: item.presentationData.theme.list.itemPrimaryTextColor) - if text.string != updatedAttributedText.string { - self.textNode.attributedText = updatedAttributedText - } - item.updated(updatedText, !isLosingFocus && editableTextNode.isFirstResponder()) - if hadReturn { - if let next = item.next { - next() - } else if !isLosingFocus { - editableTextNode.resignFirstResponder() - } - } - } - } - - func editableTextNodeBackspaceWhileEmpty(_ editableTextNode: ASEditableTextNode) { - self.item?.delete(editableTextNode.isFirstResponder()) - } - - func asyncLayout() -> (_ item: CreatePollOptionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { - let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode) - let makeTextLayout = TextNode.asyncLayout(self.measureTextNode) - let makeTextLimitLayout = TextNode.asyncLayout(self.textLimitNode) - - let currentItem = self.item - - return { item, params, neighbors in - var updatedTheme: PresentationTheme? - - if currentItem?.presentationData.theme !== item.presentationData.theme { - updatedTheme = item.presentationData.theme - } - - let reorderSizeAndApply = reorderControlLayout(item.presentationData.theme) - - let separatorHeight = UIScreenPixel - - let insets = itemListNeighborsGroupedInsets(neighbors, params) - - let leftInset: CGFloat = params.leftInset + (item.isSelected != nil ? 60.0 : 16.0) - let rightInset: CGFloat = 44.0 + params.rightInset - - let textLength = item.value.count - let displayTextLimit = textLength > item.maxLength * 70 / 100 - let remainingCount = item.maxLength - textLength - - let (textLimitLayout, textLimitApply) = makeTextLimitLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "\(remainingCount)", font: Font.regular(13.0), textColor: remainingCount < 0 ? item.presentationData.theme.list.itemDestructiveColor : item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: .greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) - - var measureText = item.value - if measureText.hasSuffix("\n") || measureText.isEmpty { - measureText += "|" - } - let attributedMeasureText = NSAttributedString(string: measureText, font: Font.regular(17.0), textColor: .black) - let attributedText = NSAttributedString(string: item.value, font: Font.regular(17.0), textColor: item.presentationData.theme.list.itemPrimaryTextColor) - let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedMeasureText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, lineSpacing: 0.05, cutout: nil, insets: UIEdgeInsets())) - - let textTopInset: CGFloat = 11.0 - let textBottomInset: CGFloat = 11.0 - - let contentSize = CGSize(width: params.width, height: textLayout.size.height + textTopInset + textBottomInset) - let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) - - let attributedPlaceholderText = NSAttributedString(string: item.placeholder, font: Font.regular(17.0), textColor: item.presentationData.theme.list.itemPlaceholderTextColor) - - return (layout, { [weak self] animation in - if let strongSelf = self { - let transition: ContainedViewLayoutTransition - switch animation { - case .System: - transition = .animated(duration: 0.3, curve: .spring) - default: - transition = .immediate - } - - strongSelf.item = item - strongSelf.layoutParams = params - - if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor - - if strongSelf.isNodeLoaded { - strongSelf.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: item.presentationData.theme.list.itemPrimaryTextColor] - strongSelf.textNode.tintColor = item.presentationData.theme.list.itemAccentColor - } - } - - let revealOffset = strongSelf.revealOffset - - let capitalizationType: UITextAutocapitalizationType - let autocorrectionType: UITextAutocorrectionType - let keyboardType: UIKeyboardType - - capitalizationType = .sentences - autocorrectionType = .default - keyboardType = UIKeyboardType.default - - let _ = textApply() - if let currentText = strongSelf.textNode.attributedText { - if currentText.string != attributedText.string || updatedTheme != nil { - strongSelf.textNode.attributedText = attributedText - } - } else { - strongSelf.textNode.attributedText = attributedText - } - - if strongSelf.textNode.keyboardType != keyboardType { - strongSelf.textNode.keyboardType = keyboardType - } - if strongSelf.textNode.autocapitalizationType != capitalizationType { - strongSelf.textNode.autocapitalizationType = capitalizationType - } - if strongSelf.textNode.autocorrectionType != autocorrectionType { - strongSelf.textNode.autocorrectionType = autocorrectionType - } - let returnKeyType: UIReturnKeyType - if let _ = item.next { - returnKeyType = .next - } else { - returnKeyType = .done - } - if strongSelf.textNode.returnKeyType != returnKeyType { - strongSelf.textNode.returnKeyType = returnKeyType - } - - if strongSelf.textNode.attributedPlaceholderText == nil || !strongSelf.textNode.attributedPlaceholderText!.isEqual(to: attributedPlaceholderText) { - strongSelf.textNode.attributedPlaceholderText = attributedPlaceholderText - } - - strongSelf.textNode.keyboardAppearance = item.presentationData.theme.rootController.keyboardColor.keyboardAppearance - - let checkSize = CGSize(width: 22.0, height: 22.0) - let checkFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset + 16.0, y: floor((layout.contentSize.height - checkSize.height) / 2.0)), size: checkSize) - if let isSelected = item.isSelected { - if let checkNode = strongSelf.checkNode { - transition.updateFrame(node: checkNode, frame: checkFrame) - checkNode.setSelected(isSelected, animated: true) - } else { - let checkNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: item.presentationData.theme.list.itemSwitchColors.positiveColor, strokeColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, borderColor: item.presentationData.theme.list.itemCheckColors.strokeColor, overlayBorder: false, hasInset: false, hasShadow: false)) - checkNode.setSelected(isSelected, animated: false) - checkNode.valueChanged = { [weak self] value in - self?.item?.toggleSelected() - } - strongSelf.checkNode = checkNode - strongSelf.containerNode.addSubnode(checkNode) - checkNode.frame = checkFrame - transition.animatePositionAdditive(node: checkNode, offset: CGPoint(x: -checkFrame.maxX, y: 0.0)) - } - - if let checkNode = strongSelf.checkNode { - transition.updateAlpha(node: checkNode, alpha: strongSelf.textNode.textView.text.isEmpty && item.placeholder == item.presentationData.strings.CreatePoll_AddOption ? 0.0 : 1.0) - } - } else if let checkNode = strongSelf.checkNode { - strongSelf.checkNode = nil - transition.updateFrame(node: checkNode, frame: checkFrame.offsetBy(dx: -checkFrame.maxX, dy: 0.0), completion: { [weak checkNode] _ in - checkNode?.removeFromSupernode() - }) - } - - transition.updateFrame(node: strongSelf.textClippingNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: textTopInset), size: CGSize(width: params.width - leftInset - params.rightInset, height: textLayout.size.height))) - transition.updateFrame(node: strongSelf.textNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: params.width - leftInset - rightInset, height: textLayout.size.height + 1.0))) - - if strongSelf.backgroundNode.supernode == nil { - strongSelf.containerNode.insertSubnode(strongSelf.backgroundNode, at: 0) - } - if strongSelf.topStripeNode.supernode == nil { - strongSelf.containerNode.insertSubnode(strongSelf.topStripeNode, at: 1) - } - if strongSelf.bottomStripeNode.supernode == nil { - strongSelf.containerNode.insertSubnode(strongSelf.bottomStripeNode, at: 2) - } - if strongSelf.maskNode.supernode == nil { - strongSelf.containerNode.insertSubnode(strongSelf.maskNode, at: 3) - } - - let bottomStripeWasHidden = strongSelf.bottomStripeNode.isHidden - - let hasCorners = itemListHasRoundedBlockLayout(params) - var hasTopCorners = false - var hasBottomCorners = false - switch neighbors.top { - case .sameSection(false): - strongSelf.topStripeNode.isHidden = true - default: - hasTopCorners = true - strongSelf.topStripeNode.isHidden = hasCorners - } - let bottomStripeInset: CGFloat - switch neighbors.bottom { - case .sameSection(false): - bottomStripeInset = leftInset - strongSelf.bottomStripeNode.isHidden = false - default: - bottomStripeInset = 0.0 - hasBottomCorners = true - strongSelf.bottomStripeNode.isHidden = hasCorners - } - - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil - - strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layout.contentSize.width, height: separatorHeight)) - if strongSelf.animationForKey("apparentHeight") == nil { - strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) - strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) - let previousX = strongSelf.bottomStripeNode.frame.minX - strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - UIScreenPixel), size: CGSize(width: layout.contentSize.width, height: separatorHeight)) - if !bottomStripeWasHidden { - transition.animatePositionAdditive(node: strongSelf.bottomStripeNode, offset: CGPoint(x: previousX - strongSelf.bottomStripeNode.frame.minX, y: 0.0)) - } - } else { - let previousX = strongSelf.bottomStripeNode.frame.minX - strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: strongSelf.bottomStripeNode.frame.minY), size: CGSize(width: layout.contentSize.width, height: separatorHeight)) - if !bottomStripeWasHidden { - transition.animatePositionAdditive(node: strongSelf.bottomStripeNode, offset: CGPoint(x: previousX - strongSelf.bottomStripeNode.frame.minX, y: 0.0)) - } - } - - let _ = reorderSizeAndApply.1(layout.contentSize.height, displayTextLimit, transition) - let reorderControlFrame = CGRect(origin: CGPoint(x: params.width + revealOffset - params.rightInset - reorderSizeAndApply.0 + (!item.canMove ? 44.0 + params.rightInset : 0.0), y: 0.0), size: CGSize(width: reorderSizeAndApply.0, height: layout.contentSize.height)) - transition.updateFrameAdditive(node: strongSelf.reorderControlNode, frame: reorderControlFrame) - strongSelf.reorderControlNode.isUserInteractionEnabled = item.canMove - - let _ = textLimitApply() - strongSelf.textLimitNode.frame = CGRect(origin: CGPoint(x: reorderControlFrame.minX + floor((reorderControlFrame.width - textLimitLayout.size.width) / 2.0) - 4.0 - UIScreenPixel, y: max(floor(reorderControlFrame.midY + 2.0), layout.contentSize.height - 15.0 - textLimitLayout.size.height)), size: textLimitLayout.size) - strongSelf.textLimitNode.isHidden = !displayTextLimit - - strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) - - strongSelf.setRevealOptions((left: [], right: item.canDelete ? [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)] : [])) - } - }) - } - } - - override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { - super.updateRevealOffset(offset: offset, transition: transition) - - guard let params = self.layoutParams, let item = self.item else { - return - } - - let revealOffset = offset - - let leftInset: CGFloat - leftInset = params.leftInset + (item.isSelected != nil ? 60.0 : 16.0) - - if let checkNode = self.checkNode { - var checkNodeFrame = checkNode.frame - checkNodeFrame.origin.x = params.leftInset + 11.0 + revealOffset - transition.updateFrame(node: checkNode, frame: checkNodeFrame) - } - - var reorderFrame = self.reorderControlNode.frame - reorderFrame.origin.x = params.width + revealOffset - params.rightInset - reorderFrame.width - transition.updateFrame(node: self.reorderControlNode, frame: reorderFrame) - - var textClippingNodeFrame = self.textClippingNode.frame - textClippingNodeFrame.origin.x = revealOffset + leftInset - transition.updateFrame(node: self.textClippingNode, frame: textClippingNodeFrame) - } - - override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) { - self.layer.allowsGroupOpacity = true - self.updateRevealOffsetInternal(offset: -self.bounds.width - 74.0, transition: .animated(duration: 0.2, curve: .spring), completion: { [weak self] in - self?.layer.allowsGroupOpacity = false - }) - self.item?.delete(self.textNode.isFirstResponder()) - } - - override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { - //self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) - } - - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { - //self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) - } - - func focus() { - self.textNode.becomeFirstResponder() - } - - func selectAll() { - self.textNode.textView.selectAll(nil) - } - - override func isReorderable(at point: CGPoint) -> Bool { - if self.reorderControlNode.frame.contains(point), !self.reorderControlNode.isHidden, !self.isDisplayingRevealedOptions { - return true - } - return false - } - - override func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) { - super.animateFrameTransition(progress, currentValue) - - var separatorFrame = self.bottomStripeNode.frame - separatorFrame.origin.y = currentValue - UIScreenPixel - self.bottomStripeNode.frame = separatorFrame - - self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.containerNode.bounds.width, height: currentValue)) - - let insets = self.insets - let separatorHeight = UIScreenPixel - guard let params = self.layoutParams else { - return - } - - self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: self.containerNode.bounds.width, height: currentValue + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) - self.maskNode.frame = self.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) - } -} diff --git a/submodules/ComposePollUI/Sources/CreatePollTextInputItem.swift b/submodules/ComposePollUI/Sources/CreatePollTextInputItem.swift deleted file mode 100644 index eef5401783..0000000000 --- a/submodules/ComposePollUI/Sources/CreatePollTextInputItem.swift +++ /dev/null @@ -1,683 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import SwiftSignalKit -import TelegramPresentationData -import ItemListUI -import TextFormat -import ObjCRuntimeUtils -import TextInputMenu -import AccountContext - -public enum CreatePollTextInputItemTextLimitMode { - case characters - case bytes -} - -public struct CreatePollTextInputItemTextLimit { - public let value: Int - public let display: Bool - public let mode: CreatePollTextInputItemTextLimitMode - - public init(value: Int, display: Bool, mode: CreatePollTextInputItemTextLimitMode = .characters) { - self.value = value - self.display = display - self.mode = mode - } -} - -public struct ItemListMultilineInputInlineAction { - public let icon: UIImage - public let action: (() -> Void)? - - public init(icon: UIImage, action: (() -> Void)?) { - self.icon = icon - self.action = action - } -} - -public class CreatePollTextInputItem: ListViewItem, ItemListItem { - let context: AccountContext - let presentationData: ItemListPresentationData - let text: NSAttributedString - let placeholder: String - public let sectionId: ItemListSectionId - let style: ItemListStyle - let capitalization: Bool - let autocorrection: Bool - let returnKeyType: UIReturnKeyType - let action: (() -> Void)? - let textUpdated: (NSAttributedString) -> Void - let shouldUpdateText: (String) -> Bool - let processPaste: ((String) -> Void)? - let updatedFocus: ((Bool) -> Void)? - let maxLength: CreatePollTextInputItemTextLimit? - let minimalHeight: CGFloat? - let inlineAction: ItemListMultilineInputInlineAction? - public let tag: ItemListItemTag? - - public init(context: AccountContext, presentationData: ItemListPresentationData, text: NSAttributedString, placeholder: String, maxLength: CreatePollTextInputItemTextLimit?, sectionId: ItemListSectionId, style: ItemListStyle, capitalization: Bool = true, autocorrection: Bool = true, returnKeyType: UIReturnKeyType = .default, minimalHeight: CGFloat? = nil, textUpdated: @escaping (NSAttributedString) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> Void)? = nil, updatedFocus: ((Bool) -> Void)? = nil, tag: ItemListItemTag? = nil, action: (() -> Void)? = nil, inlineAction: ItemListMultilineInputInlineAction? = nil) { - self.context = context - self.presentationData = presentationData - self.text = text - self.placeholder = placeholder - self.maxLength = maxLength - self.sectionId = sectionId - self.style = style - self.capitalization = capitalization - self.autocorrection = autocorrection - self.returnKeyType = returnKeyType - self.minimalHeight = minimalHeight - self.textUpdated = textUpdated - self.shouldUpdateText = shouldUpdateText - self.processPaste = processPaste - self.updatedFocus = updatedFocus - self.tag = tag - self.action = action - self.inlineAction = inlineAction - } - - public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { - async { - let node = CreatePollTextInputItemNode() - let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) - - node.contentSize = layout.contentSize - node.insets = layout.insets - - Queue.mainQueue().async { - completion(node, { - return (nil, { _ in apply() }) - }) - } - } - } - - public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { - Queue.mainQueue().async { - if let nodeValue = node() as? CreatePollTextInputItemNode { - let makeLayout = nodeValue.asyncLayout() - - async { - let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) - Queue.mainQueue().async { - completion(layout, { _ in - apply() - }) - } - } - } - } - } -} - -public class CreatePollTextInputItemNode: ListViewItemNode, ASEditableTextNodeDelegate, ItemListItemNode, ItemListItemFocusableNode { - private let backgroundNode: ASDisplayNode - private let topStripeNode: ASDisplayNode - private let bottomStripeNode: ASDisplayNode - private let maskNode: ASImageNode - - private let textClippingNode: ASDisplayNode - private let textNode: EditableTextNode - private let measureTextNode: TextNode - - private let limitTextNode: TextNode - private var inlineActionButtonNode: HighlightableButtonNode? - - private var item: CreatePollTextInputItem? - private var layoutParams: ListViewItemLayoutParams? - - private let inputMenu = TextInputMenu() - - public var tag: ItemListItemTag? { - return self.item?.tag - } - - public init() { - self.backgroundNode = ASDisplayNode() - self.backgroundNode.isLayerBacked = true - - self.topStripeNode = ASDisplayNode() - self.topStripeNode.isLayerBacked = true - - self.bottomStripeNode = ASDisplayNode() - self.bottomStripeNode.isLayerBacked = true - - self.maskNode = ASImageNode() - - self.textClippingNode = ASDisplayNode() - self.textClippingNode.clipsToBounds = true - - self.textNode = EditableTextNode() - self.measureTextNode = TextNode() - - self.limitTextNode = TextNode() - - super.init(layerBacked: false, dynamicBounce: false) - - self.textClippingNode.addSubnode(self.textNode) - self.addSubnode(self.textClippingNode) - - } - - override public func didLoad() { - super.didLoad() - - var textColor: UIColor = .black - if let item = self.item { - textColor = item.presentationData.theme.list.itemPrimaryTextColor - self.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), NSAttributedString.Key.foregroundColor.rawValue: textColor] - } else { - self.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: textColor] - } - self.textNode.clipsToBounds = true - self.textNode.delegate = self - self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) - } - - public func asyncLayout() -> (_ item: CreatePollTextInputItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { - let makeTextLayout = TextNode.asyncLayout(self.measureTextNode) - let makeLimitTextLayout = TextNode.asyncLayout(self.limitTextNode) - - let currentItem = self.item - - return { item, params, neighbors in - var updatedTheme: PresentationTheme? - if currentItem?.presentationData.theme !== item.presentationData.theme { - updatedTheme = item.presentationData.theme - } - - let itemBackgroundColor: UIColor - let itemSeparatorColor: UIColor - - let leftInset = 16.0 + params.rightInset - switch item.style { - case .plain: - itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor - itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor - case .blocks: - itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor - itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor - } - - var limitTextString: NSAttributedString? - var rightInset: CGFloat = params.rightInset - - if let maxLength = item.maxLength, maxLength.display { - let textLength: Int - switch maxLength.mode { - case .characters: - textLength = item.text.string.count - case .bytes: - textLength = item.text.string.data(using: .utf8, allowLossyConversion: true)?.count ?? 0 - } - let displayTextLimit = textLength > maxLength.value * 70 / 100 - let remainingCount = maxLength.value - textLength - if displayTextLimit { - limitTextString = NSAttributedString(string: "\(remainingCount)", font: Font.regular(13.0), textColor: remainingCount < 0 ? item.presentationData.theme.list.itemDestructiveColor : item.presentationData.theme.list.itemSecondaryTextColor) - } - - rightInset += 30.0 + 4.0 - } - - let (limitTextLayout, limitTextApply) = makeLimitTextLayout(TextNodeLayoutArguments(attributedString: limitTextString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0), alignment: .left, cutout: nil, insets: UIEdgeInsets())) - - if limitTextLayout.size.width > 30.0 { - rightInset += 30.0 - } - - if let inlineAction = item.inlineAction { - rightInset += inlineAction.icon.size.width + 8.0 - } - - let itemText = textAttributedStringForStateText(context: item.context, stateText: item.text, fontSize: 17.0, textColor: item.presentationData.theme.chat.inputPanel.primaryTextColor, accentTextColor: item.presentationData.theme.chat.inputPanel.panelControlAccentColor, writingDirection: nil, spoilersRevealed: false, availableEmojis: Set(), emojiViewProvider: nil, makeCollapsedQuoteAttachment: nil) - let measureText = NSMutableAttributedString(attributedString: itemText) - let measureRawString = measureText.string - if measureRawString.hasSuffix("\n") || measureRawString.isEmpty { - measureText.append(NSAttributedString(string: "|", font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: .black)) - } - let attributedText = itemText - let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: measureText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 16.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) - - let separatorHeight = UIScreenPixel - - let textTopInset: CGFloat = 11.0 - let textBottomInset: CGFloat = 11.0 - - var contentHeight: CGFloat = textLayout.size.height + textTopInset + textBottomInset - if let minimalHeight = item.minimalHeight { - contentHeight = max(minimalHeight, contentHeight) - } - - let contentSize = CGSize(width: params.width, height: contentHeight) - let insets = itemListNeighborsGroupedInsets(neighbors, params) - - let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) - let layoutSize = layout.size - - let attributedPlaceholderText = NSAttributedString(string: item.placeholder, font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: item.presentationData.theme.list.itemPlaceholderTextColor) - - return (layout, { [weak self] in - if let strongSelf = self { - strongSelf.item = item - strongSelf.layoutParams = params - - if let _ = updatedTheme { - strongSelf.topStripeNode.backgroundColor = itemSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor - strongSelf.backgroundNode.backgroundColor = itemBackgroundColor - - if strongSelf.isNodeLoaded { - strongSelf.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), NSAttributedString.Key.foregroundColor.rawValue: item.presentationData.theme.list.itemPrimaryTextColor] - strongSelf.textNode.tintColor = item.presentationData.theme.list.itemAccentColor - } - - if let inlineAction = item.inlineAction { - strongSelf.inlineActionButtonNode?.setImage(generateTintedImage(image: inlineAction.icon, color: item.presentationData.theme.list.itemAccentColor), for: .normal) - } - - strongSelf.inputMenu.updateStrings(item.presentationData.strings) - } - - let capitalizationType: UITextAutocapitalizationType = item.capitalization ? .sentences : .none - let autocorrectionType: UITextAutocorrectionType = item.autocorrection ? .default : .no - - if strongSelf.textNode.textView.autocapitalizationType != capitalizationType { - strongSelf.textNode.textView.autocapitalizationType = capitalizationType - } - if strongSelf.textNode.textView.autocorrectionType != autocorrectionType { - strongSelf.textNode.textView.autocorrectionType = autocorrectionType - } - if strongSelf.textNode.textView.returnKeyType != item.returnKeyType { - strongSelf.textNode.textView.returnKeyType = item.returnKeyType - } - - let _ = textApply() - if let currentText = strongSelf.textNode.attributedText { - if currentText.string != attributedText.string || updatedTheme != nil { - strongSelf.textNode.attributedText = attributedText - refreshGenericTextInputAttributes(context: item.context, textView: strongSelf.textNode.textView, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil, makeCollapsedQuoteAttachment: nil) - } - } else { - strongSelf.textNode.attributedText = attributedText - refreshGenericTextInputAttributes(context: item.context, textView: strongSelf.textNode.textView, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil, makeCollapsedQuoteAttachment: nil) - } - - if strongSelf.backgroundNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) - } - if strongSelf.topStripeNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) - } - if strongSelf.bottomStripeNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) - } - if strongSelf.maskNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.maskNode, at: 3) - } - - let hasCorners = itemListHasRoundedBlockLayout(params) - var hasTopCorners = false - var hasBottomCorners = false - switch neighbors.top { - case .sameSection(false): - strongSelf.topStripeNode.isHidden = true - default: - hasTopCorners = true - strongSelf.topStripeNode.isHidden = hasCorners - } - let bottomStripeInset: CGFloat - switch neighbors.bottom { - case .sameSection(false): - bottomStripeInset = leftInset - strongSelf.bottomStripeNode.isHidden = false - default: - bottomStripeInset = 0.0 - hasBottomCorners = true - strongSelf.bottomStripeNode.isHidden = hasCorners - } - - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil - - strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) - strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) - - if strongSelf.textNode.attributedPlaceholderText == nil || !strongSelf.textNode.attributedPlaceholderText!.isEqual(to: attributedPlaceholderText) { - strongSelf.textNode.attributedPlaceholderText = attributedPlaceholderText - } - - strongSelf.textNode.keyboardAppearance = item.presentationData.theme.rootController.keyboardColor.keyboardAppearance - - if strongSelf.animationForKey("apparentHeight") == nil { - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) - strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) - strongSelf.textClippingNode.frame = CGRect(origin: CGPoint(x: leftInset, y: textTopInset), size: CGSize(width: params.width - leftInset - params.rightInset, height: textLayout.size.height)) - } - strongSelf.textNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: params.width - leftInset - 16.0 - rightInset, height: textLayout.size.height + 1.0)) - - let _ = limitTextApply() - strongSelf.limitTextNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 16.0 - limitTextLayout.size.width, y: layout.contentSize.height - 15.0 - limitTextLayout.size.height), size: limitTextLayout.size) - if limitTextString != nil { - if strongSelf.limitTextNode.supernode == nil { - strongSelf.addSubnode(strongSelf.limitTextNode) - } - } else if strongSelf.limitTextNode.supernode != nil { - strongSelf.limitTextNode.removeFromSupernode() - } - - if let inlineAction = item.inlineAction { - let inlineActionButtonNode: HighlightableButtonNode - if let currentInlineActionButtonNode = strongSelf.inlineActionButtonNode { - inlineActionButtonNode = currentInlineActionButtonNode - } else { - inlineActionButtonNode = HighlightableButtonNode() - inlineActionButtonNode.setImage(generateTintedImage(image: inlineAction.icon, color: item.presentationData.theme.list.itemAccentColor), for: .normal) - inlineActionButtonNode.addTarget(strongSelf, action: #selector(strongSelf.inlineActionPressed), forControlEvents: .touchUpInside) - strongSelf.addSubnode(inlineActionButtonNode) - strongSelf.inlineActionButtonNode = inlineActionButtonNode - } - inlineActionButtonNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - inlineAction.icon.size.width - 11.0, y: 7.0), size: inlineAction.icon.size) - } else if let inlineActionButtonNode = strongSelf.inlineActionButtonNode { - inlineActionButtonNode.removeFromSupernode() - strongSelf.inlineActionButtonNode = nil - } - } - }) - } - } - - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { - self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) - } - - override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { - self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) - } - - override public func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) { - super.animateFrameTransition(progress, currentValue) - - guard let params = self.layoutParams else { - return - } - - let separatorHeight = UIScreenPixel - let insets = self.insets - let contentSize = CGSize(width: params.width, height: max(1.0, currentValue - insets.top - insets.bottom)) - - let leftInset = 16.0 + params.leftInset - let textTopInset: CGFloat = 11.0 - let textBottomInset: CGFloat = 11.0 - - self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) - self.maskNode.frame = self.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) - self.bottomStripeNode.frame = CGRect(origin: CGPoint(x: self.bottomStripeNode.frame.minX, y: contentSize.height), size: CGSize(width: self.bottomStripeNode.frame.size.width, height: separatorHeight)) - - self.textClippingNode.frame = CGRect(origin: CGPoint(x: leftInset, y: textTopInset), size: CGSize(width: max(0.0, params.width - leftInset - params.rightInset), height: max(0.0, contentSize.height - textTopInset - textBottomInset))) - } - - public func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { - self.item?.updatedFocus?(true) - self.inputMenu.activate() - } - - public func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { - self.item?.updatedFocus?(false) - self.inputMenu.deactivate() - } - - public func editableTextNodeTarget(forAction action: Selector) -> ASEditableTextNodeTargetForAction? { - if action == makeSelectorFromString("_showTextStyleOptions:") { - if #available(iOS 16.0, *) { - return ASEditableTextNodeTargetForAction(target: nil) - } else { - if case .general = self.inputMenu.state { - if self.textNode.attributedText == nil || self.textNode.attributedText!.length == 0 || self.textNode.selectedRange.length == 0 { - return ASEditableTextNodeTargetForAction(target: nil) - } - return ASEditableTextNodeTargetForAction(target: self) - } else { - return ASEditableTextNodeTargetForAction(target: nil) - } - } - } else if action == #selector(self.formatAttributesBold(_:)) || action == #selector(self.formatAttributesItalic(_:)) || action == #selector(self.formatAttributesMonospace(_:)) || action == #selector(self.formatAttributesLink(_:)) || action == #selector(self.formatAttributesStrikethrough(_:)) || action == #selector(self.formatAttributesUnderline(_:)) { - if case .format = self.inputMenu.state { - return ASEditableTextNodeTargetForAction(target: self) - } else { - return ASEditableTextNodeTargetForAction(target: nil) - } - } - if case .format = self.inputMenu.state { - return ASEditableTextNodeTargetForAction(target: nil) - } - return nil - } - - @objc func _showTextStyleOptions(_ sender: Any) { - self.inputMenu.format(view: self.textNode.view, rect: self.textNode.selectionRect.offsetBy(dx: 0.0, dy: -self.textNode.textView.contentOffset.y).insetBy(dx: 0.0, dy: -1.0)) - } - - @available(iOS 16.0, *) - public func editableTextNodeMenu(_ editableTextNode: ASEditableTextNode, forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu { - var actions = suggestedActions - - if editableTextNode.attributedText == nil || editableTextNode.attributedText!.length == 0 || editableTextNode.selectedRange.length == 0 { - - } else if let strings = self.item?.presentationData.strings { - let children: [UIAction] = [ - UIAction(title: strings.TextFormat_Bold, image: nil) { [weak self] (action) in - if let strongSelf = self { - strongSelf.formatAttributesBold(strongSelf) - } - }, - UIAction(title: strings.TextFormat_Italic, image: nil) { [weak self] (action) in - if let strongSelf = self { - strongSelf.formatAttributesItalic(strongSelf) - } - }, - UIAction(title: strings.TextFormat_Monospace, image: nil) { [weak self] (action) in - if let strongSelf = self { - strongSelf.formatAttributesMonospace(strongSelf) - } - }, - UIAction(title: strings.TextFormat_Link, image: nil) { [weak self] (action) in - if let strongSelf = self { - strongSelf.formatAttributesLink(strongSelf) - } - }, - UIAction(title: strings.TextFormat_Strikethrough, image: nil) { [weak self] (action) in - if let strongSelf = self { - strongSelf.formatAttributesStrikethrough(strongSelf) - } - }, - UIAction(title: strings.TextFormat_Underline, image: nil) { [weak self] (action) in - if let strongSelf = self { - strongSelf.formatAttributesUnderline(strongSelf) - } - }, - UIAction(title: strings.TextFormat_Spoiler, image: nil) { [weak self] (action) in - if let strongSelf = self { - strongSelf.formatAttributesSpoiler(strongSelf) - } - } - ] - - let formatMenu = UIMenu(title: strings.TextFormat_Format, image: nil, children: children) - actions.insert(formatMenu, at: 3) - } - - return UIMenu(children: actions) - } - - @objc func formatAttributesBold(_ sender: Any) { - self.inputMenu.back() - if let item = self.item { - chatTextInputAddFormattingAttribute(item: item, textNode: self.textNode, theme: item.presentationData.theme, attribute: ChatTextInputAttributes.bold, value: nil) - } - } - - @objc func formatAttributesItalic(_ sender: Any) { - self.inputMenu.back() - if let item = self.item { - chatTextInputAddFormattingAttribute(item: item, textNode: self.textNode, theme: item.presentationData.theme, attribute: ChatTextInputAttributes.italic, value: nil) - } - } - - @objc func formatAttributesMonospace(_ sender: Any) { - self.inputMenu.back() - if let item = self.item { - chatTextInputAddFormattingAttribute(item: item, textNode: self.textNode, theme: item.presentationData.theme, attribute: ChatTextInputAttributes.monospace, value: nil) - } - } - - @objc func formatAttributesLink(_ sender: Any) { - self.inputMenu.back() - //self.interfaceInteraction?.openLinkEditing() - } - - @objc func formatAttributesStrikethrough(_ sender: Any) { - self.inputMenu.back() - if let item = self.item { - chatTextInputAddFormattingAttribute(item: item, textNode: self.textNode, theme: item.presentationData.theme, attribute: ChatTextInputAttributes.strikethrough, value: nil) - } - } - - @objc func formatAttributesUnderline(_ sender: Any) { - self.inputMenu.back() - if let item = self.item { - chatTextInputAddFormattingAttribute(item: item, textNode: self.textNode, theme: item.presentationData.theme, attribute: ChatTextInputAttributes.underline, value: nil) - } - } - - @objc func formatAttributesSpoiler(_ sender: Any) { - self.inputMenu.back() - if let item = self.item { - chatTextInputAddFormattingAttribute(item: item, textNode: self.textNode, theme: item.presentationData.theme, attribute: ChatTextInputAttributes.spoiler, value: nil) - } - } - - @objc func formatAttributesQuote(_ sender: Any) { - self.inputMenu.back() - if let item = self.item { - chatTextInputAddFormattingAttribute(item: item, textNode: self.textNode, theme: item.presentationData.theme, attribute: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .quote, isCollapsed: false)) - } - } - - @objc func formatAttributesCodeBlock(_ sender: Any) { - self.inputMenu.back() - if let item = self.item { - chatTextInputAddFormattingAttribute(item: item, textNode: self.textNode, theme: item.presentationData.theme, attribute: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .code(language: nil), isCollapsed: false)) - } - } - - public func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { - if let item = self.item { - if text.count > 1, let processPaste = item.processPaste { - processPaste(text) - return false - } - - if let action = item.action, text == "\n" { - action() - return false - } - - let newText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text) - if !item.shouldUpdateText(newText) { - return false - } - } - return true - } - - public func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { - if let item = self.item { - if let _ = self.textNode.attributedText { - refreshGenericTextInputAttributes(context: item.context, textView: editableTextNode.textView, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil, makeCollapsedQuoteAttachment: nil) - let updatedText = stateAttributedStringForText(self.textNode.attributedText!) - item.textUpdated(updatedText) - } else { - item.textUpdated(NSAttributedString(string: "")) - } - } - } - - public func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool { - if let _ = self.item { - let text: String? = UIPasteboard.general.string - if let _ = text { - return true - } - } - return false - } - - public func editableTextNodeDidChangeSelection(_ editableTextNode: ASEditableTextNode, fromSelectedRange: NSRange, toSelectedRange: NSRange, dueToEditing: Bool) { - /*if !dueToEditing && !self.updatingInputState { - }*/ - - if let item = self.item { - if case .format = self.inputMenu.state { - self.inputMenu.deactivate() - UIMenuController.shared.update() - } - - refreshChatTextInputTypingAttributes(editableTextNode.textView, theme: item.presentationData.theme, baseFontSize: 17.0) - refreshGenericTextInputAttributes(context: item.context, textView: editableTextNode.textView, theme: item.presentationData.theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil, makeCollapsedQuoteAttachment: nil) - } - } - - public func focus() { - if !self.textNode.textView.isFirstResponder { - self.textNode.textView.becomeFirstResponder() - } - } - - public func selectAll() { - self.textNode.textView.selectAll(nil) - } - - public func animateError() { - self.textNode.layer.addShakeAnimation() - } - - @objc private func inlineActionPressed() { - if let action = self.item?.inlineAction?.action { - action() - } - } -} - -private func chatTextInputAddFormattingAttribute(item: CreatePollTextInputItem, textNode: EditableTextNode, theme: PresentationTheme, attribute: NSAttributedString.Key, value: Any?) { - if let currentText = textNode.attributedText, textNode.selectedRange.length > 0 { - let nsRange = NSRange(location: textNode.selectedRange.location, length: textNode.selectedRange.length) - var addAttribute = true - var attributesToRemove: [NSAttributedString.Key] = [] - currentText.enumerateAttributes(in: nsRange, options: .longestEffectiveRangeNotRequired) { attributes, range, stop in - for (key, _) in attributes { - if key == attribute && range == nsRange { - addAttribute = false - attributesToRemove.append(key) - } - } - } - - let result = NSMutableAttributedString(attributedString: currentText) - for attribute in attributesToRemove { - result.removeAttribute(attribute, range: nsRange) - } - if addAttribute { - result.addAttribute(attribute, value: true as Bool, range: nsRange) - } - - textNode.attributedText = result - textNode.selectedRange = nsRange - - refreshChatTextInputTypingAttributes(textNode.textView, theme: theme, baseFontSize: 17.0) - refreshGenericTextInputAttributes(context: item.context, textView: textNode.textView, theme: theme, baseFontSize: 17.0, availableEmojis: Set(), emojiViewProvider: nil, makeCollapsedQuoteAttachment: nil) - - let updatedText = stateAttributedStringForText(textNode.attributedText!) - item.textUpdated(updatedText) - } -} diff --git a/submodules/GalleryUI/BUILD b/submodules/GalleryUI/BUILD index 1ebf1cad4c..8e1eb1f63d 100644 --- a/submodules/GalleryUI/BUILD +++ b/submodules/GalleryUI/BUILD @@ -60,9 +60,12 @@ swift_library( "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/TelegramUI/Components/LottieComponent", "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BundleIconComponent", "//submodules/Components/BalancedTextComponent", "//submodules/Components/ComponentDisplayAdapters", "//submodules/ComponentFlow", + "//submodules/TelegramUI/Components/ToastComponent", + "//submodules/SemanticStatusNode", ], visibility = [ "//visibility:public", diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index f4bf3b8f89..501a12a52f 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -33,6 +33,9 @@ import RasterizedCompositionComponent import BadgeComponent import ComponentFlow import ComponentDisplayAdapters +import ToastComponent +import MultilineTextComponent +import BundleIconComponent public enum UniversalVideoGalleryItemContentInfo { case message(Message, Int?) @@ -1390,8 +1393,94 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if dismiss { self.dismiss() } + + if self.adDisposable == nil, let contentInfo = self.item?.contentInfo, case let .message(message, _) = contentInfo { + let adContext = self.context.engine.messages.adMessages(peerId: message.id.peerId, messageId: message.id) + self.adContext = adContext + self.adDisposable = (adContext.state + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let self else { + return + } + if let message = state.messages.first { + Queue.mainQueue().after(2.0, { + self.adMessage = message + if let validLayout = self.validLayout { + self.containerLayoutUpdated(validLayout.layout, navigationBarHeight: validLayout.navigationBarHeight, transition: .immediate) + } + }) + } + }) + } + + if let adMessage = self.adMessage { + let sideInset: CGFloat = 16.0 + let title = adMessage.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? "" + + let adSize = self.adView.update( + transition: .immediate, + component: AnyComponent( + ToastContentComponent( + icon: AnyComponent( + BundleIconComponent(name: "Components/AdMock", tintColor: nil, maxSize: CGSize(width: 30.0, height: 30.0)) + ), + content: AnyComponent( + HStack([ + AnyComponentWithIdentity(id: 0, component: AnyComponent( + VStack([ + AnyComponentWithIdentity(id: 0, component: AnyComponent( + HStack([ + AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white))))), + AnyComponentWithIdentity(id: 1, component: AnyComponent(Image(image: PresentationResourcesChatList.searchAdIcon(presentationData.theme, strings: presentationData.strings), size: CGSize(width: 31.0, height: 15.0)))) + ], spacing: 5.0) + )), + AnyComponentWithIdentity(id: 1, component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: adMessage.text, font: Font.regular(14.0), textColor: .white))) + )) + ], alignment: .left, spacing: 3.0, fillWidth: false) + )), + AnyComponentWithIdentity(id: 1, component: AnyComponent( + AdRemainingProgressComponent(action: { [weak self] in + guard let self else { + return + } + self.adMessage = nil + if let validLayout = self.validLayout { + self.containerLayoutUpdated(validLayout.layout, navigationBarHeight: validLayout.navigationBarHeight, transition: .immediate) + } + }) + )) + ], spacing: 16.0, alignment: .alternatingLeftRight) + ), action: { [weak self] in + if let self, let item = self.item, let ad = adMessage.adAttribute { + item.performAction(.url(url: ad.url, concealed: false)) + } + } + ) + ), + environment: {}, + containerSize: CGSize(width: layout.size.width - sideInset * 2.0, height: 70.0) + ) + if let adView = self.adView.view { + if adView.superview == nil { + self.view.addSubview(adView) + + adView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + adView.layer.animatePosition(from: CGPoint(x: 0.0, y: 64.0), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + adView.frame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - adSize.height - 145.0), size: adSize) + } + } else if let adView = self.adView.view { + adView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + adView.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 64.0), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) + } } + private var adView = ComponentView() + private var adContext: AdMessagesHistoryContext? + private var adDisposable: Disposable? + private var adMessage: Message? + func setupItem(_ item: UniversalVideoGalleryItem) { if self.item?.content.id != item.content.id { var chapters = parseMediaPlayerChapters(item.caption) diff --git a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift index 885dac1f52..2e8bf13ea2 100644 --- a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift @@ -1118,6 +1118,26 @@ private final class DemoSheetContent: CombinedComponent { ) ) + //TODO:localize + availableItems[.todo] = DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.todo, + component: AnyComponent( + PageComponent( + content: AnyComponent(PhoneDemoComponent( + context: component.context, + position: .top, + videoFile: configuration.videos["todo"], + decoration: .badgeStars + )), + title: "To-Do Lists", + text: "Plan, assign and complete tasks – seamlessly and efficiently.", + textColor: textColor + ) + ) + ) + ) + let index: Int = 0 var items: [DemoPagerComponent.Item] = [] if let item = availableItems.first(where: { $0.value.content.id == component.subject as AnyHashable }) { @@ -1216,6 +1236,9 @@ private final class DemoSheetContent: CombinedComponent { text = strings.Premium_MessageEffectsInfo case .paidMessages: text = strings.Premium_PaidMessagesInfo + case .todo: + //TODO:localize + text = "Plan, assign and complete tasks – seamlessly and efficiently." default: text = "" } @@ -1302,6 +1325,8 @@ private final class DemoSheetContent: CombinedComponent { buttonAnimationName = "premium_unlock" case .paidMessages: buttonText = strings.Premium_PaidMessages_Proceed + case .todo: + buttonText = strings.Premium_PaidMessages_Proceed default: buttonText = strings.Common_OK } @@ -1492,6 +1517,7 @@ public class PremiumDemoScreen: ViewControllerComponentContainer { case folderTags case messageEffects case paidMessages + case todo case businessLocation case businessHours @@ -1552,6 +1578,8 @@ public class PremiumDemoScreen: ViewControllerComponentContainer { return .messageEffects case .paidMessages: return .paidMessages + case .todo: + return .todo case .businessLocation: return .businessLocation case .businessHours: diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index c96c0668d6..659e7d0e36 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -309,6 +309,12 @@ public enum PremiumSource: Equatable { } else { return false } + case .todo: + if case .todo = rhs { + return true + } else { + return false + } case let .auth(lhsPrice): if case let .auth(rhsPrice) = rhs, lhsPrice == rhsPrice { return true @@ -363,6 +369,7 @@ public enum PremiumSource: Equatable { case folderTags case messageEffects case paidMessages + case todo case auth(String) var identifier: String? { @@ -459,6 +466,8 @@ public enum PremiumSource: Equatable { return "effects" case .paidMessages: return "paid_messages" + case .todo: + return "todo" case .auth: return "auth" } @@ -490,6 +499,7 @@ public enum PremiumPerk: CaseIterable { case folderTags case messageEffects case paidMessages + case todo case businessLocation case businessHours @@ -601,6 +611,8 @@ public enum PremiumPerk: CaseIterable { return "effects" case .paidMessages: return "paid_messages" + case .todo: + return "todo" case .business: return "business" case .businessLocation: @@ -672,6 +684,9 @@ public enum PremiumPerk: CaseIterable { return strings.Premium_MessageEffects case .paidMessages: return strings.Premium_PaidMessages + case .todo: + //TODO:localize + return "To-Do Lists" case .businessLocation: return strings.Business_Location case .businessHours: @@ -741,6 +756,9 @@ public enum PremiumPerk: CaseIterable { return strings.Premium_MessageEffectsInfo case .paidMessages: return strings.Premium_PaidMessagesInfo + case .todo: + //TODO:localize + return "Plan, assign and complete tasks – seamlessly and efficiently." case .businessLocation: return strings.Business_LocationInfo case .businessHours: @@ -810,6 +828,8 @@ public enum PremiumPerk: CaseIterable { return "Premium/Perk/MessageEffects" case .paidMessages: return "Premium/Perk/PaidMessages" + case .todo: + return "Premium/Perk/PaidMessages" case .businessLocation: return "Premium/BusinessPerk/Location" case .businessHours: diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaTodo.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaTodo.swift index ef033d8581..e9c9850d27 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaTodo.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaTodo.swift @@ -131,4 +131,24 @@ public final class TelegramMediaTodo: Media, Equatable { } return true } + + func withUpdated(items: [TelegramMediaTodo.Item]) -> TelegramMediaTodo { + return TelegramMediaTodo( + flags: self.flags, + text: self.text, + textEntities: self.textEntities, + items: items, + completions: self.completions + ) + } + + func withUpdated(completions: [TelegramMediaTodo.Completion]) -> TelegramMediaTodo { + return TelegramMediaTodo( + flags: self.flags, + text: self.text, + textEntities: self.textEntities, + items: self.items, + completions: completions + ) + } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Todo.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Todo.swift index c976c8cabd..6d4c3e0a47 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Todo.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Todo.swift @@ -10,26 +10,36 @@ public enum RequestUpdateTodoMessageError { } func _internal_requestUpdateTodoMessageItems(account: Account, messageId: MessageId, completedIds: [Int32], incompletedIds: [Int32]) -> Signal { - return account.postbox.loadedPeerWithId(messageId.peerId) - |> take(1) - |> castError(RequestUpdateTodoMessageError.self) - |> mapToSignal { peer -> Signal in - if let inputPeer = apiInputPeer(peer) { - return account.network.request(Api.functions.messages.toggleTodoCompleted(peer: inputPeer, msgId: messageId.id, completed: completedIds, incompleted: incompletedIds)) - |> mapError { _ -> RequestUpdateTodoMessageError in - return .generic - } - |> mapToSignal { result -> Signal in - return account.postbox.transaction { transaction in - account.stateManager.addUpdates(result) - } - |> castError(RequestUpdateTodoMessageError.self) - } - |> ignoreValues - } else { + return account.postbox.transaction { transaction -> Signal in + guard let peer = transaction.getPeer(messageId.peerId), let inputPeer = apiInputPeer(peer) else { return .complete() } + transaction.updateMessage(messageId, update: { currentMessage in + let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init) + var media: [Media] = [] + if let todo = currentMessage.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo { + let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + var updatedCompletions = todo.completions + for id in completedIds { + updatedCompletions.append(TelegramMediaTodo.Completion(id: id, date: timestamp, completedBy: account.peerId)) + } + updatedCompletions.removeAll(where: { incompletedIds.contains($0.id) }) + media = [todo.withUpdated(completions: updatedCompletions)] + } + return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: media)) + }) + return account.network.request(Api.functions.messages.toggleTodoCompleted(peer: inputPeer, msgId: messageId.id, completed: completedIds, incompleted: incompletedIds)) + |> mapError { _ -> RequestUpdateTodoMessageError in + return .generic + } + |> map { result in + account.stateManager.addUpdates(result) + } + |> ignoreValues } + |> castError(RequestUpdateTodoMessageError.self) + |> switchToLatest + |> ignoreValues } public enum AppendTodoMessageError { @@ -37,30 +47,30 @@ public enum AppendTodoMessageError { } func _internal_appendTodoMessageItems(account: Account, messageId: MessageId, items: [TelegramMediaTodo.Item]) -> Signal { - return account.postbox.loadedPeerWithId(messageId.peerId) - |> take(1) - |> castError(AppendTodoMessageError.self) - |> mapToSignal { peer -> Signal in - guard let inputPeer = apiInputPeer(peer) else { - return .single(nil) + return account.postbox.transaction { transaction -> Signal in + guard let peer = transaction.getPeer(messageId.peerId), let inputPeer = apiInputPeer(peer) else { + return .complete() } + transaction.updateMessage(messageId, update: { currentMessage in + let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init) + var media: [Media] = [] + if let todo = currentMessage.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo { + var updatedItems = todo.items + updatedItems.append(contentsOf: items) + media = [todo.withUpdated(items: updatedItems)] + } + return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: media)) + }) return account.network.request(Api.functions.messages.appendTodoList(peer: inputPeer, msgId: messageId.id, list: items.map { $0.apiItem })) |> mapError { _ -> AppendTodoMessageError in return .generic } - |> mapToSignal { result -> Signal in - return account.postbox.transaction { transaction -> TelegramMediaTodo? in - switch result { - case let .updates(updates, _, _, _, _): - let _ = updates - default: - break - } - account.stateManager.addUpdates(result) - return nil - } - |> castError(AppendTodoMessageError.self) + |> map { result in + account.stateManager.addUpdates(result) } + |> ignoreValues } + |> castError(AppendTodoMessageError.self) + |> switchToLatest |> ignoreValues } diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index b82e8717a7..150061ba27 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -168,6 +168,11 @@ public enum PresentationResourceKey: Int32 { case chatBubbleFileCloudFetchedIncomingIcon case chatBubbleFileCloudFetchedOutgoingIcon + case chatBubbleTodoDotIncomingIcon + case chatBubbleTodoDotOutgoingIcon + case chatBubbleTodoCheckIncomingIcon + case chatBubbleTodoCheckOutgoingIcon + case chatBubbleReplyThumbnailPlayImage case chatBubbleDeliveryFailedIcon diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift index 0d7dac3104..6826b80f08 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift @@ -1402,4 +1402,28 @@ public struct PresentationResourcesChat { return nil }) } + + public static func chatBubbleTodoDotIncomingIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleTodoDotIncomingIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/TodoDot"), color: theme.chat.message.incoming.accentTextColor) + }) + } + + public static func chatBubbleTodoDotOutgoingIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleTodoDotOutgoingIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/TodoDot"), color: theme.chat.message.outgoing.accentTextColor) + }) + } + + public static func chatBubbleTodoCheckIncomingIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleTodoCheckIncomingIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/TodoCheck"), color: theme.chat.message.incoming.accentTextColor) + }) + } + + public static func chatBubbleTodoCheckOutgoingIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleTodoCheckOutgoingIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/TodoCheck"), color: theme.chat.message.outgoing.accentTextColor) + }) + } } diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 0b500598aa..529f802938 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -1286,12 +1286,81 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, } attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: attributes) } - case .unknown: - attributedString = nil case let .todoCompletions(completed, incompleted): - //TODO:release - let _ = completed - let _ = incompleted + var todo: TelegramMediaTodo? + for attribute in message.attributes { + if let attribute = attribute as? ReplyMessageAttribute, let message = message.associatedMessages[attribute.messageId] { + for media in message.media { + if let media = media as? TelegramMediaTodo { + todo = media + } + } + } + } + if let todo { + if message.author?.id == accountPeerId { + let resultString: PresentationStrings.FormattedString + if let completedTaskId = completed.first, let completedTask = todo.items.first(where: { $0.id == completedTaskId }) { + resultString = strings.Notification_TodoCompletedYou(completedTask.text) + } else if let incompletedTaskId = incompleted.first, let incompletedTask = todo.items.first(where: { $0.id == incompletedTaskId }) { + resultString = strings.Notification_TodoIncompletedYou(incompletedTask.text) + } else { + fatalError() + } + attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) + } else { + let peerName = message.author?.compactDisplayTitle ?? "" + + var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)]) + attributes[1] = boldAttributes + + let resultString: PresentationStrings.FormattedString + if let completedTaskId = completed.first, let completedTask = todo.items.first(where: { $0.id == completedTaskId }) { + resultString = strings.Notification_TodoCompleted(peerName, completedTask.text) + } else if let incompletedTaskId = incompleted.first, let incompletedTask = todo.items.first(where: { $0.id == incompletedTaskId }) { + resultString = strings.Notification_TodoIncompleted(peerName, incompletedTask.text) + } else { + fatalError() + } + attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: attributes) + } + } else { + attributedString = NSAttributedString(string: ".") + } + case let .todoAppendTasks(tasks): + var todoTitle = "" + for attribute in message.attributes { + if let attribute = attribute as? ReplyMessageAttribute, let message = message.associatedMessages[attribute.messageId] { + for media in message.media { + if let todo = media as? TelegramMediaTodo { + todoTitle = todo.text + } + } + } + } + if message.author?.id == accountPeerId { + let resultString: PresentationStrings.FormattedString + if tasks.count == 1, let task = tasks.first { + resultString = strings.Notification_TodoAddedTaskYou(task.text, todoTitle) + } else { + resultString = strings.Notification_TodoAddedMultipleTasksYou(strings.Notification_TodoTasks(Int32(tasks.count)), todoTitle) + } + attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes, 1: boldAttributes]) + } else { + let peerName = message.author?.compactDisplayTitle ?? "" + var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)]) + attributes[1] = boldAttributes + attributes[2] = boldAttributes + + let resultString: PresentationStrings.FormattedString + if tasks.count == 1, let task = tasks.first { + resultString = strings.Notification_TodoAddedTask(peerName, task.text, todoTitle) + } else { + resultString = strings.Notification_TodoAddedMultipleTasks(peerName, strings.Notification_TodoTasks(Int32(tasks.count)), todoTitle) + } + attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: attributes) + } + case .unknown: attributedString = nil } break diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 922a08f279..4add7cbd2c 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -481,6 +481,7 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel", "//submodules/TelegramUI/Components/GifVideoLayer", "//submodules/TelegramUI/Components/BatchVideoRendering", + "//submodules/TelegramUI/Components/ComposeTodoScreen", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD index 26f2d17330..0359206708 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD @@ -51,6 +51,7 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageItem", "//submodules/TelegramUI/Components/Chat/ChatMessageItemView", "//submodules/TelegramUI/Components/Chat/ChatMessageSwipeToReplyNode", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 6fbd76a16d..2f87452a9d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -41,6 +41,7 @@ import ChatMessageInteractiveFileNode import ChatMessageFileBubbleContentNode import ChatMessageWebpageBubbleContentNode import ChatMessagePollBubbleContentNode +import ChatMessageTodoBubbleContentNode import ChatMessageItem import ChatMessageItemView import ChatMessageSwipeToReplyNode @@ -269,6 +270,9 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ } else if let _ = media as? TelegramMediaPoll { result.append((message, ChatMessagePollBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false + } else if let _ = media as? TelegramMediaTodo { + result.append((message, ChatMessageTodoBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) + needReactions = false } else if let _ = media as? TelegramMediaGiveaway { result.append((message, ChatMessageGiveawayBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift index 0017cedcf4..472ecf3efc 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift @@ -449,10 +449,12 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { self.buttonNode.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { - if "".isEmpty, let contentNode = strongSelf.supernode as? ChatMessagePollBubbleContentNode, let backdropNode = contentNode.bubbleBackgroundNode?.backdropNode { + if let theme = strongSelf.theme, theme.overallDarkAppearance, let contentNode = strongSelf.supernode as? ChatMessagePollBubbleContentNode, let backdropNode = contentNode.bubbleBackgroundNode?.backdropNode { strongSelf.highlightedBackgroundNode.layer.compositingFilter = "overlayBlendMode" strongSelf.highlightedBackgroundNode.frame = strongSelf.view.convert(strongSelf.highlightedBackgroundNode.frame, to: backdropNode.view) backdropNode.addSubnode(strongSelf.highlightedBackgroundNode) + } else { + strongSelf.insertSubnode(strongSelf.highlightedBackgroundNode, at: 0) } strongSelf.highlightedBackgroundNode.layer.removeAnimation(forKey: "opacity") diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/BUILD new file mode 100644 index 0000000000..07268d6b55 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/BUILD @@ -0,0 +1,35 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageTodoBubbleContentNode", + module_name = "ChatMessageTodoBubbleContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/Postbox", + "//submodules/TextFormat", + "//submodules/UrlEscaping", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/AccountContext", + "//submodules/AvatarNode", + "//submodules/TelegramPresentationData", + "//submodules/ChatMessageBackground", + "//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/Chat/PollBubbleTimerNode", + "//submodules/TelegramUI/Components/Chat/MergedAvatarsNode", + "//submodules/TelegramUI/Components/TextNodeWithEntities", + "//submodules/TelegramUI/Components/Chat/ShimmeringLinkNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift new file mode 100644 index 0000000000..7087dece29 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift @@ -0,0 +1,1244 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramCore +import Postbox +import TextFormat +import UrlEscaping +import SwiftSignalKit +import AccountContext +import AvatarNode +import TelegramPresentationData +import ChatMessageBackground +import ChatMessageDateAndStatusNode +import ChatMessageBubbleContentNode +import ChatMessageItemCommon +import PollBubbleTimerNode +import TextNodeWithEntities +import ShimmeringLinkNode + +private final class ChatMessageTaskOptionRadioNodeParameters: NSObject { + let timestamp: Double + let staticColor: UIColor + let animatedColor: UIColor + let fillColor: UIColor + let foregroundColor: UIColor + let offset: Double? + let isChecked: Bool? + let checkTransition: ChatMessageTaskOptionRadioNodeCheckTransition? + + init(timestamp: Double, staticColor: UIColor, animatedColor: UIColor, fillColor: UIColor, foregroundColor: UIColor, offset: Double?, isChecked: Bool?, checkTransition: ChatMessageTaskOptionRadioNodeCheckTransition?) { + self.timestamp = timestamp + self.staticColor = staticColor + self.animatedColor = animatedColor + self.fillColor = fillColor + self.foregroundColor = foregroundColor + self.offset = offset + self.isChecked = isChecked + self.checkTransition = checkTransition + + super.init() + } +} + +private final class ChatMessageTaskOptionRadioNodeCheckTransition { + let startTime: Double + let duration: Double + let previousValue: Bool + let updatedValue: Bool + + init(startTime: Double, duration: Double, previousValue: Bool, updatedValue: Bool) { + self.startTime = startTime + self.duration = duration + self.previousValue = previousValue + self.updatedValue = updatedValue + } +} + +private final class ChatMessageTaskOptionRadioNode: ASDisplayNode { + private(set) var staticColor: UIColor? + private(set) var animatedColor: UIColor? + private(set) var fillColor: UIColor? + private(set) var foregroundColor: UIColor? + private var isInHierarchyValue: Bool = false + private(set) var isAnimating: Bool = false + private var startTime: Double? + private var checkTransition: ChatMessageTaskOptionRadioNodeCheckTransition? + private(set) var isChecked: Bool? + + private var displayLink: ConstantDisplayLinkAnimator? + + private var shouldBeAnimating: Bool { + return self.isInHierarchyValue && (self.isAnimating || self.checkTransition != nil) + } + + func updateIsChecked(_ value: Bool, animated: Bool) { + if let previousValue = self.isChecked, previousValue != value { + self.checkTransition = ChatMessageTaskOptionRadioNodeCheckTransition(startTime: CACurrentMediaTime(), duration: 0.15, previousValue: previousValue, updatedValue: value) + self.isChecked = value + self.updateAnimating() + self.setNeedsDisplay() + } + } + + override init() { + super.init() + + self.isUserInteractionEnabled = false + self.isOpaque = false + } + + deinit { + self.displayLink?.isPaused = true + } + + override func willEnterHierarchy() { + super.willEnterHierarchy() + + let previous = self.shouldBeAnimating + self.isInHierarchyValue = true + let updated = self.shouldBeAnimating + if previous != updated { + self.updateAnimating() + } + } + + override func didExitHierarchy() { + super.didExitHierarchy() + + let previous = self.shouldBeAnimating + self.isInHierarchyValue = false + let updated = self.shouldBeAnimating + if previous != updated { + self.updateAnimating() + } + } + + func update(staticColor: UIColor, animatedColor: UIColor, fillColor: UIColor, foregroundColor: UIColor, isSelectable: Bool, isAnimating: Bool) { + var updated = false + let shouldHaveBeenAnimating = self.shouldBeAnimating + if !staticColor.isEqual(self.staticColor) { + self.staticColor = staticColor + updated = true + } + if !animatedColor.isEqual(self.animatedColor) { + self.animatedColor = animatedColor + updated = true + } + if !fillColor.isEqual(self.fillColor) { + self.fillColor = fillColor + updated = true + } + if !foregroundColor.isEqual(self.foregroundColor) { + self.foregroundColor = foregroundColor + updated = true + } + if isSelectable != (self.isChecked != nil) { + if isSelectable { + self.isChecked = false + } else { + self.isChecked = nil + self.checkTransition = nil + } + updated = true + } + if isAnimating != self.isAnimating { + self.isAnimating = isAnimating + let updated = self.shouldBeAnimating + if shouldHaveBeenAnimating != updated { + self.updateAnimating() + } + } + if updated { + self.setNeedsDisplay() + } + } + + private func updateAnimating() { + let timestamp = CACurrentMediaTime() + if let checkTransition = self.checkTransition { + if checkTransition.startTime + checkTransition.duration <= timestamp { + self.checkTransition = nil + } + } + + if self.shouldBeAnimating { + if self.isAnimating && self.startTime == nil { + self.startTime = timestamp + } + if self.displayLink == nil { + self.displayLink = ConstantDisplayLinkAnimator(update: { [weak self] in + self?.updateAnimating() + self?.setNeedsDisplay() + }) + self.displayLink?.isPaused = false + self.setNeedsDisplay() + } + } else if let displayLink = self.displayLink { + self.startTime = nil + displayLink.invalidate() + self.displayLink = nil + self.setNeedsDisplay() + } + } + + override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { + if let staticColor = self.staticColor, let animatedColor = self.animatedColor, let fillColor = self.fillColor, let foregroundColor = self.foregroundColor { + let timestamp = CACurrentMediaTime() + var offset: Double? + if let startTime = self.startTime { + offset = CACurrentMediaTime() - startTime + } + return ChatMessageTaskOptionRadioNodeParameters(timestamp: timestamp, staticColor: staticColor, animatedColor: animatedColor, fillColor: fillColor, foregroundColor: foregroundColor, offset: offset, isChecked: self.isChecked, checkTransition: self.checkTransition) + } else { + return nil + } + } + + @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { + if isCancelled() { + return + } + + guard let parameters = parameters as? ChatMessageTaskOptionRadioNodeParameters else { + return + } + + let context = UIGraphicsGetCurrentContext()! + + if let offset = parameters.offset { + let t = max(0.0, offset) + let colorFadeInDuration = 0.2 + let color: UIColor + if t < colorFadeInDuration { + color = parameters.staticColor.mixedWith(parameters.animatedColor, alpha: CGFloat(t / colorFadeInDuration)) + } else { + color = parameters.animatedColor + } + context.setStrokeColor(color.cgColor) + + let rotationDuration = 1.15 + let rotationProgress = CGFloat(offset.truncatingRemainder(dividingBy: rotationDuration) / rotationDuration) + context.translateBy(x: bounds.midX, y: bounds.midY) + context.rotate(by: rotationProgress * 2.0 * CGFloat.pi) + context.translateBy(x: -bounds.midX, y: -bounds.midY) + + let fillDuration = 1.0 + if offset < fillDuration { + let fillT = CGFloat(offset.truncatingRemainder(dividingBy: fillDuration) / fillDuration) + let startAngle = fillT * 2.0 * CGFloat.pi - CGFloat.pi / 2.0 + let endAngle = -CGFloat.pi / 2.0 + + let path = UIBezierPath(arcCenter: CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0), radius: (bounds.size.width - 1.0) / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise: true) + path.lineWidth = 1.0 + path.lineCapStyle = .round + path.stroke() + } else { + let halfProgress: CGFloat = 0.7 + let fillPhase = 0.6 + let keepPhase = 0.0 + let finishPhase = 0.6 + let totalDuration = fillPhase + keepPhase + finishPhase + let localOffset = (offset - fillDuration).truncatingRemainder(dividingBy: totalDuration) + + let angleOffsetT: CGFloat = -CGFloat(floor((offset - fillDuration) / totalDuration)) + let angleOffset = (angleOffsetT * (1.0 - halfProgress) * 2.0 * CGFloat.pi).truncatingRemainder(dividingBy: 2.0 * CGFloat.pi) + context.translateBy(x: bounds.midX, y: bounds.midY) + context.rotate(by: angleOffset) + context.translateBy(x: -bounds.midX, y: -bounds.midY) + + if localOffset < fillPhase + keepPhase { + let fillT = CGFloat(min(1.0, localOffset / fillPhase)) + let startAngle = -CGFloat.pi / 2.0 + let endAngle = (fillT * halfProgress) * 2.0 * CGFloat.pi - CGFloat.pi / 2.0 + + let path = UIBezierPath(arcCenter: CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0), radius: (bounds.size.width - 1.0) / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise: true) + path.lineWidth = 1.0 + path.lineCapStyle = .round + path.stroke() + } else { + let finishT = CGFloat((localOffset - (fillPhase + keepPhase)) / finishPhase) + let endAngle = halfProgress * 2.0 * CGFloat.pi - CGFloat.pi / 2.0 + let startAngle = -CGFloat.pi / 2.0 * (1.0 - finishT) + endAngle * finishT + + let path = UIBezierPath(arcCenter: CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0), radius: (bounds.size.width - 1.0) / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise: true) + path.lineWidth = 1.0 + path.lineCapStyle = .round + path.stroke() + } + } + } else { + if let isChecked = parameters.isChecked { + let checkedT: CGFloat + let fromValue: CGFloat + let toValue: CGFloat + let fromAlpha: CGFloat + let toAlpha: CGFloat + if let checkTransition = parameters.checkTransition { + checkedT = CGFloat(max(0.0, min(1.0, (parameters.timestamp - checkTransition.startTime) / checkTransition.duration))) + fromValue = checkTransition.previousValue ? bounds.width : 0.0 + fromAlpha = checkTransition.previousValue ? 1.0 : 0.0 + toValue = checkTransition.updatedValue ? bounds.width : 0.0 + toAlpha = checkTransition.updatedValue ? 1.0 : 0.0 + } else { + checkedT = 1.0 + fromValue = isChecked ? bounds.width : 0.0 + fromAlpha = isChecked ? 1.0 : 0.0 + toValue = isChecked ? bounds.width : 0.0 + toAlpha = isChecked ? 1.0 : 0.0 + } + + let diameter = fromValue * (1.0 - checkedT) + toValue * checkedT + let alpha = fromAlpha * (1.0 - checkedT) + toAlpha * checkedT + + if abs(diameter - 1.0) > CGFloat.ulpOfOne { + context.setStrokeColor(parameters.staticColor.cgColor) + context.strokeEllipse(in: CGRect(origin: CGPoint(x: 0.5, y: 0.5), size: CGSize(width: bounds.width - 1.0, height: bounds.height - 1.0))) + } + + if !diameter.isZero { + context.setFillColor(parameters.fillColor.withAlphaComponent(alpha).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: (bounds.width - diameter) / 2.0, y: (bounds.width - diameter) / 2.0), size: CGSize(width: diameter, height: diameter))) + + context.setLineWidth(1.5) + context.setLineJoin(.round) + context.setLineCap(.round) + + context.setStrokeColor(parameters.foregroundColor.withAlphaComponent(alpha).cgColor) + if parameters.foregroundColor.alpha.isZero { + context.setBlendMode(.clear) + } + let startPoint = CGPoint(x: 6.0, y: 12.13) + let centerPoint = CGPoint(x: 9.28, y: 15.37) + let endPoint = CGPoint(x: 16.0, y: 8.0) + + let pathStartT: CGFloat = 0.15 + let pathT = max(0.0, (alpha - pathStartT) / (1.0 - pathStartT)) + let pathMiddleT: CGFloat = 0.4 + + context.move(to: startPoint) + if pathT >= pathMiddleT { + context.addLine(to: centerPoint) + + let pathEndT = (pathT - pathMiddleT) / (1.0 - pathMiddleT) + if pathEndT >= 1.0 { + context.addLine(to: endPoint) + } else { + context.addLine(to: CGPoint(x: (1.0 - pathEndT) * centerPoint.x + pathEndT * endPoint.x, y: (1.0 - pathEndT) * centerPoint.y + pathEndT * endPoint.y)) + } + } else { + context.addLine(to: CGPoint(x: (1.0 - pathT) * startPoint.x + pathT * centerPoint.x, y: (1.0 - pathT) * startPoint.y + pathT * centerPoint.y)) + } + context.strokePath() + context.setBlendMode(.normal) + } + } else { + context.setStrokeColor(parameters.staticColor.cgColor) + context.strokeEllipse(in: CGRect(origin: CGPoint(x: 0.5, y: 0.5), size: CGSize(width: bounds.width - 1.0, height: bounds.height - 1.0))) + } + } + } +} + +private let percentageFont = Font.bold(14.5) +private let percentageSmallFont = Font.bold(12.5) + +private func generatePercentageImage(presentationData: ChatPresentationData, incoming: Bool, value: Int, targetValue: Int) -> UIImage { + return generateImage(CGSize(width: 42.0, height: 20.0), rotatedContext: { size, context in + UIGraphicsPushContext(context) + context.clear(CGRect(origin: CGPoint(), size: size)) + let font: UIFont + if targetValue == 100 { + font = percentageSmallFont + } else { + font = percentageFont + } + let string = NSAttributedString(string: "\(value)%", font: font, textColor: incoming ? presentationData.theme.theme.chat.message.incoming.primaryTextColor : presentationData.theme.theme.chat.message.outgoing.primaryTextColor, paragraphAlignment: .right) + string.draw(in: CGRect(origin: CGPoint(x: 0.0, y: targetValue == 100 ? 3.0 : 2.0), size: size)) + UIGraphicsPopContext() + })! +} + +private func generatePercentageAnimationImages(presentationData: ChatPresentationData, incoming: Bool, from fromValue: Int, to toValue: Int, duration: Double) -> [UIImage] { + let minimumFrameDuration = 1.0 / 40.0 + let numberOfFrames = max(1, Int(duration / minimumFrameDuration)) + var images: [UIImage] = [] + for i in 0 ..< numberOfFrames { + let t = CGFloat(i) / CGFloat(numberOfFrames) + images.append(generatePercentageImage(presentationData: presentationData, incoming: incoming, value: Int((1.0 - t) * CGFloat(fromValue) + t * CGFloat(toValue)), targetValue: toValue)) + } + return images +} + +private final class ChatMessageTodoItemNode: ASDisplayNode { + private let highlightedBackgroundNode: ASDisplayNode + private var avatarNode: AvatarNode? + private(set) var radioNode: ChatMessageTaskOptionRadioNode? + private var iconNode: ASImageNode? + fileprivate var titleNode: TextNodeWithEntities? + private let buttonNode: HighlightTrackingButtonNode + let separatorNode: ASDisplayNode + var option: TelegramMediaTodo.Item? + var pressed: (() -> Void)? + var selectionUpdated: (() -> Void)? + private var theme: PresentationTheme? + + weak var previousOptionNode: ChatMessageTodoItemNode? + + private var canMark = false + private var isPremium = false + + var visibilityRect: CGRect? { + didSet { + if self.visibilityRect != oldValue { + if let titleNode = self.titleNode { + if let visibilityRect = self.visibilityRect { + titleNode.visibilityRect = visibilityRect.offsetBy(dx: 0.0, dy: titleNode.textNode.frame.minY) + } else { + titleNode.visibilityRect = nil + } + } + } + } + } + + override init() { + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.alpha = 0.0 + self.highlightedBackgroundNode.isUserInteractionEnabled = false + + self.buttonNode = HighlightTrackingButtonNode() + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + + super.init() + + self.addSubnode(self.highlightedBackgroundNode) + self.addSubnode(self.separatorNode) + self.addSubnode(self.buttonNode) + + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + self.buttonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + if let theme = strongSelf.theme, theme.overallDarkAppearance, let contentNode = strongSelf.supernode as? ChatMessageTodoBubbleContentNode, let backdropNode = contentNode.bubbleBackgroundNode?.backdropNode { + strongSelf.highlightedBackgroundNode.layer.compositingFilter = "overlayBlendMode" + strongSelf.highlightedBackgroundNode.frame = strongSelf.view.convert(strongSelf.highlightedBackgroundNode.frame, to: backdropNode.view) + backdropNode.addSubnode(strongSelf.highlightedBackgroundNode) + } else { + strongSelf.insertSubnode(strongSelf.highlightedBackgroundNode, at: 0) + } + + strongSelf.highlightedBackgroundNode.layer.removeAnimation(forKey: "opacity") + strongSelf.highlightedBackgroundNode.alpha = 1.0 + + strongSelf.separatorNode.layer.removeAnimation(forKey: "opacity") + strongSelf.separatorNode.alpha = 0.0 + + strongSelf.previousOptionNode?.separatorNode.layer.removeAnimation(forKey: "opacity") + strongSelf.previousOptionNode?.separatorNode.alpha = 0.0 + } else { + strongSelf.highlightedBackgroundNode.alpha = 0.0 + strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, completion: { finished in + if finished && strongSelf.highlightedBackgroundNode.supernode != strongSelf { + strongSelf.highlightedBackgroundNode.layer.compositingFilter = nil + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: strongSelf.highlightedBackgroundNode.frame.size) + strongSelf.insertSubnode(strongSelf.highlightedBackgroundNode, at: 0) + } + }) + + strongSelf.separatorNode.alpha = 1.0 + strongSelf.separatorNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + + strongSelf.previousOptionNode?.separatorNode.alpha = 1.0 + strongSelf.previousOptionNode?.separatorNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + } + } + } + + @objc private func buttonPressed() { + if let radioNode = self.radioNode, let isChecked = radioNode.isChecked, self.canMark, self.isPremium { + radioNode.updateIsChecked(!isChecked, animated: true) + self.selectionUpdated?() + } else { + self.pressed?() + } + } + + static func asyncLayout(_ maybeNode: ChatMessageTodoItemNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ todo: TelegramMediaTodo, _ option: TelegramMediaTodo.Item, _ completion: TelegramMediaTodo.Completion?, _ translation: TranslationMessageAttribute.Additional?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessageTodoItemNode))) { + let makeTitleLayout = TextNodeWithEntities.asyncLayout(maybeNode?.titleNode) + + return { context, presentationData, message, todo, option, completion, translation, constrainedWidth in + var canMark = false + if (todo.flags.contains(.othersCanComplete) || message.author?.id == context.account.peerId) { + canMark = true + } + + let leftInset: CGFloat = canMark ? 57.0 : 29.0 + let rightInset: CGFloat = 12.0 + + let incoming = message.effectivelyIncoming(context.account.peerId) + + var optionText = option.text + var optionEntities = option.entities + if let translation { + optionText = translation.text + optionEntities = translation.entities + } + + if !canMark && completion != nil { + optionEntities.append(MessageTextEntity(range: 0 ..< (optionText as NSString).length, type: .Strikethrough)) + } + + let optionTextColor: UIColor = incoming ? presentationData.theme.theme.chat.message.incoming.primaryTextColor : presentationData.theme.theme.chat.message.outgoing.primaryTextColor + let optionAttributedText = stringWithAppliedEntities( + optionText, + entities: optionEntities, + baseColor: optionTextColor, + linkColor: optionTextColor, + baseFont: presentationData.messageFont, + linkFont: presentationData.messageFont, + boldFont: presentationData.messageFont, + italicFont: presentationData.messageFont, + boldItalicFont: presentationData.messageFont, + fixedFont: presentationData.messageFont, + blockQuoteFont: presentationData.messageFont, + message: message + ) + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: optionAttributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: max(1.0, constrainedWidth - leftInset - rightInset), height: CGFloat.greatestFiniteMagnitude), alignment: .left, cutout: nil, insets: UIEdgeInsets(top: 1.0, left: 0.0, bottom: 1.0, right: 0.0))) + + let contentHeight: CGFloat = max(46.0, titleLayout.size.height + 22.0) + + let isSelectable: Bool = true + + return (titleLayout.size.width + leftInset + rightInset, { width in + return (CGSize(width: width, height: contentHeight), { animated, inProgress, attemptSynchronous in + let node: ChatMessageTodoItemNode + if let maybeNode = maybeNode { + node = maybeNode + } else { + node = ChatMessageTodoItemNode() + } + + node.canMark = canMark + node.isPremium = context.isPremium + node.option = option + node.theme = presentationData.theme.theme + + node.highlightedBackgroundNode.backgroundColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.highlight : presentationData.theme.theme.chat.message.outgoing.polls.highlight + + node.buttonNode.accessibilityLabel = option.text + + if animated { + if let titleNode = node.titleNode, let cachedLayout = titleNode.textNode.cachedLayout { + if !cachedLayout.areLinesEqual(to: titleLayout) { + if let textContents = titleNode.textNode.contents { + let fadeNode = ASDisplayNode() + fadeNode.displaysAsynchronously = false + fadeNode.contents = textContents + fadeNode.frame = titleNode.textNode.frame + fadeNode.isLayerBacked = true + node.addSubnode(fadeNode) + fadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak fadeNode] _ in + fadeNode?.removeFromSupernode() + }) + titleNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + } + } + } + + let titleNode = titleApply(TextNodeWithEntities.Arguments( + context: context, + cache: context.animationCache, + renderer: context.animationRenderer, + placeholderColor: incoming ? presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor, + attemptSynchronous: attemptSynchronous + )) + let titleNodeFrame: CGRect + if titleLayout.hasRTL { + titleNodeFrame = CGRect(origin: CGPoint(x: width - rightInset - titleLayout.size.width, y: 12.0), size: titleLayout.size) + } else { + titleNodeFrame = CGRect(origin: CGPoint(x: leftInset, y: 12.0), size: titleLayout.size) + } + if node.titleNode !== titleNode { + node.titleNode = titleNode + node.addSubnode(titleNode.textNode) + titleNode.textNode.isUserInteractionEnabled = false + + if let visibilityRect = node.visibilityRect { + titleNode.visibilityRect = visibilityRect.offsetBy(dx: 0.0, dy: titleNodeFrame.minY) + } + } + titleNode.textNode.frame = titleNodeFrame + + if let completion, canMark && todo.flags.contains(.othersCanComplete) { + let avatarNode: AvatarNode + if let current = node.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 12.0)) + node.insertSubnode(avatarNode, at: 0) + node.avatarNode = avatarNode + if animated { + avatarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + avatarNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2, removeOnCompletion: false) + } + } + let avatarSize = CGSize(width: 22.0, height: 22.0) + avatarNode.frame = CGRect(origin: CGPoint(x: 24.0, y: 12.0), size: avatarSize) + if let peer = message.peers[completion.completedBy] { + avatarNode.setPeer(context: context, theme: presentationData.theme.theme, peer: EnginePeer(peer), displayDimensions: avatarSize, cutoutRect: CGRect(origin: CGPoint(x: -12.0, y: -1.0), size: CGSize(width: 24.0, height: 24.0))) + //avatarNode.setPeerV2(context: context, theme: presentationData.theme.theme, peer: EnginePeer(peer), displayDimensions: avatarSize) + } + } else if let avatarNode = node.avatarNode { + node.avatarNode = nil + if animated { + avatarNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak avatarNode] _ in + avatarNode?.removeFromSupernode() + }) + avatarNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + } else { + avatarNode.removeFromSupernode() + } + } + + if canMark { + let radioNode: ChatMessageTaskOptionRadioNode + if let current = node.radioNode { + radioNode = current + } else { + radioNode = ChatMessageTaskOptionRadioNode() + node.addSubnode(radioNode) + node.radioNode = radioNode + if animated { + radioNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + } + let radioSize: CGFloat = 22.0 + radioNode.frame = CGRect(origin: CGPoint(x: 12.0, y: 12.0), size: CGSize(width: radioSize, height: radioSize)) + radioNode.update(staticColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.radioButton : presentationData.theme.theme.chat.message.outgoing.polls.radioButton, animatedColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.radioProgress : presentationData.theme.theme.chat.message.outgoing.polls.radioProgress, fillColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.bar : presentationData.theme.theme.chat.message.outgoing.polls.bar, foregroundColor: incoming ? presentationData.theme.theme.chat.message.incoming.polls.barIconForeground : presentationData.theme.theme.chat.message.outgoing.polls.barIconForeground, isSelectable: isSelectable, isAnimating: inProgress) + + radioNode.updateIsChecked(completion != nil, animated: false) + } else if let radioNode = node.radioNode { + node.radioNode = nil + if animated { + radioNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak radioNode] _ in + radioNode?.removeFromSupernode() + }) + } else { + radioNode.removeFromSupernode() + } + } + + if !canMark { + let iconNode: ASImageNode + if let current = node.iconNode { + iconNode = current + } else { + iconNode = ASImageNode() + iconNode.displaysAsynchronously = false + node.addSubnode(iconNode) + node.iconNode = iconNode + if animated { + iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + } + + let icon: UIImage? + if incoming { + icon = completion != nil ? PresentationResourcesChat.chatBubbleTodoCheckIncomingIcon(presentationData.theme.theme) : PresentationResourcesChat.chatBubbleTodoDotIncomingIcon(presentationData.theme.theme) + } else { + icon = completion != nil ? PresentationResourcesChat.chatBubbleTodoCheckOutgoingIcon(presentationData.theme.theme) : PresentationResourcesChat.chatBubbleTodoDotOutgoingIcon(presentationData.theme.theme) + } + iconNode.image = icon + + let iconSize: CGFloat = 10.0 + iconNode.frame = CGRect(origin: CGPoint(x: 10.0, y: 19.0), size: CGSize(width: iconSize, height: iconSize)) + } else if let iconNode = node.iconNode { + node.iconNode = nil + if animated { + iconNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak iconNode] _ in + iconNode?.removeFromSupernode() + }) + } else { + iconNode.removeFromSupernode() + } + } + + node.buttonNode.frame = CGRect(origin: CGPoint(x: 1.0, y: 0.0), size: CGSize(width: width - 2.0, height: contentHeight)) + if node.highlightedBackgroundNode.supernode == node { + node.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: contentHeight + UIScreenPixel)) + } + node.separatorNode.backgroundColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.separator : presentationData.theme.theme.chat.message.outgoing.polls.separator + node.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentHeight - UIScreenPixel), size: CGSize(width: width - leftInset, height: UIScreenPixel)) + + node.buttonNode.isAccessibilityElement = true + + return node + }) + }) + } + } +} + +private let labelsFont = Font.regular(14.0) + + +public class ChatMessageTodoBubbleContentNode: ChatMessageBubbleContentNode { + private let textNode: TextNodeWithEntities + private let typeNode: TextNode + private var timerNode: PollBubbleTimerNode? + private let buttonViewResultsTextNode: TextNode + private let buttonNode: HighlightableButtonNode + private let statusNode: ChatMessageDateAndStatusNode + private var optionNodes: [ChatMessageTodoItemNode] = [] + private var shimmeringNodes: [ShimmeringLinkNode] = [] + + private var todo: TelegramMediaTodo? + + override public var visibility: ListViewItemNodeVisibility { + didSet { + if oldValue != self.visibility { + switch self.visibility { + case .none: + self.textNode.visibilityRect = nil + for optionNode in self.optionNodes { + optionNode.visibilityRect = nil + } + case let .visible(_, subRect): + var subRect = subRect + subRect.origin.x = 0.0 + subRect.size.width = 10000.0 + self.textNode.visibilityRect = subRect.offsetBy(dx: 0.0, dy: -self.textNode.textNode.frame.minY) + for optionNode in self.optionNodes { + optionNode.visibilityRect = subRect.offsetBy(dx: 0.0, dy: -optionNode.frame.minY) + } + } + } + } + } + + required public init() { + self.textNode = TextNodeWithEntities() + self.textNode.textNode.isUserInteractionEnabled = false + self.textNode.textNode.contentMode = .topLeft + self.textNode.textNode.contentsScale = UIScreenScale + self.textNode.textNode.displaysAsynchronously = false + + self.typeNode = TextNode() + self.typeNode.isUserInteractionEnabled = false + self.typeNode.contentMode = .topLeft + self.typeNode.contentsScale = UIScreenScale + self.typeNode.displaysAsynchronously = false + + self.buttonViewResultsTextNode = TextNode() + self.buttonViewResultsTextNode.isUserInteractionEnabled = false + self.buttonViewResultsTextNode.contentMode = .topLeft + self.buttonViewResultsTextNode.contentsScale = UIScreenScale + self.buttonViewResultsTextNode.displaysAsynchronously = false + + self.buttonNode = HighlightableButtonNode() + + self.statusNode = ChatMessageDateAndStatusNode() + + super.init() + + self.addSubnode(self.textNode.textNode) + self.addSubnode(self.typeNode) + self.addSubnode(self.buttonViewResultsTextNode) + self.addSubnode(self.buttonNode) + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + let makeTextLayout = TextNodeWithEntities.asyncLayout(self.textNode) + let makeTypeLayout = TextNode.asyncLayout(self.typeNode) + let makeViewResultsTextLayout = TextNode.asyncLayout(self.buttonViewResultsTextNode) + let statusLayout = self.statusNode.asyncLayout() + + var previousOptionNodeLayouts: [Int32: (_ contet: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ poll: TelegramMediaTodo, _ option: TelegramMediaTodo.Item, _ completion: TelegramMediaTodo.Completion?, _ translation: TranslationMessageAttribute.Additional?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessageTodoItemNode)))] = [:] + for optionNode in self.optionNodes { + if let option = optionNode.option { + previousOptionNodeLayouts[option.id] = ChatMessageTodoItemNode.asyncLayout(optionNode) + } + } + + return { item, layoutConstants, _, _, _, _ in + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) + + return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in + let message = item.message + + let incoming = item.message.effectivelyIncoming(item.context.account.peerId) + + let additionalTextRightInset: CGFloat = 24.0 + + let horizontalInset = layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right + let textConstrainedSize = CGSize(width: constrainedSize.width - horizontalInset - additionalTextRightInset, height: constrainedSize.height) + + var edited = false + if item.attributes.updatingMedia != nil { + edited = true + } + var viewCount: Int? + var dateReplies = 0 + var starsCount: Int64? + var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message) + if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) { + dateReactionsAndPeers = ([], []) + } + for attribute in item.message.attributes { + if let attribute = attribute as? EditedMessageAttribute { + edited = !attribute.isHidden + } else if let attribute = attribute as? ViewCountMessageAttribute { + viewCount = attribute.count + } else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = item.chatLocation { + if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { + dateReplies = Int(attribute.count) + } + } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { + starsCount = attribute.stars.value + } + } + + let dateFormat: MessageTimestampStatusFormat + if item.presentationData.isPreview { + dateFormat = .full + } else { + dateFormat = .regular + } + let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: dateFormat, associatedData: item.associatedData) + + let statusType: ChatMessageDateAndStatusType? + if case .customChatContents = item.associatedData.subject { + statusType = nil + } else { + switch position { + case .linear(_, .None), .linear(_, .Neighbour(true, _, _)): + if incoming { + statusType = .BubbleIncoming + } else { + if message.flags.contains(.Failed) { + statusType = .BubbleOutgoing(.Failed) + } else if (message.flags.isSending && !message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil { + statusType = .BubbleOutgoing(.Sending) + } else { + statusType = .BubbleOutgoing(.Sent(read: item.read)) + } + } + default: + statusType = nil + } + } + + var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))? + + if let statusType = statusType { + var isReplyThread = false + if case .replyThread = item.chatLocation { + isReplyThread = true + } + + statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments( + context: item.context, + presentationData: item.presentationData, + edited: edited, + impressionCount: viewCount, + dateText: dateText, + type: statusType, + layoutInput: .trailingContent(contentWidth: 1000.0, reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions) ? ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: true, preferAdditionalInset: false) : nil), + constrainedSize: textConstrainedSize, + availableReactions: item.associatedData.availableReactions, + savedMessageTags: item.associatedData.savedMessageTags, + reactions: dateReactionsAndPeers.reactions, + reactionPeers: dateReactionsAndPeers.peers, + displayAllReactionPeers: item.message.id.peerId.namespace == Namespaces.Peer.CloudUser, + areReactionsTags: item.topMessage.areReactionsTags(accountPeerId: item.context.account.peerId), + messageEffect: item.topMessage.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects), + replyCount: dateReplies, + starsCount: starsCount, + isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, + hasAutoremove: item.message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: item.topMessage), + animationCache: item.controllerInteraction.presentationContext.animationCache, + animationRenderer: item.controllerInteraction.presentationContext.animationRenderer + )) + } + + var todo: TelegramMediaTodo? + for media in item.message.media { + if let media = media as? TelegramMediaTodo { + todo = media + break + } + } + + let messageTheme = incoming ? item.presentationData.theme.theme.chat.message.incoming : item.presentationData.theme.theme.chat.message.outgoing + + + var pollTitleText = todo?.text ?? "" + var pollTitleEntities = todo?.textEntities ?? [] + var pollOptions: [TranslationMessageAttribute.Additional] = [] + + var isTranslating = false + if let todo, let translateToLanguage = item.associatedData.translateToLanguage, !todo.text.isEmpty && incoming { + isTranslating = true + for attribute in item.message.attributes { + if let attribute = attribute as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == translateToLanguage { + pollTitleText = attribute.text + pollTitleEntities = attribute.entities + pollOptions = attribute.additional + isTranslating = false + break + } + } + } + + let attributedText = stringWithAppliedEntities( + pollTitleText, + entities: pollTitleEntities, + baseColor: messageTheme.primaryTextColor, + linkColor: messageTheme.linkTextColor, + baseFont: item.presentationData.messageBoldFont, + linkFont: item.presentationData.messageBoldFont, + boldFont: item.presentationData.messageBoldFont, + italicFont: item.presentationData.messageBoldFont, + boldItalicFont: item.presentationData.messageBoldFont, + fixedFont: item.presentationData.messageBoldFont, + blockQuoteFont: item.presentationData.messageBoldFont, + message: message + ) + + let textInsets = UIEdgeInsets(top: 2.0, left: 0.0, bottom: 5.0, right: 0.0) + + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets)) + let typeText: String + + //TODO:localize + if let todo, todo.flags.contains(.othersCanComplete) { + typeText = "Group To Do List" + } else { + if let author = item.message.author, author.id != item.context.account.peerId { + typeText = "\(EnginePeer(author).compactDisplayTitle)'s To Do List" + } else { + typeText = "To Do List" + } + } + + let (typeLayout, typeApply) = makeTypeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: typeText, font: labelsFont, textColor: messageTheme.secondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + + var bottomText: String = "" + if let todo { + if let author = item.message.author, author.id != item.context.account.peerId && !todo.flags.contains(.othersCanComplete) { + bottomText = "\(todo.completions.count) of \(todo.items.count) completed by \(EnginePeer(author).compactDisplayTitle)" + } else { + bottomText = "\(todo.completions.count) of \(todo.items.count) completed" + } + } + + let (buttonViewResultsTextLayout, buttonViewResultsTextApply) = makeViewResultsTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: bottomText, font: labelsFont, textColor: messageTheme.secondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets)) + + var textFrame = CGRect(origin: CGPoint(x: -textInsets.left, y: -textInsets.top), size: textLayout.size) + var textFrameWithoutInsets = CGRect(origin: CGPoint(x: textFrame.origin.x + textInsets.left, y: textFrame.origin.y + textInsets.top), size: CGSize(width: textFrame.width - textInsets.left - textInsets.right, height: textFrame.height - textInsets.top - textInsets.bottom)) + + textFrame = textFrame.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top) + textFrameWithoutInsets = textFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top) + + var boundingSize: CGSize = textFrameWithoutInsets.size + boundingSize.width += additionalTextRightInset + boundingSize.width = max(boundingSize.width, typeLayout.size.width) + boundingSize.width = max(boundingSize.width, buttonViewResultsTextLayout.size.width + 4.0/* + (statusSize?.width ?? 0.0)*/) + + if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue { + boundingSize.width = max(boundingSize.width, statusSuggestedWidthAndContinue.0) + } + + boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right + boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom + + var pollOptionsFinalizeLayouts: [(CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessageTodoItemNode)] = [] + if let todo { + for i in 0 ..< todo.items.count { + let todoItem = todo.items[i] + + let makeLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ todo: TelegramMediaTodo, _ item: TelegramMediaTodo.Item, _ completion: TelegramMediaTodo.Completion?, _ translation: TranslationMessageAttribute.Additional?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessageTodoItemNode))) + if let previous = previousOptionNodeLayouts[todoItem.id] { + makeLayout = previous + } else { + makeLayout = ChatMessageTodoItemNode.asyncLayout(nil) + } + + var translation: TranslationMessageAttribute.Additional? + if !pollOptions.isEmpty && i < pollOptions.count { + translation = pollOptions[i] + } + + let itemCompletion = todo.completions.first(where: { $0.id == todoItem.id }) + + let result = makeLayout(item.context, item.presentationData, item.message, todo, todoItem, itemCompletion, translation, constrainedSize.width - layoutConstants.bubble.borderInset * 2.0) + boundingSize.width = max(boundingSize.width, result.minimumWidth + layoutConstants.bubble.borderInset * 2.0) + pollOptionsFinalizeLayouts.append(result.1) + } + } + + boundingSize.width = max(boundingSize.width, min(270.0, constrainedSize.width)) + + return (boundingSize.width, { boundingWidth in + var resultSize = CGSize(width: max(boundingSize.width, boundingWidth), height: boundingSize.height) + + let titleTypeSpacing: CGFloat = -4.0 + let typeOptionsSpacing: CGFloat = 3.0 + resultSize.height += titleTypeSpacing + typeLayout.size.height + typeOptionsSpacing + + var optionNodesSizesAndApply: [(CGSize, (Bool, Bool, Bool) -> ChatMessageTodoItemNode)] = [] + for finalizeLayout in pollOptionsFinalizeLayouts { + let result = finalizeLayout(boundingWidth - layoutConstants.bubble.borderInset * 2.0) + resultSize.width = max(resultSize.width, result.0.width + layoutConstants.bubble.borderInset * 2.0) + resultSize.height += result.0.height + optionNodesSizesAndApply.append(result) + } + + let statusSpacing: CGFloat = 33.0 + let optionsButtonSpacing: CGFloat = 12.0 + resultSize.height += 44.0 + + var statusSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> Void)? + if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue { + statusSizeAndApply = statusSuggestedWidthAndContinue.1(boundingWidth) + } + + if let statusSizeAndApply = statusSizeAndApply { + resultSize.height += statusSizeAndApply.0.height - 6.0 + } + + let buttonViewResultsTextFrame = CGRect(origin: CGPoint(x: floor((resultSize.width - buttonViewResultsTextLayout.size.width) / 2.0), y: optionsButtonSpacing), size: buttonViewResultsTextLayout.size) + + return (resultSize, { [weak self] animation, synchronousLoad, _ in + if let strongSelf = self { + strongSelf.item = item + strongSelf.todo = todo + + let cachedLayout = strongSelf.textNode.textNode.cachedLayout + if case .System = animation { + if let cachedLayout = cachedLayout { + if !cachedLayout.areLinesEqual(to: textLayout) { + if let textContents = strongSelf.textNode.textNode.contents { + let fadeNode = ASDisplayNode() + fadeNode.displaysAsynchronously = false + fadeNode.contents = textContents + fadeNode.frame = strongSelf.textNode.textNode.frame + fadeNode.isLayerBacked = true + strongSelf.addSubnode(fadeNode) + fadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak fadeNode] _ in + fadeNode?.removeFromSupernode() + }) + strongSelf.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + } + } + } + + let _ = textApply(TextNodeWithEntities.Arguments( + context: item.context, + cache: item.context.animationCache, + renderer: item.context.animationRenderer, + placeholderColor: incoming ? item.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : item.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor, + attemptSynchronous: synchronousLoad) + ) + let _ = typeApply() + + var verticalOffset = textFrame.maxY + titleTypeSpacing + typeLayout.size.height + typeOptionsSpacing + var updatedOptionNodes: [ChatMessageTodoItemNode] = [] + for i in 0 ..< optionNodesSizesAndApply.count { + let (size, apply) = optionNodesSizesAndApply[i] + var isRequesting = false + if let todo, i < todo.items.count { + isRequesting = false +// if let inProgressOpaqueIds = item.controllerInteraction.pollActionState.pollMessageIdsInProgress[item.message.id] { +// isRequesting = inProgressOpaqueIds.contains(poll.options[i].opaqueIdentifier) +// } + } + let optionNode = apply(animation.isAnimated, isRequesting, synchronousLoad) + let optionNodeFrame = CGRect(origin: CGPoint(x: layoutConstants.bubble.borderInset, y: verticalOffset), size: size) + if optionNode.supernode !== strongSelf { + strongSelf.addSubnode(optionNode) + let todoItem = optionNode.option + optionNode.selectionUpdated = { + guard let strongSelf = self, let item = strongSelf.item, let todoItem else { + return + } + item.controllerInteraction.requestToggleTodoMessageItem(item.message.id, todoItem.id, optionNode.radioNode?.isChecked == true) + } + optionNode.pressed = { + guard let strongSelf = self, let item = strongSelf.item else { + return + } + item.controllerInteraction.displayTodoToggleUnavailable(item.message.id) + } + optionNode.frame = optionNodeFrame + } else { + animation.animator.updateFrame(layer: optionNode.layer, frame: optionNodeFrame, completion: nil) + } + + verticalOffset += size.height + updatedOptionNodes.append(optionNode) + + if i > 0 { + optionNode.previousOptionNode = updatedOptionNodes[i - 1] + } + } + for optionNode in strongSelf.optionNodes { + if !updatedOptionNodes.contains(where: { $0 === optionNode }) { + optionNode.removeFromSupernode() + } + } + strongSelf.optionNodes = updatedOptionNodes + + if textLayout.hasRTL { + strongSelf.textNode.textNode.frame = CGRect(origin: CGPoint(x: resultSize.width - textFrame.size.width - textInsets.left - layoutConstants.text.bubbleInsets.right - additionalTextRightInset, y: textFrame.origin.y), size: textFrame.size) + } else { + strongSelf.textNode.textNode.frame = textFrame + } + let typeFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: textFrame.maxY + titleTypeSpacing), size: typeLayout.size) + animation.animator.updateFrame(layer: strongSelf.typeNode.layer, frame: typeFrame, completion: nil) + + if let statusSizeAndApply = statusSizeAndApply { + let statusFrame = CGRect(origin: CGPoint(x: resultSize.width - statusSizeAndApply.0.width - layoutConstants.text.bubbleInsets.right, y: verticalOffset + statusSpacing), size: statusSizeAndApply.0) + + if strongSelf.statusNode.supernode == nil { + statusSizeAndApply.1(.None) + strongSelf.statusNode.frame = statusFrame + strongSelf.addSubnode(strongSelf.statusNode) + } else { + statusSizeAndApply.1(animation) + animation.animator.updateFrame(layer: strongSelf.statusNode.layer, frame: statusFrame, completion: nil) + } + } else if strongSelf.statusNode.supernode != nil { + strongSelf.statusNode.removeFromSupernode() + } + + let _ = buttonViewResultsTextApply() + strongSelf.buttonViewResultsTextNode.frame = buttonViewResultsTextFrame.offsetBy(dx: 0.0, dy: verticalOffset) + + strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: resultSize.width, height: 44.0)) + + strongSelf.updateIsTranslating(isTranslating) + } + }) + }) + }) + } + } + + private func updateIsTranslating(_ isTranslating: Bool) { + guard let item = self.item else { + return + } + var rects: [[CGRect]] = [] + let titleRects = (self.textNode.textNode.rangeRects(in: NSRange(location: 0, length: self.textNode.textNode.cachedLayout?.attributedString?.length ?? 0))?.rects ?? []).map { self.textNode.textNode.view.convert($0, to: self.view) } + rects.append(titleRects) + + for optionNode in self.optionNodes { + if let titleNode = optionNode.titleNode { + let optionRects = (titleNode.textNode.rangeRects(in: NSRange(location: 0, length: titleNode.textNode.cachedLayout?.attributedString?.length ?? 0))?.rects ?? []).map { titleNode.textNode.view.convert($0, to: self.view) } + rects.append(optionRects) + } + } + + if isTranslating, !rects.isEmpty { + if self.shimmeringNodes.isEmpty { + for rects in rects { + let shimmeringNode = ShimmeringLinkNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.secondaryTextColor.withAlphaComponent(0.1) : item.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor.withAlphaComponent(0.1)) + shimmeringNode.updateRects(rects) + shimmeringNode.frame = self.bounds + shimmeringNode.updateLayout(self.bounds.size) + shimmeringNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.shimmeringNodes.append(shimmeringNode) + self.insertSubnode(shimmeringNode, belowSubnode: self.textNode.textNode) + } + } + } else if !self.shimmeringNodes.isEmpty { + let shimmeringNodes = self.shimmeringNodes + self.shimmeringNodes = [] + + for shimmeringNode in shimmeringNodes { + shimmeringNode.alpha = 0.0 + shimmeringNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak shimmeringNode] _ in + shimmeringNode?.removeFromSupernode() + }) + } + } + } + + override public func animateInsertion(_ currentTimestamp: Double, duration: Double) { + self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { + self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.textNode.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } + + override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + let textNodeFrame = self.textNode.textNode.frame + if let (index, attributes) = self.textNode.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + var concealed = true + if let (attributeText, fullText) = self.textNode.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { + concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) + } + return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: url, concealed: concealed))) + } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { + return ChatMessageBubbleContentTapAction(content: .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false)) + } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { + return ChatMessageBubbleContentTapAction(content: .textMention(peerName)) + } else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String { + return ChatMessageBubbleContentTapAction(content: .botCommand(botCommand)) + } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { + return ChatMessageBubbleContentTapAction(content: .hashtag(hashtag.peerName, hashtag.hashtag)) + } else { + return ChatMessageBubbleContentTapAction(content: .none) + } + } else { + for optionNode in self.optionNodes { + if optionNode.frame.contains(point), case .tap = gesture { + if optionNode.isUserInteractionEnabled { + return ChatMessageBubbleContentTapAction(content: .ignore) + } + } + } + if self.buttonNode.isUserInteractionEnabled, !self.buttonNode.isHidden, self.buttonNode.frame.contains(point) { + return ChatMessageBubbleContentTapAction(content: .ignore) + } + return ChatMessageBubbleContentTapAction(content: .none) + } + } + + override public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { + if !self.statusNode.isHidden { + return self.statusNode.reactionView(value: value) + } + return nil + } + + override public func messageEffectTargetView() -> UIView? { + if !self.statusNode.isHidden { + return self.statusNode.messageEffectTargetView() + } + return nil + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift index f84db49c41..2250de1168 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift @@ -170,6 +170,7 @@ public final class ChatRecentActionsController: TelegramBaseController { }, openBoostToUnrestrict: { }, updateRecordingTrimRange: { _, _, _, _ in }, dismissAllTooltips: { + }, editTodoMessage: { _, _ in }, updateHistoryFilter: { _ in }, updateChatLocationThread: { _, _ in }, toggleChatSidebarMode: { diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index 8d5710fe52..0df71d23a7 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -647,6 +647,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, playShakeAnimation: { }, displayQuickShare: { _, _ ,_ in }, updateChatLocationThread: { _, _ in + }, requestToggleTodoMessageItem: { _, _, _ in + }, displayTodoToggleUnavailable: { _ in }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: self.backgroundNode)) self.controllerInteraction = controllerInteraction diff --git a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift index a5a00e7cde..cc30bf2a7b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift @@ -502,6 +502,8 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess }, playShakeAnimation: { }, displayQuickShare: { _, _ ,_ in }, updateChatLocationThread: { _, _ in + }, requestToggleTodoMessageItem: { _, _, _ in + }, displayTodoToggleUnavailable: { _ in }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: self.context, backgroundNode: self.wallpaperBackgroundNode)) diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index 871e2b3caa..bfcc2f5bc4 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -281,7 +281,8 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public let playShakeAnimation: () -> Void public let displayQuickShare: (MessageId, ASDisplayNode, ContextGesture) -> Void public let updateChatLocationThread: (Int64?, ChatControllerAnimateInnerChatSwitchDirection?) -> Void - + public let requestToggleTodoMessageItem: (MessageId, Int32, Bool) -> Void + public let displayTodoToggleUnavailable: (MessageId) -> Void public var canPlayMedia: Bool = false public var hiddenMedia: [MessageId: [Media]] = [:] public var expandedTranslationMessageStableIds: Set = Set() @@ -444,6 +445,8 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol playShakeAnimation: @escaping () -> Void, displayQuickShare: @escaping (MessageId, ASDisplayNode, ContextGesture) -> Void, updateChatLocationThread: @escaping (Int64?, ChatControllerAnimateInnerChatSwitchDirection?) -> Void, + requestToggleTodoMessageItem: @escaping (MessageId, Int32, Bool) -> Void, + displayTodoToggleUnavailable: @escaping (MessageId) -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings, @@ -563,6 +566,8 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol self.playShakeAnimation = playShakeAnimation self.displayQuickShare = displayQuickShare self.updateChatLocationThread = updateChatLocationThread + self.requestToggleTodoMessageItem = requestToggleTodoMessageItem + self.displayTodoToggleUnavailable = displayTodoToggleUnavailable self.automaticMediaDownloadSettings = automaticMediaDownloadSettings diff --git a/submodules/TelegramUI/Components/ComposeTodoScreen/BUILD b/submodules/TelegramUI/Components/ComposeTodoScreen/BUILD new file mode 100644 index 0000000000..bdd35b09ee --- /dev/null +++ b/submodules/TelegramUI/Components/ComposeTodoScreen/BUILD @@ -0,0 +1,51 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ComposeTodoScreen", + module_name = "ComposeTodoScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/ItemListUI", + "//submodules/AccountContext", + "//submodules/AlertUI", + "//submodules/PresentationDataUtils", + "//submodules/TextFormat", + "//submodules/ObjCRuntimeUtils", + "//submodules/AttachmentUI", + "//submodules/TextInputMenu", + "//submodules/ComponentFlow", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/AppBundle", + "//submodules/TelegramUI/Components/EntityKeyboard", + "//submodules/UndoUI", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/AnimatedTextComponent", + "//submodules/TelegramUI/Components/PeerAllowedReactionsScreen", + "//submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent", + "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/TelegramUI/Components/TextFieldComponent", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", + "//submodules/ChatPresentationInterfaceState", + "//submodules/TelegramUI/Components/EmojiSuggestionsComponent", + "//submodules/TelegramUI/Components/ListComposePollOptionComponent", + "//submodules/ComposePollUI", + "//submodules/Markdown", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift b/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift new file mode 100644 index 0000000000..04714e2e0e --- /dev/null +++ b/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift @@ -0,0 +1,1531 @@ +import Foundation +import UIKit +import Display +import AccountContext +import TelegramCore +import Postbox +import SwiftSignalKit +import TelegramPresentationData +import ComponentFlow +import ComponentDisplayAdapters +import AppBundle +import ViewControllerComponent +import EntityKeyboard +import MultilineTextComponent +import UndoUI +import BundleIconComponent +import AnimatedTextComponent +import AudioToolbox +import ListSectionComponent +import PeerAllowedReactionsScreen +import AttachmentUI +import ListMultilineTextFieldItemComponent +import ListActionItemComponent +import ChatEntityKeyboardInputNode +import ChatPresentationInterfaceState +import EmojiSuggestionsComponent +import TextFormat +import TextFieldComponent +import ListComposePollOptionComponent +import Markdown + +final class ComposeTodoScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let peer: EnginePeer + let initialData: ComposeTodoScreen.InitialData + let completion: (TelegramMediaTodo) -> Void + + init( + context: AccountContext, + peer: EnginePeer, + initialData: ComposeTodoScreen.InitialData, + completion: @escaping (TelegramMediaTodo) -> Void + ) { + self.context = context + self.peer = peer + self.initialData = initialData + self.completion = completion + } + + static func ==(lhs: ComposeTodoScreenComponent, rhs: ComposeTodoScreenComponent) -> Bool { + return true + } + + private final class TodoItem { + let id: Int32 + let textInputState = TextFieldComponent.ExternalState() + let textFieldTag = NSObject() + var resetText: String? + + init(id: Int32) { + self.id = id + } + } + + final class View: UIView, UIScrollViewDelegate { + private let scrollView: UIScrollView + + private let todoTextSection = ComponentView() + + private let todoItemsSectionHeader = ComponentView() + private let todoItemsSectionFooterContainer = UIView() + private var todoItemsSectionFooter = ComponentView() + private var todoItemsSectionContainer: ListSectionContentView + + private let todoSettingsSection = ComponentView() + private let actionButton = ComponentView() + + private var isUpdating: Bool = false + private var ignoreScrolling: Bool = false + private var previousHadInputHeight: Bool = false + + private var component: ComposeTodoScreenComponent? + private(set) weak var state: EmptyComponentState? + private var environment: EnvironmentType? + + private let todoTextInputState = TextFieldComponent.ExternalState() + private let todoTextFieldTag = NSObject() + private var resetTodoText: String? + + private var nextTodoItemId: Int32 = 1 + private var todoItems: [TodoItem] = [] + private var currentTodoItemsLimitReached: Bool = false + + private var currentInputMode: ListComposePollOptionComponent.InputMode = .keyboard + + private var inputMediaNodeData: ChatEntityKeyboardInputNode.InputData? + private var inputMediaNodeDataDisposable: Disposable? + private var inputMediaNodeStateContext = ChatEntityKeyboardInputNode.StateContext() + private var inputMediaInteraction: ChatEntityKeyboardInputNode.Interaction? + private var inputMediaNode: ChatEntityKeyboardInputNode? + private var inputMediaNodeBackground = SimpleLayer() + private var inputMediaNodeTargetTag: AnyObject? + + private let inputMediaNodeDataPromise = Promise() + + private var currentEmojiSuggestionView: ComponentHostView? + + private var currentEditingTag: AnyObject? + + var isAppendableByOthers = false + var isCompletableByOthers = false + + override init(frame: CGRect) { + self.scrollView = UIScrollView() + self.scrollView.showsVerticalScrollIndicator = true + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.scrollsToTop = false + self.scrollView.delaysContentTouches = false + self.scrollView.canCancelContentTouches = true + self.scrollView.contentInsetAdjustmentBehavior = .never + self.scrollView.alwaysBounceVertical = true + + self.todoItemsSectionContainer = ListSectionContentView(frame: CGRect()) + self.todoItemsSectionContainer.automaticallyLayoutExternalContentBackgroundView = false + + super.init(frame: frame) + + self.scrollView.delegate = self + self.addSubview(self.scrollView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.inputMediaNodeDataDisposable?.dispose() + } + + func scrollToTop() { + self.scrollView.setContentOffset(CGPoint(), animated: true) + } + + func validatedInput() -> TelegramMediaTodo? { + if self.todoTextInputState.text.length == 0 { + return nil + } + + var mappedItems: [TelegramMediaTodo.Item] = [] + for todoItem in self.todoItems { + if todoItem.textInputState.text.length == 0 { + continue + } + var entities: [MessageTextEntity] = [] + for entity in generateChatInputTextEntities(todoItem.textInputState.text) { + switch entity.type { + case .CustomEmoji: + entities.append(entity) + default: + break + } + } + mappedItems.append( + TelegramMediaTodo.Item( + text: todoItem.textInputState.text.string, + entities: entities, + id: todoItem.id + ) + ) + } + + if mappedItems.count < 1 { + return nil + } + + var textEntities: [MessageTextEntity] = [] + for entity in generateChatInputTextEntities(self.todoTextInputState.text) { + switch entity.type { + case .CustomEmoji: + textEntities.append(entity) + default: + break + } + } + + var flags: TelegramMediaTodo.Flags = [] + if self.isCompletableByOthers { + flags.insert(.othersCanComplete) + if self.isAppendableByOthers { + flags.insert(.othersCanAppend) + } + } + + return TelegramMediaTodo( + flags: flags, + text: self.todoTextInputState.text.string, + textEntities: textEntities, + items: mappedItems + ) + } + + func attemptNavigation(complete: @escaping () -> Void) -> Bool { + guard let component = self.component else { + return true + } + + let _ = component + + return true + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.updateScrolling(transition: .immediate) + } + } + + private func updateScrolling(transition: ComponentTransition) { + let navigationAlphaDistance: CGFloat = 16.0 + let navigationAlpha: CGFloat = max(0.0, min(1.0, self.scrollView.contentOffset.y / navigationAlphaDistance)) + if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { + transition.setAlpha(layer: navigationBar.backgroundNode.layer, alpha: navigationAlpha) + transition.setAlpha(layer: navigationBar.stripeNode.layer, alpha: navigationAlpha) + } + } + + func isPanGestureEnabled() -> Bool { + if self.inputMediaNode != nil { + return false + } + + for (_, state) in self.collectTextInputStates() { + if state.isEditing { + return false + } + } + + return true + } + + private func updateInputMediaNode( + component: ComposeTodoScreenComponent, + availableSize: CGSize, + bottomInset: CGFloat, + inputHeight: CGFloat, + effectiveInputHeight: CGFloat, + metrics: LayoutMetrics, + deviceMetrics: DeviceMetrics, + transition: ComponentTransition + ) -> CGFloat { + let bottomInset: CGFloat = bottomInset + 8.0 + let bottomContainerInset: CGFloat = 0.0 + let needsInputActivation: Bool = !"".isEmpty + + var height: CGFloat = 0.0 + if case .emoji = self.currentInputMode, let inputData = self.inputMediaNodeData { + if let updatedTag = self.collectTextInputStates().first(where: { $1.isEditing })?.view.currentTag { + self.inputMediaNodeTargetTag = updatedTag + } + + let inputMediaNode: ChatEntityKeyboardInputNode + var inputMediaNodeTransition = transition + var animateIn = false + if let current = self.inputMediaNode { + inputMediaNode = current + } else { + animateIn = true + inputMediaNodeTransition = inputMediaNodeTransition.withAnimation(.none) + inputMediaNode = ChatEntityKeyboardInputNode( + context: component.context, + currentInputData: inputData, + updatedInputData: self.inputMediaNodeDataPromise.get(), + defaultToEmojiTab: true, + opaqueTopPanelBackground: false, + useOpaqueTheme: true, + interaction: self.inputMediaInteraction, + chatPeerId: nil, + stateContext: self.inputMediaNodeStateContext + ) + inputMediaNode.clipsToBounds = true + + inputMediaNode.externalTopPanelContainerImpl = nil + inputMediaNode.useExternalSearchContainer = true + if inputMediaNode.view.superview == nil { + self.inputMediaNodeBackground.removeAllAnimations() + self.layer.addSublayer(self.inputMediaNodeBackground) + self.addSubview(inputMediaNode.view) + } + self.inputMediaNode = inputMediaNode + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let presentationInterfaceState = ChatPresentationInterfaceState( + chatWallpaper: .builtin(WallpaperSettings()), + theme: presentationData.theme, + strings: presentationData.strings, + dateTimeFormat: presentationData.dateTimeFormat, + nameDisplayOrder: presentationData.nameDisplayOrder, + limitsConfiguration: component.context.currentLimitsConfiguration.with { $0 }, + fontSize: presentationData.chatFontSize, + bubbleCorners: presentationData.chatBubbleCorners, + accountPeerId: component.context.account.peerId, + mode: .standard(.default), + chatLocation: .peer(id: component.context.account.peerId), + subject: nil, + peerNearbyData: nil, + greetingData: nil, + pendingUnpinnedAllMessages: false, + activeGroupCallInfo: nil, + hasActiveGroupCall: false, + importState: nil, + threadData: nil, + isGeneralThreadClosed: nil, + replyMessage: nil, + accountPeerColor: nil, + businessIntro: nil + ) + + self.inputMediaNodeBackground.backgroundColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor.cgColor + + let heightAndOverflow = inputMediaNode.updateLayout(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, bottomInset: bottomInset, standardInputHeight: deviceMetrics.standardInputHeight(inLandscape: false), inputHeight: inputHeight < 100.0 ? inputHeight - bottomContainerInset : inputHeight, maximumHeight: availableSize.height, inputPanelHeight: 0.0, transition: .immediate, interfaceState: presentationInterfaceState, layoutMetrics: metrics, deviceMetrics: deviceMetrics, isVisible: true, isExpanded: false) + let inputNodeHeight = heightAndOverflow.0 + let inputNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputNodeHeight), size: CGSize(width: availableSize.width, height: inputNodeHeight)) + + let inputNodeBackgroundFrame = CGRect(origin: CGPoint(x: inputNodeFrame.minX, y: inputNodeFrame.minY - 6.0), size: CGSize(width: inputNodeFrame.width, height: inputNodeFrame.height + 6.0)) + + if needsInputActivation { + let inputNodeFrame = inputNodeFrame.offsetBy(dx: 0.0, dy: inputNodeHeight) + ComponentTransition.immediate.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) + ComponentTransition.immediate.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame) + } + + if animateIn { + var targetFrame = inputNodeFrame + targetFrame.origin.y = availableSize.height + inputMediaNodeTransition.setFrame(layer: inputMediaNode.layer, frame: targetFrame) + + let inputNodeBackgroundTargetFrame = CGRect(origin: CGPoint(x: targetFrame.minX, y: targetFrame.minY - 6.0), size: CGSize(width: targetFrame.width, height: targetFrame.height + 6.0)) + + inputMediaNodeTransition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundTargetFrame) + + transition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) + transition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame) + } else { + inputMediaNodeTransition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) + inputMediaNodeTransition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame) + } + + height = heightAndOverflow.0 + } else { + self.inputMediaNodeTargetTag = nil + + if let inputMediaNode = self.inputMediaNode { + self.inputMediaNode = nil + var targetFrame = inputMediaNode.frame + targetFrame.origin.y = availableSize.height + transition.setFrame(view: inputMediaNode.view, frame: targetFrame, completion: { [weak inputMediaNode] _ in + if let inputMediaNode { + Queue.mainQueue().after(0.3) { + inputMediaNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { [weak inputMediaNode] _ in + inputMediaNode?.view.removeFromSuperview() + }) + } + } + }) + transition.setFrame(layer: self.inputMediaNodeBackground, frame: targetFrame, completion: { [weak self] _ in + Queue.mainQueue().after(0.3) { + guard let self else { + return + } + if self.currentInputMode == .keyboard { + self.inputMediaNodeBackground.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { [weak self] finished in + guard let self else { + return + } + + if finished { + self.inputMediaNodeBackground.removeFromSuperlayer() + } + self.inputMediaNodeBackground.removeAllAnimations() + }) + } + } + }) + } + } + + /*if needsInputActivation { + needsInputActivation = false + Queue.mainQueue().justDispatch { + inputPanelView.activateInput() + } + }*/ + + if let controller = self.environment?.controller() as? ComposeTodoScreen { + let isTabBarVisible = self.inputMediaNode == nil + DispatchQueue.main.async { [weak controller] in + controller?.updateTabBarVisibility(isTabBarVisible, transition.containedViewLayoutTransition) + } + } + + return height + } + + private func collectTextInputStates() -> [(view: ListComposePollOptionComponent.View, state: TextFieldComponent.ExternalState)] { + var textInputStates: [(view: ListComposePollOptionComponent.View, state: TextFieldComponent.ExternalState)] = [] + if let textInputView = self.todoTextSection.findTaggedView(tag: self.todoTextFieldTag) as? ListComposePollOptionComponent.View { + textInputStates.append((textInputView, self.todoTextInputState)) + } + for todoItem in self.todoItems { + if let textInputView = findTaggedComponentViewImpl(view: self.todoItemsSectionContainer, tag: todoItem.textFieldTag) as? ListComposePollOptionComponent.View { + textInputStates.append((textInputView, todoItem.textInputState)) + } + } + return textInputStates + } + + func update(component: ComposeTodoScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + var alphaTransition = transition + if !transition.animation.isImmediate { + alphaTransition = alphaTransition.withAnimation(.curve(duration: 0.25, curve: .easeInOut)) + } + + let environment = environment[EnvironmentType.self].value + let themeUpdated = self.environment?.theme !== environment.theme + self.environment = environment + + let isFirstTime = self.component == nil + if self.component == nil { + if let existingTodo = component.initialData.existingTodo { + self.resetTodoText = existingTodo.text + + for item in existingTodo.items { + let todoItem = ComposeTodoScreenComponent.TodoItem( + id: item.id + ) + todoItem.resetText = item.text + self.todoItems.append(todoItem) + } + self.nextTodoItemId = (existingTodo.items.max(by: { $0.id < $1.id })?.id ?? 0) + 1 + + self.isAppendableByOthers = existingTodo.flags.contains(.othersCanAppend) + self.isCompletableByOthers = existingTodo.flags.contains(.othersCanComplete) + } else { + self.todoItems.append(ComposeTodoScreenComponent.TodoItem( + id: self.nextTodoItemId + )) + self.nextTodoItemId += 1 + self.todoItems.append(ComposeTodoScreenComponent.TodoItem( + id: self.nextTodoItemId + )) + self.nextTodoItemId += 1 + } + + self.inputMediaNodeDataPromise.set( + ChatEntityKeyboardInputNode.inputData( + context: component.context, + chatPeerId: nil, + areCustomEmojiEnabled: true, + hasTrending: false, + hasSearch: true, + hasStickers: false, + hasGifs: false, + hideBackground: true, + sendGif: nil + ) + ) + self.inputMediaNodeDataDisposable = (self.inputMediaNodeDataPromise.get() + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let self else { + return + } + self.inputMediaNodeData = value + }) + + self.inputMediaInteraction = ChatEntityKeyboardInputNode.Interaction( + sendSticker: { _, _, _, _, _, _, _, _, _ in + return false + }, + sendEmoji: { _, _, _ in + let _ = self + }, + sendGif: { _, _, _, _, _ in + return false + }, + sendBotContextResultAsGif: { _, _ , _, _, _, _ in + return false + }, + updateChoosingSticker: { _ in + }, + switchToTextInput: { [weak self] in + guard let self else { + return + } + self.currentInputMode = .keyboard + self.state?.updated(transition: .spring(duration: 0.4)) + }, + dismissTextInput: { + }, + insertText: { [weak self] text in + guard let self else { + return + } + + var found = false + for (textInputView, externalState) in self.collectTextInputStates() { + if externalState.isEditing { + textInputView.insertText(text: text) + found = true + break + } + } + if !found, let inputMediaNodeTargetTag = self.inputMediaNodeTargetTag { + for (textInputView, _) in self.collectTextInputStates() { + if textInputView.currentTag === inputMediaNodeTargetTag { + textInputView.insertText(text: text) + found = true + break + } + } + } + }, + backwardsDeleteText: { [weak self] in + guard let self else { + return + } + var found = false + for (textInputView, externalState) in self.collectTextInputStates() { + if externalState.isEditing { + textInputView.backwardsDeleteText() + found = true + break + } + } + if !found, let inputMediaNodeTargetTag = self.inputMediaNodeTargetTag { + for (textInputView, _) in self.collectTextInputStates() { + if textInputView.currentTag === inputMediaNodeTargetTag { + textInputView.backwardsDeleteText() + found = true + break + } + } + } + }, + openStickerEditor: { + }, + presentController: { [weak self] c, a in + guard let self else { + return + } + self.environment?.controller()?.present(c, in: .window(.root), with: a) + }, + presentGlobalOverlayController: { [weak self] c, a in + guard let self else { + return + } + self.environment?.controller()?.presentInGlobalOverlay(c, with: a) + }, + getNavigationController: { [weak self] () -> NavigationController? in + guard let self else { + return nil + } + guard let controller = self.environment?.controller() as? ComposeTodoScreen else { + return nil + } + + if let navigationController = controller.navigationController as? NavigationController { + return navigationController + } + if let parentController = controller.parentController() { + return parentController.navigationController as? NavigationController + } + return nil + }, + requestLayout: { [weak self] transition in + guard let self else { + return + } + if !self.isUpdating { + self.state?.updated(transition: ComponentTransition(transition)) + } + } + ) + } + + self.component = component + self.state = state + + let topInset: CGFloat = 24.0 + let bottomInset: CGFloat = 8.0 + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let sectionSpacing: CGFloat = 24.0 + + if themeUpdated { + self.backgroundColor = environment.theme.list.blocksBackgroundColor + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + var contentHeight: CGFloat = 0.0 + contentHeight += environment.navigationHeight + contentHeight += topInset + + var canEdit = true + if let _ = component.initialData.existingTodo, !component.initialData.canEdit { + canEdit = false + } + + var todoTextSectionItems: [AnyComponentWithIdentity] = [] + todoTextSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListComposePollOptionComponent( + externalState: self.todoTextInputState, + context: component.context, + theme: environment.theme, + strings: environment.strings, + isEnabled: canEdit, + resetText: self.resetTodoText.flatMap { resetText in + return ListComposePollOptionComponent.ResetText(value: NSAttributedString(string: resetText)) + }, + assumeIsEditing: self.inputMediaNodeTargetTag === self.todoTextFieldTag, + characterLimit: component.initialData.maxTodoTextLength, + emptyLineHandling: .allowed, + returnKeyAction: { [weak self] in + guard let self else { + return + } + if !self.todoItems.isEmpty { + if let todoItemView = self.todoItemsSectionContainer.itemViews[self.todoItems[0].id] { + if let todoItemComponentView = todoItemView.contents.view as? ListComposePollOptionComponent.View { + todoItemComponentView.activateInput() + } + } + } + }, + backspaceKeyAction: nil, + selection: nil, + inputMode: self.currentInputMode, + toggleInputMode: { [weak self] in + guard let self else { + return + } + switch self.currentInputMode { + case .keyboard: + self.currentInputMode = .emoji + case .emoji: + self.currentInputMode = .keyboard + } + self.state?.updated(transition: .spring(duration: 0.4)) + }, + tag: self.todoTextFieldTag + )))) + self.resetTodoText = nil + + let todoTextSectionSize = self.todoTextSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: nil, + items: todoTextSectionItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let todoTextSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: todoTextSectionSize) + if let todoTextSectionView = self.todoTextSection.view as? ListSectionComponent.View { + if todoTextSectionView.superview == nil { + self.scrollView.addSubview(todoTextSectionView) + self.todoTextSection.parentState = state + } + transition.setFrame(view: todoTextSectionView, frame: todoTextSectionFrame) + + if let itemView = todoTextSectionView.itemView(id: 0) as? ListComposePollOptionComponent.View { + itemView.updateCustomPlaceholder(value: "Title", size: itemView.bounds.size, transition: .immediate) + } + } + contentHeight += todoTextSectionSize.height + contentHeight += sectionSpacing + + var todoItemsSectionItems: [AnyComponentWithIdentity] = [] + + var todoItemsSectionReadyItems: [ListSectionContentView.ReadyItem] = [] + + let processTodoItemItem: (Int) -> Void = { i in + let todoItem = self.todoItems[i] + + let optionId = todoItem.id + + var isEnabled = true + if !canEdit, let existingTodo = component.initialData.existingTodo, existingTodo.items.contains(where: { $0.id == todoItem.id }) { + isEnabled = false + } + + todoItemsSectionItems.append(AnyComponentWithIdentity(id: todoItem.id, component: AnyComponent(ListComposePollOptionComponent( + externalState: todoItem.textInputState, + context: component.context, + theme: environment.theme, + strings: environment.strings, + isEnabled: isEnabled, + resetText: todoItem.resetText.flatMap { resetText in + return ListComposePollOptionComponent.ResetText(value: NSAttributedString(string: resetText)) + }, + assumeIsEditing: self.inputMediaNodeTargetTag === todoItem.textFieldTag, + characterLimit: component.initialData.maxTodoItemLength, + emptyLineHandling: .notAllowed, + returnKeyAction: { [weak self] in + guard let self else { + return + } + if let index = self.todoItems.firstIndex(where: { $0.id == optionId }) { + if index == self.todoItems.count - 1 { + self.endEditing(true) + } else { + if let todoItemView = self.todoItemsSectionContainer.itemViews[self.todoItems[index + 1].id] { + if let todoItemComponentView = todoItemView.contents.view as? ListComposePollOptionComponent.View { + todoItemComponentView.activateInput() + } + } + } + } + }, + backspaceKeyAction: { [weak self] in + guard let self else { + return + } + if let index = self.todoItems.firstIndex(where: { $0.id == optionId }) { + if index == 0 { + if let textInputView = self.todoTextSection.findTaggedView(tag: self.todoTextFieldTag) as? ListComposePollOptionComponent.View { + textInputView.activateInput() + } + } else { + if let todoItemView = self.todoItemsSectionContainer.itemViews[self.todoItems[index - 1].id] { + if let todoItemComponentView = todoItemView.contents.view as? ListComposePollOptionComponent.View { + todoItemComponentView.activateInput() + } + } + } + } + }, + selection: nil, + inputMode: self.currentInputMode, + toggleInputMode: { [weak self] in + guard let self else { + return + } + switch self.currentInputMode { + case .keyboard: + self.currentInputMode = .emoji + case .emoji: + self.currentInputMode = .keyboard + } + self.state?.updated(transition: .spring(duration: 0.4)) + }, + tag: todoItem.textFieldTag + )))) + + let item = todoItemsSectionItems[i] + let itemId = item.id + + let itemView: ListSectionContentView.ItemView + var itemTransition = transition + if let current = self.todoItemsSectionContainer.itemViews[itemId] { + itemView = current + } else { + itemTransition = itemTransition.withAnimation(.none) + itemView = ListSectionContentView.ItemView() + self.todoItemsSectionContainer.itemViews[itemId] = itemView + itemView.contents.parentState = state + } + + let itemSize = itemView.contents.update( + transition: itemTransition, + component: item.component, + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) + ) + + todoItemsSectionReadyItems.append(ListSectionContentView.ReadyItem( + id: itemId, + itemView: itemView, + size: itemSize, + transition: itemTransition + )) + } + + for i in 0 ..< self.todoItems.count { + processTodoItemItem(i) + } + + if self.todoItems.count > 2 { + let lastOption = self.todoItems[self.todoItems.count - 1] + let secondToLastOption = self.todoItems[self.todoItems.count - 2] + + if !lastOption.textInputState.isEditing && lastOption.textInputState.text.length == 0 && secondToLastOption.textInputState.text.length == 0 { + self.todoItems.removeLast() + todoItemsSectionItems.removeLast() + todoItemsSectionReadyItems.removeLast() + } + } + + if self.todoItems.count < component.initialData.maxTodoItemsCount, let lastOption = self.todoItems.last { + if lastOption.textInputState.text.length != 0 { + self.todoItems.append(TodoItem(id: self.nextTodoItemId)) + self.nextTodoItemId += 1 + processTodoItemItem(self.todoItems.count - 1) + } + } + + for i in 0 ..< todoItemsSectionReadyItems.count { + var activate = false + let placeholder: String + if i == todoItemsSectionReadyItems.count - 1 { + placeholder = "Add a Task" + if isFirstTime, component.initialData.append { + activate = true + } + } else { + placeholder = "Task" + } + + if let itemView = todoItemsSectionReadyItems[i].itemView.contents.view as? ListComposePollOptionComponent.View { + itemView.updateCustomPlaceholder(value: placeholder, size: todoItemsSectionReadyItems[i].size, transition: todoItemsSectionReadyItems[i].transition) + + if activate { + itemView.activateInput() + } + } + } + + let todoItemsSectionUpdateResult = self.todoItemsSectionContainer.update( + configuration: ListSectionContentView.Configuration( + theme: environment.theme, + displaySeparators: true, + extendsItemHighlightToSection: false, + background: .all + ), + width: availableSize.width - sideInset * 2.0, + leftInset: 0.0, + readyItems: todoItemsSectionReadyItems, + transition: transition + ) + + let sectionHeaderSideInset: CGFloat = 16.0 + let todoItemsSectionHeaderSize = self.todoItemsSectionHeader.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "TO DO LIST", + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - sectionHeaderSideInset * 2.0, height: 1000.0) + ) + let todoItemsSectionHeaderFrame = CGRect(origin: CGPoint(x: sideInset + sectionHeaderSideInset, y: contentHeight), size: todoItemsSectionHeaderSize) + if let todoItemsSectionHeaderView = self.todoItemsSectionHeader.view { + if todoItemsSectionHeaderView.superview == nil { + todoItemsSectionHeaderView.layer.anchorPoint = CGPoint() + self.scrollView.addSubview(todoItemsSectionHeaderView) + } + transition.setPosition(view: todoItemsSectionHeaderView, position: todoItemsSectionHeaderFrame.origin) + todoItemsSectionHeaderView.bounds = CGRect(origin: CGPoint(), size: todoItemsSectionHeaderFrame.size) + } + contentHeight += todoItemsSectionHeaderSize.height + contentHeight += 7.0 + + let todoItemsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: todoItemsSectionUpdateResult.size) + if self.todoItemsSectionContainer.superview == nil { + self.scrollView.addSubview(self.todoItemsSectionContainer.externalContentBackgroundView) + self.scrollView.addSubview(self.todoItemsSectionContainer) + } + transition.setFrame(view: self.todoItemsSectionContainer, frame: todoItemsSectionFrame) + transition.setFrame(view: self.todoItemsSectionContainer.externalContentBackgroundView, frame: todoItemsSectionUpdateResult.backgroundFrame.offsetBy(dx: todoItemsSectionFrame.minX, dy: todoItemsSectionFrame.minY)) + contentHeight += todoItemsSectionUpdateResult.size.height + + contentHeight += 7.0 + + let todoItemsLimitReached = self.todoItems.count >= component.initialData.maxTodoItemsCount + var animateTodoItemsFooterIn = false + var todoItemsFooterTransition = transition + if self.currentTodoItemsLimitReached != todoItemsLimitReached { + self.currentTodoItemsLimitReached = todoItemsLimitReached + if let todoItemsSectionFooterView = self.todoItemsSectionFooter.view { + animateTodoItemsFooterIn = true + todoItemsFooterTransition = todoItemsFooterTransition.withAnimation(.none) + alphaTransition.setAlpha(view: todoItemsSectionFooterView, alpha: 0.0, completion: { [weak todoItemsSectionFooterView] _ in + todoItemsSectionFooterView?.removeFromSuperview() + }) + self.todoItemsSectionFooter = ComponentView() + } + } + + let todoItemsComponent: AnyComponent + if !"".isEmpty, todoItemsLimitReached { + todoItemsFooterTransition = todoItemsFooterTransition.withAnimation(.none) + + let textFont = Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize) + let boldTextFont = Font.semibold(presentationData.listsFontSize.itemListBaseHeaderFontSize) + let textColor = environment.theme.list.freeTextColor + todoItemsComponent = AnyComponent(MultilineTextComponent( + text: .markdown( + text: "Limit of tasks reached. You can increase the limit to **20 tasks** by subscribing to [Telegram Premium]().", + attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: textFont, textColor: textColor), + bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), + link: MarkdownAttributeSet(font: textFont, textColor: environment.theme.list.itemAccentColor), + linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + } + ) + ), + maximumNumberOfLines: 0, + highlightColor: presentationData.theme.list.itemAccentColor.withAlphaComponent(0.2), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { [weak self] _, _ in + guard let self, let component = self.component else { + return + } + let controller = component.context.sharedContext.makePremiumIntroController( + context: component.context, + source: .chatsPerFolder, + forceDark: false, + dismissed: nil + ) + (self.environment?.controller() as? AttachmentContainable)?.parentController()?.push(controller) + } + )) + } else { + let remainingCount = component.initialData.maxTodoItemsCount - self.todoItems.count + let rawString = "You can add {count} more tasks." //environment.strings.CreatePoll_OptionCountFooterFormat(Int32(remainingCount)) + + var todoItemsFooterItems: [AnimatedTextComponent.Item] = [] + if let range = rawString.range(of: "{count}") { + if range.lowerBound != rawString.startIndex { + todoItemsFooterItems.append(AnimatedTextComponent.Item( + id: 0, + isUnbreakable: true, + content: .text(String(rawString[rawString.startIndex ..< range.lowerBound])) + )) + } + todoItemsFooterItems.append(AnimatedTextComponent.Item( + id: 1, + isUnbreakable: true, + content: .number(remainingCount, minDigits: 1) + )) + if range.upperBound != rawString.endIndex { + todoItemsFooterItems.append(AnimatedTextComponent.Item( + id: 2, + isUnbreakable: true, + content: .text(String(rawString[range.upperBound ..< rawString.endIndex])) + )) + } + } + + todoItemsComponent = AnyComponent(AnimatedTextComponent( + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + color: environment.theme.list.freeTextColor, + items: todoItemsFooterItems + )) + } + + let todoItemsSectionFooterSize = self.todoItemsSectionFooter.update( + transition: todoItemsFooterTransition, + component: todoItemsComponent, + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - sectionHeaderSideInset * 2.0, height: 1000.0) + ) + let todoItemsSectionFooterFrame = CGRect(origin: CGPoint(x: sideInset + sectionHeaderSideInset, y: contentHeight), size: todoItemsSectionFooterSize) + + if self.todoItemsSectionFooterContainer.superview == nil { + self.scrollView.addSubview(self.todoItemsSectionFooterContainer) + } + transition.setFrame(view: self.todoItemsSectionFooterContainer, frame: todoItemsSectionFooterFrame) + + if let todoItemsSectionFooterView = self.todoItemsSectionFooter.view { + if todoItemsSectionFooterView.superview == nil { + todoItemsSectionFooterView.layer.anchorPoint = CGPoint() + self.todoItemsSectionFooterContainer.addSubview(todoItemsSectionFooterView) + } + todoItemsFooterTransition.setPosition(view: todoItemsSectionFooterView, position: CGPoint()) + todoItemsSectionFooterView.bounds = CGRect(origin: CGPoint(), size: todoItemsSectionFooterFrame.size) + if animateTodoItemsFooterIn && !transition.animation.isImmediate { + alphaTransition.animateAlpha(view: todoItemsSectionFooterView, from: 0.0, to: 1.0) + } + } + contentHeight += todoItemsSectionFooterSize.height + contentHeight += sectionSpacing + + var todoSettingsSectionItems: [AnyComponentWithIdentity] = [] + if canEdit && component.peer.id != component.context.account.peerId { + todoSettingsSectionItems.append(AnyComponentWithIdentity(id: "completable", component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Allow Others to Mark as Done", + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.isCompletableByOthers, action: { [weak self] _ in + guard let self else { + return + } + self.isCompletableByOthers = !self.isCompletableByOthers + self.state?.updated(transition: .spring(duration: 0.4)) + })), + action: nil + )))) + + if self.isCompletableByOthers { + todoSettingsSectionItems.append(AnyComponentWithIdentity(id: "editable", component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Allow Others to Add Tasks", + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.isAppendableByOthers, action: { [weak self] _ in + guard let self else { + return + } + self.isAppendableByOthers = !self.isAppendableByOthers + self.state?.updated(transition: .spring(duration: 0.4)) + })), + action: nil + )))) + } + } + + if !todoSettingsSectionItems.isEmpty { + let todoSettingsSectionSize = self.todoSettingsSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: nil, + items: todoSettingsSectionItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let todoSettingsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: todoSettingsSectionSize) + if let todoSettingsSectionView = self.todoSettingsSection.view { + if todoSettingsSectionView.superview == nil { + self.scrollView.addSubview(todoSettingsSectionView) + self.todoSettingsSection.parentState = state + } + transition.setFrame(view: todoSettingsSectionView, frame: todoSettingsSectionFrame) + } + contentHeight += todoSettingsSectionSize.height + } + + var inputHeight: CGFloat = 0.0 + inputHeight += self.updateInputMediaNode( + component: component, + availableSize: availableSize, + bottomInset: environment.safeInsets.bottom, + inputHeight: 0.0, + effectiveInputHeight: environment.deviceMetrics.standardInputHeight(inLandscape: false), + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + transition: transition + ) + if self.inputMediaNode == nil { + inputHeight = environment.inputHeight + } + + let textInputStates = self.collectTextInputStates() + + let previousEditingTag = self.currentEditingTag + let isEditing: Bool + if let index = textInputStates.firstIndex(where: { $0.state.isEditing }) { + isEditing = true + self.currentEditingTag = textInputStates[index].view.currentTag + } else { + isEditing = false + self.currentEditingTag = nil + } + + if let (_, suggestionTextInputState) = textInputStates.first(where: { $0.state.isEditing && $0.state.currentEmojiSuggestion != nil }), let emojiSuggestion = suggestionTextInputState.currentEmojiSuggestion, emojiSuggestion.disposable == nil { + emojiSuggestion.disposable = (EmojiSuggestionsComponent.suggestionData(context: component.context, isSavedMessages: false, query: emojiSuggestion.position.value) + |> deliverOnMainQueue).start(next: { [weak self, weak suggestionTextInputState, weak emojiSuggestion] result in + guard let self, let suggestionTextInputState, let emojiSuggestion, suggestionTextInputState.currentEmojiSuggestion === emojiSuggestion else { + return + } + + emojiSuggestion.value = result + self.state?.updated() + }) + } + + for (_, suggestionTextInputState) in textInputStates { + var hasTrackingView = suggestionTextInputState.hasTrackingView + if let currentEmojiSuggestion = suggestionTextInputState.currentEmojiSuggestion, let value = currentEmojiSuggestion.value as? [TelegramMediaFile], value.isEmpty { + hasTrackingView = false + } + if !suggestionTextInputState.isEditing { + hasTrackingView = false + } + + if !hasTrackingView { + if let currentEmojiSuggestion = suggestionTextInputState.currentEmojiSuggestion { + suggestionTextInputState.currentEmojiSuggestion = nil + currentEmojiSuggestion.disposable?.dispose() + } + + if let currentEmojiSuggestionView = self.currentEmojiSuggestionView { + self.currentEmojiSuggestionView = nil + + currentEmojiSuggestionView.alpha = 0.0 + currentEmojiSuggestionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak currentEmojiSuggestionView] _ in + currentEmojiSuggestionView?.removeFromSuperview() + }) + } + } + } + + if let (suggestionTextInputView, suggestionTextInputState) = textInputStates.first(where: { $0.state.isEditing && $0.state.currentEmojiSuggestion != nil }), let emojiSuggestion = suggestionTextInputState.currentEmojiSuggestion, let value = emojiSuggestion.value as? [TelegramMediaFile] { + let currentEmojiSuggestionView: ComponentHostView + if let current = self.currentEmojiSuggestionView { + currentEmojiSuggestionView = current + } else { + currentEmojiSuggestionView = ComponentHostView() + self.currentEmojiSuggestionView = currentEmojiSuggestionView + self.addSubview(currentEmojiSuggestionView) + + currentEmojiSuggestionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + + let globalPosition: CGPoint + if let textView = suggestionTextInputView.textFieldView { + globalPosition = textView.convert(emojiSuggestion.localPosition, to: self) + } else { + globalPosition = .zero + } + + let sideInset: CGFloat = 7.0 + + let viewSize = currentEmojiSuggestionView.update( + transition: .immediate, + component: AnyComponent(EmojiSuggestionsComponent( + context: component.context, + userLocation: .other, + theme: EmojiSuggestionsComponent.Theme(theme: environment.theme, backgroundColor: environment.theme.list.itemBlocksBackgroundColor), + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + files: value, + action: { [weak self, weak suggestionTextInputView, weak suggestionTextInputState] file in + guard let self, let suggestionTextInputView, let suggestionTextInputState, let textView = suggestionTextInputView.textFieldView, let currentEmojiSuggestion = suggestionTextInputState.currentEmojiSuggestion else { + return + } + + let _ = self + + AudioServicesPlaySystemSound(0x450) + + let inputState = textView.getInputState() + let inputText = NSMutableAttributedString(attributedString: inputState.inputText) + + var text: String? + var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? + loop: for attribute in file.attributes { + switch attribute { + case let .CustomEmoji(_, _, displayText, _): + text = displayText + emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file) + break loop + default: + break + } + } + + if let emojiAttribute = emojiAttribute, let text = text { + let replacementText = NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute]) + + let range = currentEmojiSuggestion.position.range + let previousText = inputText.attributedSubstring(from: range) + inputText.replaceCharacters(in: range, with: replacementText) + + var replacedUpperBound = range.lowerBound + while true { + if inputText.attributedSubstring(from: NSRange(location: 0, length: replacedUpperBound)).string.hasSuffix(previousText.string) { + let replaceRange = NSRange(location: replacedUpperBound - previousText.length, length: previousText.length) + if replaceRange.location < 0 { + break + } + let adjacentString = inputText.attributedSubstring(from: replaceRange) + if adjacentString.string != previousText.string || adjacentString.attribute(ChatTextInputAttributes.customEmoji, at: 0, effectiveRange: nil) != nil { + break + } + inputText.replaceCharacters(in: replaceRange, with: NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: emojiAttribute.interactivelySelectedFromPackId, fileId: emojiAttribute.fileId, file: emojiAttribute.file)])) + replacedUpperBound = replaceRange.lowerBound + } else { + break + } + } + + let selectionPosition = range.lowerBound + (replacementText.string as NSString).length + textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition) + } + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + + let viewFrame = CGRect(origin: CGPoint(x: min(availableSize.width - sideInset - viewSize.width, max(sideInset, floor(globalPosition.x - viewSize.width / 2.0))), y: globalPosition.y - 4.0 - viewSize.height), size: viewSize) + currentEmojiSuggestionView.frame = viewFrame + if let componentView = currentEmojiSuggestionView.componentView as? EmojiSuggestionsComponent.View { + componentView.adjustBackground(relativePositionX: floor(globalPosition.x + 10.0)) + } + } + + let combinedBottomInset: CGFloat + combinedBottomInset = bottomInset + max(environment.safeInsets.bottom, 8.0 + inputHeight) + contentHeight += combinedBottomInset + + var recenterOnTag: AnyObject? + if let hint = transition.userData(TextFieldComponent.AnimationHint.self), let targetView = hint.view { + var matches = false + switch hint.kind { + case .textChanged: + matches = true + case let .textFocusChanged(isFocused): + if isFocused { + matches = true + } + } + + if matches { + for (textView, _) in self.collectTextInputStates() { + if targetView.isDescendant(of: textView) { + recenterOnTag = textView.currentTag + break + } + } + } + } + if recenterOnTag == nil && self.previousHadInputHeight != (inputHeight > 0.0) { + for (textView, state) in self.collectTextInputStates() { + if state.isEditing { + recenterOnTag = textView.currentTag + break + } + } + } + self.previousHadInputHeight = (inputHeight > 0.0) + + self.ignoreScrolling = true + let previousBounds = self.scrollView.bounds + let contentSize = CGSize(width: availableSize.width, height: contentHeight) + if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) { + self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize) + } + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: environment.safeInsets.bottom, right: 0.0) + if self.scrollView.scrollIndicatorInsets != scrollInsets { + self.scrollView.scrollIndicatorInsets = scrollInsets + } + + if let recenterOnTag { + if let targetView = self.collectTextInputStates().first(where: { $0.view.currentTag === recenterOnTag })?.view { + let caretRect = targetView.convert(targetView.bounds, to: self.scrollView) + var scrollViewBounds = self.scrollView.bounds + let minButtonDistance: CGFloat = 16.0 + if -scrollViewBounds.minY + caretRect.maxY > availableSize.height - combinedBottomInset - minButtonDistance { + scrollViewBounds.origin.y = -(availableSize.height - combinedBottomInset - minButtonDistance - caretRect.maxY) + if scrollViewBounds.origin.y < 0.0 { + scrollViewBounds.origin.y = 0.0 + } + } + if self.scrollView.bounds != scrollViewBounds { + self.scrollView.bounds = scrollViewBounds + } + } + } + if !previousBounds.isEmpty, !transition.animation.isImmediate { + let bounds = self.scrollView.bounds + if bounds.maxY != previousBounds.maxY { + let offsetY = previousBounds.maxY - bounds.maxY + transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) + } + } + self.ignoreScrolling = false + + self.updateScrolling(transition: transition) + + if isEditing { + if let controller = environment.controller() as? ComposeTodoScreen { + DispatchQueue.main.async { [weak controller] in + controller?.requestAttachmentMenuExpansion() + } + } + } + + let isValid = self.validatedInput() != nil + if let controller = environment.controller() as? ComposeTodoScreen, let sendButtonItem = controller.sendButtonItem { + if sendButtonItem.isEnabled != isValid { + sendButtonItem.isEnabled = isValid + } + } + + if let currentEditingTag = self.currentEditingTag, previousEditingTag !== currentEditingTag, self.currentInputMode != .keyboard { + DispatchQueue.main.async { [weak self] in + guard let self else { + return + } + self.currentInputMode = .keyboard + self.state?.updated(transition: .spring(duration: 0.4)) + } + } + + for i in 0 ..< self.todoItems.count { + self.todoItems[i].resetText = nil + } + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + 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) + } +} + +public class ComposeTodoScreen: ViewControllerComponentContainer, AttachmentContainable { + public final class InitialData { + fileprivate let maxTodoTextLength: Int + fileprivate let maxTodoItemLength: Int + fileprivate let maxTodoItemsCount: Int + fileprivate let existingTodo: TelegramMediaTodo? + fileprivate let append: Bool + fileprivate let canEdit: Bool + + fileprivate init( + maxTodoTextLength: Int, + maxTodoItemLength: Int, + maxTodoItemsCount: Int, + existingTodo: TelegramMediaTodo?, + append: Bool, + canEdit: Bool + ) { + self.maxTodoTextLength = maxTodoTextLength + self.maxTodoItemLength = maxTodoItemLength + self.maxTodoItemsCount = maxTodoItemsCount + self.existingTodo = existingTodo + self.append = append + self.canEdit = canEdit + } + } + + private let context: AccountContext + private let completion: (TelegramMediaTodo) -> Void + private var isDismissed: Bool = false + + fileprivate private(set) var sendButtonItem: UIBarButtonItem? + + public var isMinimized: Bool = false + + public var requestAttachmentMenuExpansion: () -> Void = { + } + public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in + } + public var parentController: () -> ViewController? = { + return nil + } + public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in + } + public var updateTabBarVisibility: (Bool, ContainedViewLayoutTransition) -> Void = { _, _ in + } + public var cancelPanGesture: () -> Void = { + } + public var isContainerPanning: () -> Bool = { + return false + } + public var isContainerExpanded: () -> Bool = { + return false + } + public var mediaPickerContext: AttachmentMediaPickerContext? + + public var isPanGestureEnabled: (() -> Bool)? { + return { [weak self] in + guard let self, let componentView = self.node.hostView.componentView as? ComposeTodoScreenComponent.View else { + return true + } + return componentView.isPanGestureEnabled() + } + } + + public init( + context: AccountContext, + initialData: InitialData, + peer: EnginePeer, + completion: @escaping (TelegramMediaTodo) -> Void + ) { + self.context = context + self.completion = completion + + super.init(context: context, component: ComposeTodoScreenComponent( + context: context, + peer: peer, + initialData: initialData, + completion: completion + ), navigationBarAppearance: .default, theme: .default) + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + if !initialData.canEdit && initialData.existingTodo != nil { + self.title = "Add a Task" + } else { + self.title = initialData.existingTodo != nil ? "Edit To Do List" : "New To Do List" + } + + self.navigationItem.setLeftBarButton(UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)), animated: false) + + let sendButtonItem = UIBarButtonItem(title: initialData.existingTodo != nil ? "Save" : presentationData.strings.CreatePoll_Create, style: .done, target: self, action: #selector(self.sendPressed)) + self.sendButtonItem = sendButtonItem + self.navigationItem.setRightBarButton(sendButtonItem, animated: false) + sendButtonItem.isEnabled = false + + self.scrollToTop = { [weak self] in + guard let self, let componentView = self.node.hostView.componentView as? ComposeTodoScreenComponent.View else { + return + } + componentView.scrollToTop() + } + + self.attemptNavigation = { [weak self] complete in + guard let self, let componentView = self.node.hostView.componentView as? ComposeTodoScreenComponent.View else { + return true + } + + return componentView.attemptNavigation(complete: complete) + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + public static func initialData(context: AccountContext, existingTodo: TelegramMediaTodo? = nil, append: Bool = false, canEdit: Bool = false) -> InitialData { + var maxTodoTextLength: Int = 32 + var maxTodoItemLength: Int = 64 + var maxTodoItemsCount: Int = 30 + if let data = context.currentAppConfiguration.with({ $0 }).data { + if let value = data["todo_title_length_max"] as? Double { + maxTodoTextLength = Int(value) + } + if let value = data["todo_item_length_max"] as? Double { + maxTodoItemLength = Int(value) + } + if let value = data["todo_items_max"] as? Double { + maxTodoItemsCount = Int(value) + } + } + return InitialData( + maxTodoTextLength: maxTodoTextLength, + maxTodoItemLength: maxTodoItemLength, + maxTodoItemsCount: maxTodoItemsCount, + existingTodo: existingTodo, + append: append, + canEdit: canEdit + ) + } + + @objc private func cancelPressed() { + self.dismiss() + } + + @objc private func sendPressed() { + guard let componentView = self.node.hostView.componentView as? ComposeTodoScreenComponent.View else { + return + } + if let input = componentView.validatedInput() { + self.completion(input) + } + self.dismiss() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + } + + public func isContainerPanningUpdated(_ panning: Bool) { + } + + public func resetForReuse() { + } + + public func prepareForReuse() { + } + + public func requestDismiss(completion: @escaping () -> Void) { + completion() + } + + public func shouldDismissImmediately() -> Bool { + return true + } +} diff --git a/submodules/TelegramUI/Components/ListComposePollOptionComponent/BUILD b/submodules/TelegramUI/Components/ListComposePollOptionComponent/BUILD new file mode 100644 index 0000000000..52d1774caa --- /dev/null +++ b/submodules/TelegramUI/Components/ListComposePollOptionComponent/BUILD @@ -0,0 +1,30 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ListComposePollOptionComponent", + module_name = "ListComposePollOptionComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/TelegramPresentationData", + "//submodules/ComponentFlow", + "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramUI/Components/SliderComponent", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/TextFieldComponent", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", + "//submodules/CheckNode", + "//submodules/AccountContext", + "//submodules/PresentationDataUtils", + ], + visibility = [ + "//visibility:public", + ], +) + diff --git a/submodules/ComposePollUI/Sources/ListComposePollOptionComponent.swift b/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift similarity index 98% rename from submodules/ComposePollUI/Sources/ListComposePollOptionComponent.swift rename to submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift index 6a0986e23d..83b11d55bf 100644 --- a/submodules/ComposePollUI/Sources/ListComposePollOptionComponent.swift +++ b/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift @@ -73,6 +73,7 @@ public final class ListComposePollOptionComponent: Component { public let theme: PresentationTheme public let strings: PresentationStrings public let placeholder: NSAttributedString? + public let isEnabled: Bool public let resetText: ResetText? public let assumeIsEditing: Bool public let characterLimit: Int? @@ -92,6 +93,7 @@ public final class ListComposePollOptionComponent: Component { theme: PresentationTheme, strings: PresentationStrings, placeholder: NSAttributedString? = nil, + isEnabled: Bool = true, resetText: ResetText? = nil, assumeIsEditing: Bool = false, characterLimit: Int, @@ -110,6 +112,7 @@ public final class ListComposePollOptionComponent: Component { self.theme = theme self.strings = strings self.placeholder = placeholder + self.isEnabled = isEnabled self.resetText = resetText self.assumeIsEditing = assumeIsEditing self.characterLimit = characterLimit @@ -140,6 +143,9 @@ public final class ListComposePollOptionComponent: Component { if lhs.placeholder != rhs.placeholder { return false } + if lhs.isEnabled != rhs.isEnabled { + return false + } if lhs.resetText != rhs.resetText { return false } @@ -400,6 +406,9 @@ public final class ListComposePollOptionComponent: Component { self.textField.parentState = state } transition.setFrame(view: textFieldView, frame: textFieldFrame) + + transition.setAlpha(view: textFieldView, alpha: component.isEnabled ? 1.0 : 0.3) + textFieldView.isUserInteractionEnabled = component.isEnabled } if let selection = component.selection { diff --git a/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift b/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift index 0c89c05b2c..85e90bf1cb 100644 --- a/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift +++ b/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift @@ -76,6 +76,7 @@ public final class ListSectionContentView: UIView { private let contentItemContainerView: UIView public let externalContentBackgroundView: DynamicCornerRadiusView + public var automaticallyLayoutExternalContentBackgroundView = true public var itemViews: [AnyHashable: ItemView] = [:] private var highlightedItemId: AnyHashable? @@ -283,7 +284,9 @@ public final class ListSectionContentView: UIView { } self.externalContentBackgroundView.update(size: backgroundFrame.size, corners: corners, transition: transition) } - transition.setFrame(view: self.externalContentBackgroundView, frame: backgroundFrame) + if self.automaticallyLayoutExternalContentBackgroundView { + transition.setFrame(view: self.externalContentBackgroundView, frame: backgroundFrame) + } transition.setAlpha(view: self.externalContentBackgroundView, alpha: backgroundAlpha) transition.setCornerRadius(layer: self.layer, cornerRadius: contentCornerRadius) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index a6b28d8ba4..9c8fd57fd5 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -435,6 +435,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { }, openBoostToUnrestrict: { }, updateRecordingTrimRange: { _, _, _, _ in }, dismissAllTooltips: { + }, editTodoMessage: { _, _ in }, updateHistoryFilter: { _ in }, updateChatLocationThread: { _, _ in }, toggleChatSidebarMode: { @@ -3854,6 +3855,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, playShakeAnimation: { }, displayQuickShare: { _, _ ,_ in }, updateChatLocationThread: { _, _ in + }, requestToggleTodoMessageItem: { _, _, _ in + }, displayTodoToggleUnavailable: { _ in }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) self.hiddenMediaDisposable = context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().startStrict(next: { [weak self] ids in diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index 087b0199bf..a6655583a8 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -823,6 +823,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { }, openBoostToUnrestrict: { }, updateRecordingTrimRange: { _, _, _, _ in }, dismissAllTooltips: { + }, editTodoMessage: { _, _ in }, updateHistoryFilter: { _ in }, updateChatLocationThread: { _, _ in }, toggleChatSidebarMode: { diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index 940c8c781b..a54f61bdb7 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -835,11 +835,21 @@ private final class StarsTransactionSheetContent: CombinedComponent { ) )) } else if case .unique = giftAnimationSubject { + let reason: String + if count < StarsAmount.zero, case let .transaction(transaction, _) = subject { + if transaction.flags.contains(.isStarGiftResale) { + reason = strings.Stars_Transaction_GiftPurchase + } else { + reason = strings.Stars_Transaction_GiftTransfer + } + } else { + reason = strings.Stars_Transaction_GiftSale + } tableItems.append(.init( id: "reason", title: strings.Stars_Transaction_Giveaway_Reason, component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: count < StarsAmount.zero ? strings.Stars_Transaction_GiftPurchase : strings.Stars_Transaction_GiftSale, font: tableFont, textColor: tableTextColor))) + MultilineTextComponent(text: .plain(NSAttributedString(string: reason, font: tableFont, textColor: tableTextColor))) ) )) } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift index 9d29afc635..65e069f264 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift @@ -328,7 +328,15 @@ final class StarsTransactionsListPanelComponent: Component { break } } - itemSubtitle = item.count > StarsAmount.zero ? environment.strings.Stars_Intro_Transaction_GiftSale : environment.strings.Stars_Intro_Transaction_GiftPurchase + if item.count > StarsAmount.zero { + itemSubtitle = environment.strings.Stars_Intro_Transaction_GiftSale + } else { + if item.flags.contains(.isStarGiftResale) { + itemSubtitle = environment.strings.Stars_Intro_Transaction_GiftPurchase + } else { + itemSubtitle = environment.strings.Stars_Intro_Transaction_GiftTransfer + } + } } } } else if let _ = item.giveawayMessageId { diff --git a/submodules/TelegramUI/Components/ToastComponent/Sources/ToastContentComponent.swift b/submodules/TelegramUI/Components/ToastComponent/Sources/ToastContentComponent.swift index 4d7b6b112c..bedee031cf 100644 --- a/submodules/TelegramUI/Components/ToastComponent/Sources/ToastContentComponent.swift +++ b/submodules/TelegramUI/Components/ToastComponent/Sources/ToastContentComponent.swift @@ -9,17 +9,20 @@ public final class ToastContentComponent: Component { public let content: AnyComponent public let insets: UIEdgeInsets public let iconSpacing: CGFloat + public let action: (() -> Void)? public init( icon: AnyComponent, content: AnyComponent, insets: UIEdgeInsets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0), - iconSpacing: CGFloat = 10.0 + iconSpacing: CGFloat = 10.0, + action: (() -> Void)? = nil ) { self.icon = icon self.content = content self.insets = insets self.iconSpacing = iconSpacing + self.action = action } public static func ==(lhs: ToastContentComponent, rhs: ToastContentComponent) -> Bool { @@ -39,6 +42,8 @@ public final class ToastContentComponent: Component { } public final class View: UIView { + private var component: ToastContentComponent? + private let backgroundView: BlurredBackgroundView private let icon = ComponentView() private let content = ComponentView() @@ -63,9 +68,21 @@ public final class ToastContentComponent: Component { fatalError("init(coder:) has not been implemented") } + + @objc private func tapped() { + self.component?.action?() + } + func update(component: ToastContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { var contentHeight: CGFloat = 0.0 + if self.component == nil { + if let _ = component.action { + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapped))) + } + } + self.component = component + let leftInset: CGFloat = component.insets.left let rightInset: CGFloat = component.insets.right let topInset: CGFloat = component.insets.top diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Todo.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Todo.imageset/Contents.json new file mode 100644 index 0000000000..8985bddebc --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Todo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "todolist_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Todo.imageset/todolist_30.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Todo.imageset/todolist_30.pdf new file mode 100644 index 0000000000000000000000000000000000000000..9532ae73d8bf9312021767747729a159d54c151a GIT binary patch literal 4046 zcmai1c{r49`!@C_5=kLEQrV3$W*BA3z8kU@Sx3e)W0_&Hgb-yZWZyzcB2i=;WE2`@ zDcQH^%@SEMmdMZaR`1vE{f^_i|9GD3IPd#9@B6x+>w1p!I*HQMmWIkGfWc5e7Qi{V zg8|^o831zD6OZ=AI^xj)L=)|gaYmczsP9ef4Tv^|Hn;co+?&=!JL6o?KW7Zl*h_df zKn{*j_=y+;ususf1?-3SMLT+diP=@D7}TLbo;F+nHC`G7vT13&W|T@-c}@Dx)iBQj zt|U6geFwRk?Kn-tIkhwyZPl2yj(Ov)T@G@ZjxaMZncH4v1HV#KyumL~&LD)JGwOS2 zRX)~KQXd*Jmp%L`_gnrh-7PGGrmSxf>x%*hdy_y-7jA)d#(f;ukA4);d1Y*pxCoFXOBK0U$TF}$&x zBaD&toTo17Z|Dk`BGLqGoNQFY)l>+9x!vunz2 zgJ4+Ek^zyA33cK&SfAyE8q`$E9FPDXj^@foc7cP4rxwa8Y1#HaHxPGt)C_gmPMBZHm+>@ZL^&XcTn>_&d5cKtcpCY z&YmCd9V@GyE-b+nA0yL>aysrKSrIp&)?gT{8?13W5wDkU_u~l=X8(gYd?r3cUVTf~ z@_1H~&9qx(WJ6YP_K04i$rX)!6r1?sZLu5am04ywwVIa=HvyfLC%2p_%hL#oMLV3b zMj<7Mw)3|KwU?*bi`q+sT7FO6zDs}hIV!xU*>vD8D&O1KQP=T&Vu@SX(c<(1>tdqW za^{B`F5?bV$A#l8S`a-C@zS@^ z8?;0kA}`dcZ#Gn#GZi@%w^-^~ikl$|<%(A@;eXNJWZGt`=aHA1v0u7K$S6Jd**wydi&qomo%Miy0{w-dLD+k z<>1|Xr)`3ipXgXKd4SAA_7SlVsqRrK3@nU($?dE#g`aYtsy}6Y=$>9uhj8-ny~hu7 zFjp~Av(m7E&M`v=|Mt?ZW6q0iGVKQSsm^5WzcjX zb!g%7eC0Q%FAk7O+~Yj&?tmDdV*F93-t@Db>VH}2#1-D3a&7Z(yJ<`{cDfHU4po9> zCuf@|-Gm^{ciP)NA->b;V#&)W6%2kRLtRjYPihG(Of_b(04 zE403M@f#$377u0FjsHFhnhv`awi0&Vz%VT)^{I4J+7@K=+1E+A2y_ctIvltqPy<;q zy=96D>@KHNP$E*Tq(XT-1sa5FL?ihsPffeD*Q2_+3%Vx{zmz;7S=Ta6E+Egfj70}U z9f>v&krF{@kHnbYB-PMWO%mw8@$x%YB~QKln;UvjnlQO`ou%QN3KA3FJ~Lr`iu}847M9L zt}{N1?2Xg|8K7i{IWuX*6U!lH4!z5F2d9^&UvdZQ8@@v3C&}s!-GKG^4~#`{q2{0sSM9p7_KQiC$Q9r)L!?c28wrxFkspcm3Z{K{sB>^$1_>ZardG;;+UY9h|ACa6j}^-2Ys^E9rG@R$KK37Fm%}1INtcke0|7NZhk1 zoCl?^b2RC6-Dp5iNUm+J7|tzvH9_Qb&uOO9K>^Z31n>U-;EUha=pSC~z1kHu(lW1V zy^ErfKQGpdlC-E=(d!QDtm|xxgyEn9>k6XX@=RmVw@ibXlba`T>*a%?W$&uLwu3%^ zIzX7gq{{aZTP*9xwrz7!DyT`D9_t6DwGUTzgvau~lNY1;6Wj#F6<219CuT?z<0XH& zljnM!2b?q7a{D6tzHYZ~S3I&LRo|gfd?tNnuTLBrB^)3SMB&CfedM*It?TD{?qheS z*Uot`6y}$h7FA5hD@r4GK72~gW9eedo>iip8D5Hg4oNY3cW>LZ__Eikxq*7@Q+Q?7 zm+alzM~F|=_w1#wD33jQd(r3OX0Rzsg~&5!YD?Yzw>g-c1sPke^ZO|CZ81I^%@7hH$^3;~i$m|r@?!kBN#pL;*}oz(CZ#!`9IHa& zXCy*Omut4JcO+{+Abs7A>2H61U)UsCr?wCh+O@H>YP|@1^`!UHP}lrRub0E3!KbiI z)|*bB(L(8QK|Ci99=z#}sN6 zaJ1ItqDR{^Yj-$ZCx#9cB*(by2etGT+o?y&UfuKw=CqITgc~+@aj{M%?(DxzUjbO? z9$}pg<$t@|xY{&-A#c2)B0Wz!cOC}!D$_lf2i$uS<-k(eA5VJq_d)LPAeE3%qSTggtSYc$aHx9?@aNmJtAN< z*5#)n+_Pw^6D;>ze*Tb;zhKyJ_yPdY)6h_N^h3JKh}IjPM;j5?FVY zZ_=YFSsnm{WkQ0>uFXi*V&2FpRc14KD!$EVCwyt7RdmR}-GnM8u52Tr+WtAO1t%vn z8uEZdZ!dxlL=1Gr!m_R?Gk5ty^}vmDOCzU(MkcISTzBhD(&9dfdKy;Eek-aU$6A2x zFN_#SuryTX^!DfIF6^It$K5njxGj0+jt}e56N@igfx(bM{bcN!PtFM5$HNbyuL}<* zKBPRqi$n%Lz9f{b1y_2q|E@!7xP}muim-`kJa3nq`Z+%312ZA7vQYT$Wl0oBG&Hgc_U(2Dh0t#Z^n>O6lSF=H4`W3 zmn3SbB+!N=WP)g9IoeHS* z=Hb=PXGaj!{ERk{&PTOfgcut{Rzn-kb4KEjej5oxqq!vfbYfl z=PJd2alzza|DOvc58n&v4@VdRw)b266H<`f%YvT}-q#W1iS`B4zPq00-bIS};k?k| wfEdO_{O{|8nBj1EfOb_sJ`Lm?CJ;@F`$tFZ=!^ex6EFn?9DMSmmWlTN0lxDBi2wiq literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/TodoCheck.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/TodoCheck.imageset/Contents.json new file mode 100644 index 0000000000..e801cb50df --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/TodoCheck.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "todo_check.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/TodoCheck.imageset/todo_check.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Message/TodoCheck.imageset/todo_check.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2c72316d33cc37cbc081bc20f665f1d4e7bd58e7 GIT binary patch literal 3906 zcmai1c{r3^8#h^-NJZB0pzMsn43m;QvJJ_;4Kp&fvCL$$WDUJiBKsgDOC*Zy+sM)= zOUb@Pip6hp?Kx*sCLgkdfU??CD;GEsT z0C4IQ0MYQoWBj~Pcnkp1#$3X>V9d^HQm3c`qKl=?QQw}_X>E)P4vqOaV~Fv-h<5`N zU~uK1h%ul@wbazW{&+tO$_q>+Ri$8&93xzvxPbL|SrEv&ttr|lg|0GM_D)2YXWo%S zI@JC{M_TL;n}#3O(PpqyXVN+5gSSE-I&3=5#K>rF7r_dCrlRr}zf?KB5dNdl;C;*T z$>x&!(2$R$vG>{AxqEa8-t^k?eg!N~^PC(_0=3bc0vinbyCV_4HN4pADaH_TK;#N8 zfTyxPZalq{ZTleJyq$lY>xgyZ9&)>pleohwS4rY-8@HO9Wyw)b`^(8oGQNn?EbgTG^WPrwwODq!twttP3NLN?;)R^df4d+%fJ79IpV)u#4HEWBNGUZtg8d*tjr zkM0-f?A4q5fSd#BE3yx?ZzF`XDBe8k+Q!$JUWIY>(RYM_Jj3XHm=;_3HzU;#fkj%~ zTNw8PjIrum!*taVr;v2(VY_#Z9RtDc98?NZyu9AA?4jj;cRJBFq^FHO^?gzZ2I7 zD`Kx>k5}jDbLH z`(C)6Sp_R`XWf{LlG9GjH@TD9Kk-5J!{JrVE$Qfp{+2WpN#4_tK};~|YB%||vpdu% zjgkiH;dv*(6?dVP4<+#M%CMfN$feWOhk{tfVn4U9qs~^mR5@nMb}`z&mGO;=3RI3g z;7|ZK05o)ypB&PX^mUZT-@vX0hP1z*Hdqp064<=5e!Bf@Zm7*!P|qREyU z?-M7ln-3j8S?hn_DGrWjsY`c~(V4v!#qT zHAjrQk=?e}3UA-X8_p*VNF3D}%&#o6`v`YuTT>uhk1pWtAAYQD1Ntm(1Mxa%tv_Oc zFhtnaYJO{|G-oVuE^4#Tw~#bLXSY=93 zUrXY%e{L(Q?^SLSH!4baTvxp1wagmlII0i(Sf{;nQ8_3 zxwD0${lx?GgX#US0sGv{{IQ~Ky_-dyui{=x_b&`&4h+3^ENRwgcGwFCwT596oP1jD zbWS(s-a^&P93*p*eMK*bR`;LG56qA0;B--*#m~CW){9wj+|y6&7ET(w_pmq%8-cw# zFRM7*Gim5_sjIa2n9HJ@oKiVl$T@*m@VcSyX_t1*r_R@OZs^24l{N^WNKhJ6PNz7Q zeJV{Xb1#i9OD^3l^DiAM-K&IF&5W84kK1co&pmQm$$PC+?)BXBTXl@rml4y?DWjhs zE>vzie{q6T;vVMs^aaHF7U6lE2hucpG~Zn~dp*B!*0u9e=S|~vW9LFe79BjPKap{uiIZ$$_N^&K^Wgyg#U8sG7fiR`+C+LdvSiJYJ`;_*PzGVv|; zt>m)ja&WJovlp5WnA;HCxZD`nnA~7VObt4--oJKjqj)2IxoXA!%h-I((t)pI3(D=U z(f%WZPm-a`d-2~VKyzUUVas8K28OAzDUW5Zrfxwdo~+I&L}J=7vf)62Kn>)pX@V&- zu&|uc1RzWu4|hk=aE0QO~wRW<&H5B zl@W#Oj>nqc?boScM=llliXkO>B5l3{c>Z6kSp;!d@nK{ zLDSN{rX@jh5aA^E7kC-fYK2RGQQinnH_g?qoFj4sb6*d86f3|Sszk~$CpCPoCcQej?8_X>);GNXZQ?}n<%e8oK=#Xku{p?nv#+#W)yI4 zDo5=E_|TxMI`4f2o2N>1XZ_AQ%Z$kJcH^IycI_k_O(DQ-p|)uu{$=x{_8vLqE7lvc zfvbUwTMyVy@>hHFjC`o5aOZd|dFlL+Yvc3U%+BgfZ$w3M4Gg=0Ls%f5B5+Tx;yfsW zJrjwlbrS(WA=!4>5;(V*l^dd}{i=+rK>@O(1fQXyV2AH(`|d{!MD$)AZ(BHHwTE0M ze_E`WXw+HPiP>=4VA)_@B#Z^+Syd42mp(KVY-bpJIPvWSZlio8wCq*&Y8U7Ys2hYG zNvwPwxy8J3Y{xDesfL`f?zbv7t-ZgzD?FL|oxB*sf5S~sQe}C*X!=8=)Ktkkck;&p zmtmLm&g{Xc!PT9vor(t*jn#M7DZVql^Vg<1CI|-!1aX)#*C2TSSB&a2jhaVxU%&F08Vjb(SS)yTj(LT| zZJ*ql*n&(h)%m}b+g^+h$IypFN;7?7(_z>Dva}dKWzw{_rSUE@eMXiY%Dy5bc}gmz zbg5?RT6dCeapUSv>`>S9LSd7bv$gq<(B9466{|(XXO9NnkM=Hfcy)}42a9<(TYYo> zgc+rzcQ>5j+$##k?0if;GqKlzI0}WW+pGqzm$xaq<8Cxpg^a-jly*1mpVg;td=q-5 zM%wJ!8D5dxdAX8a+FR3$aWrgt9KyD_HGl0VeKM`E`gAs_M0H~6q)tS8K{rS}NS?5*7cm-kfw7xcJU z=0o{k?lrA6FWBZxHB_YK$Yw7n!o14#4&?y%9$j^A((AfKXxGnH6}VN}dENY$6`&6- z6g3_=7Od$?kd41ZAMJ+}DztH!_l5GBN_`l0lN`@aMiW#Iiioao70HUv@rHS&#paGgzH3RHvG(Vh`ey^fwS1p_;&hNYr z$_4X7%4$IY8jBfKhSHSiFCW++KL1zL!f6zLYFSXcnTGc<4VH!t=`H#WHl;q&ZiIvh zkK{9ym*nzZ0jEn4`pfkby_k`_10`Xd;)^EDW1?58ht|uX@w*GNqC}NEQE?%VA4!TY zB*?q7-v4V|BOuU<8i+Sf%nCBS$C}A1(}OQ7kq|vi-{;Jo0IIcl#-J4P?pae6bF43l zpDrD?&s>a4e@G20S5|qaNjXbN=JwH7^r}(gqf_q~o<&3eIp+x)4u?Dr*4Va_{Sb-t z{dVe_GUR%W?_y{%HggUw=E8~uE~gTarF*9s%8slnrGQX5J!Z-GT3TP!4OT5G_c&e8tkUIGV{?)9s0FMRN4Jc6oJQeWxdJ0Z-N7D&13DyW?`v zHqS%F^tvWo^&o7bWio+CUN|aVVv<^xO=Z(jB zQ+K4*;4ct~@}pva%1`LODwPKP)0r7y(bP}@S_ZUF;1?r->I?l>{QgwrznBnzFwqk9 z|GgnCu)h=6Lg7)KxQljbH;0S7< zwDAw5tnjZ@$}nnKzqk|?q12TAa8y)6P=6(VLNGYB3VuR(KNQvz;|HeQ zyS_H{ASL{9UKmM00*jXX`#2$HI2<0J9o3Jkft<$%VrY5)=weZR_#Y>s2v>lCPn^&( G(ftn&Nwtmu literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/TodoDot.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/TodoDot.imageset/Contents.json new file mode 100644 index 0000000000..3e11632391 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/TodoDot.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "todo_dot.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/TodoDot.imageset/todo_dot.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Message/TodoDot.imageset/todo_dot.pdf new file mode 100644 index 0000000000000000000000000000000000000000..618b27e745ff225056bcda81dd1060367058b06a GIT binary patch literal 3832 zcmZu!c|6nqAGa7r+3O@5nL_3iimV|zZHuj~DKz24j7^*Vsk)|G|HDMKJIKpr4CxIh5l=urTw z;YPxG;n5^40M*8N<1kp$6PgSW<3M$BOc~?bjUm>?VhE1db%`Msf1czFD8LcQ>x?m= z$Utgp5F*J7i*|=lQp@9UsGYq6O$49$NLdibs;`KLd|UMg@x{`)bv%?mKToc zv)4BP8C%q6WbbL;MDEj~;diTR8(-sm6(rEk{wxUO7R2tsIoTkx7^=PvBL3K=0lXOi zUr`t6VXFu^iej4&TDi4n4+ws1t5T5SEpSZWd{bm)|)b#6FCo?ApVZOPv_Dr6}8So}uOrJ1vU)v+oVc`DQ>7 z2BY@hgq-Ahstz-eF$bg}yTW&5A~L5#$VXWpqNZ{e|=K zS%{KgTGUXOoOVL?saq)>gCA8tZlC2}K72W(qahKUD(_~$>rt;Y^w;y(62C#xkGlPCKM1!a{~9To6sM%QtY;yf5@R*y zTpCuJ;-5O8Uw`VNRwjx^YVzixe-cYmOixs4Uocz(PQ*Qk#?Yq55VX_HaM~QLE=ICh zq}i{zINn~;UOLd?M?C%ZCXL6a;G71Np4+HQ4`Z|*+WJO;bJ6a+#4O7^is@AH+sYlr zEvOcotGPGtk_^XVIuGsA>B=t6v;Bl{;hj?;U%Q+m)X`I>Z4LS&X$^HhX{Fz5fiy(g zRB0~NmYRce9P%11^ev=Jk=Y7))41UGo34ZD;2HrXnUD70&XO|=YNYMq_T?sI^|@GK zyXQ8t`tHTn5q%Qmvg-V0uQ{1H>ZgP*Nx@EmBhWFqR(p!m2s^JO)m*|7Q}+xOnIil$ z**dvC8JEljhvJM~@|~+(54glRlbwy6&C2b{!F>n%1k3E(s`-ldKK~VaK}VvN@w?L`+`RrkIt?JgB}OrqR)9W+-e%G z%e;ZE9N9`0pn6K2mZ<1BlI@!v{)``^JW3jM8Lc^Jx$};GOp91-|DF5!X}A#FrEyus zp4K75bKcE`ZF?}2&T>k{hK?;9(R}J~RVPX(;^|?70NNp1UHq|l`=ZZ< zF-0zgmy6;GSBi**{e`Qgu=0^U^PT}aZL6^d&NErBb&B1eyDe3OyMOI9`4Zpv<^DwJ zcZaX%prwTS86NFES3L7byB#_cHCi>_pFVLdJ9pHn$-C*g@w~A^uA*_^5yjNl)Kf>U zLlM@k_O=fwuXMUEjhem_C+E~WtK3VDs~)fP94Ht}ub!xy8E_rU@JpoZ_f4Iqyu-bd zns%G^Z}W0+cO?5}*80~?*ZJ1P)t;dw_#L0`n7g`=zmPOtK4bT_e>{9@%eVds*xx)_`8Wc$MQ} zSZA0%$N(irN&A>U**_IvdaiTocJJ7?v1k1LCk_8aX2!_t_oNlXC8zZzIK{^&95nJd z`6ff{0AyR2le*Ad1?#6uV<)`K9AtXsgjz_?3!CZDyW+_R8<W}888K`kI$ zZ%pay&}FWLJ#^c2lp1Qps>3qhr0VYUir7%*59(yNNR;zlDV6E*yy1^^(r*ghyHG!M zVtO!1P3c`>U9WJ$QOKo@l`X8EdFM=pMKY|hM; zJv+0&B&TFpNkta9^7cbw23H$L>i7}b(f)4{kD+l!ukO&D@-Dc~m>XzDl)+2OzoxEM z-9vn+xMMGS@z~J4muEfCF8P~ql}J3q%r{nVd6|YwixnJ&MtK*F3)k~|j^h`mCx>XD zgyOSPRbKf`*&iBDC*1wgoq8{==0)w07xjnkj8RRB(WBm#*RNdP^!w9BEGFEW`rQK} zz8_g0T!s!!RTJOIeV>dB#AuXHy*&lNirexfQH8rZhDGGjTZ`0sAqNCAHKZ=U>h9$I?F~93Nb*Mec&Z=dEXb=ZhPaT?kQ+$^-i0qDm`^cK_BSEiCPO zrIxzbOz)YIqQ9I;Dr~E4!`d6xmj&=HE_)!b-t($_4v*)ZI=&Q%e)%GV;Bl=MJRT*X zFYprkA}TI9ReF1eR;P-sTAD^eQrv3FsjJ7h)oI>r=KK63#30Fmt$w9Qjo}P%;2EFY zRgPy}n;u%az}Z@HY$!hkjwvF{!sF0N4N^X2$q_-^txbxtWL%m+}4e}HHbXCzCg-zGYZ&(8Q zuv`h_&aFY$Ds>P<0}aO~5}5SX7xgd>wlkX=60&-GlL~r!q0KTVDjVtK4{#)RhXe$R zcU+$c8HhP`LKIlz=i2zVK`XLo8vv@WrKO1`VjThItZE9_G2N^gE3vegv#S=tjo@YG zfyQ9hMp-Qw!1Q9u7(h74I?FAF5o%qX z#r+2UD`Gid+N~$|TbOnAzpuw)Fc6kHi(`SmMZd*!hB!g+5EID%c@4}y{9Hy9;mWL* zDYMwM6$Otckcfbd-TJO!e!+vWt)S57H`Wm&tRoJsNuU5Wzv{)x5%z!3h9^@jkwEss zV2J?pVVSP`*COCaSUe*l^W|)iD6|)Y160Ep$Lb=AO{ZUoYMUt?^!bc literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index 3e64d87570..a924ac308f 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -21,7 +21,6 @@ import DeviceLocationManager import ShareController import UrlEscaping import ContextUI -import ComposePollUI import AlertUI import PresentationDataUtils import UndoUI @@ -4127,6 +4126,11 @@ extension ChatControllerImpl { return } self.dismissAllTooltips() + }, editTodoMessage: { [weak self] messageId, append in + guard let self else { + return + } + self.openTodoEditing(messageId: messageId, append: append) }, updateHistoryFilter: { [weak self] update in guard let self else { return diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift index 733ef38465..8e0cb7ac42 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift @@ -595,7 +595,17 @@ extension ChatControllerImpl { self.dismissAllTooltips() let insets = layout.insets(options: [.input]) - let location = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.right - 42.0 - UIScreenPixel, y: layout.size.height - insets.bottom - 122.0), size: CGSize()) + + var screenWidth = layout.size.width + if layout.metrics.isTablet { + if layout.size.height == layout.deviceMetrics.screenSize.width { + screenWidth = layout.deviceMetrics.screenSize.height + } else { + screenWidth = layout.deviceMetrics.screenSize.width + } + } + + let location = CGRect(origin: CGPoint(x: screenWidth - layout.safeInsets.right - 42.0 - UIScreenPixel, y: layout.size.height - insets.bottom - 122.0), size: CGSize()) let tooltipController = TooltipScreen( account: self.context.account, diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index ec5bd31af6..1ff6f9702b 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -22,6 +22,7 @@ import ShareController import UrlEscaping import ContextUI import ComposePollUI +import ComposeTodoScreen import AlertUI import PresentationDataUtils import UndoUI @@ -342,6 +343,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var selectMessagePollOptionDisposables: DisposableDict? var selectPollOptionFeedback: HapticFeedback? + var updateMessageTodoDisposables: DisposableDict? + var resolveUrlDisposable: MetaDisposable? var contextQueryStates: [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)] = [:] @@ -4766,6 +4769,68 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return direction ? .right : .left } self.updateChatLocationThread(threadId: threadId, animationDirection: animationDirection ?? defaultDirection) + }, requestToggleTodoMessageItem: { [weak self] messageId, itemId, value in + guard let self else { + return + } + + let disposables: DisposableDict + if let current = self.updateMessageTodoDisposables { + disposables = current + } else { + disposables = DisposableDict() + self.updateMessageTodoDisposables = disposables + } + var completedIds: [Int32] = [] + var incompletedIds: [Int32] = [] + if value { + completedIds.append(itemId) + } else { + incompletedIds.append(itemId) + } + let signal = self.context.engine.messages.requestUpdateTodoMessageItems(messageId: messageId, completedIds: completedIds, incompletedIds: incompletedIds) + disposables.set((signal + |> deliverOnMainQueue).startStrict(next: { todo in + + }, error: { _ in + + }), forKey: messageId) + }, displayTodoToggleUnavailable: { [weak self] messageId in + guard let self else { + return + } + self.dismissAllUndoControllers() + //TODO:localize + if !self.context.isPremium { + let controller = UndoOverlayController( + presentationData: self.presentationData, + content: .premiumPaywall(title: nil, text: "Only [Telegram Premium]() subscribers can mark tasks as done.", customUndoText: nil, timeout: nil, linkAction: nil), + action: { [weak self] action in + guard let self else { + return false + } + if case .info = action { + let controller = self.context.sharedContext.makePremiumIntroController(context: context, source: .presence, forceDark: false, dismissed: nil) + self.push(controller) + } + return false + } + ) + self.present(controller, in: .current) + } else if let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { + var peerName = "" + if let author = message.author { + peerName = EnginePeer(author).compactDisplayTitle + } + let controller = UndoOverlayController( + presentationData: self.presentationData, + content: .universalImage(image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/Lock"), color: .white)!, size: nil, title: nil, text: "\(peerName) has restricted others from editing this to do list.", customUndoText: nil, timeout: nil), + action: { _ in + return false + } + ) + self.present(controller, in: .current) + } }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: self.stickerSettings, presentationContext: ChatPresentationContext(context: context, backgroundNode: self.chatBackgroundNode)) controllerInteraction.enableFullTranslucency = context.sharedContext.energyUsageSettings.fullTranslucency @@ -5819,6 +5884,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.updateChatLocationThreadDisposable?.dispose() self.accountPeerDisposable?.dispose() self.contentDataDisposable?.dispose() + self.updateMessageTodoDisposables?.dispose() } public func updatePresentationMode(_ mode: ChatControllerPresentationMode) { @@ -7549,57 +7615,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.present(tooltipScreen, in: .current) } - - func configurePollCreation(isQuiz: Bool? = nil) -> ViewController? { - guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { - return nil - } - return createPollController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), isQuiz: isQuiz, completion: { [weak self] poll in - guard let strongSelf = self else { - return - } - strongSelf.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in - guard let strongSelf = self else { - return - } - let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject - strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ - if let strongSelf = self { - strongSelf.chatDisplayNode.collapseInput() - - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) } - }) - } - }, nil) - let message: EnqueueMessage = .message( - text: "", - attributes: [], - inlineStickers: [:], - mediaReference: .standalone(media: TelegramMediaPoll( - pollId: MediaId(namespace: Namespaces.Media.LocalPoll, id: Int64.random(in: Int64.min ... Int64.max)), - publicity: poll.publicity, - kind: poll.kind, - text: poll.text.string, - textEntities: poll.text.entities, - options: poll.options, - correctAnswers: poll.correctAnswers, - results: poll.results, - isClosed: false, - deadlineTimeout: poll.deadlineTimeout - )), - threadId: strongSelf.chatLocation.threadId, - replyToMessageId: nil, - replyToStoryId: nil, - localGroupingKey: nil, - correlationId: nil, - bubbleUpEmojiOrStickersets: [] - ) - strongSelf.sendMessages([message.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel)]) - }) - }) - } - + func transformEnqueueMessages(_ messages: [EnqueueMessage], postpone: Bool = false) -> [EnqueueMessage] { let silentPosting = self.presentationInterfaceState.interfaceState.silentPosting return transformEnqueueMessages(messages, silentPosting: silentPosting, postpone: postpone) diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index ccd523a091..c9a0000ce1 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -33,6 +33,8 @@ import AutomaticBusinessMessageSetupScreen import MediaEditorScreen import CameraScreen import ShareController +import ComposeTodoScreen +import ComposePollUI extension ChatControllerImpl { enum AttachMenuSubject { @@ -113,6 +115,8 @@ extension ChatControllerImpl { availableButtons.insert(.poll, at: max(0, availableButtons.count - 1)) } + availableButtons.append(.todo) + let presentationData = self.presentationData var isScheduledMessages = false @@ -622,6 +626,26 @@ extension ChatControllerImpl { completion(controller, controller.mediaPickerContext) strongSelf.controllerNavigationDisposable.set(nil) } + case .todo: + if strongSelf.context.isPremium { + if let controller = strongSelf.configureTodoCreation() as? AttachmentContainable { + completion(controller, controller.mediaPickerContext) + strongSelf.controllerNavigationDisposable.set(nil) + } + } else { + var replaceImpl: ((ViewController) -> Void)? + let demoController = strongSelf.context.sharedContext.makePremiumDemoController(context: strongSelf.context, subject: .todo, forceDark: false, action: { + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .todo, forceDark: false, dismissed: nil) + replaceImpl?(controller) + }, dismissed: nil) + replaceImpl = { [weak demoController] c in + demoController?.replace(with: c) + } + strongSelf.push(demoController) + Queue.mainQueue().after(0.4) { + strongSelf.attachmentController?.dismiss(animated: false) + } + } case .gift: if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer, let starsContext = context.starsContext { let premiumGiftOptions = strongSelf.presentationInterfaceState.premiumGiftOptions @@ -677,12 +701,12 @@ extension ChatControllerImpl { let _ = self.context.engine.messages.acceptAttachMenuBotDisclaimer(botId: bot.peer.id).startStandalone() } let _ = (self.context.engine.messages.addBotToAttachMenu(botId: bot.peer.id, allowWrite: allowWrite) - |> deliverOnMainQueue).startStandalone(error: { _ in + |> deliverOnMainQueue).startStandalone(error: { _ in }, completed: { [weak controller] in controller?.refresh() }) }, - dismissed: { + dismissed: { strongSelf.attachmentController?.dismiss(animated: true) }) strongSelf.present(alertController, in: .window(.root)) @@ -1967,4 +1991,147 @@ extension ChatControllerImpl { mainController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) self.push(mainController) } + + func configurePollCreation(isQuiz: Bool? = nil) -> ViewController? { + guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { + return nil + } + return ComposePollScreen( + context: self.context, + initialData: ComposePollScreen.initialData(context: self.context), + peer: EnginePeer(peer), + isQuiz: isQuiz, + completion: { [weak self] poll in + guard let self else { + return + } + self.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in + guard let self else { + return + } + let replyMessageSubject = self.presentationInterfaceState.interfaceState.replyMessageSubject + self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in + if let self { + self.chatDisplayNode.collapseInput() + + self.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) } + }) + } + }, nil) + let message: EnqueueMessage = .message( + text: "", + attributes: [], + inlineStickers: [:], + mediaReference: .standalone(media: TelegramMediaPoll( + pollId: MediaId(namespace: Namespaces.Media.LocalPoll, id: Int64.random(in: Int64.min...Int64.max)), + publicity: poll.publicity, + kind: poll.kind, + text: poll.text.string, + textEntities: poll.text.entities, + options: poll.options, + correctAnswers: poll.correctAnswers, + results: poll.results, + isClosed: false, + deadlineTimeout: poll.deadlineTimeout + )), + threadId: self.chatLocation.threadId, + replyToMessageId: nil, + replyToStoryId: nil, + localGroupingKey: nil, + correlationId: nil, + bubbleUpEmojiOrStickersets: [] + ) + self.sendMessages([message.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel)]) + }) + } + ) + } + + func configureTodoCreation() -> ViewController? { + guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { + return nil + } + return ComposeTodoScreen( + context: self.context, + initialData: ComposeTodoScreen.initialData( + context: self.context + ), + peer: EnginePeer(peer), + completion: { [weak self] todo in + guard let self else { + return + } + self.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in + guard let self else { + return + } + let replyMessageSubject = self.presentationInterfaceState.interfaceState.replyMessageSubject + self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in + if let self { + self.chatDisplayNode.collapseInput() + + self.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) } + }) + } + }, nil) + let message: EnqueueMessage = .message( + text: "", + attributes: [], + inlineStickers: [:], + mediaReference: .standalone(media: todo), + threadId: self.chatLocation.threadId, + replyToMessageId: nil, + replyToStoryId: nil, + localGroupingKey: nil, + correlationId: nil, + bubbleUpEmojiOrStickersets: [] + ) + self.sendMessages([message.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel)]) + }) + } + ) + } + + func openTodoEditing(messageId: EngineMessage.Id, append: Bool) { + guard let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId), let peer = self.presentationInterfaceState.renderedPeer?.peer else { + return + } + guard let existingTodo = message.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo else { + return + } + + let canEdit = canEditMessage(context: self.context, limitsConfiguration: self.context.currentLimitsConfiguration.with { EngineConfiguration.Limits($0) }, message: message) + + let controller = ComposeTodoScreen( + context: self.context, + initialData: ComposeTodoScreen.initialData( + context: self.context, + existingTodo: existingTodo, + append: append, + canEdit: canEdit + ), + peer: EnginePeer(peer), + completion: { [weak self] todo in + guard let self else { + return + } + if canEdit { + let _ = self.context.engine.messages.requestEditMessage( + messageId: messageId, + text: "", + media: .update(.standalone(media: todo)), + entities: nil, + inlineStickers: [:] + ).start() + } else { + let appendedItems = Array(todo.items[existingTodo.items.count ..< todo.items.count]) + let _ = self.context.engine.messages.appendTodoMessageItems(messageId: messageId, items: appendedItems).start() + } + } + ) + controller.navigationPresentation = .modal + self.push(controller) + } } diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 6b3cff7b36..ef44aefeca 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -790,22 +790,22 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto self.prefetchManager = InChatPrefetchManager(context: context) self.adMessagesContext = adMessagesContext - var adMessages: Signal<(interPostInterval: Int32?, messages: [Message]), NoError> + var adMessages: Signal<(interPostInterval: Int32?, messages: [Message], startDelay: Int32?, betweenDelay: Int32?), NoError> if case .bubbles = mode, let adMessagesContext { let peerId = adMessagesContext.peerId if peerId.namespace == Namespaces.Peer.CloudUser { - adMessages = .single((nil, [])) + adMessages = .single((nil, [], nil, nil)) } else { if context.sharedContext.immediateExperimentalUISettings.fakeAds { adMessages = context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) ) - |> map { peer -> (interPostInterval: Int32?, messages: [Message]) in + |> map { peer -> (interPostInterval: Int32?, messages: [Message], startDelay: Int32?, betweenDelay: Int32?) in let fakeAdMessages: [Message] = (0 ..< 10).map { i -> Message in var attributes: [MessageAttribute] = [] let mappedMessageType: AdMessageAttribute.MessageType = .sponsored - attributes.append(AdMessageAttribute(opaqueId: "fake_ad_\(i)".data(using: .utf8)!, messageType: mappedMessageType, url: "t.me/telegram", buttonText: "VIEW", sponsorInfo: nil, additionalInfo: nil, canReport: false, hasContentMedia: false)) + attributes.append(AdMessageAttribute(opaqueId: "fake_ad_\(i)".data(using: .utf8)!, messageType: mappedMessageType, url: "t.me/telegram", buttonText: "VIEW", sponsorInfo: nil, additionalInfo: nil, canReport: false, hasContentMedia: false, minDisplayDuration: nil, maxDisplayDuration: nil)) var messagePeers = SimpleDictionary() @@ -874,14 +874,14 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto associatedStories: [:] ) } - return (10, fakeAdMessages) + return (10, fakeAdMessages, nil, nil) } } else { adMessages = adMessagesContext.state } } } else { - adMessages = .single((nil, [])) + adMessages = .single((nil, [], nil, nil)) } let clientId = Atomic(value: nextClientId) @@ -1223,9 +1223,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto self.beginChatHistoryTransitions(resetScrolling: true, switchedToAnotherSource: false) } - private func beginAdMessageManagement(adMessages: Signal<(interPostInterval: Int32?, messages: [Message]), NoError>) { + private func beginAdMessageManagement(adMessages: Signal<(interPostInterval: Int32?, messages: [Message], startDelay: Int32?, betweenDelay: Int32?), NoError>) { self.adMessagesDisposable = (adMessages - |> deliverOnMainQueue).startStrict(next: { [weak self] interPostInterval, messages in + |> deliverOnMainQueue).startStrict(next: { [weak self] interPostInterval, messages, _, _ in guard let self else { return } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index a0e51dbbc2..a77786f6e0 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -1479,25 +1479,33 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState isMigrated = false } - if data.canEdit && !isPinnedMessages && !isMigrated { - actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_MessageDialogEdit, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.actionSheet.primaryTextColor) - }, action: { c, f in - interfaceInteraction.setupEditMessage(messages[0].id, { transition in - f(.custom(transition)) - }) - }))) - } - var activePoll: TelegramMediaPoll? + var activeTodo: TelegramMediaTodo? for media in message.media { if let poll = media as? TelegramMediaPoll, !poll.isClosed, message.id.namespace == Namespaces.Message.Cloud, poll.pollId.namespace == Namespaces.Media.CloudPoll { if !isPollEffectivelyClosed(message: message, poll: poll) { activePoll = poll } + } else if let todo = media as? TelegramMediaTodo { + activeTodo = todo } } + if data.canEdit && !isPinnedMessages && !isMigrated { + actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_MessageDialogEdit, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.actionSheet.primaryTextColor) + }, action: { c, f in + if let _ = activeTodo { + interfaceInteraction.editTodoMessage(messages[0].id, false) + f(.dismissWithoutContent) + } else { + interfaceInteraction.setupEditMessage(messages[0].id, { transition in + f(.custom(transition)) + }) + } + }))) + } + if let activePoll = activePoll, let voters = activePoll.results.voters { var hasSelected = false for result in voters { @@ -1515,6 +1523,21 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } } + if let activeTodo { + var canAppend = false + if message.author?.id == context.account.peerId || activeTodo.flags.contains(.othersCanAppend) { + canAppend = true + } + if canAppend { + actions.append(.action(ContextMenuActionItem(text: "Add a Task", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddCircle"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + interfaceInteraction.editTodoMessage(messages[0].id, true) + f(.dismissWithoutContent) + }))) + } + } + var canPin = data.canPin if case let .replyThread(message) = chatPresentationInterfaceState.chatLocation { if !message.isForumPost { diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index c1f3276d74..fc0892252c 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -194,6 +194,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu }, playShakeAnimation: { }, displayQuickShare: { _, _ ,_ in }, updateChatLocationThread: { _, _ in + }, requestToggleTodoMessageItem: { _, _, _ in + }, displayTodoToggleUnavailable: { _ in }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) self.dimNode = ASDisplayNode() diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index dfdf148d15..7e040a92df 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2399,6 +2399,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, playShakeAnimation: { }, displayQuickShare: { _, _ ,_ in }, updateChatLocationThread: { _, _ in + }, requestToggleTodoMessageItem: { _, _, _ in + }, displayTodoToggleUnavailable: { _ in }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: backgroundNode as? WallpaperBackgroundNode)) @@ -2714,6 +2716,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { mappedSource = .animatedEmoji case .paidMessages: mappedSource = .paidMessages + case .todo: + mappedSource = .paidMessages case let .auth(price): mappedSource = .auth(price) } @@ -2792,6 +2796,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { mappedSubject = .messageEffects case .paidMessages: mappedSubject = .paidMessages + case .todo: + mappedSubject = .todo case .business: mappedSubject = .business buttonText = presentationData.strings.Chat_EmptyStateIntroFooterPremiumActionButton