From b1a190f87abdf2d3bccd893ab1ba6839dc850e18 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 16 Mar 2026 18:07:12 +0100 Subject: [PATCH] [WIP] Polls --- .../Sources/ChatController.swift | 6 +- .../Sources/ChatHistoryLocation.swift | 13 +- .../Sources/BrowserBookmarksScreen.swift | 1 + .../Sources/ChatInterfaceState.swift | 12 +- .../Sources/Node/ChatListItem.swift | 2 +- .../ChatPanelInterfaceInteraction.swift | 6 +- .../BubbleSettingsController.swift | 2 +- .../TextSizeSelectionController.swift | 2 +- .../Themes/ThemePreviewControllerNode.swift | 4 +- .../Themes/ThemeSettingsChatPreviewItem.swift | 2 +- .../Sources/ShareController.swift | 24 +- .../ApiUtils/StoreMessage_Telegram.swift | 32 +- .../PendingMessages/EnqueueMessage.swift | 19 +- .../StandaloneSendMessage.swift | 22 +- .../State/AccountStateManagementUtils.swift | 11 +- ...dSynchronizeChatInputStateOperations.swift | 16 +- .../Sources/State/PendingMessageManager.swift | 42 +- ...ecretChatIncomingDecryptedOperations.swift | 8 +- .../SyncCore_ReplyMessageAttribute.swift | 24 +- ...ncCore_SynchronizeableChatInputState.swift | 2 +- ...OutgoingMessageWithChatContextResult.swift | 2 +- .../ChatInputMessageAccessoryPanel.swift | 19 +- .../ChatMessageAnimatedStickerItemNode.swift | 6 +- .../Sources/ChatMessageBubbleItemNode.swift | 96 ++- .../ChatMessageInstantVideoItemNode.swift | 6 +- ...atMessageInteractiveInstantVideoNode.swift | 7 +- .../ChatMessagePollBubbleContentNode/BUILD | 1 + .../ChatMessagePollBubbleContentNode.swift | 696 +++++++++++------- .../Sources/ChatMessageReplyInfoNode.swift | 12 +- .../Sources/ChatMessageStickerItemNode.swift | 6 +- .../ChatRecentActionsControllerNode.swift | 1 + .../ChatSendAudioMessageContextPreview.swift | 3 +- .../Sources/ReplyAccessoryPanelNode.swift | 18 +- .../Sources/ChatControllerInteraction.swift | 17 +- .../Sources/ComposeTodoScreen.swift | 2 + .../Sources/PeerInfoScreen.swift | 1 + .../Sources/PeerInfoScreenOpenMessage.swift | 2 +- .../PeerNameColorChatPreviewItem.swift | 2 +- .../ThemeAccentColorControllerNode.swift | 4 +- .../Sources/WallpaperGalleryItem.swift | 2 +- ...StoryItemSetContainerViewSendMessage.swift | 6 +- .../TelegramUI/Sources/AppDelegate.swift | 2 +- .../Chat/ChatControllerLoadDisplayNode.swift | 10 +- .../ChatControllerNavigateToMessage.swift | 12 +- .../ChatControllerOpenPollContextMenu.swift | 186 +++++ .../ChatControllerOpenTodoContextMenu.swift | 5 +- .../Chat/ChatMessageActionOptions.swift | 4 +- ...UpdateChatPresentationInterfaceState.swift | 4 +- .../TelegramUI/Sources/ChatController.swift | 27 +- .../Sources/ChatControllerKeyShortcuts.swift | 6 +- .../Sources/ChatControllerNode.swift | 2 +- .../Sources/ChatHistoryListNode.swift | 8 +- .../Sources/ChatHistoryViewForLocation.swift | 2 +- .../ChatInterfaceStateAccessoryPanels.swift | 4 +- .../OverlayAudioPlayerControllerNode.swift | 1 + .../PreparedChatHistoryViewTransition.swift | 4 +- .../Sources/SharedAccountContext.swift | 2 + .../UrlHandling/Sources/UrlHandling.swift | 2 +- 58 files changed, 970 insertions(+), 470 deletions(-) create mode 100644 submodules/TelegramUI/Sources/Chat/ChatControllerOpenPollContextMenu.swift diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index c1d00519a0..a40bb235c8 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -791,11 +791,11 @@ public enum ChatControllerSubject: Equatable { } public var quote: Quote? - public var todoTaskId: Int32? + public var subject: EngineMessageReplyInnerSubject? - public init(quote: Quote? = nil, todoTaskId: Int32? = nil) { + public init(quote: Quote? = nil, subject: EngineMessageReplyInnerSubject? = nil) { self.quote = quote - self.todoTaskId = todoTaskId + self.subject = subject } } diff --git a/submodules/AccountContext/Sources/ChatHistoryLocation.swift b/submodules/AccountContext/Sources/ChatHistoryLocation.swift index bf26e96965..2ace788c81 100644 --- a/submodules/AccountContext/Sources/ChatHistoryLocation.swift +++ b/submodules/AccountContext/Sources/ChatHistoryLocation.swift @@ -1,6 +1,7 @@ import Foundation import Postbox import Display +import TelegramCore public enum ChatHistoryInitialSearchLocation: Equatable { case index(MessageIndex) @@ -20,13 +21,13 @@ public struct MessageHistoryScrollToSubject: Equatable { public var index: MessageHistoryAnchorIndex public var quote: Quote? - public var todoTaskId: Int32? + public var subject: EngineMessageReplyInnerSubject? public var setupReply: Bool - public init(index: MessageHistoryAnchorIndex, quote: Quote? = nil, todoTaskId: Int32? = nil, setupReply: Bool = false) { + public init(index: MessageHistoryAnchorIndex, quote: Quote? = nil, subject: EngineMessageReplyInnerSubject? = nil, setupReply: Bool = false) { self.index = index self.quote = quote - self.todoTaskId = todoTaskId + self.subject = subject self.setupReply = setupReply } } @@ -44,12 +45,12 @@ public struct MessageHistoryInitialSearchSubject: Equatable { public var location: ChatHistoryInitialSearchLocation public var quote: Quote? - public var todoTaskId: Int32? + public var subject: EngineMessageReplyInnerSubject? - public init(location: ChatHistoryInitialSearchLocation, quote: Quote? = nil, todoTaskId: Int32? = nil) { + public init(location: ChatHistoryInitialSearchLocation, quote: Quote? = nil, subject: EngineMessageReplyInnerSubject? = nil) { self.location = location self.quote = quote - self.todoTaskId = todoTaskId + self.subject = subject } } diff --git a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift index 2ea2b2f233..5fa4e59f7a 100644 --- a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift @@ -107,6 +107,7 @@ public final class BrowserBookmarksScreen: ViewController { }, openConferenceCall: { _ in }, longTap: { _, _ in }, todoItemLongTap: { _, _ in + }, pollOptionLongTap: { _, _ in }, openCheckoutOrReceipt: { _, _ in }, openSearch: { }, setupReply: { _ in diff --git a/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift b/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift index c8afd40bea..bd4ab384c5 100644 --- a/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift +++ b/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift @@ -481,19 +481,19 @@ public final class ChatInterfaceState: Codable, Equatable { public struct ReplyMessageSubject: Codable, Equatable { public var messageId: EngineMessage.Id public var quote: EngineMessageReplyQuote? - public var todoItemId: Int32? + public var innerSubject: EngineMessageReplyInnerSubject? - public init(messageId: EngineMessage.Id, quote: EngineMessageReplyQuote?, todoItemId: Int32?) { + public init(messageId: EngineMessage.Id, quote: EngineMessageReplyQuote?, innerSubject: EngineMessageReplyInnerSubject?) { self.messageId = messageId self.quote = quote - self.todoItemId = todoItemId + self.innerSubject = innerSubject } public var subjectModel: EngineMessageReplySubject { return EngineMessageReplySubject( messageId: self.messageId, quote: self.quote, - todoItemId: self.todoItemId + innerSubject: self.innerSubject ) } } @@ -549,7 +549,7 @@ public final class ChatInterfaceState: Codable, Equatable { return ReplyMessageSubject( messageId: $0.messageId, quote: $0.quote, - todoItemId: $0.todoItemId + innerSubject: $0.innerSubject ) }) if let timestamp = state?.timestamp { @@ -633,7 +633,7 @@ public final class ChatInterfaceState: Codable, Equatable { let replyMessageIdNamespace: Int32? = try? container.decodeIfPresent(Int32.self, forKey: "r.n") let replyMessageIdId: Int32? = try? container.decodeIfPresent(Int32.self, forKey: "r.i") if let replyMessageIdPeerId = replyMessageIdPeerId, let replyMessageIdNamespace = replyMessageIdNamespace, let replyMessageIdId = replyMessageIdId { - self.replyMessageSubject = ReplyMessageSubject(messageId: EngineMessage.Id(peerId: EnginePeer.Id(replyMessageIdPeerId), namespace: replyMessageIdNamespace, id: replyMessageIdId), quote: nil, todoItemId: nil) + self.replyMessageSubject = ReplyMessageSubject(messageId: EngineMessage.Id(peerId: EnginePeer.Id(replyMessageIdPeerId), namespace: replyMessageIdNamespace, id: replyMessageIdId), quote: nil, innerSubject: nil) } else { self.replyMessageSubject = nil } diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 8300e4270d..e58d60dcf6 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -2939,7 +2939,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } else if let image = media as? TelegramMediaImage { if let _ = largestImageRepresentation(image.representations) { let fitSize = contentImageSize - contentImageSpecs.append(ContentImageSpec(message: message, media: .image(image), size: fitSize)) + contentImageSpecs.append(ContentImageSpec(message: message, media: .image(image), size: fitSize)) } break inner } else if let file = media as? TelegramMediaFile { diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift index 76a15829be..b1c4d4f065 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift @@ -71,7 +71,7 @@ public final class ChatPanelInterfaceInteraction { case editPrice } - public let setupReplyMessage: (MessageId?, Int32?, @escaping (ContainedViewLayoutTransition, @escaping () -> Void) -> Void) -> Void + public let setupReplyMessage: (MessageId?, EngineMessageReplyInnerSubject?, @escaping (ContainedViewLayoutTransition, @escaping () -> Void) -> Void) -> Void public let setupEditMessage: (MessageId?, @escaping (ContainedViewLayoutTransition) -> Void) -> Void public let beginMessageSelection: ([MessageId], @escaping (ContainedViewLayoutTransition) -> Void) -> Void public let cancelMessageSelection: (ContainedViewLayoutTransition) -> Void @@ -200,9 +200,9 @@ public final class ChatPanelInterfaceInteraction { public let requestLayout: (ContainedViewLayoutTransition) -> Void public let chatController: () -> ViewController? public let statuses: ChatPanelInterfaceInteractionStatuses? - + public init( - setupReplyMessage: @escaping (MessageId?, Int32?, @escaping (ContainedViewLayoutTransition, @escaping () -> Void) -> Void) -> Void, + setupReplyMessage: @escaping (MessageId?, EngineMessageReplyInnerSubject?, @escaping (ContainedViewLayoutTransition, @escaping () -> Void) -> Void) -> Void, setupEditMessage: @escaping (MessageId?, @escaping (ContainedViewLayoutTransition) -> Void) -> Void, beginMessageSelection: @escaping ([MessageId], @escaping (ContainedViewLayoutTransition) -> Void) -> Void, cancelMessageSelection: @escaping (ContainedViewLayoutTransition) -> Void, diff --git a/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift b/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift index 02f8926dca..4b2c5e68fe 100644 --- a/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift +++ b/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift @@ -172,7 +172,7 @@ private final class BubbleSettingsControllerNode: ASDisplayNode, ASScrollViewDel let message1 = Message(stableId: 4, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 4), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66003, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false, rank: nil, rankRole: nil)) - let message2 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66002, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false, todoItemId: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + let message2 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66002, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false, innerSubject: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false, rank: nil, rankRole: nil)) let waveformBase64 = "DAAOAAkACQAGAAwADwAMABAADQAPABsAGAALAA0AGAAfABoAHgATABgAGQAYABQADAAVABEAHwANAA0ACQAWABkACQAOAAwACQAfAAAAGQAVAAAAEwATAAAACAAfAAAAHAAAABwAHwAAABcAGQAAABQADgAAABQAHwAAAB8AHwAAAAwADwAAAB8AEwAAABoAFwAAAB8AFAAAAAAAHwAAAAAAHgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAAAA=" diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index cca8b8b516..38fd76dd32 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -464,7 +464,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, ASScrollView let message1 = Message(stableId: 4, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 4), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66003, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false, rank: nil, rankRole: nil)) - let message2 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66002, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false, todoItemId: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + let message2 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66002, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false, innerSubject: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false, rank: nil, rankRole: nil)) let waveformBase64 = "DAAOAAkACQAGAAwADwAMABAADQAPABsAGAALAA0AGAAfABoAHgATABgAGQAYABQADAAVABEAHwANAA0ACQAWABkACQAOAAwACQAfAAAAGQAVAAAAEwATAAAACAAfAAAAHAAAABwAHwAAABcAGQAAABQADgAAABQAHwAAAB8AHwAAAAwADwAAAB8AEwAAABoAFwAAAB8AFAAAAAAAHwAAAAAAHgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAAAA=" diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index 076c2f1130..b62a2316f9 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -613,7 +613,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, ASScrollViewDelegate { messages[message4.id] = message4 sampleMessages.append(message4) - let message5 = Message(stableId: 5, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 5), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66004, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [ReplyMessageAttribute(messageId: message4.id, threadMessageId: nil, quote: nil, isQuote: false, todoItemId: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + let message5 = Message(stableId: 5, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 5), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66004, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [ReplyMessageAttribute(messageId: message4.id, threadMessageId: nil, quote: nil, isQuote: false, innerSubject: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) messages[message5.id] = message5 sampleMessages.append(message5) @@ -624,7 +624,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, ASScrollViewDelegate { let message6 = Message(stableId: 6, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 6), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66005, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) sampleMessages.append(message6) - let message7 = Message(stableId: 7, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 7), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66006, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: message5.id, threadMessageId: nil, quote: nil, isQuote: false, todoItemId: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + let message7 = Message(stableId: 7, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 7), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66006, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: message5.id, threadMessageId: nil, quote: nil, isQuote: false, innerSubject: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) sampleMessages.append(message7) let message8 = Message(stableId: 8, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 8), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66007, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift index 9c4b476f26..e96aed13d8 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift @@ -169,7 +169,7 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode { messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) } - let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: messageItem.outgoing ? otherPeerId : peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: messageItem.outgoing ? [] : [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: messageItem.outgoing ? TelegramUser(id: otherPeerId, accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil) : nil, text: messageItem.text, attributes: messageItem.reply != nil ? [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false, todoItemId: nil)] : [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: messageItem.outgoing ? otherPeerId : peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: messageItem.outgoing ? [] : [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: messageItem.outgoing ? TelegramUser(id: otherPeerId, accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil) : nil, text: messageItem.text, attributes: messageItem.reply != nil ? [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false, innerSubject: nil)] : [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false, rank: nil, rankRole: nil)) } diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index c5622becc0..6a9f7c3535 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -2034,10 +2034,10 @@ public final class ShareController: ViewController { var messages: [EnqueueMessage] = [] if !text.isEmpty { messages.append(.message(text: url + "\n\n" + text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { - EngineMessageReplySubject(messageId: $0, quote: nil, todoItemId: nil) + EngineMessageReplySubject(messageId: $0, quote: nil, innerSubject: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } else { - messages.append(.message(text: url, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, todoItemId: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: url, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, innerSubject: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } messages = transformMessages(messages, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId]) shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages)) @@ -2070,9 +2070,9 @@ public final class ShareController: ViewController { var messages: [EnqueueMessage] = [] if !text.isEmpty { - messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, todoItemId: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, innerSubject: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } - messages.append(.message(text: string, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, todoItemId: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: string, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, innerSubject: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) messages = transformMessages(messages, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId]) shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages)) } @@ -2104,12 +2104,12 @@ public final class ShareController: ViewController { var messages: [EnqueueMessage] = [] if !text.isEmpty { - messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, todoItemId: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, innerSubject: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } let attributedText = NSMutableAttributedString(string: string, attributes: [ChatTextInputAttributes.italic: true as NSNumber]) attributedText.append(NSAttributedString(string: "\n\n\(url)")) let entities = generateChatInputTextEntities(attributedText) - messages.append(.message(text: attributedText.string, attributes: [TextEntitiesMessageAttribute(entities: entities)], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, todoItemId: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: attributedText.string, attributes: [TextEntitiesMessageAttribute(entities: entities)], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, innerSubject: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) messages = transformMessages(messages, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId]) shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages)) } @@ -2140,7 +2140,7 @@ public final class ShareController: ViewController { } var messages: [EnqueueMessage] = [] - messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), representations: representations.map({ $0.representation }), immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])), threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, todoItemId: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), representations: representations.map({ $0.representation }), immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])), threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, innerSubject: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) messages = transformMessages(messages, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId]) shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages)) } @@ -2224,7 +2224,7 @@ public final class ShareController: ViewController { var messages: [EnqueueMessage] = [] if !text.isEmpty && !sendTextAsCaption { - messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, todoItemId: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, innerSubject: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } var attributes: [MessageAttribute] = [] if let startAtTimestamp = mediaParameters?.startAtTimestamp, let startAtTimestampNode = strongSelf.controllerNode.startAtTimestampNode, startAtTimestampNode.value { @@ -2233,7 +2233,7 @@ public final class ShareController: ViewController { if let forwardSourceMessageId { messages.append(.forward(source: forwardSourceMessageId, threadId: threadId, grouping: .auto, attributes: attributes, correlationId: nil)) } else { - messages.append(.message(text: sendTextAsCaption ? text : "", attributes: attributes, inlineStickers: [:], mediaReference: mediaReference, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, todoItemId: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: sendTextAsCaption ? text : "", attributes: attributes, inlineStickers: [:], mediaReference: mediaReference, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, innerSubject: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } messages = transformMessages(messages, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId]) shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages)) @@ -2266,9 +2266,9 @@ public final class ShareController: ViewController { var messages: [EnqueueMessage] = [] if !text.isEmpty { - messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, todoItemId: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, innerSubject: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } - messages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, todoItemId: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + messages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, innerSubject: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) messages = transformMessages(messages, showNames: showNames, silently: silently, sendPaidMessageStars: requiresStars[peerId]) shareSignals.append(enqueueMessages(account: currentContext.context.account, peerId: peerId, messages: messages)) } @@ -2302,7 +2302,7 @@ public final class ShareController: ViewController { let correlationId = Int64.random(in: Int64.min ... Int64.max) correlationIds.append(correlationId) - messagesToEnqueue.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, todoItemId: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])) + messagesToEnqueue.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, innerSubject: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])) } for message in messages { for media in message.media { diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index ded978e88a..6cba95495e 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -868,7 +868,7 @@ extension StoreMessage { var threadMessageId: MessageId? switch replyTo { case let .messageReplyHeader(messageReplyHeaderData): - let (innerFlags, replyToMsgId, replyToPeerId, replyHeader, replyMedia, replyToTopId, quoteText, quoteEntities, quoteOffset, todoItemId) = (messageReplyHeaderData.flags, messageReplyHeaderData.replyToMsgId, messageReplyHeaderData.replyToPeerId, messageReplyHeaderData.replyFrom, messageReplyHeaderData.replyMedia, messageReplyHeaderData.replyToTopId, messageReplyHeaderData.quoteText, messageReplyHeaderData.quoteEntities, messageReplyHeaderData.quoteOffset, messageReplyHeaderData.todoItemId) + let (innerFlags, replyToMsgId, replyToPeerId, replyHeader, replyMedia, replyToTopId, quoteText, quoteEntities, quoteOffset, todoItemId, pollOption) = (messageReplyHeaderData.flags, messageReplyHeaderData.replyToMsgId, messageReplyHeaderData.replyToPeerId, messageReplyHeaderData.replyFrom, messageReplyHeaderData.replyMedia, messageReplyHeaderData.replyToTopId, messageReplyHeaderData.quoteText, messageReplyHeaderData.quoteEntities, messageReplyHeaderData.quoteOffset, messageReplyHeaderData.todoItemId, messageReplyHeaderData.pollOption) let isForumTopic = (innerFlags & (1 << 3)) != 0 var quote: EngineMessageReplyQuote? @@ -919,7 +919,13 @@ extension StoreMessage { threadId = Int64(threadIdValue.id) } } - attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId, quote: quote, isQuote: isQuote, todoItemId: todoItemId)) + var innerSubject: EngineMessageReplyInnerSubject? + if let todoItemId { + innerSubject = .todoItem(todoItemId) + } else if let pollOption { + innerSubject = .pollOption(pollOption.makeData()) + } + attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId, quote: quote, isQuote: isQuote, innerSubject: innerSubject)) } if let replyHeader = replyHeader { attributes.append(QuotedReplyMessageAttribute(apiHeader: replyHeader, quote: quote, isQuote: isQuote)) @@ -1256,7 +1262,7 @@ extension StoreMessage { if chatPeerId.peerId.namespace == Namespaces.Peer.CloudChannel, let replyTo { switch replyTo { case let .messageReplyHeader(messageReplyHeaderData): - let (innerFlags, replyToMsgId, replyToPeerId, replyHeader, replyMedia, quoteText, quoteEntities, quoteOffset, todoItemId) = (messageReplyHeaderData.flags, messageReplyHeaderData.replyToMsgId, messageReplyHeaderData.replyToPeerId, messageReplyHeaderData.replyFrom, messageReplyHeaderData.replyMedia, messageReplyHeaderData.quoteText, messageReplyHeaderData.quoteEntities, messageReplyHeaderData.quoteOffset, messageReplyHeaderData.todoItemId) + let (innerFlags, replyToMsgId, replyToPeerId, replyHeader, replyMedia, quoteText, quoteEntities, quoteOffset, todoItemId, pollOption) = (messageReplyHeaderData.flags, messageReplyHeaderData.replyToMsgId, messageReplyHeaderData.replyToPeerId, messageReplyHeaderData.replyFrom, messageReplyHeaderData.replyMedia, messageReplyHeaderData.quoteText, messageReplyHeaderData.quoteEntities, messageReplyHeaderData.quoteOffset, messageReplyHeaderData.todoItemId, messageReplyHeaderData.pollOption) var quote: EngineMessageReplyQuote? let isQuote = (innerFlags & (1 << 9)) != 0 if quoteText != nil || replyMedia != nil { @@ -1265,7 +1271,15 @@ extension StoreMessage { if let replyToMsgId = replyToMsgId { let replyPeerId = replyToPeerId?.peerId ?? peerId - attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: nil, quote: quote, isQuote: isQuote, todoItemId: todoItemId)) + + var innerSubject: EngineMessageReplyInnerSubject? + if let todoItemId { + innerSubject = .todoItem(todoItemId) + } else if let pollOption { + innerSubject = .pollOption(pollOption.makeData()) + } + + attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: nil, quote: quote, isQuote: isQuote, innerSubject: innerSubject)) } else if let replyHeader = replyHeader { attributes.append(QuotedReplyMessageAttribute(apiHeader: replyHeader, quote: quote, isQuote: isQuote)) } @@ -1278,7 +1292,7 @@ extension StoreMessage { var threadMessageId: MessageId? switch replyTo { case let .messageReplyHeader(messageReplyHeaderData): - let (innerFlags, replyToMsgId, replyToPeerId, replyHeader, replyMedia, replyToTopId, quoteText, quoteEntities, quoteOffset, todoItemId) = (messageReplyHeaderData.flags, messageReplyHeaderData.replyToMsgId, messageReplyHeaderData.replyToPeerId, messageReplyHeaderData.replyFrom, messageReplyHeaderData.replyMedia, messageReplyHeaderData.replyToTopId, messageReplyHeaderData.quoteText, messageReplyHeaderData.quoteEntities, messageReplyHeaderData.quoteOffset, messageReplyHeaderData.todoItemId) + let (innerFlags, replyToMsgId, replyToPeerId, replyHeader, replyMedia, replyToTopId, quoteText, quoteEntities, quoteOffset, todoItemId, pollOption) = (messageReplyHeaderData.flags, messageReplyHeaderData.replyToMsgId, messageReplyHeaderData.replyToPeerId, messageReplyHeaderData.replyFrom, messageReplyHeaderData.replyMedia, messageReplyHeaderData.replyToTopId, messageReplyHeaderData.quoteText, messageReplyHeaderData.quoteEntities, messageReplyHeaderData.quoteOffset, messageReplyHeaderData.todoItemId, messageReplyHeaderData.pollOption) var quote: EngineMessageReplyQuote? let isQuote = (innerFlags & (1 << 9)) != 0 if quoteText != nil || replyMedia != nil { @@ -1304,7 +1318,13 @@ extension StoreMessage { default: break } - attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId, quote: quote, isQuote: isQuote, todoItemId: todoItemId)) + var innerSubject: EngineMessageReplyInnerSubject? + if let todoItemId { + innerSubject = .todoItem(todoItemId) + } else if let pollOption { + innerSubject = .pollOption(pollOption.makeData()) + } + attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId, quote: quote, isQuote: isQuote, innerSubject: innerSubject)) } else if let replyHeader = replyHeader { attributes.append(QuotedReplyMessageAttribute(apiHeader: replyHeader, quote: quote, isQuote: isQuote)) } diff --git a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift index 6f611685d7..3488be4f27 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift @@ -9,6 +9,11 @@ public enum EnqueueMessageGrouping { case auto } +public enum EngineMessageReplyInnerSubject: Codable, Equatable { + case todoItem(Int32) + case pollOption(Data) +} + public struct EngineMessageReplyQuote: Codable, Equatable { private enum CodingKeys: String, CodingKey { case text = "t" @@ -82,12 +87,12 @@ public struct EngineMessageReplyQuote: Codable, Equatable { public struct EngineMessageReplySubject: Codable, Equatable { public var messageId: EngineMessage.Id public var quote: EngineMessageReplyQuote? - public var todoItemId: Int32? + public var innerSubject: EngineMessageReplyInnerSubject? - public init(messageId: EngineMessage.Id, quote: EngineMessageReplyQuote?, todoItemId: Int32?) { + public init(messageId: EngineMessage.Id, quote: EngineMessageReplyQuote?, innerSubject: EngineMessageReplyInnerSubject?) { self.messageId = messageId self.quote = quote - self.todoItemId = todoItemId + self.innerSubject = innerSubject } } @@ -387,7 +392,7 @@ public func enqueueMessagesToMultiplePeers(account: Account, peerIds: [PeerId], for peerId in peerIds { var replyToMessageId: EngineMessageReplySubject? if let threadIds = threadIds[peerId] { - replyToMessageId = EngineMessageReplySubject(messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadIds)), quote: nil, todoItemId: nil) + replyToMessageId = EngineMessageReplySubject(messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadIds)), quote: nil, innerSubject: nil) } var messages = messages if let replyToMessageId = replyToMessageId { @@ -433,7 +438,7 @@ public func resendMessages(account: Account, messageIds: [MessageId]) -> Signal< var forwardSource: MessageId? inner: for attribute in message.attributes { if let attribute = attribute as? ReplyMessageAttribute { - replyToMessageId = EngineMessageReplySubject(messageId: attribute.messageId, quote: attribute.quote, todoItemId: attribute.todoItemId) + replyToMessageId = EngineMessageReplySubject(messageId: attribute.messageId, quote: attribute.quote, innerSubject: attribute.innerSubject) } else if let attribute = attribute as? ReplyStoryAttribute { replyToStoryId = attribute.storyId } else if let attribute = attribute as? OutgoingMessageInfoAttribute { @@ -528,7 +533,7 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, mediaReference = .standalone(media: media) } } - updatedMessages.append((transformedMedia, .message(text: sourceMessage.text, attributes: sourceMessage.attributes, inlineStickers: [:], mediaReference: mediaReference, threadId: threadId, replyToMessageId: threadId.flatMap { EngineMessageReplySubject(messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: $0)), quote: nil, todoItemId: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))) + updatedMessages.append((transformedMedia, .message(text: sourceMessage.text, attributes: sourceMessage.attributes, inlineStickers: [:], mediaReference: mediaReference, threadId: threadId, replyToMessageId: threadId.flatMap { EngineMessageReplySubject(messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: $0)), quote: nil, innerSubject: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))) continue outer } } @@ -666,7 +671,7 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, quote = EngineMessageReplyQuote(text: replyMessage.text, offset: nil, entities: messageTextEntitiesInRange(entities: replyMessage.textEntitiesAttribute?.entities ?? [], range: NSRange(location: 0, length: nsText.length), onlyQuoteable: true), media: replyMedia) } } - attributes.append(ReplyMessageAttribute(messageId: replyToMessageId.messageId, threadMessageId: threadMessageId, quote: quote, isQuote: isQuote, todoItemId: replyToMessageId.todoItemId)) + attributes.append(ReplyMessageAttribute(messageId: replyToMessageId.messageId, threadMessageId: threadMessageId, quote: quote, isQuote: isQuote, innerSubject: replyToMessageId.innerSubject)) } if let replyToStoryId = replyToStoryId { attributes.append(ReplyStoryAttribute(storyId: replyToStoryId)) diff --git a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift index 59721a8c68..b4bf907713 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift @@ -171,7 +171,7 @@ public func standaloneSendEnqueueMessages( } if let replyToMessageId = message.replyToMessageId { - attributes.append(ReplyMessageAttribute(messageId: replyToMessageId, threadMessageId: nil, quote: nil, isQuote: false, todoItemId: nil)) + attributes.append(ReplyMessageAttribute(messageId: replyToMessageId, threadMessageId: nil, quote: nil, isQuote: false, innerSubject: nil)) } if let forwardOptions = message.forwardOptions { attributes.append(ForwardOptionsMessageAttribute(hideNames: forwardOptions.hideNames, hideCaptions: forwardOptions.hideCaptions)) @@ -339,6 +339,7 @@ private func sendUploadedMessageContent( } var replyToStoryId: StoryId? var replyTodoItemId: Int32? + var replyPollOption: Buffer? var scheduleTime: Int32? var scheduleRepeatPeriod: Int32? var videoTimestamp: Int32? @@ -352,7 +353,14 @@ private func sendUploadedMessageContent( for attribute in attributes { if let replyAttribute = attribute as? ReplyMessageAttribute { replyMessageId = replyAttribute.messageId.id - replyTodoItemId = replyAttribute.todoItemId + switch replyAttribute.innerSubject { + case let .todoItem(todoItemId): + replyTodoItemId = todoItemId + case let .pollOption(pollOption): + replyPollOption = Buffer(data: pollOption) + default: + break + } } else if let attribute = attribute as? ReplyStoryAttribute { replyToStoryId = attribute.storyId } else if let outgoingInfo = attribute as? OutgoingMessageInfoAttribute { @@ -445,7 +453,10 @@ private func sendUploadedMessageContent( if let _ = replyTodoItemId { replyFlags |= 1 << 6 } - replyTo = .inputReplyToMessage(.init(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: topMsgId, replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil, monoforumPeerId: monoforumPeerId, todoItemId: replyTodoItemId, pollOption: nil)) + if let _ = replyPollOption { + replyFlags |= 1 << 7 + } + replyTo = .inputReplyToMessage(.init(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: topMsgId, replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil, monoforumPeerId: monoforumPeerId, todoItemId: replyTodoItemId, pollOption: replyPollOption)) } else if let replyToStoryId { if let inputPeer = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputPeer) { flags |= 1 << 0 @@ -479,7 +490,10 @@ private func sendUploadedMessageContent( if let _ = replyTodoItemId { replyFlags |= 1 << 6 } - replyTo = .inputReplyToMessage(.init(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: topMsgId, replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil, monoforumPeerId: monoforumPeerId, todoItemId: replyTodoItemId, pollOption: nil)) + if let _ = replyPollOption { + replyFlags |= 1 << 7 + } + replyTo = .inputReplyToMessage(.init(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: topMsgId, replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil, monoforumPeerId: monoforumPeerId, todoItemId: replyTodoItemId, pollOption: replyPollOption)) } else if let replyToStoryId = replyToStoryId { if let inputPeer = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputPeer) { flags |= 1 << 0 diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 34d903709e..9e7eae61c2 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1694,7 +1694,7 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: if let replyToMsgHeader { switch replyToMsgHeader { case let .inputReplyToMessage(inputReplyToMessageData): - let (replyToMsgId, topMsgId, replyToPeerId, quoteText, quoteEntities, quoteOffset, monoforumPeerId, todoItemId) = (inputReplyToMessageData.replyToMsgId, inputReplyToMessageData.topMsgId, inputReplyToMessageData.replyToPeerId, inputReplyToMessageData.quoteText, inputReplyToMessageData.quoteEntities, inputReplyToMessageData.quoteOffset, inputReplyToMessageData.monoforumPeerId, inputReplyToMessageData.todoItemId) + let (replyToMsgId, topMsgId, replyToPeerId, quoteText, quoteEntities, quoteOffset, monoforumPeerId, todoItemId, pollOption) = (inputReplyToMessageData.replyToMsgId, inputReplyToMessageData.topMsgId, inputReplyToMessageData.replyToPeerId, inputReplyToMessageData.quoteText, inputReplyToMessageData.quoteEntities, inputReplyToMessageData.quoteOffset, inputReplyToMessageData.monoforumPeerId, inputReplyToMessageData.todoItemId, inputReplyToMessageData.pollOption) let _ = topMsgId let _ = monoforumPeerId @@ -1733,10 +1733,17 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: break } + var innerSubject: EngineMessageReplyInnerSubject? + if let todoItemId { + innerSubject = .todoItem(todoItemId) + } else if let pollOption { + innerSubject = .pollOption(pollOption.makeData()) + } + replySubject = EngineMessageReplySubject( messageId: MessageId(peerId: parsedReplyToPeerId ?? peer.peerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), quote: quote, - todoItemId: todoItemId + innerSubject: innerSubject ) case .inputReplyToStory: break diff --git a/submodules/TelegramCore/Sources/State/ManagedSynchronizeChatInputStateOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSynchronizeChatInputStateOperations.swift index f6a151301c..a3f9e97831 100644 --- a/submodules/TelegramCore/Sources/State/ManagedSynchronizeChatInputStateOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedSynchronizeChatInputStateOperations.swift @@ -197,7 +197,16 @@ private func synchronizeChatInputState(transaction: Transaction, postbox: Postbo } } - let replyTodoItemId = replySubject.todoItemId + var replyTodoItemId: Int32? + var replyPollOption: Buffer? + switch replySubject.innerSubject { + case let .todoItem(todoItemId): + replyTodoItemId = todoItemId + case let .pollOption(pollOption): + replyPollOption = Buffer(data: pollOption) + default: + break + } if replyToPeer != nil { innerFlags |= 1 << 1 @@ -214,8 +223,11 @@ private func synchronizeChatInputState(transaction: Transaction, postbox: Postbo if let _ = replyTodoItemId { innerFlags |= 1 << 6 } + if let _ = replyPollOption { + innerFlags |= 1 << 7 + } if !discard { - replyTo = .inputReplyToMessage(.init(flags: innerFlags, replyToMsgId: replySubject.messageId.id, topMsgId: topMsgId, replyToPeerId: replyToPeer, quoteText: quoteText, quoteEntities: quoteEntities, quoteOffset: quoteOffset, monoforumPeerId: monoforumPeerId, todoItemId: replyTodoItemId, pollOption: nil)) + replyTo = .inputReplyToMessage(.init(flags: innerFlags, replyToMsgId: replySubject.messageId.id, topMsgId: topMsgId, replyToPeerId: replyToPeer, quoteText: quoteText, quoteEntities: quoteEntities, quoteOffset: quoteOffset, monoforumPeerId: monoforumPeerId, todoItemId: replyTodoItemId, pollOption: replyPollOption)) } } else if let topMsgId { flags |= 1 << 0 diff --git a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift index 41ea1e76ec..0434b0e286 100644 --- a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift +++ b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift @@ -563,7 +563,7 @@ public final class PendingMessageManager { } var attributes = currentMessage.attributes if !attributes.contains(where: { $0 is ReplyMessageAttribute }) { - attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: id.peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: topicId)), threadMessageId: nil, quote: nil, isQuote: false, todoItemId: nil)) + attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: id.peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: topicId)), threadMessageId: nil, quote: nil, isQuote: false, innerSubject: nil)) } return .update(StoreMessage(id: id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: topicId, 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: attributes, media: currentMessage.media)) }) @@ -1026,6 +1026,7 @@ public final class PendingMessageManager { var replyQuote: EngineMessageReplyQuote? var replyToStoryId: StoryId? var replyTodoItemId: Int32? + var replyPollOption: Buffer? var scheduleTime: Int32? var scheduleRepeatPeriod: Int32? var videoTimestamp: Int32? @@ -1046,7 +1047,14 @@ public final class PendingMessageManager { if replyAttribute.isQuote { replyQuote = replyAttribute.quote } - replyTodoItemId = replyAttribute.todoItemId + switch replyAttribute.innerSubject { + case let .todoItem(todoItemId): + replyTodoItemId = todoItemId + case let .pollOption(pollOption): + replyPollOption = Buffer(data: pollOption) + default: + break + } } else if let attribute = attribute as? ReplyStoryAttribute { replyToStoryId = attribute.storyId } else if let _ = attribute as? ForwardSourceInfoAttribute { @@ -1289,8 +1297,11 @@ public final class PendingMessageManager { if let _ = replyTodoItemId { replyFlags |= 1 << 6 } + if let _ = replyPollOption { + replyFlags |= 1 << 7 + } - replyTo = .inputReplyToMessage(.init(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: topMsgId, replyToPeerId: replyToPeerId, quoteText: quoteText, quoteEntities: quoteEntities, quoteOffset: quoteOffset, monoforumPeerId: monoforumPeerId, todoItemId: replyTodoItemId, pollOption: nil)) + replyTo = .inputReplyToMessage(.init(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: topMsgId, replyToPeerId: replyToPeerId, quoteText: quoteText, quoteEntities: quoteEntities, quoteOffset: quoteOffset, monoforumPeerId: monoforumPeerId, todoItemId: replyTodoItemId, pollOption: replyPollOption)) } else if let replyToStoryId { if let inputPeer = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputPeer) { flags |= 1 << 0 @@ -1532,6 +1543,7 @@ public final class PendingMessageManager { var replyQuote: EngineMessageReplyQuote? var replyToStoryId: StoryId? var replyTodoItemId: Int32? + var replyPollOption: Buffer? var scheduleTime: Int32? var scheduleRepeatPeriod: Int32? var videoTimestamp: Int32? @@ -1565,7 +1577,14 @@ public final class PendingMessageManager { if replyAttribute.isQuote { replyQuote = replyAttribute.quote } - replyTodoItemId = replyAttribute.todoItemId + switch replyAttribute.innerSubject { + case let .todoItem(todoItemId): + replyTodoItemId = todoItemId + case let .pollOption(pollOption): + replyPollOption = Buffer(data: pollOption) + default: + break + } } else if let attribute = attribute as? ReplyStoryAttribute { replyToStoryId = attribute.storyId } else if let outgoingInfo = attribute as? OutgoingMessageInfoAttribute { @@ -1682,7 +1701,10 @@ public final class PendingMessageManager { if let _ = replyTodoItemId { replyFlags |= 1 << 6 } - replyTo = .inputReplyToMessage(.init(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: topMsgId, replyToPeerId: replyToPeerId, quoteText: quoteText, quoteEntities: quoteEntities, quoteOffset: quoteOffset, monoforumPeerId: monoforumPeerId, todoItemId: replyTodoItemId, pollOption: nil)) + if let _ = replyPollOption { + replyFlags |= 1 << 7 + } + replyTo = .inputReplyToMessage(.init(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: topMsgId, replyToPeerId: replyToPeerId, quoteText: quoteText, quoteEntities: quoteEntities, quoteOffset: quoteOffset, monoforumPeerId: monoforumPeerId, todoItemId: replyTodoItemId, pollOption: replyPollOption)) } else if let replyToStoryId = replyToStoryId { if let inputPeer = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputPeer) { flags |= 1 << 0 @@ -1780,7 +1802,10 @@ public final class PendingMessageManager { if let _ = replyTodoItemId { replyFlags |= 1 << 6 } - replyTo = .inputReplyToMessage(.init(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: topMsgId, replyToPeerId: replyToPeerId, quoteText: quoteText, quoteEntities: quoteEntities, quoteOffset: quoteOffset, monoforumPeerId: monoforumPeerId, todoItemId: replyTodoItemId, pollOption: nil)) + if let _ = replyPollOption { + replyFlags |= 1 << 7 + } + replyTo = .inputReplyToMessage(.init(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: topMsgId, replyToPeerId: replyToPeerId, quoteText: quoteText, quoteEntities: quoteEntities, quoteOffset: quoteOffset, monoforumPeerId: monoforumPeerId, todoItemId: replyTodoItemId, pollOption: replyPollOption)) } else if let replyToStoryId = replyToStoryId { if let inputPeer = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputPeer) { flags |= 1 << 0 @@ -1928,7 +1953,10 @@ public final class PendingMessageManager { if let _ = replyTodoItemId { replyFlags |= 1 << 6 } - replyTo = .inputReplyToMessage(.init(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: message.threadId.flatMap(Int32.init(clamping:)), replyToPeerId: replyToPeerId, quoteText: quoteText, quoteEntities: quoteEntities, quoteOffset: quoteOffset, monoforumPeerId: monoforumPeerId, todoItemId: replyTodoItemId, pollOption: nil)) + if let _ = replyPollOption { + replyFlags |= 1 << 7 + } + replyTo = .inputReplyToMessage(.init(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: message.threadId.flatMap(Int32.init(clamping:)), replyToPeerId: replyToPeerId, quoteText: quoteText, quoteEntities: quoteEntities, quoteOffset: quoteOffset, monoforumPeerId: monoforumPeerId, todoItemId: replyTodoItemId, pollOption: replyPollOption)) } else if let replyToStoryId = replyToStoryId { if let inputPeer = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputPeer) { flags |= 1 << 0 diff --git a/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift b/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift index 753cb0a3e6..2867a94d87 100644 --- a/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift +++ b/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift @@ -881,7 +881,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 } if let replyToRandomId = replyToRandomId, let replyMessageId = messageIdForGloballyUniqueMessageId(replyToRandomId) { - attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false, todoItemId: nil)) + attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false, innerSubject: nil)) } var entitiesAttribute: TextEntitiesMessageAttribute? @@ -1113,7 +1113,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 } if let replyToRandomId = replyToRandomId, let replyMessageId = messageIdForGloballyUniqueMessageId(replyToRandomId) { - attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false, todoItemId: nil)) + attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false, innerSubject: nil)) } var entitiesAttribute: TextEntitiesMessageAttribute? @@ -1392,7 +1392,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 } if let replyToRandomId = replyToRandomId, let replyMessageId = messageIdForGloballyUniqueMessageId(replyToRandomId) { - attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false, todoItemId: nil)) + attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false, innerSubject: nil)) } var entitiesAttribute: TextEntitiesMessageAttribute? @@ -1593,7 +1593,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 } if let replyToRandomId = replyToRandomId, let replyMessageId = messageIdForGloballyUniqueMessageId(replyToRandomId) { - attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false, todoItemId: nil)) + attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false, innerSubject: nil)) } var entitiesAttribute: TextEntitiesMessageAttribute? diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMessageAttribute.swift index d7e57a3215..bd5cfa7cc4 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMessageAttribute.swift @@ -7,18 +7,18 @@ public class ReplyMessageAttribute: MessageAttribute { public let threadMessageId: MessageId? public let quote: EngineMessageReplyQuote? public let isQuote: Bool - public let todoItemId: Int32? + public let innerSubject: EngineMessageReplyInnerSubject? public var associatedMessageIds: [MessageId] { return [self.messageId] } - public init(messageId: MessageId, threadMessageId: MessageId?, quote: EngineMessageReplyQuote?, isQuote: Bool, todoItemId: Int32?) { + public init(messageId: MessageId, threadMessageId: MessageId?, quote: EngineMessageReplyQuote?, isQuote: Bool, innerSubject: EngineMessageReplyInnerSubject?) { self.messageId = messageId self.threadMessageId = threadMessageId self.quote = quote self.isQuote = isQuote - self.todoItemId = todoItemId + self.innerSubject = innerSubject } required public init(decoder: PostboxDecoder) { @@ -33,7 +33,13 @@ public class ReplyMessageAttribute: MessageAttribute { self.quote = decoder.decodeCodable(EngineMessageReplyQuote.self, forKey: "qu") self.isQuote = decoder.decodeBoolForKey("iq", orElse: self.quote != nil) - self.todoItemId = decoder.decodeOptionalInt32ForKey("tid") + if let todoItemId = decoder.decodeOptionalInt32ForKey("tid") { + self.innerSubject = .todoItem(todoItemId) + } else if let pollOptionId = decoder.decodeDataForKey("pid") { + self.innerSubject = .pollOption(pollOptionId) + } else { + self.innerSubject = nil + } } public func encode(_ encoder: PostboxEncoder) { @@ -51,10 +57,16 @@ public class ReplyMessageAttribute: MessageAttribute { encoder.encodeNil(forKey: "qu") } encoder.encodeBool(self.isQuote, forKey: "iq") - if let todoItemId = self.todoItemId { + switch self.innerSubject { + case let .todoItem(todoItemId): encoder.encodeInt32(todoItemId, forKey: "tid") - } else { + encoder.encodeNil(forKey: "pid") + case let .pollOption(pollOptionId): + encoder.encodeData(pollOptionId, forKey: "pid") encoder.encodeNil(forKey: "tid") + default: + encoder.encodeNil(forKey: "tid") + encoder.encodeNil(forKey: "pid") } } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_SynchronizeableChatInputState.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_SynchronizeableChatInputState.swift index c514083406..c250c60eba 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_SynchronizeableChatInputState.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_SynchronizeableChatInputState.swift @@ -40,7 +40,7 @@ public struct SynchronizeableChatInputState: Codable, Equatable { self.replySubject = replySubject } else { if let messageIdPeerId = try? container.decodeIfPresent(Int64.self, forKey: "m.p"), let messageIdNamespace = try? container.decodeIfPresent(Int32.self, forKey: "m.n"), let messageIdId = try? container.decodeIfPresent(Int32.self, forKey: "m.i") { - self.replySubject = EngineMessageReplySubject(messageId: MessageId(peerId: PeerId(messageIdPeerId), namespace: messageIdNamespace, id: messageIdId), quote: nil, todoItemId: nil) + self.replySubject = EngineMessageReplySubject(messageId: MessageId(peerId: PeerId(messageIdPeerId), namespace: messageIdNamespace, id: messageIdId), quote: nil, innerSubject: nil) } else { self.replySubject = nil } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift index 0d8e6dc1c4..e7d30b530f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift @@ -13,7 +13,7 @@ func _internal_enqueueOutgoingMessageWithChatContextResult(account: Account, to func _internal_outgoingMessageWithChatContextResult(to peerId: PeerId, threadId: Int64?, botId: PeerId, result: ChatContextResult, replyToMessageId: EngineMessageReplySubject?, replyToStoryId: StoryId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, sendPaidMessageStars: StarsAmount?, postpone: Bool, correlationId: Int64?) -> EnqueueMessage? { var replyToMessageId = replyToMessageId if replyToMessageId == nil, let threadId = threadId { - replyToMessageId = EngineMessageReplySubject(messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: MessageId.Id(clamping: threadId)), quote: nil, todoItemId: nil) + replyToMessageId = EngineMessageReplySubject(messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: MessageId.Id(clamping: threadId)), quote: nil, innerSubject: nil) } var webpageUrl: String? diff --git a/submodules/TelegramUI/Components/Chat/ChatInputMessageAccessoryPanel/Sources/ChatInputMessageAccessoryPanel.swift b/submodules/TelegramUI/Components/Chat/ChatInputMessageAccessoryPanel/Sources/ChatInputMessageAccessoryPanel.swift index 2892c5ba4a..b211f0390b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInputMessageAccessoryPanel/Sources/ChatInputMessageAccessoryPanel.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInputMessageAccessoryPanel/Sources/ChatInputMessageAccessoryPanel.swift @@ -103,13 +103,13 @@ public final class ChatInputMessageAccessoryPanel: Component { public final class Reply: Equatable { public let id: EngineMessage.Id public let quote: EngineMessageReplyQuote? - public let todoItemId: Int32? + public let innerSubject: EngineMessageReplyInnerSubject? public let message: EngineMessage? - public init(id: EngineMessage.Id, quote: EngineMessageReplyQuote?, todoItemId: Int32?, message: EngineMessage?) { + public init(id: EngineMessage.Id, quote: EngineMessageReplyQuote?, innerSubject: EngineMessageReplyInnerSubject?, message: EngineMessage?) { self.id = id self.quote = quote - self.todoItemId = todoItemId + self.innerSubject = innerSubject self.message = message } @@ -120,7 +120,7 @@ public final class ChatInputMessageAccessoryPanel: Component { if lhs.quote != rhs.quote { return false } - if lhs.todoItemId != rhs.todoItemId { + if lhs.innerSubject != rhs.innerSubject { return false } if lhs.message?.id != rhs.message?.id { @@ -597,9 +597,13 @@ public final class ChatInputMessageAccessoryPanel: Component { authorName = EnginePeer(author).displayTitle(strings: environment.strings, displayOrder: environment.nameDisplayOrder) } - if let _ = reply.todoItemId { + if case .todoItem = reply.innerSubject { let string = environment.strings.Chat_ReplyPanel_ReplyToTodoItem titleText = [.text(NSAttributedString(string: string, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor))] + } else if case .pollOption = reply.innerSubject { + //TODO:localize + let string = "Reply to Poll Option" + titleText = [.text(NSAttributedString(string: string, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor))] } else if let _ = reply.quote { let string = environment.strings.Chat_ReplyPanel_ReplyToQuoteBy(authorName).string titleText = [.text(NSAttributedString(string: string, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor))] @@ -628,9 +632,12 @@ public final class ChatInputMessageAccessoryPanel: Component { if let quote = reply.quote { let textColor = environment.theme.chat.inputPanel.primaryTextColor textString = stringWithAppliedEntities(trimToLineCount(quote.text, lineCount: 1), entities: quote.entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message._asMessage()) - } else if let todoItemId = reply.todoItemId, let todo = message.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo, let todoItem = todo.items.first(where: { $0.id == todoItemId }) { + } else if case let .todoItem(todoItemId) = reply.innerSubject, let todo = message.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo, let todoItem = todo.items.first(where: { $0.id == todoItemId }) { let textColor = environment.theme.chat.inputPanel.primaryTextColor textString = stringWithAppliedEntities(trimToLineCount(todoItem.text, lineCount: 1), entities: todoItem.entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message._asMessage()) + } else if case let .pollOption(pollOptionId) = reply.innerSubject, let poll = message.media.first(where: { $0 is TelegramMediaPoll }) as? TelegramMediaPoll, let pollOption = poll.options.first(where: { $0.opaqueIdentifier == pollOptionId }) { + let textColor = environment.theme.chat.inputPanel.primaryTextColor + textString = stringWithAppliedEntities(trimToLineCount(pollOption.text, lineCount: 1), entities: pollOption.entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message._asMessage()) } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index d73739878f..00da8a1024 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -1215,7 +1215,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { var replyMessage: Message? var replyForward: QuotedReplyMessageAttribute? var replyQuote: (quote: EngineMessageReplyQuote, isQuote: Bool)? - var replyTodoItemId: Int32? + var replyInnerSubject: EngineMessageReplyInnerSubject? var replyStory: StoryId? for attribute in item.message.attributes { if let attribute = attribute as? InlineBotMessageAttribute { @@ -1243,7 +1243,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { replyMessage = item.message.associatedMessages[replyAttribute.messageId] } replyQuote = replyAttribute.quote.flatMap { ($0, replyAttribute.isQuote) } - replyTodoItemId = replyAttribute.todoItemId + replyInnerSubject = replyAttribute.innerSubject } else if let quoteReplyAttribute = attribute as? QuotedReplyMessageAttribute { replyForward = quoteReplyAttribute } else if let attribute = attribute as? ReplyStoryAttribute { @@ -1283,7 +1283,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { message: replyMessage, replyForward: replyForward, quote: replyQuote, - todoItemId: replyTodoItemId, + innerSubject: replyInnerSubject, story: replyStory, isSummarized: false, parentMessage: item.message, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index af5cdf95c3..763e76e3e1 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -724,7 +724,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI private struct HighlightedState: Equatable { var quote: ChatInterfaceHighlightedState.Quote? - var todoTaskId: Int32? + var subject: EngineMessageReplyInnerSubject? } private var highlightedState: HighlightedState? @@ -1994,7 +1994,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI var replyMessage: Message? var replyForward: QuotedReplyMessageAttribute? var replyQuote: (quote: EngineMessageReplyQuote, isQuote: Bool)? - var replyTodoItemId: Int32? + var replyInnerSubject: EngineMessageReplyInnerSubject? var replyStory: StoryId? var replyMarkup: ReplyMarkupMessageAttribute? var authorNameColor: UIColor? @@ -2012,7 +2012,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI replyMessage = firstMessage.associatedMessages[attribute.messageId] } replyQuote = attribute.quote.flatMap { ($0, attribute.isQuote) } - replyTodoItemId = attribute.todoItemId + replyInnerSubject = attribute.innerSubject } else if let attribute = attribute as? QuotedReplyMessageAttribute { replyForward = attribute } else if let attribute = attribute as? ReplyStoryAttribute { @@ -2823,7 +2823,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI message: replyMessage, replyForward: replyForward, quote: replyQuote, - todoItemId: replyTodoItemId, + innerSubject: replyInnerSubject, story: replyStory, isSummarized: isSummarized, parentMessage: item.message, @@ -5599,7 +5599,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if let replyInfoNode = self.replyInfoNode { progress = replyInfoNode.makeProgress() } - item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote.flatMap { quote in NavigateToMessageParams.Quote(string: quote.text, offset: quote.offset) } : nil, todoTaskId: attribute.todoItemId, progress: progress)) + item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote.flatMap { quote in NavigateToMessageParams.Quote(string: quote.text, offset: quote.offset) } : nil, subject: attribute.innerSubject, progress: progress)) }, contextMenuOnLongPress: true)) } else if let attribute = attribute as? ReplyStoryAttribute { return .action(InternalBubbleTapAction.Action({ @@ -6423,7 +6423,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if let highlightedStateValue = item.controllerInteraction.highlightedState { for (message, _) in item.content { if highlightedStateValue.messageStableId == message.stableId { - highlightedState = HighlightedState(quote: highlightedStateValue.quote, todoTaskId: highlightedStateValue.todoTaskId) + highlightedState = HighlightedState(quote: highlightedStateValue.quote, subject: highlightedStateValue.subject) break } } @@ -6520,44 +6520,57 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } } }) - } else if highlightedState?.todoTaskId != nil { + } else if let replySubject = highlightedState?.subject { Queue.mainQueue().after(0.3, { [weak self] in guard let self, let _ = self.item, let backgroundHighlightNode = self.backgroundHighlightNode else { return } + let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) - if let highlightedState = self.highlightedState, let todoTaskId = highlightedState.todoTaskId { - let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) - - var taskFrame: CGRect? - for contentNode in self.contentNodes { - if let contentNode = contentNode as? ChatMessageTodoBubbleContentNode { - contentNode.updateTaskHighlightState(id: todoTaskId, color: highlightColor, animated: false) - var sourceFrame = backgroundHighlightNode.view.convert(backgroundHighlightNode.bounds, to: contentNode.view) - if item.message.effectivelyIncoming(item.context.account.peerId) { - sourceFrame.origin.x += 6.0 - sourceFrame.size.width -= 6.0 - } else { - sourceFrame.size.width -= 6.0 - } - - if let localFrame = contentNode.animateTaskItemHighlightIn(id: todoTaskId, sourceFrame: sourceFrame, transition: transition) { - taskFrame = contentNode.view.convert(localFrame, to: backgroundHighlightNode.view.superview).insetBy(dx: -3.0, dy: 0.0) - } - break + var itemFrame: CGRect? + + switch replySubject { + case let .todoItem(todoItemId): + if let contentNode = self.contentNodes.first(where: { $0 is ChatMessageTodoBubbleContentNode }) as? ChatMessageTodoBubbleContentNode { + contentNode.updateTaskHighlightState(id: todoItemId, color: highlightColor, animated: false) + var sourceFrame = backgroundHighlightNode.view.convert(backgroundHighlightNode.bounds, to: contentNode.view) + if item.message.effectivelyIncoming(item.context.account.peerId) { + sourceFrame.origin.x += 6.0 + sourceFrame.size.width -= 6.0 + } else { + sourceFrame.size.width -= 6.0 + } + + if let localFrame = contentNode.animateTaskItemHighlightIn(id: todoItemId, sourceFrame: sourceFrame, transition: transition) { + itemFrame = contentNode.view.convert(localFrame, to: backgroundHighlightNode.view.superview).insetBy(dx: -3.0, dy: 0.0) } } - - if let taskFrame { - self.backgroundHighlightNode = nil + case let .pollOption(pollOption): + if let contentNode = self.contentNodes.first(where: { $0 is ChatMessagePollBubbleContentNode }) as? ChatMessagePollBubbleContentNode { + contentNode.updateOptionHighlightState(id: pollOption, color: highlightColor, animated: false) + var sourceFrame = backgroundHighlightNode.view.convert(backgroundHighlightNode.bounds, to: contentNode.view) + if item.message.effectivelyIncoming(item.context.account.peerId) { + sourceFrame.origin.x += 6.0 + sourceFrame.size.width -= 6.0 + } else { + sourceFrame.size.width -= 6.0 + } - backgroundHighlightNode.updateLayout(size: taskFrame.size, transition: transition) - transition.updateFrame(node: backgroundHighlightNode, frame: taskFrame) - backgroundHighlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, delay: 0.05, removeOnCompletion: false, completion: { [weak backgroundHighlightNode] _ in - backgroundHighlightNode?.removeFromSupernode() - }) + if let localFrame = contentNode.animateOptionItemHighlightIn(id: pollOption, sourceFrame: sourceFrame, transition: transition) { + itemFrame = contentNode.view.convert(localFrame, to: backgroundHighlightNode.view.superview).insetBy(dx: -3.0, dy: 0.0) + } } } + + if let itemFrame { + self.backgroundHighlightNode = nil + + backgroundHighlightNode.updateLayout(size: itemFrame.size, transition: transition) + transition.updateFrame(node: backgroundHighlightNode, frame: itemFrame) + backgroundHighlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, delay: 0.05, removeOnCompletion: false, completion: { [weak backgroundHighlightNode] _ in + backgroundHighlightNode?.removeFromSupernode() + }) + } }) } } @@ -7085,12 +7098,15 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return nil } - public func getTodoTaskRect(id: Int32) -> CGRect? { - for contentNode in self.contentNodes { - if let contentNode = contentNode as? ChatMessageTodoBubbleContentNode { - if let result = contentNode.getTaskRect(id: id) { - return contentNode.view.convert(result, to: self.view) - } + public func getInnerReplySubjectRect(innerSubject: EngineMessageReplyInnerSubject) -> CGRect? { + switch innerSubject { + case let .todoItem(todoItemId): + if let contentNode = self.contentNodes.first(where: { $0 is ChatMessageTodoBubbleContentNode }) as? ChatMessageTodoBubbleContentNode, let result = contentNode.getTaskRect(id: todoItemId) { + return contentNode.view.convert(result, to: self.view) + } + case let .pollOption(pollOption): + if let contentNode = self.contentNodes.first(where: { $0 is ChatMessagePollBubbleContentNode }) as? ChatMessagePollBubbleContentNode, let result = contentNode.getOptionRect(id: pollOption) { + return contentNode.view.convert(result, to: self.view) } } return nil diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift index c1ae3540ca..e1a44d039f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift @@ -464,7 +464,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco var replyMessage: Message? var replyForward: QuotedReplyMessageAttribute? var replyQuote: (quote: EngineMessageReplyQuote, isQuote: Bool)? - var replyTodoItemId: Int32? + var replyInnerSubject: EngineMessageReplyInnerSubject? var replyStory: StoryId? for attribute in item.message.attributes { if let attribute = attribute as? InlineBotMessageAttribute { @@ -508,7 +508,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco replyMessage = item.message.associatedMessages[replyAttribute.messageId] } replyQuote = replyAttribute.quote.flatMap { ($0, replyAttribute.isQuote) } - replyTodoItemId = replyAttribute.todoItemId + replyInnerSubject = replyAttribute.innerSubject } else if let attribute = attribute as? QuotedReplyMessageAttribute { replyForward = attribute } else if let attribute = attribute as? ReplyStoryAttribute { @@ -528,7 +528,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco message: replyMessage, replyForward: replyForward, quote: replyQuote, - todoItemId: replyTodoItemId, + innerSubject: replyInnerSubject, story: replyStory, isSummarized: false, parentMessage: item.message, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift index 64a44892e7..70ac211208 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -352,9 +352,8 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { var replyMessage: Message? var replyForward: QuotedReplyMessageAttribute? var replyQuote: (quote: EngineMessageReplyQuote, isQuote: Bool)? - var replyTodoItemId: Int32? + var replyInnerSubject: EngineMessageReplyInnerSubject? var replyStory: StoryId? - for attribute in item.message.attributes { if let attribute = attribute as? InlineBotMessageAttribute { var inlineBotNameString: String? @@ -383,7 +382,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { replyMessage = item.message.associatedMessages[replyAttribute.messageId] } replyQuote = replyAttribute.quote.flatMap { ($0, replyAttribute.isQuote) } - replyTodoItemId = replyAttribute.todoItemId + replyInnerSubject = replyAttribute.innerSubject } else if let attribute = attribute as? QuotedReplyMessageAttribute { replyForward = attribute } else if let attribute = attribute as? ReplyStoryAttribute { @@ -402,7 +401,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { message: replyMessage, replyForward: replyForward, quote: replyQuote, - todoItemId: replyTodoItemId, + innerSubject: replyInnerSubject, story: replyStory, isSummarized: false, parentMessage: item.message, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/BUILD index 3cc089c040..5a28cfef60 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/BUILD @@ -31,6 +31,7 @@ swift_library( "//submodules/TelegramUI/Components/EmojiTextAttachmentView", "//submodules/PhotoResources", "//submodules/LocationResources", + "//submodules/TelegramUI/Components/ChatControllerInteraction", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift index 5d718c3aec..74dcd432e3 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift @@ -21,6 +21,7 @@ import MergedAvatarsNode import TextNodeWithEntities import ShimmeringLinkNode import EmojiTextAttachmentView +import ChatControllerInteraction private final class ChatMessagePollOptionRadioNodeParameters: NSObject { let timestamp: Double @@ -32,7 +33,7 @@ private final class ChatMessagePollOptionRadioNodeParameters: NSObject { let offset: Double? let isChecked: Bool? let checkTransition: ChatMessagePollOptionRadioNodeCheckTransition? - + init(timestamp: Double, isRectangle: Bool, staticColor: UIColor, animatedColor: UIColor, fillColor: UIColor, foregroundColor: UIColor, offset: Double?, isChecked: Bool?, checkTransition: ChatMessagePollOptionRadioNodeCheckTransition?) { self.timestamp = timestamp self.isRectangle = isRectangle @@ -43,7 +44,7 @@ private final class ChatMessagePollOptionRadioNodeParameters: NSObject { self.offset = offset self.isChecked = isChecked self.checkTransition = checkTransition - + super.init() } } @@ -53,7 +54,7 @@ private final class ChatMessagePollOptionRadioNodeCheckTransition { let duration: Double let previousValue: Bool let updatedValue: Bool - + init(startTime: Double, duration: Double, previousValue: Bool, updatedValue: Bool) { self.startTime = startTime self.duration = duration @@ -73,13 +74,13 @@ private final class ChatMessagePollOptionRadioNode: ASDisplayNode { private var startTime: Double? private var checkTransition: ChatMessagePollOptionRadioNodeCheckTransition? 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 = ChatMessagePollOptionRadioNodeCheckTransition(startTime: CACurrentMediaTime(), duration: 0.15, previousValue: previousValue, updatedValue: value) @@ -88,21 +89,21 @@ private final class ChatMessagePollOptionRadioNode: ASDisplayNode { 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 @@ -110,10 +111,10 @@ private final class ChatMessagePollOptionRadioNode: ASDisplayNode { self.updateAnimating() } } - + override func didExitHierarchy() { super.didExitHierarchy() - + let previous = self.shouldBeAnimating self.isInHierarchyValue = false let updated = self.shouldBeAnimating @@ -121,7 +122,7 @@ private final class ChatMessagePollOptionRadioNode: ASDisplayNode { self.updateAnimating() } } - + func update(isRectangle: Bool, staticColor: UIColor, animatedColor: UIColor, fillColor: UIColor, foregroundColor: UIColor, isSelectable: Bool, isAnimating: Bool) { var updated = false let shouldHaveBeenAnimating = self.shouldBeAnimating @@ -165,7 +166,7 @@ private final class ChatMessagePollOptionRadioNode: ASDisplayNode { self.setNeedsDisplay() } } - + private func updateAnimating() { let timestamp = CACurrentMediaTime() if let checkTransition = self.checkTransition { @@ -173,7 +174,7 @@ private final class ChatMessagePollOptionRadioNode: ASDisplayNode { self.checkTransition = nil } } - + if self.shouldBeAnimating { if self.isAnimating && self.startTime == nil { self.startTime = timestamp @@ -193,7 +194,7 @@ private final class ChatMessagePollOptionRadioNode: ASDisplayNode { 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() @@ -206,18 +207,18 @@ private final class ChatMessagePollOptionRadioNode: ASDisplayNode { 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? ChatMessagePollOptionRadioNodeParameters else { return } - + let context = UIGraphicsGetCurrentContext()! - + if let offset = parameters.offset { let t = max(0.0, offset) let colorFadeInDuration = 0.2 @@ -228,19 +229,19 @@ private final class ChatMessagePollOptionRadioNode: ASDisplayNode { 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 @@ -252,18 +253,18 @@ private final class ChatMessagePollOptionRadioNode: ASDisplayNode { 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 @@ -272,7 +273,7 @@ private final class ChatMessagePollOptionRadioNode: ASDisplayNode { 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 @@ -299,13 +300,13 @@ private final class ChatMessagePollOptionRadioNode: ASDisplayNode { 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) - + if parameters.isRectangle{ context.addPath(UIBezierPath(roundedRect: CGRect(origin: .zero, size: bounds.size).insetBy(dx: 0.5, dy: 0.5), cornerRadius: 6.0).cgPath) context.strokePath() @@ -313,7 +314,7 @@ private final class ChatMessagePollOptionRadioNode: ASDisplayNode { context.strokeEllipse(in: CGRect(origin: .zero, size: bounds.size).insetBy(dx: 0.5, dy: 0.5)) } } - + if !diameter.isZero { context.setFillColor(parameters.fillColor.withAlphaComponent(alpha).cgColor) if parameters.isRectangle{ @@ -322,11 +323,11 @@ private final class ChatMessagePollOptionRadioNode: ASDisplayNode { } else { 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) @@ -334,15 +335,15 @@ private final class ChatMessagePollOptionRadioNode: ASDisplayNode { 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) @@ -357,7 +358,7 @@ private final class ChatMessagePollOptionRadioNode: ASDisplayNode { } } else { context.setStrokeColor(parameters.staticColor.cgColor) - + if parameters.isRectangle { context.addPath(UIBezierPath(roundedRect: CGRect(origin: .zero, size: bounds.size).insetBy(dx: 0.5, dy: 0.5), cornerRadius: 6.0).cgPath) context.strokePath() @@ -448,7 +449,11 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { private static let avatarsSize = CGSize(width: MergedAvatarsNode.defaultMergedImageSize + MergedAvatarsNode.defaultMergedImageSpacing, height: MergedAvatarsNode.defaultMergedImageSize) private static let avatarsSpacing: CGFloat = 6.0 private static let countSpacing: CGFloat = 6.0 - + + private var extractedContainerView: UIView? + + fileprivate let contextSourceNode: ContextExtractedContentContainingNode + fileprivate let containerNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode private(set) var radioNode: ChatMessagePollOptionRadioNode? private let percentageNode: ASDisplayNode @@ -473,10 +478,13 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { private(set) var currentSelection: ChatMessagePollOptionSelection? var pressed: (() -> Void)? var selectionUpdated: (() -> Void)? + var longTapped: (() -> Void)? private var theme: PresentationTheme? - + weak var previousOptionNode: ChatMessagePollOptionNode? - + + private var ignoreNextTap = false + var visibilityRect: CGRect? { didSet { if self.visibilityRect != oldValue { @@ -490,24 +498,26 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { } } } - + override init() { + self.contextSourceNode = ContextExtractedContentContainingNode() + self.containerNode = ASDisplayNode() + self.highlightedBackgroundNode = ASDisplayNode() self.highlightedBackgroundNode.alpha = 0.0 self.highlightedBackgroundNode.isUserInteractionEnabled = false - + self.buttonNode = HighlightTrackingButtonNode() self.separatorNode = ASDisplayNode() - self.separatorNode.isLayerBacked = true - + self.resultBarBackgroundNode = ASImageNode() self.resultBarBackgroundNode.isLayerBacked = true self.resultBarBackgroundNode.alpha = 0.0 - + self.resultBarNode = ASImageNode() self.resultBarNode.isLayerBacked = true self.resultBarNode.alpha = 0.0 - + self.resultBarIconNode = ASImageNode() self.resultBarIconNode.isLayerBacked = true @@ -518,27 +528,32 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { self.countNode = ASDisplayNode() self.countNode.alpha = 0.0 self.countNode.isLayerBacked = true - + self.avatarsNode = MergedAvatarsNode() self.avatarsNode.alpha = 0.0 self.avatarsNode.isUserInteractionEnabled = false - + super.init() - + self.addSubnode(self.highlightedBackgroundNode) + self.addSubnode(self.contextSourceNode) + self.addSubnode(self.containerNode) self.addSubnode(self.separatorNode) - self.addSubnode(self.resultBarBackgroundNode) - self.addSubnode(self.resultBarNode) - self.addSubnode(self.resultBarIconNode) - self.addSubnode(self.percentageNode) - self.addSubnode(self.countNode) - self.addSubnode(self.avatarsNode) self.addSubnode(self.buttonNode) - + + self.containerNode.addSubnode(self.resultBarBackgroundNode) + self.containerNode.addSubnode(self.resultBarNode) + self.containerNode.addSubnode(self.resultBarIconNode) + self.containerNode.addSubnode(self.percentageNode) + self.containerNode.addSubnode(self.countNode) + self.containerNode.addSubnode(self.avatarsNode) + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) self.buttonNode.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { + strongSelf.containerNode.view.tag = 0x2bad + 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) @@ -546,16 +561,25 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { } 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 + + Queue.mainQueue().after(0.5) { + if strongSelf.highlightedBackgroundNode.alpha == 1.0 { + strongSelf.ignoreNextTap = true + strongSelf.longTapped?() + } + } } else { + strongSelf.containerNode.view.tag = 0 + 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 { @@ -564,18 +588,57 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { 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) } } } + + self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtractedToContextPreview, _ in + guard let self else { + return + } + if isExtractedToContextPreview { + self.buttonNode.highligthedChanged(false) + self.extractedContainerView?.removeFromSuperview() + self.extractedContainerView = nil + + if let extractedContainerView = self.containerNode.view.snapshotContentTree(unhide: true) { + extractedContainerView.frame = self.containerNode.bounds + self.contextSourceNode.contentNode.view.addSubview(extractedContainerView) + self.extractedContainerView = extractedContainerView + } + } + } + + self.contextSourceNode.isExtractedToContextPreviewUpdated = { [weak self] isExtracted in + guard let self else { + return + } + if !isExtracted { + self.extractedContainerView?.removeFromSuperview() + self.extractedContainerView = nil + } + } } - + + override func didLoad() { + super.didLoad() + + self.highlightedBackgroundNode.view.tag = 0x1bad + self.separatorNode.view.tag = 0x3bad + } + @objc private func buttonPressed() { + guard !self.ignoreNextTap else { + self.ignoreNextTap = false + return + } + if let radioNode = self.radioNode, let isChecked = radioNode.isChecked { radioNode.updateIsChecked(!isChecked, animated: true) self.selectionUpdated?() @@ -583,20 +646,20 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { self.pressed?() } } - + private func updateMediaVisibility() { let alpha: CGFloat = self.mediaHidden ? 0.0 : 1.0 self.mediaNode?.alpha = alpha self.stickerMediaLayer?.opacity = Float(alpha) } - + func setMediaHidden(_ hidden: Bool) { if self.mediaHidden != hidden { self.mediaHidden = hidden self.updateMediaVisibility() } } - + func transitionNode(media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { guard let currentMedia = self.currentMedia, currentMedia.isEqual(to: media), let mediaNode = self.mediaNode, !mediaNode.isHidden else { return nil @@ -605,17 +668,16 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { return (mediaNode?.view.snapshotContentTree(unhide: true), nil) }) } - - static func asyncLayout(_ maybeNode: ChatMessagePollOptionNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ translation: TranslationMessageAttribute.Additional?, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode))) { + + static func asyncLayout(_ maybeNode: ChatMessagePollOptionNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ translation: TranslationMessageAttribute.Additional?, _ optionResult: ChatMessagePollOptionResult?, _ hasAnyMedia: Bool, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode))) { let makeTitleLayout = TextNodeWithEntities.asyncLayout(maybeNode?.titleNode) let currentResult = maybeNode?.currentResult let currentSelection = maybeNode?.currentSelection let currentTheme = maybeNode?.theme - - return { context, presentationData, message, poll, option, translation, optionResult, constrainedWidth in + + return { context, presentationData, message, poll, option, translation, optionResult, hasAnyMedia, constrainedWidth in let leftInset: CGFloat = 50.0 let media = option.media - let hasMedia = media != nil // let hasRecentVoterAvatars = !(optionResult?.recentVoterPeerIds.isEmpty ?? true) // let avatarsInset: CGFloat // if hasRecentVoterAvatars { @@ -624,21 +686,21 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { // avatarsInset = 0.0 // } let mediaInset: CGFloat - if hasMedia { + if hasAnyMedia { mediaInset = ChatMessagePollOptionNode.mediaSize.width + ChatMessagePollOptionNode.mediaSpacing + ChatMessagePollOptionNode.mediaRightInset } else { mediaInset = 0.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 } - + let optionTextColor: UIColor = incoming ? presentationData.theme.theme.chat.message.incoming.primaryTextColor : presentationData.theme.theme.chat.message.outgoing.primaryTextColor let optionAttributedText = stringWithAppliedEntities( optionText, @@ -654,7 +716,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { blockQuoteFont: presentationData.messageFont, message: message ) - + let shouldHaveRadioNode = optionResult == nil let isSelectable: Bool if shouldHaveRadioNode, poll.kind.multipleAnswers, !Namespaces.Message.allNonRegular.contains(message.id.namespace) { @@ -662,7 +724,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { } else { isSelectable = false } - + let themeUpdated = presentationData.theme.theme !== currentTheme var updatedPercentageImage: UIImage? @@ -670,7 +732,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { let value = optionResult?.percent ?? 0 updatedPercentageImage = generatePercentageImage(presentationData: presentationData, incoming: incoming, value: value, targetValue: value) } - + let displayCount = optionResult != nil var updatedCountImage: UIImage? if displayCount && (currentResult != optionResult || themeUpdated) { @@ -685,14 +747,14 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { // countInset = 0.0 // } let rightInset: CGFloat = 10.0 + mediaInset - + 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(52.0, titleLayout.size.height + 28.0) - + var resultIcon: UIImage? var updatedResultIcon = false - + var selection: ChatMessagePollOptionSelection? if optionResult != nil { if let voters = poll.results.voters { @@ -734,14 +796,14 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { fillColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.bar : presentationData.theme.theme.chat.message.outgoing.polls.bar } context.setFillColor(fillColor.cgColor) - + if poll.kind.multipleAnswers { context.addPath(UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 4.0).cgPath) context.fillPath() } else { context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) } - + let strokeColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.barIconForeground : presentationData.theme.theme.chat.message.outgoing.polls.barIconForeground if strokeColor.alpha.isZero { context.setBlendMode(.copy) @@ -764,7 +826,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { }) } } - + return (titleLayout.size.width + leftInset + rightInset, { width in return (CGSize(width: width, height: contentHeight), { animated, inProgress, attemptSynchronous in let node: ChatMessagePollOptionNode @@ -773,7 +835,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { } else { node = ChatMessagePollOptionNode() } - + node.option = option let previousMedia = node.currentMedia node.currentMedia = media @@ -782,11 +844,11 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { node.currentResult = optionResult node.currentSelection = selection 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) { @@ -796,7 +858,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { fadeNode.contents = textContents fadeNode.frame = titleNode.textNode.frame fadeNode.isLayerBacked = true - node.addSubnode(fadeNode) + node.containerNode.addSubnode(fadeNode) fadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak fadeNode] _ in fadeNode?.removeFromSupernode() }) @@ -805,7 +867,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { } } } - + let titleNode = titleApply(TextNodeWithEntities.Arguments( context: context, cache: context.animationCache, @@ -821,22 +883,22 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { } if node.titleNode !== titleNode { node.titleNode = titleNode - node.addSubnode(titleNode.textNode) + node.containerNode.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 shouldHaveRadioNode { let radioNode: ChatMessagePollOptionRadioNode if let current = node.radioNode { radioNode = current } else { radioNode = ChatMessagePollOptionRadioNode() - node.addSubnode(radioNode) + node.containerNode.addSubnode(radioNode) node.radioNode = radioNode if animated { radioNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) @@ -874,21 +936,21 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { } } } - + if let updatedCountImage = updatedCountImage { node.countNode.contents = updatedCountImage.cgImage node.countImage = updatedCountImage } - + let recentVoterPeers: [Peer] if let optionResult { recentVoterPeers = optionResult.recentVoterPeerIds.compactMap { message.peers[$0] } } else { recentVoterPeers = [] } - + var trailingOriginX = width - rightInset - + let mediaFrame: CGRect? if let _ = media { mediaFrame = CGRect(origin: CGPoint(x: width - ChatMessagePollOptionNode.mediaRightInset - ChatMessagePollOptionNode.mediaSize.width, y: floor((contentHeight - ChatMessagePollOptionNode.mediaSize.height) * 0.5)), size: ChatMessagePollOptionNode.mediaSize) @@ -897,7 +959,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { mediaFrame = nil } node.mediaFrame = mediaFrame - + if !recentVoterPeers.isEmpty { let avatarsFrame = CGRect(origin: CGPoint(x: trailingOriginX + 15.0 - ChatMessagePollOptionNode.avatarsSize.width, y: floor((contentHeight - ChatMessagePollOptionNode.avatarsSize.height) * 0.5)), size: ChatMessagePollOptionNode.avatarsSize) node.avatarsNode.frame = avatarsFrame @@ -909,7 +971,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { node.avatarsNode.update(context: context, peers: [], synchronousLoad: attemptSynchronous, imageSize: MergedAvatarsNode.defaultMergedImageSize, imageSpacing: MergedAvatarsNode.defaultMergedImageSpacing, borderWidth: MergedAvatarsNode.defaultBorderWidth) node.avatarsNode.alpha = 0.0 } - + if let image = node.countImage, displayCount { let countFrame = CGRect(origin: CGPoint(x: trailingOriginX - image.size.width, y: floor((contentHeight - image.size.height) * 0.5)), size: image.size) node.countNode.frame = countFrame @@ -925,11 +987,11 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { } } } - + var isSticker = false if let media, let mediaFrame, let file = media as? TelegramMediaFile, file.isSticker || file.isCustomEmoji { isSticker = true - + let stickerLayer: InlineStickerItemLayer if let current = node.stickerMediaLayer, previousMedia?.isEqual(to: media) == true { stickerLayer = current @@ -950,7 +1012,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { dynamicColor: nil, loopCount: nil ) - node.layer.addSublayer(stickerLayer) + node.containerNode.layer.addSublayer(stickerLayer) node.stickerMediaLayer = stickerLayer } stickerLayer.frame = mediaFrame @@ -963,7 +1025,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { node.stickerMediaLayer = nil } } - + if let media, let mediaFrame, !isSticker { let mediaNode: TransformImageNode if let current = node.mediaNode { @@ -972,12 +1034,12 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { let current = TransformImageNode() current.contentAnimations = [.subsequentUpdates] node.mediaNode = current - node.addSubnode(current) + node.containerNode.addSubnode(current) mediaNode = current } mediaNode.isHidden = false mediaNode.frame = mediaFrame - + let mediaReference = AnyMediaReference.standalone(media: media) var imageSize = ChatMessagePollOptionNode.mediaSize if let image = media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) { @@ -1002,7 +1064,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { mediaNode.setSignal(chatMapSnapshotImage(engine: context.engine, resource: resource)) } } - + let makeLayout = mediaNode.asyncLayout() let apply = makeLayout(TransformImageArguments( corners: ImageCorners(radius: 10.0), @@ -1017,18 +1079,21 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { node.mediaNode = nil } node.setMediaHidden(node.mediaHidden) - + 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 - 10.0, height: UIScreenPixel)) - + node.containerNode.frame = CGRect(origin: .zero, size: CGSize(width: width, height: contentHeight)) + node.contextSourceNode.frame = CGRect(origin: .zero, size: CGSize(width: width, height: contentHeight)) + node.contextSourceNode.contentRect = CGRect(origin: .zero, size: CGSize(width: width, height: contentHeight)) + if node.resultBarBackgroundNode.image == nil || themeUpdated { node.resultBarBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 4.0, color: optionTextColor.withMultipliedAlpha(0.07)) } - + if node.resultBarNode.image == nil || updatedResultIcon { var isQuiz = false if case .quiz = poll.kind { @@ -1054,14 +1119,14 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { } else { fillColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.bar : presentationData.theme.theme.chat.message.outgoing.polls.bar } - + node.resultBarNode.image = generateStretchableFilledCircleImage(diameter: 4.0, color: fillColor) } - + if updatedResultIcon { node.resultBarIconNode.image = resultIcon } - + let minBarWidth: CGFloat = 6.0 let maxBarWidth = width - leftInset - rightInset let resultBarWidth = minBarWidth + floor((width - leftInset - rightInset - minBarWidth) * (optionResult?.normalized ?? 0.0)) @@ -1097,16 +1162,16 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { node.resultBarIconNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) } } - + node.buttonNode.isAccessibilityElement = shouldHaveRadioNode - + let previousResultBarWidth = minBarWidth + floor((width - leftInset - rightInset - minBarWidth) * (currentResult?.normalized ?? 0.0)) let previousFrame = CGRect(origin: CGPoint(x: leftInset, y: contentHeight - 6.0 - 1.0), size: CGSize(width: previousResultBarWidth, height: 6.0)) - + node.resultBarNode.layer.animateSpring(from: NSValue(cgPoint: previousFrame.center), to: NSValue(cgPoint: node.resultBarNode.frame.center), keyPath: "position", duration: 0.6, damping: 110.0) node.resultBarNode.layer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(), size: previousFrame.size)), to: NSValue(cgRect: CGRect(origin: CGPoint(), size: node.resultBarNode.frame.size)), keyPath: "bounds", duration: 0.6, damping: 110.0) } - + return node }) }) @@ -1119,34 +1184,34 @@ private let labelsFont = Font.regular(14.0) private final class SolutionButtonNode: HighlightableButtonNode { private let pressed: () -> Void let iconNode: ASImageNode - + private var theme: PresentationTheme? private var incoming: Bool? - + init(pressed: @escaping () -> Void) { self.pressed = pressed - + self.iconNode = ASImageNode() self.iconNode.displaysAsynchronously = false - + super.init() - + self.addSubnode(self.iconNode) - + self.addTarget(self, action: #selector(self.pressedEvent), forControlEvents: .touchUpInside) } - + @objc private func pressedEvent() { self.pressed() } - + func update(size: CGSize, theme: PresentationTheme, incoming: Bool) { if self.theme !== theme || self.incoming != incoming { self.theme = theme self.incoming = incoming self.iconNode.image = PresentationResourcesChat.chatBubbleLamp(theme, incoming: incoming) } - + if let image = self.iconNode.image { self.iconNode.frame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size) } @@ -1169,13 +1234,13 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { private var shuffledOptionOpaqueIdentifiers: [Data]? private var shimmeringNodes: [ShimmeringLinkNode] = [] private let temporaryHiddenMediaDisposable = MetaDisposable() - + private var poll: TelegramMediaPoll? - + public var solutionTipSourceNode: ASDisplayNode { return self.solutionButtonNode } - + override public var visibility: ListViewItemNodeVisibility { didSet { if oldValue != self.visibility { @@ -1197,59 +1262,59 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } } } - + 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.avatarsNode = MergedAvatarsNode() - + self.votersNode = TextNode() self.votersNode.isUserInteractionEnabled = false self.votersNode.contentMode = .topLeft self.votersNode.contentsScale = UIScreenScale self.votersNode.displaysAsynchronously = false self.votersNode.clipsToBounds = true - + var displaySolution: (() -> Void)? self.solutionButtonNode = SolutionButtonNode(pressed: { displaySolution?() }) self.solutionButtonNode.alpha = 0.0 - + self.buttonSubmitInactiveTextNode = TextNode() self.buttonSubmitInactiveTextNode.isUserInteractionEnabled = false self.buttonSubmitInactiveTextNode.contentMode = .topLeft self.buttonSubmitInactiveTextNode.contentsScale = UIScreenScale self.buttonSubmitInactiveTextNode.displaysAsynchronously = false - + self.buttonSubmitActiveTextNode = TextNode() self.buttonSubmitActiveTextNode.isUserInteractionEnabled = false self.buttonSubmitActiveTextNode.contentMode = .topLeft self.buttonSubmitActiveTextNode.contentsScale = UIScreenScale self.buttonSubmitActiveTextNode.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.avatarsNode) @@ -1259,14 +1324,14 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { self.addSubnode(self.buttonSubmitActiveTextNode) self.addSubnode(self.buttonViewResultsTextNode) self.addSubnode(self.buttonNode) - + displaySolution = { [weak self] in guard let strongSelf = self, let item = strongSelf.item, let poll = strongSelf.poll, let solution = poll.results.solution else { return } item.controllerInteraction.displayPollSolution(solution, strongSelf.solutionButtonNode) } - + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) self.buttonNode.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { @@ -1283,16 +1348,16 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } } } - + self.avatarsNode.pressed = { [weak self] in self?.buttonPressed() } } - + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + private func optionNodeForMedia(_ media: Media) -> ChatMessagePollOptionNode? { for optionNode in self.optionNodes { if let optionMedia = optionNode.option?.media, optionMedia.isEqual(to: media) { @@ -1307,7 +1372,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { guard poll.shuffleAnswers else { return (defaultOrderedOptions, nil) } - + let currentOpaqueIdentifiers = poll.options.map(\.opaqueIdentifier) let resolvedOpaqueIdentifiers: [Data] if let shuffledOptionOpaqueIdentifiers = self.shuffledOptionOpaqueIdentifiers, shuffledOptionOpaqueIdentifiers.count == currentOpaqueIdentifiers.count, Set(shuffledOptionOpaqueIdentifiers) == Set(currentOpaqueIdentifiers) { @@ -1317,7 +1382,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { generatedOpaqueIdentifiers.shuffle() resolvedOpaqueIdentifiers = generatedOpaqueIdentifiers } - + let indicesByOpaqueIdentifier = Dictionary(uniqueKeysWithValues: poll.options.enumerated().map { ($0.element.opaqueIdentifier, $0.offset) }) let orderedOptions = resolvedOpaqueIdentifiers.compactMap { opaqueIdentifier -> (Int, TelegramMediaPollOption)? in guard let index = indicesByOpaqueIdentifier[opaqueIdentifier] else { @@ -1325,21 +1390,21 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } return (index, poll.options[index]) } - + if orderedOptions.count == poll.options.count { return (orderedOptions, resolvedOpaqueIdentifiers) } else { return (defaultOrderedOptions, currentOpaqueIdentifiers) } } - + private func openOptionMedia(_ media: Media) { guard let item = self.item else { return } - + let _ = item - + // let message = item.message.withUpdatedMedia([media]) // let _ = item.context.sharedContext.openChatMessage(OpenChatMessageParams( // context: item.context, @@ -1422,12 +1487,12 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { // gallerySource: .standaloneMessage(message, 0) // )) } - + @objc private func buttonPressed() { guard let item = self.item, let poll = self.poll, let pollId = poll.id else { return } - + var hasSelection = false var selectedOpaqueIdentifiers: [Data] = [] for optionNode in self.optionNodes { @@ -1448,7 +1513,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { item.controllerInteraction.requestSelectMessagePollOptions(item.message.id, selectedOpaqueIdentifiers) } } - + 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) @@ -1457,7 +1522,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { let makeSubmitActiveTextLayout = TextNode.asyncLayout(self.buttonSubmitActiveTextNode) let makeViewResultsTextLayout = TextNode.asyncLayout(self.buttonViewResultsTextNode) let statusLayout = self.statusNode.asyncLayout() - + var previousPoll: TelegramMediaPoll? if let item = self.item { for media in item.message.media { @@ -1466,31 +1531,31 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } } } - - var previousOptionNodeLayouts: [Data: (_ contet: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ translation: TranslationMessageAttribute.Additional?, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode)))] = [:] + + var previousOptionNodeLayouts: [Data: (_ contet: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ translation: TranslationMessageAttribute.Additional?, _ optionResult: ChatMessagePollOptionResult?, _ hasAnyMedia: Bool, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode)))] = [:] for optionNode in self.optionNodes { if let option = optionNode.option { previousOptionNodeLayouts[option.opaqueIdentifier] = ChatMessagePollOptionNode.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) var isBotChat: Bool = false if let peer = item.message.peers[item.message.id.peerId] as? TelegramUser, peer.botInfo != nil { isBotChat = true } - + 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 @@ -1515,7 +1580,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { starsCount = attribute.stars.value } } - + let dateFormat: MessageTimestampStatusFormat if item.presentationData.isPreview { dateFormat = .full @@ -1523,7 +1588,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { 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 @@ -1545,15 +1610,15 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { 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, @@ -1580,7 +1645,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { animationRenderer: item.controllerInteraction.presentationContext.animationRenderer )) } - + var poll: TelegramMediaPoll? for media in item.message.media { if let media = media as? TelegramMediaPoll { @@ -1590,12 +1655,12 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } let messageTheme = incoming ? item.presentationData.theme.theme.chat.message.incoming : item.presentationData.theme.theme.chat.message.outgoing - - + + var pollTitleText = poll?.text ?? "" var pollTitleEntities = poll?.textEntities ?? [] var pollOptions: [TranslationMessageAttribute.Additional] = [] - + var isTranslating = false if let poll, let translateToLanguage = item.associatedData.translateToLanguage, !poll.text.isEmpty && incoming { isTranslating = true @@ -1609,7 +1674,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } } } - + let attributedText = stringWithAppliedEntities( pollTitleText, entities: pollTitleEntities, @@ -1624,12 +1689,12 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { 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 - + var avatarPeers: [Peer] = [] if let poll = poll { for peerId in poll.results.recentVoters { @@ -1638,7 +1703,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } } } - + if let poll = poll, isPollEffectivelyClosed(message: message, poll: poll) { typeText = item.presentationData.strings.MessagePoll_LabelClosed } else if let poll = poll { @@ -1662,9 +1727,9 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { typeText = item.presentationData.strings.MessagePoll_LabelAnonymous } 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())) - + let votersString: String? - + if isBotChat { votersString = nil } else if let poll = poll, let totalVoters = poll.results.totalVoters { @@ -1686,7 +1751,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { votersString = " " } let (votersLayout, votersApply) = makeVotersLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: votersString ?? "", font: labelsFont, textColor: messageTheme.secondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets)) - + let viewResultsString: String if let poll = poll, let totalVoters = poll.results.totalVoters { //TODO:localize @@ -1694,38 +1759,38 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } else { viewResultsString = item.presentationData.strings.MessagePoll_ViewResults } - + let (buttonSubmitInactiveTextLayout, buttonSubmitInactiveTextApply) = makeSubmitInactiveTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.MessagePoll_SubmitVote, font: Font.regular(17.0), textColor: messageTheme.accentControlDisabledColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets)) let (buttonSubmitActiveTextLayout, buttonSubmitActiveTextApply) = makeSubmitActiveTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.MessagePoll_SubmitVote, font: Font.regular(17.0), textColor: messageTheme.polls.bar), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets)) let (buttonViewResultsTextLayout, buttonViewResultsTextApply) = makeViewResultsTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: viewResultsString, font: Font.regular(17.0), textColor: messageTheme.polls.bar), 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, votersLayout.size.width + 4.0/* + (statusSize?.width ?? 0.0)*/) boundingSize.width = max(boundingSize.width, buttonSubmitInactiveTextLayout.size.width + 4.0/* + (statusSize?.width ?? 0.0)*/) 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 - + let isClosed: Bool if let poll = poll { isClosed = isPollEffectivelyClosed(message: message, poll: poll) } else { isClosed = false } - + var pollOptionsFinalizeLayouts: [(CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode)] = [] var orderedPollOptions: [(Int, TelegramMediaPollOption)] = [] var shuffledOptionOpaqueIdentifiers: [Data]? @@ -1733,7 +1798,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { let resolvedOptionOrder = self.resolvedOptionOrder(for: poll) orderedPollOptions = resolvedOptionOrder.orderedOptions shuffledOptionOpaqueIdentifiers = resolvedOptionOrder.shuffledOpaqueIdentifiers - + var optionVoterCount: [Int: Int32] = [:] var maxOptionVoterCount: Int32 = 0 var totalVoterCount: Int32 = 0 @@ -1764,17 +1829,19 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } } } - + var optionVoterCounts: [Int] if totalVoterCount != 0 { optionVoterCounts = countNicePercent(votes: (0 ..< poll.options.count).map({ Int(optionVoterCount[$0] ?? 0) }), total: Int(totalVoterCount)) } else { optionVoterCounts = Array(repeating: 0, count: poll.options.count) } - + + let hasAnyOptionMedia = orderedPollOptions.contains(where: { $0.1.media != nil }) + for (i, option) in orderedPollOptions { - - let makeLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ translation: TranslationMessageAttribute.Additional?, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode))) + + let makeLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ translation: TranslationMessageAttribute.Additional?, _ optionResult: ChatMessagePollOptionResult?, _ hasAnyMedia: Bool, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode))) if let previous = previousOptionNodeLayouts[option.opaqueIdentifier] { makeLayout = previous } else { @@ -1797,20 +1864,20 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } else if isClosed { optionResult = ChatMessagePollOptionResult(normalized: 0, percent: 0, count: 0, recentVoterPeerIds: recentVoterPeerIds) } - + var translation: TranslationMessageAttribute.Additional? if !pollOptions.isEmpty && i < pollOptions.count { translation = pollOptions[i] } - - let result = makeLayout(item.context, item.presentationData, item.message, poll, option, translation, optionResult, constrainedSize.width - layoutConstants.bubble.borderInset * 2.0) + + let result = makeLayout(item.context, item.presentationData, item.message, poll, option, translation, optionResult, hasAnyOptionMedia, 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)) - + var canVote = false if (item.message.id.namespace == Namespaces.Message.Cloud || Namespaces.Message.allNonRegular.contains(item.message.id.namespace)), let poll = poll, poll.pollId.namespace == Namespaces.Media.CloudPoll, !isClosed { var hasVoted = false @@ -1826,14 +1893,14 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { canVote = true } } - + 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) -> ChatMessagePollOptionNode)] = [] for finalizeLayout in pollOptionsFinalizeLayouts { let result = finalizeLayout(boundingWidth - layoutConstants.bubble.borderInset * 2.0) @@ -1841,7 +1908,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { resultSize.height += result.0.height optionNodesSizesAndApply.append(result) } - + let optionsVotersSpacing: CGFloat = 11.0 let optionsButtonSpacing: CGFloat = 9.0 let votersBottomSpacing: CGFloat = 11.0 @@ -1850,25 +1917,25 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } else { resultSize.height += 26.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 buttonSubmitInactiveTextFrame = CGRect(origin: CGPoint(x: floor((resultSize.width - buttonSubmitInactiveTextLayout.size.width) / 2.0), y: optionsButtonSpacing), size: buttonSubmitInactiveTextLayout.size) let buttonSubmitActiveTextFrame = CGRect(origin: CGPoint(x: floor((resultSize.width - buttonSubmitActiveTextLayout.size.width) / 2.0), y: optionsButtonSpacing), size: buttonSubmitActiveTextLayout.size) 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.poll = poll - + let cachedLayout = strongSelf.textNode.textNode.cachedLayout if case .System = animation { if let cachedLayout = cachedLayout { @@ -1888,7 +1955,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } } } - + let _ = textApply(TextNodeWithEntities.Arguments( context: item.context, cache: item.context.animationCache, @@ -1897,7 +1964,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { attemptSynchronous: synchronousLoad) ) let _ = typeApply() - + var verticalOffset = textFrame.maxY + titleTypeSpacing + typeLayout.size.height + typeOptionsSpacing var updatedOptionNodes: [ChatMessagePollOptionNode] = [] for i in 0 ..< optionNodesSizesAndApply.count { @@ -1913,28 +1980,33 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { if optionNode.supernode !== strongSelf { strongSelf.addSubnode(optionNode) let option = optionNode.option - optionNode.pressed = { - guard let strongSelf = self, let item = strongSelf.item, let option = option else { + optionNode.pressed = { [weak self] in + guard let self, let item = self.item, let option else { return } - item.controllerInteraction.requestSelectMessagePollOptions(item.message.id, [option.opaqueIdentifier]) } - optionNode.selectionUpdated = { - guard let strongSelf = self else { + optionNode.selectionUpdated = { [weak self] in + guard let self else { return } - strongSelf.updateSelection() + self.updateSelection() + } + optionNode.longTapped = { [weak self] in + guard let self, let item = self.item, let option else { + return + } + item.controllerInteraction.pollOptionLongTap(option.opaqueIdentifier, ChatControllerInteraction.LongTapParams(message: item.message, contentNode: optionNode.contextSourceNode, messageNode: strongSelf, progress: nil)) } optionNode.frame = optionNodeFrame } else { animation.animator.updateFrame(layer: optionNode.layer, frame: optionNodeFrame, completion: nil) } - + verticalOffset += size.height updatedOptionNodes.append(optionNode) optionNode.isUserInteractionEnabled = canVote && item.controllerInteraction.pollActionState.pollMessageIdsInProgress[item.message.id] == nil - + if i > 0 { optionNode.previousOptionNode = updatedOptionNodes[i - 1] } @@ -1946,7 +2018,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } strongSelf.optionNodes = updatedOptionNodes strongSelf.shuffledOptionOpaqueIdentifiers = shuffledOptionOpaqueIdentifiers - + 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 { @@ -1954,11 +2026,11 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } 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) - + let deadlineTimeout = poll?.deadlineTimeout var displayDeadline = true var hasSelected = false - + if let poll = poll { if let voters = poll.results.voters { for voter in voters { @@ -1970,10 +2042,10 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } } } - + if let deadlineTimeout = deadlineTimeout, !isClosed { var endDate: Int32? - + if message.id.namespace == Namespaces.Message.Cloud { let startDate: Int32 if let forwardInfo = message.forwardInfo { @@ -1983,7 +2055,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } endDate = startDate + deadlineTimeout } - + let timerNode: PollBubbleTimerNode if let current = strongSelf.timerNode { timerNode = current @@ -2008,7 +2080,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } //item.controllerInteraction.requestMessageUpdate(item.message.id) } - + let timerTransition: ContainedViewLayoutTransition if animation.isAnimated { timerTransition = .animated(duration: 0.25, curve: .easeInOut) @@ -2026,7 +2098,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { timerNode.frame = CGRect(origin: CGPoint(x: resultSize.width - layoutConstants.text.bubbleInsets.right, y: typeFrame.minY), size: CGSize()) } else if let timerNode = strongSelf.timerNode { strongSelf.timerNode = nil - + let timerTransition: ContainedViewLayoutTransition if animation.isAnimated { timerTransition = .animated(duration: 0.25, curve: .easeInOut) @@ -2038,11 +2110,11 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { }) timerTransition.updateTransformScale(node: timerNode, scale: 0.1) } - + let solutionButtonSize = CGSize(width: 32.0, height: 32.0) let solutionButtonFrame = CGRect(origin: CGPoint(x: resultSize.width - layoutConstants.text.bubbleInsets.right - solutionButtonSize.width + 5.0, y: typeFrame.minY - 16.0), size: solutionButtonSize) strongSelf.solutionButtonNode.frame = solutionButtonFrame - + if (strongSelf.timerNode == nil || !displayDeadline), let poll = poll, case .quiz = poll.kind, let _ = poll.results.solution, (isClosed || hasSelected) { if strongSelf.solutionButtonNode.alpha.isZero { let timerTransition: ContainedViewLayoutTransition @@ -2063,7 +2135,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } timerTransition.updateAlpha(node: strongSelf.solutionButtonNode, alpha: 0.0) } - + let avatarsFrame = CGRect(origin: CGPoint(x: typeFrame.maxX + 6.0, y: typeFrame.minY + floor((typeFrame.height - MergedAvatarsNode.defaultMergedImageSize) / 2.0)), size: CGSize(width: MergedAvatarsNode.defaultMergedImageSize + MergedAvatarsNode.defaultMergedImageSpacing * 2.0, height: MergedAvatarsNode.defaultMergedImageSize)) strongSelf.avatarsNode.frame = avatarsFrame strongSelf.avatarsNode.updateLayout(size: avatarsFrame.size) @@ -2076,20 +2148,20 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } else { alphaTransition = .immediate } - + let _ = votersApply() let votersFrame = CGRect(origin: CGPoint(x: floor((resultSize.width - votersLayout.size.width) / 2.0), y: verticalOffset + optionsVotersSpacing), size: votersLayout.size) animation.animator.updateFrame(layer: strongSelf.votersNode.layer, frame: votersFrame, completion: nil) - + if animation.isAnimated, let previousPoll = previousPoll, let poll = poll { if previousPoll.results.totalVoters == nil && poll.results.totalVoters != nil { strongSelf.votersNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } } - + if let statusSizeAndApply = statusSizeAndApply { let statusFrame = CGRect(origin: CGPoint(x: resultSize.width - statusSizeAndApply.0.width - layoutConstants.text.bubbleInsets.right, y: votersFrame.maxY), size: statusSizeAndApply.0) - + if strongSelf.statusNode.supernode == nil { statusSizeAndApply.1(.None) strongSelf.statusNode.frame = statusFrame @@ -2101,21 +2173,21 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } else if strongSelf.statusNode.supernode != nil { strongSelf.statusNode.removeFromSupernode() } - + let _ = buttonSubmitInactiveTextApply() strongSelf.buttonSubmitInactiveTextNode.frame = buttonSubmitInactiveTextFrame.offsetBy(dx: 0.0, dy: verticalOffset) - + let _ = buttonSubmitActiveTextApply() strongSelf.buttonSubmitActiveTextNode.frame = buttonSubmitActiveTextFrame.offsetBy(dx: 0.0, dy: verticalOffset) - + 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.updateSelection() strongSelf.updatePollTooltipMessageState(animated: false) - + strongSelf.updateIsTranslating(isTranslating) } }) @@ -2123,7 +2195,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { }) } } - + private func updateIsTranslating(_ isTranslating: Bool) { guard let item = self.item else { return @@ -2131,14 +2203,14 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { 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 { let color: UIColor @@ -2161,7 +2233,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } 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 @@ -2170,21 +2242,21 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } } } - + private func updateSelection() { guard let item = self.item, let poll = self.poll else { return } - + var isBotChat: Bool = false if let peer = item.message.peers[item.message.id.peerId] as? TelegramUser, peer.botInfo != nil { isBotChat = true } - + let disableAllActions = false - + var hasSelection = poll.kind.multipleAnswers - + var hasSelectedOptions = false for optionNode in self.optionNodes { if let isChecked = optionNode.radioNode?.isChecked { @@ -2193,9 +2265,9 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } } } - + let isClosed = isPollEffectivelyClosed(message: item.message, poll: poll) - + var hasResults = false if isClosed { hasResults = true @@ -2215,7 +2287,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } } } - + if !disableAllActions && hasSelection && !hasResults && poll.pollId.namespace == Namespaces.Media.CloudPoll { self.votersNode.isHidden = true self.buttonViewResultsTextNode.isHidden = true @@ -2226,7 +2298,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } else { if case .public = poll.publicity, hasResults, !disableAllActions { self.votersNode.isHidden = true - + if isBotChat { self.buttonViewResultsTextNode.isHidden = true self.buttonNode.isHidden = true @@ -2234,7 +2306,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { self.buttonViewResultsTextNode.isHidden = false self.buttonNode.isHidden = false } - + if Namespaces.Message.allNonRegular.contains(item.message.id.namespace) { self.buttonNode.isUserInteractionEnabled = false } else { @@ -2249,25 +2321,25 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { self.buttonSubmitInactiveTextNode.isHidden = true self.buttonSubmitActiveTextNode.isHidden = true } - + self.avatarsNode.isUserInteractionEnabled = !self.buttonViewResultsTextNode.isHidden } - + 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)) { @@ -2293,7 +2365,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { if let item = self.item, let peer = item.message.peers[item.message.id.peerId] as? TelegramUser, peer.botInfo != nil { isBotChat = true } - + for optionNode in self.optionNodes { if optionNode.frame.contains(point), case .tap = gesture { if let mediaFrame = optionNode.mediaFrame, mediaFrame.offsetBy(dx: optionNode.frame.minX, dy: optionNode.frame.minY).contains(point), let media = optionNode.option?.media { @@ -2354,14 +2426,14 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { return ChatMessageBubbleContentTapAction(content: .none) } } - + override public func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { guard let item = self.item, item.message.id == messageId, let optionNode = self.optionNodeForMedia(media), let transitionNode = optionNode.transitionNode(media: media) else { return nil } return (transitionNode.0, transitionNode.1, transitionNode.2) } - + override public func updateHiddenMedia(_ media: [Media]?) -> Bool { var updated = false for optionNode in self.optionNodes { @@ -2376,7 +2448,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } return updated } - + public func updatePollTooltipMessageState(animated: Bool) { guard let item = self.item else { return @@ -2393,18 +2465,112 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { transition.updateSublayerTransformScale(node: self.solutionButtonNode, scale: displaySolutionButton ? 1.0 : 0.1) } } - + 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 } + + public func getOptionRect(id: Data?) -> CGRect? { + var rectsSet: [CGRect] = [] + for node in self.optionNodes { + if node.option?.opaqueIdentifier == id { + rectsSet.append(node.frame.insetBy(dx: 3.0 - UIScreenPixel, dy: 2.0 - UIScreenPixel)) + } + } + if !rectsSet.isEmpty { + var currentRect = CGRect() + for rect in rectsSet { + if currentRect.isEmpty { + currentRect = rect + } else { + currentRect = currentRect.union(rect) + } + } + + return currentRect.offsetBy(dx: self.textNode.textNode.frame.minX, dy: self.textNode.textNode.frame.minY) + } + return nil + } + + private var optionHighlightingNode: LinkHighlightingNode? + public func updateOptionHighlightState(id: Data?, color: UIColor, animated: Bool) { + var rectsSet: [CGRect] = [] + for node in self.optionNodes { + if node.option?.opaqueIdentifier == id { + rectsSet.append(node.frame.insetBy(dx: 3.0 - UIScreenPixel, dy: 2.0 - UIScreenPixel)) + } + } + if !rectsSet.isEmpty { + let rects = rectsSet + let optionHighlightingNode: LinkHighlightingNode + if let current = self.optionHighlightingNode { + optionHighlightingNode = current + } else { + optionHighlightingNode = LinkHighlightingNode(color: color) + optionHighlightingNode.innerRadius = 0.0 + optionHighlightingNode.outerRadius = 0.0 + self.optionHighlightingNode = optionHighlightingNode + self.insertSubnode(optionHighlightingNode, belowSubnode: self.buttonNode) + } + optionHighlightingNode.frame = self.bounds + optionHighlightingNode.updateRects(rects) + } else { + if let optionHighlightingNode = self.optionHighlightingNode { + self.optionHighlightingNode = nil + if animated { + optionHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak optionHighlightingNode] _ in + optionHighlightingNode?.removeFromSupernode() + }) + } else { + optionHighlightingNode.removeFromSupernode() + } + } + } + } + + public func animateOptionItemHighlightIn(id: Data, sourceFrame: CGRect, transition: ContainedViewLayoutTransition) -> CGRect? { + if let optionHighlightingNode = self.optionHighlightingNode { + var currentRect = CGRect() + for rect in optionHighlightingNode.rects { + if currentRect.isEmpty { + currentRect = rect + } else { + currentRect = currentRect.union(rect) + } + } + if !currentRect.isEmpty { + currentRect = currentRect.insetBy(dx: -optionHighlightingNode.inset, dy: -optionHighlightingNode.inset) + let innerRect = currentRect.offsetBy(dx: optionHighlightingNode.frame.minX, dy: optionHighlightingNode.frame.minY) + + optionHighlightingNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, delay: 0.04) + + let fromScale = CGPoint(x: sourceFrame.width / innerRect.width, y: sourceFrame.height / innerRect.height) + + var fromTransform = CATransform3DIdentity + let fromOffset = CGPoint(x: sourceFrame.midX - innerRect.midX, y: sourceFrame.midY - innerRect.midY) + + fromTransform = CATransform3DTranslate(fromTransform, fromOffset.x, fromOffset.y, 0.0) + + fromTransform = CATransform3DTranslate(fromTransform, -optionHighlightingNode.bounds.width * 0.5 + currentRect.midX, -optionHighlightingNode.bounds.height * 0.5 + currentRect.midY, 0.0) + fromTransform = CATransform3DScale(fromTransform, fromScale.x, fromScale.y, 1.0) + fromTransform = CATransform3DTranslate(fromTransform, optionHighlightingNode.bounds.width * 0.5 - currentRect.midX, optionHighlightingNode.bounds.height * 0.5 - currentRect.midY, 0.0) + + optionHighlightingNode.transform = fromTransform + transition.updateTransform(node: optionHighlightingNode, transform: CGAffineTransformIdentity) + + return currentRect.offsetBy(dx: optionHighlightingNode.frame.minX, dy: optionHighlightingNode.frame.minY) + } + } + return nil + } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift index 350ee0afef..c57f74ffd7 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift @@ -83,7 +83,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { public let message: Message? public let replyForward: QuotedReplyMessageAttribute? public let quote: (quote: EngineMessageReplyQuote, isQuote: Bool)? - public let todoItemId: Int32? + public let innerSubject: EngineMessageReplyInnerSubject? public let story: StoryId? public let isSummarized: Bool public let parentMessage: Message @@ -100,7 +100,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { message: Message?, replyForward: QuotedReplyMessageAttribute?, quote: (quote: EngineMessageReplyQuote, isQuote: Bool)?, - todoItemId: Int32?, + innerSubject: EngineMessageReplyInnerSubject?, story: StoryId?, isSummarized: Bool, parentMessage: Message, @@ -116,7 +116,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { self.message = message self.replyForward = replyForward self.quote = quote - self.todoItemId = todoItemId + self.innerSubject = innerSubject self.story = story self.isSummarized = isSummarized self.parentMessage = parentMessage @@ -446,7 +446,11 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { var textLeftInset: CGFloat = 0.0 var messageText: NSAttributedString var todoItemCompleted: Bool? - if let todoItemId = arguments.todoItemId, let todo = arguments.message?.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo, let todoItem = todo.items.first(where: { $0.id == todoItemId }) { + + if case let .pollOption(optionId) = arguments.innerSubject, let poll = arguments.message?.media.first(where: { $0 is TelegramMediaPoll }) as? TelegramMediaPoll, let pollOption = poll.options.first(where: { $0.opaqueIdentifier == optionId }) { + messageText = stringWithAppliedEntities(pollOption.text, entities: pollOption.entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: nil) + textLeftInset += 16.0 + } else if case let .todoItem(todoItemId) = arguments.innerSubject, let todo = arguments.message?.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo, let todoItem = todo.items.first(where: { $0.id == todoItemId }) { messageText = stringWithAppliedEntities(todoItem.text, entities: todoItem.entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: nil) textLeftInset += 16.0 diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index cf4fff8c4a..bbea729223 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -706,7 +706,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { var replyMessage: Message? var replyForward: QuotedReplyMessageAttribute? var replyQuote: (quote: EngineMessageReplyQuote, isQuote: Bool)? - var replyTodoItemId: Int32? + var replyInnerSubject: EngineMessageReplyInnerSubject? var replyStory: StoryId? for attribute in item.message.attributes { if let attribute = attribute as? InlineBotMessageAttribute { @@ -735,7 +735,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { replyMessage = item.message.associatedMessages[replyAttribute.messageId] } replyQuote = replyAttribute.quote.flatMap { ($0, replyAttribute.isQuote) } - replyTodoItemId = replyAttribute.todoItemId + replyInnerSubject = replyAttribute.innerSubject } else if let attribute = attribute as? QuotedReplyMessageAttribute { replyForward = attribute } else if let attribute = attribute as? ReplyStoryAttribute { @@ -775,7 +775,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { message: replyMessage, replyForward: replyForward, quote: replyQuote, - todoItemId: replyTodoItemId, + innerSubject: replyInnerSubject, story: replyStory, isSummarized: false, parentMessage: item.message, diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index c18599f74e..701578877d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -572,6 +572,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { } } }, todoItemLongTap: { _, _ in + }, pollOptionLongTap: { _, _ in }, openCheckoutOrReceipt: { _, _ in }, openSearch: { }, setupReply: { _ in diff --git a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift index adbdd583ef..48d5b879ba 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift @@ -434,7 +434,8 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess }, chatControllerNode: { return nil }, presentGlobalOverlayController: { _, _ in }, callPeer: { _, _ in }, openConferenceCall: { _ in - }, longTap: { _, _ in }, todoItemLongTap: { _, _ in }, openCheckoutOrReceipt: { _, _ in }, openSearch: { }, setupReply: { _ in + }, longTap: { _, _ in }, todoItemLongTap: { _, _ in }, pollOptionLongTap: { _, _ in + }, openCheckoutOrReceipt: { _, _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { _ in return .none }, canSendMessages: { diff --git a/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/Sources/ReplyAccessoryPanelNode.swift b/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/Sources/ReplyAccessoryPanelNode.swift index 9170830f35..574d0f2f29 100644 --- a/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/Sources/ReplyAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/Sources/ReplyAccessoryPanelNode.swift @@ -26,7 +26,7 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode { public let chatPeerId: EnginePeer.Id public let messageId: MessageId public let quote: EngineMessageReplyQuote? - public let todoItemId: Int32? + public let innerSubject: EngineMessageReplyInnerSubject? private var previousMediaReference: AnyMediaReference? @@ -47,11 +47,11 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode { private var validLayout: (size: CGSize, inset: CGFloat, interfaceState: ChatPresentationInterfaceState)? - public init(context: AccountContext, chatPeerId: EnginePeer.Id, messageId: MessageId, quote: EngineMessageReplyQuote?, todoItemId: Int32?, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, animationCache: AnimationCache?, animationRenderer: MultiAnimationRenderer?) { + public init(context: AccountContext, chatPeerId: EnginePeer.Id, messageId: MessageId, quote: EngineMessageReplyQuote?, innerSubject: EngineMessageReplyInnerSubject?, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, animationCache: AnimationCache?, animationRenderer: MultiAnimationRenderer?) { self.chatPeerId = chatPeerId self.messageId = messageId self.quote = quote - self.todoItemId = todoItemId + self.innerSubject = innerSubject self.context = context self.theme = theme @@ -269,9 +269,13 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode { } } } else { - if let _ = strongSelf.todoItemId { + if case .todoItem = strongSelf.innerSubject { let string = strongSelf.strings.Chat_ReplyPanel_ReplyToTodoItem titleText = [.text(NSAttributedString(string: string, font: Font.medium(15.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor))] + } else if case .pollOption = strongSelf.innerSubject { + //TODO:localize + let string = "Reply to Poll Option" + titleText = [.text(NSAttributedString(string: string, font: Font.medium(15.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor))] } else if let _ = strongSelf.quote { let string = strongSelf.strings.Chat_ReplyPanel_ReplyToQuoteBy(authorName).string titleText = [.text(NSAttributedString(string: string, font: Font.medium(15.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor))] @@ -303,10 +307,14 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode { let quoteText = stringWithAppliedEntities(trimToLineCount(quote.text, lineCount: 1), entities: quote.entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message) strongSelf.textNode.attributedText = quoteText - } else if let todoItemId = strongSelf.todoItemId, let todo = message?.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo, let todoItem = todo.items.first(where: { $0.id == todoItemId }) { + } else if case let .todoItem(todoItemId) = strongSelf.innerSubject, let todo = message?.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo, let todoItem = todo.items.first(where: { $0.id == todoItemId }) { let textColor = strongSelf.theme.chat.inputPanel.primaryTextColor let itemText = stringWithAppliedEntities(trimToLineCount(todoItem.text, lineCount: 1), entities: todoItem.entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message) strongSelf.textNode.attributedText = itemText + } else if case let .pollOption(pollOptionId) = strongSelf.innerSubject, let poll = message?.media.first(where: { $0 is TelegramMediaPoll }) as? TelegramMediaPoll, let pollOption = poll.options.first(where: { $0.opaqueIdentifier == pollOptionId }) { + let textColor = strongSelf.theme.chat.inputPanel.primaryTextColor + let itemText = stringWithAppliedEntities(trimToLineCount(pollOption.text, lineCount: 1), entities: pollOption.entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message) + strongSelf.textNode.attributedText = itemText } strongSelf.titleNode.components = titleText diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index aec8d1f4e8..4884746cf2 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -32,12 +32,12 @@ public struct ChatInterfaceHighlightedState: Equatable { public let messageStableId: UInt32 public let quote: Quote? - public let todoTaskId: Int32? + public let subject: EngineMessageReplyInnerSubject? - public init(messageStableId: UInt32, quote: Quote?, todoTaskId: Int32?) { + public init(messageStableId: UInt32, quote: Quote?, subject: EngineMessageReplyInnerSubject?) { self.messageStableId = messageStableId self.quote = quote - self.todoTaskId = todoTaskId + self.subject = subject } } @@ -96,18 +96,18 @@ public struct NavigateToMessageParams { self.offset = offset } } - + public var timestamp: Double? public var quote: Quote? - public var todoTaskId: Int32? + public var subject: EngineMessageReplyInnerSubject? public var progress: Promise? public var forceNew: Bool public var setupReply: Bool - public init(timestamp: Double?, quote: Quote?, todoTaskId: Int32? = nil, progress: Promise? = nil, forceNew: Bool = false, setupReply: Bool = false) { + public init(timestamp: Double?, quote: Quote?, subject: EngineMessageReplyInnerSubject? = nil, progress: Promise? = nil, forceNew: Bool = false, setupReply: Bool = false) { self.timestamp = timestamp self.quote = quote - self.todoTaskId = todoTaskId + self.subject = subject self.progress = progress self.forceNew = forceNew self.setupReply = setupReply @@ -217,6 +217,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public let openConferenceCall: (Message) -> Void public let longTap: (ChatControllerInteractionLongTapAction, LongTapParams?) -> Void public let todoItemLongTap: (Int32, LongTapParams?) -> Void + public let pollOptionLongTap: (Data, LongTapParams?) -> Void public let openCheckoutOrReceipt: (MessageId, OpenMessageParams?) -> Void public let openSearch: () -> Void public let setupReply: (MessageId) -> Void @@ -388,6 +389,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol openConferenceCall: @escaping (Message) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, LongTapParams?) -> Void, todoItemLongTap: @escaping (Int32, LongTapParams?) -> Void, + pollOptionLongTap: @escaping (Data, LongTapParams?) -> Void, openCheckoutOrReceipt: @escaping (MessageId, OpenMessageParams?) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, @@ -512,6 +514,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol self.openConferenceCall = openConferenceCall self.longTap = longTap self.todoItemLongTap = todoItemLongTap + self.pollOptionLongTap = pollOptionLongTap self.openCheckoutOrReceipt = openCheckoutOrReceipt self.openSearch = openSearch self.setupReply = setupReply diff --git a/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift b/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift index e63818f11d..50ff7c9a77 100644 --- a/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift +++ b/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift @@ -875,7 +875,9 @@ final class ComposeTodoScreenComponent: Component { }, assumeIsEditing: self.inputMediaNodeTargetTag === todoItem.textFieldTag, characterLimit: component.initialData.maxTodoItemLength, + hasLeftInset: true, canReorder: isEnabled, + canAdd: isEnabled && i != 0 && i < component.initialData.maxTodoItemsCount, emptyLineHandling: .notAllowed, returnKeyAction: { [weak self] in guard let self else { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 154e208da5..5500e01039 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -1167,6 +1167,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro break } }, todoItemLongTap: { _, _ in + }, pollOptionLongTap: { _, _ in }, openCheckoutOrReceipt: { _, _ in }, openSearch: { }, setupReply: { _ in diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenMessage.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenMessage.swift index 5bf0c949d3..2bb6b7f03e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenMessage.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenMessage.swift @@ -123,7 +123,7 @@ extension PeerInfoScreenNode { strongSelf.enqueueMediaMessageDisposable.set((legacyAssetPickerEnqueueMessages(context: strongSelf.context, account: strongSelf.context.account, signals: signals!) |> deliverOnMainQueue).startStrict(next: { [weak self] messages in if let strongSelf = self { - let _ = enqueueMessages(account: strongSelf.context.account, peerId: strongSelf.peerId, messages: messages.map { $0.message.withUpdatedReplyToMessageId(.init(messageId: message.id, quote: nil, todoItemId: nil)) }).startStandalone() + let _ = enqueueMessages(account: strongSelf.context.account, peerId: strongSelf.peerId, messages: messages.map { $0.message.withUpdatedReplyToMessageId(.init(messageId: message.id, quote: nil, innerSubject: nil)) }).startStandalone() } })) } diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift index 182e867dab..1758ade1a1 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift @@ -240,7 +240,7 @@ final class PeerNameColorChatPreviewItemNode: ListViewItemNode { media.append(TelegramMediaWebpage(webpageId: MediaId(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: "", displayUrl: "", hash: 0, type: nil, websiteName: site, title: title, text: text, embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, isMediaLargeByDefault: nil, imageIsVideoCover: false, image: nil, file: nil, story: nil, attributes: [], instantPage: nil)))) } - let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: messageItem.outgoing ? [] : [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[authorPeerId], text: messageItem.text, attributes: messageItem.reply != nil ? [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false, todoItemId: nil)] : [], media: media, peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: messageItem.outgoing ? [] : [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[authorPeerId], text: messageItem.text, attributes: messageItem.reply != nil ? [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false, innerSubject: nil)] : [], media: media, peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false, rank: nil, rankRole: nil)) } diff --git a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift index 55a90c51e0..5f5eac2573 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift @@ -1070,7 +1070,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate messages[message4.id] = message4 sampleMessages.append(message4) - let message5 = Message(stableId: 5, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 5), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66004, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [ReplyMessageAttribute(messageId: message4.id, threadMessageId: nil, quote: nil, isQuote: false, todoItemId: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + let message5 = Message(stableId: 5, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 5), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66004, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [ReplyMessageAttribute(messageId: message4.id, threadMessageId: nil, quote: nil, isQuote: false, innerSubject: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) messages[message5.id] = message5 sampleMessages.append(message5) @@ -1081,7 +1081,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate let message6 = Message(stableId: 6, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 6), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66005, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) sampleMessages.append(message6) - let message7 = Message(stableId: 7, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 7), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66006, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: message5.id, threadMessageId: nil, quote: nil, isQuote: false, todoItemId: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + let message7 = Message(stableId: 7, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 7), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66006, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: message5.id, threadMessageId: nil, quote: nil, isQuote: false, innerSubject: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) sampleMessages.append(message7) let message8 = Message(stableId: 8, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 8), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66007, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift index aa8f57373b..7bd2d9037d 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift @@ -1524,7 +1524,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { let replyMessageId = MessageId(peerId: peerId, namespace: 0, id: 3) messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: messageAuthor, text: self.presentationData.strings.WallpaperPreview_ChannelReplyText, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - messageAttributes = [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false, todoItemId: nil)] + messageAttributes = [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false, innerSubject: nil)] } peers[peerId] = messageAuthor diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index ec54cec618..278f932639 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -2563,7 +2563,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } let file = TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData, thumbnail: false), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(item.fileSize), attributes: attributes, alternativeRepresentations: []) - let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: replyMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, todoItemId: nil) }, replyToStoryId: replyToStoryId, localGroupingKey: groupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: []) + let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: replyMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, innerSubject: nil) }, replyToStoryId: replyToStoryId, localGroupingKey: groupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: []) messages.append(message) } if let _ = groupingKey, messages.count % 10 == 0 { @@ -2668,7 +2668,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { threadId: nil, botId: results.botId, result: result, - replyToMessageId: replyMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, todoItemId: nil) }, + replyToMessageId: replyMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, innerSubject: nil) }, replyToStoryId: storyId, hideVia: hideVia, silentPosting: silentPosting, @@ -3056,7 +3056,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { mappedMessages.append(message) } - strongSelf.sendMessages(view: view, peer: peer, messages: mappedMessages.map { $0.withUpdatedReplyToMessageId(replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, todoItemId: nil) }).withUpdatedReplyToStoryId(replyToStoryId) }, silentPosting: silentPosting, scheduleTime: scheduleTime) + strongSelf.sendMessages(view: view, peer: peer, messages: mappedMessages.map { $0.withUpdatedReplyToMessageId(replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, innerSubject: nil) }).withUpdatedReplyToStoryId(replyToStoryId) }, silentPosting: silentPosting, scheduleTime: scheduleTime) completion() } diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 042bcda543..3746158718 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -2885,7 +2885,7 @@ private func extractAccountManagerState(records: AccountRecordsView map { messageIds -> MessageId? in if messageIds.isEmpty { return nil diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index c53d8b004d..6ff78cc0b7 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -1580,7 +1580,7 @@ extension ChatControllerImpl { strongSelf.window?.presentInGlobalOverlay(controller) } - let interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { [weak self] messageId, todoItemId, completion in + let interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { [weak self] messageId, innerSubject, completion in guard let strongSelf = self, strongSelf.isNodeLoaded else { return } @@ -1607,7 +1607,7 @@ extension ChatControllerImpl { $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject( messageId: message.id, quote: nil, - todoItemId: todoItemId + innerSubject: innerSubject )) }).updatedReplyMessage(message).updatedSearch(nil).updatedShowCommands(false) }, completion: { t in completion(t, {}) @@ -1631,7 +1631,7 @@ extension ChatControllerImpl { let replySubject = ChatInterfaceState.ReplyMessageSubject( messageId: messageId, quote: nil, - todoItemId: todoItemId + innerSubject: innerSubject ) completion(.immediate, { @@ -5160,7 +5160,7 @@ extension ChatControllerImpl { } } - let highlightedState = ChatInterfaceHighlightedState(messageStableId: message.stableId, quote: toSubject.quote.flatMap { quote in ChatInterfaceHighlightedState.Quote(string: quote.string, offset: quote.offset) }, todoTaskId: toSubject.todoTaskId) + let highlightedState = ChatInterfaceHighlightedState(messageStableId: message.stableId, quote: toSubject.quote.flatMap { quote in ChatInterfaceHighlightedState.Quote(string: quote.string, offset: quote.offset) }, subject: toSubject.subject) controllerInteraction.highlightedState = highlightedState strongSelf.updateItemNodesHighlightedStates(animated: initial) strongSelf.contentData?.scrolledToMessageIdValue = ScrolledToMessageId(id: mappedId, allowedReplacementDirection: []) @@ -5172,7 +5172,7 @@ extension ChatControllerImpl { } else { strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Chat_ToastQuoteNotFound, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return true }), in: .current) } - } else if let _ = toSubject.todoTaskId { + } else if let _ = toSubject.subject { extendHighlight = true } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift index 0fa4310255..815a39f0e5 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift @@ -340,15 +340,15 @@ extension ChatControllerImpl { } var quote: (string: String, offset: Int?)? - var todoTaskId: Int32? + var subject: EngineMessageReplyInnerSubject? var setupReply = false if case let .id(_, params) = messageLocation { quote = params.quote.flatMap { quote in (string: quote.string, offset: quote.offset) } setupReply = params.setupReply - todoTaskId = params.todoTaskId + subject = params.subject } - self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: message.index, animated: animated, quote: quote, todoTaskId: todoTaskId, scrollPosition: scrollPosition, setupReply: setupReply) + self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: message.index, animated: animated, quote: quote, subject: subject, scrollPosition: scrollPosition, setupReply: setupReply) if delayCompletion { Queue.mainQueue().after(0.25, { @@ -512,15 +512,15 @@ extension ChatControllerImpl { self.loadingMessage.set(.single(statusSubject) |> delay(0.1, queue: .mainQueue())) var quote: ChatControllerSubject.MessageHighlight.Quote? - var todoTaskId: Int32? + var subject: EngineMessageReplyInnerSubject? var setupReply = false if case let .id(_, params) = messageLocation { quote = params.quote.flatMap { quote in ChatControllerSubject.MessageHighlight.Quote(string: quote.string, offset: quote.offset) } - todoTaskId = params.todoTaskId + subject = params.subject setupReply = params.setupReply } - let historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation, quote: quote.flatMap { quote in MessageHistoryInitialSearchSubject.Quote(string: quote.string, offset: quote.offset) }, todoTaskId: todoTaskId), count: 50, highlight: true, setupReply: setupReply), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tag: nil, additionalData: []) + let historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation, quote: quote.flatMap { quote in MessageHistoryInitialSearchSubject.Quote(string: quote.string, offset: quote.offset) }, subject: subject), count: 50, highlight: true, setupReply: setupReply), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tag: nil, additionalData: []) var signal: Signal signal = historyView |> mapToSignal { historyView -> Signal in diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPollContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPollContextMenu.swift new file mode 100644 index 0000000000..4a45cfc53c --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPollContextMenu.swift @@ -0,0 +1,186 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Postbox +import TelegramCore +import AsyncDisplayKit +import Display +import ContextUI +import UndoUI +import AccountContext +import ChatMessageItemView +import ChatMessageItemCommon +import AvatarNode +import ChatControllerInteraction +import Pasteboard +import TelegramStringFormatting +import TelegramPresentationData + +private enum OptionsId: Hashable { + case item + case message +} + +extension ChatControllerImpl { + func openPollOptionContextMenu(optionId: Data, params: ChatControllerInteraction.LongTapParams) -> Void { + guard let message = params.message, let poll = message.media.first(where: { $0 is TelegramMediaPoll }) as? TelegramMediaPoll, let pollOptionIndex = poll.options.firstIndex(where: { $0.opaqueIdentifier == optionId }), let contentNode = params.contentNode else { + return + } + + let pollOption = poll.options[pollOptionIndex] + + let _ = (contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: self.presentationInterfaceState, context: self.context, messages: [message], controllerInteraction: self.controllerInteraction, selectAll: false, interfaceInteraction: self.interfaceInteraction, messageNode: params.messageNode as? ChatMessageItemView) + |> deliverOnMainQueue).start(next: { [weak self] actions in + guard let self else { + return + } + + var items: [ContextMenuItem] = [] +// if canMark { +// items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Todo_ContextMenu_CheckTask, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in +// guard let self else { +// return +// } +// +// if !self.context.isPremium { +// f(.default) +// let controller = UndoOverlayController( +// presentationData: self.presentationData, +// content: .premiumPaywall(title: nil, text: self.presentationData.strings.Chat_Todo_PremiumRequired, 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 { +// c?.dismiss(completion: { +// let _ = self.context.engine.messages.requestUpdateTodoMessageItems(messageId: message.id, completedIds: [todoItemId], incompletedIds: []).start() +// }) +// } +// }))) +// } + + //TODO:localize + if canReplyInChat(self.presentationInterfaceState, accountPeerId: self.context.account.peerId) { + items.append(.action(ContextMenuActionItem(text: "Reply to Option", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reply"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] c, _ in + guard let self else { + return + } + self.interfaceInteraction?.setupReplyMessage(message.id, .pollOption(pollOption.opaqueIdentifier), { transition, completed in + c?.dismiss(result: .custom(transition), completion: { + completed() + }) + }) + }))) + } + + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuCopy, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + + guard let self else { + return + } + storeMessageTextInPasteboard(pollOption.text, entities: pollOption.entities) + + self.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + }))) + + var isReplyThreadHead = false + if case let .replyThread(replyThreadMessage) = self.presentationInterfaceState.chatLocation { + isReplyThreadHead = message.id == replyThreadMessage.effectiveTopId + } + + if message.id.namespace == Namespaces.Message.Cloud, let channel = message.peers[message.id.peerId] as? TelegramChannel, !channel.isMonoForum, !isReplyThreadHead { + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuCopyLink, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + guard let self else { + return + } + var threadMessageId: MessageId? + if case let .replyThread(replyThreadMessage) = self.presentationInterfaceState.chatLocation { + threadMessageId = replyThreadMessage.effectiveMessageId + } + let _ = (self.context.engine.messages.exportMessageLink(peerId: message.id.peerId, messageId: message.id, isThread: threadMessageId != nil) + |> map { result -> String? in + return result + } + |> deliverOnMainQueue).startStandalone(next: { [weak self] link in + guard let self, let link else { + return + } + UIPasteboard.general.string = link + "?option=\(pollOptionIndex)" + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + + var warnAboutPrivate = false + if case .peer = self.presentationInterfaceState.chatLocation { + if channel.addressName == nil { + warnAboutPrivate = true + } + } + Queue.mainQueue().after(0.2, { + if warnAboutPrivate { + self.controllerInteraction?.displayUndo(.linkCopied(title: nil, text: presentationData.strings.Conversation_PrivateMessageLinkCopiedLong)) + } else { + self.controllerInteraction?.displayUndo(.linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied)) + } + }) + }) + f(.default) + }))) + } + + self.canReadHistory.set(false) + + //TODO:localize + var sources: [ContextController.Source] = [] + sources.append( + ContextController.Source( + id: AnyHashable(OptionsId.item), + title: "Option", + footer: "You're viewing actions for one option.\nYou can switch to actions for the list.", + source: .extracted(ChatTodoItemContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)), + items: .single(ContextController.Items(content: .list(items))) + ) + ) + + let messageContentSource = ChatMessageContextExtractedContentSource(chatController: self, chatNode: self.chatDisplayNode, engine: self.context.engine, message: message, selectAll: false, snapshot: true) + + sources.append( + ContextController.Source( + id: AnyHashable(OptionsId.message), + title: self.presentationData.strings.Chat_Todo_ContextMenu_SectionList, + source: .extracted(messageContentSource), + items: .single(actions) + ) + ) + + contentNode.onDismiss = { [weak messageContentSource] in + messageContentSource?.snapshotView?.removeFromSuperview() + } + + let contextController = makeContextController( + presentationData: self.presentationData, + configuration: ContextController.Configuration( + sources: sources, + initialId: AnyHashable(OptionsId.item) + ) + ) + contextController.dismissed = { [weak self] in + self?.canReadHistory.set(true) + } + + self.window?.presentInGlobalOverlay(contextController) + }) + } +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenTodoContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenTodoContextMenu.swift index f6c8eb8f77..c48cef418d 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenTodoContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenTodoContextMenu.swift @@ -29,9 +29,6 @@ extension ChatControllerImpl { let completion = todo.completions.first(where: { $0.id == todoItemId }) -// let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil// anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer -// let gesture: ContextGesture? = nil // anyRecognizer as? ContextGesture - var canMark = false if (todo.flags.contains(.othersCanComplete) || message.author?.id == context.account.peerId) { canMark = true @@ -135,7 +132,7 @@ extension ChatControllerImpl { guard let self else { return } - self.interfaceInteraction?.setupReplyMessage(message.id, todoItem.id, { transition, completed in + self.interfaceInteraction?.setupReplyMessage(message.id, .todoItem(todoItem.id), { transition, completed in c?.dismiss(result: .custom(transition), completion: { completed() }) diff --git a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift index d28ce57345..d09670712e 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift @@ -316,7 +316,7 @@ private func generateChatReplyOptionItems(selfController: ChatControllerImpl, ch quote = EngineMessageReplyQuote(text: trimmedText.string, offset: textSelection.offset, entities: trimmedText.entities, media: nil) } - selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject(messageId: replySubject.messageId, quote: quote, todoItemId: nil)).withoutSelectionState() }) }) + selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject(messageId: replySubject.messageId, quote: quote, innerSubject: nil)).withoutSelectionState() }) }) } let items = combineLatest(queue: .mainQueue(), @@ -381,7 +381,7 @@ private func generateChatReplyOptionItems(selfController: ChatControllerImpl, ch quote = EngineMessageReplyQuote(text: trimmedText.string, offset: textSelection.offset, entities: trimmedText.entities, media: nil) } - selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject(messageId: replySubject.messageId, quote: quote, todoItemId: nil)).withoutSelectionState() }) }) + selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject(messageId: replySubject.messageId, quote: quote, innerSubject: nil)).withoutSelectionState() }) }) f(.default) }))) diff --git a/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift b/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift index a4d8ecb4c7..acf7b004a7 100644 --- a/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift +++ b/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift @@ -49,7 +49,7 @@ func updateChatPresentationInterfaceStateImpl( $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject( messageId: keyboardButtonsMessage.id, quote: nil, - todoItemId: nil + innerSubject: nil )).withUpdatedMessageActionsState({ value in var value = value value.processedSetupReplyMessageId = keyboardButtonsMessage.id @@ -73,7 +73,7 @@ func updateChatPresentationInterfaceStateImpl( temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject( messageId: keyboardButtonsMessage.id, quote: nil, - todoItemId: nil + innerSubject: nil )).withUpdatedMessageActionsState({ value in var value = value value.processedSetupReplyMessageId = keyboardButtonsMessage.id diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 2c4ea64530..3ac958ec82 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -991,19 +991,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .pinnedMessageUpdated, .gameScore, .setSameChatWallpaper, .giveawayResults, .customText, .todoCompletions, .todoAppendTasks, .suggestedPostRefund, .suggestedPostSuccess, .suggestedPostApprovalStatus: for attribute in message.attributes { if let attribute = attribute as? ReplyMessageAttribute { - var todoTaskId: Int32? + var subject: EngineMessageReplyInnerSubject? if case let .todoCompletions(completed, incompleted) = action.action { if let completedTaskId = completed.first { - todoTaskId = completedTaskId + subject = .todoItem(completedTaskId) } else if let incompletedTaskId = incompleted.first { - todoTaskId = incompletedTaskId + subject = .todoItem(incompletedTaskId) } } else if case let .todoAppendTasks(tasks) = action.action { if let task = tasks.first { - todoTaskId = task.id + subject = .todoItem(task.id) } } - self.navigateToMessage(from: message.id, to: .id(attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote.flatMap { quote in NavigateToMessageParams.Quote(string: quote.text, offset: quote.offset) } : nil, todoTaskId: todoTaskId))) + self.navigateToMessage(from: message.id, to: .id(attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote.flatMap { quote in NavigateToMessageParams.Quote(string: quote.text, offset: quote.offset) } : nil, subject: subject))) break } } @@ -1589,7 +1589,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if isCaptionAbove { parameters = ChatSendMessageActionSheetController.SendParameters(effect: nil, textIsAboveMedia: true) } - self.enqueueMediaMessages(signals: signals, silentPosting: false, replyToSubject: .init(messageId: message.id, quote: nil, todoItemId: nil), parameters: parameters) + self.enqueueMediaMessages(signals: signals, silentPosting: false, replyToSubject: .init(messageId: message.id, quote: nil, innerSubject: nil), parameters: parameters) } }, present: { [weak self] c, a in self?.present(c, in: .window(.root), with: a) @@ -3163,7 +3163,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } var replyToMessageId: EngineMessageReplySubject? if postAsReply, let messageId { - replyToMessageId = EngineMessageReplySubject(messageId: messageId, quote: nil, todoItemId: nil) + replyToMessageId = EngineMessageReplySubject(messageId: messageId, quote: nil, innerSubject: nil) } strongSelf.sendMessages([.message(text: command, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyToMessageId, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) } @@ -3283,6 +3283,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } self.openTodoItemContextMenu(todoItemId: todoItemId, params: params) + }, pollOptionLongTap: { [weak self] optionId, params in + guard let self, let params else { + return + } + self.openPollOptionContextMenu(optionId: optionId, params: params) }, openCheckoutOrReceipt: { [weak self] messageId, params in guard let strongSelf = self else { return @@ -4018,7 +4023,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let replySubject = ChatInterfaceState.ReplyMessageSubject( messageId: message.id, quote: quoteData, - todoItemId: nil + innerSubject: nil ) if canSendMessagesToChat(strongSelf.presentationInterfaceState) { let _ = strongSelf.presentVoiceMessageDiscardAlert(action: { @@ -8323,13 +8328,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } case let .replyThread(replyThreadMessage): if let effectiveMessageId = replyThreadMessage.effectiveMessageId { - defaultReplyMessageSubject = EngineMessageReplySubject(messageId: effectiveMessageId, quote: nil, todoItemId: nil) + defaultReplyMessageSubject = EngineMessageReplySubject(messageId: effectiveMessageId, quote: nil, innerSubject: nil) } case .customChatContents: break } if let postSuggestionState = self.presentationInterfaceState.interfaceState.postSuggestionState, let editingOriginalMessageId = postSuggestionState.editingOriginalMessageId { - defaultReplyMessageSubject = EngineMessageReplySubject(messageId: editingOriginalMessageId, quote: nil, todoItemId: nil) + defaultReplyMessageSubject = EngineMessageReplySubject(messageId: editingOriginalMessageId, quote: nil, innerSubject: nil) } return messages.map { message in @@ -9127,7 +9132,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if case .peer(peerId.id) = strongSelf.chatLocation { if let subject = subject, case let .message(messageSubject, highlight, timecode, _) = subject { if case let .id(messageId) = messageSubject { - strongSelf.navigateToMessage(from: sourceMessageId, to: .id(messageId, NavigateToMessageParams(timestamp: timecode, quote: nil, todoTaskId: highlight?.todoTaskId))) + strongSelf.navigateToMessage(from: sourceMessageId, to: .id(messageId, NavigateToMessageParams(timestamp: timecode, quote: nil, subject: highlight?.subject))) } } else { self?.playShakeAnimation() diff --git a/submodules/TelegramUI/Sources/ChatControllerKeyShortcuts.swift b/submodules/TelegramUI/Sources/ChatControllerKeyShortcuts.swift index 9a93f12694..c52cd0b417 100644 --- a/submodules/TelegramUI/Sources/ChatControllerKeyShortcuts.swift +++ b/submodules/TelegramUI/Sources/ChatControllerKeyShortcuts.swift @@ -174,7 +174,7 @@ extension ChatControllerImpl { return state.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject( messageId: message.id, quote: nil, - todoItemId: nil + innerSubject: nil )) }) if updatedState.inputMode == .none { @@ -195,7 +195,7 @@ extension ChatControllerImpl { return ChatInterfaceState.ReplyMessageSubject( messageId: id, quote: nil, - todoItemId: nil + innerSubject: nil ) }) }) @@ -229,7 +229,7 @@ extension ChatControllerImpl { if replyMessageSubject.messageId == lastMessage?.id { updatedReplyMessageSubject = nil } else if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(after: replyMessageSubject.messageId) { - updatedReplyMessageSubject = ChatInterfaceState.ReplyMessageSubject(messageId: message.id, quote: nil, todoItemId: nil) + updatedReplyMessageSubject = ChatInterfaceState.ReplyMessageSubject(messageId: message.id, quote: nil, innerSubject: nil) } strongSelf.updateChatPresentationInterfaceState(interactive: true, { state in diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 9462b2d182..c751156f35 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -656,7 +656,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { mappedQuote = EngineMessageReplyQuote(text: quote, offset: nil, entities: [], media: nil) } - attributes.append(ReplyMessageAttribute(messageId: replyMessage.id, threadMessageId: nil, quote: mappedQuote, isQuote: mappedQuote != nil, todoItemId: nil)) + attributes.append(ReplyMessageAttribute(messageId: replyMessage.id, threadMessageId: nil, quote: mappedQuote, isQuote: mappedQuote != nil, innerSubject: nil)) } let message = Message( diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 31a339b443..dc10773c4b 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -1014,7 +1014,7 @@ public final class ChatHistoryListNodeImpl: ListViewImpl, ChatHistoryNode, ChatH initialSearchLocation = .index(MessageIndex.absoluteUpperBound()) } } - self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: initialSearchLocation, quote: (highlight?.quote).flatMap { quote in MessageHistoryInitialSearchSubject.Quote(string: quote.string, offset: quote.offset) }, todoTaskId: highlight?.todoTaskId), count: historyMessageCount, highlight: highlight != nil, setupReply: setupReply), id: 0) + self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: initialSearchLocation, quote: (highlight?.quote).flatMap { quote in MessageHistoryInitialSearchSubject.Quote(string: quote.string, offset: quote.offset) }, subject: highlight?.subject), count: historyMessageCount, highlight: highlight != nil, setupReply: setupReply), id: 0) } else if let subject = subject, case let .pinnedMessages(maybeMessageId) = subject, let messageId = maybeMessageId { self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(messageId)), count: historyMessageCount, highlight: true, setupReply: false), id: 0) } else { @@ -1923,7 +1923,7 @@ public final class ChatHistoryListNodeImpl: ListViewImpl, ChatHistoryNode, ChatH initialSearchLocation = .index(.absoluteUpperBound()) } } - strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: initialSearchLocation, quote: (highlight?.quote).flatMap { quote in MessageHistoryInitialSearchSubject.Quote(string: quote.string, offset: quote.offset) }, todoTaskId: highlight?.todoTaskId), count: historyMessageCount, highlight: highlight != nil, setupReply: setupReply), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) + strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: initialSearchLocation, quote: (highlight?.quote).flatMap { quote in MessageHistoryInitialSearchSubject.Quote(string: quote.string, offset: quote.offset) }, subject: highlight?.subject), count: historyMessageCount, highlight: highlight != nil, setupReply: setupReply), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) } else if let subject = subject, case let .pinnedMessages(maybeMessageId) = subject, let messageId = maybeMessageId { strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(messageId)), count: historyMessageCount, highlight: true, setupReply: false), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) } else if var chatHistoryLocation = strongSelf.chatHistoryLocationValue { @@ -3489,8 +3489,8 @@ public final class ChatHistoryListNodeImpl: ListViewImpl, ChatHistoryNode, ChatH } } - public func scrollToMessage(from fromIndex: MessageIndex, to toIndex: MessageIndex, animated: Bool, highlight: Bool = true, quote: (string: String, offset: Int?)? = nil, todoTaskId: Int32? = nil, scrollPosition: ListViewScrollPosition = .center(.bottom), setupReply: Bool = false) { - self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .message(toIndex), quote: quote.flatMap { quote in MessageHistoryScrollToSubject.Quote(string: quote.string, offset: quote.offset) }, todoTaskId: todoTaskId, setupReply: setupReply), anchorIndex: .message(toIndex), sourceIndex: .message(fromIndex), scrollPosition: scrollPosition, animated: animated, highlight: highlight, setupReply: setupReply), id: self.takeNextHistoryLocationId()) + public func scrollToMessage(from fromIndex: MessageIndex, to toIndex: MessageIndex, animated: Bool, highlight: Bool = true, quote: (string: String, offset: Int?)? = nil, subject: EngineMessageReplyInnerSubject? = nil, scrollPosition: ListViewScrollPosition = .center(.bottom), setupReply: Bool = false) { + self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .message(toIndex), quote: quote.flatMap { quote in MessageHistoryScrollToSubject.Quote(string: quote.string, offset: quote.offset) }, subject: subject, setupReply: setupReply), anchorIndex: .message(toIndex), sourceIndex: .message(fromIndex), scrollPosition: scrollPosition, animated: animated, highlight: highlight, setupReply: setupReply), id: self.takeNextHistoryLocationId()) } public func anchorMessageInCurrentHistoryView() -> Message? { diff --git a/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift b/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift index b990c02468..959b23e6bc 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift @@ -284,7 +284,7 @@ func chatHistoryViewForLocation( preloaded = true - return .HistoryView(view: view, type: reportUpdateType, scrollPosition: .index(subject: MessageHistoryScrollToSubject(index: anchorIndex, quote: searchLocationSubject.quote.flatMap { quote in MessageHistoryScrollToSubject.Quote(string: quote.string, offset: quote.offset) }, todoTaskId: searchLocationSubject.todoTaskId, setupReply: setupReply), position: .center(.bottom), directionHint: .Down, animated: false, highlight: highlight, displayLink: false, setupReply: setupReply), flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData), id: location.id) + return .HistoryView(view: view, type: reportUpdateType, scrollPosition: .index(subject: MessageHistoryScrollToSubject(index: anchorIndex, quote: searchLocationSubject.quote.flatMap { quote in MessageHistoryScrollToSubject.Quote(string: quote.string, offset: quote.offset) }, subject: searchLocationSubject.subject, setupReply: setupReply), position: .center(.bottom), directionHint: .Down, animated: false, highlight: highlight, displayLink: false, setupReply: setupReply), flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData), id: location.id) } } case let .Navigation(index, anchorIndex, count, _): diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift index 1278c6bb61..a40d494a9c 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift @@ -218,7 +218,7 @@ func textInputAccessoryPanel( contents: .reply(ChatInputMessageAccessoryPanel.Contents.Reply( id: replyMessageSubject.messageId, quote: replyMessageSubject.quote, - todoItemId: replyMessageSubject.todoItemId, + innerSubject: replyMessageSubject.innerSubject, message: nil )), chatPeerId: chatPeerId, @@ -322,7 +322,7 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS } if let chatPeerId { - let panelNode = ReplyAccessoryPanelNode(context: context, chatPeerId: chatPeerId, messageId: replyMessageSubject.messageId, quote: replyMessageSubject.quote, todoItemId: replyMessageSubject.todoItemId, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder, dateTimeFormat: chatPresentationInterfaceState.dateTimeFormat, animationCache: chatControllerInteraction?.presentationContext.animationCache, animationRenderer: chatControllerInteraction?.presentationContext.animationRenderer) + let panelNode = ReplyAccessoryPanelNode(context: context, chatPeerId: chatPeerId, messageId: replyMessageSubject.messageId, quote: replyMessageSubject.quote, innerSubject: replyMessageSubject.innerSubject, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder, dateTimeFormat: chatPresentationInterfaceState.dateTimeFormat, animationCache: chatControllerInteraction?.presentationContext.animationCache, animationRenderer: chatControllerInteraction?.presentationContext.animationRenderer) panelNode.interfaceInteraction = interfaceInteraction return panelNode } else { diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 879170fee8..4ed84c1056 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -151,6 +151,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu }, openConferenceCall: { _ in }, longTap: { _, _ in }, todoItemLongTap: { _, _ in + }, pollOptionLongTap: { _, _ in }, openCheckoutOrReceipt: { _, _ in }, openSearch: { }, setupReply: { _ in diff --git a/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift b/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift index 67e24534ad..cfdec9f045 100644 --- a/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift +++ b/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift @@ -230,10 +230,10 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie } return 0.0 })) - } else if let todoTaskId = scrollSubject.todoTaskId { + } else if let subject = scrollSubject.subject { position = .center(.custom({ itemNode in if let itemNode = itemNode as? ChatMessageBubbleItemNode { - if let taskRect = itemNode.getTodoTaskRect(id: todoTaskId) { + if let taskRect = itemNode.getInnerReplySubjectRect(innerSubject: subject) { return taskRect.midY } } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 73f6119a36..4383793660 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2416,6 +2416,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, todoItemLongTap: { _, _ in }, + pollOptionLongTap: { _, _ in + }, openCheckoutOrReceipt: { _, _ in }, openSearch: { diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index a10bd445db..e7c3f5012c 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -1143,7 +1143,7 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) return .result(.replyThreadMessage(replyThreadMessage: result, messageId: messageId)) }) } else { - return .single(.result(.peer(foundPeer._asPeer(), .chat(textInputState: nil, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil, todoTaskId: taskId), timecode: timecode, setupReply: false), peekData: nil)))) + return .single(.result(.peer(foundPeer._asPeer(), .chat(textInputState: nil, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil, subject: taskId.flatMap { .todoItem($0) }), timecode: timecode, setupReply: false), peekData: nil)))) } } else { return .single(.result(.inaccessiblePeer))